一、背景介绍

          因为工作关系,需要用到C++编程。对于我来说,虽然一直从事的是linux平台下的嵌入式软件开发,但深入用到C++的特性的地方并不多。对于C++,用得最多的无非是指针、封装、继承、组合以及虚函数。对于复制构造函数、重载操作符、智能指针等概念,虽然也时有接触,但真正自己写代码需要用到的时候,并不多。

          本文尝试对复制构造函数的定义、作用及需要注意的地方做一个简单的解剖。希望能抛砖引玉,对大家的学习起到一个帮助作用。

虽然复制构造函数对于基本的C++编程来说,可能不太用得着。不过这并不说明复制构造函数没什么用,其实复制构造函数能解决一些我们常常会忽略的问题。

假设有一个CStudent类,有数据成员char* name;

现在有一个对象CStudent stu1("tom");//数据成员name="tom";

再生成一对象,stu2,把stu1拷贝给stu2,即CStudent stu2(stu1);

如果我们没有写拷贝构造函数,那么编译器会调用默认的拷贝构造函数.即进行浅拷贝.

浅拷贝的意思就是内存中只有一份"tom",对象stu1的name指针指向了它.把stu1拷贝给stu2后,stu2的name指向也指向了"tom",指向的是内存中同一块地址.这就会出现一个问题:当stu1的对象被销毁了,那么stu1的name指向的"tom"也会被销毁,由于stu2中的name也是指向"tom"的,所以这时stu2中的name就变成空的了,甚至是乱码.

如果我们自己写了拷贝构造函数,那么在内存中就会有两份"tom"了,这时stu1的name和stu2的name就没有什么关系了,只不过他们的值都是"tom"而已!

类似的问题,我们看看如下代码,并编译运行看看结果:

#include <iostream>

class Array {
public:
int size;
int* data; implicit Array(int size)
: size(size), data(new int[size])
{
} ~Array()
{
delete[] this->data;
}
}; int main()
{
Array first(20);
first.data[0] = 25; {
Array copy = first;
std::cout << first.data[0] << " " << copy.data[0] << std::endl;
} // (1) first.data[0] = 10; // (2)
}

运行结果:

25 25

Segmentation fault

为什么会出现segmentation fault段错误这么严重的问题呢?

根本原因跟前面那个例子一样,我们没有写拷贝构造函数。因此,编译器会为我们生成一个默认的拷贝构造函数。默认的构造函数形式如下:

Array(const Array& other)
: size(other.size), data(other.data) {}

该默认构造函数有什么问题呢?

它对data 指针执行的是浅拷贝。也就是说,它只是拷贝原始data成员的地址,这样就带来了两个对象共享指向同一块内存的指针的风险。代码执行到main函数(1)处时,即执行到:

    {
Array copy = first;
std::cout << first.data[0] << " " << copy.data[0] << std::endl;
} // (1)

(1)处时,copy的析构函数会被调用,因为分配在stack上的对象,是自动释放的。当我们进入一个特定的作用域时,堆栈指针会向下移动一个单位,为那个作用域内创建的、以堆栈为基础的所有对象分配存储空间。而当我们离开作用域的时候(调用完毕所有局部构建器后),堆栈指针会向上移动一个单位,也就是copy对象的析构函数在执行到(1)处后会自动得到调用。Array的析构函数,删除了copy的data.由于first和copy对象都共享一个data指针,因此当执行到(2)处,即调用first.data[0]=10时,就会产生令人头痛的段错误::segmentation fault.

如何解决这个问题呢?

写一个自己的拷贝构造函数,并执行深拷贝

// for std::copy
#include <algorithm> Array(const Array& other)
: size(other.size), data(new int[other.size])
{
std::copy(other.data, other.data + other.size, data);
}

二、浅拷贝与深拷贝

浅拷贝:仅仅逐个成员拷贝,而不拷贝资源的方式;浅拷贝是指源对象与拷贝对象共用一份实体,仅仅是引用的变量不同(名称不同)。对其中任何一个对象的改动都会影响另外一个对象。举个例子,一个人一开始叫张三,后来改名叫李四了,可是还是同一个人,不管是张三缺胳膊少腿还是李四缺胳膊少腿,都是这个人倒霉。比较典型的就有Reference(引用)对象,如Class(类)。

深拷贝:既拷贝成员,又拷贝资源的方式。深拷贝是指源对象与拷贝对象互相独立,其中任何一个对象的改动都不会对另外一个对象造成影响。举个例子,一个人名叫张三,后来用他克隆(假设法律允许)了另外一个人,叫李四,不管是张三缺胳膊少腿还是李四缺胳膊少腿都不会影响另外一个人。比较典型的就是Value(值)对象,如预定义类型Int32,Double,以及结构(struct),枚举(Enum)等。

系统提供的默认拷贝构造函数为浅拷贝,深拷贝必须自己定义。

这也解释了背景中的段错误原因及为什么要自己写拷贝构造函数.

三、什么是拷贝构造函数

拷贝构造函数,是一种特殊的构造函数,它由编译器调用来完成一些基于同一类的其他对象的构建及初始化。其唯一的参数(对象的引用)是不可变的(const类型)。也就是说,当用一个已经存在的对象为一个新的对象进行赋值时,首先要给新对象的数据成员分配空间资源以创建新对象,然后用源对象的成员值进行初始化。这个行为必须在对象构造的时候进行完成,而普通的构造函数无法完成这项工作,因此拷贝构造函数应运而生!

在C++中,下面三种对象需要拷贝的情况。因此,拷贝构造函数将会被调用。
  1). 一个对象以值传递的方式传入函数体
  2). 一个对象以值传递的方式从函数返回
  3). 一个对象需要通过另外一个对象进行初始化搜索
  以上的情况需要拷贝构造函数的调用。如果在前两种情况不使用拷贝构造函数的时候,就会导致一个指针指向已经被删除的内存空间。对于第三种情况来说,初始化和赋值的不同含义是构造函数调用的原因。事实上,拷贝构造函数是由普通构造函数和赋值操作赋共同实现的。

拷贝构造函数的标准写法如下:

class  Base

{

public:

Base(){} 
       Base( const Base &b){..}  //拷贝构造函数的参数必须是该对象的引用

}

进一步总结:

1) 拷贝构造函数的作用就是用来复制对象的,在使用这个对象的实例来初始化这个对象的一个新的实例。
2)如果不显式声明拷贝构造函数的时候,编译器也会生成一个默认的拷贝构造函数,只执行浅拷贝在一般的情况下,浅拷贝运行的也很好。

但是在遇到类有指针数据成员时就出现问题了:因为默认的拷贝构造函数是按成员拷贝构造,这导致了两个不同的指针(如ptr1=ptr2)指向了相同的 内存。当一个实例销毁时,调用析构函数free(ptr1)释放了这段内存,那么剩下的一个实例的指针ptr2就无效了,在被销毁的时候free(ptr2)就会出现错误了, 这相当于重复释放
一块内存两次。这种情况必须显式声明并实现自己的拷贝构造函数,来为新的实例的指针分配新的内存。

:

如何避免被C++默认拷贝构造函数忽悠?的更多相关文章

  1. C++ 默认拷贝构造函数 深度拷贝和浅拷贝

    C++类默认拷贝构造函数的弊端 C++类的中有两个特殊的构造函数,(1)无参构造函数,(2)拷贝构造函数.它们的特殊之处在于: (1) 当类中没有定义任何构造函数时,编译器会默认提供一个无参构造函数且 ...

  2. CPP_类默认函数:构造函数,拷贝构造函数,赋值函数和析构函数

    类默认函数:构造函数,拷贝构造函数,赋值函数和析构函数 // person.h #ifndef _PERSON_H_ #define _PERSON_H_ class Person{ public : ...

  3. C++有关拷贝构造函数(默认/浅/深拷贝构造函数)

    拷贝结构函数顾名思义就是复制对象. 先讲一下默认拷贝函数: 默认拷贝就是直接赋值,让程序调用默认拷贝结构函数. Student p1; Student p2 = p1//或者Student p2(p1 ...

  4. C++拷贝构造函数

    拷贝构造函数是一种特殊的构造函数,其定义为第一个参数为为本类型的一个引用或者是常引用,且无其它参数或者其它参数为默认值,例如下面的函数: X::X(const X&); X::X(X& ...

  5. C++拷贝构造函数(深拷贝,浅拷贝)

    对于普通类型的对象来说,它们之间的复制是很简单的,例如:int a=88;int b=a; 而类对象与普通对象不同,类对象内部结构一般较为复杂,存在各种成员变量.下面看一个类对象拷贝的简单例子. #i ...

  6. 一个CString的实现 拷贝构造函数的应用

    class CString { public: CString (char* s); CString(); ~CString(); private: char *str; int len; stati ...

  7. 【转】C++的拷贝构造函数深度解读,值得一看

    建议看原帖  地址:http://blog.csdn.net/lwbeyond/article/details/6202256 一. 什么是拷贝构造函数 首先对于普通类型的对象来说,它们之间的复制是很 ...

  8. C++拷贝构造函数详解(转载)

    一. 什么是拷贝构造函数 首先对于普通类型的对象来说,它们之间的复制是很简单的,例如: int a = 100; int b = a; 而类对象与普通对象不同,类对象内部结构一般较为复杂,存在各种成员 ...

  9. 【C++对象模型】构造函数语意学之二 拷贝构造函数

    关于默认拷贝构造函数,有一点和默认构造函数类似,就是编译器只有在[需要的时候]才去合成默认的拷贝构造函数. 在什么时候才是[需要的时候]呢? 也就是类不展现[bitwise copy semantic ...

随机推荐

  1. SpringMVC04controller中定义多个方法

    public class MyController extends MultiActionController { // 新增 方法修饰符要是public public ModelAndView ad ...

  2. iptables里filter表前面几个数字的意思

    一般的linux系统iptables配置文件filter表前面都带下面三行,但是具体是什么意思呢! *filter:INPUT ACCEPT [0:0]:FORWARD ACCEPT [0:0]:OU ...

  3. iOS用心学 UI基础之UIView

    一.引入UI 在实际开发中,基本的流程大致如下图所示: UI(User Interface)作为最基本的要点,也是非常重要的一部分,UI界面的美观直接决定着着用户的体验,苹果官方给开发中提供了非常强大 ...

  4. hdu2112(HDU Today 简单最短路)

    Problem Description 经过锦囊相助,海东集团终于度过了危机,从此,HDU的发展就一直顺风顺水,到了2050年,集团已经相当规模了,据说进入了钱江肉丝经济开发区500强.这时候,XHD ...

  5. Win7下Solr4.10.1和IK Analyzer中文分词

    1.下载IK中文分词压缩包IK Analyzer 2012FF_hf1,并解压到D:\IK Analyzer 2012FF_hf1: 2.将D:\IK Analyzer 2012FF_hf1\IKAn ...

  6. .net转php laraval框架学习系列(一) 环境搭建

    之前也没写过什么博客,可能文章结构比较混乱,想到那写到哪. 主要是把自己学习中的经验写下来. 为什么选择laravel框架,是因为laravel框架目前是Php最流行的框架,深入研究后发现和asp.n ...

  7. Android Framework------之Property子系统

    概述 Property是Android系统中一个重要的概念,在Android系统内,主要用于系统配置,以及不同服务间的简单信息分享.比如设备名字,蓝牙名字,编译信息,网络dns地址,以及其他的一些基本 ...

  8. 無心插柳的Linux學習者代言人——蔡德明

    誰是「蔡德明」恐怕沒有多少人知道,不過提到「鳥哥」這個稱號,在臺灣的Linux社群幾乎是無人不知無人不曉,蔡德明正是鳥哥的本名.鳥哥究竟多有名? 如果你是有意學習Linux的初學者,卻不知如何下手,1 ...

  9. 【原创】CLEVO P157SM外接鼠标键盘失灵解决:更换硅脂(附带最新跑分数据)

    作者批注:本文允许转载,并且希望给搜索未来人类.蓝天.CLEVO.更换硅脂或者任何有关关键字的朋友提供帮助. 原文地址:http://www.cnblogs.com/c4isr/p/3514140.h ...

  10. 转:fopen()函数

    1.2 文件的输入输出函数 键盘.显示器.打印机.磁盘驱动器等逻辑设备, 其输入输出都可以通过文件管理的方法来完成.而在编程时使用最多的要算是磁盘文件, 因此本节主要以磁盘文件为主, 详细介绍Turb ...