C++11 特性
1 变量和基本类型
1.1 long long 类型
扩展精度浮点数,10位有效数字
1.2 列表初始化
初始化的几种不同形式,其中用花括号来初始化变量称为列表初始化;
int i = 0;
int i = {0};
int i{0};
int i(0);
- 需要注意的是,当用于内置类型的变量时,这种初始化形式有一个重要的特点:
- 如果我们使用初始化且初始值存在丢失信息的风险,则编译器报错;
long double ld = 3.1414141414;
int a{ld}, b = {ld}; //报错
int c(ld), d = ld; //正确
1.3 nullptr 常量
int *p1 = nullptr; // 等价于int *p1 = 0;
1.4 constexpr 变量
将变量声明为constexpr类型以便由编译器来验证变量的值是否是一个常量表达式;
声明为constexpr的变量一定是一个常量,而且必须用常量表达式来初始化,比如说下面的情况则是不正确的:
int t = 10;
constexpr int q = t + 20;
cout << "q" << q << endl;
需要将t声明为 const
才是正确的;
一般来说,如果你认定变量是一个常量表达式,那就把它声明为constexpr类型;
1.5 类型别名声明
使用类型别名可以使复杂的类型名字变得更简单明了,易于理解和使用;
现在有两种方法可以用来定义类型别名,一种是 typedef
,另一种则是新标准中的 using
;
1.6 auto 类型指示符
auto让编译器通过初始值来推算变量的类型,所以,其定义的变量必须要有初始值;
int main() {
int i = 0;
const int ci = i;
auto b = &i; // b是一个整形指针(整数的地址就是指向整数的指针)
auto c = &ci; // c是一个指向整数常量的指针(对常量对象取地址是一种底层const)
return 0;
}
1.7 decltype 类型指示符
处理顶层const和引用的方式
const int ci = 0, &cj = ci;
decltype(ci) x = 0;
decltype(cj) y = x;
decltype(cj) z; //报错,因为cj是一个引用,因此作为引用的 z 必须要进行初始化
引用从来都是作为其所指对象的同义词出现,也就是根据其所指的对象决定,只有在decltype处是个例外;
1.8 类内初始化
class A
{
int x{0}
}
2 字符串、向量和数组
2.1 使用 auto 或 decltype 缩写类型
2.2 范围 for 语句
string str("hello world");
for (auto c : str) {
cout << c;
}
2.3 定义vector对象的vector(向量的向量)
编译器根据模版vector生成了三种不同的类型,分别是:
vector<int>,vector<vector<int>>, vector<classname>
新的版本已经不需要再添加一个空格
vector<vector<int> >
2.4 vector对象的列表初始化
放在花括号里
2.5 容器的cbegin 和 cend 函数
2.6 标准库begin 和 end 函数
2.7 使用auto和decltype简化声明
3 表达式
3.1 除法的舍入规则
新标准中,一律向0取整(直接切除小数部分)
double a = 12/5;
cout << a << endl;
输出结果为2,删掉了小数部分;
3.2 用大括号包围的值列表赋值
3.3 将sizeof用于类成员
使用作用域运算符来获取类成员的大小是许可的;
- 对数组执行sizeof运算得到整个数组所占空间的大小,等价于对数组中所有的元素各执行一次sizeof运算并将所得结果求和;
- 对string对象或vector对象执行sizeof运算只返回该类型固定部分的大小,不会计算对象中的元素占用多少空间;
4 语句
4.1 范围for语句
5 函数
5.1 标准库 initializer_list类
可以处理不同数量实参的函数,前提是实参类型要相同;
其提供的操作包括:
- 默认初始化
- 列表初始化
- 拷贝对象
- size
- begin
- end
int sum(const std::initializer_list<int>& list) {
int total = 0;
for (auto& e : list) {
total += e;
}
return total;
}
auto list = {1, 2, 3};
sum(list); // == 6
sum({1, 2, 3}); // == 6
sum({}); // == 0
5.2 列表初始化返回值
函数可以返回花括号包围的值的列表,如果列表为空,临时量执行初始化,否则,返回的值由函数的返回类型决定;
vector<string> getAddress(int num) {
if (num == 0) {
return {};
}
return {"address1", "address2", "address3"};
}
5.3 定义尾置返回类型
任何函数的定义都能够使用尾置来返回,最适用于返回类型比较复杂的情况;
比如:
// 返回类型为指针,该指针指向含有10个整数的数组
auto func(int i) -> int(*)[10] {
int arr[10] = {0};
return &arr;
}
5.4 使用decltype简化返回类型定义
int odd[] = {1, 3, 5, 7, 9};
int even[] = {2, 4, 6, 8, 10};
// 返回一个指针,指向数组
decltype(odd) *getArr(int i) {
return (i % 2) ? &odd : &even;
}
decltype并不会因为获取的是数组,则返回的是指针,还是需要在函数名前面加上 *
;
5.5 constexpr函数
是指能用于常量表达式的函数;
需要遵从几项约定:
- 函数的返回类型以及所有形参的类型都是字面值类型(只能用它的值来称呼它);
- 函数体中必须有且只有一条return语句(C++14不再做要求);
- 必须非virtual
constexpr int func2() {
return 10;
}
int main() {
int arr[func2() + 10] = {0};
return 0;
}
6 类
6.1 使用=default生成默认构造函数
如果实现了默认的构造函数,编译器则不会自动生成默认版本;可以通过使用关键字 default
来控制默认构造函数的生成,显示的指示编译器生成该函数的默认版本;
class MyClass{
public:
MyClass()=default; //同时提供默认版本和带参版本
MyClass(int i):data(i){}
private:
int data;
};
比如说如果想要禁止使用拷贝构造函数,则使用关键字 delete
;
class MyClass
{
public:
MyClass()=default;
MyClass(const MyClass& )=delete;
};
int main() {
MyClass my;
MyClass my2(my); //报错
return 0;
}
但需要注意的是,析构函数是不允许使用delete的,否则无法删除;
6.2 类对象成员的类内初始化
当提供一个类内初始值时,必须以符号=或者花括号表示
6.3 委托构造函数
一个委托构造函数使用它所属类的其它构造函数执行它自己的初始化过程,或者说它把它自己的一些(或者全部)指责委托给了其它构造函数;
class MyClass
{
public:
MyClass() : MyClass(10) {}
MyClass(int d) : data(d){}
inline int getData() {return data;}
private:
int data;
};
int main() {
MyClass my;
cout << my.getData() << endl;
return 0;
}
6.4 constexpr构造函数
如果想要使得函数拥有编译时计算的能力,则使用关键字 constexpr
同样的编译时使用对象:
class Square {
public:
constexpr Square(int e) : edge(e){};
constexpr int getArea() {return edge * edge;}
private:
int edge;
};
int main() {
Square s(10);
cout << s.getArea() << endl;
return 0;
}
如果成员函数标记为 constexpr
,则默认其是内联函数,如果变量声明为
constexpr
,则默认其是 const
;
7 IO库
7.1 用string对象处理文件名
新版本中增加了使用库类型string对象作为文件名,之前只能使用C风格的字符数组;
ifstream infile("/hello.sh");
8 顺序容器
补充:
顺序容器的类型:
容器 | 描述 |
---|---|
vector | 可变大小数组。支持快速随机访问。在尾部之外的位置插入或者删除元素时可能很慢 |
deque | 双端队列。支持快速随机访问。在头尾位置插入/删除速度很快 |
list | 双向链表。只支持双向双向顺序访问。在list中任何位置进行插入/删除操作 |
forward_list | 单向链表。只支持单向顺序访问。在链表任何位置进行插入/删除操作都很快 |
array | 固定大小数组。支持快速随即访问。不能添加或删除元素 |
string | 与vector相似的容器,但专门用于保存字符(字符数组,封装了char而已)。随机访问速度快,在尾部插入、删除速度快 |
总而言之,数组随机访问速度快,链表插入删除速度快;
8.1 array和forward_list容器
-
array
#include <string> #include <iterator> #include <iostream> #include <algorithm> #include <array> int main() { // 使用聚合初始化来构造 std::array<int, 3> a1{ {1,2,3} }; // C++11中需要使用双重花括号(而14中不需要) std::array<int, 3> a2 = {1, 2, 3}; // 不再需要等号了 std::array<std::string, 2> a3 = { {std::string("a"), "b"} }; // 支持基本的容器操作 std::sort(a1.begin(), a1.end()); std::reverse_copy(a2.begin(), a2.end(), std::ostream_iterator<int>(std::cout, " ")); // 支持范围for for(auto& s: a3) std::cout << s << ' '; }
-
forward_list
和
list
的使用类似,就不贴代码了;template < class T, class Alloc = allocator<T> > class forward_list;
Alloc,容器内部用来管理内存分配以及释放的内存分配器的类型,默认使用的是
std::allocator<T>
,;
8.2 容器的cbegin和cend函数
返回的是const的迭代器,当不需要写访问时,应使用cbegin和cend;
8.3 容器的列表初始化
8.4 容器的非成员函数swap
除了 array
外,swap不对任何元素进行拷贝、删除或者插入操作,因此可以保证常数时间内完成;swap 只是交换了容器内部数据结构,不会交换元素,因此,除string
外,指向容器的迭代器、引用和指针在 swap 操作后都不会失效;
但是,array会真正的交换它们的元素。
8.5 容器insert成员的返回类型
在新标准下,接受元素个数或范围的insert版本返回的是-指向第一个新加入元素的迭代器;
如果范围为空,不插入任何元素,insert
操作会将第一个参数返回;
int main() {
list<string> lst;
auto itr = lst.begin();
string word;
while (cin >> word) {
itr = lst.insert(itr, word); //等价于push_front
}
return 0;
}
8.6 容器的emplace成员
当调用 push
或 insert
成员函数时,我们将元素类型的对象传递给它们,这些对象被拷贝到容器里面,但是,当我们调用一个 emplace
成员函数时,则是将函数传递给元素类型的构造函数,会使用这些参数在容器管理的内存中直接构造元素;包括三个函数:emplace_front
,emplace
,emplace_back
,分别对应着 push_front
,insert
,push_back
为头部,指定位置,尾部;
class MyClass
{
public:
MyClass(int d) : data(d){}
inline int getData() {return data;}
private:
int data;
};
int main() {
vector<MyClass> mys;
mys.emplace_back(10);
mys.push_back(MyClass(20));
for (auto itr = mys.begin(); itr != mys.end(); itr++) {
cout << (*itr).getData() << endl;
}
return 0;
}
8.7 shrink_to_fit
调用该函数要求 deque
,vector
,string
退回不需要的内存空间;
int main() {
vector<int> nums;
nums.push_back(10);
nums.push_back(10);
nums.push_back(10);
nums.push_back(10);
nums.push_back(10);
cout << nums.capacity() << endl;
nums.shrink_to_fit();
cout << nums.capacity() << endl;
return 0;
}
返回结果为8,5
区别:
- reserve:预分配存储区大小,即capacity的值
- resize:容器大小,即size的值
8.9 string的数值转换函数
新标准中,引入多个函数实现数值数据和标准库string之间的转换:
函数 | 描述 |
---|---|
to_string(val) | 返回任意算术类型val的字符串 |
stoi(s, p, b) | int类型 |
stol(s, p, b) | long类型 |
stoul(s, p, b) | unsigned long类型 |
stoll(s, p, b) | long long类型 |
stoull(s, p, b) | unsigned long long类型 |
stof(s, p, b) | float类型 |
stod(s, p, b) | double类型 |
stold(s, p, b) | long double类型 |
9 泛型算法
9.1 lambda表达式
一个lambda具有一个返回类型、一个参数列表和一个函数体;
[capture list](parameter list) -> return type { function body }
9.2 lambda表达式中的尾置返回类型
9.3 标准库bind函数
auto newCallable = bind(callable, arg_list);
例子(绑定类成员函数)
#include <iostream>
#include <functional>
using namespace std;
using namespace std::placeholders;
class MyClass
{
public:
void fun1(void)
{
cout << "void fun1(void)" << endl;
}
int fun2(int i)
{
cout << "int fun2(int i)" << " i = " << i << endl;
return i;
}
};
int main()
{
MyClass my;
//使用类对象绑定
auto fun1 = bind(&MyClass::fun1, my);
fun1();
MyClass *p;
//使用类指针绑定,-1为占位符
auto fun2 = bind(&MyClass::fun2, p, _1);
int i = fun2(1);
cout << "i = " << i << endl;
cin.get();
return 0;
}
10. 关联容器
关联器类型
- map 关联数组,保存关键字-值对
- set 关键字即值,即只保存关键字的容器
- multimap 关键字可重复出现的map
- multiset 关键字可重复出现的set
- unordered_map 用哈希函数组织的map
- unordered_set 用哈希函数组织的set
- unordered_multimap 用哈希函数组织的map,关键字可重复
- unordered_multiset 用哈希函数组织的set,关键字可重复
10.1 关联容器的列表初始化
和顺序容器差不多
10.2 列表初始化pair的返回类型
10.3 pair的列表初始化
10.4 无序容器
包括4个无序关联容器(使用哈希函数和关键字类型的 == 运算符)
- unordered_map
- unordered_set
- unordered_multiset
- unordered_multimap
11 动态内存
已总结,略
11.1 智能指针
11.2 shared_ptr类
11.3 动态分配对象的列表初始化
11.4 auto和动态分配
11.5 unique_ptr类
11.6 weak_ptr类
11.7 范围for语句不能应用于动态分配数组
因为分配的内存并不是一个数组类型
11.8 动态分配数组的列表初始化
可以对数组中的元素进行值初始化,方法是在大小之后跟一对括号;
int *pia = new int[10]; //10个未初始化的int
int *pia2 = new int[10](); //10个初始化为0的int
int *pia3 = new int[10]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
delete [] pia; //释放
delete [] pia2;
delete [] pia3;
11.9 auto不能用于分配数组
虽然我们用空括号对数组中元素进行值初始化,但不能在括号中给出初始化器,这意味着不能用auto分配数组;
11.10 allocator::construct可使用任意构造函数
allocator分配的内存是未构造的,我们按需要在此内存中构造对象。在新标准库中,construct成员函数接受一个指针和零个或多个额外参数,在给定位置构造一个元素。
int main() {
allocator<string> alloc;
string* str = alloc.allocate(4); //分配10个未初始化的string
auto itr = str;
alloc.construct(itr, "chengdu"); //chengdu
cout << str[0] << endl;
itr++;
alloc.construct(itr, "beijing"); //beijing
cout << str[1] << endl;
itr++;
alloc.construct(itr, 10, 'c'); //10个c组成的字符串
cout << str[2] << endl;
itr++;
alloc.construct(itr); //空字符串
cout << str[3] << endl;
for (int i = 0; i < 4; i++) {
cout << str[i] << endl;
}
// 销毁
while (itr != str) {
alloc.destroy(itr--);
}
return 0;
}
为了使用allocate返回的内存,我们必须用construct构造对象,使用未构造的内存,其行为是未定义的;我们只有对真正构造了的元素进行destroy操作;
12 拷贝控制
12.1 将=default用于拷贝控制类对对象
可以类内或者类外修饰成员函数,如果是类内,合成的函数将隐式地声明未内联的,如果不希望这样,则在类外声明;
我们只能对具有合成版本的成员函数使用=default(即,默认构造函数或拷贝控制成员)
12.2 使用=delete用于拷贝控制成员
对于析构函数已删除的类型,不能定义该类型的变量或释放指向该类型动态分配的指针;
12.3 用移动类对象代替拷贝类对象
为了解决之前存在的性能问题,避免不必要的内存拷贝,新标准中有两种方法来替代拷贝函数:移动函数和 move
移动构造函数
A(A && h) : a(h.a){
h.a = nullptr;
}
12.4 右值引用
右值引用就是必须绑定到右值的引用,通过 &&
来获得右值引用;
右值引用就是必须绑定到右值的引用,通过 &&
来获得右值引用;
12.4.1 区别左值和右值
- 左值:在赋值号左边,可以被赋值的值,可以取地址;
- 右值:在赋值号右边,取出值赋给其他变量的值;
- 左值:可以放到等号左边的东西叫左值。
- 右值:不可以放到等号左边的东西就叫右值。
- 左值引用:type & 引用名 = 左值表达式
- 右值引用:type && 引用名 = 右值表达式
左值一般有
- 函数名和变量名
- 返回左值引用的函数调用
- 前置自增自减表达式++i、–i
- 由赋值表达式或赋值运算符连接的表达式(a=b, a += b等)
- 解引用表达式*p
- 字符串字面值"abcd"
纯右值
- 除字符串字面值外的字面值
- 返回非引用类型的函数调用
- 后置自增自减表达式i++、i–
- 算术表达式(a+b, a*b, a&&b, a==b等)
- 取地址表达式等(&a)
- 运算表达式产生的临时变量、
- 不和对象关联的原始字面量、
- 非引用返回的临时变量、
- lambda表达式
将亡值
- 右值引用相关的表达式,
- 通常指将要被移动的对象、
- T&&函数的返回值、
- std::move函数的返回值、
- 转换为T&&类型转换函数的返回值,
- 将亡值可以理解为即将要销毁的值,通过“盗取”其它变量内存空间方式获取的值,在确保其它变量不再被使用或者即将被销毁时,可以避免内存空间的释放和分配,延长变量值的生命周期,常用来完成移动构造或者移动赋值的特殊任务。
int a = 0; //2
int b = a; //1
一个对象被用作右值时,使用的是它的内容(值),比如1中的 a
,被当作左值时,使用的是它的地址,比如2中的 a
,常规左值;
int main() {
int i = 1; //i为常规左值
int &r = i; //正确:r绑定到i上,r是一个引用
int &&rr = i; //错误:不能将一个右值引用绑定到左值i上
int &r2 = i * 1; //错误:等号右边是一个右值,但左值引用只能绑定到左值上
int &&rr2 = i * 1; //正确:右值引用绑定到右值上
const int &r3 = i * 1; //正确:可以将一个const的左值引用绑定到右值上
return 0;
}
- 返回左值引用的函数、赋值、下标、解引用和前置递增/递减运算符,都是返回左值的表达式的例子,我们可以将一个左值引用绑定到这类表达式的结果上;
- 返回非引用类型的函数,连同算术、关系、位以及后置递增/递减运算符,都生成右值;我们不能将左值引用绑定到这类表达式上,但是我们可以将一个const的左值引用或者一个右值引用绑定到这类表达式之上;
12.5 标准库move函数
将左值强制转换成右值
我们可以销毁一个移后源对象,也可以赋予它新值,但不能使用一个移后源对象的值
// 移动构造函数,参数 "arg.member" 是左值
A(A&& arg) : member(std::move(arg.member))
{
}
// 移动赋值函数
A& operator=(A&& other) {
member = std::move(other.member);
return *this;
}
12.6 移动构造函数和移动赋值
12.7 移动构造函数通常是noexcept
如果需要通知标准库这是一个不会抛出异常的移动操作,可以在函数后面指明 noexcept
,如果在一个构造函数中,noexcept
出现在参数列表和初始化列表开始的冒号之间;
不抛出异常的移动构造函数和移动赋值运算符必须标记为noexcept
12.8 移动迭代器
新标准中可以通过调用标准库的 make_move_iterator
函数将一个普通迭代器转换为一个移动迭代器,这样可以避免拷贝操作带来的性能问题;
#include <iostream> // std::cout
#include <iterator> // std::make_move_iterator
#include <vector> // std::vector
#include <string> // std::string
#include <algorithm> // std::copy
int main() {
std::vector<std::string> foo(3);
std::vector<std::string> bar{ "one","two","three" };
std::copy(make_move_iterator(bar.begin()),
make_move_iterator(bar.end()),
foo.begin());
// bar now contains unspecified values; clear it:
bar.clear();
std::cout << "foo:";
for (std::string& x : foo) std::cout << ' ' << x;
std::cout << '\n';
return 0;
}
12.9 引用限定成员函数
引用限定符可以是 &
或者 &&
,分别指出 this
可以指向一个左值或者右值,引用限定符只能用于成员函数,而且必须同时出现在函数的声明和定义中;
可以在参数列表之后使用引用限定符来指定this对象的左值与右值属性;
- 若引用限定符为&,则表明this对象指向着左值对象;
- 若引用限定符为&&,则表明this对象指向着右值对象;
13 重载运算与类型转换
13.1 function类模版
function模版提供一种通用、多态的函数封装。其实例可以对任何可调用的目标进行存储、赋值和调用操作,这些目标包括函数、lambda表达式、绑定表达式以及其它函数对象等;
#include <functional>
#include <iostream>
struct Foo {
Foo(int num) : num_(num) {}
void print_add(int i) const { std::cout << num_+i << '\n'; }
int num_;
};
void print_num(int i)
{
std::cout << i << '\n';
}
struct PrintNum {
void operator()(int i) const
{
std::cout << i << '\n';
}
};
int main()
{
// 保存自由函数
std::function<void(int)> f_display = print_num;
f_display(-9);
// 保存 lambda 表达式
std::function<void()> f_display_42 = []() { print_num(42); };
f_display_42();
// 保存 std::bind 的结果
std::function<void()> f_display_31337 = std::bind(print_num, 31337);
f_display_31337();
// 保存成员函数
std::function<void(const Foo&, int)> f_add_display = &Foo::print_add;
Foo foo(314159);
f_add_display(foo, 1);
// 保存成员函数和对象
using std::placeholders::_1;
std::function<void(int)> f_add_display2= std::bind( &Foo::print_add, foo, _1 );
f_add_display2(2);
// 保存成员函数和对象指针
std::function<void(int)> f_add_display3= std::bind( &Foo::print_add, &foo, _1 );
f_add_display3(3);
// 保存函数对象
std::function<void(int)> f_display_obj = PrintNum();
f_display_obj(18);
}
结合lambda
std::function<void(int)> f; // 这里表示function的对象f的参数是int,返回值是void
#include <functional>
#include <iostream>
struct Foo {
Foo(int num) : num_(num) {}
void print_add(int i) const { std::cout << num_ + i << '\n'; }
int num_;
};
void print_num(int i) { std::cout << i << '\n'; }
struct PrintNum {
void operator()(int i) const { std::cout << i << '\n'; }
};
int main() {
// 存储自由函数
std::function<void(int)> f_display = print_num;
f_display(-9);
// 存储 lambda
std::function<void()> f_display_42 = []() { print_num(42); };
f_display_42();
// 存储到 std::bind 调用的结果
std::function<void()> f_display_31337 = std::bind(print_num, 31337);
f_display_31337();
// 存储到成员函数的调用
std::function<void(const Foo&, int)> f_add_display = &Foo::print_add;
const Foo foo(314159);
f_add_display(foo, 1);
f_add_display(314159, 1);
// 存储到数据成员访问器的调用
std::function<int(Foo const&)> f_num = &Foo::num_;
std::cout << "num_: " << f_num(foo) << '\n';
// 存储到成员函数及对象的调用
using std::placeholders::_1;
std::function<void(int)> f_add_display2 = std::bind(&Foo::print_add, foo, _1);
f_add_display2(2);
// 存储到成员函数和对象指针的调用
std::function<void(int)> f_add_display3 = std::bind(&Foo::print_add, &foo, _1);
f_add_display3(3);
// 存储到函数对象的调用
std::function<void(int)> f_display_obj = PrintNum();
f_display_obj(18);
}
13.2 explicit类型转换运算符
用来防止由构造函数定义的隐式转换;
看看构造函数的隐式转换,如果类的构造函数有一个参数,那么在编译的时候就会有一个缺省的转换操作,会将其参数转化成类的对象;
class MyClass
{
public:
MyClass( int num );
}
// 编译器会将10转换成MyClass对象
MyClass obj = 10;
避免这种自动转换的操作则需要 explicit
:
class MyClass
{
public:
explicit MyClass( int num );
}
MyClass obj = 10; //err,can't non-explict convert
其只能用在类内部的构造函数声明上,不能用在类外部的函数定义上;
14 面向对象程序设计
14.1 虚函数的override和final指示符
override
可以帮助程序员的意图更加的清晰的同时让编译器可以为我们发现一些错误。其只能用于覆盖基类的虚函数;
final
使得任何尝试覆盖该函数的操作都将引发错误,并不特指虚函数;
均出现在形参列表(包括任何const或者引用限定符)以及尾置返回类型之后
14.2 删除的拷贝控制和继承
如果函数在基类中被定义为是删除的,则派生类对应的也是删除的;
14.3 继承的构造函数
委派和继承构造函数是由C++11引进为了减少构造函数重复代码而开发的两种不同的特性;
a. 通过特殊的初始化列表语法,委派构造函数允许类的一个构造函数调用其它的构造函数
X::X(const string& name) : name_(name) {
...
}
X::X() : X("") { }
b. 继承构造函数允许派生类直接调用基类的构造函数,一如继承基类的其它成员函数,而无需重新声明,当基类拥有多个构造函数时这一功能尤其有用:
class Base {
public:
Base();
Base(int n);
Base(const string& s);
...
};
class Derived : public Base {
public:
using Base::Base; // Base's constructors are redeclared here.
};
using声明语句将令编译器产生代码,对于基类的每个构造函数,编译器都在派生类中生成一个形参列表完全相同的构造函数;如果派生类含有自己的数据成员,则这些成员将会被默认初始化;
15 模版与泛型编程
15.1 声明模版类型形参为友元
在新标准中,我们可以将模板类型参数声明为友元:
template <typename Type> class Bar {
friend Type; //将访问权限授予用来实例化Bar的类型
};
此处我们将用来实例化Bar的类型声明为友元。因此,对于某个类型名Foo,Foo
将成 Bar<Foo>
的友元。
15.2 模版类型别名
新标准中允许我们为模版定义一个类型别名:
template<typename T> using twin = pair<T, T>;
twin<string> authors;
其中,twin是一个 pair<T,T>
;
15.3 模版函数的默认模版参数
template <typename T, typename F = less<T>>
int compare(const T &v1, const T &v2, F f= F()) {
if (f(v1, v2)) return -1;
if (f(v2, v1)) return 1;
return 0;
}
就像之前能为函数参数提供默认实参一样,compare有一个默认模版实参 less<T>
和一个默认函数实参 F()
15.4 实例化的显示控制
显示实例化:在不发生函数调用的时候将函数模版实例化或者在不适用类模版的时候将类模版实例化,这可以避免实例化相同的模版所带来的额外开销;
- 函数模版实例化
template函数返回类型 函数模板名<实际类型列表>(函数参数列表)
- 类模版实例化
template class 类模板名<实际类型列表>
15.5 模版函数与尾置返回类型
例子:
//尾置返回允许我们在参数列表之后声明返回类型
template <typename T>
auto func(T beg, T end) -> decltype(*beg) {
return *beg; //返回序列中一个元素的引用
}
15.6 引用折叠规则
规则如下:
- 所有右值引用折叠到右值引用上仍然是一个右值引用,比如(A&& && 变成 A&&)
- 所有的其它引用类型之间的折叠都将会变成左值引用,比如(A& &变成A&,A& && 变成 A&,A&& & 变成 A&)
15.7 用static_cast将左值转换为右值
可以通过类型转换 static_cast<Type&&>
来将返回右值引用;
int s=101;
int&& foo(){
return static_cast<int&&>(s);
} //返回值为右值引用
int main() {
int i=foo(); //右值引用作为右值,在赋值运算符的右侧
int&& j=foo(); //j是具名引用。因此运算符右侧的右值引用作为左值
int* p=&j; //取得j的内存地址
}
15.8 标准库forward函数
右值引用至少解决了两个问题:
- 实现
move
语义 - 完美转发
完美转发
有的时候,我们需要将一个函数中某一组参数原封不动的传递给另一个函数,这里不仅仅需要参数的值不变,而且需要**参数的类型属性(左值/右值)**保持不变-完美转发;
使用forward
- 原型:
template< class T >
constexpr T&& forward( typename std::remove_reference<T>::type& t );
template< class T >
constexpr T&& forward( typename std::remove_reference<T>::type&& t );
- 例子:
#include <iostream>
#include <utility>
#include <memory>
class A {
public:
A(int && n) { std::cout << "rvalue constructor -> n=" << n << std::endl;}
A(int& n) { std::cout << "lvalue constructor -> n=" << n << std::endl;}
};
template<class T, class U>
std::unique_ptr<T> make_unique1(U&& u) {
return std::unique_ptr<T>(new T(std::forward<U>(u)));
//return std::unique_ptr<T>(new T(u));
}
int main() {
int i = 24;
auto p1 = make_unique1<A>(666); // rvalue forwarding
auto p2 = make_unique1<A>(i); // lvalue forwarding
auto p3 = make_unique1<A>(std::move(i)); // rvalue forwarding
return 0;
}
但是如果我们没有使用 forward
函数,结果则全部都调用的是 lvalue constructor;
15.9 可变参数模版与转发
右值引用+完美转发+可变参数模版实现下面这个函数;
// args为右值引用,decltype用于返回值
template<class Function, class... Args>
auto FuncWrapper(Function && f, Args && ... args) -> decltype(f(std::forward<Args>(args)...))
{
return f(std::forward<Args>(args)...);
}
void test0()
{
cout <<"void"<< endl;
}
int test1()
{
return 1;
}
int test2(int x)
{
return x;
}
string test3(string s1, string s2)
{
return s1 + s2;
}
int main() {
int num = 10;
int && nnum = num + 10;
int & nnum2 = num;
FuncWrapper(test0); // 没有返回值,打印1
cout << FuncWrapper(test1) << endl; // 没有参数,有返回值,返回1
cout << FuncWrapper(test2, 1) << endl; // 有参数,有返回值,返回1
cout << FuncWrapper(test2, std::move(num)) << endl; // 有参数,有返回值,返回左值10
cout << FuncWrapper(test2, std::move(nnum2)) << endl; // 有参数,有返回值,返回左值引用10
cout << FuncWrapper(test2, nnum) << endl; // 有参数,有返回值,返回右值引用20
cout << FuncWrapper(test3, "aa", "bb") << endl; // 有参数,有返回值,返回"aabb"
return 0;
}
16 新的字符串表示方式——原生字符串(Raw String Literals)
string path4 = R"(C:\Program "Files" (x86)\\alipay\aliedit\5.1.0.3754)";
17.强类型枚举
// Specifying underlying type as `unsigned int`
enum class Color : unsigned int { Red = 0xff0000, Green = 0xff00, Blue = 0xff };
Color c = Color::Red;
18.attributes
// `noreturn` attribute indicates `f` doesn't return.
[[ noreturn ]] void f() {
throw "error";
}
19.std::chrono
std::chrono::time_point<std::chrono::steady_clock> start, end;
start = std::chrono::steady_clock::now();
// Some computations...
end = std::chrono::steady_clock::now();
std::chrono::duration<double> elapsed_seconds = end - start;
double t = elapsed_seconds.count(); // t number of seconds, represented as a `double`
duration
std::chrono::duration表示一段时间,常见的单位有s、ms等,示例代码:
// 拿休眠一段时间举例,这里表示休眠100msstd::this_thread::sleep_for(std::chrono::milliseconds(100));
sleep_for里面其实就是std::chrono::duration,表示一段时间,实际是这样:
typedef duration<int64_t, milli> milliseconds;typedef duration<int64_t> seconds;
duration具体模板如下:
template <class Rep, class Period = ratio<1> > class duration;
Rep表示一种数值类型,用来表示Period的数量,比如int、float、double,Period是ratio类型,用来表示【用秒表示的时间单位】比如second,常用的duration已经定义好了,在std::chrono::duration下:
- ratio<3600, 1>:hours
- ratio<60, 1>:minutes
- ratio<1, 1>:seconds
- ratio<1, 1000>:microseconds
- ratio<1, 1000000>:microseconds
- ratio<1, 1000000000>:nanosecons
ratio的具体模板如下:
template <intmax_t N, intmax_t D = 1> class ratio;
N代表分子,D代表分母,所以ratio表示一个分数,我们可以自定义Period,比如ratio<2, 1>表示单位时间是2秒。
** **
time_point
表示一个具体时间点,如2020年5月10日10点10分10秒,拿获取当前时间举例:
std::chrono::time_point<std::chrono::high_resolution_clock> Now() {
return std::chrono::high_resolution_clock::now();
}// std::chrono::high_resolution_clock为高精度时钟,下面会提到
20.std::future std::async
std::async会自动创建一个线程去调用 线程函数,它返回一个std::future,这个future中存储了线程函数返回的结果,当我们需要线程函数的结果时,直接从future中获取,非 常方便。
std::async的原型async(std::launch::async | std::launch::deferred, f, args…),第一个参数是线程的创建策略,有两种策略,默认的策略是立即创建线程:
- std::launch::async:在调用async就开始创建线程。
- std::launch::deferred:延迟加载方式创建线程。调用async时不创建线程,直到调用了future的get或者wait时才创建线程。
第二个参数是线程函数,第三个参数是线程函数的参数。
std::future<int> f1 = std::async(std::launch::async, [](){
return 8;
});
cout << f1.get() << endl; //output: 8
std::future<void> f2 = std::async(std::launch::async, [](){
cout << 8 << endl;
//return 8;
});
f2.wait(); //output: 8
std::future<int> future = std::async(std::launch::async, [](){
std::this_thread::sleep_for(std::chrono::seconds(3));
return 8;
});
std::cout << "waiting...\n";
//Test12();
std::future_status status;
Sleep(3000);
do {
status = future.wait_for(std::chrono::seconds(1));
if (status == std::future_status::deferred) {
std::cout << "deferred\n";
}
else if (status == std::future_status::timeout) {
std::cout << "timeout\n";
}
else if (status == std::future_status::ready) {
std::cout << "ready!\n";
}
} while (status != std::future_status::ready);
std::cout << "result is " << future.get() << '\n';
21.packaged_task
- 这时我们常常会设计出任务队列和线程池的结构。此时,就可以使用packaged_task来包装任务。
- packaged_task绑定到一个函数或者可调用对象上。
- 当它被调用时,它就会调用其绑定的函数或者可调用对象。
- 并且,可以通过与之相关联的future来获取任务的结果。
- 调度程序只需要处理packaged_task,而非各个函数。
- packaged_task对象是一个可调用对象,它可以被封装成一个std::fucntion,或者作为线程函数传递给std::thread,或者直接调用。
代码示例
double concurrent_worker(int min, int max) {
double sum = 0;
for (int i = min; i <= max; i++) {
sum += sqrt(i);
}
return sum;
}
double concurrent_task(int min, int max) {
vector<future<double>> results; // ①
unsigned concurrent_count = thread::hardware_concurrency();
min = 0;
for (int i = 0; i < concurrent_count; i++) { // ②
packaged_task<double(int, int)> task(concurrent_worker); // ③
results.push_back(task.get_future()); // ④
int range = max / concurrent_count * (i + 1);
thread t(std::move(task), min, range); // ⑤
t.detach();
min = range + 1;
}
cout << "threads create finish" << endl;
double sum = 0;
for (auto& r : results) {
sum += r.get(); ⑥
}
return sum;
}
int main() {
auto start_time = chrono::steady_clock::now();
double r = concurrent_task(0, MAX);
auto end_time = chrono::steady_clock::now();
auto ms = chrono::duration_cast<chrono::milliseconds>(end_time - start_time).count();
cout << "Concurrent task finish, " << ms << " ms consumed, Result: " << r << endl;
return 0;
}
- 首先创建一个集合来存储future对象。我们将用它来获取任务的结果。
- 同样的,根据CPU的情况来创建线程的数量。
- 将任务包装成packaged_task。请注意,由于concurrent_worker被包装成了任务,我们无法直接获取它的return值。而是要通过future对象来获取。
- 获取任务关联的future对象,并将其存入集合中。
- 通过一个新的线程来执行任务,并传入需要的参数。
- 通过future集合,逐个获取每个任务的计算结果,将其累加。这里r.get()获取到的就是每个任务中concurrent_worker的返回值。
22. promise与future
- 在上面的例子中,concurrent_task的结果是通过return返回的。但在一些时候,我们可能不能这么做:在得到任务结果之后,可能还有一些事情需要继续处理,例如清理工作。
- 这个时候,就可以将promise与future配对使用。这样就可以将返回结果和任务结束两个事情分开。
改写
double concurrent_worker(int min, int max) {
double sum = 0;
for (int i = min; i <= max; i++) {
sum += sqrt(i);
}
return sum;
}
void concurrent_task(int min, int max, promise<double>* result) { // ①
vector<future<double>> results;
unsigned concurrent_count = thread::hardware_concurrency();
min = 0;
for (int i = 0; i < concurrent_count; i++) {
packaged_task<double(int, int)> task(concurrent_worker);
results.push_back(task.get_future());
int range = max / concurrent_count * (i + 1);
thread t(std::move(task), min, range);
t.detach();
min = range + 1;
}
cout << "threads create finish" << endl;
double sum = 0;
for (auto& r : results) {
sum += r.get();
}
result->set_value(sum); // ②
cout << "concurrent_task finish" << endl;
}
int main() {
auto start_time = chrono::steady_clock::now();
promise<double> sum; // ③
concurrent_task(0, MAX, &sum);
auto end_time = chrono::steady_clock::now();
auto ms = chrono::duration_cast<chrono::milliseconds>(end_time - start_time).count();
cout << "Concurrent task finish, " << ms << " ms consumed." << endl;
cout << "Result: " << sum.get_future().get() << endl; // ④
return 0;
}
- concurrent_task不再直接返回计算结果,而是增加了一个promise对象来存放结果。
- 在任务计算完成之后,将总结过设置到promise对象上。一旦这里调用了set_value,其相关联的future对象就会就绪。
- 这里是在main中创建一个promoise来存放结果,并以指针的形式传递进concurrent_task中。
- 通过sum.get_future().get()来获取结果。第2点中已经说了:一旦调用了set_value,其相关联的future对象就会就绪。
需要注意的是,future对象只有被一个线程获取值。并且在调用get()之后,就没有可以获取的值了。如果从多个线程调用get()会出现数据竞争,其结果是未定义的。
如果真的需要在多个线程中获取future的结果,可以使用shared_future。