C++构造函数种类、拷贝构造函数、拷贝赋值运算符、深拷贝浅拷贝

构造函数是干什么的

该类对象被创建时,编译系统对象分配内存空间,并自动调用该构造函数,由构造函数完成成员的初始化工作,故:构造函数的作用:初始化对象的数据成员。

构造函数的种类

 1 class Complex 
 2 {         
 3  
 4 private :
 5     double m_real;
 6     double m_imag;
 7  
 8 public:
 9  
10     // 无参数构造函数
11     // 如果创建一个类你没有写任何构造函数,则系统会自动生成默认的无参构造函数,函数为空,什么都不做
12     // 只要你写了一个下面的某一种构造函数,系统就不会再自动生成这样一个默认的构造函数,如果希望有一个这样的无参构造函数,则需要自己显示地写出来
13     Complex(void)
14     {
15          m_real = 0.0;
16          m_imag = 0.0;
17     } 
18          
19     // 一般构造函数(也称重载构造函数)
20     // 一般构造函数可以有各种参数形式,一个类可以有多个一般构造函数,前提是参数的个数或者类型不同(基于c++的重载函数原理)
21     // 例如:你还可以写一个 Complex( int num)的构造函数出来
22     // 创建对象时根据传入的参数不同调用不同的构造函数
23     Complex(double real, double imag)
24     {
25          m_real = real;
26          m_imag = imag;         
27      }
28      
29     // 复制构造函数(也称为拷贝构造函数)
30     // 复制构造函数参数为类对象本身的引用,用于根据一个已存在的对象复制出一个新的该类的对象,一般在函数中会将已存在对象的数据成员的值复制一份到新创建的对象中
31     // 若没有显示的写复制构造函数,则系统会默认创建一个复制构造函数,但当类中有指针成员时,由系统默认创建该复制构造函数会存在风险,具体原因请查询有关 “浅拷贝” 、“深拷贝”的文章论述
32     Complex(const Complex & c)
33     {
34         // 将对象c中的数据成员值复制过来
35         m_real = c.m_real;
36         m_img  = c.m_img;
37     }            
38  
39     // 类型转换构造函数,根据一个指定的类型的对象创建一个本类的对象
40     // 例如:下面将根据一个double类型的对象创建了一个Complex对象
41     Complex::Complex(double r)
42     {
43         m_real = r;
44         m_imag = 0.0;
45     }
46  
47     // 等号运算符重载
48     // 注意,这个类似复制构造函数,将=右边的本类对象的值复制给等号左边的对象,它不属于构造函数,等号左右两边的对象必须已经被创建
49     // 若没有显示的写=运算符重载,则系统也会创建一个默认的=运算符重载,只做一些基本的拷贝工作
50     Complex &operator=(const Complex &rhs)
51     {
52         // 首先检测等号右边的是否就是左边的对象本,若是本对象本身,则直接返回
53         if ( this == &rhs ) 
54         {
55             return *this;
56         }
57              
58         // 复制等号右边的成员到左边的对象中
59         this->m_real = rhs.m_real;
60         this->m_imag = rhs.m_imag;
61              
62         // 把等号左边的对象再次传出
63         // 目的是为了支持连等 eg:    a=b=c 系统首先运行 b=c
64         // 然后运行 a= ( b=c的返回值,这里应该是复制c值后的b对象)    
65         return *this;
66     }
67 };
复制代码

 下面使用上面定义的类对象来说明各个构造函数的用法:

 1 void main()
 2 {
 3     // 调用了无参构造函数,数据成员初值被赋为0.0
 4     Complex c1,c2;
 5  
 6     // 调用一般构造函数,数据成员初值被赋为指定值
 7     Complex c3(1.0,2.5);
 8     // 也可以使用下面的形式
 9     Complex c3 = Complex(1.0,2.5);
10          
11     // 把c3的数据成员的值赋值给c1
12     // 由于c1已经事先被创建,故此处不会调用任何构造函数
13     // 只会调用 = 号运算符重载函数
14     c1 = c3;
15          
16     // 调用类型转换构造函数
17     // 系统首先调用类型转换构造函数,将5.2创建为一个本类的临时对象,然后调用等号运算符重载,将该临时对象赋值给c1
18     c2 = 5.2;
19        
20     // 调用拷贝构造函数( 有下面两种调用方式) 
21     Complex c5(c2);
22     Complex c4 = c2;  // 注意和 = 运算符重载区分,这里等号左边的对象不是事先已经创建,故需要调用拷贝构造函数,参数为c2       
23          
24 }
 
  

复制构造函数

几个原则:

C++ primer p406 :复制构造函数是一种特殊的构造函数,具有单个形参,该形参(常用const修饰)是对该类类型的引用。当定义一个新对象并用一个同类型的对象对它进行初始化时,将显示使用复制构造函数。当该类型的对象传递给函数或从函数返回该类型的对象时,将隐式调用复制构造函数。

 

C++支持两种初始化形式:复制初始化(int a = 5;)和直接初始化(int a(5);)对于其他类型没有什么区别,对于类类型直接初始化直接调用实参匹配的构造函数,复制初始化总是调用复制构造函数,也就是说:

A x(2);  //直接初始化,调用构造函数
A y = x;  //复制初始化,调用复制构造函数

 

必须定义复制构造函数的情况:

只包含类类型成员或内置类型(但不是指针类型)成员的类,无须显式地定义复制构造函数也可以复制;有的类有一个数据成员是指针,或者是有成员表示在构造函数中分配的其他资源,这两种情况下都必须定义复制构造函数。

 

什么情况使用复制构造函数:

类的对象需要拷贝时,拷贝构造函数将会被调用。以下情况都会调用拷贝构造函数:
(1)一个对象以值传递的方式传入函数体 
(2)一个对象以值传递的方式从函数返回 
(3)一个对象需要通过另外一个对象进行初始化。

 

深拷贝和浅拷贝:

所谓浅拷贝,指的是在对象复制时,只对对象中的数据成员进行简单的赋值,默认拷贝构造函数执行的也是浅拷贝。在“深拷贝”的情况下,对于对象中动态成员,就不能仅仅简单地赋值了,而应该重新动态分配空间

如果一个类拥有资源,当这个类的对象发生复制过程的时候,资源重新分配,这个过程就是深拷贝

上面提到,如果没有自定义复制构造函数,则系统会创建默认的复制构造函数,但系统创建的默认复制构造函数只会执行“浅拷贝”,即将被拷贝对象的数据成员的值一一赋值给新创建的对象,若该类的数据成员中有指针成员,则会使得新的对象的指针所指向的地址与被拷贝对象的指针所指向的地址相同,delete该指针时则会导致两次重复delete而出错。下面是示例:

 1 #include 
 2 #include <string.h>
 3 class Person 
 4 {
 5 public :
 6          
 7     // 构造函数
 8     Person(char * pN)
 9     {
10         cout << "一般构造函数被调用 !\n";
11         m_pName = new char[strlen(pN) + 1];
12         //在堆中开辟一个内存块存放pN所指的字符串
13         if(m_pName != NULL) 
14         {
15            //如果m_pName不是空指针,则把形参指针pN所指的字符串复制给它
16              strcpy(m_pName ,pN);
17         }
18     }        
19        
20     // 系统创建的默认复制构造函数,只做位模式拷贝
21     Person(Person & p)    
22     { 
23         //使两个字符串指针指向同一地址位置         
24         m_pName = p.m_pName;         
25     }
26  
27     ~Person( )
28     {
29         delete m_pName;
30     }
31          
32 private :
33     char * m_pName;
34 };
35  
36 void main( )
37 { 
38     Person man("lujun");
39     Person woman(man); 
40      
41     // 结果导致   man 和    woman 的指针都指向了同一个地址
42      
43     // 函数结束析构时
44     // 同一个地址被delete两次
45 }
46  
47  
48 // 下面自己设计复制构造函数,实现“深拷贝”,即不让指针指向同一地址,而是重新申请一块内存给新的对象的指针数据成员
49 Person(Person & chs);
50 {
51      // 用运算符new为新对象的指针数据成员分配空间
52      m_pName=new char[strlen(p.m_pName)+ 1];
53  
54      if(m_pName)         
55      {
56              // 复制内容
57             strcpy(m_pName ,chs.m_pName);
58      }
59    
60     // 则新创建的对象的m_pName与原对象chs的m_pName不再指向同一地址了
61 }
拷贝构造函数和赋值运算符
在默认情况下(用户没有定义,但是也没有显式的删除),编译器会自动的隐式生成一个拷贝构造函数和赋值运算符。但用户可以使用 delete 来指定不生成拷贝构造函数和赋值运算符,这样的对象就不能通过值传递,也不能进行赋值运算。
class Person{ public : Person( const Person& p) = delete ; Person& operator =( const Person& p) = delete ; private : int age; string name;};
上面的定义的类 Person 显式的删除了拷贝构造函数和赋值运算符,在需要调用拷贝构造函数或者赋值运算符的地方,会提示_无法调用该函数,它是已删除的函数_。
还有一点需要注意的是, 拷贝构造函数必须以引用的方式传递参数 。这是因为,在值传递的方式传递给一个函数的时候,会调用拷贝构造函数生成函数的实参。如果拷贝构造函数的参数仍然是以值的方式,就会无限循环的调用下去,直到函数的栈溢出。
何时调用
拷贝构造函数和赋值运算符的行为比较相似,都是将一个对象的值复制给另一个对象;但是其结果却有些不同,拷贝构造函数使用传入对象的值生成一个新的对象的实例,而赋值运算符是将对象的值复制给一个 已经存在的实例 。这种区别从两者的名字也可以很轻易的分辨出来,拷贝构造函数也是一种构造函数,那么它的功能就是创建一个新的对象实例;赋值运算符是执行某种运算,将一个对象的值复制给另一个对象(已经存在的)。 调用的是拷贝构造函数还是赋值运算符,主要是看是否有新的对象实例产生。如果产生了新的对象实例,那调用的就是拷贝构造函数;如果没有,那就是对已有的对象赋值,调用的是赋值运算符
调用拷贝构造函数主要有以下场景:
  • 对象作为函数的参数,以值传递的方式传给函数。 
  • 对象作为函数的返回值,以值的方式从函数返回
  • 使用一个对象给另一个对象初始化

代码如下:

class Person
{
public:
    Person(){}
    Person(const Person& p)
    {
        cout << "Copy Constructor" << endl;
    }

    Person& operator=(const Person& p)
    {
        cout << "Assign" << endl;
        return *this;
    }

private:
    int age;
    string name;
};

void f(Person p)
{
    return;
}

Person f1()
{
    Person p;
    return p;
}

int main()
{
    Person p;
    Person p1 = p;    // 1
    Person p2;
    p2 = p;           // 2
    f(p2);            // 3

    p2 = f1();        // 4

    Person p3 = f1(); // 5

    getchar();
    return 0;
}
上面代码中定义了一个类 Person ,显式的定义了拷贝构造函数和赋值运算符。然后定义了两个函数: f ,以值的方式参传入Person对象; f1 ,以值的方式返回Person对象。在 main 中模拟了5中场景,测试调用的是拷贝构造函数还是赋值运算符。执行结果如下:
分析如下:
1.这是虽然使用了"=",但是实际上使用对象p来创建一个新的对象p1。也就是产生了新的对象,所以调用的是拷贝构造函数.
2. 首先声明一个对象p2,然后使用赋值运算符"=",将p的值复制给p2,显然是调用赋值运算符,为一个已经存在的对象赋值
3. 以值传递的方式将对象p2传入函数f内,调用拷贝构造函数构建一个函数f可用的实参。
4.这条语句拷贝构造函数和赋值运算符都调用了。函数f1以值的方式返回一个Person对象,在返回时会调用拷贝构造函数创建一个临时对象tmp作为返回值;返回后调用赋值运算符将临时对象tmp赋值给p2.
5.按照4的解释,应该是首先调用拷贝构造函数创建临时对象;然后再调用拷贝构造函数使用刚才创建的临时对象创建新的对象p3,也就是会调用两次拷贝构造函数。不过,编译器也没有那么傻,应该是直接调用拷贝构造函数使用返回值创建了对象p3。

参考:https://www.cnblogs.com/raichen/p/4752025.html

https://www.cnblogs.com/wangguchangqing/p/6141743.html

 
  

你可能感兴趣的:(C++,学习笔记,构造函数,拷贝构造函数,拷贝赋值运算符,深拷贝浅拷贝)