#include <iostream>
#include <type_traits>
void foo(char *);
void foo(int);
int main() {
if (std::is_same<decltype(NULL), decltype(0)>::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); // 调用 foo(int)
// foo(NULL); // 该行不能通过编译
foo(nullptr); // 调用 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;
}
#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]; // 合法
char arr_2[LEN]; // 合法
int len = 10;
// char arr_3[len]; // 非法
const int len_2 = len + 1;
constexpr int len_2_constexpr = 1 + 2 + 3;
// char arr_4[len_2]; // 非法
char arr_4[len_2_constexpr]; // 合法
// char arr_5[len_foo()+5]; // 非法
char arr_6[len_foo_constexpr() + 1]; // 合法
std::cout << fibonacci(10) << std::endl;
// 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 常数,而不是一个常量表达式,因此(即便这种行为在大部分编译器中都支持,但是)它是一个非法的行为,我们需要使用接下来即将介绍的 C + + 11 \color{red}{C++11} C++11 引入的 c o n s t e x p r \color{red}{constexpr} constexpr 特性来解决这个问题;而对于 arr_5 来说,C++98 之前的编译器无法得知 len_foo() 在运行期实际上是返回一个常数,这也就导致了非法的产生。
C + + 11 提 供 了 c o n s t e x p r 让 用 户 显 式 的 声 明 函 数 或 对 象 构 造 函 数 在 编 译 期 会 成 为 常 量 表 达 式 , 这 个 关 键 字 明 确 的 告 诉 编 译 器 应 该 去 验 证 l e n f o o 在 编 译 期 就 应 该 是 一 个 常 量 表 达 式 。 \color{red}{C++11 提供了 constexpr 让用户显式的声明函数或对象构造函数在编译期会成为常量表达式,这个关键字明确的告诉编译器应该去验证 len_foo 在编译期就应该是一个常量表达式。} C++11提供了constexpr让用户显式的声明函数或对象构造函数在编译期会成为常量表达式,这个关键字明确的告诉编译器应该去验证lenfoo在编译期就应该是一个常量表达式。
此外,constexpr 的函数可以使用递归:
constexpr int fibonacci(const int n) {
return n == 1 || n == 2 ? 1 : fibonacci(n-1)+fibonacci(n-2);
}
从 C + + 14 \color{red}{C++14} C++14 开始,constexpr 函数可以 在 内 部 使 用 局 部 变 量 、 循 环 和 分 支 等 简 单 语 句 \color{red}{在内部使用局部变量、循环和分支等简单语句} 在内部使用局部变量、循环和分支等简单语句,例如下面的代码在 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);
}
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> vec = {
1, 2, 3, 4};
// 在 c++17 之前
const std::vector<int>::iterator itr = std::find(vec.begin(), vec.end(), 2);
if (itr != vec.end()) {
*itr = 3;
}
// 需要重新定义一个新的变量
const std::vector<int>::iterator itr2 = std::find(vec.begin(), vec.end(), 3);
if (itr2 != vec.end()) {
*itr2 = 4;
}
// 将输出 1, 4, 3, 4
for (std::vector<int>::iterator element = vec.begin(); element != vec.end(); ++element)
std::cout << *element << std::endl;
}
在上面的代码中,我们可以看到 itr 这一变量是定义在整个 main() 的作用域内的,这导致当我们需要再次遍历整个 std::vectors 时,需要重新命名另一个变量。 C + + 17 \color{red}{C++17} C++17 消除了这一限制,使得我们可以在 if(或 switch)中完成这一操作:
/ 将临时变量放到 if 语句内
if (const std::vector<int>::iterator itr = std::find(vec.begin(), vec.end(), 3);
itr != vec.end()) {
*itr = 4;
}
#include <iostream>
#include <vector>
class Foo {
public:
int value_a;
int value_b;
Foo(int a, int b) : value_a(a), value_b(b) {
}
};
int main() {
// before C++11
int arr[3] = {
1, 2, 3};
Foo foo(1, 2);
std::vector<int> vec = {
1, 2, 3, 4, 5};
std::cout << "arr[0]: " << arr[0] << std::endl;
std::cout << "foo:" << foo.value_a << ", " << foo.value_b << std::endl;
for (std::vector<int>::iterator it = vec.begin(); it != vec.end(); ++it) {
std::cout << *it << std::endl;
}
return 0;
}
为了解决这个问题, C + + 11 \color{red}{C++11} 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 \color{red}{C++11} C++11 还提供了统一的语法来 初 始 化 任 意 的 对 象 \color{red}{初始化任意的对象} 初始化任意的对象,例如:
Foo foo2 {
3, 4};
C + + 17 \color{red}{C++17} 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;
}
C + + 11 \color{red}{C++11} C++11 引入了 auto 和 decltype 这两个关键字实现了类型推导,让编译器来操心变量的类型。这使得 C++ 也具有了和其他现代编程语言一样,某种意义上提供了无需操心变量类型的使用习惯。
auto 在很早以前就已经进入了 C++,但是他始终作为一个存储类型的指示符存在,与 register 并存。在传统 C++ 中,如果一个变量没有声明为 register 变量,将自动被视为一个 auto 变量。而随着 register 被弃用(在 C++17 中作为保留关键字,以后使用,目前不具备实际意义),对 auto 的语义变更也就非常自然了。
使用 auto 进行类型推导的一个最为常见而且显著的例子就是迭代器。你应该在前面的小节里看到了传统 C++ 中冗长的迭代写法:
// 在 C++11 之前
// 由于 cbegin() 将返回 vector::const_iterator
// 所以 itr 也应该是 vector::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) {
// 从 C++11 起, 使用 auto 关键字进行类型推导
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 被推导为 int
auto arr = new auto(10); // arr 被推导为 int *
注 意 : a u t o 不 能 用 于 函 数 传 参 , 因 此 下 面 的 做 法 是 无 法 通 过 编 译 的 ( 考 虑 重 载 的 问 题 , 我 们 应 该 使 用 模 板 ) : \color{red}{注意:auto 不能用于函数传参,因此下面的做法是无法通过编译的(考虑重载的问题,我们应该使用模板):} 注意:auto不能用于函数传参,因此下面的做法是无法通过编译的(考虑重载的问题,我们应该使用模板):
int add(auto x, auto y);
2.6.auto.cpp:16:9: error: 'auto' not allowed in function prototype
int add(auto x, auto y) {
^~~~
此 外 , a u t o 还 不 能 用 于 推 导 数 组 类 型 : \color{red}{此外,auto 还不能用于推导数组类型:} 此外,auto还不能用于推导数组类型:
auto auto_arr2[10] = {
arr}; // 错误, 无法推导数组元素类型
2.6.auto.cpp:30:19: error: 'auto_arr2' declared as array of 'auto'
auto auto_arr2[10] = {
arr};
decltype 关键字是为了解决 auto 关键字只能对变量进行类型推导的缺陷而出现的。它的用法和 typeof 很相似:
decltype(表达式)
有时候我们需要计算某个表达式的类型:
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
type x == int
type z == type x
template<typename R, typename T, typename U>
R add(T x, U y) {
return x+y
}
注 意 : t y p e n a m e 和 c l a s s 在 模 板 参 数 列 表 中 没 有 区 别 , 在 t y p e n a m e 这 个 关 键 字 出 现 之 前 , 都 是 使 用 c l a s s 来 定 义 模 板 参 数 的 。 但 在 模 板 中 定 义 有 嵌 套 依 赖 类 型 的 变 量 时 , 需 要 用 t y p e n a m e 消 除 歧 义 \color{red}{注意:typename 和 class 在模板参数列表中没有区别,在 typename 这个关键字出现之前,都是使用 class 来定义模板参数的。但在模板中定义有嵌套依赖类型的变量时,需要用 typename 消除歧义} 注意:typename和class在模板参数列表中没有区别,在typename这个关键字出现之前,都是使用class来定义模板参数的。但在模板中定义有嵌套依赖类型的变量时,需要用typename消除歧义
这样的代码其实变得很丑陋,因为程序员在使用这个模板函数的时候,必须明确指出返回类型。但事实上我们并不知道 add() 这个函数会做什么样的操作,获得一个什么样的返回类型。
在 C + + 11 \color{red}{ C++11} C++11 中这个问题得到解决。虽然你可能马上会反应出来使用 decltype 推导 x+y 的类型,写出这样的代码:
decltype(x+y) add(T x, U y)
但事实上这样的写法并 不 能 通 过 编 译 \color{red}{ 不能通过编译} 不能通过编译 。这是因为在编译器读到 decltype(x+y) 时,x 和 y 尚未被定义。为了解决这个问题, C + + 11 还 引 入 了 一 个 叫 做 尾 返 回 类 型 ( t r a i l i n g r e t u r n t y p e ) \color{red}{ C++11 还引入了一个叫做尾返回类型(trailing return type)} C++11还引入了一个叫做尾返回类型(trailingreturntype),利用 auto 关键字将返回类型后置:
template<typename T, typename U>
auto add2(T x, U y) -> decltype(x+y){
return x + y;
}
令人欣慰的是从 C + + 14 \color{red}{ C++14} 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;
d e c l t y p e ( a u t o ) \color{red}{ decltype(auto)} decltype(auto) 是 C + + 14 \color{red}{ C++14} C++14 开始提供的一个略微复杂的用法。
简单来说,decltype(auto) 主要用于对转发函数或封装的返回类型进行推导,它使我们无需显式的指定 decltype 的参数表达式。考虑看下面的例子,当我们需要对下面两个函数进行封装时:
std::string lookup1();
std::string& lookup2();
在 d e c l t y p e ( C + + 11 ) \color{red}{ decltype(C++11)} decltype(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();
}
#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;
}
#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
}
模 板 \color{red}{ 模板} 模板
C++ 的模板一直是这门语言的一种特殊的艺术,模板甚至可以独立作为一门新的语言来进行使用。模板的哲学在于将一切能够在编译期处理的问题丢到编译期进行处理,仅在运行时处理那些最核心的动态服务,进而大幅优化运行期的性能。因此模板也被很多人视作 C++ 的黑魔法之一。
外 部 模 板 \color{red}{ 外部模板} 外部模板
传统 C++ 中,模板只有在使用时才会被编译器实例化。换句话说,只要在每个编译单元(文件)中编译的代码中遇到了被完整定义的模板,都会实例化。这就产生了重复实例化而导致的编译时间的增加。并且,我们没有办法通知编译器不要触发模板的实例化。
为此, C + + 11 \color{red}{ C++11} C++11 引入了外部模板,扩充了原来的强制编译器在特定位置实例化模板的语法,使我们能够显式的通知编译器何时进行模板的实例化:
template class std::vector<bool>; // 强行实例化
extern template class std::vector<double>; // 不在该当前编译文件中实例化模板
在传统 C++ 的编译器中,>>一律被当做右移运算符来进行处理。但实际上我们很容易就写出了嵌套模板的代码:
std::vector<std::vector<int>> matrix;
这在传统C++编译器下是不能够被编译的,而 C + + 11 \color{red}{ C++11} C++11 开始,连续的右尖括号将变得合法,并且能够顺利通过编译。甚至于像下面这种写法都能够通过编译:
template<bool T>
class MagicType {
bool magic = T;
};
// in main function:
std::vector<MagicType<(1>2)>> magic; // 合法, 但不建议写出这样的代码
template<typename T, typename U>
class MagicType {
public:
T dark;
U magic;
};
// 不合法
template<typename T>
typedef MagicType<std::vector<T>, std::string> FakeDarkMagic;
通常我们使用 typedef 定义别名的语法是:typedef 原名称 新名称;,但是对函数指针等别名的定义语法却不相同,这通常给直接阅读造成了一定程度的困难。
使用 using 引入了下面这种形式的写法,并且同时支持对传统 typedef 相同的功效:
typedef int (*process)(void *);
using NewProcess = int(*)(void *);
template<typename T>
using TrueDarkMagic = MagicType<std::vector<T>, std::string>;
int main() {
TrueDarkMagic<bool> you;
}
template<typename T, typename U>
auto add(T x, U y) -> decltype(x+y) {
return x+y;
}
但在使用时发现,要使用 add,就必须每次都指定其模板参数的类型。
在 C + + 11 \color{red}{C++11} C++11 中提供了一种便利,可以指定模板的默认参数:
emplate<typename T = int, typename U = int>
auto add(T x, U y) -> decltype(x+y) {
//C++14 no need decltype,this is c++11
return x+y;
}
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 \color{red}{C++11} C++11 除了能定义类别安全的变长参数函数外, 还可以使类似 printf 的函数能自然地处理非自带类别的对象。 除了在模板参数中能使用 … 表示不定长模板参数外, 函数参数也使用同样的表示法代表不定长参数, 这也就为我们简单编写变长参数函数提供了便捷的手段,例如:
template<typename... Args> void printf(const std::string &str, Args... args);
那么我们定义了变长的模板参数,如何对参数进行解包呢?
首先,我们可以使用 sizeof… 来计算参数的个数:
template<typename... Ts>
void magic(Ts... args) {
std::cout << sizeof...(args) << std::endl;
}
我们可以传递任意个参数给 magic 函数:
magic(); // 输出0
magic(1); // 输出1
magic(1, ""); // 输出2
其次,对参数进行解包,到目前为止还没有一种简单的方法能够处理参数包,但有两种经典的处理手法:
1. 递 归 模 板 函 数 \color{red}{1. 递归模板函数} 1.递归模板函数
递归是非常容易想到的一种手段,也是最经典的处理方法。这种方法不断递归地向函数传递模板参数,进而达到递归遍历所有模板参数的目的:
#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;
}
2. 变 参 模 板 展 开 \color{red}{2. 变参模板展开} 2.变参模板展开
你应该感受到了这很繁琐,在 C + + 17 \color{red}{C++17} 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...);
}
3. 初 始 化 列 表 展 开 \color{red}{3. 初始化列表展开} 3.初始化列表展开
递归模板函数是一种标准的做法,但缺点显而易见的在于必须定义一个终止递归的函数。
这里介绍一种使用初始化列表展开的黑魔法:
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 \color{red}{C++11} C++11 中提供的初始化列表以及 Lambda 表达式的特性(下一节中将提到)。
通过初始化列表,(lambda 表达式, value)… 将会被展开。由于逗号表达式的出现,首先会执行前面的 lambda 表达式,完成参数的输出。 为了避免编译器警告,我们可以将 std::initializer_list 显式的转为 void。
#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;
}
template <typename T, typename U>
auto add(T t, U u) {
//C++14 可用
return t+u;
}
其中模板的参数 T 和 U 为具体的类型。 但还有一种常见模板参数形式可以让不同字面量成为模板参数,即非类型模板参数:
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 作为模板参数
在这种模板参数形式下,我们可以将 100 作为模板的参数进行传递。 在 C++11 引入了类型推导这一特性后,我们会很自然的问,既然此处的模板参数 以具体的字面量进行传递,能否让编译器辅助我们进行类型推导, 通过使用占位符 auto 从而不再需要明确指明类型? 幸运的是, C + + 17 \color{red}{C++17} C++17 引入了这一特性,我们的确可以 auto 关键字,让编译器辅助完成具体类型的推导, 例如:
template <auto value> void foo() {
std::cout << value << std::endl;
return;
}
int main() {
foo<10>(); // value 被推导为 int 类型
}
#include <iostream>
class Base {
public:
int value1;
int value2;
Base() {
value1 = 1;
}
Base(int value) : Base() {
// 委托 Base() 构造函数
value2 = value;
}
};
int main() {
Base b(2);
std::cout << b.value1 << std::endl;
std::cout << b.value2 << std::endl;
}
2. 继 承 构 造 \color{red}{2.继承构造} 2.继承构造
在传统 C++ 中,构造函数如果需要继承是需要将参数一一传递的,这将导致效率低下。 C + + 11 \color{red}{C++11} C++11 利用关键字 using 引入了继承构造函数的概念:
#include <iostream>
class Base {
public:
int value1;
int value2;
Base() {
value1 = 1;
}
Base(int value) : Base() {
// 委托 Base() 构造函数
value2 = value;
}
};
class Subclass : public Base {
public:
using Base::Base; // 继承构造
};
int main() {
Subclass s(3);
std::cout << s.value1 << std::endl;
std::cout << s.value2 << std::endl;
}
3. 显 式 虚 函 数 重 载 \color{red}{3.显式虚函数重载} 3.显式虚函数重载
在传统 C++中,经常容易发生意外重载虚函数的事情。例如:
struct Base {
virtual void foo();
};
struct SubClass: Base {
void foo();
};
SubClass::foo 可能并不是程序员尝试重载虚函数,只是恰好加入了一个具有相同名字的函数。另一个可能的情形是,当基类的虚函数被删除后,子类拥有旧的函数就不再重载该虚拟函数并摇身一变成为了一个普通的类方法,这将造成灾难性的后果。
C + + 11 \color{red}{C++11} C++11 引入了 override 和 final 这两个关键字来防止上述情形的发生。
当重载虚函数时,引入 override 关键字将显式的告知编译器进行重载,编译器将检查基函数是否存在这样的虚函数,否则将无法通过编译:
struct Base {
virtual void foo(int);
};
struct SubClass: Base {
virtual void foo(int) override; // 合法
virtual void foo(float) override; // 非法, 父类没有此虚函数
};
final 则是为了防止类被继续继承以及终止虚函数继续重载引入的。
struct Base {
virtual void foo() final;
};
struct SubClass1 final: Base {
}; // 合法
struct SubClass2 : SubClass1 {
}; // 非法, SubClass1 已 final
struct SubClass3: Base {
void foo(); // 非法, foo 已 final
};
在传统 C++ 中,如果程序员没有提供,编译器会默认为对象生成默认构造函数、 复制构造、赋值算符以及析构函数。 另外,C++ 也为所有类定义了诸如 new delete 这样的运算符。 当程序员有需要时,可以重载这部分函数。
这就引发了一些需求:无法精确控制默认函数的生成行为。 例如禁止类的拷贝时,必须将复制构造函数与赋值算符声明为 private。 尝试使用这些未定义的函数将导致编译或链接错误,则是一种非常不优雅的方式。
并且,编译器产生的默认构造函数与用户定义的构造函数无法同时存在。 若用户定义了任何构造函数,编译器将不再生成默认构造函数, 但有时候我们却希望同时拥有这两种构造函数,这就造成了尴尬。
C + + 11 \color{red}{C++11} C++11 提供了上述需求的解决方案,允许显式的声明采用或拒绝编译器自带的函数。 例如:
class Magic {
public:
Magic() = default; // 显式声明使用编译器生成的构造
Magic& operator=(const Magic&) = delete; // 显式声明拒绝编译器生成构造
Magic(int magic_number);
}
在传统 C++中,枚举类型并非类型安全,枚举类型会被视作整数,则会让两种完全不同的枚举类型可以进行直接的比较(虽然编译器给出了检查,但并非所有),甚至同一个命名空间中的不同枚举类型的枚举值名字不能相同,这通常不是我们希望看到的结果。
C + + 11 \color{red}{C++11} C++11 引入了枚举类(enumeration class),并使用 enum class 的语法进行声明:
enum class new_enum : unsigned int {
value1,
value2,
value3 = 100,
value4 = 100
};
这样定义的枚举实现了类型安全,首先他不能够被隐式的转换为整数,同时也不能够将其与整数数字进行比较, 更不可能对不同的枚举类型的枚举值进行比较。但相同枚举值之间如果指定的值相同,那么可以进行比较:
if (new_enum::value3 == new_enum::value4) {
// 会输出
std::cout << "new_enum::value3 == new_enum::value4" << std::endl;
}
在这个语法中,枚举类型后面使用了冒号及类型关键字来指定枚举中枚举值的类型,这使得我们能够为枚举赋值(未指定时将默认使用 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);
}
这时,下面的代码将能够被编译:
std::cout << new_enum::value3 << std::endl
强类型枚举示例:
1 // C++11之前的enum类型是继承C的,不温不火;
2 // C++11对enum动刀了,加强了类型检查,推出强类型enum类型,眼前一亮
3 // 使用过QT 的都知道,早就应该这么做了,用的很爽!!
4
5 // 一、C中enum类型的局限
6 // 1、非强类型作用域
7
8 enum type1{
a, b, c};
9 enum type2{
a, d, e};
10
11 //问题来了,两种枚举类型都有a枚举常量,而且作用域相同,发生了冲突
12
13 //2、允许隐式类型转换
14
15 if(type1::a == type2::d){
;}
16
17 //两种不同enum常量比较没有实际意义,但是编译器是先将enum常量转换为int类型后,再比较的,不会报错
18
19 // 3、占用存储空间极其符号不确定
20
21 enum type3{
a=1, b=0xFFFFFFFFFLL};
22 cout<<sizeof(type3::a)<<endl;//可能为8,视编译器而定,由其对齐字长决定的
23 cout<<type3::b<<endl;//输出大小不定,可能为0
24
25 //问题来了,存储大小和符号都有未定的因素
26
27 // 二、C++11强类型enum类型
28
29 //语法:
30
31 enum class type4:type{
a, b, c};//加class,具名,指定底层类型type
32 enum class type5{
a, b, c};//不指定底层类型
33
34 //特点及用法:
35 // 1、强作用域
36
37 type4 t = type4::a; //必须指定强类型名称
38 type4 p = a; //错误
39
40 //2、不可隐式转换
41
42 if(t<type4::b); //同一个enum类型比较,可以
43 if(t>type5::b); //非同一个enum类型,不能隐式转换为int等值,不嗯呢该比较
44 if((int)t > (int)type5::b) //强制类型转换,可以通过
45
46 //3、可以指定底层类型
47
48 enum class type6:char{
a=1, b=2};
49 enum class type7:unsigned int{
a=0xFFFFFFFF};
50
51 //既可以节省空间,又可以指定符号
52
53 //三、C++11是兼容并拓展了C enum类型的
54
55 enum type8:char{
a=1, b=2};
56 type8 t =a;
57
58 //拓展并兼容C enum类型