C++11标准对C++核心语言进行了扩充,引入了很多有用的特性,在很大程度上方便了用户的使用。
C++11扩大了使用大括号初始化的适用范围,使大括号括起来的初始化列表可以初始化所有内置类型和用户自定义类型,而且在使用时,可以加"="也可以不加。
// 内置类型的初始化
int x1 = { 42 };
int x2{ 42 };
// 动态数组的初始化
int* arr = new int[3]{ 5,6,7 };
// 容器的初始化
vector<char> v = { 'a','*','b' };
list<char> l{ 'c','d' };
map<int, char> m{ {1,'x'},{42,'*'} };
// 自定义类型的列表初始化
class People
{
public:
People(int age,string sex):_age(age),_sex(sex)
{ }
private:
int _age;
string _sex;
};
People p{ 20,"男" };
C++11中可以使用auto关键字来自动推导变量类型,给使用带来了很多方便。
// 定义一个map容器并初始化其内容
std::map<std::string, std::string> dict{ {"sort", "排序"}, {"search","搜索"} };
// 使用迭代器遍历容器, 迭代器类型太繁琐
//std::map::iterator it = dict.begin();
// 使用auto自动推导迭代器的类型,书写起来就会很方便
auto it = dict.begin();
while (it != dict.end())
{
cout << it->first << " " << it->second << endl;
++it;
}
范围for循环用来遍历容器,其底层由迭代器支持,导致迭代器失效的操作都可能会导致范围for循环出错。
// 定义一个int类型的vector容器并初始化其内容
vector<int> v = { 1,2,3,4,5,6,7,8,9,0 };
// 使用范围for循环遍历打印
for (auto e : v)
{
cout << e << " ";
}
// 取容器中的元素依次赋值给e,因此对e进行修改不会改变容器中的元素
for (auto e : v)
{
e *= 2;
}
// 当拷贝代价大的对象时,尽量使用&
vector<string> vs = { "sort","search","find","swap" };
for (const auto& e : vs)
{
cout << e << " ";
}
final:修饰类时作用是定义最终类,使其不能再被继承;修饰成员函数时作用是使其不能被子类重写。
override:写在子类虚函数后面,修饰子类虚函数,检查其是否对基类的虚函数进行了重写,未重写就报错。
class Animal
{
public:
virtual void Speak() final // 动物类的Speak函数被final修饰,Cat类将无法对其进行重写
{
cout << "动物在说话" << endl;
}
virtual void Eat()
{
cout << "动物吃食物" << endl;
}
};
class Cat final:public Animal // Cat类被final修饰,OrangeCat类将无法继承Cat类
{
public:
/*void Speak() //编译错误,无法被重写
{
cout << "小猫在说话" << endl;
}*/
void Eat() override
{
cout << "小猫吃猫粮" << endl;
}
};
//class OrangeCat:public Cat // 编译错误,无法从Cat类继承,因为它已被声明为"final"
//{
//public:
//};
关于智能指针请移步到我的另一篇博客:智能指针
C++11中新增了6个容器,静态数组array和单链表forward_list的使用场景并不多,unordered系列使用场景较为广泛,可分为set和map两种,而set和map又分别可以分为允许冗余的multi版本和不允许冗余的普通版本,其底层都是由哈希桶实现,使用方法比较类似,具体使用可参考下面的文档。
静态数组array:array使用文档
单链表forward_list:forward_list使用文档
无序集合unordered_set:unordered_set使用文档
无序映射unordered_map:unordered_map使用文档
在C++11中,可以在默认函数定义或者声明时加上=default,从而显式的指示编译器生成该函数的默认版
本,用=default修饰的函数称为显式缺省函数。
class A
{
public:
A(int a): _a(a)
{}
// 显式缺省构造函数,由编译器生成
A() = default;
// 在类中声明,在类外定义时让编译器生成默认赋值运算符重载
A& operator=(const A& a);
private:
int _a;
};
A& A::operator=(const A& a) = default;
在C++98中,如果想要限制某些成员函数的生成,可将该函数设置成private,并且不给定义。在C++11中更简单,只需在该函数声明加上=delete即可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。
class Student
{
public:
Student(string name,int age):_name(name),_age(age)
{ }
// 禁止编译器生成默认的拷贝构造函数
Student(const Student&) = delete;
// 禁止编译器生成默认的等号赋值运算符重载
Student& operator=(const Student&) = delete;
private:
string _name;
int _age;
};
什么是左值?什么是右值?
// 在下列代码中,当a表示的意义不同时,它的含义也不同
int a = 42; // a为左值,42为右值 a是一个int类型的对象(C语言称为常量),42是常量
char c = a; // c为左值,a为右值 c是一个char类型的对象,a是值为42的常量
移动构造和移动赋值:
// 假设有这样一个场景:我们需要使用str1创建str2
string create_string()
{
// 创建一个string类型的对象str1
string str1 = "hello";
// 调用string的拷贝构造函数创建一个str2
string str2 = str1;
return str2;
}
这里实际是先把str1中的内容复制出来,再复制到str2中,花销较大;我们最终需要的是str2,出了这个函数体后str1会自动销毁;我们不想让str拷贝一份再拷贝到str2里,因此可以使用C++11新增的移动构造和移动赋值来完成:
string create_string()
{
// 创建一个string类型的对象str1
string str1 = "hello";
// 创建一个空的str2,将str2和str1的内容进行交换,这样就可以节省拷贝的开销
string str2;
str2.swap(str1);
return str2;
}
什么是左值引用?什么是右值引用?
// 左值引用
// 1.做参数
swap(T& a, T& b); // 输出型参数
vector<int>& func(const vector<int> & v); // 提高传递效率
// 2.做返回值
T& operator[](size_t index); // 需要修改返回的对象
vector<T>& operator=(const vector<T> & v); // 提高传递效率
// 右值引用
string str2 = move(str1); // 等效于string str2 = (string &&) str1; 相当于把str1强转为了string&&类型的无名对象
// 当使用了move后,编译器会对str1进行区分,若str1为左值会调用拷贝构造函数,若str1为右值则会调用移动构造函数
Lambda 表达式(lambda expression)是一个匿名函数,其书写格式为:
[capture list] (parameter list) -> return type { function body }
// 最简单的lambda表达式,没有任何意义
[] {};
// 交换a和b的值,因为lambda函数是一个const函数,送一需要使用mutable取消其常量性
// 使用mutable修饰符时,参数列表不可省略(即使参数为空)
// 捕捉列表可由多个捕捉项组成,并以逗号分隔,但不允许变量重复传递
// 其实这段代码并不能实现a和b的值的交换,因为是传值调用的
int a = 3, b = 4;
[a, b]()mutable {int tmp = a; a = b; b = tmp; };
// 交换a,b的lambda表达式的正确写法---以传引用的方式捕捉a,b
[&a, &b] {int tmp = a; a = b; b = tmp; };
// 也可以不捕捉,直接传参数
[](int& a, int& b) {int tmp = a; a = b; b = tmp; };
// 也可以用引用传递捕捉所有变量
[&] {int tmp = a; a = b; b = tmp; };
因为匿名函数无法直接调用,调用时一般借助auto将其赋值给一个变量
int a = 3, b = 4;
// 借助auto将其赋值给一个变量
auto lambda_swap1 = [&a, &b] {int tmp = a; a = b; b = tmp; };
auto lambda_swap2 = [](int& a, int& b) {int tmp = a; a = b; b = tmp; };
auto lambda_swap3 = [&] {int tmp = a; a = b; b = tmp; };
// 调用上面三个变量
lambda_swap1();
lambda_swap2(a, b); //lambda_swap2有参数列表
lambda_swap3();
仿函数与lambda表达式使用方式一样,作用也相同;实际编译器在处理lambda表达式时就是将其转换成了一个lambda+uuid字符串的仿函数的类
C++11中新增了线程库,使得C++在并行编程时不需要依赖第三方库,而且在原子操作中还引入了原子类的概念。要使用标准库中的线程,必须包含< thread >头文件。
C++11中的线程类:C++11线程类文档
函数名 | 功能 |
---|---|
thread() | 构造一个线程对象,没有关联任何线程函数,即没有启动任何线程 |
thread(fn,args1, args2,…) | 构造一个线程对象,并关联线程函数fn,args1,args2,…为线程函数的参数 |
get_id() | 获取线程id |
jionable() | 线程是否还在执行,joinable代表的是一个正在执行中的线程。 |
jion() | 该函数调用后会阻塞住线程,当该线程结束后,主线程继续执行 |
detach() | 在创建线程对象后马上调用,用于把被创建线程与线程对象分离开,分离的线程变为后台线程,创建的线程的"死活"就与主线程无关 |
创建线程的几种方法:
void f1(int n)
{
for (int i = 0; i < n; ++i)
{
cout << this_thread::get_id() << ":" << i << endl;
}
}
struct F2
{
void operator()(int n)
{
for (int i = 0; i < n; ++i)
{
cout << this_thread::get_id() << ":" << i << endl;
}
}
};
int main()
{
thread t1;
thread t2(f1, 10); // 函数指针的方式
F2 f2;
thread t3(f2, 10); // 仿函数
thread t4(F2(), 10); // 仿函数匿名对象
// lambda表达式--参数列表
thread t5([](int n)->void
{
for (int i = 0; i < n; ++i)
{
cout << this_thread::get_id() << ":" << i << endl;
}
}, 10);
//t1.join(); // t1只是创建了这个对象,没有执行什么操作,因此不能join;若要join可先给t1移动赋值
t2.join();
t3.join();
t4.join();
t5.join();
return 0;
}
创建多个线程执行同一段代码的场景:
void f1(int n)
{
for (int i = 0; i < n; ++i)
{
cout << this_thread::get_id() << ":" << i << endl;
}
}
int main()
{
vector<thread> vt;
int n = 8;
vt.resize(n); // 创建8个线程
for (int i = 0; i < n; ++i)
{
vt[i] = thread(f1, 4); // 每个线程执行4次
}
for (auto& e : vt)
{
e.join();
}
return 0;
}
此外,多线程并发一定会涉及到锁的问题,将上述代码的执行部分加锁后就可以解决多线程并发的问题:
mutex _mtx; // 定义互斥锁
void f1(int n)
{
_mtx.lock(); // 加锁
for (int i = 0; i < n; ++i)
{
cout << this_thread::get_id() << ":" << i << endl;
}
_mtx.unlock(); // 解锁
}
上面这段代码使用了互斥锁,此外还有一些有关锁的文章,推荐阅读:
原子操作
惊群效应
使用互斥锁时遇到频繁的线程的上下文切换的场景:
mutex _mtx; // 定义互斥锁
int x;
void f1(int n)
{
// 锁的粒度大,串行执行,速度较快
_mtx.lock(); // 加锁
for (int i = 0; i < n; ++i)
{
++x; // 汇编代码中的++并不是原子操作
}
_mtx.unlock(); // 解锁
// 锁的粒度小,并行执行,速度反而变慢了不少
for (int i = 0; i < n; ++i)
{
_mtx.lock(); // 加锁
++x; // 汇编代码中的++并不是原子操作
_mtx.unlock(); // 解锁
}
}
int main()
{
vector<thread> vt;
int n = 8;
vt.resize(n); // 创建8个线程
for (int i = 0; i < n; ++i)
{
vt[i] = thread(f1, 1000000); // 每个线程执行1000000次
}
for (auto& e : vt)
{
e.join();
}
cout << x << endl;
return 0;
}
在分别对上面f1函数中的两段代码执行并输出x时,可以明显感觉到串行执行的代码速度较快,并行执行,速度反而变慢了不少。这是因为多个线程间不断在休眠状态和唤醒状态间切换,会消耗大量的时间。因此,当执行的语句较简单时,可以使用自旋锁来规避频繁的线程上下文切换:互斥锁和自旋锁的区别
此外,还可以使用C++11提供的atomic原子库来不加锁实现原子操作:
atomic<int> x;
void f1(int n)
{
for (int i = 0; i < n; ++i)
{
++x; // 汇编代码中的++并不是原子操作
}
}
int main()
{
vector<thread> vt;
int n = 8;
vt.resize(n); // 创建8个线程
for (int i = 0; i < n; ++i)
{
vt[i] = thread(f1, 1000000); // 每个线程执行1000000次
}
for (auto& e : vt)
{
e.join();
}
cout << x << endl;
return 0;
}