万字详解C++类的初始化的五种语义

五种语义概述

1、析构函数
2、拷贝构造函数
3、拷贝赋值运算符
4、移动构造函数
5、移动赋值运算符

析构函数

析够函数是一个类的生命周期结束时候,自动调用的终结函数,主要用于资源的释放等。
析构函数demo

#include <iostream>
#include <cstdio>

class MyClass
{
public:
    MyClass()
    {
        NumCount = new int(10);
        printf("this is %s class constructor\n", __FUNCTION__);
    }
    ~MyClass()
    {
        delete NumCount;
        NumCount = nullptr;
        printf("this is %s class destructor\n", __FUNCTION__);
    }

private:
    int *NumCount = nullptr;
};

int main()
{
    MyClass obj;
    return 0;
}

运行效果

this is MyClass class constructor
this is ~MyClass class destructor

如果我们修改以下代码

#include <iostream>
#include <cstdio>

class MyClass
{
public:
    MyClass()
    {
        NumCount = new int(3);
        NumCount[0] = 0;
        NumCount[1] = 1;
        NumCount[2] = 2;
        printf("this is %s class constructor\n", __FUNCTION__);
    }

    // ~MyClass()
    // {
    //     delete NumCount;
    //     NumCount = nullptr;
    //     printf("this is %s class destructor\n", __FUNCTION__);
    // }

public:
    int *GetPtr()
    {
        return NumCount;
    }

private:
    int *NumCount = nullptr;
};

int main()
{
    int * ptr = nullptr;
    {
        MyClass obj;
        ptr = obj.GetPtr();
    }
    printf("ptr[0] : %d\n", ptr[0]);
    printf("ptr[1] : %d\n", ptr[1]);
    printf("ptr[2] : %d\n", ptr[2]);
    return 0;
}

运行结果

this is MyClass class constructor
ptr[0] : 0
ptr[1] : 1
ptr[2] : 2

可以看到在注释掉MyClass的析构函数后,MyClass的生命周期已经结束了,但是NumCount的资源未被释放掉。这种资源泄漏的危害是十分巨大的。

当我们注释掉MyClass的析构函数,编译器会自动生成默认的析够函数,其语义如下:

~MyClass() = default;

默认的析构函数只能释放上面的资源,对于手动分配的上的资源是无法释放的。因此在写一个类的时候,一定要显式声明出其对应的析构函数

拷贝构造函数

拷贝构造函数即用一个已有对象初始化另一个同类型对象。

MyClass a;         // 普通构造
MyClass b = a;     // 调用拷贝构造
MyClass c(a);      // 同样调用拷贝构造

拷贝构造demo

#include <iostream>
#include <cstdio>

class MyClass
{
public:
    MyClass() //构造函数
    {
        printf("this is %s class constructor\n", __FUNCTION__);
    }

    MyClass(const MyClass &) //拷贝构造函数
    {
        printf("this is %s class copy construction \n", __FUNCTION__);
    }

    ~MyClass() //析构函数
    {
        printf("this is %s class destructor\n", __FUNCTION__);
    }

};

int main()
{
    MyClass obj1;
    MyClass obj2 = obj1;
    MyClass obj3(obj1);
    return 0;
}

运行效果

this is MyClass class constructor
this is MyClass class copy construction 
this is MyClass class copy construction 
this is ~MyClass class destructor
this is ~MyClass class destructor
this is ~MyClass class destructor

可以看到obj2obj3都是调用拷贝构造去创建的。现在我们将显式的拷贝构造变成默认的。即不写拷贝构造,或者将拷贝构造写成default。

#include <iostream>
#include <cstdio>

class MyClass
{
public:
    MyClass() //构造函数
    {
        a = 1;
        printf("this is %s class constructor\n", __FUNCTION__);
    }

    MyClass(const MyClass &) = default;// 默认构造函数显式声明,等同将这句代码删掉

    ~MyClass() //析构函数
    {
        printf("this is %s class destructor\n", __FUNCTION__);
    }

public:
    int GetData()
    {
        return a;
    }

    void SetData(const int &data)
    {
        a = data;
    }

private:
    int a = 0;

};

int main()
{
    MyClass obj1; //构造函数
    printf("obj1 a : %d\n", obj1.GetData());

    MyClass obj2 = obj1; //拷贝构造
    printf("obj2 a : %d\n", obj2.GetData());

    obj1.SetData(3);
    MyClass obj3(obj1); //拷贝构造
    printf("obj3 a : %d\n", obj3.GetData());

    return 0;
}

运行结果

this is MyClass class constructor
obj1 a : 1
obj2 a : 1
obj3 a : 3
this is ~MyClass class destructor
this is ~MyClass class destructor
this is ~MyClass class destructor

obj1调用构造函数将a初始化为1
obj2调用拷贝构造,将obj1中的a数值拷贝给了obj2中的a,所以obj2中的a = 1
obj1通过SetData接口将a设置为3
obj3调用拷贝构造,将obj1中的a数值拷贝给了obj3中的a,所以obj3中的a = 3

现在我们修改一下代码:

#include <iostream>
#include <cstdio>

class MyClass
{
public:
    MyClass() //构造函数
    {
        NumCount = new int(3);
        printf("this is %s class constructor\n", __FUNCTION__);
    }

    MyClass(const MyClass &) = default;

    ~MyClass() //析构函数
    {
        delete NumCount;
        NumCount = nullptr;
        printf("this is %s class destructor\n", __FUNCTION__);
    }

    int * GetPtr()
    {
        return NumCount;
    }

private:
    int *NumCount = nullptr;

};

int main()
{
    MyClass obj1; //构造函数
    printf("obj1 NumCount : %p\n", obj1.GetPtr());

    MyClass obj2 = obj1; //拷贝构造
    printf("obj2 NumCount : %p\n", obj2.GetPtr());

    MyClass obj3(obj1); //拷贝构造
    printf("obj3 NumCount : %p\n", obj3.GetPtr());

    return 0;
}

运行结果

this is MyClass class constructor
obj1 NumCount : 0x953eb0
obj2 NumCount : 0x953eb0
obj3 NumCount : 0x953eb0
this is ~MyClass class destructor
free(): double free detected in tcache 2
已放弃 (核心已转储)

可以看到三个对象中的NumCount地址是一样的,到在通过默认拷贝构造中,对于分配在上面的成员变量即NumCount发生的是浅拷贝obj2,obj3只是将obj1中的NumCount地址拷贝了,并没有分配其新的空间。导致在析构时候对同一个地址进行**多次释放(又称 Double Free)**从而使得程序挂掉。

我们重新实现拷贝构造函数,实现深拷贝。

#include <iostream>
#include <cstdio>

class MyClass
{
public:
    MyClass() //构造函数
    {
        NumCount = new int(size);
        NumCount[0] = 0;
        NumCount[1] = 1;
        NumCount[2] = 2;
        printf("this is %s class constructor\n", __FUNCTION__);
    }

    MyClass(const MyClass &other)
    {
        if (NumCount == nullptr) {
            NumCount = new int(other.size);
            for (int i =  0; i < size; i++) {
                NumCount[i] = other.NumCount[i];
            }
        }
    }

    ~MyClass() //析构函数
    {
        delete[] NumCount;
        NumCount = nullptr;
        printf("this is %s class destructor\n", __FUNCTION__);
    }

    int * GetPtr()
    {
        return NumCount;
    }

private:
    int size = 3;
    int *NumCount = nullptr;

};

int main()
{
    MyClass obj1; //构造函数
    printf("obj1 NumCount : %p\n", obj1.GetPtr());
    printf("obj1 NumCount[0] = %d\n", obj1.GetPtr()[0]);
    printf("obj1 NumCount[1] = %d\n", obj1.GetPtr()[1]);
    printf("obj1 NumCount[2] = %d\n", obj1.GetPtr()[2]);

    MyClass obj2 = obj1; //拷贝构造
    printf("obj2 NumCount : %p\n", obj2.GetPtr());
    printf("obj2 NumCount[0] = %d\n", obj2.GetPtr()[0]);
    printf("obj2 NumCount[1] = %d\n", obj2.GetPtr()[1]);
    printf("obj2 NumCount[2] = %d\n", obj2.GetPtr()[2]);

    MyClass obj3(obj1); //拷贝构造
    printf("obj3 NumCount : %p\n", obj3.GetPtr());
    printf("obj3 NumCount[0] = %d\n", obj3.GetPtr()[0]);
    printf("obj3 NumCount[1] = %d\n", obj3.GetPtr()[1]);
    printf("obj3 NumCount[2] = %d\n", obj3.GetPtr()[2]);

    return 0;
}

运行结果

this is MyClass class constructor
obj1 NumCount : 0x17a3eb0
obj1 NumCount[0] = 0
obj1 NumCount[1] = 1
obj1 NumCount[2] = 2
obj2 NumCount : 0x17a42e0
obj2 NumCount[0] = 0
obj2 NumCount[1] = 1
obj2 NumCount[2] = 2
obj3 NumCount : 0x17a4300
obj3 NumCount[0] = 0
obj3 NumCount[1] = 1
obj3 NumCount[2] = 2
this is ~MyClass class destructor
this is ~MyClass class destructor
this is ~MyClass class destructor

可以看到三个对象中的NumCount指针都不一样,NumCount中的数据完全拷贝过来了,程序也没有异常。因此在写构造函数的时候需要注意:
1、如果对象都是在上分配的可以不写拷贝构造函数。或者显式的默认声明出来

MyClass(const MyClass &) = default;

2、如果这个类不会使用拷贝构造

    MyClass obj2 = obj1; //拷贝构造
    MyClass obj3(obj1); //拷贝构造

可设置为delete

MyClass(const MyClass &) = delete;

这样在编译时候既可以检查出报错

code1.cpp: In function ‘int main():
code1.cpp:39:20: error: use of deleted function ‘MyClass::MyClass(const MyClass&)’
     MyClass obj2 = obj1; //拷贝构造
                    ^
code1.cpp:16:5: note: declared here
     MyClass(const MyClass &) = delete;
     ^
code1.cpp:40:22: error: use of deleted function ‘MyClass::MyClass(const MyClass&)’
     MyClass obj3(obj1); //拷贝构造
                      ^
code1.cpp:16:5: note: declared here
     MyClass(const MyClass &) = delete;

拷贝赋值运算符

拷贝赋值运算符是 C++ 中用于把一个对象的值赋给另一个已经存在的对象的运算符函数。
上面我们讲了拷贝构造如下:

    MyClass obj2 = obj1; //拷贝构造
    MyClass obj3(obj1); //拷贝构造

那么拷贝赋值运算符是下述方式的构造

    MyClass obj2; 拷贝构造
   
    MyClass obj3; 
    obj3 = obj2; 拷贝赋值运算符!!!!!!

拷贝赋值运算符demo

#include <iostream>
#include <cstdio>

class MyClass
{
public:
    MyClass()                                       // 构造函数
    {
        Count = 1;
        printf("this is %s class constructor\n", __FUNCTION__);
    }

    MyClass(const MyClass &) = delete;              // 禁用拷贝构造
    MyClass& operator=(const MyClass &) = default;  // 使用默认拷贝赋值运算符

    ~MyClass()                                      // 析构函数
    {
        printf("this is %s class destructor\n", __FUNCTION__);
    }

public:
    void SetCount(const int &num)
    {
        Count = num;
    }

    int GetCount()
    {
        return Count;
    }

private:
    int Count = 0;

};

int main()
{
    MyClass obj1; //构造函数
    MyClass obj2; //拷贝构造

    printf("obj1 Count : %d\n", obj1.GetCount());
    printf("obj2 Count : %d\n", obj2.GetCount());

    obj1.SetCount(6);
    obj2 = obj1;
    printf("obj2 Count : %d\n", obj2.GetCount());

    return 0;
}

运行结果

this is MyClass class constructor
this is MyClass class constructor
obj1 Count : 1
obj2 Count : 1
obj2 Count : 6
this is ~MyClass class destructor
this is ~MyClass class destructor

现在我们修改一下Count的类型

#include <iostream>
#include <cstdio>

class MyClass
{
public:
    MyClass() //构造函数
    {
        if (Count == nullptr) {
            Count = new int(3);
        }
        printf("this is %s class constructor\n", __FUNCTION__);
    }

    MyClass(const MyClass &) = delete;  //拷贝构造

    MyClass& operator=(const MyClass &other) = default;

    ~MyClass() //析构函数
    {
        delete Count;
        Count = nullptr;
        printf("this is %s class destructor\n", __FUNCTION__);
    }

public:
    void SetCount(const int &num)
    {
        *Count = num;
    }

    int *GetCount()
    {
        return Count;
    }

private:
    int * Count = nullptr;

};

int main()
{
    MyClass obj1; //构造函数
    MyClass obj2; //构造函数

    printf("obj1 Count : %d, addr : %p\n", *obj1.GetCount(), obj1.GetCount());
    printf("obj2 Count : %d, addr : %p\n", *obj2.GetCount(), obj2.GetCount());

    obj1.SetCount(6);
    obj2 = obj1;
    printf("obj2 Count : %d, addr : %p\n", *obj2.GetCount(), obj2.GetCount());

    return 0;
}

运行结果

this is MyClass class constructor
this is MyClass class constructor
obj1 Count : 3, addr : 0x893eb0
obj2 Count : 3, addr : 0x8942e0
obj2 Count : 6, addr : 0x893eb0
this is ~MyClass class destructor
free(): double free detected in tcache 2
已放弃 (核心已转储)

在创建obj1和obj2的时候,可以看到 obj1->Count = 0x893eb0, obj2->Count = 0x8942e0;在执行完拷贝赋值运算后,obj2->Count的地址变成了obj1->Count。也就是说执行完拷贝赋值运算obj2obj1中的成员执行了浅拷贝。只是将obj1->Count中的地址拷贝过来。obj1和obj2访问的是同一块空间,此时同时obj2中的Count空间无指针指向,出现了内存泄漏。同时在这两个对象生命周期结束时,执行析构函数会出现,出现“double free” 导致程序直接挂掉。

因此在涉及到分配在上的内存时,拷贝赋值运算符要格外注意。下面是修正版本

#include <iostream>
#include <cstdio>

class MyClass
{
public:
    MyClass() //构造函数
    {
        if (Count == nullptr) {
            Count = new int(3);
        }
        printf("this is %s class constructor\n", __FUNCTION__);
    }

    MyClass(const MyClass &) = delete;  //拷贝构造

    MyClass& operator=(const MyClass &other) //拷贝赋值运算符
    {
        printf("this is %s class copy assignment operator \n", __FUNCTION__);
        if (this == &other) {  // 自赋值保护
            return *this;
        }

        delete Count; //释放构造函数分配的资源
        Count = nullptr;

        Count = new int(*other.Count);
        return *this; //注意返回的是当前对象不是Count !!!!!!!
    }

    ~MyClass() //析构函数
    {
        delete Count;
        Count = nullptr;
        printf("this is %s class destructor\n", __FUNCTION__);
    }

public:
    void SetCount(const int &num)
    {
        *Count = num;
    }

    int *GetCount()
    {
        return Count;
    }

private:
    int * Count = nullptr;

};

int main()
{
    MyClass obj1; //构造函数
    MyClass obj2; //构造函数

    printf("obj1 Count : %d\n", *obj1.GetCount());
    printf("obj2 Count : %d\n", *obj2.GetCount());

    obj1.SetCount(6);
    obj2 = obj1;
    printf("obj2 Count : %d\n", *obj2.GetCount());

    return 0;
}

运行结果

this is MyClass class constructor
this is MyClass class constructor
obj1 Count : 3
obj2 Count : 3
this is operator= class copy assignment operator 
obj2 Count : 6
this is ~MyClass class destructor
this is ~MyClass class destructor

移动构造函数

移动构造函数是 C++11 引入的一个特殊构造函数,用来从另一个对象“窃取”资源,而不是拷贝资源,从而提升性能。

上诉的拷贝构造拷贝赋值运算符中都讲述了double free问题移动构造函数中也和上述一样,再使用默认移动构造函数时候,对于分配在上的数据,只会使用浅拷贝,导致出现double free

#include <iostream>
#include <cstdio>

class MyClass
{
public:
    MyClass() //构造函数
    {
        if (Count == nullptr) {
            Count = new int(3);
        }
        printf("this is %s class constructor\n", __FUNCTION__);
    }

    MyClass(const MyClass &) = delete;  //拷贝构造
    MyClass& operator=(const MyClass &other) = delete; //拷贝赋值运算符
    MyClass(MyClass &&other) = default; //移动构造函数

    ~MyClass() //析构函数
    {
        delete Count;
        Count = nullptr;
        printf("this is %s class destructor\n", __FUNCTION__);
    }

public:
    void SetCount(const int &num)
    {
        *Count = num;
    }

    int *GetCount()
    {
        return Count;
    }

private:
    int * Count = nullptr;

};

int main()
{
    MyClass obj1; //构造函数
    printf("obj1 Count : %d, addr : %p\n", *obj1.GetCount(), obj1.GetCount());
    obj1.SetCount(6);
    MyClass obj2 = std::move(obj1); //构造函数
    printf("obj2 Count : %d, addr : %p\n", *obj2.GetCount(), obj2.GetCount());

    return 0;
}

运行结果

this is MyClass class constructor
obj1 Count : 3, addr : 0xc0feb0
obj2 Count : 6, addr : 0xc0feb0
this is ~MyClass class destructor
free(): double free detected in tcache 2
已放弃 (核心已转储)

可以看到obj1->Countobj2->Count的指针完全一样,在析构的时候出现double free。导致程序挂掉。
在类中如果有使用裸指针的话,一定要注意移动构造函数。下面我们修正上述的问题

#include <iostream>
#include <cstdio>

class MyClass
{
public:
    MyClass() //构造函数
    {
        if (Count == nullptr) {
            Count = new int(3);
        }
        printf("this is %s class constructor\n", __FUNCTION__);
    }

    MyClass(const MyClass &) = delete;  //拷贝构造
    MyClass& operator=(const MyClass &other) = delete; //拷贝赋值运算符
    MyClass(MyClass &&other)
    {
        Count = other.Count;
        other.Count = nullptr;
    }
	
    ~MyClass() //析构函数
    {
    	//C++ 标准规定:delete nullptr; 是安全的,不会出错。 可以不判断是否为nullptr
        if(Count == nullptr)
        {
            delete Count;
            Count = nullptr;
        }
        printf("this is %s class destructor\n", __FUNCTION__);
    }

public:
    void SetCount(const int &num)
    {
        *Count = num;
    }

    int *GetCount()
    {
        return Count;
    }

private:
    int * Count = nullptr;

};

int main()
{
    MyClass obj1; //构造函数
    printf("obj1 Count : %d, addr : %p\n", *obj1.GetCount(), obj1.GetCount());
    obj1.SetCount(6);
    MyClass obj2 = std::move(obj1);
    printf("obj1 addr : %p\n", obj1.GetCount());
    printf("obj2 Count : %d, addr : %p\n", *obj2.GetCount(), obj2.GetCount());

    return 0;
}

运行结果

this is MyClass class constructor
obj1 Count : 3, addr : 0x51ceb0
obj1 addr : (nil)
obj2 Count : 6, addr : 0x51ceb0
this is ~MyClass class destructor
this is ~MyClass class destructor

可以看到obj1->Countobj2->Count的指针完全一样,在move obj2对象后obj2->Count被设置为了nullptr,避免了double free

穿插一个小知识点

struct MyStringHolder {
    std::string s;

    // 方式1:初始化列表
    MyStringHolder(MyStringHolder&& other)
        : s(std::move(other.s)) {}  // 直接调用string的移动构造

    // 方式2:构造体内赋值
    MyStringHolder(MyStringHolder&& other) {
         s = std::move(other.s);  // 先默认构造s,再移动赋值
     }
};

struct Demo {
    std::string s = "world";  // 默认初始值

    // 情况1:用初始化列表 → 忽略默认初始值,直接构造为 "Hello"
    Demo() : s("Hello") {}

    // 情况2:没用初始化列表 → 用默认初始值 "world"
    // Demo() {}

    // 情况3:没默认初始值也没初始化列表 → s 默认构造为空串
    // std::string s; Demo() {}
};

因此在初始化类的成员变量时候,尽量使用初始化成员列表方式

移动赋值运算符

移动赋值运算符是 C++ 中用于把 一个对象的资源搬过来而不是拷贝一份 的运算符函数。
上面我们讲了移动构造如下:

    MyClass obj1;
    MyClass obj2 = std::move(obj1);

那么移动赋值运算符是下述方式

    MyClass obj1; 
    MyClass obj2; 
    obj2 = std::move(obj3);

移动赋值运算:参考上面举的例子这里就不举double free 这种例子了。

移动赋值运算符运算符demo

#include <iostream>
#include <cstring>

class MyClass
{
private:
    char *data;

public:
    MyClass(const char *str = "")
    {
        data = new char[strlen(str) + 1];
        strcpy(data, str);
    }

    ~MyClass()
    {
        delete[] data;
    }

    // 拷贝构造
    MyClass(const MyClass &other) = delete;
    // 移动构造
    MyClass(MyClass &&other) = delete;
    // 拷贝赋值
    MyClass &operator=(const MyClass &other) = delete;

    // 移动赋值
    MyClass &operator=(MyClass &&other) noexcept
    {
        if (this != &other)
        {
            delete[] data;
            data = other.data;
            other.data = nullptr;
        }
        printf("Move assignment\n");
        return *this;
    }

    void print()
    {
        printf("data : %s\n", (data ? data : "null"));
    };
};

int main()
{
    MyClass a("Hello");
    MyClass b("World");

    b = std::move(a); // 调用移动赋值
    a.print();
    b.print();
}

运行结果

Move assignment
data : null
data : Hello

总结

类型范式使用场景
析构~MyClass()对象生命周期结束
拷贝构造MyClass(const MyClass &)MyClass a; MyClass b = a; MyClass c(a);
拷贝赋值运算符MyClass& operator=(const MyClass &other)MyClass obj2; MyClass obj3; obj3 = obj2;
移动构造MyClass(MyClass &&other)MyClass obj1; MyClass obj2 = std::move(obj1);
移动赋值运算符MyClass &operator=(MyClass &&other)MyClass a; MyClass b; b = std::move(a);

–结语–
感谢您的阅读,如有问题可以私信或评论区交流。
^ _ ^

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值