成都网站建设技术支持,宁波seo关键词,黄冈网站优化公司哪家好,银川网站建设那家好文章目录6个默认成员函数构造函数概念默认构造函数的类型默认实参概念默认实参的使用默认实参声明全局变量作为默认实参某些类不能依赖于编译器合成的默认构造函数第一个原因第二个原因第三个原因构造函数初始化构造函数里面的“”是初始化吗#xff1f;为什么要使用列表初始化…
文章目录6个默认成员函数构造函数概念默认构造函数的类型默认实参概念默认实参的使用默认实参声明全局变量作为默认实参某些类不能依赖于编译器合成的默认构造函数第一个原因第二个原因第三个原因构造函数初始化构造函数里面的“”是初始化吗为什么要使用列表初始化列表初始化成员初始化的顺序类内成员的默认初始化赋值和初始化的效率差异拷贝构造函数概念拷贝构造函数的参数必须是引用类型编译器合成的拷贝构造函数构造函数体内赋值和列表初始化的效率差异构造函数体内赋值列表初始化总结拷贝构造函数的两种调用方式及易错问题调用方式一调用方式二易错问题重载运算符概念运算符的重载赋值运算符的重载分清拷贝构造函数和赋值运算符注意实例取地址运算符重载、对const对象取地址运算符的重载析构函数概念构造函数和析构函数的类比怎样调用析构函数下面代码中会调用几次析构函数析构函数实例defaultdelete三/五法则6个默认成员函数 class Date
{
};可以看到上面那个类没有任何成员是一个空类但是它真的什么都没有吗
其实一个类在我们不写的情况下也会生成6个默认的成员函数分别是构造函数析构函数拷贝构造函数赋值运算符重载取地址运算符重载对const对象取地址运算符的重载 构造函数
概念
特征
函数名与类名相同。无返回值。对象实例化时编译器自动调用对应的构造函数。构造函数可以重载。不同于其他成员函数构造函数不能被声明成const的。
不能被声明成const的原因
构造函数的作用就是为了初始化对象的成员参数如果被声明为const则会认为自己无法修改调用对象的值也就剥夺了构造函数的作用。
但构造函数仍可以用来初始化const对象
当我们需要创建类的一个const对象时直到构造函数完成初始化过程对象才能真正取得其“常量”属性构造函数在进行初始化时对象的const属性不生效。因此构造函数在const对象的构造过程中可以向其写值并且构造函数不必实则不能被声明为const。 默认构造函数的类型
可分成两类
编译器合成的默认构造函数程序员定义的默认构造函数
合成的默认构造函数按照如下规则初始化类的数据成员 如果存在类内的初始值用它来初始化成员否则默认初始化该成员 如果我们要自己定义一个默认构造函数那我们有两种方法 1.定义一个无参的构造函数。 2.定义所有参数都有缺省值默认实参在下面介绍的构造函数【全缺省的构造函数】。 在实际编程中只能使用上述两种方法中的一种全缺省的构造函数和无参构造函数不能同时出现因为编译器会无法识别此时到底该调用哪一个。
class Date
{
public://无参默认构造函数Date(){_year 0;_month 1;_day 1;}//全缺省的默认构造函数Date(int year 0, int month 1, int day 1){_year year;_month month;_day day;}//有参构造函数也就是一般构造函数Date(int year, int month, int day){_year year;_month month;_day day;}private:int _year;int _month;int _day;
};int main()
{Date d1;//调用默认构造函数Date d3();//如果要调用默认构造函数后面不能加上括号//加上了则变成了函数声明Date d2(2020, 4, 19);//调用有参数的return 0;
}默认实参
概念
默认实参某些函数有这样一种形参在函数的很多次调用中它们都被赋予一个相同的值此时我们把这个反复出现的值成为函数的默认实参。
默认实参作为形参的初始值出现在形参列表中。
需要注意的是
我们可以为一个或多个形参定义默认值但一旦某个形参被赋予了默认值它后面的所有形参都必须有默认值。局部变量不能作为默认形参。类型可以转化成形参所需类型的表达式也能作为默认实参。比如函数的返回类型
第一点
class Date{Date(int year, int month 1, int day)// error:一旦某个形参被赋予了默认值month// 它后面的所有形参都必须有默认值day{_year year;_month month;_day day;}
};第二点
class Date{int i;Date(int year i, int month 1, int day 1)// error:局部变量不能作为默认形参{_year year;_month month;_day day;}
};第三点
int sd();class Date
{
public:Date(int year 0, int month sd(), int day 1)
}默认实参的使用
如果想使用默认实参只要在调用函数时忽略该实参就行了。
int main()
{Date d2(2020); // 等价于d2(2020,1,1)return 0;
}函数调用时实参按其位置解析默认实参负责填补函数调用缺少的尾部实参靠右侧位置。
int main()
{Date d2(,3,); // error:只能省略尾部实参return 0;
}默认实参声明
通常的习惯是一个函数只声明一次但是实际上多次声明同一个函数也是合法的。
不过给定作用域中一个形参只能被赋予一次默认实参。换言之函数的后续声明只能为之前那些没有默认值的形参添加默认实参。
而且同样要遵循该形参右侧的所有形参必须都有默认值。 Date(int year, int month, int day 1)Date(int year, int month, int day 2)// error:重复声明Date(int year 2, int month 2, int day)// 正确 全局变量作为默认实参
//伪代码
int i 100, y 3;
class Date
{
public:Date(int year i, int month y, int day 1)// 用作默认实参的变量名在函数声明所在的作用域内解析
}
void fun()
{i 2020; // 改变默认实参的值int y 4; //隐藏了外层定义的y但没有改变默认实参的值Date d Date(); // 调用Date(2020,3,1)// 而变量名的求值过程发生在函数调用时
}其实就是fun中的 i 仍为全局变量改变函数中的 i 就是改变 全局变量 i 。但是 函数中的 y 是生存期只在函数体内的局部变量改变其值不影响全局变量 y 的值。 某些类不能依赖于编译器合成的默认构造函数
第一个原因
如果我们没有显式创建构造函数编译器会自动构建一个默认构造函数但如果我们已经显式定义了构造函数则编译器不会再生成默认构造函数。那么除非我们再定义一个默认的构造函数否则类将没有默认的构造函数。
这条规则的依据是如果一个类在某种情况下需要控制对象初始化我们显式定义构造函数那么该类很可能在所有情况下都需要控制。 第二个原因
合成的默认构造函数可能执行错误的操作。如果定义在块中的内置类型或复合类型比如数组和指针的对象被默认初始化则他们的值将是未定义的。 类中有其他类类型成员也一样
如果真的非常想使用合成的默认构造函数又不想得到未定义的值则可以将成员全部赋予类内的初始值这个类才适合于使用编译器合成的默认构造函数。
第三个原因
编译器不能为有些类合成默认构造函数例如如果类中包含一个其他类类型的成员且这个成员的类型没有默认构造函数那么编译器将无法初始化该成员。 从例子中可以看出A类的构造函数可以正常工作但是当使用Date类的合成默认构造函数创建一个对象时由于Date类中有其他类类型的成员A类型的成员a且其所在类A类没有默认构造函数只有一般构造函数导致编译器无法初始化该成员a。 构造函数初始化
构造函数里面的“”是初始化吗
上面构造函数内的赋值语句是初始化吗 乍一看很可能会觉得构造函数内的赋值语句是初始化但是如果这样写呢
class Date
{
public:Date(int year, int month, int day){_year year;_month month;_day day;_year 2020;}int getyear(){return this - _year;}private:int _year;int _month;int _day;
};往后面加上了一个_year 2020那这样还是初始化吗总不可能是先用year初始化_year再用2020来初始化它这明显不成立因为初始化只能一次而函数体内的赋值可以多次 所以我们可以将函数体内的赋值理解为赋初值而非初始化。 为什么要使用列表初始化
有时我们可以忽略数据成员初始化和赋值之间的差异但并非总能这样。
当类的成员是以下三种时必须通过构造函数初始值列表为它们提供初值列表初始化
引用成员变量引用必须在定义的时候初始化并且不能重新赋值所以也要写在初始化列表中const成员变量因为常量只能在初始化不能赋值所以必须放在初始化列表中未提供默认构造函数的类类型因为使用初始化列表可以不必调用默认构造函数来初始化而是直接调用拷贝构造函数
通过一个例子来理解1、2点的意思
随着构造函数体一开始执行初始化就完成了初始化完成了也就意味着只能进行赋值操作了不能进行定义且赋值的操作了。不能赋值的成员const属性、引用类型如果没有在此之前完成初始化过程也就成为了未定义的状态。例如下面的_month的const属性已经成立无法再在函数体内为_month赋值了_day是引用类型没有进行初始化的引用类型是无法赋值的。
而我们上面说过构造函数里面的“”是赋值行为而非初始化因此
class Date
{
public:Date(int i) {_year i; // 正确_month i; // 错误不能给const赋值_day i; // 错误i没被初始化}private:int _year;const int _month;int _day;
};因此我们初始化const或者引用类型的数据成员的唯一机会就是 通过构造函数初始值列表为它们提供初值。 列表初始化
class Date
{
public:// 列表初始化Date(int year, const int month, int day):_year(year),_day(day),_month(month){}int getye(){return this - _year;}int getmoth(){return this - _month;}int getday(){return this - _day;}private:int _year;const int _month;int _day;
};
int main(int argc, char const *argv[]) {int i 2020;Date a(i,3,14);cout a.getye() endl;cout a.getmoth() endl;cout a.getday() endl;return 0;
}成员初始化的顺序
列表初始化中有一个容易出错的地方——成员初始化的顺序可以看到我这里初始化列表的顺序是year,day,month。但是实际上初始化的顺序和初始化列表中顺序毫无关联初始化的顺序是按照参数在类中声明的顺序的 也就是下面的year,month,day如图。 一般来说初始值列表的初始化顺序不会影响什么就如上面的代码结果依然符合我们的预期
不过如果一个成员是用另一个成员来初始化的那么这两个成员的初始化顺序就很关键了具体是什么意思呢举个例子将初始值列表做出如下更改
Date(const int year, const int month):_year(year),_day(month),_month(_day){}查看结果
int main()
{int i 2020;Date d2(i, 4);cout d2.getmoth() endl;cout d2.getday() endl;return 0;
}
从形式上初始值列表的顺序来讲
先用形参month初始化成员_day再用初始化成功的_day去初始化成员_month
但实际上真的是这样吗 我们来看看运行结果 可以看到初始化成功的只有成员_day实际上初始化的顺序是按照参数在类中的声明顺序来的
也就是先用形参year初始化成员_year。再用成员_day初始化成员_month但由于此时成员_day尚未被形参month初始化因此成员_month值是未定义的。接下来用形参month初始化成员_day。
从而生成了上图的结果。 类内成员的默认初始化
如果没有在构造函数的初始值列表中显式地初始化成员则该成员会在构造函数体之前执行默认初始化。
执行默认初始化分两种情况
第一种被忽略的成员有类内初始值本例中的_month_day
class Date
{
public:Date(int year):_year(year){ }int getye(){return this - _year;}int getmoth(){return this - _month;}int getday(){return this - _day;}private:int _year;int _month 3;int _day 24;
};
int main(int argc, char const *argv[]) {Date a(2020);cout a.getye() endl;cout a.getmoth() endl;cout a.getday() endl;return 0;
}从结果可知没有在初始值列表中显式初始化的数据成员如果其具有类内初始值会隐式地使用类内初始值初始化。
第二种情况被忽略的成员没有类内初始值本例中的_month_day
class Date
{
public:Date(int year):_year(year){ }int getye(){return this - _year;}int getmoth(){return this - _month;}int getday(){return this - _day;}private:int _year;int _month;int _day;
};
int main(int argc, char const *argv[]) {Date a(2020);cout a.getye() endl;cout a.getmoth() endl;cout a.getday() endl;return 0;
}从结果可知没有在初始值列表中显式初始化的数据成员如果其也没有类内初始值则其值是未定义的试图拷贝或以其他形式访问此类值将引发错误。
综述
构造函数不应该轻易覆盖掉类内初始值除非新赋的值与原值不同。构造函数使用类内初始值不失为一种好的选择因为这样能确保为成员赋予了一个正确的值。如果不能使用类内初始值编译器不支持或其他原因则所有构造函数都应该显式地初始化每一个内置类型的成员。 赋值和初始化的效率差异
在很多类中赋值和初始化的区别事关底层效率问题对于内置类型而言赋值和初始化在效率上的区别不是很大
赋值首先会用默认构造函数来构造对象再通过重载后的赋值运算符进行赋值。列表初始化会直接调用拷贝构造函数减少了一次调用默认构造函数的时间。
具体详解在下面的拷贝构造函数中。 拷贝构造函数
概念
如果构造函数的
第一个参数是自身类类型的引用其他参数如果有的话都有默认值。
则此构造函数是拷贝构造函数。
拷贝构造函数和赋值运算符重载是C为我们准备的两种能够通过其他对象的值来初始化另一个对象的默认成员函数。
拷贝构造函数是构造函数的一个重载形式 Date(const Date d){_year d._year;_month d._month;_day d._day;}调用方法用别的对象来为本对象赋值同时因为不需要修改引用的对象则为它加上const属性。
int main()
{Date d1;Date d2 d1;Date d3(d1);//这两种等价都是拷贝构造函数并且d2不是赋值运算符重载
}拷贝构造函数的参数必须是引用类型
原因如下
函数具有以下特性
函数调用过程中非引用类型的参数需要进行拷贝初始化。函数具有非引用的返回类型时返回值会被用来初始化调用方的结果。
根据上述特性可知
拷贝构造函数被用来初始化函数的非引用类类型参数如果拷贝函数本身的参数不是引用类型为了调用拷贝构造函数我们必须拷贝他的实参为了拷贝实参又需要调用拷贝构造函数如此无限循环。
以本例来讲 当我们要用d1来初始化d2的时候需要将d1先传递给形参d再用形参d进行赋值但是d1传递给d的时候又会再次调用一个拷贝构造函数这个d又会给它的拷贝构造函数的形参d传参这又会调用新的拷贝构造函数就导致了一个无限循环 所以要加上引用。
编译器合成的拷贝构造函数
如果我们不去定义一个拷贝构造函数编译器也会默认创建一个。默认的拷贝构造函数对象按内存存储按字节序完成拷贝 这种拷贝我们叫做浅拷贝或者值拷贝。 如果是上面这个日期类当然没问题但如果涉及到了动态开辟的数据就会有问题了。
假设在堆上开辟了某个大小的一个数据默认的拷贝构造函数会按照字节序来拷贝这个数据这一步其实没问题问题就出在析构函数上。因为析构函数会在类的生命周期结束后将类中所有成员变量释放这时浅拷贝的数据就会存在问题因为他们指向的是同一块空间。 合成拷贝构造函数做的是用“别的对象”来为本对象赋值本对象只是创建了一个指针再指向“别的对象”动态开辟的空间而非用new再开辟一个新空间此时就出现了多个指针指向同一个空间的情况 而原对象和拷贝的对象会分别对它释放一次就导致了重复释放同一块内存空间double free。
所以对于动态开辟的数据我们需要使用深拷贝。
构造函数体内赋值和列表初始化的效率差异
代码如下
class A{int test 666;
public:A(int i):test(i){cout a列表初始化 endl;}A(){cout this endl;cout a默认构造函数 endl;cout endl;}A(const A a2){test a2.test;cout 调用者 this endl;cout 被调用者 a2 endl;cout a拷贝构造函数 endl;cout endl;}A operator(const A a4){cout 调用者 this endl;cout 被调用者: a4 endl;cout a赋值运算符重载 endl;if(this ! a4){test a4.test;}return *this;}int gett(){return this-test;}~A(){cout a析构函数 this endl;}
};class Date
{
public:Date(int year, int month, int day, A a1)//构造函数{cout 执行函数体前 endl;_year year;_month month;_day day;_a a1;cout endl;cout 成员_a _a endl;cout a1 a1 endl;cout d构造函数 endl;}Date(int flag,A a3):_year(flag), _month(flag), _day(flag), _a(a3){cout a3 a3 endl;cout _a: _a endl;cout d列表初始化 endl;}Date(){cout this endl;cout d默认构造函数 endl;}Date(const Date d){cout before endl;_year d._year;_month d._month;_day d._day;_a d._a;cout endl;cout _a: _a endl;cout d._a: d._a endl;cout d拷贝构造函数 endl;}Date operator(const Date d){if(this ! d){_year d._year;_month d._month;_day d._day;_a d._a;}cout 调用者 this endl;cout 被调用者: d endl;cout d赋值运算符重载 endl;return *this;}~Date(){ cout d析构函数 this endl; }private:int _year 2001;int _month 13;int _day 250;A _a;
};两者运行结果如下
构造函数体内赋值
int main()
{A a;// 调用有参构造函数cout d1: endl;Date d1(2021,3,20,a);cout endl;return 0;
}从d1的运行结果可知我们进行构造函数体内赋值操作时
编译器在调用d的构造函数之前执行d的构造函数体之前首先调用A的拷贝构造函数用实参a初始化a1d构造函数的形参。然后调用A的默认构造函数创建数据成员_a。创建好数据成员_a之后开始执行b的构造函数函数体即调用b的构造函数使用A的重载赋值运算符将a1的值赋给数据成员_a。执行完d1的构造操作后编译器调用a的析构函数释放之前创建的形参a1。
列表初始化
int main()
{A a;// 调用列表初始化的构造函数cout d5: endl;Date d5(888,a);
}而对比d5的运行结果可知我们进行列表初始化时编译器两次调用拷贝构造函数
一次用实参a初始化形参a3。一次用形参a3初始化数据成员_a。之后执行a的析构函数释放创建的形参a3。
总结
列表初始化的第2点在函数体内赋值中被拆分成了2、3点。换言之默认构造函数、重载的赋值运算符两步完成的操作 与 拷贝构造函数一步完成的操作是等价的而我们说过 拷贝构造函数和赋值运算符重载是C为我们准备的两种能够通过其他对象的值来初始化另一个对象的默认成员函数。 它们起到的功能是一样的因此我们说减少了一次调用默认构造函数的时间。
拷贝构造函数的两种调用方式及易错问题
调用方式一
int main()
{A a;// 调用拷贝构造函数的方式一cout d2: endl;Date d2(d1);cout endl;
}调用方式二
int main()
{A a;//调用拷贝构造函数的方式二cout d4: endl;Date d4 d1;cout endl;
}可以发现两种调用方式执行的底层操作都是一样的
1. 函数声明阶段也就是执行拷贝构造函数函数体之前图中的before之前调用a的默认构造函数创建数据成员_a 2. 执行函数体用被调用的Date类d1初始化调用的Date类d2d4执行到数据成员_a的初始化时用A的重载赋值运算符将d._a赋给_a
易错问题
但是如以下形式的代码看起来类似第二种调用方式但其实不然
int main()
{A a;cout d3: endl;Date d3;cout 赋值之前 endl;d3 d1;cout endl;
}其执行步骤如下
1. 先用默认构造函数创建d3创建类内数据成员_a时调用A的默认构造函数 2. 再使用重载赋值运算符进行赋值为类内数据成员_a赋值时调用A的重载赋值运算符
区别 粗略理解的话就是两者一个是由拷贝构造函数直接创建对象d2d4一个是用默认构造函数创建对象d3之后再用重载赋值运算符进行赋值。类比内置类型的初始化和定义再赋值可能更便于理解。 重载运算符
类的赋值运算符实际上是对赋值运算符的重载因此我们先介绍一下重载运算符。
概念
函数原型
返回值类型 operator操作符(参数列表)关于重载运算符
某些运算符如赋值运算符必须定义为成员函数。如果一个运算符是一个成员函数其左侧运算对象就绑定到隐式的 this 参数。 规则
不能通过连接其他符号来创建新的操作符比如operator重载操作符必须有一个类类型或者枚举类型的操作数用于内置类型的操作符其含义不能改变例如内置的整型不 能改变其含义作为类成员的重载函数时其形参看起来比操作数数目少1成员函数的操作符有一个默认的形参this限定为第一个形参:: 、*、?、: 、. 注意以上5个运算符不能重载。
通常情况下不应该重载逗号、取地址、逻辑与和逻辑或运算符。 选择重载运算符作为成员函数or非成员函数是很重要的 赋值、下标[ ]、调用()和成员访问箭头-运算符必须是成员。复合赋值运算符一般来说应该是成员但并非必须这一点与赋值运算符略有不同。改变对象状态的运算符或者与给定类型密切相关的运算符如递增、递减和解引用运算符通常应该是成员。具有对称性的运算符可能转换任意一端的运算对象例如算术、相等性、关系和位运算符等因此它们通常应该是普通的非成员函数。
什么叫对称性运算符其实就是形如 这样的 int double 和 double int 是一样的。
如果对称性运算符是成员函数呢假设 operator 是 string类 的成员实际上是非成员
operator 是 string类 的成员上面的第一个加法等价于 s.operator。his 等价于 hi.operators 。hi 的类型是 const char这是一种内置类型内置类型根本就没有成员函数。
因为 string 将 定义成了普通的非成员函数所以 his 等价于 operatorhis 。和任何其他函数调用一样每个实参都能被转换成形参类型**。唯一的要求是至少有一个运算对象是类类型**并且两个运算对象都能准确无误地转换成 string。
运算符的重载 //成员函数的操作符重载bool operator(const Date d2){return _year d2._year _month d2._month _day d2._day;}//如果写成普通的函数bool operator(const Date d1, const Date d2)
{return d1._year d2._year d1._month d2._month d1._day d2._day;
}int main()
{bool isSame (d1 d2)//调用时等价于 isSame d1.operator(d2);//或者 isSame operator(d1, d2);
}赋值运算符的重载
分清拷贝构造函数和赋值运算符
上面说过有两种方法能够实现用其他类来拷贝一个类一个是拷贝构造函数一个是赋值运算符重载
int main()
{Date d;Date d1(d);Date d2 d;//在声明的时候初始化用d初始化d2调用的是拷贝构造函数d1 d2;//是在对象d1已经存在的情况下用d2来为d1赋值这才是赋值运算符重载//声明阶段的“”都自动调用了拷贝构造函数只有不是声明阶段的“”才是赋值运算符重载return 0;
}
Date operator(const Date d){if(this ! d){_year d._year;_month d._month;_day d._day;}return *this;}注意
需要注意的有几点
返回 *this。原因有两点 避免了返回非引用所需的拷贝操作提高效率当出现形如这样的操作时(ab)c如果返回类型不是引用则对括号内 ab 得到的结果进行一次拷贝初始化得到一个匿名对象临时对象这个匿名对象是一个右值对其进行c的赋值操作是未定义行为。 检测是否是自己给自己赋值如果是则忽略因为不需要修改任何参数所以参数都需要加上const并且为了不花费多余的空间去拷贝数据都采取引用一个类如果没有显式定义赋值运算符重载编译器也会生成一个完成对象按字节序的值拷贝。
实例
举一个包含指针成员的类的重载赋值运算符该怎么写的例子
class A
{
public:A(const string s string()):ps(new string(s)){ }A operator(const A);void getps() {cout ps endl;}private:int i 0;string* ps;
};A A::operator(const A a)
{string* newps new string(*a.ps);cout *a.ps endl;cout a.ps endl;// 拷贝指针指向的对象// 不加解引用符就成了拷贝指针本身delete ps; // 销毁ps指向的内存避免内存泄漏ps newps; // 将newps指向的内存赋给ps// newps和ps现在指向同一块内存i a.i;return *this; // 返回此对象的引用
}
int main() {A a;string s1 hello;A a1(s1);a a1;
}输出结果 下面思考一个问题 A A::operator(const A a)
{string* newps new string(*a.ps);delete ps; // 销毁ps指向的内存避免内存泄漏ps newps; i a.i;return *this; // 返回此对象的引用
}A A::operator(const A a)
{delete ps; // 销毁ps指向的内存避免内存泄漏ps new string(*(a.ps)); i a.i;return *this; // 返回此对象的引用
}为什么我们先将 a.ps 拷贝到一个局部临时对象中newps然后销毁 *this的ps 释放旧内存再将 newps 赋值给 *this.ps 而不是像第二种写法那样先释放旧内存再直接将 a.ps 拷贝给 *this.ps 这是因为如果 a 和 *this 是 同一个对象delete ps 会释放 *this 和 a 指向的 string。接下来当我们在 new表达式 中试图拷贝*(a.ps)时就会访问一个指向无效内存的指针其行为和结果是未定义的。
示例中的 A类 在 《CPrimer》中也被称为行为像指针的类这个概念我将在另一篇博客中细讲。 取地址运算符重载、对const对象取地址运算符的重载
取地址运算符也有两个默认的成员函数编译器默认生成不需要我们定义一般只有想让别人获取指定内容的时候才自己定义一个。
class Date
{
public:Date(int year 0, int month 1, int day 1){_year year;_month month;_day day;}//默认取地址重载Date* operator(){return this;}//const取地址重载const Date* operator()const{return this;}int _year;int _month;int _day;
}; 析构函数
概念
析构函数也是一个特殊的成员函数它的功能是
释放对象在生存期分配的的所有资源销毁对象的非static数据成员
特征
析构函数名是在类名前加上字符 ~。无参数无返回值。因为不接受参数因此不能被重载。一个类有且只有一个析构函数。若未显式定义系统会自动生成默认的析构函数。对象生命周期结束时C编译系统系统自动调用析构函数。 构造函数和析构函数的类比
构造函数有一个初始化部分和一个函数体。析构函数有一个析构部分和一个函数体。构造函数中成员初始化是在函数体执行之前完成的按照在类中出现的顺序进行初始化。析构函数中首先执行函数体然后销毁成员。 成员按照初始化顺序的逆序销毁。
逻辑上
析构函数体一般负责销毁对象引用的内存持有的资源。析构部分则是负责对象本身成员的析构。析构部分会逐个调用类类型成员的析构函数调用顺序与声明顺序相反除此之外析构部分还负责调用父类析构函数。
实现上
只有析构函数体是对程序员可见的析构部分是隐式的。所谓隐式的是指这部分代码即调用类成员析构函数和父类析构函数的代码是由编译器合成的。成员销毁时发生什么完全依赖于成员的类型销毁类类型的成员需要执行成员自己的析构函数。内置类型没有析构函数因此销毁内置类型成员什么也不需要做。
销毁普通指针和销毁智能指针的不同
关于智能指针的知识在这里
基于上述红字部分
隐式销毁一个内置指针类型的成员不会delete它所指向的对象。也就是要格外注意类内指针的释放避免内存泄漏。与普通指针不同智能指针是类类型所以具有析构函数。因此智能指针成员在析构阶段会被自动销毁。 怎样调用析构函数
无论何时一个对象被销毁就会自动调用其析构函数
变量在离开其作用域时被销毁。当一个对象被销毁时其成员被销毁。容器无论是标准库容器还是数组被销毁时其元素被销毁。对于动态分配的对象当对指向它的指针应用delete运算符时被销毁。对于临时对象当创建它的完整表达式结束时被销毁。
具体如下 当指向一个对象的引用或指针离开作用域时析构函数不会执行。 因此上述代码唯一需要直接管理的内存就是直接分配的Sales_data对象只需要直接释放绑定到 p 的动态分配对象。 下面代码中会调用几次析构函数 这段代码中会发生三次析构函数调用
函数结束时局部变量item1的生命期结束被销毁Sales_data的析构函数被调用。类似的item2在函数结束时被销毁Sales_data的析构函数被调用。函数结束时参数accum的生命期结束被销毁Sales_data的析构函数被调用。
在函数结束时trans的生命期也结束了但并不是它指向的Sales_data对象的生命期结束只有delete指针时指向的动态对象的生命期才结束所以不会引起析构函数的调用。 析构函数实例
class A
{
public:A(const char* str hello world, int num 3){_str (char*)malloc(sizeof(str));strcpy(_str, str);_num num;cout constructor function endl;}~A(){free(_str);_str nullptr;_num 0;cout destructor function endl;}char* getstr(){return this-_str;}int getnum(){return this-_num;}
private:char* _str;int _num;
};
int main(int argc, char const *argv[]) {A a A();cout a.getstr() endl;cout a.getnum() endl;return 0;
}从结果可以看到析构函数的执行在return语句之前。 default
可以通过使用 default 来显式的要求编译器生成合成的拷贝控制成员函数。 但值得注意的是
当我们在类内用default修饰成员的声明时合成的函数将隐式地声明为内联的就像任何其他类内声明的成员函数一样。如果我们不希望合成的成员是内联函数应该只对成员的类外定义使用default就像对拷贝赋值运算符所做的那样。
delete
虽然大部分情况下都需要拷贝构造函数和拷贝赋值运算符但是对于某些类来讲这些操作没有合理的意义此时应该使用 delete 将无意义的操作定义为删除的函数。其含义是虽然该函数被定义但无法被使用。如iostream类阻止了拷贝以避免多个对象写入或读取相同的IO缓冲。
struct A{A() default; // 使用合成的默认构造函数A(const A) delete; // 阻止拷贝A operator(const A) delete; // 阻止赋值
};与default不同delete必须出现在函数第一次声明的时候。
从逻辑上讲默认的成员只影响为这个成员而生的代码因此default直到编译器调用默认成员时才需要。而编译器需要在第一时间知道一个函数是删除的以便禁止试图使用它的操作。
两者另一个不同之处是我们可以对任何函数指定delete但是只能对编译器可以合成的函数默认构造函数或拷贝控制成员使用default。
在旧标准中我们用声明成private但不定义的方法来起到新标准中 delete 的作用此时试图使用该种函数的用户代码将在编译阶段被标记为链接错误。 三/五法则
由于拷贝控制操作是由三个特殊的成员函数来完成的
拷贝构造函数定义了当用同类型的另一个对象初始化新对象时做什么拷贝赋值运算符定义了将一个对象赋予同类型的另一个对象时做什么析构函数定义了此类型的对象销毁时做什么。
所以我们称此为“C三法则”。在较新的 C11 标准中为了支持移动语义又增加了移动构造函数和移动赋值运算符这样共有五个特殊的成员函数所以又称为“C五法则”。也就是说“三法则”是针对较旧的 C89 标准说的“五法则”是针对较新的 C11 标准说的。为了统一称呼后来人们把它叫做“C 三/五法则”。
需要析构函数的类也需要拷贝构造函数和拷贝赋值运算符。 从“需要析构函数”可知类中必然出现了指针类型的成员否则不需要我们写析构函数默认的析构函数就够了一般是内置指针类型类类型的话一般直接调用该类的析构函数不用我们自己再实现一个析构函数所以我们需要自己写析构函数来释放给指针所分配的内存来防止内存泄漏。 那么为什么说“也需要拷贝构造函数和赋值操作”呢原因是类中出现了指针类型的成员这样的外部资源合成的拷贝构造函数和合成的拷贝赋值运算符是对外部资源的浅拷贝因此析构函数执行delete运算符时会出现double free的错误。拷贝构造函数和拷贝赋值运算符要么都是合成版本要么都是自定义版本。 拷贝构造函数用已有对象构造新对象函数体内类成员的构造方法就是利用拷贝赋值运算符。析构函数不能是删除的否则便无法销毁此类型的对象了。 同时编译器不允许定义该类型的变量或创建该类的临时对象。可以动态分配该对象并获得其指针但无法销毁这个动态分配的对象(delete 失效。如果一个类有私有的或不可访问的析构函数那么其默认和拷贝构造函数会被定义为私有的。如果一个类有const或引用成员则不能使用默认的拷贝赋值操作。 原因很简单const或引用成员只能在初始化时被赋值一次而默认的拷贝赋值操作会对所有成员都进行赋值。显然它不能赋值const和引用成员所以默认的拷贝构造函数不能被使用即会被定义为私有的。
//关于第三点的代码
struct A{A() default;~A() delete;
};
A a; //ERROR:A的析构函数是删除的。
A *p new A(); //正确但无法delete p;
delete p; // ERRORA类没有析构函数无法释放指向A类动态分配对象的指针。