C++基础:(4)构造函数和析构函数的概念

构造函数和析构函数的概念

  1. 构造函数定义及调用
  • a) C++中的类可以定义与类名相同的特殊成员函数,这种与类名相同的成员函数叫做构造函数;
  • b) 构造函数在定义时可以有参数;
  • c) 没有任何返回类型的声明。
  1. 构造函数的调用
  • 自动调用:一般情况下C++编译器会自动调用构造函数,因为编译器可以自动调用构造函数,因此当我们在创建对象为元素的数组时,就不用一个一个地手动初始化,而是编译器在底层为我们把每个元素都初始化好了,这是非常高效便捷的方式;
  • 手动调用:在一些情况下则需要手工调用构造函数,这里有个概念就行暂时不表。

2.1 简单构造函数的三种形式

#include "iostream"

using namespace std;

class Test2
{
    public:
        // 形式1
        // 无参数的构造函数
        Test2()
        {
            m_a = 100;
            m_b = 101;
            cout << "无参数的构造函数" << endl;
        }

        // 形式2
        // 有参数的构造函数
        Test2(int a, int b)
        {
            m_a = a;
            m_b = b;
            cout << "有参数的构造函数,参数个数为2个" << endl;
            cout << "a:" << m_a << "\tb:" << m_b << endl;
        }
    

        Test2(int a)
        {
            cout << "有参数的构造函数,参数个数为1个" << endl;
            m_a = a;
            m_b = 0;
            cout << "a:" << m_a << "\tb:" << m_b << endl;
        }
        
        // 形式3
        // 用一个对象来初始化新的对象:赋值构造函数/拷贝构造函数
        Test2(const Test2& obj)
        {
            cout << "我也是构造函数" << endl; 
        }

        void printT()
        {
            cout << "普通成员函数" << endl;
        }
    protected:
    private:
        int m_a, m_b;
};

void objPlay()
{
    Test2 t1;   // 什么都不写,将会调用无参构造函数 C++编译器自动调用构造函数
    // 调用有参数的构造函数,有三种方法
    // 方法1:括号法
    Test2 t2(1, 2);
    t2.printT();
    // 方法2:
    Test2 t3 = 4;   //C++编译器自动调用构造函数
    Test2 t4 = (3, 4, 6, 8, 90, 100, 12, 99999);  // c++对=运算符进行增强,C++编译器自动调用构造函数
    // 上面的逗号表达式的意思是,有多个逗号隔开的一串值,这个表达式的值就是最后一个数值
    // 方法3:手工调用构造函数
    // 这样调用构造函数,将会产生一个匿名对象(匿名对象的去和留老师先作为一个话题抛砖引出来)
    Test2 t5 = Test2(98, 99);

}

int main()
{
    int nRet = 0;
    
    objPlay();
    cout << "hello world.." << endl;

    return nRet;
}


2.2 拷贝构造函数

拷贝构造函数使用的四种时机

  • 1). 用一个对象区初始化另外一个对象 Test t2 = t1;
  • 2). 用一个对象区初始化另外一个对象 Test t2(t1);
#include "iostream"

using namespace std;

class Test
{
    public:
        Test()
        {
            m_a = 0;
            m_b = 0;
            cout << "无参数的构造函数" << endl;
        }

        Test(int a, int b)
        {
            m_a = a;
            m_b = b;
            cout << "输入两个参数的构造函数" << endl;
            cout << "m_a: " << m_a << "\tm_b: " << m_b << endl;
        }

        Test(int a)
        {
            m_a = a;
            m_b = 0;
            cout << "输入一个参数的构造函数" << endl;
            cout << "m_a: " << m_a << "\tm_b: " << m_b << endl;
        }

        Test(const Test & obj)
        {
            m_a = obj.m_a;
            m_b = obj.m_b;
            cout << "我也是构造函数:拷贝构造函数." << endl;
            cout << "m_a: " << m_a << "\tm_b: " << m_b << endl;
        }

        void printTest()
        {
            cout << "printTest::普通成员函数" << endl; 
            cout << "m_a: " << m_a << "\tm_b: " << m_b << endl;
        }
    protected:
    private:
        int m_a;
        int m_b;
};

int main()
{
    int nRet = 0;

    Test t0(1, 2);  // 用两个输入参数为对象初始化

    // 1. 用一个对象区初始化另外一个对象
    Test t1 = t0;
    // 但是,下面的语句是表示使用对象对另一个对象赋值操作,这是两个不同的概念!!! 
    // 这里使用的是C++重载的等号运算符
    Test t2;
    t2 = t0;    // 使用t1初始化t2
    t2.printTest();

    // 2. 用一个对象区初始化另外一个对象 Test t2(t1);
    // 这里调用的构造函数和第一种方法调用的是同一个构造函数
    Test t3(t0);

    cout << "hello world... " << endl;

    return nRet;
}
  • 3).第三种使用情况就是当函数的形参是一个对象,在调用函数的时候,参数传递的过程也会自动调用拷贝构造函数。
#include "iostream"

using namespace std;

class Location
{
    public:
        Location(int xx = 0, int yy = 0)
        {
            // 两个参数的构造函数
            cout << "Normal-Constructor function." << endl;
            m_x = xx;
            m_y = yy;
            printObj();
        }

        Location(const Location & obj)
        {
            // 拷贝构造函数
            cout << "Copy-Contructor function." << endl;
            m_x = obj.m_x;
            m_y = obj.m_y;
            printObj();
        }

        ~Location()
        {
            cout << "m_x:" << m_x << "\tm_y:" << m_y << "\tObjcet destroyed." << endl;    
        }
        
        int getX()
        {
            return m_x;
        }

        int getY()
        {
            return m_y;
        }

        void printObj()
        {
            cout << "m_x:" << m_x << "\t m_y:" << m_y << endl;
        }
    protected:
    private:
        int m_x;
        int m_y;
};

void f(Location p)
{
    // 这是一个业务函数,形参是一个对象
    cout << p.getX() << endl;
}

void playObj()
{
    cout << "func playObj()" << endl; 
    Location a(1, 2);
    Location b = a;
    
    cout << "对象b已经初始化完毕" << endl;
    // 这里调用业务函数,在传入参数的时候,即把b传给形参p
    // 因此会调用拷贝构造函数
    f(b);
}

int main()
{
    int nRet = 0;
    
    playObj();
    cout << "hello world..." << endl;

    return nRet;
}
  • 4).第四种使用情况:函数的返回值是一个对象,这个对象没有名字所以叫做“匿名对象”,匿名对象怎么处理,就看调用函数的地方怎么接这个对象。

    • 有两种处理情况:

    -- a. 若返回的匿名对象,赋值给另外一个同类型的对象(即该对象已经声明),那么匿名对象就会被析构;

    Location g()
    {
        Location A(1, 2);   // 调用Location的构造函数
        return A;
    }    

    void mainObjPlay()
    {
        Location B;
        B = g();    // 用匿名对象赋值给已经声明的B,然后匿名对象被析构
    }

PS: 通过在Ubuntu和VS2015中运行同样的代码,我发现了一些小的区别:在VS2015中,程序进入mainObjPlay()后,

> 1) 调用两个参数的构造函数,初始化对象A;
> 2) 在return A;的时候,程序自动调用了拷贝构造函数,使用对象A创建并初始化了一个匿名变量;
> 3) 然后函数结束了,所以A的声明周期结束,调用了A的析构函数;
> 4) 在调用g()的地方没有变量来接返回值,所以这个匿名变量也被析构了。
> 程序运行情况如下:
Normal-Constructor function.
m_x: 1  m_y:3
Copy-Construector function.
m_x:1   m_y:3
m_x:1   m_y:3   Object destroyed.
m_x:1   m_y:3   Object destroyed.
hello world.
请按任意键继续...

但是在Ubuntu中,程序进入mainObjPlay()后,

> 1) 与VS2015相同:调用两个参数的构造函数,初始化对象A;
> 2) return A;的时候,直接调用了析构函数
> 3) mainObjPlay()函数结束
> 4) 程序运行结果如下:
Normal-Constructor function.
m_x:1    m_y:3
m_x:1   m_y:3   Objcet destroyed.
hello world.

这里Ubuntu的执行结果好像有点“预先判断”的意思,调用函数的地方没有接收返回值的对象,那么函数g()也不创建匿名对象了,直接析构了临时变量,就是那个对象A,然后说了句:懒得理你......

-- b. 若返回的匿名对象被使用来初始化另外一个同类型的对象,那么这个匿名对象会直接转换为新的对象

    Location g()
    {
        Location A(1, 2);   // 调用Location的构造函数
        return A;
    }   

    void mianObjPlay()
    {
        cout << "00000000000" << endl;
        Location B = g();     // 匿名对象会直接转换为新的对象
        cout << "11111111111" << endl;
    }

VS2015运行结果如下:能够看出Location B = g();这一句,并没有再次调用一个拷贝构造函数来初始化新的对象B,所以是匿名对象直接被转为B。这里做的就是相当于提前消耗了内存空间来获取执行的速率。
设计C++编译器的大牛们就是认为用户想要把返回的对象直接用起来,所以就不再调用拷贝构造函数了。

00000000000
Normal-Constructor function.
m_x: 1  m_y:3                       >>>>这里是Location A(1, 2);的结果
Copy-Construector function.
m_x:1   m_y:3                       >>>>创建匿名对象,调用了拷贝构造函数
m_x:1   m_y:3   Object destroyed.   >>>>临时变量A被析构
11111111111
m_x:1   m_y:3   Object destroyed.   >>>>临时变量B被析构
hello world.
请按任意键继续...

还可以继续探索,若接受返回值的对象已经被创建,代码如下:

    Location g()
    {
        Location A(1, 2);   // 调用Location的构造函数
        return A;
    }   

    void mianObjPlay()
    {
        Location B;
        cout << "00000000000" << endl;
        B = g();     // 匿名对象的值赋值给对象B,然后匿名对象就被析构,它的声明周期到此为止
        cout << "11111111111" << endl;
    }

运行结果:

Normal-Constructor function.
m_x: 0  m_y:0                    >>>>这里是Location B;的结果
00000000000
Normal-Constructor function.
m_x: 1  m_y:3                    >>>>这里是Location A(1, 2);的结果
Copy-Construector function.
m_x:1   m_y:3                    >>>>创建匿名对象
m_x:1   m_y:3   Object destroyed.>>>> 局部变量A被析构
m_x:1   m_y:3   Object destroyed.>>>> 匿名对象在赋值结束后,被析构
11111111111
m_x:1   m_y:3   Object destroyed.>>>> 对象B的生命周期结束,被析构
hello world.
请按任意键继续...

结论1:函数的返回值是一个元素(复杂类型的),返回的是一个新的匿名对象(所以会调用匿名对象类的拷贝构造函数);

结论2:有关匿名对象的去和留:

如果用匿名对象 初始化 另外一个同类型的对象,匿名对象被转正为有名对象;

如果用匿名对象 赋值给 另外一个同类型的对象,匿名对象赋值之后立马被析构。

如果你写了构造函数,那你必须要用,原来编译器提供的默认无参构造函数就被代替了;或者说,你在试图初始化对象的时候,要三思一下,定义类的时候,里面有没有同样参数列表的构造函数?如果没有这样的构造函数,那就需要反思是初始化对象的方式需要调整,还是说需要编写一个具有对应参数列表的构造函数。

  1. 析构函数定义及调用
    1. C++中的类可以定义一个特殊的成员函数清理对象,这个特殊的成员函数叫做析构函数;
      语法: ~ClassName()
    1. 析构函数没有参数也没有任何返回类型的声明;
    1. 析构函数在对象销毁时自动被调用, 同一个类的不同对象如果生命周期相同,那么先被创建的对象排在后面释放,因为对象也是存在栈上的。
  1. 析构函数调用机制:由C++编译器自动调用

  2. 如果当对象的成员变量中存在有malloc的情况时,C++编译器默认的拷贝构造函数会存在风险

#include "iostream"
#include <string.h>
#include <stdio.h>

using namespace std;

class Name
{
    public:
        Name(const char *myp)
        {
            int len = strlen(myp);
            mLen = len; // mLen保存的是对象的字符串里面有效的字符个数,不包含‘\0’
            mP = (char *)malloc(len + 1);   // 给\0留一个位置
            strcpy(mP, myp);
        }

        ~Name()
        {
            if (mP != NULL) {
                cout << "Destroy Name obj." << endl;
                free(mP);
                mP = NULL;
                mLen = 0;
            }
        }
        
        void printName()
        {
            printf("address: %ld\n", mP); 
        }    
    protected:
    private:
        char *mP;
        int mLen;
};


// 在下面的函数中,使用默认的拷贝构造函数对obj2进行了初始化,这里的操作是:
//      1)把obj1.mP赋值给obj2.mP; 
//      2)把obj1.mLen 赋值给obj2.mLen;
// 但是,因为mP是使用malloc分配的堆空间,随着objPlay()函数运行结束,程序要释放局部变量时,就会启用~Name()
// 来先释放obj2,再释放obj1;
// 可以看到obj1.mP和obj2.mP的值都是一样的,都指向了同一片堆区;
// 那么在释放obj2后释放obj1的时候,free(mP)就会把同一片堆空间释放两次,程序运行就会报错。
// 这就是所谓“浅拷贝”带来的风险。
// 因此,在这种必要的情况下:我们就要手工编写拷贝构造函数
void objPlay()
{
    Name obj1("Hello Seiko");
    obj1.printName();
    Name obj2 = obj1;
    obj2.printName();
}

int main()
{
    int nRet = 0;
    objPlay();
    cout << "hello world !!!" << endl;
    return nRet;
}

自行编写的拷贝构造函数如下

Name(const Name & obj)
{
    mLen = obj.mLen;
    mP = (char *)malloc(mLen+1);
    strcpy(mP, obj.mP);
}
  1. 构造函数的初始化列表

使用构造函数的初始化列表可以面对这样的情况, 以保证对象能够被初始化:a)在类class B中包含了一个类class A,b)而且class A中已经设计了有参数的构造函数。那么根据构造函数的调用规则,既然已经有了class A的构造函数,则必须按照构造函数的参数进行调用,如果我们在class B中仍然很简单地使用C++默认的无参数构造函数,则C++编译器在构造class B的对象时,无法构造其中的class A对象(因为没有提供参数啊),那么就会报错。基于这种类中嵌套类的初始化需求,C++提供了构造函数的对象初始化列表。
语法规则为:

Constructor::Constructor() : m1(paramater1), m2(parameter2), m3(parameter3, parameter 4)...
{
    // some some assignment operation.
}

对象的初始化列表也可以使用构造函数参数列表里面的形参变量。
PS: 初始化的顺序:

  • 1)先执行被组合对象的构造函数;
  • 2)如果被组合的对象有多个,则按照定义类时的变量的定义顺序,而不是初始化列表中对象的前后顺序,
  • 3) 被组合对象的析构函数调用顺序和构造函数的调用顺序相反。
using namespace std;

class TestA
{
public:
    TestA(int a)
    {
        mA = a;
        cout << "TestA 构造函数, mA:" << mA << endl;
    }

    ~TestA()
    {
        cout << "TestA 析构函数" << endl;
    }
protected:
private:
    int mA;
};

class TestB
{
    public:
//  这里我故意把mTest2写在mTest1的前面,在后面的程序运行结果中可以看到,mTest1比mTest2先初始化
        TestB(int p1, int p2, int p3, int p4) : mTestA2(p4), mTestA1(p3)
        {
            mB1 = p1;
            mB2 = p2;
            cout << "TestB构造函数:mB1:" << mB1 << "\t mB2: " << mB2 << endl;
        }

        ~TestB()
        {
            cout << "TestB 析构函数" << endl;
        }

    protected:
    private:
        int mB1, mB2;
        TestA mTestA1, mTestA2;
};

void objPlay()
{
   TestA a1(10) ;
   TestB b1(1,2,3,4);
}


int main()
{
    int nRet = 0;
    
    objPlay();
    cout << "hello world... I am so tired..." << endl;

    return nRet;
}

函数的运行结果

TestA 构造函数, mA:10  >>>>TestA a1(10) ;
TestA 构造函数, mA:3   >>>>虽然对象初始化列表里面TestA2在TestA1的前面,但是还是先把TestA1初始化,对应的参数为3
TestA 构造函数, mA:4   >>>>然后按照TestB中定义成员变量的顺序,为TestA2初始化
TestB构造函数:mB1:1    mB2: 2     >>>>初始化TestB自己的变量
TestB 析构函数         >>>>TestB最后被初始化,所以最先调用TestB的析构函数
TestA 析构函数         >>>>析构Test2
TestA 析构函数         >>>>析构Test1
TestA 析构函数         >>>>析构a1
hello world... I am so tired...

最后,还有一个范例示范了使用构造函数要避开的误区:

// 这段代码的目的是展示一个“构造中调用构造”
#include "iostream"

using namespace std;

class myTest
{
public:
    myTest(int a, int b, int c)
    {
        this->a = a;
        this->b = b;
        this->c = c;
    }

    myTest(int a, int b)
    {
        this->a = a;
        this->b = b;
// 在构造函数中调用构造函数是一种危险的行为:
// 你调用了,现有的对象没法接;
// 被调用的构造函数也不能帮你做初始化
// 所以每一个构造函数都要做活做全套



    // 下面这句不正常的语句会产生两个动作:
    // 1)调用ABCD构造函数;
    // 2)匿名对象没有目标接收,然后立马被析构函数释放掉,所以下面这句话产生的匿名对象的生命周期只有这一句话
        myTest(a, b, 100);
    }

    ~myTest()
    {
        printf("~myTest: a: %d, b: %d, c: %d\n", a, b, c);
    }
protected:
private:
    int a, b, c;

public:
    int getC() const {return c;}
    void setC(int val) {c = val;}

};


int main()
{
    int nRet = 0;
    myTest t1(1, 2);
    printf("c: %d\n", t1.getC());

    return nRet;
}


// 运行结果:
 #if (0)
~myTest: a: 1, b: 2, c: 100
c: 21945
~myTest: a: 1, b: 2, c: 21945
#endif
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,128评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,316评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,737评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,283评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,384评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,458评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,467评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,251评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,688评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,980评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,155评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,818评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,492评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,142评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,382评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,020评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,044评论 2 352

推荐阅读更多精彩内容