详细技术视频请看我的主页
什么是默认构造
默认构造就是不带参数的构造函数,如果我们不实现任何构造函数,系统会为我们生成一个默认的构造函数
比如下面
1 |
|
所以我们可以直接使用默认构造函数构造一个对象,并且打印成员_i
1 | //测试默认合成 |
输出
1 | member _i is -1284874240 |
可以看到默认构造函数并不会帮我们初始化类成员变量。
什么是有参构造
有参构造就是传递参数的构造函数,可以根据参数构造对象
1 |
|
我们可以通过如下方式构造
1 | JoiningThread jt(1); |
当我们执行程序,会输出
1 | member _i is 1 |
但如果这样构造会产生问题
1 | JoiningThread jt; |
注意
如果我们实现了参数构造而不实现无参构造,系统将不会为我们实现默认构造,导致无法使用默认构造生成对象。
所以稳妥一点,我们基本都会实现无参构造
1 |
|
拷贝构造函数是什么
回答要点:
定义:拷贝构造函数用于创建一个对象,该对象是通过复制另一个同类型对象来初始化的。
调用时机
:
- 使用现有对象初始化新对象。
- 按值传递对象作为函数参数。
- 按值返回对象。
默认拷贝构造函数:成员逐个拷贝。
示例:
1 | class MyClass { |
是否会默认生成拷贝构造
在 C++ 中,如果你没有为一个类显式定义拷贝构造函数,编译器会自动生成一个默认的拷贝构造函数。这个默认拷贝构造函数会按成员的逐个拷贝(member-wise copy)方式来复制对象的每个成员变量。
默认拷贝构造函数的行为
- 逐个拷贝:默认拷贝构造函数会逐个拷贝所有的非静态成员变量。
- 指针成员:如果类中有指针成员,默认拷贝构造函数只会拷贝指针的值(地址),而不会拷贝指针所指向的对象。这可能会导致多个对象指向同一块内存,进而引发问题(如双重释放、内存泄漏等)。
- **
const
和引用成员
**:如果类中有const
成员或引用成员,编译器不会生成默认的拷贝构造函数,因为这些成员不能被复制。 - 类中包含不可拷贝对象时,无法合成默认拷贝构造函数
拷贝构造是否必须实现
当一个类A中有成员变量是另一个类类型B的时候,有时候拷贝构造会失效。
比如一个类JoiningThread
中有成员变量std::thread
,std::thread
没有构造函数,所以A类的拷贝构造无法合成,需要显示编写。
比如我们这样调用
1 | JoiningThread jt(1); |
上面代码报错
1 | error: use of deleted function 'std::thread::thread(const std::thread&)' |
所以我们要显示实现拷贝构造,指定一个拷贝规则
1 | JoiningThread(const JoiningThread & other): _i(other._i){} |
什么是浅拷贝和深拷贝
类在拷贝构造或者拷贝赋值的时候,将被拷贝的类中的成员值拷贝到目的类,如果被拷贝的类中包含指针成员,只是简单的拷贝指针的值。
同样析构也要显示编写,等待线程完成。
除此之外我们可以自己实现拷贝构造,进而实现浅拷贝和深拷贝的不同效果
构造顺序和析构顺序
类A中包含成员变量是类B的类型,如果是先调用A的构造还是B的构造呢?
如果析构的时候是A先析构还是B先析构呢?
1 | class InnerB { |
执行结果,先调用B的构造,在调用C的构造。
析构时,先析构C再析构B
1 | InnerB() |
类默认构造是否必须实现
如果类中有继承关系或者其他类型的成员,默认构造函数是很有必要实现的。
系统提供的合成的默认构造函数不会对成员做初始化操作。
比如我们之后要学到的继承
1 | class DerivedA: public BaseA { |
调用
1 | DerivedA a("zack","1001"); |
this
指针的特性和用途
指向当前对象:
this
指针是一个隐式参数,指向调用成员函数的对象。通过this
,你可以访问当前对象的属性和方法。
区分成员变量和参数:
在构造函数或成员函数中,参数名和成员变量可能同名。使用
1
this
可以明确指代成员变量。例如:
1
2
3
4
5
6
7
8class MyClass {
private:
int value;
public:
MyClass(int value) {
this->value = value; // 使用 this 指针区分成员变量和参数
}
};
返回当前对象:
- ```
this1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
可以用于返回当前对象的引用,以支持链式调用。例如:
```cpp
class MyClass {
private:
int value;
public:
MyClass& setValue(int value) {
this->value = value;
return *this; // 返回当前对象的引用
}
};
MyClass obj;
obj.setValue(10).setValue(20); // 链式调用
- ```
在 const 成员函数中的使用:
- 在
const
成员函数中,this
的类型为const MyClass*
,这意味着你不能通过this
修改成员变量。这有助于确保对象的状态不被改变。
- 在
在静态成员函数中的不可用性:
- 静态成员函数没有
this
指针,因为它们不属于任何特定对象,而是属于类本身。因此,静态成员函数不能访问非静态成员变量和成员函数。
- 静态成员函数没有
示例代码
以下是一个简单的示例,展示了 this
指针的用法:
1 |
|
delete和default
C++11
用法:
delete可以删除指定的构造函数。
default可以指定某个构造函数为系统默认合成。
1 | class DefaultClass { |
主函数中调用
1 | DefaultClass b; |
输出num
是一个随机数
1 | DefaultClass num is 331 |
什么是移动构造函数?与拷贝构造函数有何不同?
回答要点:
定义:移动构造函数用于通过“移动”资源来初始化对象,而不是复制资源。
语法:使用右值引用作为参数 (
MyClass(MyClass&& other)
).优势
:
- 性能更高,避免不必要的深拷贝。
- 适用于临时对象。
区别
:
- 拷贝构造函数复制资源,移动构造函数转移资源所有权。
示例:
1 | class MyClass { |
默认构造函数和用户定义的构造函数有什么区别?
回答要点:
默认构造函数
:
- 无参数的构造函数。
- 如果没有用户定义的构造函数,编译器会自动生成一个默认构造函数。
用户定义的构造函数
:
- 开发者自定义的构造函数,可以有参数。
- 一旦定义了任何构造函数,编译器不会再生成默认构造函数,除非显式声明。
示例:
1 | class MyClass { |
什么是初始化列表?为什么在构造函数中使用它?
回答要点:
定义:初始化列表是在构造函数的参数列表之后,函数体之前,用于初始化成员变量的语法。
优点
:
- 提高性能,特别是对于常量成员或引用成员。
- 必须用于初始化常量成员、引用成员以及基类。
- 避免对象先默认构造再赋值,减少不必要的操作。
示例:
1 | class MyClass { |
什么是虚析构函数?为什么需要它?
回答要点:
定义:在基类中将析构函数声明为
virtual
,以确保通过基类指针删除派生类对象时,能正确调用派生类的析构函数。用途
:
- 防止内存泄漏。
- 确保派生类的资源被正确释放。
不使用虚析构函数的风险
:
- 仅调用基类析构函数,导致派生类资源未释放。
示例:
如果BaseA
的析构不写成虚析构,则主函数开辟子类对象赋值给基类指针,以后delete
基类指针的时候会发现没有析构子类
1 | class BaseA{ |
主函数回收内存
1 | BaseA* base = new DerivedA("zack","1002"); |
会看到只调用了基类BaseA
的析构函数。
当BaseA的析构改为虚析构的时候,才会回收子类DerivedA
1 | class BaseA{ |
什么是委托构造函数?它是如何工作的?(C++11引入的特性)
回答要点:
定义:一个构造函数可以调用同一类中的另一个构造函数,从而委托初始化任务。
优点
:
- 避免代码重复,提升代码可维护性。
语法:使用构造函数初始化列表中的类名和参数。
示例:
1 | class MyClass { |
什么是析构函数的顺序?
回答要点:
- 成员变量的析构顺序:按照声明的逆序析构。
- 继承关系的析构顺序:先析构派生类的成员和资源,再析构基类。
- 全局/静态对象:按照创建的逆序析构。
示例说明:
1 | class Base { |
输出:
1 | Derived析构 |
如何防止对象被复制?
回答要点:
- C++11及以上:使用
delete
关键字显式删除拷贝构造函数和拷贝赋值运算符。 - C++11之前:将拷贝构造函数和拷贝赋值运算符声明为私有且不实现。
示例(C++11及以上):
1 | class NonCopyable { |
构造函数中抛出异常会发生什么?
回答要点:
- 对象未完全构造:如果构造函数中抛出异常,析构函数不会被调用,因为对象尚未完全构造。
- 资源泄漏风险:如果在构造函数中分配了资源,需使用RAII(资源获取即初始化)类或智能指针来确保资源被正确释放。
- 异常安全:确保在构造函数抛出异常时,任何已经初始化的成员都会被正确析构。
示例:
1 | class MyClass { |
解释RAII
及其与构造函数、析构函数的关系
回答要点:
RAII
(资源获取即初始化):
- 编程范式,通过对象的生命周期管理资源。
- 资源在对象构造时获取,在对象析构时释放。
关系
:
- 构造函数负责获取资源。
- 析构函数负责释放资源。
优点
:
- 自动管理资源,防止内存泄漏。
- 异常安全,确保资源在异常发生时被释放。
示例:
1 | class FileHandler { |
解释什么是赋值运算符重载?与拷贝构造函数有何不同?
回答要点:
赋值运算符重载:通过重载
operator=
,定义对象之间的赋值行为。区别与拷贝构造函数
:
- 拷贝构造函数用于初始化新对象。
- 赋值运算符用于将一个已存在的对象赋值给另一个已存在的对象。
常规实现
:
- 检查自赋值。
- 释放已有资源。
- 复制资源。
- 返回
*this
。
示例:
1 | class MyClass { |
解释静态成员变量在构造和析构中的处理
回答要点:
静态成员变量
:
- 属于类本身,而非任何对象实例。
- 在程序开始时初始化,在程序结束时析构。
构造顺序
:
- 单例模式中,静态成员在第一次使用时构造。
析构顺序
:
- 按逆序构造顺序析构,确保依赖关系被正确处理。
示例:
1 | class MyClass { |
输出:
1 | 构造 |
虚函数原理
包含虚函数的类构成
参考我得另一篇文章,https://llfc.club/category?catid=225RaiVNI8pFDD5L4m807g7ZwmF#!aid/2CmcOeP6BZMbtNiglPTkbmnpb73
虚继承与菱形继承问题
1 |
|
输出:
1 | Brand: TechBrand |
解析:
- 在无虚继承的情况下,
Convertible
类将拥有两份Device
的成员变量,这会导致二义性问题。 - 通过使用虚继承(
virtual public
),确保Convertible
类只有一份Device
的成员。 - 在
Convertible
的构造函数中,需要明确调用基类Device
的构造函数,避免二义性。 - 在
main
函数中,创建一个Convertible
对象,并调用showBrand()
函数,正确显示品牌名称。
注意事项:
- 菱形继承(多重继承导致的重复基类)可以通过虚继承来解决,确保共享同一份基类成员。
- 虚继承会增加一定的开销,需根据具体需求权衡使用。
协变返回类型
概念定义
- 在 C++ 中,协变返回类型(Covariant Return Types)是一个与函数重写(Override)相关的概念。当一个派生类重写基类的虚函数时,协变返回类型允许派生类的函数返回一个从基类函数返回类型派生而来的类型。
1 |
|
输出:
1 | Base cloned. |
解析:
基类
Base
定义了一个虚函数clone()
,返回Base*
类型的指针。派生类
Derived
重写了clone()
函数,返回类型为Derived*
,这是一种协变返回类型。在
1
main
函数中,通过基类指针调用
1
clone()
函数:
- 对于
Base
对象,调用Base::clone()
。 - 对于
Derived
对象,由于虚函数机制,调用Derived::clone()
。
- 对于
协变返回类型允许派生类的重写函数返回更具体的类型,增强类型安全性和代码可读性。
注意事项:
- 协变返回类型必须满足派生类返回类型是基类返回类型的派生类。
- 编译器会检查协变返回类型的正确性,确保类型安全。