构造函数(constructors)
对象(object)在生成过程中通常需要初始化变量或分配动态内存,以便我们能够操作,或防止在执行过程中返回意外结果。
例如,在前面的例子 类Class(一) 中,如果我们在调用函数 set_values( ) 之前就调用了函数 area(),将会产生什么样的结果呢?
可能会是一个不确定的值,因为成员 width 和 height 还没有被赋于任何值。
为了避免这种情况发生,一个 class 可以包含一个特殊的函数:构造函数 constructor,它可以通过声明一个与 class 同名的函数来定义。
当且仅当要生成一个 class 的新的实例 (instance)的时候,也就是当且仅当声明一个新的对象,或给该 class 的一个对象分配内存的时候,
这个构造函数将自动被调用。下面,我们将实现包含一个构造函数的Rectangle class:
|
rect area: 12
rectb area: 30
|
这个例子的输出结果与前面一个没有区别。在这个例子中,我们只是把函数 set_values 换成了class 的构造函数 constructor。注意这里参数是如何在 class 实例 (instance)生成的时候传递给构造函数的:
Rectangle rect (3,4);
Rectangle rectb (5,6);
同时你可以看到,构造函数的原型和实现中都没有返回值(return value),也没有 void 类型声明。构造函数必须这样写。一个构造函数永远没有返回值,也不用声明 void,就像我们在前面的例子中看到的。
一致性初始化
上面调用构造函数的方式:Rectangle rect (3,4); 被称为函数形式(functional form)。除此之外,构造函数的调用还有其它的语法:
- 如果构造函数只有一个参数,调用时,可以用等式语法:
class_name object_name = initialization_value;
- 最近,C++ 介绍了另外一种调用语法:一致性初始化(Uniform initialization)。语法是把括号() 换成 大括号 {}
class_name object_name { value, value, value, ... }
- object_name 和 大括号之间的等号可有可无
下面这个例子,使用4种方式调用构造函数:
#include <iostream>
using namespace std;
class Circle {
double radius;
public:
Cricle(double r) { radius = r; }
double circum() { return 2*radius*3.14159265;}
};
int main () {
Circle foo (10.0); //functional form
Circle bar = 20.0; //等式语法
Circle baz {30.0}; //uniform init
Circle qux = {40.0}; //带等式的 uniform init
cout << "foo's circumference: " << foo.circum() << '
';
return 0;
}
使用一致性初始化的优势是:使用函数形式调用构造函数(使用括号()),会和函数调用方式混淆,但使用大括号{} 这种语法就不会了。并且这种语法会显示调用默认构造函数:
Rectangle rectb; // default constructor called
Rectangle rectc(); // function declaration (default constructor NOT called)
Rectangle rectd{}; // default constructor called
关于默认构造函数,在构造函数重载这一篇里面有所提及。大部分开发者习惯使用函数形式调用构造函数,一些新的风格指南推荐使用大括号的方式。
构造函数中的成员初始化
使用构造函数初始化其他成员变量的时候,有两种方式:
1. 在方法体(body)中对这些变量赋值,
2. 通过在方法体前插入冒号(:),冒号后跟上一系列初始化值 (用逗号隔开)。这种方式被称为成员初始化(member initialization)
比如下面的例子:
class Rectangle {
int width,height;
public:
Rectangle(int,int);
int area() {return width*height;}
};
对构造函数声明后,接下来进行函数实现。按照 方式1 的做法是:
Rectangle::Rectangle(int x, int y) { width=x, height=y;}
方式2 的做法如下:
Rectangle::Rectangle(int x, int y) : width(x) {height=y;}
更激进一点:
Rectangle::Rectangle (int x, int y) : width(x), height(y) {};
使用成员初始化的情形
对于基本类型的初始化,上面两种方式没有区别。但是,如果某个成员变量是 类对象呢?这种成员变量在声明的时候,默认调用它的默认构造函数初始化的。这会产生两个问题:
1. 假设类 A 的某个成员变量是 类对象 b,属于类B。如果 b 在 类A 的构造函数中被重新初始化了(比如重新赋值),那么之前 b 走的默认构造路线就是一种浪费
2. 如果类B 此时没有默认构造函数怎么办?就会报错:no matching function for call to 'B::B()'
通过冒号(:) 的形式使用 成员初始化 可以避免上面的问题:
#include <iostream>
using namespace std;
class Circle {
double radius;
public:
Circle(double r) : radius(r) { }
double area() {return radius*radius*3.14159265;}
};
class Cylinder {
Circle base;
double height;
public:
Cylinder(double r, double h) : base (r), height(h) {}
double volume() {return base.area() * height;}
};
int main () {
Cylinder foo (10,20);
cout << "foo's volume: " << foo.volume() << '
';
return 0;
}
上面的代码,在类 Cylinder 中有类型是 Circle 的成员变量 base。类 Circle 没有默认构造函数,只有一个带参数的构造函数。在类 Cylinder 中,声明对象 base 的时候,它的默认构造函数就会被调用:
Circle base;
类 Cylinder 的构造函数又需要调用类 Circle 的构造函数去初始化 base,唯一的办法就是把初始化放在 成员初始化列表里面,也就是冒号(:) 后面的用逗号隔开的列表中。这样就避免了上面提到的问题。
成员初始化的语法中还可以使用一致性初始化语法(大括号{}):
Cylinder::Cylinder (double r, double h) : base{r}, height{h} { }