目录
- 默认构造函数
- 普通构造函数
- 拷贝构造函数
- vector存含有指针的自定义类型主义问题
- 转换构造函数
- 移动构造函数
- 举例两个场景
下面以Complex 复数类来学习C++类中的各种构造函数;
#include <iostream>
using namespace std;//复数类
class Complex{friend ostream & operator<<(ostream &out, Complex &c); //友元函数
public:Complex(){} //默认构造函数Complex(double real, double imag): m_real(real), m_imag(imag){ } //普通构造函数(可以加缺省值)Complex(const Complex &c):m_real(c.m_real),m_imag(c.m_imag) { } //拷贝构造函数(参数是const Complex &类型)Complex(double real):m_real(real), m_imag(0.0) { } //转换构造函数 Complex(Complex &&c):m_real(c.m_real),m_imag(c.m_imag) { } //移动构造函数 private:double m_real; //实部double m_imag; //虚部
};//重载<<运算符
ostream & operator<<(ostream &out, Complex &c){out << c.m_real <<" + "<< c.m_imag <<"i";;return out;
}int main(){Complex a(10.0, 20.0); //调用普通构造函数Complex b(89.5); //调用转换构造函数Complex c; //调用默认构造函数return 0;}
默认构造函数
- 未显式提供初始值时,用来创建对象的构造函数;
Complex(){} //默认构造函数
如果该类采用了继承或者定义了虚函数或者成员由非内置类型,那么系统在我们没有定制写出该函数的时候,会自动生成默认构造函数;
普通构造函数
Complex(double real, double imag); //普通构造函数(可以带上缺省参数方便初始化!)
用户可以传参与对应普通构造函数进行匹配来构造对象;
拷贝构造函数
拷贝构造 也叫 复制构造
Complex(const Complex &c); //拷贝构造函数(参数是const Complex &类型)
调用场景:
Complex a(1.1,2.2);//用户调用普通构造函数初始化a对象Complex b(a); //此时b调用拷贝构造函数,用a对象进行拷贝
Complex b = a; //等价与上面的Complex b(a);//下面两条语句不会调拷贝构造,因为c1,c2已经通过默认构造初始化了,再进行=只是调用了operator=(const Complex& src)赋值语句
Complex c1, c2;
c1=c2;
注意,拷贝构造函数与默认构造函数类似,系统一定条件下会自动生成;
(但是系统默认生成的拷贝构造是浅拷贝,遇到了某些自定义类型是很危险的!eg:string 内部有堆地址char*的浅拷贝问题)
拷贝构造参数是const 类型名 &a 的原因是:
- &避免了传参时候,需要多拷贝一次的开销
- 不加&会产生“拷贝时需要传参,传参又是一次拷贝的套娃行为”
- 用a对象拷贝构造的时候,const防止在函数内部拷贝a时,不小心把a本体给改了;
vector存含有指针的自定义类型主义问题
vectror, 万一存的自定义对象里有含有堆上的指针咋办? 比如存个string,string底层就是个char* ;
这就得对存储的自定义对象设计深拷贝的拷贝构造函数!!!
否则系统自动生成的浅拷贝就会出现内存问题!string里已经帮我们搞了深拷贝的靠背可构造函数了不用担心;
转换构造函数
- C++ 允许我们自定义类型转换规则,用户可以将其它类型转换为当前类类型。这种自定义的类型转换规则只能以类的成员函数的形式出现,换句话说,这种转换规则只适用于类。
调用场景:
- 直接一个=,将等号右边的其它类型转换为当前类类型时
- or(类型)强转时调用转换构造函数(有点C语言隐式类型转换的意思)
Complex(double real):m_real(real), m_imag(0.0) { } //转换构造函数
调用场景1:
Complex a(1.1,2.2);//先用普通构造初始化a对象;
cout<<a<<endl; //输出1.1 + 2.2ia = 3.14; //double转Complex,调用转换构造,将double 3.14直接转换为了Complex类型: a(3.14,0.0) ; 有点像C的隐式类型转换 int a = 3.14这种操作
cout<<a<<endl; //输出3.14 + 0.0i
调用场景2(本质上等价于场景1):
Complex a = (Complex)25.5;;//C语言的强转;
如果Complex类没有写对应double类型的转换构造函数:Complex(double a),那编译器因为没有转换规则,上述两个场景就会报错,转换失败!
移动构造函数
Complex(Complex &&c) { } //移动构造函数格式(参数是一个右值 or 将亡值)
先了解一下移动语义: 右值引用和移动语义参考博客
所谓移动语义,指的就是以移动而非深拷贝的方式初始化含有指针成员的类对象。
简单的理解,移动语义指的就是将其他对象(通常是临时对象(将亡值))拥有的内存资源“移为已用”。
事实上,对于程序执行过程中产生的临时对象,往往只用于传递数据(没有其它的用处),并且会很快会被销毁。因此在使用临时对象初始化新对象时,我们可以将其包含的指针成员指向的内存资源直接转移给新对象所有,无需再新拷贝一份,这大大提高了初始化的执行效率。
移动构造函数的参数和拷贝构造函数不同,拷贝构造函数的参数是一个左值引用,但是移动构造函数的参数是一个右值引用。
当类中同时包含拷贝构造函数和移动构造函数时,如果使用临时对象初始化当前类的对象,编译器会优先调用移动构造函数来完成此操作。只有当类中没有合适的移动构造函数时,编译器才会退而求其次,调用拷贝构造函数。
这意味着,移动构造函数的参数是一个右值或者将亡值的引用;
也就是说,使用一个右值,或者将亡值初始化另一个对象的时候,才会调用移动构造函数。
举例两个场景
(eg1:某个成员函数,返回该类型自己的对象的场景) string 的string operator+();:
可以看到,有了移动构造以后,我们避免了拷贝临时变量,再释放临时变量的操作,我们直接把临时变量内的指针转移给了要赋值的对象,避免了多余的拷贝,提高了效率!
(eg2:move() 函数强制让一个左值变成右值,因此a=move(b),等号右边是右值,那么调用的就是移动构造函数:
执行过程:
- move让b变成了右值,走了移动构造;,把b中的各种资源转移给了a;
- b完成转移以后,其中的资源置空nullptr,代表他的生命周期结束了,置空的原因是防止对同一个地址2次析构等;
很明显,移动构造的优势:
- 减少了额外深拷贝的时间上的开销;
- 不需要给构造的对象分配内存,因为我们把参数(右值)的资源转移过去就行;