C++ 构造函数总结

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指针对象,则这个对象不会被析构,从而导致内存泄漏

扫描二维码关注公众号,回复: 5148198 查看本文章

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()函数

猜你喜欢

转载自blog.csdn.net/u010670411/article/details/85561928