02 October 2014

朋友某天在 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