本课程分为两个部分:语言的部分和标准库的部分,只谈新特性。
C++11新特性 语言部分
语言
- Variadic Templates
- move semantics
- auto
- Range-based for loop
- Initializer list
- Lambdas
- …
头文件
关于C++的头文件,有以下几点:
- C++标准库的头文件均不带 .h,如 #include
- 在C++中,旧式的C的头文件(带有 .h)依然可用,如 #include <stdio.h>
- 建议在C++中使用,新式的C头文件,与旧式的关系:xxx.h -> cxxx,如 #include
Variatic Templates
假如我想设计一个函数 print,它能够接收任意数量的参数,并且这个参数的类型也是任意的,就可以利用 Variatic Templates 来递归地实现:
#include <iostream>
void print() {} // 1
template<typename T, typename... Types> // 2
void print(const T& firstArg, const Types&... args) { // 3
std::cout << firstArg << std::endl;
print(args...); // 4
}
int main() {
print("dfafda", 's', 123);
}
在变参模板中,如果我们想要知道可变参数的个数,可通过:sizeof…(args)。
变参模板的应用:
-
万能的hash function:多种函数重载 + 递归 + 函数变参模板 —>花式调用
-
tuple:类变参模板 + 继承
Spaces in Template Expressions
在C++11之前,如果有模板嵌套,右侧的两个尖括号不能靠在一起,中间须有空格,否则编译器会认为那是个右移运算符,在C++11之后编译器变聪明了,不再需要这个空格。
vector<list<int> >; // OK in each C++ version
vector<list<int>>; // OK since C++ 11
nullptr and std::nullptr_t
在C++11之后,我们可以使用 nullptr (而非之前的 NULL 或者 0)来表示空指针。注意 NULL 就是一个宏,其值为0,而 nullptr 确实是个指针,其类型为 std::nullptr_t。下面的例子可以验证:
#include <iostream>
void f(int) {
std::cout << "call f(int)" << std::endl;
}
void f(void*) {
std::cout << "call f(void*)" << std::endl;
}
int main() {
f(0); // calls f(int)
f(NULL); // calls f(int) if NULL is 0; ambiguous otherwise
f(nullptr); // calls f(void*)
}
auto
自动类型推导 auto:在C++11之后,可以用 auto 来定义变量的类型,编译器会自动进行类型推导。
auto i = 42; // i是int类型
double f();
auto d = f(); // d是double类型
注意:不建议在任何时候都使用 auto ,而是推荐在这个变量的类型名称实在是很长或者很复杂,实在是懒得打那么多字时使用,但是我们要知道变量应该是什么类型,如:
vector<string> v;
auto pos = v.begin(); // 过长
auto f = [](int x) -> bool { // 过于复杂
// ...
}
程序员要做到对自己的变量类型心中有数。
Uniform Initialization
在C++11之前,许多程序员会疑惑,一个变量或者对象的初始化可能会发生于小括号,大括号,赋值运算符。如:
vector<int> vec(3, 5);
vector<int> vec {1, 2 ,3};
int a = 1;
C++11引入一致初始化,使用大括号,在变量后面直接跟大括号,大括号中可以有任意多个元素个数,设置初值,进行初始化,如:
int values[] {1, 2, 3};
vector<int> v {2, 3, 4};
complex<double> {4.0, 3.0};
实际上,编译器看到 {} 就会作出一个 initializer_list<T>
,它关联至一个 array<T, n>
。调用函数(如ctor)时该 array 的元素被编译器分解逐一传给函数。
需要注意的是:若某个函数参数就是个 initializer_list<T>
,调用者不能传递数个 T 参数然后以为它们会被自动转换为一个 intializer_list<T>
传入,即需要自己手动将数个参数转换为 initializer_list<T>
再进行传值。
比如:
vector<string> cities {"Berlin", "New York", "London"};
这形成一个 initializer_list<string>
,背后有个 array<string, 3>
。调用 vector<string>
的 ctors(构造函数)中的接收 initialize_list<string>
的版本,标准库中所有容器都有接收这个 initializer_list<T>
的构造函数。
但是对于我们自己的类,可能没有接收 intializer_list<T>
这种参数的构造函数,此时这个初始化列表逐一分解拆成一个一个的参数传给函数,再去找与多个单个参数相匹配的构造函数。
initializer_list
初始化列表是支持上面提到的大括号形式的一致性初始化的背后方法。
为了支持用户自定义的类的 initializer_list。C++11提供了一个类模板:std::initializer_list。他可以用用于使用一包参数值来进行初始化,或者用来其他你想要处理一包参数的地方。如使用initalizer_list传参:
#include <iostream>
void print(std::initializer_list<int> vals) {
for (auto ite = vals.begin(); ite!=vals.end(); ++ite) {
std::cout << *ite << "\n";
}
}
int main() {
print( {1,2,3,4} ); // 使用initalizer_list传参
}
即 {} 即可形成一个 initializer_list
不同于前面的 variadic template,这里的 initializer_list 需要的是固定类型 T 的任意多个参数。也可以看做是一种容器。
initializer_list背后由array构建。
intializer_list如果被拷贝,会是浅拷贝(引用语义)
在C++11之后的标准库中,initializers_list 有许多应用,最常见的肯定是上面提到过的各个容器的构造函数中可以使用其作为参数。另外,在一些算法中也有应用,比如 min/max 函数,在C++11之前,它们只能支持两个元素的比较:
std::min(1, 2);
在C++11之后,借助 initializer_list 它可以支持多个元素的比较:
std::min( {1, 2, 3, 4} );
range-based for loop
在C++11之后
std::vector<int> vec = {1, 2, 3, 4};
for (int i : vec) {
std::cout << i << std::endl;
}
也可以用引用:
std::vector<double> vec;
for (auto& elem : vec) {
elem *= 3; // 因为是引用,所以会改变原vector
}
=default, =delete
如果你自行定义了一个 ctor,那么编译器就不会再给你一个 default ctor;但是如果你强制加上 =default (可以空格),就可以重新获得并使用默认的 default ctor。例如:
// use of defaulted functions
#include <iostream>
using namespace std;
class A {
public:
// A user-defined
A(int x){
cout << "This is a parameterized constructor";
}
// Using the default specifier to instruct
// the compiler to create the default implementation of the constructor.
A() = default;
};
int main(){
A a; //call A()
A x(1); //call A(int x)
cout<<endl;
return 0;
}
在C ++ 11之前,操作符delete 只有一个目的,即释放已动态分配的内存。而C ++ 11标准引入了此操作符的另一种用法,即:禁用成员函数的使用。这是通过附加 = delete 来完成的; 说明符到该函数声明的结尾。
使用 = delete 说明符禁用其使用的任何成员函数称为explicitly deleted函数。
虽然不限于它们,但这通常是针对隐式函数。以下示例展示了此功能派上用场的一些任务:
禁用拷贝构造函数
// copy-constructor using delete operator
#include <iostream>
using namespace std;
class A {
public:
A(int x): m(x) { }
// Delete the copy constructor
A(const A&) = delete;
// Delete the copy assignment operator
A& operator=(const A&) = delete;
int m;
};
int main() {
A a1(1), a2(2), a3(3);
// Error, the usage of the copy assignment operator is disabled
a1 = a2;
// Error, the usage of the copy constructor is disabled
a3 = A(a2);
return 0;
}
禁用不需要的类型转换
// type conversion using delete operator
#include <iostream>
using namespace std;
class A {
public:
A(int) {}
// Declare the conversion constructor as a deleted function. Without this step,
// even though A(double) isn't defined, the A(int) would accept any double value
// for it's argumentand convert it to an int
A(double) = delete;
};
int main() {
A A1(1);
// Error, conversion from double to class A is disabled.
A A2(100.1);
return 0;
}
请注意,删除的函数定义必须是函数的第一个声明。
class C {
public:
C(C& a) = delete;
};
但是以下尝试声明删除函数的方法会产生错误:
// incorrect syntax of declaring a member function as deleted
class C {
public:
C();
};
// Error, the deleted definition of function C must be the first declaration of the function.
C::C() = delete;
Big Five,指每个类的拷贝控制,即构造函数、拷贝构造函数、移动构造函数、拷贝赋值函数、移动赋值函数、析构函数。它们默认是 public 且 inline 的。
- =default 不能用于 Big Five 之外的常规函数:编译会报错,因为其他函数并没有默认的版本。
- =delete 可以用于任何函数身上(但好像没什么意义,不需要某个函数不写就是了,为什么要写了再=delete呢),注意类似的 =0 只能用于虚函数,这样会使得该虚函数称为纯虚函数,强迫子类重写该函数。
alias template (template typedef)
带参数的别名模板。
template <typename T>
using Vec = std::vector<T, MyAlloc<T>>;
在经过了上面的定义之后,以下两种写法是等价的:
Vec<int> coll;
// 等价于
std::vector<int, MyAlloc<int>> coll;
如此我们可以方便地使用我们自己的分配器 MyAlloc 创建一个类型可选的 vector 对象。
注意,大家注意到这种用法和我们的宏定义和 typedef 好像有些类似,但是实际上使用 macro 宏定义或 typedef 均无法实现上面的效果。
注意 alias template 不能做偏特化或全特化
。
type alias (similar to typedef)
using value_type = T;
// 等价于
typedef T value_type;
using func = void(*)(int, int);
// 等价于
typedef void (*func)(int, int);
// 使用func,作为函数指针类
void example(int, int) {}
func fn = example;
//func 被定义为一种类型,它是一个函数指针类型。
using关键字总结
- type alias (since C++11)
- alias template (since C++11)
noexcept
void foo() noexcept {
// ...
}
程序员保证 foo() 函数不会抛出异常,让别人/编译器“放心地”调用该函数。
实际上 noexcept 关键字还可以加参数,来表示在…条件下,函数不会抛出异常,上面的 void foo() noexcept ; 就等价于 void foo() noexcept(true);, 即相当于无条件保证。
void swap(Type& x, Type& y) noexcept(noexcept(x.swap(y))) {
x.swap(y);
}
//则意为在 x.swap(y) 不抛出异常的条件下,本函数不会抛出异常。
异常是这样的,如果 A 调用 B,B 调用 C,而在 C 执行的过程中出现了异常,则先看 C 有没有写明异常处理程序,如果有,则处理;如果没有,则异常传递给 B,然后看 B 有没有对应的异常处理程序,如果有,则处理;如果也没有,则继续传递给 A。即按照调用顺序一层一层地向上传递,直到有对应的异常处理程序。如果用户一直没有异常处理程序,则执行 std::terminate() ,进而执行 std::abort() ,程序退出。
override
override 关键字,标明重写,应用于虚函数身上。
struct Base {
virtual void vfunc(float) { }
};
struct Derived: Base {
// virtual void vfunc(int) { }
virtual void vfunc(int) override { }
}
子类 Derived 在继承父类 Base 之后想要重写父类的 void vfunc(float)
方法,但是我们知道,要重写父类方法需要函数签名完全一致,这里可能由于疏忽大意,将参数类型写为了 int。这就导致子类的这个函数定义了一个新函数,而非是期望中的对于父类函数的重写了。而编译器肯定是不知道你其实是想重写父类方法的,因为你函数签名的不一致,就按照一个新方法来处理了。
在 C++11 之后,引入了 override 关键字,在你确实想要重写的函数之后,加上这个关键字,编译器会在你在想重写但是函数签名写错的时候提醒你,这个被标记为重写函数的函数实际上并未进行重写。
final
有两个作用:
- 修饰类,使得该类不能被继承
struct Base final {};
struct Derived: Base {}; // Error
Base 类被 final 关键字修饰,使得其不能被继承,下面的 Derived 试图继承它,会报错。
- 修饰虚函数,使得该虚函数不能被重写
struct Base {
virtual void func() final;
}
struct Derived : Base {
void func(); // Error
}
Base 类本身没有被 final 修饰,所以可以被继承。但是其虚函数 func() 被 final 关键字修饰,故 func() 不能被重写。下面的 Derive 类试图重写它,会报错。
decltype
获取一个变量/一个对象的类型 (即 tpyeof(a) )是非常常见的需求,但是在 C++11 之前并没有直接提供这样的关键字(仅有 typeid 等)。 decltype 可以满足这一需求,方便地获得变量 / 对象的类型。
decltype 用来定义一种类型,该类型等同于一个类型表达式的结果。如 decltype(x+y) 定义了 x+y
这个表达式的返回类型。
map<string, float> coll;
decltype(coll)::value_type elem;
在C++11之前只能:
map<string, float>::value_type elem;
decltype 的三种应用场景:
- 用来声明返回值类型
有时候,函数返回值的类型取决于参数的表达式的执行结果。然而,在C++11之前,没有 decltype 之前,以下语句是不可能成立的:
template<typename T1, typename T2>
decltype(x+y) add(T1 x, T2 y);
因为上面的返回值的类型使用了尚未引入且不再作用域内的对象。
但是在C++11之后,我们可以通过在函数的参数列表之后声明一个返回值类型来实现:
template<typename T1, typename T2>
auto add(T1 x, T2 y) -> decltype(x+y);
这与 lambda 表达式声明返回值的语法相同:
- 元编程
元编程是对模板编程的运用。
举例:
typdef typename decltype(obj)::iterator iType;
// 类似 typedef typename T::iterator iType;
decltype(obj) anotherObj(obj);
- 传递lambda的类型
面对lambda,我们手上往往只有对象,没有类型,要获得其类型就得借助于 decltype 。
如:
auto cmp = [] (const Person& p1, const Person &p2) {
return /* 给出Person类比大小的准则 */
}
//...
std::set<Person, decltype(cmp)> coll<cmp>;
我们知道由于 set 是有序容器,所以在将自定义的类构成一个 set 的时候需要给出该类比大小的准则(谓词),通常是函数、仿函数或者 lambda 表达式。但是这里我们同样需要指定类型,这就可以用 decltype 来指定。
lambdas
C++11 引入了 lambdas ,允许定义一个单行的函数,可以用作是参数或者局部对象。Lambdas 的引入改变了C++标准库的使用方式(比如原来的一些仿函数谓词,现在可直接用)。
最简单的 lambda 函数不接收参数,并做一些简单的事情,比如这里的打印一句话:
[] { std::cout << "Hello Lambda" << std::endl; }
我们可以直接调用它,就像调用普通函数和函数对象那样,用 () :
[] { std::cout << "Hello Lambda" << std::endl; }();
或者
auto l = [] { std::cout << "Hello Lambda" << std::endl; };
l();
l();
l();
这里 lambda 对象的类型很复杂,通常也没有必要显式地写出来,我们正好用前面介绍过的 C++11 中的 auto 来简化我们的代码。如果一定要拿到 lambda 函数对象的类型,参考上面的 decltype 的用法三。
lambda 表达式的完整形式:
- lambda 函数除了少数几处细节(如没有默认构造函数、需要加mutable),几乎完全等同于一个对应的函数对象
- [] 称为 lambda introducer ,其中存放要捕获的外部变量表,外部变量要注意区分值传递和引用传递。如果里面放一个等号:[=, &y] 表示接收以值传递的形式接收所有的外界变量。
- () 中存放 lambda 函数的参数列表
- {} 是 lambda 函数的函数体
- 中间的三项(标明 opt 的)都是看情况可有可无的,但是一旦三个中有一个是出现的,那么小括号 () 就必须有;而若三个可选项都没有,则 () 也是可有可无的。
- mutable 指明参数是否可被改变,throwSpec 指明是否可能会抛出异常,retType 指明返回值的类型
- lambda 函数默认是内联的
例如:
#include <iostream>
int main() {
int id = 0;
//不加 mutable 关键字会报 id 是只读变量,不能修改。
auto f = [id] () mutable {
std::cout << "id: " << id << std::endl;
++id;
};
id = 42;
f(); id=0
f(); id=1
f(); id=2
std::cout << id << std::endl;
}
varidic template 变参模板详解
原视频花了很大篇幅来讲解这个新特性,但是没怎么用过(而多是出现在大型模板库的设计中),暂时略过。
C++11新特性 标准库部分
- type_traits
- unodered containers
- forward_list
- array
- tuple
- concurrency
- RegEx
- …
Rvalue references and Move Semantics
右值引用,是一种新的引用类型,避免不必要的copy(unnecessary copying)。当赋值右手边为rvalue,赋值左手边的值可以通过steal来获取rvalue的值,而不需要内存分配。
- Lvalue: 可以出现在operator =左侧者。
- Rvalue:只能出现在operator=右侧者。
上文简单介绍了右值引用的一些特点,但是右值引用最大的用处(可能没有之一)在于构建移动构造函数来减少拷贝次数。
深拷贝:简单的说如果类里面有指针,那么浅拷贝就是只拷贝指针,拷贝者和被拷贝者指向同一个地址。深拷贝就是构造一个等大的内存,然后从被拷贝者指针指向的内存复制数据到自己中。
我们需要拷贝ptr指向的所有内容,但是很多时候我们赋值之后就不需要nweNode了,因此我们可能会想如果能直接接管newNode的内容就好了。但是我们不能直接让newNode.ptr=nullptr,因为不是所有的赋值操作后newNode就不需要了,因此我们需要使用新的函数进行移动构造。
Perfect forwarding
写一个 move-aware class
class MyString {
public:
static size_t DCtor; //累计default-ctor呼叫次数
static size_t Ctor; //累计ctor呼叫次数
static size_t CCtor; //累计copy-ctor呼叫次数
static size_t CAsgn; //累计copy-asgn呼叫次数
static size_t MCtor; //累计move-ctor呼叫次数
static size_t MAsgn; //累计move-asgn呼叫次数
static size_t Dtor; //累计dtor的次数
private:
char* _data;
size_t _len;
void _init_data(const char* s) {
_data = new char[_len + 1];
memcpy(_data, s, _len);
_data[_len] = '\0';
}
public:
//default constuctor
MyString() :_data(nullptr), _len(0) { ++DCtor; }
//constructor
MyString(const char* p) :_len(strlen(p)) {
++Ctor;
_init_data(p);
}
//copy constructor
MyString(const MyString& str) :_len(str._len) {
++CCtor; //测试需要
_init_data(str._data);
}
//move constructor
MyString(MyString&& str) noexcept
:_data(str._data), _len(str._len) { //直接使用原来的指针
++MCtor;
str._len = 0;
str._data = nullptr; // 重要
}
//copy assignment
MyString& operator=(const MyString& str) {
++CAsgn;
//自我赋值检查
if (this != &str) {
if (_data) delete _data;
_len = str._len;
_init_data(str._data);
}
return *this;
}
//move assignment
MyString& operator=(MyString&& str) noexcept {
++MAsgn;
//自我赋值检查
if (this != &str) {
if (_data) delete _data;
_len = str._len;
_data = str._data;//直接使用原来的指针
str._len = 0;
str._data = nullptr;
}
return *this;
}
//dtor
virtual ~MyString() {
++Dtor;
if (_data) {
delete _data;
}
}
};
//...
视频PPT图解: