构造函数和析构函数

01构造函数和析构函数初始化列表

在C/C++中,变量(包括对象)在定义的时候,可以被初始化,如果不初始化,变量的值是什么呢?

        未初始化的全局变量和静态变量 ------> 0

        未初始化的局部变量 -----> 随机值(不可预测的)

int a = 100; // 初始化

int b;
b = 100; // 不是初始化,是赋值

对于C++中的对象来说,对象和基本变量一样,定义的时候也可以进行初始化,但是,一个对象的内部结构可能比较复杂,在定义的时候,如果不进行初始化,在使用的时候就可以能会产生一些错误
        如:
                有一些以指针为成员变量的类实例化的时候,如果没有初始化,成员指针就是野指针
                我们的屏幕对象,在实例化之后,必须经过"初始化"工作,确定内部的(状态)
                ....
        所以,对象的初始化往往不仅仅是对成员变量的赋值那么简单,也可能还需要进行一些动态内存分配,打开文件等复杂的操作,这种情况下,我们就不能以初始化基本类型的方式来对对象进行初始化了....
 
        虽然可以为类设计一个"初始化"函数,对象在实例化之后,就立即调用这个函数,但是这样做的话,初始化函数就不具有强制性,很难确保程序每一次实例化之后都调用它


        面向对象的程序设计语言倾向于对象(变量)在实例化之后,一定要有一个确定的状态(值),使用起来才会比较安全


        因此,C++引入了构造函数(constructor)的概念,用于对对象进行自动初始化工作

1. 构造函数(constructor)

构造函数:在创建一个新对象的时候一定会调用
        功能:专门用于对象的初始化工作,在类的对象创建时定义初始状态

        特点:
                1. 构造函数的名字和类名相同 !!!

                2. 构造函数没有返回值类型,也不能写void,可以有参数(可以重载,可以有很多个构造函数),参数可以设置默认值,不能产生二义性

                3. 在创建对象的时候,自动调用,而且一定会调用,且只调用一次
                    不能通过已有对象手动调用构造函数 !!!

                4. 如果一个类中没有显示的声明构造函数,编译器会自动的生成一个构造函数
                    自动生成的构造函数函数体为空且没有参数
                            类名(){}
                    如果你自己显示的写了任何构造函数时,编译器就不会自动生成了 !!!

#include <iostream>

using namespace std;

class Test {
    private:
        int m_a = 1; // C++11新标准,构造函数优先
                     // 如果构造函数有重新给它赋值,那么就会修改这个值
        int m_b = 2; 
        int *m_p;
    public:
        // 构造函数(自己写了构造函数,编译器就不会自动生成了)
        // 可以有参数(可以重载,可以有很多个构造函数),参数可以设置默认值,
        // 不能产生二义性
        Test(int a, int b = 2) {
            cout << "有参-构造函数" << endl;
            m_a = a;
            m_b = b;
            m_p = (int *)malloc(sizeof(int));
            *m_p = 1024;
        }
        // 重载的构造函数
        Test() {
            cout << "无参-构造函数" << endl;
            m_a = 100;
            m_b = 100;
            m_p = (int *)malloc(sizeof(int));
            *m_p = 1024;
        }
        void show() {
            cout << "m_a:" << m_a << endl;
            cout << "m_b:" << m_b << endl;
            cout << "*m_p:" << *m_p << endl;
        }
};

int main() {
    // 编译器在调用构造函数的时候,会根据构造函数参数自动匹配相应的构造函数
    // 在创建对象的时候,没有提供实际参数,编译器会自动查找 Test::Test()
    Test t; // 调用无参构造函数,实例化一个 Test 对象
    t.show();
    // t.Test(10, 11); // 对象一旦创建,就不能手动调用构造函数

    // 在创建对象的时候,同时指定初始化参数(调用有参构造函数)
    // 类名 对象名(实际参数列表);
    Test t1(12, 13); // 调用有参构造函数(Test::Test(int, int))
                     // 实例化一个 Test 对象
    t1.show();

    // Test t2(); // 这不是实例化对象,而是声明了一个函数
                  // 函数返回值为Test,函数名为t2,没有参数
    // t2.show();

    // 为了解决这个问题,C++11新标准中引入了一种新的初始化方式
    // 就是在初始化的时候统一使用大括号
    // 如:
    Test t3{}; // 调用无参构造函数
    Test t4{100}; // 调用Test::Test(int)
    Test t5{100, 101}; // 调用Test::Test(int, int)
    // Test t6{100, 101, 102}; // 调用Test::Test(int, int, int)
                               // ---> 没写这个构造函数,找不到会报错

    return 0;
}

/*
无参-构造函数
m_a:100
m_b:100
*m_p:1024
有参-构造函数
m_a:12
m_b:13
*m_p:1024
无参-构造函数
有参-构造函数
有参-构造函数
*/

不要因为构造函数的名称而认为构造函数负责为对象分配空间,构造函数在执行的时候,对象的内存空间就已经分配好了,构造函数的作用只是给成员变量赋值,初始化资源


C++中如何清理需要销毁的对象呢?
        一般而言,需要销毁的对象都需要进行清理工作(释放资源)
        如:关闭文件,释放动态内存分配的空间.....
   

解决方案:
        为每一个类提供一个公有的 free函数
        对象不需要的时候,立即调用 free函数 释放资源

class Test {
            
    private:
        int *m_p;
    public:
        Test() {
            m_p = malloc(4)
        };

        void free() {
            ::free(m_p);
        }
};

存在一个问题,
    free只是一个普通的函数,必须要显示的调用
    对象在销毁前没有调用这个函数,很有可能会造成资源浪费
    ======>
    C++编译器在对象销毁的时候能够自动调用一个特殊的函数进行对象的清理工作

2. 析构函数(de-structor)

C++编译器在对象销毁的时候能够自动调用一个特殊的函数进行对象的清理工作

        =====> 这个特殊的清理函数就是析构函数

        析构函数的作用和构造函数的作用是相反的 =====> 释放对象占用的资源

定义

        ~类名() {}

功能

        专门用于对象的清理工作
特点

        1. 析构函数的名字和类名相似   ~类名
        2. 析构函数没有返回值类型
也不能写void也没有参数 (不能设置默认值不能重载只能有一个析构函数)
        3. 在对象销毁的时候自动调用(不提倡使用类对象手动调用)
        4. 如果一个类中没有显示的声明析构函数
编译器会自动的生成一个析构函数
            自动生成的析构函数函数体为空


 问题

        1. 在相同的作用域下面,多个对象的构造和析构顺序是怎么样的?

                按照声明的顺序构造对象
                先构造的对象,后析构,后构造的对象,先析构

        2. 全局对象和静态对象在什么时候构造,什么时候析构?

                全局对象(包括全局静态对象)在程序开始前构造,在程序结束之后析构
                局部静态对象在用到的时候构造,在程序结束之后析构

        

        3. 构造函数和析构函数是做什么的如何使用的?

                构造函数:初始化对象

                析构函数:释放资源

                        能不能不写构造函数和析构函数:

                                能

                        在什么情况下必须手动的添加构造函数和析构函数?

                                对象需要初始化的时候

                                对象中拥有资源(指针,打开文件....)的时候一定要自己实现构造函数和析构函数 
 

                为前面写的代码添加构造函数和析构函数

                        只有屏幕类需要实现构造函数和析构函数(只有屏幕类拥有资源)

                        arm-linux-g++  *.cpp -o main  -std=c++11


        4. 当A(Rect)类对象中拥有一个B(Point)类对象时,创建和销毁A类对象时,A和B的构造函数是如何调用的?          C++中,主张任何对象的初始化工作由自己的构造函数完成
                 调用A类的构造函数前,先调用B类的构造函数(先把成员对象构造出来)

                析构的时候,先调用A类的析构函数,再调用B的析构函数

                构造一个对象时,成员对象先被构造(先构造的后析构)   

                

                如果A类中拥有多个其他类的对象,其他类的构造函数的调用顺序是按照在A中声明顺序调用的 

        如果B类对象只有有参构造函数的时候怎么办?
                 默认调用的是B类对象的无参构造函数
如果B类对象没有无参构造函数就需要想办法 !!!

                ======>
                通过构造函数初始化列表的方式指定B类对象的构造函数

        如果在一个对象(A)中拥有另一个类的对象(B)时,在构造这个对象(A)的时候,会先默认调用成员对象(B)的无参构造函数,以初始化成员对象B

        

        当类的成员对象(B)中,没有无参构造函数时,就会因为找不到对应的构造函数而报错

        任何解决:

                1. 手动在成员对象类中写一个无参构造函数

                2. 在成员对象类的有参构造函数中,给参数添加默认值

                        1和2这两种方法都是让编译器可以以没有参数的形式调用成员对象的无参构造函数

                3. 利用构造函数初始化列表的方式指定成员对象的构造函数  <------

       

                通常是使用第三种方法,因为前面两种方法都需要修改成员对象类,有可能成员对象类是不可以修改的

3. 构造函数初始化列表

构造函数初始化列表是对构造函数的一种增强,它能够初始化一些特殊成员

如:

        常量,引用,调用成员对象指定的构造函数......

在构造函数中,仅仅是对成员变量的赋值,这里严格意义上来说,不是初始化

仅仅是一个赋值计算的过程,当成员变量是常量的时候,常量是不能赋值的,给常量赋值就会报错


格式:

        构造函数名(参数列表) : 成员变量名{初始值}, 成员变量名2{初始值}......

        {

                构造函数的函数体!

        }

        

        如果成员变量名是另一个类(B)的对象(b),那么这种情况下就是指定成员对象(b)的初始化方式(构造函数)


必须使用构造函数初始化列表的场合:

        1. 成员变量是常量(const)

        2. 成员变量是引用(在定义的时候必须初始化)

        3. 成员对象没有无参构造函数,指定成员对象特定的构造函数(指定成员对象的初始化方法)

        4. 初始化基类成员(指定基类的构造函数)

建议:

        成员变量的初始化尽量都使用构造函数初始化列表的方式

                1. 代码简洁

                2. 初始化列表的这种方式效率比普通构造函数高

                3. 构造函数的声明和定义分开的时候,构造函数初始化列表必须写在定义的地方

02临时对象(无名对象)

临时对象是指在函数传参或者函数返回的时候,创建的没有名字的对象

对象在用完之后,会被立即销毁,用户也不能去使用它

例子:

Color c; // 实例化一个颜色对象,表示一个颜色
c.setColor(0xff, 0xff, 0xff);

Screen s; // 实例化一个屏幕类对象
s.clearScreen(c); // 把屏幕对象设置为c表示的颜色
// 此处的c对象仅仅是用来作为函数的参数传递的,其他的地方并没有用到
====================>
Screen s; // 实例化一个屏幕类对象
s.clearScreen(Color{});  // 调用Color的无参构造函数
                         // 构造一个临时对象,作为参数传递进入函数内部
                         // 临时对象的生存期也仅仅在这一行
    or
Screen s; // 实例化一个屏幕类对象
s.clearScreen(Color{0xff, 0xff, 0xff}); // 调用Color的有参构造函数
                                       // 构造一个临时对象,作为参数传递进入函数内部

Color{}
    or 
Color{0xff, 0xff, 0xff}
都会创建一个临时对象,只不过这个对象我们用户没有办法去引用它,用完之后会立即被销毁

例子:

#include <iostream>

using namespace std;

class Test {

    private:
        int m_x;
        int m_y;
    public:
        Test(int x, int y) : m_x(x), m_y(y) {
            cout << "构造函数" << endl;
        }

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

        void show() {
            cout << "m_x:" << m_x << endl;
            cout << "m_y:" << m_y << endl;
        }
};

/*
    Test t(形式参数) = t(实际参数);
    函数调用时把实际参数赋值给形式参数
    形式参数t时实际参数t的一个副本
    调用了"拷贝构造函数"(利用一个已有对象生成一个新对象)
*/
void fun(Test t) {
    t.show();
    // Test{10, 11}; // 此处仅仅是创建一个临时对象,和t对象没有关系
    // t.show();
}

Test fun2() {
    return Test{20, 21}; // 把临时对象作为一个返回值
}

int main() {
    
    // Test t{100, 101};
    // t.show();

    // Test{}; // 调用无参构造函数创建临时对象
    // cout << "---------" << endl;
    // Test{100, 101};
    // cout << "---------" << endl;
    
    // fun(t);

    /* 
        Test t = Test{12, 13}; 
        先创建一个无名对象,再利用这个无名对象生成一个形参t对象
        但是编译器会产生优化,会直接把名字对象构造到它要赋值的对象中去
        可以关闭编译器的这种优化,编译的时候在后面加上:
            -fno-elide-constructors
        fun(Test{12, 13}); // 生成一个临时变量,作为函数的参数
    */
    // fun(Test(12, 13)); // 生成一个临时对象作为函数的参数
    
    Test rt = fun2(); // 使用函数的返回值对象去初始化rt对象
                      // 会产生编译器优化        
    /* 
        理解:
            因为作用域的原因,会创建一个第三方对象,两者都可以访问的对象
                拷贝构造函数
            所以会有一次构造,三次析构
    */

    rt.show();
    cout << "-------" << endl;

    return 0;
}

03对象的实例化

实例化:根据已有的类型创建对象的过程

        可以在栈中实例化对象

        可以在堆中实例化对象

        可以创建全局的对象


全局变量

在栈中创建对象(局部对象)

        形式和定义普通变量一致

                数据类型 变量名;

                ------>

                类名  对象名{};  // 调用无参构造函数,实例化一个普通对象

                        or

                类名  对象名(参数 ... ); // 调用有参构造函数,实例化一个普通对象

                

        定义一个对象数组(一组对象)

                数组  ---> 元素类型  数组名[元素个数];

                                ======>

                                类名  数组名[元素个数];

         // Test ta[10]; // 实例化一个对象数组,调用无参构造函数

        Test tb[10] = {{1, 2}, {3, 4}, {5, 6}}; // 前面3个调用有参构造函数,后面7个调用无参构造函数

        for (int i = 0; i < 10; i++) {

                tb[i].show;

        }
 

在动态内存中创建对象(动态内存分配)

        C语言使用malloc等函数来分配动态内存,使用free释放动态内存,C++也可以使用这些函数

        Test *pt = (Test *)malloc(sizeof(Test));

        

        但是malloc和free处理自定义类型的时候(类),功能不够完善,仅仅只能分配内存空间,不会自动调用构造函数和析构函数

        

        C++中提供了两个运算符(new / delete)用来分配和释放动态内存,会自动调用构造函数和析构函数

        new 用来分配动态内存,delete 用来释放动态内存


        new 是根据类型来自动的计算需要分配的空间的大小,返回对应类型的指针

        如果提供初始值,分配空间的时候还能进行初始化动作(调用有参构造函数)

        如果不提供初始值,分配空间的时候,数字默认初始化为0,如果是类类型,会调用无参构造函数初始化对象

        

        格式
                类型  *指针名 = new 类型;  // 如果是类类型,调用无参构造函数,没有会报错

                类型 * 指针名 = new 类型(初值);
                类型  *指针名 = new 类型{初值};  // 建议这样!!! 调用有参构造函数

                delete 指针名;


                也可以申请一组对象:
                        
类型 *指针名 = new 类型[元素个数];
                      
  or
                       类型 *指针名 = new 类型[元素个数]{初始值};

                       

                       delete [] 指针名;

int *p = new int{1024};
cout << *p <<endl;
delete p;

// Test *tp = new Test;
Test *tp = new Test{1, 2};
tp->show();
delete tp; 
=================================================
也可以申请一组对象:
    类型 *指针名 = new 类型[元素个数];
        or
    类型 *指针名 = new 类型[元素个数]{初始值};

    delete [] 指针名;

// int *p = new int[10];
int *p = new int[10]{1, 2, 3, 4, 5, 6};
for (int i = 0; i < 10; i++) {
    cout << p[i] <<endl;
}
cout << sizeof(p) <<endl;
delete [] p;

// Test *pa = new Test[10]; // 调用10次构造函数
Test *pa = new Test[10]{{1, 2}, {3, 4}, {5, 6}};
for (int i = 0; i < 10; i++) {
    pa[i].show();
}
delete [] pa; //调用10次析构函数

注意:
        栈内存是自动管理的,不能使用delete去释放栈上面创建的对象
            Test t{100, 101};
            delete &t;  // ERROR

        在实际开发中,new和delete一般成对出现,以保证能够及时的释放不再使用的空间,防止内存泄漏
        

        malloc 和 free 不会调用构造函数和析构函数
        new / delete 会自动调用构造函数和析构函数

        new 是根据类型来自动的计算需要分配的空间的大小,返回对应类型的指针

04布尔类型(bool)

虽然在C语言中也可以使用布尔类型,但是在C++中,布尔类型成为了内置类型之一

类似于int,double,...... 基本类型


布尔类型用于表示数学上面的逻辑概念,它只有两种值:真(1) 和 假(0)

使用true表示真,使用false表示假

布尔类型占用多大的空间呢?

        sizeof(bool) == 1

        理论上来说,bool类型使用 1 bit 就可以表示,为什么会分配一个字节呢?

                内存的分配是以字节为单位的,所以 bool类型 至少占用一个字节

作用:
        经常用于条件判断和函数返回(从逻辑上来说,bool只有两个值)

                可以提高程序的可读性

                可以提高程序的运行效率

                int isEmpty( ... ) {

                        ......

                }

                bool isEmpty( ... ) {

                        ......

                }

布尔类型也可以像数字一样进行运算(很少这样使用),如:

        book ok = true;

        ok = ok + 100; // 语法不报错

        cout << ok << endl; // 1

        

如果bool类型变量的值为:0,NULL,nullptr,false,值为0的表达式等 都表示假

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值