• C++类和动态数组



    析构函数

    在类属性中使用动态变量有一个问题。在函数中即使使用局部指针变量创建动态变量,且局部指针变量在函数调用结束时离去,除非调用 delete,否则动态变量仍会保存在内存中。不调用 delete 销毁动态变量,动态变量就会一直占用内存空间,这会导致程序因耗尽自由存储而终止。此外,将动态变量嵌入类的实现中,由于使用该类的程序员并不知道动态变量的存在,所以不能指望他们帮你调用 delete。事实上,由于数据成员通常是私有成员,所以程序员通常不能访问所需的指针变量,所以根本不能为这些指针变量调用 delete。为了解决这个问题,C++ 提供了称为 析构函数 的特殊成员函数。

    析构函数(destructor) 是成员函数,在类的对象离开作用域时自动调用。换言之,如函数包含局部变量,而且这个局部变量是提供了析构函数的对象,那么函数调用终止时会自动调用析构函数。如果正确定义了析构函数,析构函数就会调用 delete 销毁由对象创建的所有动态变量。为达到 “析构” 的目的,可能只需要调用一次 delete,也可能需要调用多次。可让析构函数执行其他清理工作,但将内存回收到自由存储是析构函数的主职。

    析构函数的定义为 ~ + 类名,其与构造函数的定义类似,只是前面多出一个 ~ 符号,比如 StringVar 类的析构函数就为 ~StringVar。析构函数不能指定返回值类型,无参数,所以每个类只能有一个析构函数,不能为类重载析构函数。除了这些区别,析构函数的定义方式与其他成员函数相同。

    下面借助下面的示例进行说明:

    #include 
    #include 
    #include 
    #include 
    using namespace std;
    
    class StringVar
    {
    public:
        StringVar(const char a[]);
        // 前条件:数组 a 包含以 '\0' 终止的一组字符
        // 初始化对象,使它的值成为a中存储的字符串
        // 并使其以后能设置成最大长度为strlen(a)的字符串值
    
        ~StringVar();
        // 析构函数
    
        friend ostream& operator <<(ostream& outs, const StringVar& theString);
    
    private:
        char *value; //指向容纳字符串值的动态数组的指针
    };
    void conversation();
    // 开始与用户的对话
    int main()
    {
        conversation();
        return 0;
    }
    
    StringVar::StringVar(const char a[])
    {
        value = new char[strlen(a)+1];
        strcpy(value, a);
    }
    
    StringVar::~StringVar()
    {
        delete [] value;
    }
    ostream& operator <<(ostream& outs, const StringVar& theString)
    {
        outs << theString.value;
        return outs;
    }
    
    void conversation()
    {
        StringVar ourname("Borg");
        cout <<"We are "<< ourname <<endl;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51

    上面定义的析构函数 ~StringVar() 调用 delete 销毁成员指针变量 value 指向的动态数组。分析 conversation 函数,局部变量 ourName 会创建动态数组。如果类没有析构函数,在对 conversation 的调用结束后,动态数组仍会占用内存,即使它们对于程序来说已完全无用。对于这个示范程序,似乎不是很严重的问题,因为在对 conversation 的调用结束之后,程序会马上终止。但如果写程序反复调用 conversation 这样的函数,而且 StringVar 类没有合适的析构函数,函数调用就会不断消耗自由存储中的内存,直至所有内存都消耗殆尽,造成程序不得不异常终止。


    拷贝构造函数

    **拷贝构造函数(copy constructor)**要求获取一个参数,该参数具有与类相同的类型。该参数必须传引用,而且通常要在前面附加 const 参数修饰符,使它成为常量参数。除此之外,拷贝构造函数的定义和用法与其它任何构造函数完全相同。

    例如,我们在上面的代码中使用拷贝构造函数。

    StringVar ourname("Brog");
    StringVar myname(ourname);
    
    • 1
    • 2

    成员变量 myname.value 不能简单地设置成与 ourname.value 相同的值,那样会造成两个指针指向同一个动态数组,即浅拷贝。

    StringVar::StringVar(const StringVar& stringObject)
    {
        value = new char[10];
        strcpy(value, stringObject.value);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    而应该像上面的代码新建一个动态数组,并将一个动态数组的内容拷贝到另一个动态数组。这就是深拷贝。

    具体地说,拷贝构造函数会在三种情况下自动调用:

    • 声明类的对象,并由同类型的另一个对象初始化。
    • 函数返回类类型的值。
    • 在传值形参的位置“插入”类类型的实参。

    下面举一个例子说明没有拷贝构造函数会出现什么问题。

    void showString(StringVar theString)
    {
    	cout<<"The string is: "
    		<< theString << endl;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    再给定以下代码,其中包括一个函数调用:

    StringVar greeting("Hello");
    showString(greeting);
    cout << "After call: " << greeting << endl;
    
    • 1
    • 2
    • 3

    假定没有拷贝构造函数,那么具体过程是:执行函数调用时,greeting 的值复制给局部变量 theString,所以 theString.value 被设置成与 greeting,value 相等。但是,这些是指针变量,所以在函数调用期间,theString.valuegreeting.value 指向同一个动态数组。

    函数调用结束之后,会调用 StringVar 的析构函数,将 theString 使用的内存返回给自由存储。由于 greeting.valuetheString.value 指向同一个动态数组,所以删除 theString.value 就是删除 greeting.value。那么之和执行 cout<<"After call:"< 就是未定义的。如果 greeting 对象是某些函数的局部变量,就会出现重大问题。在这种情况下,析构函数调用等价于:

    delete [] greeting.value
    
    • 1

    重复调用 delete 来删除同一个动态数组,可能会造成严重的系统错误,并导致程序崩溃。


    重载赋值操作符

    假设 string1string2 像下面这样声明:

    StringVar string1("abc"), string2("xyz");
    
    • 1

    下面的赋值函数,会将 string2 的属性拷贝给 string1

    string1 = string2;
    
    • 1

    当然这里为浅拷贝,如果需要深拷贝,要重载赋值操作符。重载赋值操作符与重载其他操作符不同,重载赋值操作符必须是类的成员,而不能是类的友元。

    class StringVar
    {
    public:
    	void operator =(const StringVar& rightSide);
    	// 重载赋值操作符=,将字符串从一个对象复制到另一个
    
    • 1
    • 2
    • 3
    • 4
    • 5

    函数定义如下:

    void StringVar::operator =(const StringVar& rightSide)
    {
    	for(int i=0; i<strlen(rightSide.value); ++i)
    		value[i] = rightSide.value[i];
    	value[strlen(rightSide.value)] = '\0';
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    Big Three

    拷贝构造函数、操作符= 以及 析构函数统称为 Big Three。专家认为,如果需要定义其中一个,就必须定义全部三个。缺少任何一个,编译器都会帮你创建它,只是可能达不到你预期的效果。所以,有必要每次都自己定义。假如所有成员变量都具有预定义了类型(比如 intdouble),那么编译器生成的拷贝构造函数和重载的 = 能很好的工作。但假如类中包含类或指针成员变量,它们就可能表现失常,对于使用指针和操作符 new 的任何类,最保险的做法就是定义自己的拷贝构造函数、重载的操作符=以及析构函数。

  • 相关阅读:
    深入浅出排序算法之希尔排序
    SwiftUI4.0在iOS 16中新添加的inner和drop阴影效果
    智慧河湖方案:AI赋能水利水务,构建河湖智能可视化监管大数据平台
    【C语言】#define宏与函数的优劣对比
    前端开发常用网站
    Jenkins+RebotFramework 持续环境集成
    【数据结构】线性表的抽象数据类型
    力扣刷题训练(二)
    dreamweaver作业静态HTML网页设计 大学美食菜谱网页制作教程(web前端网页制作课作业)
    【洛谷】P3378 【模板】堆
  • 原文地址:https://blog.csdn.net/weixin_44491423/article/details/126082435