拷贝构造函数 订阅
拷贝构造函数,又称复制构造函数,是一种特殊的构造函数,它由编译器调用来完成一些基于同一类的其他对象的构建及初始化。其形参必须是引用,但并不限制为const,一般普遍的会加上const限制。此函数经常用在函数调用时用户定义类型的值传递及返回。拷贝构造函数要调用基类的拷贝构造函数和成员函数。如果可以的话,它将用常量方式调用,另外,也可以用非常量方式调用。 展开全文
拷贝构造函数,又称复制构造函数,是一种特殊的构造函数,它由编译器调用来完成一些基于同一类的其他对象的构建及初始化。其形参必须是引用,但并不限制为const,一般普遍的会加上const限制。此函数经常用在函数调用时用户定义类型的值传递及返回。拷贝构造函数要调用基类的拷贝构造函数和成员函数。如果可以的话,它将用常量方式调用,另外,也可以用非常量方式调用。
信息
属    于
一种特殊的构造函数
应用学科
计算机科学、测绘科学
形参形式
引用
中文名
拷贝构造函数
外文名
Copy Constructor
又    称
复制构造函数
拷贝构造函数概念
在C++中,下面三种对象需要调用拷贝构造函数(有时也称“复制构造函数”):1) 一个对象作为函数参数,以值传递的方式传入函数体;2) 一个对象作为函数返回值,以值传递的方式从函数返回;3) 一个对象用于给另外一个对象进行初始化(常称为赋值初始化);如果在前两种情况不使用拷贝构造函数的时候,就会导致一个指针指向已经被删除的内存空间。对于第三种情况来说,初始化和赋值的不同含义是拷贝构造函数调用的原因。事实上,拷贝构造函数是由普通构造函数和赋值操作符共同实现的。描述拷贝构造函数和赋值运算符的异同的参考资料有很多。通常的原则是:①对于凡是包含动态分配成员或包含指针成员的类都应该提供拷贝构造函数;②在提供拷贝构造函数的同时,还应该考虑重载"="赋值操作符号。拷贝构造函数必须以引用的形式传递(参数为引用值)。其原因如下:当一个对象以传递值的方式传一个函数的时候,拷贝构造函数自动的被调用来生成函数中的对象。如果一个对象是被传入自己的拷贝构造函数,它的拷贝构造函数将会被调用来拷贝这个对象这样复制才可以传入它自己的拷贝构造函数,这会导致无限循环直至栈溢出(Stack Overflow)。除了当对象传入函数的时候被隐式调用以外,拷贝构造函数在对象被函数返回的时候也同样的被调用。
收起全文
精华内容
下载资源
问答
  • 在C++中, 构造函数,拷贝构造函数,析构函数和赋值函数(赋值运算符重载)是最基本不过的需要掌握的知识。 但是如果我问你“拷贝构造函数的参数为什么必须使用引用类型?”这个问题, 你会怎么回答? 或许你会回答...
  • C++中一般创建对象,拷贝或赋值的方式有构造函数,拷贝构造函数,赋值函数这三种方法。下面就详细比较下三者之间的区别以及它们的具体实现 1.构造函数 构造函数是一种特殊的类成员函数,是当创建一个类的对象时,它...
  • 详解C++ 编写String 的构造函数、拷贝构造函数、析构函数和赋值函数  编写类String 的构造函数、析构函数和赋值函数,已知类String 的原型为: class String { public: String(const char *str = NULL); // 普通...
  • 1.什么是拷贝构造函数拷贝构造函数嘛,当然就是拷贝和构造了。(其实很多名字,只要静下心来想一想,就真的是顾名思义呀)拷贝又称复制,因此拷贝构造函数又称复制构造函数。百度百科上是这样说的:拷贝构造函数...
  • 拷贝构造函数是C++最基础的概念之一,大家自认为对拷贝构造函数了解么?请大家先回答一下三个问题:1. 以下函数哪个是拷贝构造函数,为什么?X::X(const X&); X::X(X); X::X(X&, int a=1); X::X(X&, int a=1, b=2);...
  • 以下几种情况会调用拷贝构造函数 1. 以一个对象初始化另一个对象 2. 函数以某个对象为参数 3. 函数返回某个对象 4. 初始化序列式容器的元素 2. void test2() { Test t1(1,2); Test t2; t2=t1;//调用的不是拷贝构造...
  • 拷贝和浅拷贝可以简单理解为:如果一个类拥有资源,当这个类的对象发生复制过程的时候,资源重新分配,这个过程就是深拷贝,反之,没有重新分配资源,就是浅拷贝
  • 本文主要介绍了拷贝构造函数和赋值运算符的区别,以及在什么时候调用拷贝构造函数、什么情况下调用赋值运算符。最后,简单的分析了下深拷贝和浅拷贝的问题。有需要的朋友可以看下
  • 拷贝构造函数

    千次阅读 2019-04-15 11:07:18
    拷贝构造函数,顾明思议,就是通过拷贝对象的方式创建一个新对象。拷贝构造函数有两种原型: book(book &b); book(const book &b); 这两种原型都是book类对象的引用。下面一种原型则规定在创建新对象的...

    拷贝构造函数,顾明思议,就是通过拷贝对象的方式创建一个新对象。拷贝构造函数有两种原型:
    book(book &b);
    book(const book &b);
    这两种原型都是book类对象的引用。下面一种原型则规定在创建新对象的时候不得修改被拷贝的对象,如果拷贝构造函数的参数不是对象的引用,则是不允许的。如下面这种构造函数形式则是无法编译通过的。
    book(book b);
    为什么拷贝构造函数的参数一定要是对象引用呢?
    我们可以想一下,如果不是引用,而是通过传值的方式将实参传递给形参,这中间本身就要经历一次对象的拷贝过程,而对象拷贝则必须调用拷贝构造函数,如此一来则会形成一个死循环,无解。所以拷贝构造函数的参数必须是对象的引用。

    拷贝构造函数除了能用对象引用这样的参数之外,同样也能有其他参数,但是其他参数必须给出默认值。例如下面这种拷贝构造函数声明方式。
    book (const book &b,price = 5.0);
    如果类的设计人员不在类中显示的声明一个拷贝构造函数,则系统会自定的为类生成一个拷贝构造函数,自动生成的拷贝构造函数功能简单,只能将源对象的所有成员变量一一复制给当前创建的对象。

    例1:
    class book
    {
    public:
        book(){}
        book(book &b);
        book(char* a, double p = 5.0);
        void display();
    private:
        double price;
        char * title;
    };
    
    book::book(book &b)
    {
        price = b.price;
        title = b.title;
    }
    
    book::book(char* a, double p)
    {
        title = a;
        price = p;
    }
    
    void book::display()
    {
        cout<<"The price of "<<title<<" is $"<<price<<endl;
    }
    

    在本例中的book类中就声明了一个拷贝构造函数book(book &b);当然这个拷贝构造函数跟系统默认生成的拷贝构造函数功能是一样的,也就只是实现了数据成员的对应拷贝功能。
    了解了拷贝构造函数的声明及定义方式,我们再来看一下我们在设计类的时候,什么时候才需要设计拷贝构造函数,我们先来看下面一个例子,相信看完之后会有一定领会,之后再来揭晓答案

    例2:
    #include<iostream>
    using namespace std;
    class Array
    {
    public:
    	Array(){length = 0;num = NULL;}
    	Array(int *A,int n);
    	void setnum(int vallue,int index);
    	int *getaddress();
    	int getaddress();
    	void display();
    private:
    	int length;
    	int *num;
    };
    Array::Array(int *A,int n)
    {
    	num  = new int [n];
    	length = n;
    	for (int i = 0;i < n; i++)
    		num[i] = A[i];
    }
    void Array::setnum(int value,int index)
    {
    	if(index < length)
    		num[index] = value;
    	else
    		cout<<"index out of range!"<<endl;
    }
    void Array::display()
    {
    	for(int i = 0;i < length;i++)
    		cout<<num[i]<<" ";
    	cout<<endl;
    }
    int *Arry::getaddress()
    {
    	return num;
    }
    int main()
    {
    	int A[5] = {1,2,3,4,5};
    	Array arr1(A,5);
    	arr1.display();
    	Array arr2(arr1);
    	arr2.display();
    	arr2.setnum(8,2);
    	arr2.display();
    	arr1.display();
    	cout<<arr1.getaddress()<<" "<<arr2.getaddress()<<endl;
    	return 0;
    }
    运行结果如下:
    1 2 3 4 5
    1 2 3 4 5
    1 2 8 4 5
    1 2 8 4 5
    00331F58 00331F58
    

    在本例中,我们重新定义了一个Array类,可以理解为一个整形数组类,这个类中我们定义了两个成员变量:整形指针num和数组长度length。

    类中定义了一个默认构造函数,声明了一个带参构造函数。默认构造函数很简单,带参构造函数则是用于将一个已有的数组全部拷贝给类对象。

    除了两个构造函数之外,我们还定义四个成员函数,一个是用于修改数组中数值的setnum函数、一个打印数组中所有元素的display函数、一个返回数组首地址的函数getaddress和一个返回数组长度的函数getlength。除了默认构造函数之外和getlength函数之外,所有的函数在类外都有定义。

    接下来我们看一下主函数。主函数中,我们先定义了一个数组,包含五个元素,分别是从1到5。之后用Array类创建对象arr1,并且用A数组初始化对象arr1,此时arr1对象相当于拥有一个数组,该数组包含5个元素,打印出来的结果是“1 2 3 4 5 ”,没有问题。之后用arr1对象初始化arr2对象,因为我们在类中没有显示地定义一个拷贝构造函数,因此系统会自动为我们生成一个拷贝构造函数,该拷贝构造函数的定义如下:
    Array::Array(Array &a)
    {
    length = a.length;
    num = a.num;
    }
    通过系统自动生成的拷贝构造函数完成arr2对象的创建,同样的arr2也是有5个元素的数组,打印出来的结果是“1 2 3 4 5 ”,同样没有问题。

    之后我们调用成员函数setnum,将arr2对象下标为2的元素修改为8(原先是3)。此时打印arr2中数组元素,结果为“1 2 8 4 5 ”,正确,arr2第三个元素确实被修改掉了。

    后我们再调用arr1.display(),奇怪的事情发生了,它的打印结果竟然也是“1 2 8 4 5 ”!我们之前并未修改过第三个元素的值的,这是怎么一回事呢?不急,我们再来看一下最后一句“cout<<arr1.getaddress()<<" "<<arr2.getaddress()<<endl;”其显示结果竟然是一样的!看到这里是不是有些明白了上面的问题呢?很明显,arr1和arr2所指向的数组是同一个数组,在内存中的位置是一致的,因此当我们利用对象arr2去修改数组中第三个元素的数值的时候,arr1中的数组也被修改了,其实它们本来就是使用的是同一个内存中的数组而已.

    这问题是怎么产生的呢?不难想到拷贝构造函数参数为引用,系统自动生成的拷贝构造函数功能简单,只是将arr1的数组首地址直接赋值给arr2的数组首地址,也即num = a.num;这必然导致两个对象指向同一块内存。既然问题出在系统自动生成的拷贝构造函数上,自然要从拷贝构造函数上下手了。下面我们将正确的程序展示如例3。

    例3:
    #include<iostream>
    using namespace std;
    class Array
    {
    public:
    	Array(){length = 0;num = NULL;}
    	Array(int *A,int n);
    	Array(Array &a);
    	void setnum(int value,int index);
    	int *getaddress();
    	void display();
    	int getlength(){return length;}
    private:
    	int length;
    	int *num;
    };
    Array::Array(Array&a)
    {
    	if(a.num!=NULL)
    	{
    		length = a.length;
    		num = new int [length];
    		for(int i = 0;i < length;i++)
    			num[i] = a.num[i];
    	}
    	else
    	{
    		length = 0;
    		num = 0;
    	}
    }
    Array::Array(int *A,int n)
    {
    	num = new int [n];
    	length = n;
    	for(int i = 0;i < n;i++)
    		num[i] = A[i];
    }
    void Array::setnum(int value,int index)
    {
    	if(index <length)
    		num[index] = value;
    	else
    		cout<<"index out of range!"<<endl;
    }
    
    void Array::display()
    {
    	for (int i = 0;i < length;i++)
    		cout<<num[i]<<" ";
    	cout<<endl;
    }
    int *Array::getaddress()
    {
    	return num;
    }
    
    int main()
    {
    	int A[5] = {1,2,3,4,5};
    	Array arr1(A,5);
    	arr1.display();
    	Arry arr2(arr1);
    	arr2.display();
    	arr2.setnum(8,2);
    	arr2.display();
    	arr1.display();
    	cout<<arr1.getaddress();" "<<arr2.getaddress()<<endl;
    	return 0;
    }
    
    运行结果如下:
    1 2 3 4 5
    1 2 3 4 5
    1 2 8 4 5
    1 2 3 4 5
    00311F58 00487268
    

    看例3运行结果,如此一来,程序运行结果正确,而且两个对象arr1和arr2所指向的内存空间也是不一样的。我们在例3中自己定义了一个拷贝构造函数,并且开辟了一个新的空间用于存储数据。如此一来当然是不会有问题的了。本例中所介绍的是一个非常微妙的错误,在程序设计过程中,一定要加以避免。

    从这个例子中我们是不是领会到了什么呢?通常,如果一个类中包含指向动态分配存储空间的指针类型的成员变量时,就应该为这个类设计一个拷贝构造函数,除了需要设计一个拷贝构造函数之外,还需要为它添加一个赋值操作符重载函数(即重载“=”操作符,这将会在操作符重载那一章加以介绍)。

    由于类会自动生成拷贝构造函数,因此有些时候为了不让对象发生拷贝行为,我们可以显示声明一个拷贝构造函数,并将其设置为private属性。这跟通过将默认构造函数设置成private属性限制对象的创建时一样的道理。当然,禁止对象发生拷贝的需求较少,如果有这样的需求的话,知道还可以这么做就足够了,这是一个类设计的技巧。

    展开全文
  • 拷贝构造函数.rar

    2020-04-03 14:40:05
    本资源通过代码实例详细介绍在C++程序中调用拷贝构造函数的三种情境,每行代码配有详细的注释,帮助学习者直观理解
  • 本篇文章是对C++中构造函数、拷贝构造函数、赋值操作符、析构函数的调用过程进行了总结与分析,需要的朋友参考下
  • php中拷贝构造函数、赋值运算符重载方法, 需要的朋友可以参考下
  • 1.什么是拷贝构造函数: CA(const CA& C)就是我们自定义的拷贝构造函数。可见,拷贝构造函数是一种特殊的构造函数,函数的名称必须和类名称一致,它的唯一的一个参数是本类型的一个引用变量,该参数是const类型...

    1.什么是拷贝构造函数
    CA(const CA& C)就是我们自定义的拷贝构造函数。可见,拷贝构造函数是一种特殊的构造函数,函数的名称必须和类名称一致,它的唯一的一个参数是本类型的一个引用变量,该参数是const类型,不可变的。例如:类X的拷贝构造函数的形式为X(X& x)。
    当用一个已初始化过了的自定义类类型对象去初始化另一个新构造的对象的时候,拷贝构造函数就会被自动调用。也就是说,当类的对象需要拷贝时,拷贝构造函数将会被调用。以下情况都会调用拷贝构造函数:
    ① 程序中需要新建立一个对象,并用另一个同类的对象对它初始化,如前面介绍的那样。
    ② 当函数的参数为类的对象时。在调用函数时需要将实参对象完整地传递给形参,也就是需要建立一个实参的拷贝,这就是按实参复制一个形参,系统是通过调用复制构造函数来实现的,这样能保证形参具有和实参完全相同的值。
    ③ 函数的返回值是类的对象。在函数调用完毕将返回值带回函数调用处时。此时需要将函数中的对象复制一个临时对象并传给该函数的调用处。如
    Box f( ) //函数f的类型为Box类类型
    {Box box1(12,15,18);
    return box1; //返回值是Box类的对象
    }
    int main( )
    {Box box2; //定义Box类的对象box2
    box2=f( ); //调用f函数,返回Box类的临时对象,并将它赋值给box2
    }
    如果在类中没有显式地声明一个拷贝构造函数,那么,编译器将会自动生成一个默认的拷贝构造函数,该构造函数完成对象之间的位拷贝。位拷贝又称浅拷贝,后面将进行说明。
      自定义拷贝构造函数是一种良好的编程风格,它可以阻止编译器形成默认的拷贝构造函数,提高源码效率。
    浅拷贝和深拷贝
      在某些状况下,类内成员变量需要动态开辟堆内存,如果实行位拷贝,也就是把对象里的值完全复制给另一个对象,如A=B。这时,如果B中有一个成员变量指针已经申请了内存,那A中的那个成员变量也指向同一块内存。这就出现了问题:当B把内存释放了(如:析构),这时A内的指针就是野指针了,出现运行错误。
      深拷贝和浅拷贝可以简单理解为:如果一个类拥有资源,当这个类的对象发生复制过程的时候,资源重新分配,这个过程就是深拷贝,反之,没有重新分配资源,就是浅拷贝。
    2.C++拷贝构造函数的几个细节

    1) 以下函数哪个是拷贝构造函数,为什么?
    1.X::X( const X&);
    2.X::X(X);
    3.X::X(X&, int a=1);
    4.X::X(X&, int a=1, b=2);
    解答:1) 对于一个类X,如果一个构造函数的第一个参数是下列之一:
    a) X&
    b) const X&
    c) volatile X&
    d) const volatile X&
    且没有其他参数或其他参数都有默认值,那么这个函数是拷贝构造函数.
    1.X::X( const X&); //是拷贝构造函数
    2.X::X(X&, int =1); //是拷贝构造函数

    2) 一个类中可以存在多于一个的拷贝构造函数吗?
    解答:类中可以存在超过一个拷贝构造函数,
    1.class X {
    2.public :
    3. X( const X&);
    4. X(X&); // OK
    5.};
    注意,如果一个类中只存在一个参数为X&的拷贝构造函数,那么就不能使用const X或volatile X的对象实行拷贝初始化.
    1.class X {
    2.public :
    3. X();
    4. X(X&);
    5.};
    6.
    7.const X cx;
    8.X x = cx; // error
    如果一个类中没有定义拷贝构造函数,那么编译器会自动产生一个默认的拷贝构造函数.
    这个默认的参数可能为X::X(const X&)或X::X(X&),由编译器根据上下文决定选择哪一个.
    默认拷贝构造函数的行为如下:
    默认的拷贝构造函数执行的顺序与其他用户定义的构造函数相同,执行先父类后子类的构造.
    拷贝构造函数对类中每一个数据成员执行成员拷贝(memberwise Copy)的动作.
    a)如果数据成员为某一个类的实例,那么调用此类的拷贝构造函数.
    b)如果数据成员是一个数组,对数组的每一个执行按位拷贝.
    c)如果数据成员是一个数量,如int,double,那么调用系统内建的赋值运算符对其进行赋值.
    3) 拷贝构造函数不能由成员函数模版生成.
    struct X {
    template < typename T>
    X( const T& ); // NOT copy ctor, T can't be X
    template < typename T>
    operator=( const T& ); // NOT copy ass't, T can't be X
    };
    原因很简单, 成员函数模版并不改变语言的规则,而语言的规则说,如果程序需要一个拷贝构造函数而你没有声明它,那么编译器会为你自动生成一个.所以成员函数模版并不会阻止编译器生成拷贝构造函数, 赋值运算符重载也遵循同样的规则
    3.拷贝构造函数与赋值函数的异同:
    1) 拷贝构造,是一个的对象来初始化一片内存区域,这片内存区域就是你的新对象的内存区域赋值运算,对于一个已经被初始化的对象来进行operator=操作
    class A;
    A a;
    A b=a; //拷贝构造函数调用
    //或
    A b(a); //拷贝构造函数调用
    ///
    A a;
    A b;
    b =a; //赋值运算符调用
    你只需要记住,在C++语言里,
    String s2(s1);
    String s3 = s1;
    只是语法形式的不同,意义是一样的,都是定义加初始化,都调用拷贝构造函数。
    2) 一般来说是在数据成员包含指针对象的时候,应付两种不同的处理需求的 一种是复制指针对象,一种是引用指针对象 copy大多数情况下是复制,=则是引用对象的
    例子:
    class A
    {
    int nLen;
    char * pData;
    }
    显然
    A a, b;
    a=b的时候,对于pData数据存在两种需求
    第一种copy
    a.pData = new char [nLen];
    memcpy(a.pData, b.pData, nLen);
    另外一种(引用方式):
    a.pData = b.pData

    通过对比就可以看到,他们是不同的
    往往把第一种用copy使用,第二种用=实现
    你只要记住拷贝构造函数是用于类中指针,对象间的COPY
    3) 拷贝构造函数首先是一个构造函数,它调用的时候产生一个对象,是通过参数传进来的那个对象来初始化,产生的对象。
    operator=();是把一个对象赋值给一个原有的对象,所以如果原来的对象中有内存分配要先把内存释放掉,而且还要检查一下两个对象是不是同一个对象,如果是的话就不做任何操作。
    还要注意的是拷贝构造函数是构造函数,不返回值
    而赋值函数需要返回一个对象自身的引用,以便赋值之后的操作
    4) 在形式上
    类名(形参表列); //普通构造函数的声明,如Box(int h,int w,int len);
    类名(类名& 对象名); //复制构造函数的声明,如Box(Box &b);
    5) 在建立对象时,实参类型不同。系统会根据实参的类型决定调用普通构造函数或复制构造函数。如:
    Box box1(12,15,16); //实参为整数,调用普通构造函数
    Box box2(box1); //实参是对象名,调用复制构造函数

    拷贝构造函数,是一种特殊的构造函数,它由编译器调用来完成一些基于同一类的其他对象的构建及初始化。其唯一的参数(对象的引用)是不可变的(const类型)。此函数经常用在函数调用时用户定义类型的值传递及返回。拷贝构造函数要调用基类的拷贝构造函数和成员函数。

    在C++中,下面三种对象需要调用拷贝构造函数:   

    1) 一个对象以值传递的方式传入函数体;   

    2) 一个对象以值传递的方式从函数返回;   

    3) 一个对象需要通过另外一个对象进行初始化;   

    如果在前两种情况不使用拷贝构造函数的时候,就会导致一个指针指向已经被删除的内存空间。对于第三种情况来说,初始化和赋值的不同含义是构造函数调用的原因。事实上,拷贝构造函数是由普通构造函数和赋值操作符共同实现的。描述拷贝构造函数和赋值运算符的异同的参考资料有很多。   

    拷贝构造函数不可以改变它所引用的对象,其原因如下:当一个对象以传递值的方式传一个函数的时候,拷贝构造函数自动的被调用来生成函数中的对象。如果一个对象是被传入自己的拷贝构造函数,它的拷贝构造函数将会被调用来拷贝这个对象这样复制才可以传入它自己的拷贝构造函数,这会导致无限循环直至栈溢出(Stack Overflow)。除了当对象传入函数的时候被隐式调用以外,拷贝构造函数在对象被函数返回的时候也同样的被调用。

    展开全文
  • 拷贝构造函数详解

    千次阅读 2020-08-09 15:24:51
    拷贝构造函数详解 拷贝构造函数 int main() { int a; int b = 10; int c(b); return 0 } //结果就是b和c的结果都是10 int main() { int a; int b = 10; int c(b); string s1("hello"); string s2(s1); ...

    拷贝构造函数详解

    拷贝构造函数

    int main()
    {
    	int a;
    	int b = 10;
    	int c(b); 
    	return 0
    }
    //结果就是b和c的结果都是10
    
    int main()
    {
    	int a;
    	int b = 10;
    	int c(b);
    
    	string s1("hello");    
    	string s2(s1);     //用s1去构造s2,然后s1和s2的内容就一模一样了
    	return 0
    }
    //但是通过监视可以看出,s1和s2的地址是一样的,也就是说s1和s2是同一块空间,那么在进行析构的时候就会出现问题,也就是说,同一块空间析构了两次
    

    在现实生活中,可能存在一个与你一样的自己,我们称其为双胞胎.那在创建对象时,可否创建一个与一个对象一某一样的新对象呢?

    概念
    • 拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用
    • 为什么要给const进行修饰,因为我们在进行拷贝构造的时候,只是希望吧d1中的内容给d2,我们并不希望试图去改变d1中的内容,但是或许可能会不小心修改了d1中的内容,所以我们为了方便,给出const进行修饰,这样的话,就算我们不小心操作失误,d1中的内容也不会被改变的
    #include<iostream>
    using namespace std;
    class Date
    {
    public:
    	Date(int year=2020, int month=5, int day=5)
    	{
    		_year = year;
    		_month = month;
    		_day = day;
    		cout << "Date(int,int ,int):" << this << endl;
    		//看构造的是哪一个对象
    	}
    
    	~Date()
    	{
    		//对于日期类来说,这里面没有什么资源是需要去释放的
    		//所以对于日期类来说,给不给析构函数其实都没有什么影响
    		cout << "~Date():" << this << endl;
    		//看析构的是哪一个对象
    	}
    private:
    	int _year;
    	int _month;
    	int _day;
    };
    int main()
    {
    	Date d1(2020, 5, 5);
    	Date d2(d1);
    
    	return 0;
    }
    
    拷贝构造函数的特征
    • 拷贝构造函数是构造函数的一个重载形式。
    • 拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用(原因在于,如果是利用值得方式来进行传递的话,就会开辟一段临时的空间,那么这个临时的空间也是需要构造的,那么这个临时的空间就会去调用构造函数,然后移植重复进行下去,就会造成无穷的递归调用)
      在这里插入图片描述
    #include<iostream>
    using namespace std;
    class Date
    {
    public:
    	//构造函数
    	Date(int year = 2020, int month = 5, int day = 5)
    	{
    		_year = year;
    		_month = month;
    		_day = day;
    		cout << "Date(int,int ,int):" << this << endl;
    		//看构造的是哪一个对象
    	}
    
    	//拷贝构造函数
    	Date(const Date& d)
    	{
    		_year = d._year;
    		_month = d._month;
    		_day = d._day;
    		cout << "Date(const Date&d):" << this << endl;
    	}
    
    	//析构函数
    	~Date()
    	{
    		//对于日期类来说,这里面没有什么资源是需要去释放的
    		//所以对于日期类来说,给不给析构函数其实都没有什么影响
    		cout << "~Date():" << this << endl;
    		//看析构的是哪一个对象
    	}
    private:
    	int _year;
    	int _month;
    	int _day;
    };
    void TestDate()
    {
    	Date d1(2020, 5, 5);
    	Date d2(d1);
    }
    int main()
    {
    	TestDate();
    	return 0;
    }
    
    由析构的结果可以看出,先析构的是d2的内容,再去析构d1的内容,也就是说,先构造的后析构,后构造的先析构,原因在于:

    在使用构造函数和析构函数时,需要特别注意对它们的调用时间和调用顺序。在一般情况下,调用析构函数的次序正好与调用构造函数的次序相反:最先被调用的构造函数,其对应的(同一对象中的)析构函数最后被调用,而最后被调用的构造函数,其对应的析构函数最先被调用。可以简记为:先构造的后析构,后构造的先析构,它相当于一个栈,先进后出。下面归纳一下什么时候调用构造函数和析构函数:

    • 在全局范围中定义的对象(即在所有函数之外定义的对象),它的构造函数在文件中的所有函数(包括main函数)执行之前调用。但如果一个程序中有多个文件,而不同的文件中都定义了全局对象,则这些对象的构造函数的执行顺序是不确定的。当main函数执行完毕或调用exit函数时(此时程序终止),调用析构函数。
    • 如果定义的是局部自动对象(例如在函数中定义对象),则在建立对象时调用其构造函数。如果函数被多次调用,则在每次建立对象时都要调用构造函数。在函数调用结束、对象释放时先调用析构函数。
    • 如果在函数中定义静态(static)局部对象,则只在程序第一次调用此函数建立对象时调用构造函数一次,在调用结束时对象并不释放,因此也不调用析构函数,只在main函数结束或调用exit函数结束程序时,才调用析构函数。
    • 局部变量存在栈中,当函数退出时要一个个销毁,先进后出的原理
    若未显示定义,系统生成默认的拷贝构造函数。默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝我们叫做浅拷贝或者值拷贝

    在这里插入图片描述

    #include<iostream>
    using namespace std;
    class Date
    {
    public:
    	//构造函数
    	Date(int year = 2020, int month = 5, int day = 5)
    	{
    		_year = year;
    		_month = month;
    		_day = day;
    		cout << "Date(int,int ,int):" << this << endl;
    		//看构造的是哪一个对象
    	}
    
    	//析构函数
    	~Date()
    	{
    		//对于日期类来说,这里面没有什么资源是需要去释放的
    		//所以对于日期类来说,给不给析构函数其实都没有什么影响
    		cout << "~Date():" << this << endl;
    		//看析构的是哪一个对象
    	}
    private:
    	int _year;
    	int _month;
    	int _day;
    };
    void TestDate()
    {
    	Date d1(2020, 5, 5);
    	Date d2(d1);
    }
    int main()
    {
    	TestDate();
    	return 0;
    }
    
    • 这个代码我们没有显示的声明拷贝构造函数,但是d2对象仍然创建成功了,d2的内容和d1的内容是一摸一样的,同时,代码并没有什么问题,也没有崩溃,但是有些时候,编译器自动声明的拷贝构造函数是有问题的,比如针对string类来说
    那么编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,我们还需要自己实现吗?当然像日期类这样的类是没必要的。那么下面的类呢?验证一下试试?

    在这里插入图片描述

    • 也就是说s1和s2其实指向的是同一块堆内存空间,但是这么看来,是存在着很大的问题的,问题在于:s1和s2是栈上面的两个对象,这两个对象指向的是同一块地址空间,那么当在进行资源释放的时候,会先去释放s2,然后再去释放s1,那么在释放s2的时候,这一块空间肯定就会被释放掉,但是,在s2释放掉资源的时候,s1是不直到这块空间已经被释放掉了,s1仍然会去释放这块空间,那么这个时候,就会引起代码的崩溃
      在这里插入图片描述
      在这里插入图片描述
      在这里插入图片描述
      在这里插入图片描述
    #include<iostream>
    #include<stdlib.h>
    #include<string.h>
    using namespace std;
    class String
    {
    public:
    	String(const char* str = "")
    	{
    		cout << "String(const char* ):" << this << endl;
    		if (nullptr == str)
    			str = "";
    
    		_str = (char*)malloc(strlen(str) + 1);
    		strcpy(_str, str);
    	}
    
    	// 拷贝构造函数必须显式提供
    
    	~String()
    	{
    		cout << "~String():" << this << endl;
    		free(_str);
    		_str = nullptr;
    	}
    
    private:
    	char* _str;
    };
    
    void TestString()
    {
        String s1("hello");
        String s2(s1);
    }
    
    int main()
    {
    	TestString();
    	return 0;
    }
    
    • 上面写的这个代码是有问题的代码,因为用s1去拷贝构造s2的话,通过监视看,s1和s2公用的是用一块内存空间,也就是说两个变量的地址是一样的,又因为先构造的后析构,后构造的析构,所以在释放空间的时候,是需要先去析构s2的,那么当s2析构完成了之后,那么s1就相当于是野指针了,再去析构s1的话,就会出现问题,从而代码崩溃(因为编译器所提供的默认的拷贝构造函数,是把s1中的内容原封不动的拷贝到s2中去的,当然包括s1中指针的地址,所以两个变量公用同一块堆上的内容)
      在这里插入图片描述
    展开全文
  • C++ 拷贝构造函数和赋值构造函数

    万次阅读 多人点赞 2019-04-06 16:43:35
    在C++中复制控制是一个比较重要的话题,主要包括复制构造函数、重载赋值操作符、析构函数这三部分,这三个函数是一致的,如果类需要析构函数,则它也需要复制操作符 和 复制构造函数,这个规则被称为 C++的“三法则...

            在C++中复制控制是一个比较重要的话题,主要包括复制构造函数、重载赋值操作符、析构函数这三部分,这三个函数是一致的,如果类需要析构函数,则它也需要复制操作符 和 复制构造函数,这个规则被称为 C++的“三法则”。如果需要手动定义了其中了一个,那么另外的两个也需要定义,通常在存在指针或者前期相关操作的情况下,都需要手动的定义。
            复制构造函数与重载赋值操作符实现的大题相同,如果没有手动的实现,那么编译器会自动生成一个,而且这两个函数的参数也是一致的,是不能够改变的。析构函数相比前面的两个存在一个巨大的差别,就是无论我们是否定义这个函数,编译器都会自动生成一个析构函数。析构函数主要是完成对象的释放操作。
            复制构造函数与重载赋值操作符在没有定义的情况下,编译器会为我们生成一个,这说明这两个函数是一个类必不可少的部分。由此可知如果一个类没有定义任何的东西,编译器也会帮助我们生成下面的4个函数:
    1、一个构造函数,也就是所谓的类名比如classname(),这是在没有定义构造函数时,编译器会自动生成的。
    2、析构函数,
    3、复制构造函数。
    4、重载赋值操作符。
    假设存在一个类Base;
       class Base 
        {
            public:
                Base(); //构造函数
                Base(const Base &);  // 复制构造函数
                Base & operator = (const Base &);   // 赋值操作符
                ~Base();   // 析构函数
            private:
                .......
        };

    现在对复制构造函数和赋值操作符做个详细的说明,下面是一个将要使用的例子;

    class CExample
    {
    public :
         CExample(){pBuffer=NULL; nSize=0;}   //构造函数
         ~CExample(){delete pBuffer;}   // 析构函数
        void Init(int n){ pBuffer=new char [n]; nSize=n;}
    private :
        char *pBuffer; //类的对象中包含指针,指向动态分配的内存资源
        int nSize;
    };
        这个类的主要特点是包含指向其他资源的指针。 pBuffer指向堆中分配的一段内存空间。 
    一、拷贝构造函数
    int main(int argc, char * argv[])
    {
         CExample A;
         A.Init40);
        
         CExample B=A; //把B初始化为A的副本
         ...
    }
        B = A ; 此语句的具体过程:首先建立对象theObjtwo,并调用其构造函数,然后成员被拷贝。
        语句"CExample B=A;"  用 A 初始化 B。 其完成方式是内存拷贝,复制所有成员的值。 完成后,A.pBuffer = B.pBuffer,  即它们将指向同样的地方,指针虽然复制了,但所指向的空间并没有复制,而是由两个对象共用了。这样不符合要求,对象之间不独立了,并为空间的删除带来隐患。 所以需要采用必要的手段(拷贝构造函数)来避免此类情况。 
     

    拷贝构造函数的格式为 : 构造函数名(对象的引用)  提供了拷贝构造函数后的CExample类定义为: 
    class CExample
    {
    public :
         CExample(){pBuffer=NULL; nSize=0;}  //构造函数
         ~CExample(){delete pBuffer;}   // 析构函数
         CExample(const CExample&); //拷贝构造函数
        void Init(int n){ pBuffer=new char [n]; nSize=n;}
    private :
        char *pBuffer; //类的对象中包含指针,指向动态分配的内存资源
        int nSize;
    };

     //拷贝构造函数的定义
    CExample::CExample(const CExample& RightSides)
    {
         nSize=RightSides.nSize;    //复制常规成员
         pBuffer=new char [nSize];    //复制指针指向的内容
         memcpy(pBuffer,RightSides.pBuffer,nSize*sizeof (char ));
    }
         这样,定义新对象,并用已有对象初始化新对象时,即执行语句“CExample B=A; ” 时,CExample(const CExample& RightSides)将被调用,而已有对象用别名RightSides传给构造函数,以用来作复制。原则上,应该为所有包含动态分配成员的类都提供拷贝构造函数。 

    拷贝函数被调用的情况有:
    1,定义新对象,并用已有对象初始化新对象时; 即执行语句“CExample B=A; ” 时(定义对象时使用赋值初始化);
    2,当对象直接作为参数传给函数时,函数将建立对象的临时拷贝,这个拷贝过程也将调同拷贝构造函数。 
        例如:
            BOOL testfunc(CExample obj)
            {
                 //针对obj的操作实际上是针对复制后的临时拷贝进行的
            }
            testfunc(theObjone); //对象直接作为参数,拷贝函数将被调用;
    3,当函数中的局部对象被返回给函数调者时,也将建立此局部对象的一个临时拷贝,拷贝构造函数也将被调用 ;
        例如:
        CTest func()
        {
             CTest   theTest;
             return   theTest
         }  

    二、赋值符的重载 
        下面的代码与上例相似
    int main(int argc, char * argv[])
    {
         CExample A;
         A.Init(40);
        
         CExample C;
         C.Init(60);


          //现在需要一个对象赋值操作,被赋值对象的原内容被清除,并用右边对象的内容填充。
         C = A;
        return 0;
    }
         也用到了"="号,但与上面的例子中语句“ CExample B=A;  ” 不同“ CExample B=A;  ”语句中的 "=" 在对象声明语句中,表示初始化。更多时候,这种初始化也可用括号表示。 例如 CExample B(A); 

         而本例子中,"=" 表示赋值操作。将对象 A 的内容复制到对象C;,这其中涉及到对象C 原有内容的丢弃,新内容的复制。 但"="的缺省操作只是将成员变量的值相应复制。旧的值被自然丢弃。 由于对象内包含指针,将造成不良后果:指针的值被丢弃了,但指针指向的内容并未释放。指针的值被复制了,但指针所指内容并未复制。 因此,包含动态分配成员的类除提供拷贝构造函数外,还应该考虑重载"="赋值操作符号。 

    类定义变为: 
    class CExample
    {
    public :
         CExample(){pBuffer=NULL; nSize=0;}  //构造函数
         ~CExample(){delete pBuffer;}   // 析构函数
         CExample(const CExample&); //拷贝构造函数
         CExample& operator = (const CExample&); //赋值符重载
        void Init(int n){ pBuffer=new char [n]; nSize=n;}
    private :
        char *pBuffer; //类的对象中包含指针,指向动态分配的内存资源
        int nSize;
    };
    //赋值操作符重载
    CExample & CExample::operator = (const CExample& RightSides)
    {
         nSize=RightSides.nSize; //复制常规成员
        char *temp=new char [nSize]; //复制指针指向的内容 
         memcpy(temp,RightSides.pBuffer,nSize*sizeof (char ));

        delete []pBuffer; //删除原指针指向内容   (将删除操作放在后面,避免X=X特殊情况下,内容的丢失)
         pBuffer=temp;    //建立新指向
        return *this 
    }
    三、拷贝构造函数使用赋值运算符重载的代码。 
    CExample::CExample(const CExample& RightSides)
    {
         pBuffer=NULL;
         *this =RightSides      //调用重载后的"="
    }

        为了更好地理解拷贝构造函数   
     1、为什么要有拷贝构造函数,它跟构造函数有什么区别?
           答:拷贝构造函数其实也是构造函数,只不过它的参数是const 的类自身的对象的引用。如果类里面没有指针成员(该指针成员指向动态申请的空间),是没有必要编写拷贝构造函数的 。     我们知道,如果有一个类CObj,它已经产生了一个对象ObjA,现在又用CObj去创建ObjB,如果程序中使用语句ObjB = ObjA; 也就是说直接使用ObjA的数据给ObjB赋值。这对于一般的类,没有任何问题,但是如果CObj里面有个char * pStr的成员,用来存放动态申请的字符串的地址,在ObjA中使用new 方法动态申请了内存并让ObjA.pStr指向该申请的空间,在OjbB = OjbA之后,ObjA.pStr和ObjB.pStr将同时指向那片空间,这样到导致了谁也不知道到底该由谁来负责释放那块空间,很有可能导致同一块内存被释放两次。     使用拷贝构造函数,先申请ObjA.pStr所指向的空间大小的空间,然后将空间内容拷贝过来,这样就不会同时指向同一块内存,各自有各自申请的内存,各自负责释放各自申请的内存,从而解决了刚才的问题。所以这里的“拷贝”拷贝的是动态申请的空间的内容,而不是类本身的数据。另外注意到,拷贝构造函数的参数是对象的引用,而不是对象的指针。至于为什么要用引用,不能够用指针暂时还没有搞明白,等搞明白了再说。    
    2、为什么要对=赋值操作符进行重载?
        答:接上面的例子,用户在使用语句ObjB = ObjA的时候,或许ObjB的pStr已经指向了动态申请的空间,如果直接简单将其指向的地址覆盖,就会导致内存泄露,所以需要对=赋值操作符进行重载,在重载函数中判断pStr如果已经指向了动态申请的空间,就先将其释放。    
    3、拷贝构造函数和=赋值操作符重载的关系。
        答:从原文的例子中可以看出,=赋值操作符重载比拷贝构造函数做得要多,它除了完成拷贝构造函数所完成的拷贝动态申请的内存的数据之外,还释放了原本自己申请的内存空间。所以原文最后给出的拷贝构造函数的实现可以使用=赋值操作符的重载来完成。    
    4、拷贝构造函数何时被调用?
        a.对象的直接赋值也会调用拷贝构造函数  ;
        b.函数参数传递只要是按值传递也调用拷贝构造函数;
        c.函数返回只要是按值返回也调用拷贝构造函数。 

     四、拷贝构造函数 和 赋值运算符重载 为什么要使用引用? 
        
    首先先说下基类 和 派生类的关系:
        例如:
            class Derived:public Base
            {
                public:
                  .....
                private:
                    .......
            };
     
     不同继承方式的基类和派生类特性

    继承方式基类特性派生类特性
    公有继承publicpublic
    protected
    private
    protected
    不可访问
    私有继承publicprivate
    protected
    private
    private
    不可访问
    保护继承publicprotected
    protected
    private
    protected
    不可访问


            首先,派生类对象的引用初始化基类引用。多态性的动态绑定中存在两个条件:1,必须是virtual 函数(虚函数);2, 必须是通过基类的引用或基类的指针进行成员函数的调用。
         

        由于派生类中存在基类的成员,也就相当于一个派生类对象中包含了一个基类对象,所以可以采用一个基类引用来绑定一个派生类对象。引用实质上是针对一块内存区域,引用是一个标号,是这块内存区域的一个名字,一个引用与一块内存区域绑定,因为派生对象中存在基类部分,可以认为派生对象的区域中存在基类对象,这时可用基类的引用来表明这块内存区域,即采用一个基类的别名来表示(绑定)这段内存区域,派生对象的地址(这段内存)以及内容都没有发生改变,也没有重现创造出一个新的对象,基类的引用还是指向这个派生对象。对于指针的分析方式相似。因此可以采用基类的引用绑定派生类对象。

       

        但是如何实现派生类对象到基类的转换呢?

        这时候的转换与前面的绑定存在很大的差别,因为这是重新分配一个基类对象,而不再是引用问题,不再是绑定问题,是依据一个派生类对象生成一个新的基类对象。因为派生类对象中存在一个基类对象基本的信息,完全可以生成一个基类对象,完全将此过程看作是一个初始化或者赋值的问题。也就是采用派生类创建一个新的对象或者赋值一个对象。

        从上面的分析我们可以采用下面的形式来实现:
         Base(const Derived &);
         Base &operator=(const Derived &);

        是在基类函数中采用构造函数基于派生类来重载一系列的构造函数,但是这也存在一个问题,如果存在很多派生类,这时候就要重载很多构造函数,这肯定不是我们需要的。

     

        这时候我们发现对于一个类而言,为什么复制构造函数和重载赋值操作符这么重要了。因为这两个函数都是接受一个基类的引用,根据前面的分析我们知道一个基类引用完全可以绑定一个派生类的对象,而派生类对象中又包含了一个基类对象的基本信息。我们能够实现一个从一个派生对象到基类的构造过程。
        我们用一个基类引用绑定一个派生对象,然后采用基类引用对基类成员进行访问,完成了一个基类对象基本要素的填充操作,相当于完成了基类对象的创建,也就是构造问题。这样也就能完成由派生类对象到基类对象的构造过程。

     

        总结起来说了,因为在复制构造函数中,C++中的基类引用可以绑定一个派生类的对象,如果在允许访问的情况下,采用基类引用可以访问基类的成员以及派生类的其他成员,采用引用可以复制派生类对象中基类成员的值到新创建的基类成员中,完成一个基类成员数据的填充操作,这时候一个完整的基类对象就创建完成了。

     

        重载赋值操作符则是发生在使用一个派生对象来赋值一个基类对象时,这时候也是const基类引用绑定一个派生类对象,然后复制对应的基类成员到基类对象对于的成员中,完成一个基类对象成员的更新操作。

     

        复制构造函数不仅仅实现了同类型之间的初始化操作,同时也完成了采用一个派生类对象初始化一个基类对象的操作,重载赋值操作符实现了同类型之间的赋值操作,也完成了采用派生类对象赋值基类对象的操作。如果没有这两个函数的存在,也就不能完成派生类到基类的赋值和初始化操作。这也是为什么一定会存在这两个函数的原因。

    展开全文
  • c++拷贝构造函数

    千次阅读 多人点赞 2019-06-28 16:22:40
    什么是拷贝构造函数 首先对于普通类型的对象来说,它们之间的复制是很简单的,例如: int a = 100; int b = a; 而类对象与普通对象不同,类对象内部结构一般较为复杂,存在各种成员变量。 下面看一个类对象...
  • 构造函数与拷贝构造函数

    千次阅读 多人点赞 2019-04-03 09:07:15
    拷贝构造函数和构造函数不能分开说,他们都是初始化对象的一种方法。但是我们这里用构造函数辅助说明拷贝构造函数,主要说说拷贝构造函数的声明,用途和使用注意事项。 众所周知,构造函数是一个初始化类对象的函数...
  • C++实现 类string的 普通构造函数, 拷贝构造函数 析构函数 和赋值函数
  • 详解C++ 拷贝构造函数

    2021-01-19 23:44:42
    拷贝构造函数是一种特殊的构造函数,它在创建对象时,是使用同一类中之前创建的对象来初始化新创建的对象。拷贝构造函数通常用于: 通过使用另一个同类型的对象来初始化新创建的对象。 复制对象把它作为参数传递给...
  • 拷贝构造函数,默认拷贝构造函数

    千次阅读 2019-09-20 18:05:52
    拷贝构造函数,默认拷贝构造函数 1.c++的默认拷贝构造函数,从深度拷贝和浅拷贝说起 c++类的默认拷贝构造函数的弊端 c++类的中有两个特殊的构造函数,(1)无参构造函数,(2)拷贝构造函数。它们的特殊之处在于: (1)...
  • C 拷贝构造函数.rar

    2019-07-10 12:26:01
    C 拷贝构造函数,编写程序熟悉拷贝函数的三种用法。理解临时对象的概念,并知道它的作用、何时被创建、何时被析构。
  • 一、C++中拷贝构造函数的定义: 有一个参数的类型是其类类型的构造函数是为拷贝构造函数。 如下所示: X::X( const X& x); Y::Y( const Y& y, int =0 ); //可以是多参数形式,但其第二个即后继参数都有一个默认值 ...
  • 一、移动构造函数: 定义: 所谓移动语义,指的就是以移动而非深拷贝的方式初始化含有指针成员的类对象。简单的理解,移动语义指的就是将其他对象(通常是临时对象)拥有的内存资源“移为已用”。 优点: 提高执行...
  • 编译器永远不会把模板构造函数视为构造函数,即使客户没有自己定义拷贝构造函数,编译器也会生成一个默认的拷贝构造函数,这种情况同样存在于拷贝赋值函数和模板拷贝赋值函数。请看下面的例子:...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 247,907
精华内容 99,162
关键字:

拷贝构造函数