写在前面
本篇文章,是对于C++ 构造函数的一个整理,因为析构和构造分不开,所有就放在了一块,文章篇幅有长有短,主要是用来记录一些知识点,和简单用法。如有问题欢迎随时指出
目录
什么是构造函数?
每个类都分别定义了它的对象初始化的方式,类通过一个或几个特殊的函数来控制它对象的初始化过程。我们将其称为构造函数
构造函数初始化对象非static成员
构造函数,成员的初始化实在函数体执行之前完成的,且按照它们在类中出现的顺序完成初始化
构造函数没有返回类型
构造函数不能被申明为 const 。 注:当我们当我们创建一个 const 对象时,只有在完成构造函数之后,才会获得 const 属性。
默认构造函数
如果类没有显示的创建构造函数,那么编译器就会隐式的定义一个构造函数。
通过编译器创建的构造函数又称之为 合成的默认构造函数。
对大多数类来说,合成的默认构造函数按以下两点初始化数据成员:
- 如果存在类内初始化,则用它来初始化
- 否则,默认初始化该成员
如果类包含有 内置类型 或 复合类型 成员,则将这些成员全部赋予初始值,那么这个类才适合使用合成的默认构造函数。
有时候编译器不能为某些类合成默认构造函数
例:如果类中包含一个其它类的成员,且这个类没有默认构造函数,那么编译器无法初始化该成员。对于这样的类必须自定义默认构造函数,否则该类没有可用的默认构造函数。
注:如果类中存在 const 成员 或 引用 ,需要通过构造函数初始值,现显示的初始化。
如果一个构造函数为所有参数都提供了默认实参,则它实际上也定义了默认构造函数。
//默认构造函数
class A
{
A() = default; //默认构造函数
A(){ i=1,j=2; }
A(int x,int y):i(x),j(y){};
};
委托构造函数
C++11扩展了构造函数初始值的功能,使得我们可以定义所谓的委托构造函数。
一个委托构造函数使用它所属类型的其他构造函数,执行它的初始化过程,或者它把自己的一些职责委托给了其他构造函数。
注:假如受委托的函数体包含有代码的话,将先执行这些代码,然后控制权才会交还给委托者的函数体。
//委托构造函数
class Sal
{
//非委托构造函数使用对应的实参初始化
Sal(string s,int n):str(s),i(n){};
//其余构造函数都委托给另一个构造函数
Sal():Sal("",0){};
Sal(string s):Sal(s,0){} ; //会发生隐式转化
};
转换构造函数
如果构造函数只接受一个实参,则它实际上定义了转换为此类型的隐式转换机制,则称为 转换构造函数。
Sal(string s):Sal(s,0){};
接受string的构造函数定义了从这种类型向sal转换的规则,也就是说在需要使用Sal的地方可以使用string来代替。
可以理解为相当于隐式的构造了一个Sal对象
注: 编译器只能经过一次类类型转换:
例:将string转换为Sal类型 而不是 将字符串 先转换为sring在转换为Sal类型
抑制构造函数定义的隐式转换
可以将构造函数申明为 explicit 加以阻止进行隐式转换。
explicit Sal(string s):Sal(s,0){};
explicit 的构造函数只对一个参数有效,且转换构造函数只要求一个参数
explicit 的构造函数只能用于直接初始
发生隐式转换的一种情况是当我们执行拷贝初始化时(使用 = ),此时只能用直接初始化而不能使用explicit构造函数。
注:使用explicit关键字申明的构造函数,它只能以直接初始化形式使用,而且编译器不会在自动转换过程中使用该构造函数。
Sal sal(s); //直接初始化
Sal sal = s; //不能将explain构造函数用于拷贝形式的初始化
拷贝构造函数与拷贝赋值运算符
拷贝构造函数
如果一个构造函数的第一个参数是自身类型的 引用 ,且任何额外参数都有默认值,则称为拷贝构造函数。
拷贝构造函数第一个参数必须是一个 引用 类型,一般为 const引用类型,也可以是 非const的引用。
注:因为拷贝构造函数在几种情况下都会被隐式的使用,所以拷贝构造函数不因该是explicit的。
如果没有为一个类定义拷贝构造函数,编译器会 合成拷贝构造函数 ,即使定义了其他拷贝构造函数,编译器也会 合成拷贝构造函数。
一般情况下,合成的拷贝构造函数会将其参数逐个拷贝到正在创建的对象中。(非static成员)
对于类类型成员,拷贝时用拷贝构造函数,对于内置类型则直接拷贝。
拷贝初始化
当使用直接初始化时,实际上是编译器使用普通的函数匹配,来选择所提供参数最匹 配的构造函数,
当使用拷贝构造函数时,编译器将右侧运算对象拷贝到正在运算的对象中,如果可以还要进行类型转换。
拷贝初始化通常由拷贝构造函数完成。但,如果一个类有一个移动构造函数,则拷贝初始化有时候会使用移动构造函数而不是拷贝构造函数。
拷贝初始化在以下几种情况下会发生:
- 使用 = 定义变量时发生
- 将一个对象作为实参传递给一个非引用类型的形参
- 从一个返回类型为 非引用类型 的函数返回一个对象
- 用花括号列表初始化一个数组中的元素或一个聚合类中的成员
- 某些类类型还会对他们分配的对象使用
例:初始化标准库容器或是调用其它insert或push成员是,容器会对其元素进行拷贝初始化。
编译器可以绕过拷贝构造函数
在拷贝初始化过程中编译器可以跳过拷贝/移动构造函数,直接创建对象。
但,即使编译器绕过了拷贝/移动构造函数,但在程序上拷贝/移动构造函数必须是存在且可访问的,即不能是private的。
string s = "9999"; //拷贝初始化
string s (9999); //编译器略过了拷贝构造函数
//拷贝构造函数
class Foo
{
Foo(); //默认构造函数
Foo(const Foo&); //拷贝构造函数
private:
int size;
string buff;
};
拷贝赋值运算符
与类控制其对象如何初始化一样,类也可以控制其对象如何赋值:
Sal sal1,sal2;
sal1 = sal2; //使用Sal的拷贝赋值运算符
如果类未定义自己的拷贝赋值运算符,则编译器会为生成一个合成的拷贝赋值运算符。
对于某些类来说,合成拷贝运算符用来禁止该类型对象的赋值。如果拷贝赋值运算符并非出自此目的,则它会将右侧运算对象的每个非static成员赋予左侧运算对象的对应成员。合成的拷贝运算符会返回一个指向其左侧对象的引用。
可参考重载赋值运算符
Foo& Foo::operator=(const Fool &s)
{
size=s.size; //使用内置的int赋值
buff=s.buff; //调用string::operator=
return *this; //返回该对象的引用
}
移动构造函数与移动赋值运算符
移动构造函数
移动构造函数通常是将资源从给定的对象“移动”而不是拷贝到正在创建的对象
在很多情况下都会发生对象拷贝,并且在默写情况下,对象拷贝后都立即被销毁了,在这种情况下,使用移动而非拷贝会大幅度提升性能。
使用移动而不是拷贝的一个原因源于IO类或unique_ptr这样的类,这些类都包含不能被共享的资源(如指针或IO操作)
标准库容器,string类和shared_ptr类既支持移动也支持拷贝。IO类和unique_ptr类可以移动但不能够拷贝。
移动构造函数和移动赋值运算符,它们从给定对象“窃取”资源而不是拷贝资源。
移动构造函数第一个参数是该类类型的一个引用,且这个引用参数在移动构造函数中是一个右值引用。
//我们通过&&而不是&来获得右值的引用
除了完成资源控制,移动构造函数还必须确保以后源对象处于这样一种状态——销毁它是无害的,特别是,一旦完成移动,源对象不在指向被移动到资源,这些资源的所属权已近归属新创建的对象。
StrVec::StrVec(StrVec &&s) noexcept //通知标准库构造函数不抛出任何异常
: elements(s.element),first_tree(s.first_tree)
{
s.elements = s.first_tree = nullptr; //令s进入这样的状态,对其析构函数是安全的
}
与拷贝构造函数不同,移动构造函数不分配任何新内存,他接管StrVec的内存。在接管测内存后,它将给定对象中的指针都置为nullptr。这样就完成了移动操作。最终移后源对象会被销毁,意味着在其上运行了析构函数。StrVec的析构函数在first_tree上调用dealocate。如果忘记了改变s.first_tree,则销毁后源对象就会释放掉刚刚移动的内存。
移动赋值运算符
类似拷贝赋值运算符,移动赋值运算符必须正确处理自赋值。
StrVec &StrVec::operator=(StrVec &&rhs) noexcept
{
if(this != &rhs) //直接检测自赋值
{
free(); //释放已有元素
elements = rhs.elements; //从rhs接管资源
first_tree = rhs.first_tree;
rhs.elements = rhs.first_tree = nullptr; //将rhs置为可析构状态
}
return *this;
}
除了将移后源对象值为析构状态,移动操作还必须保证源对象仍是有效的,一般来说,对象有效是指可以安全的为其赋予新制 或者 可以安全的使用而不依赖其当前值。
在移动操作之后,以后原对象必须保持有效的、可析构的状态,但是用户不能对其值进行任何假设。
与拷贝不同,编译器不会为某些类合成移动操作,尤其是,一个类定义了自己的拷贝构造函数、拷贝赋值运算符或者析构函数,编译器不会为它合成移动构造函数和移动赋值运算符。如果一个类没有移动操作,通过正常的函数匹配,类会使用对应的拷贝操作来代替移动操作。
只有一个类没有定义任何自己版本的拷贝控制成员,且它的所有数据成员都能移动构造或移动赋值,编译器才会为它合成移动构造函数或移动赋值运算符。
定义了一个移动构造函数或移动构造赋值运算符的类必然也定义自己的拷贝操作,不然,这些成员默认的定义为删除的。
阻止拷贝
对大多数类来说因该定义拷贝构造函数和拷贝赋值运算符,但对于某些类这些操作没有合理的意义,这时候便可以使用某种机制阻止拷贝和赋值。
也就是定义删除的函数,在函数参数列表后面加上 =delete 来指出
class no_copy
{
no_copy() = default; //合成的默认构造函数
no_copy(const no_copy&) = delete; //阻止拷贝
no_copy &operator=(const no_copy&) = delete; //阻止删除
}
本质上,当不可能拷贝,赋值或销毁类成员时,类的合成拷贝控制成员就被定义为删除的。
析构函数
构造函数初始化对象非static成员
析构函数释放对象使用的资源,并销毁对象的非static成员
。
构造函数,成员的初始化实在函数体执行之前完成的,且按照它们在类中出现的顺序完成初始化
析构函数,首先执行函数体,然后销毁成员,成员按初始化顺序逆序销毁
由于析构函数不接收参数,则析构函数无法被重载
当一个对象的引用或指针离开作用域时,不会执行析构函数
当一个类未定义一个析构函数时,编译器会定义一个合成的析构函数,它被用来阻止该类型的对象被销毁
析构函数自身并不直接销毁成员,成员是在析构函数之后隐含的析构阶段中被销毁的。在整个对象销毁过程中,析构函数是作为成员销毁步骤之外的一部分而进行。