现在的位置: 首页 > 综合 > 正文

C++入门系列——C++中的复制控制(复制构造函数、赋值函数和析构函数)、智能指针

2013年03月31日 ⁄ 综合 ⁄ 共 4301字 ⁄ 字号 评论关闭

 

这张图摘来酷壳(陈浩大神的博客),感觉他总结的这个图比较经典,至少我是深有感触,从一开始不顾任何规则乱写,到四处碰壁开始明白c++需要编程规则。

一直以来都鼓吹自己是一个写C++的选手,但是说实话从来没有系统的打过C++的基础。对于C++的复制控制机制更是知之甚少,这里将结合C++primer中的相关章节谈一下自己的理解。

复制控制

首先解释下什么是复制控制,复制构造函数、赋值操作和析构函数统称为复制控制,这些函数编译器都会自动实现,但是我们也可以定义自己的版本。

下面我们分别分析下这三个不同的函数。

复制构造函数

估计这三个函数中大家最不熟悉的就是复制构造函数,对于没有意识到规则乱写C++的程序的人来说感觉不到它的意义在哪里。这里首先介绍一下它的应用场景,

1)根据一个同类型的对象初始化一个对象的时候,记住这里是初始化,这是复制构造函数与赋值操作的重要区别。这里提到赋值操作的原因是,连个操作都会用到=这个操作符,这样很容易给人造成误解。例如,①A a = b;(其中b是A类型的对象)。②A a;a =b。其中①调用的是复制构造函数进行初始化, ②调用的是赋值操作符。虽然两者都是使用了=,但是执行的函数完全不同。

2)作为实参传递给函数,因为这时要生成临时对象。

3)从函数中返回一个对象,同样也是需要生成临时对象,注意这2)3)都是传递的是对象的本身,不是引用也不是指针。

4)用于初始化容器和数组中的元素。

在大多数情况下不需要定义自己的复制构造函数,比如类只含有内置类型和类类型的数据成员,但是某些时候我们必须定义自己的复制构造函数,比如类中含有指针类型的数据成员(可能造成悬垂指针的问题)。另外的一种情况就是复制完数据之后需要做一些特殊的操作,这一般与具体的应用需求相关联,合成的复制构造函数显然无法完成这样的工作。

其实分析一下我们可以发现复制构造函数没有想象中的那么神秘,其实就是自定义一些行为,最难的地方就是要知道什么时候需要自己定义复制构造函数。

最后提一下如何禁止复制,比如C++中的iostream中的类型是禁止复制的,如何做到这一点呢,因为即使我们不定义自己的复制构造函数,编译器也会帮我们生成一个。我们可以声明一个私有的复制构造函数,并且只声明不定义,这样即使是友元的和成员也不能进行复制,因为没有定义。

顺便再提一句,既然iostream中的类是不允许复制的,那么我们如何使用iostream中的类型作为函数的参数或者返回值呢,可以使用引用。但是不能作为容器的元素。

赋值操作符

这个函数大家应该不会陌生,上面也提到复制构造函数和赋值操作符的重要区别是是否是在初始化的时候。

其实在这部分主要注意一下自己定义赋值操作符的格式就行了,其实就是一个对象调用赋值操作符用另外一个对象的数据改变自己的数据然后将自己返回给自己,好别扭啊,其实就是这样。在定义自己的赋值函数的时候唯一需要注意的就是函数自我赋值需要考虑一下会不会带来问题。

析构函数

这个函数估计是大家最熟悉的一个函数,至少我是这样,但是想要用好析构函数还是要注意一些细节,这个函数使用过程中很容易造成的一个问题就是double free的问题。

自己定义析构函数的场景是我们动态分配了内存空间,如果这种情况下不进行内存释放的话,就会造成内存的泄露。

与构造函数不同的地方是,析构函数即使我们自己定义了,编译器还是会为我们生成合成析构函数,用于释放系统分配的内存空间,另外析构函数可以是虚函数,支持多态,因为当用父类的指针操纵子类的对象时,我们其实希望使用子类的析构函数而不是父类的。

到这里我们把复制控制的概念介绍完了,关键不是懂这些概念,而是什么时候使用这些技术,如何使用。上面我们也提到一个使用到复制控制的场景是,类中包含指针,这种情况下很容易才成悬垂指针的问题。所谓悬垂指针就是两个指针指向同一个对象,其中一个指针删除数据后,另外一个指针就悬垂了,使用或者删除都为出现问题。

智能指针

现在来介绍复制控制使用比较经典一个案例—智能指针。

所谓智能指针就是在普通指针的基础上增加了引用计数的功能,就是说记录有多少指针指向同一个对象,只有当只剩下一个指针的时候才能释放该对象的内存空间,从而避免了前面提到的悬垂指针的问题。

具体智能指针可以用一个类来做实现,其中所有的数据成员都是私有的,将使用该指针的类设为友元函数,包含一个特定类型的指针和一个计数器。

在赋值控制中需要配合智能指针的操作,

1)复制构造函数,在赋值构造函数中需要将计数器加1

2)在析构函数中,需要将指针的计数器减1,同时判断计数器是否等于0,只有等于才能释放对象的内存空间。

3)赋值操作,在赋值操作中需要将自身指针的计数器减1,右操作数的指针计数器加1.但是为了防止自我赋值的情况,需要先将右操作数加1,在将自身减1.

感觉这个智能指针使用起来比较复杂,不知道真正的工程应用会不会使用这个玩意,给人的感觉一点都不美,而且使用友元破坏了面向对象的特性,增加了耦合。

另外一种避免悬垂指针的方法是,在使用复制构造函数或者复制操作的时候,不是直接复制指针本身,而是去指针的值重新new一个对象,将新创建的对象的地址返回,这样就保证了两个指针指向的是不同的副本。

Ok,一点点总结和自我的见解,欢迎大家交流、指导!

这张图摘来酷壳(陈浩大神的博客),感觉他总结的这个图比较经典,至少我是深有感触,从一开始不顾任何规则乱写,到四处碰壁开始明白c++需要编程规则。

一直以来都鼓吹自己是一个写C++的选手,但是说实话从来没有系统的打过C++的基础。对于C++的复制控制机制更是知之甚少,这里将结合C++primer中的相关章节谈一下自己的理解。

复制控制

首先解释下什么是复制控制,复制构造函数、赋值操作和析构函数统称为复制控制,这些函数编译器都会自动实现,但是我们也可以定义自己的版本。

下面我们分别分析下这三个不同的函数。

复制构造函数

估计这三个函数中大家最不熟悉的就是复制构造函数,对于没有意识到规则乱写C++的程序的人来说感觉不到它的意义在哪里。这里首先介绍一下它的应用场景,

1)根据一个同类型的对象初始化一个对象的时候,记住这里是初始化,这是复制构造函数与赋值操作的重要区别。这里提到赋值操作的原因是,连个操作都会用到=这个操作符,这样很容易给人造成误解。例如,①A a = b;(其中b是A类型的对象)。②A a;a =b。其中①调用的是复制构造函数进行初始化, ②调用的是赋值操作符。虽然两者都是使用了=,但是执行的函数完全不同。

2)作为实参传递给函数,因为这时要生成临时对象。

3)从函数中返回一个对象,同样也是需要生成临时对象,注意这2)3)都是传递的是对象的本身,不是引用也不是指针。

4)用于初始化容器和数组中的元素。

在大多数情况下不需要定义自己的复制构造函数,比如类只含有内置类型和类类型的数据成员,但是某些时候我们必须定义自己的复制构造函数,比如类中含有指针类型的数据成员(可能造成悬垂指针的问题)。另外的一种情况就是复制完数据之后需要做一些特殊的操作,这一般与具体的应用需求相关联,合成的复制构造函数显然无法完成这样的工作。

其实分析一下我们可以发现复制构造函数没有想象中的那么神秘,其实就是自定义一些行为,最难的地方就是要知道什么时候需要自己定义复制构造函数。

最后提一下如何禁止复制,比如C++中的iostream中的类型是禁止复制的,如何做到这一点呢,因为即使我们不定义自己的复制构造函数,编译器也会帮我们生成一个。我们可以声明一个私有的复制构造函数,并且只声明不定义,这样即使是友元的和成员也不能进行复制,因为没有定义。

顺便再提一句,既然iostream中的类是不允许复制的,那么我们如何使用iostream中的类型作为函数的参数或者返回值呢,可以使用引用。但是不能作为容器的元素。

赋值操作符

这个函数大家应该不会陌生,上面也提到复制构造函数和赋值操作符的重要区别是是否是在初始化的时候。

其实在这部分主要注意一下自己定义赋值操作符的格式就行了,其实就是一个对象调用赋值操作符用另外一个对象的数据改变自己的数据然后将自己返回给自己,好别扭啊,其实就是这样。在定义自己的赋值函数的时候唯一需要注意的就是函数自我赋值需要考虑一下会不会带来问题。

析构函数

这个函数估计是大家最熟悉的一个函数,至少我是这样,但是想要用好析构函数还是要注意一些细节,这个函数使用过程中很容易造成的一个问题就是double free的问题。

自己定义析构函数的场景是我们动态分配了内存空间,如果这种情况下不进行内存释放的话,就会造成内存的泄露。

与构造函数不同的地方是,析构函数即使我们自己定义了,编译器还是会为我们生成合成析构函数,用于释放系统分配的内存空间,另外析构函数可以是虚函数,支持多态,因为当用父类的指针操纵子类的对象时,我们其实希望使用子类的析构函数而不是父类的。

到这里我们把复制控制的概念介绍完了,关键不是懂这些概念,而是什么时候使用这些技术,如何使用。上面我们也提到一个使用到复制控制的场景是,类中包含指针,这种情况下很容易才成悬垂指针的问题。所谓悬垂指针就是两个指针指向同一个对象,其中一个指针删除数据后,另外一个指针就悬垂了,使用或者删除都为出现问题。

智能指针

现在来介绍复制控制使用比较经典一个案例—智能指针。

所谓智能指针就是在普通指针的基础上增加了引用计数的功能,就是说记录有多少指针指向同一个对象,只有当只剩下一个指针的时候才能释放该对象的内存空间,从而避免了前面提到的悬垂指针的问题。

具体智能指针可以用一个类来做实现,其中所有的数据成员都是私有的,将使用该指针的类设为友元函数,包含一个特定类型的指针和一个计数器。

在赋值控制中需要配合智能指针的操作,

1)复制构造函数,在赋值构造函数中需要将计数器加1

2)在析构函数中,需要将指针的计数器减1,同时判断计数器是否等于0,只有等于才能释放对象的内存空间。

3)赋值操作,在赋值操作中需要将自身指针的计数器减1,右操作数的指针计数器加1.但是为了防止自我赋值的情况,需要先将右操作数加1,在将自身减1.

感觉这个智能指针使用起来比较复杂,不知道真正的工程应用会不会使用这个玩意,给人的感觉一点都不美,而且使用友元破坏了面向对象的特性,增加了耦合。

另外一种避免悬垂指针的方法是,在使用复制构造函数或者复制操作的时候,不是直接复制指针本身,而是去指针的值重新new一个对象,将新创建的对象的地址返回,这样就保证了两个指针指向的是不同的副本。

Ok,一点点总结和自我的见解,欢迎大家交流、指导!

抱歉!评论已关闭.