您的位置:首页 > 编程语言 > C语言/C++

C++ 13章 类的拷贝控制

2017-12-27 21:13 204 查看
拷贝控制操作:定义类的时候, 显示或隐式指定了在此类型对象拷贝,移动,赋值和销毁时做了什么。拷贝构造函数和移动构造函数定义了当同类型的另一个对象初始化本对象做了什么。移动和拷贝赋值运算符定义了当同类型的另一个对象赋予给本对象做了什么。析构函数定义此类型对象销毁做了什么。

如果一个类没有定义所有这些拷贝控制成员,那么编译器自动添加缺省的操作。但是如果我们不定义这些操作,编译器帮助我们定义,但是编译器定义的可能并不是我们想要的操作。

拷贝构造函数

拷贝构造函数:将通类型对象的数据成员拷贝给新对象的数据成员。一个构造函数的第一个参数是自身类类型的引用,且其他参数都有默认值。

class String {
public:
String(const char*);//构造函数
String(const String&);//拷贝构造函数
}


合成拷贝构造函数:区别与合成默认构造函数,即使其他构造函数已经定义了,编译器也会合成一个默认的。一般情况下,合成的拷贝构造函数将参数成员逐个拷贝到新创建对象中。

拷贝初始化化:区别直接初始化(选择合适的构造函数初始化)和拷贝初始化(先创建对象并用构造函数初始化,然后将先前创建的对象数据成员拷贝到新对象里面)。使用=定义变量、将一个对象作为实参传递给非引用参数、从一个返回类型为非引用的函数返回对象、用花括号列表初始化元素或一个聚合类的成员 是进行拷贝初始化。

拷贝构造的限制:

编译器可以绕开拷贝构造函数:

拷贝赋值运算符

定义的时候赋值叫做初始化,以后赋值叫做拷贝赋值运算符。如果类未定义自己的拷贝赋值运算符,编译器也自动合成一个。

重载赋值运算符:本质是一个函数,其名字由operator关键字后接要定义的运算符合组成。operator=函数,运算符函数也有返回类型和一个参数列表。参数表示运算符的运算对象。

class foo{
public:
foo& operator=(const foo &);//拷贝赋值运算符。
}
Sale_data a , b;
a = b;//使用拷贝赋值运算符


合成拷贝赋值运算符:未定义,那么编译器将帮助你合成拷贝赋值运算符。并在使用的对新对象进行赋值操作。为什么需要有这种鬼东西?全部是因为类中,数据成员太多了,但是需要完成一次赋值操作,但是又不想让程序员来做,所以就指定对应的规则,然后让编译器帮助我们完成重复性强的操作。

Sale_data& Sale_data::operator=(Sale_data &a)//等价于合成拷贝赋值运算符
{
bookNo = a.bookNo;
units_sold = a.units_sold;
revenue = a.revenue;
return *this;//返回本对象的引用
}


析构函数

构造函数初始化对象每一个非static数据成员,还可能做做其他工作因为有函数体;析构函数释放对象使用资源,并销毁对象的非static数据成语。波浪号接类名组成。没有返回值也不接受参数,所以不可被重载。

class foo{
public:
foo& operator=(const foo &);//拷贝赋值运算符。
~foo();//析构函数
}


析构函数完成了什么工作:构造函数先成员初始化,然后再执行函数体。析构函数先执行函数体,然后销毁成员,成员按初始化顺序的逆序销毁。析构部分是隐式的,成员销毁发生什么依赖于成员的类型。销毁类类型的成员需要执行成员自己的析构函数,内置类型没有析构函数,因此销毁内置类型啥也不用做。区别析构普通指针(不会delete所指向的对象)和智能指针(会调用自己的析构函数,delete所指向的对象)的区别。

什么时候调用析构函数:因为析构函数自动调用,所以有了所谓的智能指针。很重要,C语言不需要析构函数,因为内存管理相对简单,但是C++有了类,类里面可能还含有动态内存分配的对象,甚至类里面的类一样有,所以这样手动管理可能相对困难,这就需要提供一种机制,那么就是析构函数进行处理调用各自释放内存函数进行是否内存。这点很重要。重点就是进行了内存管理。

在一个析构函数(重点销毁在堆上的数据)中,不存在类似构造函数中初始化列表的东西来控制成员如何销毁,析构部分是隐式的。成员销毁时发生什么完全依赖于成员的类型 。销毁类类型的成员需要执行成员自己的析构函数 。内置类型没有析构函数,因此销毁内置类型成员什么也不需要做(分配在栈上,由栈进行管理管理销毁了)。

无论何时一个对象被销毁, 就会自动调用其析构函数 :

1、变量在离开其作用域时被销毁。(由栈管理)

2、当一个对象被销毁时,其成员被销毁。(析构和栈共同管理)

3、容器(无论是标准库容器还是数组) 被销毁时,其元素被销毁。(析构管理)

4、对于动态分配的对象,当对指向它的指针应用delete运算符时被销毁。(动态)

5、对于临时对象,当创建它的完整表达式结束时被销毁。

由于析构函数自动运行,我们的程序可以按需要分配资源,而(通常)无须担心何时释放这些资源。但是通过malloc和new的除外。因为类里面需要分配资源的都配有析构函数,可以释放对应的资源,而new和malloc不是类,所以必须手动释放。

{//新作用域
Sale_data *p = new Sale_data;//动态分配一个对象,和malloc类似

auto p2 = make_shared<Sale_data>();
//智能指针,分配shared_ptr。
//shared ptr的析构函数会递减p2指向的对象的引用计数,引用计数会变为0, 因此shared_ptr的析构函数会delete p2分配的Sales_data对象

Sale_data items(*p);//拷贝构造函数将*p拷贝到items

vector<Sale_data> vec;//局部对象

vec.push_back(*P2)//拷贝p2指向的对象

delete p;//对p指向的地方执行构造函数
}
//退出局部作用域;编译器自动调用item、p2和vec调用析构函数。
/*
我们只需直接释放绑定到p的动态分配对象。其他Sale_data会离开作用域自行销毁。
item、p2和vec离开作用域时候会调用(编译器自动加入)对应的析构函数。vector析构函数会销毁添加的vec元素。
shared_ptr会递减p2的引用计数。
所有Sale_data的析构函数隐式销毁bookNo成员,销毁bookNo成员会调用string析构函数,会释放用来保存ISBN的内存。
当指向一个对象的引用或指针离开时候,析构函数不会执行,因为内置类型没有析构函数,仅仅通过栈进行管理释放局部变量而已。
*/


合成析构函数:未定义,则编译器帮你定义一个空函数体的析构函数。除了销毁成员,不做任何其他事情。认识到析构函数体本身并不直接销毁成员很重要,函数体是销毁步骤另一个部分,析构函数先执行函数体后,然后成员在隐含的析构阶段被逐个销毁。特别的string的析构函数会被调用释放bookNo成员用的内存。这都是一种机制,让程序员尽量少做事情的机制。累死编译器了,真是累死编译器了。。

-需要析构函数的类也需要拷贝和赋值操作:加入动态分配内存new,然后在析构函数里面释放。但是编译器合成的拷贝和赋值构造函数简单拷贝指针成员而已。

//等价的合成析构函数
class foo{
public:
foo& operator=(const foo &);//拷贝赋值运算符。
~foo(){}//合成的析构函数,成员自动销毁,不做其他事情。
}


三五法则

需要析构函数也需要拷贝和赋值函数。最好就是前面说的三个函数都定义起来,否则可能出现内存管理上面的问题。这是一些简单的逻辑,没事看看这个书本,就会明白这个道理。

使用=default:类内用=default修饰具有合成版本的成员函数,显示让编译器生成内联函数。

阻止拷贝

大多数情况定义拷贝函数和赋值运算符有合理的意义,但是某些类拥有这些操作没有什么意义,也就是需要机制阻止拷贝或赋值。用来阻止编译器合成。

定义删除函数:声明了它们,但是告诉编译器永远也不可能使用他们。= delete告诉编译器和程序员我们不希望定义这些成员。

class NoCopy{
public:
NoCopy() = default;//使用合成默认并且内联进来
NoCopy(const NoCopy &) = delete;//阻止拷贝
NoCopy &operator=(const NoCopy &) = delete;//阻止赋值
~NoCopy() = default;//合成析构并且内联进来
}


析构函数不能是删除成员:否则编译器将不允许创建该类型的变量或临时对象,因为不能释放掉内存,这是严重错误的操作。

合成的拷贝控制成员可能是删除的:
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: