现代c++编程c++11/14/17/20:Chapter 02: Language Usability Enhancements

在运行前声明、定义一个变量或常量、控制代码流、面向对象的函数、模板编程等操作可能发生在编写代码或编译器编译代码时。为此,我们通常谈论语言可用性,它指的是在运行时之前发生的语言行为。

2.1 Constants

2.1.1 nullptr

nullptr的用途似乎取代了NULL。 在某种意义上,传统的c++将NULL和0看作是一样的东西,这取决于编译器如何定义NULL,一些编译器将NULL定义为((void*)0),一些将直接定义为0。

c++不允许隐式地将void * 转换为其他类型。但是如果编译器试图将NULL定义为((void*)0),那么在下面的代码中:

char *ch = NULL;

没有void *隐式转换的c++必须将NULL定义为0。这仍然产生了一个新问题。将NULL定义为0将导致c++中的重载特性令人困惑。

考虑以下两个foo函数:

void foo(char*);
void foo(int);

然后foo(NULL)语句将调用foo(int),这将导致代码违反直觉。为了解决这个问题,c++ 11引入了nullptr关键字,它是专门用来区分空指针0的。nullptr的类型是nullptr_t,它可以被隐式地转换为任何指针或成员指针类型,并且可以与它们相等或不相等地进行比较。

You can try to compile the following code using clang++:

#include <iostream>
#include <type_traits>
void foo(char *);
void foo(int);
int main() {
	if (std::is_same<decltype(NULL), decltype(0)>::value ||
		std::is_same<decltype(NULL), decltype(0L)>::value)
		std::cout << "NULL == 0" << std::endl;
	if (std::is_same<decltype(NULL), decltype((void*)0)>::value)
		std::cout << "NULL == (void *)0" << std::endl;
	if (std::is_same<decltype(NULL), std::nullptr_t>::value)
		std::cout << "NULL == nullptr" << std::endl;
	
	foo(0); // will call foo(int)
	// foo(NULL); // doesn't compile
	foo(nullptr); // will call foo(char*)
	
	return 0;
}

void foo(char *) {
std::cout << "foo(char*) is called" << std::endl;
}
void foo(int i) {
std::cout << "foo(int) is called" << std::endl;
}

输出:

foo(int) is called
foo(char*) is called

从输出中我们可以看到NULL不同于0和nullptr。所以,养成直接使用nullptr的习惯。

另外,在上面的代码中,我们使用了现代c++语法decltype和std::is_same。简单地说,decltype用于类型派生,std::is_same用于比较两种类型的相等性。我们将在稍后的decltype部分详细讨论它们。

2.1.2 constexpr

c++本身已经有了常量表达式的概念,比如1+ 2,3 *4。这样的表达式总是产生相同的结果而没有任何副作用。如果编译器能在编译时直接优化并将这些表达式嵌入到程序中,将会提高程序的性能。一个非常明显的例子是数组的定义阶段:

#include <iostream>
#define LEN 10

int len_foo() {
	int i = 2;
	return i;
}

constexpr int len_foo_constexpr() {
	return 5;
}

constexpr int fibonacci(const int n) {
	return n == 1 || n == 2 ? 1 : fibonacci(n-1) + fibonacci(n-2);
}

int main() {
	char arr_1[10]; // legal
	char arr_2[LEN]; // legal

	int len = 10;
	// char arr_3[len]; // illegal
	const int len_2 = len + 1;
	constexpr int len_2_constexpr = 1 + 2 + 3;
	// char arr_4[len_2]; // illegal, but ok for most of the compilers
	char arr_4[len_2_constexpr]; // legal
	// char arr_5[len_foo()+5]; // illegal
	char arr_6[len_foo_constexpr() + 1]; // legal
	// 1, 1, 2, 3, 5, 8, 13, 21, 34, 55
	std::cout << fibonacci(10) << std::endl;

	return 0;
}

在上面的例子中,char arr_4[len_2]可能会令人困惑,因为len_2被定义为一个常量。为什么char arr_4[len_2]仍然是非法的?这是因为数组的长度在c++标准必须是一个常量表达式,对len_2来说,这是一个const常数,常数表达式,因此,即使这种行为支持大多数编译器,但它是一种违法行为,我们需要使用constexpr特性介绍了c++ 11日将介绍下,为了解决这个问题;对于arr_5,在c++ 98之前,编译器无法知道len_foo()实际上返回一个常量a。

注意,大多数编译器现在都有自己的编译器优化。在编译器的优化下,许多非法行为变成了合法行为。如果需要重新生成错误,则需要使用旧版本的编译器。

c++ 11提供了constexpr让用户显式地声明函数或对象构造函数将在编译时成为常量表达式。这个关键字显式地告诉编译器它应该验证len_foo应该是一个编译时常量表达式。常数表达式。

此外,constexpr的函数可以使用递归:

constexpr int fibonacci(const int n) {
	return n == 1 || n == 2 ? 1 : fibonacci(n-1) + fibonacci(n-2);	
}	

从c++ 14开始,constexpr函数可以在内部使用简单的语句,如局部变量、循环和分支。例如,以下代码不能在c++ 11标准下编译:

constexpr int fibonacci(const int n) {
	if(n == 1) return 1;
	if(n == 2) return 1;
	return fibonacci(n-1) + fibonacci(n-2);
}

为了做到这一点,我们可以写一个简化版本,像这样,使函数符合c++ 11标准:

constexpr int fibonacci(const int n) {
	return n == 1 || n == 2 ? 1 : fibonacci(n-1) + fibonacci(n-2);
}

2.2 Variables and initialization

2.2.1 if-switch

在传统的c++中,变量的声明可以声明临时变量int,它可以位于任何位置,甚至在for语句中,但是在if和switch语句中始终没有方法声明临时变量。例如:

#include <iostream>
#include <vector>
#include <algorithm>

int main() {
	std::vector<int> vec = {1, 2, 3, 4};

	// after c++17, can be simplefied by using `auto`
	const std::vector<int>::iterator itr = std::find(vec.begin(), vec.end(), 2);

	if (itr != vec.end()) {
		*itr = 3;
	}

	if (const std::vector<int>::iterator itr = std::find(vec.begin(), vec.end(), 3);	
		itr != vec.end()) {
		*itr = 4;
	}

	// should output: 1, 4, 3, 4. can be simplefied using `auto`
	for (std::vector<int>::iterator element = vec.begin(); 
		element != vec.end(); ++element)
		std::cout << *element << std::endl;
}

在上面的代码中,我们可以看到itr变量是在整个main()的范围内定义的,这导致我们在一个变量需要再次遍历整个std::vector时重命名另一个变量。c++ 17消除了这个限制,所以我们可以在if(或switch)中这样做:

if (const std::vector<int>::iterator itr = std::find(vec.begin(), vec.end(), 3);
	itr != vec.end()) {
	*itr = 4;
}

和Go类似吗?

2.2.2 Initializer list

初始化是一种非常重要的语言特性,最常见的是对象初始化的时候。在传统的c++中,不同的对象有不同的初始化方法,比如普通的数组,PODs(没有构造函数,析构函数和虚函数的类)或者结构类型可以用{}来初始化,这就是我们所说的初始化列表。对于类对象的初始化,需要使用copy构造,或者使用()。这些不同的方法是彼此特定的,不能是通用的。

c++ 11首先要解决这个问题,结合初始化列表的概念类型和调用std:: initializer_list,允许构造函数或其他功能使用初始化列表参数,初始化的类对象提供了一个统一的普通初始化数组和POD方法之间的桥梁、。例如:

#include <initializer_list>
#include <vector>

class MagicFoo {
public:
	std::vector<int> vec;
	MagicFoo(std::initializer_list<int> list) {
		for (std::initializer_list<int>::iterator it = list.begin();
			it != list.end(); ++it)
			vec.push_back(*it);
		}
	};

int main() {
// after C++11
	MagicFoo magicFoo = {1, 2, 3, 4, 5};
	std::cout << "magicFoo: ";
	
	for (std::vector<int>::iterator it = magicFoo.vec.begin(); it != magicFoo.vec.end(); ++it) 
		std::cout << *it << std::endl;

}

这个构造函数被称为初始化列表构造函数,在初始化期间将特别关注这个构造函数的类型。

除了构造对象外,初始化列表还可以作为普通函数的形参,例如:

public:
void foo(std::initializer_list<int> list) {
	for (std::initializer_list<int>::iterator it = list.begin(); it != list.end(); ++it) 
		vec.push_back(*it);
}

magicFoo.foo({6,7,8,9});

第二,c++ 11为初始化任意对象中还提供了一种统一的语法,例如:

Foo foo2 {3, 4};

2.2.3 Structured binding

结构化绑定提供的功能类似于其他语言提供的多个返回值。在容器这一章中,我们将了解到c++ 11添加了一个std::tuple 容器,用于构造包含多个返回值的元组。但缺陷是c++ 11/14不提供一个简单的方法,直接从元组定义元素的元组,虽然我们可以解压元组使用std::tie, 但是我们仍然要非常清楚这个元组包含多少个对象,每个对象是什么类型的,很麻烦的。

c++ 17完成了这个设置,结构化绑定让我们可以这样写代码:

#include <iostream>
#include <tuple>
std::tuple<int, double, std::string> f() {
	return std::make_tuple(1, 2.3, "456");
}
int main() {
	auto [x, y, z] = f();
	
	std::cout << x << ", " << y << ", " << z << std::endl;
	
	return 0;
}

auto类型派生在auto类型推断部分中进行了描述。

2.3 Type inference

在传统的C和c++中,参数的类型必须明确定义,这对我们快速编码没有帮助,特别是当我们面对大量复杂的模板类型时,我们必须明确指明变量的类型才能继续下去。随后的编码,不仅降低了我们的开发效率,而且还使代码变得冗长而糟糕。

c++ 11引入了两个关键字auto和decltype来实现类型派生,让编译器担心变量的类型。这使得c++与其他现代编程一样语言的方式提供的习惯不用担心变量类型。

2.3.1 auto

auto在c++中已经存在很长时间了,但它总是作为一种存储类型的指示符存在,与register共存。在传统的c++中,如果一个变量没有声明为寄存器变量,它就会被自动当作自动变量处理。

使用auto进行类型派生的最常见和最值得注意的示例之一是迭代器。您应该在前一节中看到用传统c++进行冗长的迭代编写:

// before C++11
// cbegin() returns vector<int>::const_iterator
// and therefore itr is type vector<int>::const_iterator
for(vector<int>::const_iterator it = vec.cbegin(); itr != vec.cend(); ++it)

当我们有auto:

#include <initializer_list>
#include <vector>
#include <iostream>

class MagicFoo {
public:
	std::vector<int> vec;
	MagicFoo(std::initializer_list<int> list) {
		for (auto it = list.begin(); it != list.end(); ++it) {
			vec.push_back(*it);
		}
	}
};

int main() {
	MagicFoo magicFoo = {1, 2, 3, 4, 5};
	
	std::cout << "magicFoo: ";
	
	for (auto it = magicFoo.vec.begin(); it != magicFoo.vec.end(); ++it) {
		std::cout << *it << ", ";
	}
	
	std::cout << std::endl;
	
	return 0;
}

其他一些常见用法:

auto i = 5; // i as int
auto arr = new auto(10); // arr as int *

注意:auto不能用于函数参数,因此以下内容无法编译(考虑到超载,我们应该使用模板):

int add(auto x, auto y);

此外,auto不能用于派生数组类型:

auto auto_arr2[10] = {arr}; // illegal, can't infer array type

2.3.2 decltype

decltype关键字用来解决auto关键字只能输入变量的缺陷。它的用法与typeof非常相似:

decltype(expression)

例如,有时我们可能需要计算表达式的类型:

auto x = 1;
auto y = 2;
decltype(x+y) z;

在前面的示例中,您已经看到decltype用于推断类型的用法。以下是确定上述变量x、y、z是否为同一类型的例子:

if (std::is_same<decltype(x), int>::value)
	std::cout << "type x == int" << std::endl;
if (std::is_same<decltype(x), float>::value)
	std::cout << "type x == float" << std::endl;
if (std::is_same<decltype(x), decltype(z)>::value)
	std::cout << "type z == type x" << std::endl;

其中std::is_same<T, U>用于确定T和U两种类型是否相等。
输出是:

type x == int
type z == type x

2.3.3 tail type inference

您可能认为,在引入auto时,我们已经提到auto不能用作用于类型派生的函数参数。auto可以用来派生一个函数的返回类型吗?
仍然考虑一个添加函数的例子,我们必须用传统的c++编写:

template<typename R, typename T, typename U>
R add(T x, U y) {
	return x+y
}

注意:模板参数列表中的typename和class之间没有区别。
在关键字typename出现之前,class用于定义模板参数。但是,当在模板中定义具有嵌套依赖类型的变量时,您需要使用typename来消除歧义。

这样的代码实际上非常难看,因为程序员在使用这个模板函数时必须显式地指明返回类型。但实际上,我们并不知道add()函数将执行何种操作,以及要获取何种返回类型。

这个问题在c++ 11中解决了。尽管您可能会立即对使用decltype派生x+y的类型做出反应,编写如下内容:

decltype(x+y) add(T x, U y)

但事实上,这种书写方式是无法编译的。这是因为当编译器读取decltype(x+y)时,x和y还没有定义。为了解决这个问题,c++ 11还引入了一个尾部返回类型,它使用auto关键字来发布返回类型:

template<typename T, typename U>
auto add2(T x, U y) -> decltype(x+y){
	return x + y;
}

好消息是,从c++ 14可以直接推导出一个正常函数的返回值,所以下面的方法是合法的:

template<typename T, typename U>
	auto add3(T x, U y){
	return x + y;
}

您可以检查类型派生是否正确:

// after c++11
auto w = add2<int, double>(1, 2.0);
if (std::is_same<decltype(w), double>::value) {
	std::cout << "w is double: ";
}
std::cout << w << std::endl;

// after c++14
auto q = add3<double, int>(1.0, 2);
std::cout << "q: " << q << std::endl;

2.3.4 decltype(auto)

decltype(auto)是c++ 14中稍微复杂一点的用法。要理解它,您需要了解c++中参数转发(parameter forwarding)的概念,我们将在语言运行时加强(Language Runtime Hardening)一章中详细介绍,稍后您可以回到本节的内容。

简单来说,decltype(auto)主要用于派生转发函数或包的返回类型,不需要我们显式指定decltype的参数表达式。考虑下面的例子,当我们需要包装以下两个函数:

std::string lookup1();
std::string& lookup2();

在C++11中:

std::string look_up_a_string_1() {
	return lookup1();
}
std::string& look_up_a_string_2() {
	return lookup2();
}

使用decltype(auto),我们可以让编译器做这个烦人的参数转发:

decltype(auto) look_up_a_string_1() {
	return lookup1();
}
decltype(auto) look_up_a_string_2() {
	return lookup2();
}

2.4 Control flow

2.4.1 if constexpr

正如我们在本章开头看到的,我们知道c++ 11引入了constexpr关键字,它将表达式或函数编译成常量结果。一个很自然的想法是,如果我们在条件判断中引入这个特性,让代码在编译时完成分支判断,它能使程序更高效吗?c++ 17在if语句中引入了constexpr关键字,允许您在代码中声明常量表达式的条件。考虑以下代码:

#include <iostream>

template<typename T>
auto print_type_info(const T& t) {
	if constexpr (std::is_integral<T>::value) {
		return t + 1;
	} else {
		return t + 0.001;
	}
}

int main() {
	std::cout << print_type_info(5) << std::endl;
	std::cout << print_type_info(3.14) << std::endl;
}

在编译时,实际的代码表现如下:

int print_type_info(const int& t) {
	return t + 1;
}
double print_type_info(const double& t) {
	return t + 0.001;
}

int main() {
	std::cout << print_type_info(5) << std::endl;
	std::cout << print_type_info(3.14) << std::endl;
}

2.4.2 Range-based for loop

最后,c++ 11引入了一种基于范围的迭代方法,我们有能力编写像Python一样简洁的循环,我们可以进一步简化前面的例子:

#include <iostream>
#include <vector>
#include <algorithm>
int main() {
	std::vector<int> vec = {1, 2, 3, 4};
	if (auto itr = std::find(vec.begin(), vec.end(), 3); itr != vec.end()) *itr = 4;

	for (auto element : vec)
		std::cout << element << std::endl; // read only

	for (auto &element : vec) {
		element += 1; // writeable
	}

	for (auto element : vec)
		std::cout << element << std::endl; // read only
}

2.5 Templates

c++模板一直是一种特殊的语言艺术,模板甚至可以作为一种新的语言独立使用。模板的原理是将所有可以在编译时处理的问题都扔到编译时,只在运行时处理那些核心动态服务,从而极大地优化运行时的性能。因此,模板也被许多人视为是一个c++的黑魔法。

2.5.1 Extern templates

在传统的c++中,模板只有在使用时才由编译器实例化。换句话说,只要在每个编译单元中编译的代码中遇到一个完整定义的模板(文件),它将被实例化。由于重复的实例化,这会导致编译时间的增加。另外,我们没有办法告诉编译器不要触发模板的实例化。

为此,c++ 11引入了一个外部模板,它扩展了原来的强制编译器的语法来在特定位置实例化一个模板,允许我们显式地告诉编译器何时实例化模板:

template class std::vector<bool>; // force instantiation
extern template class std::vector<double>; // should not instantiation in current file

2.5.2 The “>”

在传统的c++编译器中,>>总是被当作一个右移位运算符来处理。但实际上我们可以很容易地为嵌套模板编写代码:

std::vector<std::vector<int>> matrix;

这不是在传统的c++编译器,编译和c++ 11开始连续右尖括号,成为法律,可以编译成功。甚至以下文字也可以通过以下方式进行编译:

template<bool T>
class MagicType {
	bool magic = T;
};
// in main function:
std::vector<MagicType<(1>2)>> magic; // legal, but not recommended

2.5.3 Type alias templates

在您理解类型别名模板之前,您需要了解它们之间的区别“模板”和“类型”。仔细理解这句话:模板是用来生成类型的。在传统的c++中,typedef可以为类型定义一个新名称,但无法为模板定义一个新名称。因为模板不是类型。例入:

template<typename T, typename U>
class MagicType {
public:
	T dark;
	U magic;
};
// not allowed
template<typename T>
typedef MagicType<std::vector<T>, std::string> FakeDarkMagic;

c++ 11使用using来引入如下的书写形式,同时支持与传统typedef相同的效果:
通常我们使用typedef来定义别名语法:typedef original name new name;,但是别名的定义语法如函数指针是不同的,这通常会给直接读取带来一定的难度:

typedef int (*process)(void *);
using NewProcess = int(*)(void *);
template<typename T>

using TrueDarkMagic = MagicType<std::vector<T>, std::string>;

int main() {
	TrueDarkMagic<bool> you;
}

2.5.4 Default template parameters

我们可能已经定义了一个加法函数:

template<typename T, typename U>
auto add(T x, U y) -> decltype(x+y) {
	return x+y;
}

但是,在使用时,发现要使用add,每次都必须指定其模板参数的类型。

在c++ 11中可以方便地指定模板的默认参数:

template<typename T = int, typename U = int>
auto add(T x, U y) -> decltype(x+y) {
	return x+y;
}

2.5.5 Variadic templates

模板一直是c++独家的黑魔法之一。在传统的c++中,类模板和函数模板都只能接受指定的固定模板参数集;
c++ 11添加了一个新的表示,允许任意数量、任意类别的模板参数,并且在定义时不需要固定参数的数量。

template<typename... Ts> class Magic;

模板类Magic对象可以接受不限数量的typename作为模板的形式参数,如下面的定义:

class Magic<int,
	std::vector<int>,
	std::map<std::string,
	std::vector<int>>> darkMagic;

因为它是任意的,所以一个数目为0的模板参数也是可能的:class Magic<> nothing;。
如果你不想生成0个模板参数,你可以手动定义至少一个模板参数:

template<typename Require, typename... Args> class Magic;

变量长度的模板参数也可以直接调整模板函数。传统C中的printf函数虽然也可以调用不定数量的形式参数,但它不是类安全的。除了定义类安全性的可变长度参数函数外,c++ 11还可以使类似printf的函数自然地处理非自包含的对象。除了…之外…在表示模板参数不定长度的模板参数中,函数参数也用同样的表示不定长度参数,这为我们简单地写出变长参数函数提供了方便的手段,例如

template<typename... Args> void printf(const std::string &str, Args... args);

然后我们定义可变长度模板参数,如何解包参数?
首先,我们可以使用sizeof…计算参数的数量:

#include <iostream>
template<typename... Ts>
void magic(Ts... args) {
	std::cout << sizeof...(args) << std::endl;
}

我们可以向这个神奇的函数传递任意数量的参数:

magic(); // 0
magic(1); // 1
magic(1, ""); // 2

其次,参数被解压缩。到目前为止还没有简单的方法来处理参数包,但是有两种经典的处理方法:

  1. Recursive template function
    递归是一种非常容易想到的方法,也是最经典的方法。该方法连续递归传递模板参数给函数,从而达到递归遍历所有模板参数的目的:
#include <iostream>

template<typename T0>
void printf1(T0 value) {
	std::cout << value << std::endl;
}

template<typename T, typename... Ts>
void printf1(T value, Ts... args) {
	std::cout << value << std::endl;
	printf1(args...);
}
int main() {
	printf1(1, 2, "123", 1.1);
	return 0;
}
  1. Variable parameter template expansion
    您应该感到这是非常麻烦的。在c++ 17中增加了对变量参数模板扩展的支持,所以你可以在函数中编写printf:
	template<typename T0, typename... T>
		void printf2(T0 t0, T... t) {
		std::cout << t0 << std::endl;
		if constexpr (sizeof...(t) > 0) printf2(t...);
	}

实际上,有时我们使用可变参数模板,但不一定需要逐个遍历参数。我们可以利用std::bind和perfect forwarding的特性来实现函数和参数的绑定,从而获得调用的目的。

  1. Initialize list expansion
    递归模板函数是一种标准实践,但其明显的缺点是必须定义终止递归的函数。
    下面是对黑魔法的描述,使用初始化列表展开:
template<typename T, typename... Ts>
auto printf3(T value, Ts... args) {
	std::cout << value << std::endl;
	(void) std::initializer_list<T>
		{([&args] {std::cout << args << std::endl;}(), value)...};
}

在此代码中,c++ 11提供的初始化列表和Lambda表达式的属性
(下一节将提到)是额外使用的。通过初始化列表,(lambda表达式,值)…将会扩展。由于出现了逗号表达式,所以首先执行前面的lambda表达式,然后完成参数的输出。为了避免编译器警告,我们可以显式地将std::initializer_list转换为void。

2.5.6 Fold expression

在c++ 17中,将可变长度参数的这一特性进一步引入到表达式中,考虑如下例子:

#include <iostream>
template<typename ... T>
auto sum(T ... t) {
	return (t + ...);
}
int main() {
	std::cout << sum(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) << std::endl;
}

2.5.7 Non-type template parameter deduction

我们上面主要提到的是模板形参的一种形式:type template parameters.

template <typename T, typename U>
auto add(T t, U u) {
	return t+u;
}


模板参数T和U是特定的类型。但也有一种常见的模板形参形式,允许不同的 literals 作为模板形参,即非类型模板形参Non-type template parameter:

template <typename T, int BufSize>
class buffer_t {
public:
	T& alloc();
	void free(T& item);
private:
	T data[BufSize];
}

buffer_t<int, 100> buf; // 100 as template parameter

2.6 Object-oriented

2.6.1 Delegate constructor

c++ 11引入了委托构造的概念,它允许一个构造函数调用同一个类中的另一个构造函数,从而简化了代码:

#include <iostream>
class Base {
public:
	int value1;
	int value2;
	Base() {
		value1 = 1;
	}
	Base(int value) : Base() { // delegate Base() constructor
		value2 = value;
	}
};

2.6.2 Inheritance constructor

在传统的c++中,如果需要继承,构造函数需要一个接一个地传递参数,这会导致效率低下。c++ 11使用关键字using引入了继承构造函数的概念:

#include <iostream>
class Base {
public:
	int value1;
	int value2;
	Base() {
		value1 = 1;
	}
	Base(int value) : Base() { // delegate Base() constructor
		value2 = value;
	}
};

class Subclass : public Base {
public:
	using Base::Base; // inheritance constructor
};

int main() {
	Subclass s(3);
	std::cout << s.value1 << std::endl;
	std::cout << s.value2 << std::endl;
}

2.6.3 Explicit virtual function overwrite

在传统的c++中,经常会意外地重载虚函数。

struct Base {
	virtual void foo();
};
struct SubClass: Base {
	void foo();
};

foo可能不是一个程序员试图重载一个虚函数,只是添加一个具有相同名称的函数。另一种可能的情况是,当基类的虚函数被删除时,子类拥有旧的函数,不再重载虚函数并将其转换为普通的类方法,这会产生灾难性的后果。

c++ 11引入了两个关键字override和final来防止这种情况发生。

  1. override
    当重写一个虚函数时,引入override关键字会显式地告诉编译器重载,编译器会检查基函数是否有这样的虚函数,否则不会编译:
struct Base {
	virtual void foo(int);
};
struct SubClass: Base {
	virtual void foo(int) override; // legal
	virtual void foo(float) override; // illegal, no virtual function in super class
};
  1. final
    final是为了防止类被继续继承,并终止虚函数以继续被重载。
struct Base {
	virtual void foo() final;
};
struct SubClass1 final: Base {
}; // legal
struct SubClass2 : SubClass1 {
}; // illegal, SubClass1 has final
struct SubClass3: Base {
	void foo(); // illegal, foo has final
};

2.6.4 Explicit delete default function

在传统的c++中,如果程序员不提供它,编译器将默认生成对象的默认构造函数、复制构造函数、赋值操作符和析构函数。
c++还为所有类定义了诸如new delete之类的操作符。当程序员需要时,可以重写函数的这一部分。

这就提出了一些要求:无法控制精确控制默认函数生成的能力。例如,当禁止复制类时,复制构造函数和赋值操作符必须声明为private。尝试使用这些未定义的函数将导致编译或链接错误,这是一种非常非常规的方式。

此外,编译器生成的默认构造函数不能与用户定义的构造函数同时存在。如果用户定义了任何构造函数,编译器将不再生成默认构造函数,但有时我们希望同时拥有两个构造函数,这是很尴尬的。

c++ 11为上述需求提供了一个解决方案,允许显式声明接受或拒绝编译器提供的函数。

class Magic {
public:
	Magic() = default; // explicit let compiler use default constructor
	Magic& operator=(const Magic&) = delete; // explicit declare refuse constructor
	Magic(int magic_number);
}

2.6.5 Strongly typed enumerations

在传统的c++中,枚举类型不是类型安全的,和枚举类型被当作整数,它允许两个完全不同的枚举类型直接相比(虽然编译器给出了检查,但不是全部),* 甚至不同的enum类型的枚举值名称相同的名称空间中不能相同的 *,这通常不是我们想看到的。

c++ 11引入了一个枚举类,并使用enum类的语法声明它:

enum class new_enum : unsigned int {
	value1,
	value2,
	value3 = 100,
	value4 = 100
};

因此定义的枚举实现了类型安全。首先,它不能隐式转换为整数,也不能与整数进行比较,而且更不可能比较不同枚举类型的枚举值。但是如果指定的值在相同的枚举值之间是相同的,那么您可以比较:

if (new_enum::value3 == new_enum::value4) // true

在此语法中,枚举类型后跟一个冒号和一个type关键字,以指定枚举中枚举值的类型,这允许我们为枚举赋值(在未指定时,默认使用int)。

并且我们想要得到枚举值的值,我们将不得不显式类型转换,但我们可以重载<<运算符来输出,你可以收集以下代码片段:

#include <iostream>
template<typename T>
std::ostream& operator<<(typename std::enable_if<std::is_enum<T>::value, 
	std::ostream>::type& stream, const T& e)
{
	return stream << static_cast<typename std::underlying_type<T>::type>(e);
}

Conclusion

本节介绍了现代c++对语言可用性的增强,我相信这是几乎每个人都需要知道和使用的最重要的特性:

1. auto type derivation
2. Scope for iteration
3. Initialization list
4. Variable parameter template
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
现代C++程序设计(原书第2版)》图文并茂,通俗易懂,真正做到寓教于乐,是一本难得的C++面向对象设计入门教材。 出版者的话 译者序 前言 第1章 C++概述与软件开发 1.1 什么是C语言和C++ 1.1.1 C和C++历史回顾 1.1.2 C/C++是一门编译语言 1.1.3 为什么许多程序员都选择C++ 1.2 什么是面向对象 1.2.1 C++程序并不一定是面向对象的 1.2.2 一个简单的面向对象程序示例 1.2.3 面向对象的软件更有优势 1.3 结构化设计与面向对象设计 1.3.1 ATM——结构化设计 1.3.2 采用面向对象方法的ATM——究竟是谁的任务 1.3.3 汽车维护——结构化设计 1.3.4 采用面向对象方法的汽车维护——究竟是谁的任务 1.4 软件开发技术概述 1.5 问题发现与解决 复习题 第2章 C++的入门知识 2.1 编程基础 2.1.1 算法设计 2.1.2 正确的软件开发步骤 2.2 专业术语及工程创建 2.3 C++程序的一般格式 2.3.1 “Hello World!”程序 2.3.2 “How’s the Weather?”程序 2.4 程序的数据及数据类型 2.4.1 C++的数据类型 2.4.2 容器=数据类型,标签=变量名 2.4.3 数据类型修饰符 2.4.4 问题分析:整型数据究竟有多大 2.5 C++中的变量声明 2.5.1 C++的命名规则 2.5.2 在哪里声明变量 2.6 C++中的运算符 2.6.1 计算路程的程序 2.6.2 从键盘输入程序所需数据 2.6.3 赋值运算符 2.6.4 运算符的优先级 2.6.5 数据类型及其存储的值 2.6.6 算术运算符 2.6.7 自增运算符和自减运算符 2.6.8 复合赋值运算符 2.7 #define、const和数据类型转换 2.7.1 #define预处理指令 2.7.2 const修饰符 2.7.3 const比#define好吗 2.7.4 数据类型转换 2.8 关于键盘输入和屏幕输出的更多内容 2.8.1 转义序列 2.8.2 ios格式标记 2.8.3 流的IO控制符 2.9 开始使用类和对象、C++string类 2.10 练习 复习题 第3章 控制语句和循环 3.1 关系运算符和逻辑运算符 3.2 if语句 3.2.1 if-else语句 3.2.2 问题分析:在if语句中使用大括号 3.2.3 if-else if-else语句 3.2.4 低效的编程方法 3.2.5 if-else程序示例 3.2.6 嵌套if-else语句 3.2.7 条件运算符“?” 3.3 switch语句 3.4 循环 3.4.1 括号的用法 3.4.2 无限循环 3.5 for循环 3.5.1 不要改变循环索引 3.5.2 for循环示例 3.6 while循环 3.7 do while循环 3.8 跳转语句 3.8.1 break语句 3.8.2 continue语句 3.9 问题发现与解决 3.9.1 五个常见错误 3.9.2 调试程序 3.10 C++类与vector类 3.11 总结 3.12 练习 复习题 第4章 函数一:基础 4.1 C++中的函数 4.1.1 只由一个main函数构成的程序 4.1.2 包含多个函数的程序 4.1.3 函数是个好东西 4.1.4 三个重要的问题 4.2 函数:基本格式 4.3 函数的编写要求 4.3.1 你想住在C++旅馆中吗 4.3.2 函数为先 4.3.3 函数声明或函数原型 4.3.4 函数定义、函数标题行与函数体 4.3.5 函数调用 4.3.6 传值调用 4.3.7 问题分析:未声明的标识符 4.4 重载函数 4.5 具有默认输入参数列表的函数 4.6 局部变量、全局变量和静态变量 4.6.1 局部变量 4.6.2 块范围 4.6.3 全局变量 4.6.4 危险的全局变量 4.6.5 问题分析:全局变量y0、y1与cmath 4.6.6 静态变量 4.7 C++stringstream类 4.8 总结 4.9 练习 复习题 第5章 函数二:变量地址、指针以及引用 5.1 数据变量和内存 5.1.1 sizeof运算符 5.1.2 预留内存 5.1.3 计算机内存和十六进制 5.2 取地址运算符& 5.3 指针 5.4 函数、指针以及间接运算符 5.4.1 解决思路 5.4.2 指针和函数 5.4.3 有效处理大型数据 5.5 函数和引用 5.5.1 复习:两种机制 5.5.2 为什么要强调指针的重要性 5.6 queue类 5.7 总结 5.8 练习 复习题 第6章 数组 6.1 使用单个数据变量 6.2 数组基础 6.2.1 数组的索引值从0开始 6.2.2 使用for循环和数组来实现的电话账单程序 6.2.3 数组的声明和初始化 6.2.4 数组越界==严重的问题 6.2.5 vector与数组的比较 6.3 数组和函数 6.3.1 每个数组都有一个指针 6.3.2 数组指针 6.3.3 向函数传递数组:最开始的引用调用 6.3.4 利用数组和函数生成随机数并进行排序 6.4 C字符串,也称为字符数组 6.4.1 字符数组的初始化 6.4.2 null字符 6.4.3 C字符串的输入 6.4.4 C++中提供的字符数组函数 6.5 多维数组 6.5.1 二维数组的初始化 6.5.2 嵌套的for循环和二维数组 6.5.3 利用二维数组来实现Bingo游戏 6.6 多维数组和函数 6.6.1 改进的Bingo卡片程序 6.6.2 白雪公主:利用二维数组来存储姓名 6.7 利用数据文件对数组赋值 6.8 总结 6.9 练习 复习题 第7章 类和对象 7.1 我们所了解的类和对象 7.2 编写自己的类 7.2.1 入门实例:自定义日期类 7.2.2 第一个C++类:Date类 7.2.3 揭开类的生命之谜 7.2.4 set和get函数的作用与VolumeCalc类 7.2.5 PICalculator类 7.3 作为类成员的对象 7.4 类的析构函数 7.5 对象数组 7.6 重载运算符与对象 7.7 指针、引用和类 7.7.1 指针和引用实例 7.7.2 处理日期和时间的程序实例 7.8 总结 7.9 练习 复习题 第8章 继承和虚函数 8.1 为什么继承如此重要 8.1.1 IceCreamDialog实例 8.1.2 Counter类实例 8.2 继承基础 8.2.1 Counter和DeluxeCounter实例 8.2.2 保护成员 8.2.3 员工、老板和CEO 8.3 访问控制符的规范和多继承 8.4 继承、构造和析构 8.4.1 构造函数和析构函数回顾 8.4.2 基类和派生类的默认构造函数——没有参数 8.4.3 在重载的构造函数中使用参数 8.4.4 基类和派生类的析构函数 8.4.5 医生也是人 8.4.6 关于派生类和基类构造函数的规则 8.5 多态和虚函数 8.5.1 多态——同一个接口,不同的行为 8.5.2 什么是虚函数 8.5.3 虚函数的作用 8.6 总结 8.7 练习 复习题 附录A 学习使用Visual C++2005Express Edition 附录B C++关键字表 附录C C++运算符 附录D ASCII码 附录E 位、字节、内存和十六进制表示 附录F 文件输入/输出 附录G 部分C++类 附录H 多文件程序 附录I Microsoft visual C++2005Express Edit
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值