类(五):构造函数

<基于 C++ Primer P257>
1.构造函数初始值列表
我们在定义变量时习惯于立即对其初始化,而非先定义、在赋值。

string foo = "Hello World!"; // 定义并初始化
string bar;                  // 默认初始化成空 string 对象
bar = "Hello World!";        // 为 bar 赋一个新值

类似的,对于对象的数据成员,如果没有在构造函数的初始值列表中显式地初始化成员,则该成员将在构造函数体之前执行默认初始化。

Sales_data::Sales_data(const string &s, unsigned cnt, double price)
{
	bookNo = s;
	units_sold = cnt;
	revenue = cnt * price;
}

(1)构造函数的初始值有时必不可少
初始化和赋值之间的差异完全依赖于数据成员的类型。
如果成员是 cosnt 或者是引用的话,必须将其初始化。类似的,当成员属于某种类类型且该类型没有定义默认构造函数时,也必须将这个成员初始化。

class ConstRef
{
public:
	ConstRef(int ii):
private:
	int i;
	const int ci;
	int &ri;
};
// 错误:ci 和 ri 必须被初始化
ConstRef::ConstRef(int ii)
{
	i = ii;  // 正确
	ci = ii; // 错误:不能给 const 赋值
	ri = i;  // 错误:ri 没被初始化
}
// 正确:显式地初始化引用和 const 成员
ConstRef::ConstRef(int ii) : i(ii), ci(ii), ri(i) { }

随着构造函数体一开始执行,初始化就完成了。因此,我们初始化 const 或者引用类型过的数据成员的唯一机会就是通过构造函数的初始值。
(2)在很多类中,初始值和赋值的区别事关底层效率问题:前者直接初始化成员,后者则先初始化再赋值。
建议养成使用构造函数初始值的习惯,可以避免某些意想不到的编译错误。
(3)成员初始化的顺序
构造函数初始值列表只说明用于初始化成员的值,而不限定初始化的具体执行顺序。
成员的初始化顺序与它们在类定义中的顺序一致,构造函数初始值列表中初始值的前后位置关系不会影响实际的初始化顺序。
一般而言,初始化的顺序没有什么特别的要求。不过如果一个成员是用另一个成员来初始化的,那么这两个成员的初始化顺序就很关键了。

class X
{
	int i;
	int j;
public:
	X(int val) : j(val), i(j) { } //未定义的:i 在 j 之前被初始化
};

因此,最好令构造函数初始值的顺序与成员声明的顺序保持一致。而且如果可能的话,尽量避免使用某些成员初始化其他成员。
(4)默认实参和构造函数

class Salea_data
{
public:
	// 定义默认构造函数,令其与只接受一个 string 实参的构造函数功能相同
	Sales_data(std::string s = "") : bookNo(s) { }
	// ...
}

如果一个构造函数为所有参数都提供了默认实参,则它实际上也定义了默认构造函数。
2.委托构造函数
一个委托构造函数使用它所属类的其他构造函数执行它自己的初始化过程,或者说它把自己的一些(或者全部)职责委托给了其他构造函数。
一个委托构造函数也有一个成员初始值列表和一个函数体。在委托构造函数内,成员初始值列表只有一个唯一的入口,就是类名本身。和其他成员初始值一样,类名后面紧跟圆括号括起来的参数列表,参数列表必须与类中另外一个构造函数匹配。

class Sales_data
{
public:
	Sales_data(std::string s, unsigned cnt, double price) :
			bookNo(s), units_sold(cnt), revenue(cnt * price) { }
	Sales_data() : Sales_data("", 0, 0) { }
	Sales_data(std::string s) : Sales_data(s, 0, 0) { }
	Sales_data(std::istream &is) : Sales_data() {read(is, *this);}
	// ...
};

当一个构造函数委托给另一个构造函数时,受委托的构造函数的初始值和函数体被依次执行。加入函数体包含有代码的话,将先执行这些代码,然后控制权才会交还给委托者的函数体。
3.默认构造函数的作用
当对象被默认初始化或值初始化时自动执行默认构造函数。类必须包含一个默认构造函数以便在以下情况使用。
(1)默认初始化在以下情况发生:
①在块作用域内不使用任何初始值定义一个非静态变量或者数组时。
②当一个类本身含有类类型成员且使用合成的默认构造函数时。
③当类类型的成员没有在构造函数初始值列表中显式地初始化时。
(2)值初始化在以下情况发生:
①在数组初始化的过程中如果我们提供的初始值数量少于数组的大小时。
②当我们不使用初始值定义一个局部静态变量时。
③当我们通过书写形如 T()的表达式显式地请求值初始化时,其中 T 是类型名。

class NoDefault
{
public:
	NoDefault(const std::string&);
};
struct A
{
	NoDefault my_mem; // 错误:my_mem 没有初始值
};   
A a;                  // 错误:A 没有定义构造函数

(3)使用默认构造函数

Sales_data obj(); // 错误:声明了一个函数而非对象
Sales_data obj2;   // 正确

4.隐式的类类型转换
我们可以为类定义隐式转换规则。如果构造函数只接受一个实参,则它实际上定义了转换为此类类型的隐式转换机制。这种构造函数又被成为转换构造函数。

string null_book = "9-999-99999-9";
// 构造一个临时的 Sales_data 对象
// 该对象的 units_sold 和 revenue 等于 0,bookNo 等于 null_book
item.combine(null_book);

(1)只允许一步类类型转换
编译器只会自动地执行一步类型转换。

// 错误:需要用户定义的两种转换
//(1)"9-999-99999-9" -> string
//(2)string -> Sales_data
item.combine("9-999-99999-9");

// 正确:显式地转换成 string,隐式地转换成 Sales_data
item.combine(string("9-999-99999-9");
// 正确:隐式地转换成 string,显式地转换成 Sales_data
item.combine(Sales_data("9-999-99999-9");

(2)类类型转换不是总有效
是否需要类类型转换取决于我们对用户使用该转换的看法。
(3)抑制构造函数定义的隐式转换
我们可以通过将构造函数声明为 explicit 阻止隐式转换。

class Sales_data
{
public:
	Sales_data() = default;
	Sales_data(const std::string &s, unsigned n, double p) : 
			   bookNo(s), units_sold(n), revenue(p * n) { }
	explicit Sales_data(const std::string &s) : bookNo(s) { }
	explicit Sales_data(std::istream&);
};

item.combine(null_book); // 错误
item.combine(ciin);      // 错误

注意,只能在类内声明构造函数时使用 explicit 关键字,在类外部定义时不应重复。
(4)explicit 构造函数只能用于直接初始化
当我们用 explicit 关键字声明构造函数时,它将只能以直接初始化的形式使用。因为当我们执行拷贝形式的初始化时(使用 = ),同样发生了隐式转换。
(5)为转换显式地使用构造函数
尽管编译器不会将 explicit 的构造函数用于隐式转换过程,但是我们可以使用这样的构造函数显式地强制进行转换。

item.combine(Sales_data(null_book));        // 正确
item.combine(static_cast<Sales_data>(cin)); // 正确

(6)标准库中含有显式构造函数的类
①接受一个单参数的 const char* 的 string 构造函数不是 explicit 的。
②接受一个容量参数的 vector 构造函数是 explicit 的。
5.聚合类
聚合类使得用户可以直接访问其成员,并且具有特殊的初始化语法形式。要求:
①所有成员都是 public 的。
②没有定义任何构造函数。
③没有类内初始值。
④没有基类,也没有 virtual 函数。

struct Data
{
	int ival;
	string s;
};
// vall.ival = 0; vall.s = string("Anna")
Data vall = {0, "Anna"};

初始值的顺序必须与声明的顺序一致。
如果初始值列表中的元素个数少于类的成员数量,则靠后的成员被值初始化。初始值列表的元素个数绝对不能超过类的成员数量。
显式地初始化类的对象有三个缺点:
①要求类的所有成员都是 public 的。
②将正确初始化每个对象的每个成员的重任交给了类的用户。因为用户很容易忘掉某个初始值,或者提供一个不恰当的初始值,所以这样的初始化过程冗长乏味且容易出错。
③添加或删除一个成员之后,所有初始化语句都需要更新。
6.字面值常量类
数据成员都是字面值类型的聚合类是字面值常量类。如果一个类不是聚合类,但它符合下述要求,它也是一个字面值常量类。
①数据成员都必须是字面值类型。
②类必须至少含有一个 constexpr 构造函数。
③如果一个数据成员含有类内初始值,则内置类型成员的初始值必须是一条常量表达式;或者如果成员属于某种类类型,则初始值必须使用成员自己的 constexpr 构造函数。
④类必须使用析构函数的默认定义,该成员负责销毁类的对象。
(1)constexpr 构造函数
尽管构造函数不能是 const 的,但是字面值常量类的构造函数可以是 constexpr 函数。事实上,一个字面值常量类必须至少提供一个 constexpr 构造函数。
constexpr 构造函数可以声明成 = default 的形式。
否则,constexpr 构造函数就必须既符合构造函数的要求(意味着不能包含返回的语句),又符合 constexpr 函数的要求(拥有的唯一可执行语句就是返回语句)。因此,constexpr 构造函数函数体一般来说应该是空的。
constexpr 构造函数必须初始化所有数据成员,初始值或者使用 constexpr 构造函数,或者是一条常量表达式。

class Debug
{
public:
	constexpr Debug(bool b = true) : hw(b), io(b), other(b) { }
	constexpr Debug(bool b, bool i, bool o) : hw(b), io(i), other(o) { }
	constexpr bool any() {return hw||io||other;}
	void set_io(bool b) {io = b;}
	void set_hw(bool b) {hw = b;}
	void set_other(bool b) {other = b;}
private:
	bool hw;
	bool io;
	bool other;
};

constexpr 构造函数用于生成 constexpr 对象以及constexpr 函数的参数或者返回类型。

constexpr Debug io_sub(false, true, false);           // 调试 IO
if (io_sub.any())                                     // 等价于 if (true)
	cerr << print appropriate error message" << endl;
constexpr Debug prod(false);                          // 无调试
if (prod.any())                                       // 等价于 if (false)
	cerr << print an error message" << endl;
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值