《C++ Primer Plus》第18章:探讨 C++ 新标准(3)

新的类功能

除本章前面提到的显式转换运算符和类内成员初始化外,C++11还新增了其他几个类功能。

特殊的成员函数

在原有 4 个特殊成员函数(默认构造函数、复制构造函数、复制赋值运算符和析构函数)的基础上,C++11新增了两个:移动构造函数和移动赋值运算符。这些成员函数是编译器在各种情况下自动提供的。

前面说过,在没有提供任何参数的情况下,将调用默认构造函数。如果您没有给类定义任何构造函数,编译器将提供一个默认构造函数。这种版本的默认构造函数被称为默认的默认构造函数。对于使用内置类型的成员,默认的默认构造函数不对其进行初始化;对于属于类对象的成员,则调用其默认构造函数。

另外,如果您没有提供复制构造函数,而代码又需要使用它,编译器将提供一个默认的复制构造函数;如果您没有提供移动构造函数,而代码又需要使用它,编译器将提供一个默认的移动构造函数。假定类名为 Someclass,这两个默认的构造函数的原型如下:

Someclass::Someclass(const Someclass &);		// defaulted copy constructor
Someclass::Someclass(Someclass &&);		// defaulted move constructor

在类似的情况下,编译器将提供默认的复制运算符和默认的移动运算符,它们的原型如下:

Someclass & Someclass::operator=(const Someclass &);		// defaulted copy assignment
Someclass & Someclass::operator=(Someclass &&);		// defaulted move assignment

最后,如果您没有提供析构函数,编译器将提供一个。

对于前面描述的情况,有一些例外。如果您提供了析构函数、复制构造函数和复制赋值运算符,编译器将不会提供移动构造函数和移动赋值运算符;如果您提供了移动构造函数和移动赋值运算符,编译器将不会自动提供复制构造函数和复制赋值运算符。

另外,默认的移动构造函数和移动赋值运算符的工作方式与复制版本类似:执行逐成员初始化并复制内置类型。如果成员是类对象,将使用相应类的构造函数和赋值运算符,就像参数为右值一样。如果定义了移动构造函数和移动赋值运算符,这将调用它们;否则将调用复制构造函数和复制赋值运算符。

默认的方法和禁用的方法

C++11 让您能够更好地控制要使用的方法。假定您要使用某个默认的函数,而这个函数由于某种原因不会自动创建。例如,您提供了移动构造函数,因此编译器不会自动创建默认的构造函数、复制构造函数和复制赋值运算符。在这些情况下,您可使用关键字 default 显式地声明这些方法的默认版本:

class Someclass {
public:
	Someclass(Someclass &&);
	Someclass() = default;		// use compiler-generated default constructor
	Someclass(const Someclass &) = default;
	Someclass & operator=(const Someclass &) = default;
...
};

编译器将创建在您没有提供移动构造函数的情况下自动提供的构造函数。

另一方面,关键字 delete 可用于禁止编译器使用特定方法。例如,要禁止复制对象,可禁用复制构造函数和复制赋值运算符:

class Someclass {
public:
	Someclass() = default;		// use-compiler-generated default constructor
	// disable copy constructor and copy assignment operator:
	Someclass(const Someclass &) = delete;
	Someclass & operator=(const Someclass &) = delete;
	// use compiler-generated move contructor and move assignment operator:
	Someclass(Someclass &&) = default;
	Someclass & operator=(Someclass &&) = default;
	Someclass & operator+(const Someclass &) const;
};

第12章说过,要禁止复制,可将复制构造函数和赋值运算符放在类定义的private部分,但使用 delete 也能达到这个目的,且更不容易犯错、更容易理解。

如果在启用移动方法的同时禁用复制方法,结果将如何呢?前面说过,移动操作使用的右值引用只能关联到右值表达式,这意味着:

Someclass one;
Someclass two;
Someclass three(one);	// not allowed, one an lvalue
Someclass four(one+two);	// allowed, expression is an rvalue

关键字 default 只能用于 6 个特殊成员函数,但 delete 可用于任何成员函数。delete 的一种可能用法是禁止特定的转换。例如,假设 Someclass 类有一个接受 double 参数的方法:

class Someclass {
public:
...
	void redo(double);
...
};

再假设有如下代码:

Someclass sc;
sc.redo(5);

int 值 5 将被提升为 5.0,进而执行方法 redo()。

现在假设将 Someclass 类的定义改成了下面这样:

class Someclass {
public:	
...
	void redo(double);
	void redo(int) = delete;
...
};

在这种情况下,方法调用 sc.redo(5) 与原型 redo(int) 匹配。编译器检测到这一点以及 redo(int) 被禁用后,将这种调用视为编译错误。这说明了禁用函数的重要一点:它们只用于查找匹配函数,使用它们将导致编译错误。

委托构造函数

如果给类提供了多个构造函数,您可能重复编写相同的代码。也就是说,有些构造函数可能需要包含其他构造函数中已有的代码。为让编码工作更简单、更可靠,C++11 允许您在一个构造函数的定义中使用另一个构造函数。这被称为委托,因为构造函数暂时将创建对象的工作委托给另一个构造函数。委托使用成员初始化列表语法的变种:

class Notes {
	int k;
	double x;
	std::string st;
public:
	Notes();
	Notes(int);
	Notes(int, double);
	Notes(int, double , std::string);
};
Notes::Notes(int kk, double xx, std::string stt) : k(kk), x(xx), st(stt) { /*do stuff*/}
Notes::Notes() : Notes(0, 0.01, "Oh") {/* do other stuff*/ }
Notes::Notes(int kk) : Notes(kk, 0.01, "Ah") { /* do yet other stuff */ }
Notes::Notes(int kk, double xx ) : Notes(kk, xx, "Uh") {/* ditto */ }

例如,上述默认构造函数使用第一个构造函数初始化数据成员执行其函数体,然后再执行自己的函数体。

继承构造函数

为进一步简化编码工作,C++11 提供了一种让派生类能够继承基类构造函数的机制。C++98提供了一种让名称空间中函数可用的方法:

namespace Box {
	int fn(int) { ... }
	int fn(double) { ... }
	int fn(const char *) { ... }
}
using Box::fn;

这让函数 fn 的所有重载版本都可用。也可使用这种方法让基类的所有非特殊成员函数对派生类可用。

例如,请看下面的代码:

class C1 {
...
public:
...
	int fn(int j) { ... }
	double fn(double w) { ... }
	void fn(const char * s) { ... }
};
...
C2 c2;
int k = c2.fn(3);		// uses C1::fn(int)
double z = c2.fn(2.4);		// uses C2::fn(double)

C2 中的 using 声明让 C2 对象可使用 C1 的三个 fn() 方法,但将选择 C2 而不是 C1 定义的方法 fn(double)。

C++11 将这种方法用于构造函数。这让派生类继承基类的所有构造函数(默认构造函数、复制构造函数和移动构造函数除外),但不会使用与派生类构造函数的特征标匹配的构造函数:

class BS {
	int q;
	double w;
public:
	BS() : q(0), w(0) {}
	BS(int k) : q(k), w(100) {}
	BS(double x) : q(-1), w(x) {}
	B0(int k, double x) : q(k), w(x) {}
	void Show() const { std::out << q << ", " << w << '\n'; }
};

class DR : public BS {
	short j;
public:
	using BS:BS;
	DR() : j(-100) {}  	// DR needs its own default constructor
	DR(double x) : BS(2*x), j(int (x)) {}
	DR(int i) : j(-2), BS(i, 0.5*i) {}
	void Show() const { std::cout << j << ", "; BS::Show(); }
};

int main() {
	DR o1;		// use DR()
	DR o2(18.81);		// use DR(double) instread of BS(double)
	DR o3(10, 1.8);		// use BS(int, double)
	...
}

由于没有构造函数 DR(int, double),因此创建 DR 对象 o3 时,将使用继承而来的 BS(int, double)。请注意,继承的基类构造函数只初始化基类成员;如果还要初始化派生类成员,则应使用成员列表初始化语法:

DR(int i, int k, double x) : j(i), BS(k, x) {}

管理虚方法:override 和 final

虚方法对实现多态类层次结构很重要,让基类引用或指针能够根据指向的对象类型调用相应的方法,但虚方法也带来了一些编程陷阱。例如,假设基类声明了一个虚方法,而您决定在派生类中提供不同的版本,这将覆盖旧版本。但正如第13章讨论的,如果特征标不匹配,将隐藏而不是覆盖旧版本:

class Action {
	int a;
public:
	Action(int i = 0) : a(i) {}
	int val() const { return a; }
	virtual void f(char ch) const { std::cout << val() << ch << "\n"; }
};

class Bingo : public Action {
public:
	Bingo(int i = 0) : Action(i) {}
	virtual void f(char * ch) const { std::cout << val() << ch << "!\n"; }
};

由于类 Bingo 定义的是 f(char * ch) 而不是 f(char ch),将对 Bingo 对象隐藏 f(char ch),这导致程序不能使用类似于下面的代码:

Bingo b(10);
b.f('@');		// works for Action object, fails for Bingo object

在 C++11 中,可使用虚说明符 override 指出您要覆盖一个虚函数:将其放在参数列表后面。如果声明与基类方法不匹配,编译器将视为错误。因此,下面的 Bingo::f() 版本将生成一条编译错误消息:

virtual void f(char * ch) const override { std::cout << val() << ch << "!\n"; }

例如,在 Microsoft Visual C++ 2010 中,出现的错误消息如下:

method with override specifier 'override' did not override any
base class methods

说明符 final 解决了另一个问题。您可能想禁止派生类覆盖特定的虚方法,为此可在参数列表后面家伙是那个 final。例如,下面的代码禁止 Action 的派生类重新定义函数 f():

virtual void f(char ch) const final {
	std::cout << val() << ch << "\n";
}

说明符 override 和 final 并非关键字,而是具有特殊含义的标识符。这意味着编译器根据上下文确定它们是否有特殊含义;在其他上下文中,可将它们用作常规标识符,如变量名或枚举。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值