C++基础——构造函数、析构函数、深浅拷贝详解

构造函数

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

构造函数的种类
class Complex
{

private :
    double m_real;
    double m_imag;

public:

    // 无参数构造函数
    // 如果创建一个类你没有写任何构造函数,则系统会自动生成默认的无参构造函数,函数为空,什么都不做
    // 只要你写了一个下面的某一种构造函数,系统就不会再自动生成这样一个默认的构造函数,如果希望有一个这样的无参构造函数,则需要自己显示地写出来
    Complex(void)
    {
         m_real = 0.0;
         m_imag = 0.0;
    }

    // 一般构造函数(也称重载构造函数)
    // 一般构造函数可以有各种参数形式,一个类可以有多个一般构造函数,前提是参数的个数或者类型不同(基于c++的重载函数原理)
    // 例如:你还可以写一个 Complex( int num)的构造函数出来
    // 创建对象时根据传入的参数不同调用不同的构造函数
    Complex(double real, double imag)
    {
         m_real = real;
         m_imag = imag;
     }

    // 复制构造函数(也称为拷贝构造函数)
    // 复制构造函数参数为类对象本身的引用,用于根据一个已存在的对象复制出一个新的该类的对象,一般在函数中会将已存在对象的数据成员的值复制一份到新创建的对象中
    // 若没有显示的写复制构造函数,则系统会默认创建一个复制构造函数,但当类中有指针成员时,由系统默认创建该复制构造函数会存在风险,具体原因请查询有关 “浅拷贝” 、“深拷贝”的文章论述
    Complex(const Complex & c)
    {
        // 将对象c中的数据成员值复制过来
        m_real = c.m_real;
        m_img  = c.m_img;
    }

    // 类型转换构造函数,根据一个指定的类型的对象创建一个本类的对象
    // 例如:下面将根据一个double类型的对象创建了一个Complex对象
    Complex::Complex(double r)
    {
        m_real = r;
        m_imag = 0.0;
    }

    // 等号运算符重载
    // 注意,这个类似复制构造函数,将=右边的本类对象的值复制给等号左边的对象,它不属于构造函数,等号左右两边的对象必须已经被创建
    // 若没有显示的写=运算符重载,则系统也会创建一个默认的=运算符重载,只做一些基本的拷贝工作
    Complex &operator=(const Complex &rhs)
    {
        // 首先检测等号右边的是否就是左边的对象本,若是本对象本身,则直接返回
        if ( this == &rhs )
        {
            return *this;
        }

        // 复制等号右边的成员到左边的对象中
        this->m_real = rhs.m_real;
        this->m_imag = rhs.m_imag;

        // 把等号左边的对象再次传出
        // 目的是为了支持连等 eg:    a=b=c 系统首先运行 b=c
        // 然后运行 a= ( b=c的返回值,这里应该是复制c值后的b对象)
        return *this;
    }
};

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

void main()
{
    // 调用了无参构造函数,数据成员初值被赋为0.0
    Complex c1,c2;

    // 调用一般构造函数,数据成员初值被赋为指定值
    Complex c3(1.0,2.5);
    // 也可以使用下面的形式
    Complex c3 = Complex(1.0,2.5);

    // 把c3的数据成员的值赋值给c1
    // 由于c1已经事先被创建,故此处不会调用任何构造函数
    // 只会调用 = 号运算符重载函数
    c1 = c3;

    // 调用类型转换构造函数
    // 系统首先调用类型转换构造函数,将5.2创建为一个本类的临时对象,然后调用等号运算符重载,将该临时对象赋值给c1
    c2 = 5.2;

    // 调用拷贝构造函数( 有下面两种调用方式)
    Complex c5(c2);
    Complex c4 = c2;  // 注意和 = 运算符重载区分,这里等号左边的对象不是事先已经创建,故需要调用拷贝构造函数,参数为c2

}
复制构造函数

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

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

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

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

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

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

类的对象需要拷贝时,拷贝构造函数将会被调用。以下情况都会调用拷贝构造函数:

(1)一个对象以值传递的方式传入函数体
(2)一个对象以值传递的方式从函数返回
(3)一个对象需要通过另外一个对象进行初始化。

深拷贝和浅拷贝

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

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

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

 1 #include <iostream.h>
 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 }
重载赋值操作符

通过定义operate=的函数,可以对赋值进行定义。像其他任何函数一样,操作符函数有一个返回值和形参表。形参表必须具有与该操作符操作数书目相同的形参(如果操作符是一个成员,则包括隐式this形参)。赋值是二元运算,所以该操作符函数有两个形参:第一个形参(隐含的this指针)对应着左操作数,第二个形参对应右操作数。

一个应用了对赋值号重载的拷贝构造函数的例子:

 1 #include <iostream>
 2 
 3 using namespace std;
 4 
 5 class A
 6 {
 7 public:
 8     A(int);//构造函数
 9     A(const A &);//拷贝构造函数
10     ~A();
11     void print();
12     int *point;
13     A &operator=(const A &);
14 };
15 
16 A::A(int p)
17 {
18     point = new int;
19     *point = p;
20 }
21 
22 A::A(const A &b)
23 {
24     *this = b;
25     cout<<"调用拷贝构造函数"<<endl;
26 }
27 
28 A::~A()
29 {
30     delete point;
31 }
32 
33 void A::print()
34 {
35     cout<<"Address:"<<point<<" value:"<<*point<<endl;
36 }
37 
38 A &A::operator=(const A &b)
39 {
40     if( this != &b)
41     {
42         delete point;
43         point = new int;
44         *point = *b.point;
45     }
46 }
47 
48 
49 int main()
50 {
51     A x(2);
52     A y = x;
53     x.print();
54     delete x.point;
55     y.print();
56 
57     return 0;
58 }
析构函数

析构函数(destructor)是成员函数的一种,它的名字与类名相同,但前面要加~,没有参数和返回值。

一个类有且仅有一个析构函数。如果定义类时没写析构函数,则编译器生成默认析构函数。如果定义了析构函数,则编译器不生成默认析构函数。

析构函数在对象消亡时即自动被调用。可以定义析构函数在对象消亡前做善后工作。例如,对象如果在生存期间用 new 运算符动态分配了内存,则在各处写 delete 语句以确保程序的每条执行路径都能释放这片内存是比较麻烦的事情。有了析构函数,只要在析构函数中调用 delete 语句,就能确保对象运行中用 new 运算符分配的空间在对象消亡时被释放。例如下面的程序:

class String{
private:
    char* p;
public:
    String(int n);
    ~String();
};
String::~String(){
    delete[] p;
}
String::String(int n){
    p = new char[n];
}

String 类的成员变量 p 指向动态分配的一片存储空间,用于存放字符串。动态内存分配在构造函数中进行,而空间的释放在析构函数 ~String() 中进行。这样,在其他地方就不用考虑释放空间的事情了。

只要对象消亡,就会引发析构函数的调用。下面的程序说明了析构函数起作用的一些情况。

#include<iostream>
using namespace std;
class CDemo {
public:
    ~CDemo() {  //析构函数
        cout << "Destructor called"<<endl;
    }
};
int main() {
    CDemo array[2];  //构造函数调用2次
    CDemo* pTest = new CDemo;  //构造函数调用
    delete pTest;  //析构函数调用
    cout << "-----------------------" << endl;
    pTest = new CDemo[2];  //构造函数调用2次
    delete[] pTest;  //析构函数调用2次
    cout << "Main ends." << endl;
    return 0;
}

程序的输出结果是:

Destructor called
-----------------------
Destructor called
Destructor called
Main ends.
Destructor called
Destructor called

第一次析构函数调用发生在第 13 行,delete 语句使得第 12 行动态分配的 CDemo 对象消亡。

接下来的两次析构函数调用发生在第 16 行,delete 语句释放了第 15 行动态分配的数组,那个数组中有两个 CDemo 对象消亡。最后两次析构函数调用发生在 main 函数结束时,因第 11 行的局部数组变量 array 中的两个元素消亡而引发。

函数的参数对象以及作为函数返回值的对象,在消亡时也会引发析构函数调用。例如:

#include <iostream>
using namespace std;
class CDemo {
public:
    ~CDemo() { cout << "destructor" << endl; }
};
void Func(CDemo obj) {
    cout << "func" << endl;
}
CDemo d1;
CDemo Test() {
    cout << "test" << endl;
    return d1;
}
int main() {
    CDemo d2;
    Func(d2);
    Test();
    cout << "after test" << endl;
    return 0;
}
程序的输出结果是:
func
destructor
test
destructor
after test
destructor
destructor

程序共输出 destructor 四次:

1、第一次是由于 Func 函数结束时,参数对象 obj 消亡导致的。
2、第二次是因为:第 20 行调用 Test 函数,Test 函数的返回值是一个临时对象,该临时对象在函数调用所在的语句结束时就消亡了,因此引发析构函数调用。
3、第三次是 main 函数结束时 d2 消亡导致的。
4、第四次是整个程序结束时全局对象 d1 消亡导致的。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值