1.常规构造函数、析构函数
构造函数的作用是进行类的初始化,在没有显式自定义的情况下,C++会提供默认的(无参)构造函数和析构函数:
class A {};
int main() {
A *a = new A;
delete a;
}
如果自定义了构造函数,则不会自动生成无参构造函数:
class A {
public:
A(int a){};
};
int main() {
A *a = new A; //Error
}
析构函数就不存在这个问题,因为其定义不允许包含参数
构造函数不允许声明为virtual,原因是:
1)构造时必须知道具体类型,如果是virtual的,则不知道该构造什么类型
2)虚函数表是在构造后初始化的,构造函数执行前没有虚函数表自然不可能执行
即:虚函数是开启动态绑定的,要根据对象类型进行,构造时对象都还不存在,自然不可能进行动态绑定
而析构函数可以,有时必须是虚函数,原因是:
创建派生类对象时,往往使用
Base_class *object = new Derived_class;
这种形式,在析构(delete)时,如果析构函数不是虚函数,则只会调用基类的析构函数,假如派生类包含了一个基类没有的new指针对象,则这个对象不会被析构,从而导致内存泄漏
2.复制构造函数
C++允许使用一个实例变量初始化另一个实例变量,如:
A *a = new A;
A b(*a); //相当于A b = *a; 但是实现原理不一样
这种方式称为复制构造,赋值形式的称为复制赋值
C++默认也会生成复制构造函数和复制赋值函数
复制构造是将类的成员变量逐个复制给新对象,即两者数据相同
使用默认复制函数(浅复制or浅拷贝)有两个问题:
1)野指针(悬挂指针)问题:
假设类A有一个new初始化的指针成员,实例对象b复制实例对象a后,实际复制的是指针,而不是真实数据,也就是说假设a被析构了,那么这个指针成员也被析构,就会导致b的指针成员指向不存在的地址,从而导致错误,这样的指针称为野指针
class A { public: char* c = new char[10]; };
int main() {
A *a = new A;
A b(*a);
delete a;
doSomething(b.c); //Error
}
因此需要重写复制函数,确保将指针成员指向的数据也进行复制(深复制or深拷贝)
class A {
public:
char* c = new char[10];
A(const A& a) { c = new char[10]; strcpy(c, a.c); };
};
2)性能问题:
假设A有一个对象,存储了一个非常大的数据(总之就是很大)
如果要进行数据转移,则需要:
a.创建一个临时变量
b.进行数据复制
c.销毁临时变量(自动析构)
很浪费时间,还要划出来一大块临时空间,根本没有必要
实际上只需要让新对象的成员直接指向旧对象的成员就好,根本没必要复制,这种操作称为移动
3.移动构造函数
C++也提供了默认的移动构造函数和移动赋值函数,区分复制和移动的,是左值引用和右值引用
右值一般指的就是数据,数据本身作为常量,除非从内存中释放,否则是不会被修改的,而引用一般就意味着要进行修改,所以C++11之前右值是无法进行引用的,C++11之后,增加了右值引用的概念,使用"&&"表示
例如: int && a = 1;
则:&a表示的就是1这个数字存放的地址
移动构造函数写法则是:
class A {
public:
char* c = new char[10];
A(A && a) { c = a.c;a.c=nullptr; };
};
由于移动意味着修改,因此不能使用const限定符
相比于复制构造,变化主要有:
1)不需要担心野指针问题,所以不需要深复制
2)要显式将旧数据清空
使用场景:
假设重载了"+"运算,并且用两个实例之和初始化第三个实例:
int main(){
A a(10);
A b(20);
A c(a+b);
A d(std::move(c));
}
对于c来说,a+b代表了一个右值,那么就会使用移动构造函数进行初始化
d对象则使用了强制移动功能:
c在初始化以后,就成为了一个左值,本来用c初始化d的话,会调用复制构造函数,如果想强制使用移动构造函数,就可以用std::move函数,但是必须存在移动构造函数才有意义,否则还是会降级调用复制构造函数
如果自己实现了析构函数、复制构造/赋值函数,则不提供移动构造/赋值函数,反之则不提供复制函数
4.强制默认实现与禁止使用
上面说到,如果自己实现了带参构造函数,则不会默认生成无参构造函数,如果确实需要带参构造函数,但是又不想自己编写无参版本,则可以使用default关键字,强制编译器为该函数生成默认版本
class A{
public:
A() = default;
A(int a){};
}
default仅能够用于默认构造、默认析构、默认复制构造/赋值、默认移动构造/赋值六类函数
禁止使用某个方法的使用,有两种方式:
1)private化:
把这个函数放在private区域,例如:单例模式中把构造函数限定为private,使得只能通过getInstance方法获取单例对象
2)使用delete:
例如,不希望使用复制构造,则可以:
class A{
public:
A() = default;
A(int a){};
A(const A& a) = delete;
}
还可以用来防止类型自动提升。C++所有类型全部基于数字,例如double、int可以在一定范围内转换,使得接受double类型参数的函数也可以接受int,并且会自动进行类型提升,如果想禁止这种特性,就可以:
void func(double a) {……}
void func(int a) = delete;
5.委托构造函数(成员初始化列表)
在构造函数的函数体前面使用,例如:
class A{
int num;
string s;
public:
A() = default;
A(int a,string b) : num(a),s(b) {……};
A(int a,string b,bool c) : A(a,b) {……};
}
有两种用法:
1)成员初始化列表:调用成员变量的构造函数进行初始化
2)委托构造:调用自己的构造函数,类似于Java的 this() 函数
6.继承构造函数
使用using来继承基类构造方法。
例如:
class A{
public:
A();
A(int a);
A(int a,string b);
func(int a);
func(string b);
}
class B:public A{
using A::func; //继承了A类所有func函数
using A::A; //继承了A类所以构造函数
}
这样写,就可以调用A的构造函数初始化B,并用其调用A的成员方法。
类似于Java的super()函数