从一道题目来看学习 C++ 的类对象的构造
朋友某天在 QQ 上问我一道 C++ 的题目:
class A
{
public:
A()
{
cout << "A";
}
~A()
{
cout << "~A";
}
};
class B
{
public:
B(const A &a) : _a(a)
{
cout << "B";
}
~B()
{
cout << "~B";
}
private:
A _a;
};
int main()
{
A a;
B b(a);
return 0;
}
他很疑惑为什么这个结果输出 AB~B~A~A
而不是 AAB~B~A~A
,给出的理由是 B 类中有个 A 类的对象 _a,B 在构造的时候会一一去构造它的成员,在构造 A 的对象 _a 时会调用 A 的构造函数输出 A 了。
这就让我总结一下 C++ 中对象构造相关的知识点了,会以题作为例子来展开。
默认构造函数
默认构造函数是指用户在定义一个类时,但并没有显示地定义构造函数,那么编译器会隐式地合成一个构造函数,该默认构造函数会为你完成类成员的构造。如我定义了一个类,该类有三个数据成员,分别为 int 型、string 型、vector 型:
class A {
private:
int var_int;
string var_str;
vector<char> vec_char;
};
A a;
该类并没有定义构造函数,那么编译器会自动给你合成一个构造函数,默认构造函数并不是什么事情都不干,它会按照类成员的定义顺序依次构造这些成员。也就是在申请 A 的对象内存空间上分别构造出 int、string 和 vector 对象,对于 C++ 内置类型,如 int、char、double 等类型,系统并不会初始化内置类型的对象,对于其他类类型,会调用这些类型的默认构造函数来构造并初始化这些对象。那么对于此例中,var_int 中存的不一定为 0,而是随机的值,而 var_str 一定为空字符串,因为 string 类型的默认构造函数是构造出一个空字符串,同样 vec_char 是一个长度为0 的 vector 对象。
自定义构造函数
这里如果我们想在构造 A 的对象时,可以初始化 A 对象成员,就需要自定义构造函数,我们先看看一个典型的构造函数的模样:
className (parameters list) : initializer list
{
constructor body
}
以类名作为函数名,可以包含0个或多个参数列表,如果有初始化列表,需要在参数列表括号后以冒号分隔,函数体与普通函数一样,但记住不能有返回值。
如上面例子,如果在定义 A 的对象时需要初始化 A 的成员 var_int 为 42,var_str 为 “helloworld”,有两种自定义构造函数(但这两种构造函数不能同时出现):
class A {
public:
//method 1
A()
{
var_int = 42;
var_str = "helloworld";
}
//method 2
A() : var_int(42), var_str("helloworld")
{
}
private:
//...
};
A a;
上面定义了两种没有带参数的构造函数,都可以达到我们在定义 A 对象时初始化成员的目的,但却有本质的区别,区别在于对于 string 类型的 var_str 在第一种构造方法中,首先调用默认构造函数构造了一个空的 string 对象,然后在函数体中赋值为 “helloworld”;而第二种方法直接在初始化列表中对 var_str 进行初始化为 “helloworld”,其实调用的是 string 的拷贝构造函数来构造了 var_str。很明显第二种方式的效率更高。
我们可以得出结论:调用自定义构造函数构造类的对象时,编译器默认会插入一些代码,用来按照类成员定义的顺序调用默认构造函数构造这些成员,如果某些成员出现在初始化列表中,对这些成员的构造则会调用相应的构造函数来构造这些成员;其次才会执行构造函数体的代码。
回到开始题目中
解释了这么多,再回头看看开始的题目,定义了两个对象 A 和 B:
A a;
B b(a);
- 定义 A 对象时,很显然会调用自定义的构造函数
A()
,输出 A - 定义 B 对象时,调用带有一个参数的构造函数
B(const A &a)
,根据上面的解释,在执行函数体代码之前会默认构造类 B 的成员,如果成员出现在初始化列表中,就会调用相应的构造函数进行构造。那么对于这个例子 B 类中有个成员对象A _a
并且出现在初始化列表中,对成员 _a 的构造会调用A(const A &)
这样形式的构造函数。也许你会有疑问,这样的构造函数在类 A 中并没有定义啊,其实这是一个拷贝构造函数,如果用户没有显示地定义编译器也会默认地合成一个。这样对成员 _a 的构造就是调用默认拷贝构造函数,什么都不输出,然后再调用函数体执行,输出 B - 在函数返回时会对对象进行析构,析构的顺序和构造的顺序相反,因此会先析构 b,输出 ~B;再析构 B 中对象成员 _a,输出 ~A;最后析构 a,输出 ~A。
我们可以通过几个例子来验证我们所说的,
1. 在类 A 中添加一个自定义的拷贝构造函数:
A(const A &a)
{
cout << "copy A";
}
看看结束输出什么?
2.将类 B 的构造函数改成赋值 _a:
B(const A &a)
{
_a = a;
cout << "B";
}
看看结果输出什么?
3.在 2 的基础上在类 A 中添加赋值操作符函数:
A &operator = (const A &a)
{
cout << "assign A";
return *this;
}
看看结果又输出什么?
comments powered by Disqus