C++ 类的构造与析构

类的构造

1
User(int id, string name, string phone);

可以采用如下格式来进行类实例的创建:

1
2
3
4
5
6
7
User user = User(1, "Jone", "123456");
User user(1, "Jone", "123456");
User* user = new User(1, "Jone", "123456");

User user = { 1, "Jone", "123456" }; //C++ 11
User user{ 1, "Jone", "123456" }; //C++ 11
User* user = new User{ 1, "Jone", "123456" }; //C++ 11

如果User类有默认构造函数,则下面方式可以直接构造一个类:

1
2
User user;  //此处不是变量的声明,而是类实例的创建
User* user = new User; //此处也是使用默认构造函数

对于只有一个参数的构造函数,可以采用赋值的方式创建一个类实例:

1
A a = 10;  //构造函数为 A(int id);

这种形式容易带来问题,可以使用explicit来修饰只有一个参数的构造函数,就能禁用这种类的构造写法。

构造函数分类

  • 明确定义的可以传参的普通构造函数;
  • 默认构造函数,如果没有明确定义构造函数,那么编译器就会生成一个默认的构造函数;
  • 拷贝构造函数,如果没有明确定义拷贝构造函数,编译器会默认生成一个拷贝构造函数,默认的拷贝构造函数是浅拷贝,如果需要实现深拷贝,则需要明确定义一个;
  • 赋值运算符,建议和构造函数一起定义,而不要只定义其中一个【参考】;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class A {
public:
//明确定义的默认构造函数
A() { cout << "A()" << endl; }
//可以传参的普通构造函数
A(string name) : mName(name) { cout << "A(" << name << ")" << endl; }
//拷贝构造函数
A(const A& other) : mName(other.mName) { cout << "A copy(" + other.mName + ")" << endl; }

string getName() { return mName; }
void setName(string name) { mName = name; }
private:
string mName;
};

int main() {
A a("a1");
A a1 = a; //调用的拷贝构造函数
return 0;
}

上述代码的输出结构是:

1
2
A(a1)
A copy(a1)

从输出中可以看出赋值操作符调用了拷贝构造函数;

类的初始化顺序

根据ISO/IEC 14882:1998(E) 中12.6.2一节的介绍。

Initialization shall proceed in the following order:

  • First, and only for the constructor of the most derived class as described below, virtual base classes shall be initialized in the order they appear on a depth-first left-to-right traversal of the directed acyclic graph of base classes, where “left-to-right” is the order of appearance of the base class names in the derived class base-specifier-list.
  • Then, direct base classes shall be initialized in declaration order as they appear in the base-specifier-list (regardless of the order of the mem-initializers).
  • Then, nonstatic data members shall be initialized in the order they were declared in the class definition (again regardless of the order of the mem-initializers).
  • Finally, the body of the constructor is executed.

上面的引用文字我没有完全看懂。但可以大体解释如下:

  • 首先,对基类进行初始化;
  • 然后,初始化非静态数据成员。对于在初始化成员列表中的数据成员,编译器会使用列表中的值对数据成员进行初始化;没有出现在初始化成员列表中的数据成员,编译器会按照默认方式进行初始化(如果是类,就是调用类的默认构造函数);
  • 最后,构造函数体会被执行。

初始化成员列表(member initializer list)

执行构造函数时,先执行初始化列表的内容,若初始化里面没有数据,则编译器按照系统默认的方式对成员变量赋值,随后再进行构造函数中花括号内部的指令。

  • 对于引用型成员变量和const常量型成员变量,则必须通过初始化列表初始化该成员变量。
  • 如果成员类或者基类没有默认构造函数,则必须通过初始化表初始化该成员变量。
  • 成员变量初始化顺序有声明顺序决定,而与初始化列表的顺序无关。
  • 对于参数列表中所列的成员变量,可以不包含所有的成员变量。

出于效率方面的考虑:对于有默认构造函数的成员类,编译器会在构建父类之前调用成员类的默认构造函数,如果在构造函数中又对该成员类进行赋值,则该成员类的构造函数调用了两次。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class A {
public:
A() { cout << "A()" << endl; }
A(string name) { cout << "A(" << name << ")" << endl; }
};

class B {
public:
B(A& a);
private:
A _a;
};

B::B(A& a) {
cout << "B()" << endl;
this->_a = a;
}

int main() {
A a("a1");
B b(a);
return 0;
}

上述程序的执行结果是:

1
2
3
A(a1)
A()
B()

可以看出A的默认构造函数在B构造函数之前被执行了,这不符合预期,我们期望只有A(a1),而A()是多余的。如果使用初始化列表的方式,则不存在这个问题,将B的构造函数改成如下形式:

1
2
3
B::B(A& a) : _a(a) {
cout << "B()" << endl;
}

修改之后的程序运行结果如下,这样就符合预期了:

1
2
A(a1)
B()

构造和析构顺序

C++一个原则是:先构造后析构,在类的继承关系中,父类先构造,子类后构造;而析构时,则是子类先析构,父类后析构。如果在一个含注重,栈中构造了先后构造了A、B两个类(注意不是在堆中),则函数退出时,B先执行析构,然后才是A的析构。

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
class GrandFather {
public:
GrandFather() { std::cout << "construct grandfather" << std::endl; }
~GrandFather() { std::cout << "destruct grandfather" << std::endl; }
};

class Father : public GrandFather {
public:
Father() { std::cout << "construct father" << std::endl; }
~Father() { std::cout << "destruct father" << std::endl; }
};

class Son : public Father {
public:
Son() { std::cout << "construct son" << std::endl; }
~Son() { std::cout << "destruct son" << std::endl; }
};


int main()
{
Son* f = new Son();
delete f;
return 0;
}
1
2
3
4
5
6
construct grandfather
construct father
construct son
destruct son
destruct father
destruct grandfather

可以看出来,构造函数的顺序是先父类后子类,而析构则是先子类后父类。

析构函数导致的资源泄漏

如果main函数改为下面代码:

1
2
3
4
5
6
int main()
{
Father* f = new Son();
delete f;
return 0;
}
1
2
3
4
5
construct grandfather
construct father
construct son
destruct father
destruct grandfather

Son的析构函数没有调用,同样如果改成GrandFather* f = new Son();SonFather的析构函数都不会被调用。如果析构函数中需要进行资源释放,那么就会导致资源泄漏问题。要解决此问题,需要通过virtual来修饰析构函数。可以只修饰GrandFather的析构,也可以全部都加上virtual。

父类被多次构造

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
class GrandFather {
public:
GrandFather() { std::cout << "construct grandfather" << std::endl; }
~GrandFather() { std::cout << "destruct grandfather" << std::endl; }
};

class Father1: public GrandFather {
public:
Father1() { std::cout << "construct father1" << std::endl; }
~Father1() { std::cout << "destruct father1" << std::endl; }
};

class Father2: public GrandFather {
public:
Father2() { std::cout << "construct father2" << std::endl; }
~Father2() { std::cout << "destruct father2" << std::endl; }
};

class Son: public Father1, Father2 {
public:
Son() { std::cout << "construct son" << std::endl; }
~Son() { std::cout << "destruct son" << std::endl; }
};

int main() {
Son* f = new Son();
delete f;
return 0;
}

运行程序的输出结果:

1
2
3
4
5
6
7
8
9
10
construct grandfather
construct father1
construct grandfather
construct father2
construct son
destruct son
destruct father2
destruct grandfather
destruct father1
destruct grandfather

可以看到GrandFather被构建了两次,这明显不符合预期。要解决这个问题需要在继承时添加virtual关键字,如下:

1
2
class Father1: virtual public GrandFather {};
class Father2: virtual public GrandFather {};

数组与构造函数

1
Stock stocks[4];

这个数据会构造4个Stock类,即,调用了4次Stock的默认构造函数。如果数组的元素类没有默认构造函数可以使用下面形式:

1
2
3
4
5
6
Shop shops[4] = {
Stock(0, "name"),
Stock(1, "name"),
Stock(2, "name"),
Stock(3, "name")
};

参考链接