C++ 类和对象(三)(6)

2022-06-19  本文已影响0人  maskerII

1. 多个对象的构造函数和析构函数

1.如果类存在成员对象,那么先调用成员对象的构造函数,再调用本身的构造函数,析构函数调用顺序反之
2.成员对象的构造函数的调用顺序和定义的顺序一样
3. 注意:如果有成员对象,那么实例化对象时,必须保证成员对象的构造函数和析构函数能被调用

class BMW
{
private:
public:
    BMW()
    {
        cout << "BMW 构造函数" << endl;
    }

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

class Buick
{
private:
public:
    Buick()
    {
        cout << "Buick 构造函数" << endl;
    }

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

class Maker
{
private:
    Buick buick; // 成员对象
    BMW bmw;     // 成员对象

public:
    Maker()
    {
        cout << "Maker 构造函数" << endl;
    }

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

// 多个对象的构造函数和析构函数
// 1.如果类存在成员对象,那么先调用成员对象的构造函数,再调用本身的构造函数,析构函数调用顺序反之
// 2.成员对象的构造函数的调用顺序和定义的顺序一样
// 3. 注意:如果有队员对象,那么实例化对象时,必须保证成员对象的构造函数和析构函数能被调用
void test01()
{
    Maker m;
}

class BMW2
{
public:
    int ma;

public:
    BMW2(int a)
    {
        ma = a;
        cout << "BMW2 构造函数 " << a << endl;
    }

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

运行结果如下:

Maker2 析构函数
BMW2 析构函数
Buick2 析构函数
Maker2 析构函数
BMW2 析构函数
Buick2 析构函数

2. 初始化列表

初始化成员列表,是调用成员对象的制定构造函数

注意1:初始化列表只能写在构造函数
注意2:如果使用了初始化列表,那么所有的构造函数都要写初始化列表。 如果不写拷贝构造函数,那么编译器会使用默认的拷贝构造函数
注意3:如果有多个对象需要制定调用某个构造函数,用逗号隔开
注意4:可以使用对象的构造函数传递数值给成员对象的构造函数

class BMW2
{
public:
    int ma;

public:
    BMW2(int a)
    {
        ma = a;
        cout << "BMW2 构造函数 " << a << endl;
    }

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

class Buick2
{
public:
    int mb;

public:
    Buick2(int b)
    {
        mb = b;
        cout << "Buick2 构造函数" << endl;
    }

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

class Maker2
{
public:
    Buick2 buick; // 成员对象
    BMW2 bmw;     // 成员对象

public:
    // 注意1:初始化列表只能写在构造函数
    // 注意3:如果有多个对象需要制定调用某个构造函数,用逗号隔开
    Maker2(int a, int b) : bmw(a), buick(b)
    {
        cout << "Maker2 构造函数" << endl;
    }
    // 注意2:如果使用了初始化列表,那么所有的构造函数都要写初始化列表
    // 如果不写拷贝构造函数,那么编译器会使用默认的拷贝构造函数
    Maker2(const Maker2 &m) : bmw(40), buick(30)
    {
        cout << "Maker2 拷贝构造函数" << endl;
    }

    ~Maker2()
    {
        cout << "Maker2 析构函数" << endl;
    }
};
void test02()
{
    Maker2 m1(10, 20);
    cout << "m1 " << m1.bmw.ma << endl;
    Maker2 m2(m1);
    cout << "m2 " << m2.bmw.ma << endl;
}

3. explicit 关键词

当构造函数只有一个参数或其他参数有默认值时,加关键字explicit 可以防止编译器优化 Maker m=10; 这种格式
explicit 只能放在构在函数前面

class Maker
{
private:
    int mId;

public:
    // explicit 只能放在构在函数前面,
    // 当构造函数只有一个参数或其他参数有默认值时,加关键字explicit 可以防止编译器优化 Maker m=10; 这种格式
    explicit Maker(int id)
    {
        mId = id;
    }
    ~Maker()
    {
    }
};

void test01()
{
    // Maker m1 = 10; //构造函数前不加关键字explicit,Maker m1 = 10,编译器会优化成 Maker m1 = Maker(10), 加关键字explicit,编译器不会优化,报错
    Maker m1 = Maker(10);
}

4. 深拷贝和浅拷贝

4.1 浅拷贝

同一类型的对象之间可以赋值,使得两个对象的成员变量的值相同,两个对象仍然是独立的两个对象,这种情况被称为浅拷贝.

image.png

一般情况下,浅拷贝没有任何副作用,但是当类中有指针,并且指针指向动态分配的内存空间,析构函数做了动态内存释放的处理,会导致内存问题。

默认的拷贝构造函数,是浅拷贝,可能会导致同一块空间被释放多次

// 浅拷贝
// 默认的拷贝构造函数,是浅拷贝,可能会导致同一块空间被释放多次

class Maker2
{
private:
    char *pName;
    int mAge;

public:
    Maker2(const char *name, int age)
    {
        // 从堆区申请空间
        pName = (char *)malloc(strlen(name) + 1);
        // 写入数据
        strcpy(pName, name);

        mAge = age;
    }
    ~Maker2()
    {
        if (pName != NULL)
        {
            free(pName);
            pName = NULL;
        }
    }
};

// 错误 会崩溃
// 原因 同一块空间被释放多次
// test02() 函数执行完毕后,会调用m1,m2的析构函数,对pName所指向的内存空间释放。
// m2是通过默认拷贝构造函数生成的对象,是简单的赋值操作,是浅拷贝,即m2的pName所指向的内存空间是同一块,
// 这样的话,pName所指向的内存空间会被释放2次,会崩溃

void test02() {
    Maker2 m1("Emily",18);
    Maker2 m2(m1);
    cout << "test02() 函数执行完毕 " << endl;
}

4.2 深拷贝

当类中有指针,并且此指针有动态分配空间,析构函数做了释放处理,往往需要自定义拷贝构造函数,自行给指针动态分配空间,深拷贝。

image.png
// 深拷贝
class Maker3
{
private:
    char *pName;
    int mAge;

public:
    Maker3(const char *name, int age)
    {
        // 从堆区申请空间
        pName = (char *)malloc(strlen(name) + 1);
        // 写入数据
        strcpy(pName, name);

        mAge = age;
    }
    Maker3(const Maker3 &m)
    {
        cout << "自己的拷贝构造函数" << endl;
        pName = (char *)malloc(strlen(m.pName) + 1);
        strcpy(pName, m.pName);

        mAge = m.mAge;
    }
    ~Maker3()
    {
        if (pName != NULL)
        {
            free(pName);
            pName = NULL;
        }
    }
};

void test03()
{
    Maker3 m1("Emily", 20);
    Maker3 m2(m1);
    cout << "test03() 函数执行完毕 " << endl;
}

5. 动态对象创建

5.1 对象创建

当创建一个c++对象时会发生两件事:
1.为对象分配内存
2.调用构造函数来初始化那块内存
第一步我们能保证实现,需要我们确保第二步一定能发生。c++强迫我们这么做是因为使用未初始化的对象是程序出错的一个重要原因。

5.2 C动态分配内存方法

为了在运行时动态分配内存,c在他的标准库中提供了一些函数,malloc以及它的变种calloc和realloc,释放内存的free,这些函数是有效的、但是原始的,需要程序员理解和小心使用。
为了使用c的动态内存分配函数在堆上创建一个类的实例,我们必须这样做:

class Person{
public:
    Person(){
        mAge = 20;
        pName = (char*)malloc(strlen("john")+1);
        strcpy(pName, "John");
    }
    void Init(){
        mAge = 20;
        pName = (char*)malloc(strlen("john")+1);
        strcpy(pName, "John");
    }
    void Clean(){
        if (pName != NULL){
            free(pName);
        }
    }
public:
    int mAge;
    char* pName;
};
int main(){

    //分配内存
    Person* person = (Person*)malloc(sizeof(Person));
    if(person == NULL){
        return 0;
    }
    //调用初始化函数
    person->Init();
    //清理对象
    person->Clean();
    //释放person对象
    free(person);

    return EXIT_SUCCESS;
}

用C语言方式申请堆区空间,会存在以下问题:

1. 程序员必须确定对象的长度**
2. malloc返回一个void指针,c++不允许将void赋值给其他任何指针,必须强转
3. malloc可能申请内存失败,所以必须判断返回值来确保内存分配成功
4. 在使用对象前必须对对象初始化,而对象的构造函数和析构函数不能显示调用,malloc不会调用调用对象的构造函数,释放空间时,不会调用对象的析构函数。**

5.3 new operator

C++中解决动态内存分配的方案是把创建一个对象所需要的操作都结合在一个称为new的运算符里。当用new创建一个对象时,它就在堆里为对象分配内存并调用构造函数完成初始化。

Person* person = new Person;
相当于:
Person* person = (Person*)malloc(sizeof(Person));
    if(person == NULL){
        return 0;
    }
person->Init();

New操作符能确定在调用构造函数初始化之前内存分配是成功的,所有不用显式确定调用是否成功。

5.4 delete operator

new表达式的反面是delete表达式。delete表达式先调用析构函数,然后释放内存。正如new表达式返回一个指向对象的指针一样,delete需要一个对象的地址。

delete只适用于由new创建的对象。
如果使用一个由malloc或者calloc或者realloc创建的对象使用delete,这个行为是未定义的。因为大多数new和delete的实现机制都使用了malloc和free,所以很可能没有调用析构函数就释放了内存。

如果正在删除的对象的指针是NULL,将不发生任何事,因此建议在删除指针后,立即把指针赋值为NULL,以免对它删除两次,对一些对象删除两次可能会产生某些问题。

void test02() {
    // 用new方式申请堆区空间,会调用类的构造函数
    Maker *m = new Maker;

    // 用delete方式释放堆区空间,会调用类的析构函数
    delete m;
    m = NULL; // 防止野指针

    Maker *m1 = new Maker(10);
    delete m1;
    m1 = NULL; // 防止野指针
}

5.5 用于数组的new和delete

//创建字符数组
char* pStr = new char[100];
//创建整型数组
int* pArr1 = new int[100]; 
//创建整型数组并初始化
// 聚合初始化 有的编译器不支持
int* pArr2 = new int[10]{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; 

//释放数组内存
delete[] pStr;
delete[] pArr1;
delete[] pArr2;
void test02() {
    Maker *ms = new Maker[2]; // 调用无参构造
    delete[] ms; // 必须使用delete[] ,不然会崩溃
    // 有的编译器支持,大部分不支持这种写法(聚合初始化)
    // Maker *ms2 = new Maker[2]{Maker(10),Maker(20)};
}

5.6 delete void*

如果对一个void*指针执行delete操作,这将可能成为一个程序错误,除非指针指向的内容是非常简单的,因为它将不执行析构函数.

void test03() {
    void *m = new Maker;
    // 如果用void *来接new的对象,那么delete时不会调用析构函数
    delete m;
    // 在编译阶段,编译器就确定好了函数的调用地址
    // C++编译器不认识void*, 不知道void*指向哪个函数,所以不会调用析构函数
    // 这种编译方式叫做静态联编
}

5.7 C和C++申请和释放堆区空间不要混用

void test04() {
    Maker *m = new Maker;
    free(m); // 不会调用析构函数
}

5.8 使用new和delete采用相同形式

如果在new表达式中使用[],必须在相应的delete表达式中也使用[].如果在new表达式中不使用[], 一定不要在相应的delete[]表达式中使用[]

以下代码会出问题

Person* person = new Person[10];
    delete person;

以上代码有什么问题吗?(vs下直接中断、qt下析构函数调用一次)
使用了new也搭配使用了delete,问题在于Person有10个对象,那么其他9个对象可能没有调用析构函数,也就是说其他9个对象可能删除不完全,因为它们的析构函数没有被调用。

我们现在清楚使用new的时候发生了两件事: 一、分配内存;二、调用构造函数,那么调用delete的时候也有两件事:一、析构函数;二、释放内存。

那么刚才我们那段代码最大的问题在于:person指针指向的内存中到底有多少个对象,因为这个决定应该有多少个析构函数应该被调用。换句话说,person指针指向的是一个单一的对象还是一个数组对象,由于单一对象和数组对象的内存布局是不同的。更明确的说,数组所用的内存通常还包括“数组大小记录”,使得delete的时候知道应该调用几次析构函数。单一对象的话就没有这个记录。单一对象和数组对象的内存布局可理解为下图:

image.png

本图只是为了说明,编译器不一定如此实现,但是很多编译器是这样做的。

当我们使用一个delete的时候,我们必须让delete知道指针指向的内存空间中是否存在一个“数组大小记录”。当我们使用delete[],那么delete就知道是一个对象数组,从而清楚应该调用几次析构函数。

上一篇下一篇

猜你喜欢

热点阅读