C++/copy/Destructor(析构函数)

本文详细介绍了C++中的析构函数,包括析构函数的作用、何时被调用以及合成析构函数的概念。讨论了析构函数与复制构造函数、复制赋值操作符的关系,并提醒了开发者在处理含有指针成员时需要注意的内存释放问题。此外,还阐述了如何禁止对象复制以及复制控制成员可能导致的合成成员函数为delete的情况。

析构函数(destructor)

析构函数(destructor)和结构函数(constructor)的作用恰好相反,结构函数(constructor)是对对象的非静态成员(nonstatic)进行初始化或者一些其他的操作,而析构函数则是将对象撤销并把对象的非静态成员撤销。
析构函数是一个类的成员函数,它的名字是在类名加一个前缀“”,没有返回值,也没有参数表,例如:

class Foo{
public:
    ~Foo()
    {
    cout<<"Foo has been destructed!"<<endl;
    //通过输出提示告诉我们析构函数确实被调用了
    }   
    //...
};

注意:由于没有参数,所以不能被重载。在一个类中总是存在一个析构函数。

析构函数作用过程

  正如结构函数有初始化部分和函数体,析构函数也有析构部分和函数体。在结构函数中,类成员在函数体执行之前就已经被初始化,且成员初始化的顺序和在类中声明的上下顺序相同。在析构函数当中,函数体先执行,然后才是类成员被撤销,且撤销的顺序和成员在类中的声明顺序相反
  在析构函数体中,类设计者可以对对象进行操作,析构函数,最后撤销该对象并且结束其生命周期。对于一个析构函数,并不像结构函数那样存在初始化部分(初始值list),它的析构部分是隐藏的,成员如何被撤销取决于成员的类型,比如,一个类型为类的成员被它自己的析构函数所撤销,内嵌类型有自己的析构函数,不需要管它是如何被撤销的。

析构函数何时被调用

析构函数自动用于在该类型的对象被撤销的时候:

  1. 变量被撤销在跳出一个作用域,比如函数内部声明定义的变量,在函数调用结束之后,撤销掉在此函数生命周期内的变量。
  2. 对象的成员被撤销
  3. 容器中的元素在容器被撤销之后也被撤销
  4. 动态分配产生的对象,该对象被delete后
  5. 语句中temp 对象,在的表达式结束以后被撤销:for(int i….
    例如:
    对于Sales_data这样一个类的一个片段
returnType Sales_data::func(/*...*/)
{//新作用域
    //p和p2都指向一个动态分配的对象
    Sales_data *p =new Sales_data;       //p是一个内嵌的指针
    Sales_data item(*p);                 //copy结构函数复制*p到item中
    vector<Sales_data> vec;              //类成员对象
    vec.push_back(*p);                   //复制p指向的对象到vec中
    delete p;                            //析构函数作用于p所指向的对象//跳出当前作用域,析构函数将作用于item,vec,p2;
 // 撤销vec的同时,撤销其中的元素

合成析构函数(The Synthesized Destructor)

编译器会自动为那些没有定义析构函数的类型定义合成析构函数(Synthesized Destructor)。也就是说,类设计者没有定义显式定义析构函数(destructor), 编译器就会自动生成合成析构函数,他的作用就像一个函数体没有内容的析构函数例如:

class Sales_data{
public:
    //...
    ~Sales_data(){/*nothing*/}
    //...
}

成员函数在析构函数函数体执行以后自动被撤销,对于string类型,它的析构函数撤销所占内存。值得主义的是## 析构函数的函数体并不会撤销掉成员,成员的撤销实在析构函数函数体执行结束之后。
其他博主对于合成析构函数的高见可见此

析构函数和复制结构函数及复制赋值操作符函数

很多时候我们不一定知道到底需不需要复制结构函数(copy constructor)和复制赋值操作符(copy-assignment operator),但是我问总是知道定义一个类时我们需不需要析构函数(destructor),一旦我们需要析构函数,我们时常都会需要赋值结构函数和复制赋值重载符函数。下面举例说明一下:

class Hasptr{
public:
    Hasptr(const std::string &s=std::string()):
        ps(new std::string(s)),i(0){}
    ~Hasptr(){delete ps;}
public:
std::string *ps;
int i;
};

Tips:合成析构函数是不会撤销删除一个指针成员!!!,所以我们需要在析构函数的函数体中实现删除指针的工作
注意:我们这里并没有定义复制结构函数和复制赋值重载符函数!!!
对于下面这样的一个函数

Hasptr func(Hasptr hp)
{
    Hasptr ret=hp;
    return ret;
}

看上面这个函数,形参hp和作用域中定义的ret, 两个变量的类型均是Hasptr, 再上面我们提到并没有声明定义复制结构函数和复制复制操作符!上述语句复制的过程中,就满足合成复制赋值操作符函数ret.p=rhp.p,则两个指针指向同一地址,在该函数生命周期结束之后,两个变量,ret, hp, 都执行析构函数,则对同一地址的对象被删除两次,这就呵呵了,error!!!
例如下面这个例子:

Hasptr p("May the force be with you!my master");
func(p);
Hasptr q(p);

这有发生什么情况了,实参p传递进入到func中指向统一地址的对象被删除两次,所以,然后p无效了,最后q用p初始化(这里使用合成复制结构函数)也就和上面的ret=hp一个道理,最后也导致q无效了。

Tip:当我们发现这个类需要析构函数,几乎白分子八九十了,可以写上复制结构函数和复制赋值操作符函数,以避免指针复制之类的问题;

class Hasptr{
public:
    Hasptr(const std::string &s=std::string()):
        ps(new std::string(s)),i(0){}
    ~Hasptr(){delete ps;}
    Hasptr(const Hasptr &orig)i(orig.i),ps(new std::string(*orig.ps)){}
    //复制指针指向的string
    Hasptr &operator=(const Hasptr &rhs){
    i=rhs.i;
    *ps=*rhs.ps;   //复制指针指向的string
    return *this;
    }
public:
std::string *ps;
int i;
};
Hasptr func(Hasptr hp)
{
    Hasptr ret=hp;
    return ret;
}

Hasptr p("May the force be with you!my master");
func(p);
Hasptr q(p);
cout<<*q.ps<<endl;//may the force be with you!my master

禁止复制操作

什么时候我们不希望同类型之间的对象相互复制了,比如,iostream类就禁止复制,从而防止多个对象读写同一快IO buffer,然而并不是我们不定义复制控制成员函数就可以实现禁止对象之间的复制,因为编译器会自动生成合成复制函数和合成复制赋值操作符函数。这时我们可以使用在参数表后加上=delete,例如:

struct Nocopy{
Nocopy()=default;
Nocopy(const Nocopy& )=delete;
Nocopy &operator=(const Nocopy& )=delete;
~Nocopy()=default;

Tips:=default相比,=delete必须出现再声明函数的时候,也可以对任何函数进行修饰

WARNING: 析构函数不能设置为delete成员,否则就会导致析构函数失效,造成的结果是不能定义一个该类(析构函数是delete修饰的)的对象,不能删除动态分配的对象。

复制控制成员也会可能导致合成成员函数为delete

如果一个类没有定义结构函数,编译器就会自动生成一个默认的合成函数,这样也可能导致这些合成函数被delete修饰,以下是造成合成函数成员被delete修饰的原因:

  1. 如果一个新类的成员所对应的类中的析构函数被delete修饰,或者该析构函数是在private下
  2. 如果一个新类的成员所对应的类的复制结构函数被delete修饰,或者该复制函数在private下,那新类的合成复制函数也是delete修饰的
  3. 复制赋值操作符同上理
  4. 如果一个新类的成员所对应的类的结构函数被修饰为delete或者该类有一个成员是被delete修饰或者不可读取(private),又或者存在一个引用没有被在所在类中初始化,或者存在一个const类型的成员且该类没有显式定义默认的结构函数或者成员在类中没有初始值。

### 析构函数的使用场景与必要性 析构函数作为C++中的一个重要概念,主要用于管理对象生命周期结束时的行为。以下是关于析构函数的具体使用场景及其重要性的详细介绍。 #### 1. **资源释放** 对象在其生命周期内可能持有多种资源,例如动态分配的内存、文件句柄或网络连接等。当这些资源不再需要时,必须确保它们被正确释放以避免资源泄漏。析构函数在此过程中扮演着至关重要的角色,因为它会在对象销毁时自动调用,从而提供了一种可靠的机制来清理这些资源[^1]。 #### 2. **基类指针指向派生类对象的情况** 在面向对象编程中,经常会出现通过基类指针或引用操作派生类对象的情形。此时,如果基类的析构函数不是虚函数,则仅会调用基类的析构函数而忽略派生类的部分,进而引发潜在的风险,如资源未完全释放等问题。为此,定义虚析构函数成为解决此问题的标准实践,它可以保证即使是从基类指针删除派生类对象也能正确调用相应的派生类析构函数[^4]。 #### 3. **私有析构函数的应用** 将析构函数声明为私有可以限制外界直接销毁某个类的对象实例。这种技术常应用于单例模式(Singleton Pattern),其中我们希望整个程序只有一个全局共享的实例存在。通过使析构函数变为私有,我们可以阻止其他部分随意破坏这个唯一的实例状态[^3]。 #### 实际应用举例 考虑如下情况: - 创建了一个包含动态数组的大数据结构; - 打开了多个文件进行读写操作; 以上两种情形都需要特别注意在适当时候关闭文件或者释放堆上的存储空间以免造成浪费甚至崩溃现象发生。借助良好的析构函数设计即可轻松应对这些问题。 ```cpp #include <iostream> using namespace std; class MyClass { public: MyClass() { cout << "Constructor Called\n"; } ~MyClass() { cout << "Destructor Called\n"; /* Here you would add code to release resources */ } }; void function(MyClass obj){ } int main(){ MyClass myObject; function(myObject); // Copy constructor and destructor will be invoked here. return 0; } ``` 上面的例子展示了简单版本的构造和析构流程演示。每当创建一个新的`MyClass`型别的实体(`myObject`)的时候,“Constructer Called”消息就会显示出来;同样地,在任何此类别物件消亡之前——无论是因为离开了局部作用域还是其它原因——都会先看到“Destructed Called”的通知再继续后续动作。 --- ### 总结 综上所述,合理运用析构函数不仅有助于维持软件系统的健康运转状况,而且还可以提升整体性能表现以及降低错误发生的可能性。特别是在涉及继承关系或是需严格管控单一实例的情况下显得尤为重要。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值