[CPP] 构造与析构

本文对 C++ 中对构造与析构函数做一次总结。

构造函数

  • 继承:先父后子。如果继承链为 A->B->C ,那么一次执行 ABC 的构造函数。
#include <iostream>
using namespace std;
class A
{
public:
    A() { cout << "A "; }
};
class B : public A
{
public:
    B() { cout << "B "; }
};
class C : public B
{
public:
    C() { cout << "C "; }
};
int main()
{
    C c;
}
// 输出:A B C
  • 对象组合:假如 A 的成员包含 A1, A2 两个对象,那么先执行成员变量的。
#include <iostream>
using namespace std;
class A1
{
public:
    A1() { cout << "A1 "; }
};
class A2
{
public:
    A2() { cout << "A2 "; }
};
class A
{
public:
    A1 a1;
    A2 a2;
    A() { cout << "A "; }
};
int main()
{
    A a;
}
// 输出:A1 A2 A

总结:构造顺序总是按体积从小到大 。例如,在继承中,父类的「体积」一般比子类小(因为子类包含更多的信息);在对象组合中,成员变量中的对象的体积自然比整个对象要「小」。

析构函数

普通情况:先构造后析构,即 与构造顺序相反

⚠️特殊情况:全局变量,局部 static 变量,普通局部变量同时出现。直接看下面「特殊情况」一节即可。

按照对象关系分类

  • 继承关系
class A
{
public:
    A() { cout << "A "; }
    ~A() { cout << "~A "; }
};
class B : public A
{
public:
    B() { cout << "B "; }
    ~B() { cout << "~B "; }
};
class C : public B
{
public:
    C() { cout << "C "; }
    ~C() { cout << "~C "; }
};
int main()
{
    C c;
}
// 输出:A B C ~C ~B ~A 
  • 对象组合
class A1
{
public:
    A1() { cout << "A1 "; }
    ~A1() { cout << "~A1 "; }
};
class A2
{
public:
    A2() { cout << "A2 "; }
    ~A2() { cout << "~A2 "; }
};
class A
{
public:
    A1 a1;
    A2 a2;
    A() { cout << "A "; }
    ~A() { cout << "~A "; }
};
int main()
{
    A a;
}
// Output: A1 A2 A ~A ~A2 ~A1 

按照对象所在存储区域分类

  • 在栈上
void func()
{
	A a;
	B b;
	C c;
}

那么,构造顺序:a -> b -> c,析构顺序为:c -> b -> a .

如果使用 new 关键字,输出不变(堆其实是一个特殊的栈区域)。

  • 在全局/静态存储区

C/C++ 中定义的全局变量,或者使用 static 修饰的局部变量,都会存储在一个叫「全局/静态存储区」的地方(即 DATA 数据段)。

例如:

A a;
B b;
C c;
int main(){}
// Output: A B C ~C ~B ~A

依旧遵循「先构造后析构」的规则。

特殊情况

但是如果既有普通变量,又有全局变量,还有 static 局部变量,那么就有点搞头了。

C c;
int main()
{
    A *pa = new A();
    B b;
    static D d;
    delete pa;
}

构造顺序:c -> a -> b -> d,析构顺序:~A -> ~B -> ~D -> ~C .

这种情况不符合上述规则。原因:栈上的变量是代码块结束的时候释放,全局/静态数据区的变量是 程序结束时 才释放。 main 函数结束时,程序还没结束。

逐行代码代码看,构造顺序是毫无疑问的。当运行至第 6 行代码时,内存状态如下:

|  b |   d    |
|  a |   c    |
|堆/栈|全局/静态|

执行至第 7 行:释放 A 。

执行至第 8 行:释放 B 。

返回到执行 main 函数的位置(有些汇编中使用符号 __start 或者 __init 来标记 ):依次释放 D 和 C 。

💬 一个有趣的问题: main 函数之前发生了什么?

在这里,在 main 函数执行前,全局变量中的对象应该且必须完成初始化。那么这个初始化动作由谁来完成的呢?我们猜是 main 的调用者来完成的,学过 ICS/CSAPP 这门课的话,应该知道,在 IA32 当中,还有一个 __start/__init 函数位于 main 的更高一层,对程序猿不可见。

至于程序第一行执行的代码是什么?这就是一个更值得探究的问题了(可惜我不会😅。

拷贝构造函数与赋值运算符重载

先看一段代码:

#include<iostream>
using namespace std;
class MyClass
{
public:
    MyClass(int i = 0)
    {
        cout << i;
    }
    MyClass(const MyClass &x)
    {
        cout << 2;
    }
    MyClass &operator=(const MyClass &x)
    {
        cout << 3;
        return *this;
    }
    ~MyClass()
    {
        cout << 4;
    }
};
int main()
{
    MyClass obj1(1), obj2(2);
    MyClass obj3 = obj1;
    return 0;
}
// Output:122444

解析:

MyClass obj3 = obj1; 
obj3 还不存在,所以调用拷贝构造函数输出2,
如果 obj3 存在,obj3=obj,则调用复制运算符重载函数,输出3 

OS:TMD 这都什么破面试题,招的语言律师还是咋滴,人晕了。

posted @ 2020-10-06 17:05  sinkinben  阅读(196)  评论(0编辑  收藏  举报