C++11
1. 类成员变量初始化
C++11允许在类成员变量定义时直接给初始缺省值,默认生成构造函数会使用这些缺省值初始化
class A
{
public:
A(){}
private:
int a = 0;
int b = 1;
int c = 2;
};
2. initializer_list
initializer_list属于一种特殊类型,字面意义算作初始化列表
在官方文档中有详细解释 https://cplusplus.com/reference/initializer_list/initializer_list/
int main()
{
auto l = { 1,2,3,4 };
cout << typeid(li).name() << endl; //打印出来的类型为initializer_list<int>
return 0;
}
初始化列表类型通常拿来做一些容器的构造函数的参数。
int main()
{
//vector
vector<int> v = { 1,2,3,4,5 };
//list
list<int> ls = { 1,2,3,4,5 };
//set
set<int> st = { 1,2,3,4,5 };
//map
map<int, int> mp = { {1,1},{2,2} };
mp.insert({ 3,3 }); //这里会通过{3,3} 先构造一个pair<int,int>对象
return 0;
}
3. auto
在C++98中,auto是一个存储类型的说明符,说明变量是局部自动存储类型(在函数内部定义的变量,函数调用时自动分配存储空间,函数调用结束时自动释放空间,通常存储在栈上),但通常系统默认变量为自动存储类型,因此不需要特意声明auto
在C++11中,auto可以实现自动类型推断
int main()
{
vector<int> v = { 1,2,3,4,5 };
auto it1 = v.begin();
cout << typeid(it1).name() << endl;
//自动推导it1为vector<int>类型迭代器
return 0;
}
4. decltype
decltype(变量a) 变量名 = 变量值
decltype可以把变量a的类型推出来,返回值可以直接声明其他变量
int main()
{
int a = 0;
decltype(a) b = 1;
cout << typeid(b).name() << endl; //打印 int
return 0;
}
5. 范围 for
针对容器类遍历的for循环另类写法
int main()
{
vector<int> vt = { 1,2,3,4,5 };
set<int> st = { 6,9,8,7,10 };
for (auto i : vt) cout << i << " "; //打印1 2 3 4 5
for (auto i : st) cout << i << " "; //打印6 7 8 9 10
return 0;
}
6. 新容器
array
array是固定大小的数组容器,没有vector那样通过push_back()添加元素自动扩容,编译时就确定大小。
int main()
{
array<int, 5> arr = {1,2,3}; //array声明数组大小是在模版参数部分
arr[3] = 4; // 可以通过方括号进行访问元素或者改变元素值
arr.at(4) = 5; // 也可以通过.at()方式改变下标值,如果越界会抛异常
for (int i : arr) cout << i << " ";
return 0;
}
forward_list
单链表,不支持反向迭代器,仅能从头往后遍历
int main()
{
forward_list<int> fls = { 1,2,3,4 };
auto it = fls.begin();
while (it != fls.end())
{
cout << *it++ << endl;
}
return 0;
}
unordered_set / unordered_map
基于哈希表原理的容器,用法与set和map相同,但是通过遍历出来的数据是无序的。
哈希的查找效率为O(1),而红黑树为O(longN)
int main()
{
unordered_set<int> st;
unordered_map<int, int> mp;
for (int i = 0; i < 5; i++)
{
st.insert(rand()); //插入随机值
mp.insert({ rand(),rand()});
}
for (auto i : st) cout << i << " "; cout << endl;
for (auto e : mp) cout << e.first << " : " << e.second << endl;
return 0;
}
7. 左值引用和右值引用
概念
左值: 可以表示数据的表达式(变量名,解引用的指针),可以对它取地址 + 赋值
左值可以出现在赋值符号的左边,右值不可以。
无论是左值引用还是右值引用,都是给对象取别名
int main()
{
//左值列举
int* ptr = new int;
int a = 1;
const int b = 2;
//对上面左值的左值引用
int*& rptr = ptr;
int& rp = *ptr;
int& ra = a;
const int& rb = b;
return 0;
}
右值可以出现在赋值符号的右边,右值不能取地址
右值引用用法为:类型&& 别名 = 变量名
常见右值:匿名对象,临时对象,常量,表达式返回值,函数返回值,内置类型(纯数字10,1.1),将亡值(作函数返回值的自定义类型)
int main()
{
int a = 1, b = 2;
//常见右值
a + b; //表达式返回值
10; //数字常量
fun(a, b);//函数返回值
int&& r1 = a + b;
int&& r2 = 10;
int&& r3 = fun(a, b);
int& r4 = 10; //错误,左值引用无法给右值取别名
const int& r5 = 10; //正确, 常量引用可以给右值取别名
return 0;
}
右值无法取地址,但是给右值取别名后是可以取地址并且更改的
如果不想更改可以添加const
int main()
{
int&& r1 = 10;
cout << r1 << endl; // 打印10
r1 = 20;
cout << r1 << endl; // 打印20
return 0;
}
总结
const左值引用既可以引用左值,也可以引用右值
右值只能引用右值,不能引用左值
但是右值引用可以引用move后的左值
左值使用场景
做函数的参数和返回值都可以提高效率
传值做参数会产生深拷贝,传引用则不会
void func1(string& s) //左值引用做参数可以减少拷贝
{
s += '!'; //string& operator+=(char ch); //string中重载的+=返回是传引用
}
int main()
{
string s = "hahaha";
func1(s);
cout << s << endl;
return 0;
}
左值引用做函数返回对象时,如果是局部变量,就不能使用左值引用,只能传值返回。
右值引用解决左值缺点
左值不能返回函数局部变量这一缺点,右值引用可以完美弥补。
原理:函数局部变量出了函数作用域自动释放空间,这个值也叫将亡值。返回的将亡值可以被右值引用。
STL容器新增的移动构造和移动赋值就是基于右值引用的特性,
右值引用想引用左值需要利用move函数,move唯一功能是将一个左值强制转换为右值引用
int main()
{
string s1 = "12345";
string s2 = move(s1); //s1被强制转换成右值,资源会被转移给s2,s1被置空
cout << s1 << endl; //打印空
cout << s2 << endl; //打印12345
return 0;
}
STL容器很多接口函数都增加了右值引用版本
完美转发
- 模版中的&&不代表右值引用,而是代表万能引用,既能接受左值,也能接受右值。
- 模版的万能引用只是提供了能够同时接受左值引用和右值引用的能力
- 但引用类型的唯一作用就是限制了接受的类型,接受进来后都会回退成左值
- 如果需要再传参时保持他的属性,需要forward
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
template<typename T>
void PerfectForward(T&& t)
{
Fun(t); // t传进来全部变成左值
Fun(forward<T>(t)) //保证t传进来时的属性,左就是左,右就是右
}
int main()
{
PerfectForward(10); //右值
int a;
PerfectForward(a); //左值
PerfectForward(move(a)); //右值
const int b = 8;
PerfectForward(b); //左值
PerfectForward(move(b)); //右值
return 0;
}
新的默认成员函数
在原来的C++类中,有六个默认成员函数
构造函数,析构函数,拷贝构造,拷贝赋值,取地址重载,const取地址重载
在C++11中,新增了移动构造和移动赋值
- 如果没有实现移动构造和移动赋值,且没有实现析构函数,拷贝构造和拷贝赋值的任意一个,编译器会自动生成默认移动构造和默认移动赋值
8. default 和 delete
如果你实现了析构函数,拷贝构造和拷贝赋值的任意一个,又想让编译器生成默认移动构造,可以使用default关键字
class A
{
public:
A(){}
A(const A& a) {}
A(A&& a) = default; //让编译器显式生成默认移动构造
private:
int k = 0;
};
如果想要限制某些默认函数的生成,可以使用delete关键字,称这种函数为删除函数
class A
{
public:
A(){}
A(const A& a) = delete;
private:
int k = 0;
};
9. lambda表达式
用法
书写格式:
[捕捉列表](参数列表)mutable -> 返回值类型 {函数体}
捕捉列表:该列表总是出现在lambda函数开始的位置,编译器根据[]来判断接下来代码是否为lambda函数,捕捉列表能够捕捉上下文变量供lambda函数使用,当为[ = ] 时表示值捕获,为[ & ]时为引用捕获,为[变量名…]时则是对上下文特定变量进行捕获
参数列表:与普通函数的参数列表一致,如果不需要参数传递,可以连同()一起省略
mutable:意为可变的,默认情况下,lambda函数总是一个const函数,mutable可以取消其常量特性,使用这个修饰符(参数列表的()不可省略),不使用则忽略。
返回值类型:没有返回值可以省略,返回值类型明确的情况下可以省略,由编译器自行推导
函数体:在函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量
int main()
{
int a = 0;
auto fun = [=]()mutable {
a = 5; //如果不用mutable会该语句报错
cout << a << endl; // 打印5
};
fun();
cout << a << endl; // 打印0
return 0;
}
在值捕获时,mutable虽然会赋予在函数体内修改外部值的权利,但是并不会修改外部值,它修改的只是一个临时对象
捕捉列表上下文哪些能够被lambda使用
- [变量名]:值传递捕捉变量
- [&变量名]:引用传递捕捉变量
- [=]:值传递捕捉所有变量
- [&]:引用传递所有变量
- [this]:值传递捕捉this指针
- 从语法上捕捉列表可以有多个捕捉项组成,用逗号分割,如[=,&a,&b]
- 但捕捉列表不能重复传递,否则会编译器出错,如[=,a,b]
底层原理
lambda表达式不能互相赋值
int main()
{
auto f1 = [] {cout << "haha" << endl; };
auto f2 = [] {cout << "haha" << endl; };
f1 = f2;
return 0;
}
上述代码报错原因是找不到operator=()
- 底层编译器对于lambda表达式的处理方式,完全按照函数对象(仿函数)的方式处理。编译器自动生成一个类,在该类中重载了operator(),但并没有重载operator=()
- 但是能够通过拷贝构造的方式构造一个新的副本,如 auto f3(f2)
- 甚至可以将lambda表达式赋值给相同类型的函数指针
void (*fun)(); //定义一个函数指针变量
int main()
{
auto f1 = [] {cout << "haha" << endl; };
fun = f1;
fun(); //打印 haha
return 0;
}
10. function包装器
function能够对函数进行一定的包装
#include<functional>
int fun(int a,int b)
{
return a + b;
}
int main()
{
//包装函数
function<int(int, int)> ft1 = fun;
//包装lambda表达式
function<int(int, int)> ft2 = [](int a, int b) {return a + b;};
cout << ft1(1, 2) << endl;
cout << ft2(1, 2) << endl;
return 0;
}
包装器可以使得一些看起来不一样的函数被统一成function<>的形式
11. bind
bind就像一个函数包装器,可以生成一个新的可调用对象来适应原对象的参数列表
bind(函数指针, 参数1,参数2…)
int fun(int a, int b)
{
return a + b;
}
int main()
{
//调用f1时,等于调用fun但是只导入f1的第一个参数和第二个参数
function<int(int,int,int)> f1 = bind(fun, placeholders::_1,placeholders::_2);
cout << f1(1, 2, 3) << endl;
return 0;
}
placeholders::_1对应f1调用时的第一个参数,自动推导。依次类推_2表示第二个参数,_3表示第三个参数,如果bind_4则会报错,参数不一致。
class A
{
public:
int Add(int a, int b)
{
return a + b;
}
};
int main()
{
A ca;
function<int(int, int)> fun = bind(&A::Add, &ca, placeholders::_1, placeholders::_2);
cout << fun(1, 2) << endl;
return 0;
}
bind可以绑定一些类内函数(类内函数需要传递this指针),类外可以创造一个对象,构造函数对象时可以绑定这个对象。
12. 线程库
调用线程库必须包含< thread >头文件
- thread() 构造一个线程对象,没有关联任何线程
- thread(func, 参数1,参数2…) 构造一个线程对象,并关联函数func, 参数为传入func的参数
- get_id() 获取线程id
- joinable() 线程是否在执行(无参构造的函数对象,线程对象的状态已经转移,线程已经join或者detach结束,这三种情况线程无效)
- join() 该函数调用后立马阻塞,线程结束后,主线程继续执行
- detach() 创建线程对象后马上调用,用于把被创建线程和线程对象分离开,分离的线程为后台线程。
#include<thread>
void func(int a)
{
while (a--);
}
int main()
{
int a = 10;
thread t1(func,a);
t1.join();
cout << a << endl;
return 0;
}
在函数中对a修改,不会影响到外部实参。但可以通过地址来对其进行影响
#include<thread>
void func(int* a)
{
while ((*a)--);
}
int main()
{
int a = 10;
thread t1(func,&a); //a的地址
t1.join();
cout << a << endl;// 打印-1
return 0;
}
或者传递引用,但是必须是用std::ref()的形式,不能直接写变量名
#include<thread>
void func(int& a)
{
while (a--);
}
int main()
{
int a = 10;
thread t1(func,std::ref(a));
t1.join();
cout << a << endl;// 打印-1
return 0;
}
13. mutex
多线程场景下,如果想保证某个变量的安全性,可以通过加锁的方式来控制
但锁控制不好,可能会造成死锁,比方说在锁中间代码返回或者中间出现抛异常
#include<mutex>
int k = 0;
mutex glock;
void fun(int n)
{
for (int i = 0; i < n; i++)
{
glock.lock();
k++;
glock.unlock();
}
}
int main()
{
thread t1(fun, 100000);
thread t2(fun, 100000);
t1.join();
t2.join();
cout << k << endl;
return 0;
}
lock_guard
lock_guard类模板主要是通过RAII的方式,对其管理的互斥量进行封装。
咋子作用域内可以实例化一个lock_guard,自动上锁,出了作用域对象自动销毁并解锁。
#include<mutex>
int k = 0;
mutex glock;
void fun(int n)
{
for (int i = 0; i < n; i++)
{
lock_guard<mutex> lg(glock); //创建lock_gard对象
k++;
}
}
int main()
{
thread t1(fun, 100000);
thread t2(fun, 100000);
t1.join();
t2.join();
cout << k << endl;
return 0;
}
lock_guard大致逻辑如下
template<class Mutex>
class LockGuard
{
public:
explicit LockGuard(Mutex& mtx)
:_mtx(mtx)
{
_mtx.lock(); //构造函数直接上锁
}
~LockGuard()
{
_mtx.unlock(); //析构函数自动释放
}
LockGuard(const LockGuard& lg) = delete;
LockGuard& operator=(const LockGuard& lg) = delete;
private:
Mutex& _mtx;
};
unique_lock
unique_lock在lock_guard的基础上,添加了很多成员函数
上锁操作:lock、try_lock、try_lock_for、try_lock_unitl和unlock
- try_lock:和lock不同,它不会阻塞在当前位置,被调用时没有获得锁(锁被其他线程持有)直接返回 -1
- try_lock_for:接受一个时间范围,在这一段时间内线程等待锁的时候为阻塞状态,超时返回false
- try_lock_until:接受一个时间点作为参数,到指定时间点这段期间,线程等待锁时是阻塞状态,超时返回false
修改操作:移动赋值,交换,释放
获取属性:owns_lock(返回当前对象是否上了锁),mutex(返回当前管理的互斥量指针)
#include<windows.h>
#include<mutex>
#include<condition_variable> //条件变量
int k = 0;
mutex lock1;
condition_variable c;
bool flag = true;
void fun1(int n)
{
for (int i = 0; i < n; i += 2)
{
unique_lock<mutex> ul(lock1);
c.wait(ul, [&]()->bool {return flag; });
cout << i << endl;
flag = false;
c.notify_one();
Sleep(1000);
}
}
void fun2(int n)
{
for (int i = 1; i < n; i += 2)
{
unique_lock<mutex> ul(lock1);
c.wait(ul, [&]()->bool {return !flag; });
cout << i << endl;
flag = true;
c.notify_one();
Sleep(1000);
}
}
int main()
{
thread t1(fun1, 100000);
thread t2(fun2, 100000);
t1.join();
t2.join();
return 0;
}
交替两个线程一个打印偶数一个打印奇数
14. atomic
在多线程场景下,对共享数据进行修改时需要通过加锁来防止数据错乱,锁控制不好容易死锁
C++11引入原子操作,使得线程间数据同步变的十分高效
#include<atomic>
atomic_long k = 0;
void fun(int n)
{
for (int i = 0; i < n; i++)
{
k++;
}
}
int main()
{
thread t1(fun,100000);
thread t2(fun,100000);
t1.join();
t2.join();
cout << k << endl; // k = 200000
return 0;
}
不需要对原子类型进行加锁解锁操作,线程能够对原子类型变量互斥访问
也可使用 atomic t 定义出需要的任意原子类型
atomic模版类中的拷贝构造、移动构造、赋值运算符重载默认是删除的
15. 可变参数模版
可变参数模版能够创建可以接受可变参数的函数模版和类模板
template<class ...Args>
void showlist(Args... args){}
如何从中获取参数有两种方法
递归函数方式展开参数包
//递归终止函数
template<class T>
void showlist(const T& t)
{
cout << t << endl;
}
//展开函数
template<class T, class ...Args>
void showlist(T value, Args... args)
{
cout << value << " ";
showlist(args...);
}
int main()
{
showlist(1);
showlist(1,2);
showlist(1,2,3);
return 0;
}
与普通递归不同的是,递归结束并不是定义在递归函数里面,而是额外重载一个单参数的同名函数。
逗号表达式展开参数包
template<class T>
void PrintArgs(T t)
{
cout << t << " ";
}
template<class ...Args>
void showlist(Args... args)
{
int arr[] = { (PrintArgs(args),0)... };
cout << endl;
}
int main()
{
showlist(1, 2, 3, 4, 5);
return 0;
}
原理:
逗号表达式(a,b)会执行a部分最后返回b部分。初始化列表能够将(PrintArgs(args),0)展开成(PrintArgs(args1),0),(PrintArgs(args2),0),(PrintArgs(args3),0)…
- 创造数组纯粹是为了展开可变参数
- 通过逗号表达式执行函数
STL容器emplace系列接口
emplace系列接口支持可变参数,并且万能引用
emplace直接拿参数自己去创建对象,像push_back和insert这些函数是先构造再拷贝
int main()
{
vector<pair<int, string>> v;
v.emplace_back(1, "hh"); //直接把参数传进去创建对象
v.push_back(make_pair(2,"haha")); //在外部构造对象再传进去
return 0;
}