C++11新特性
1、列表初始化
1.1 内置类型列表初始化
int x1 = {10};
int x2{10};
-------------
int arr1[5]{1,2,3,4,5};
int arr2[]{1,2,3,4,5};
-----------------------
int* arr3 = new int[5]{1,2,3,4,5};
------------------------------------
vector<int> v{1,2,3,4,5};
map<int, int> m{{1,1}, {2,2,},{3,3},{4,4}};
1.2 自定义类型的列表初始化
class A
{
public:
A(int x = 0, int y = 0)
: _x(x), _y(y)
{ }
private:
int _x;
int _y;
};
2、变量类型推导
2.1 decltype运行时识别类型
int a = 10;
int b = 20;
decltype(a+b) c;
void* A(size_t size)
{
return malloc(size);
}
int main()
{
// 如果没有带参数,推导函数的类型
cout << typeid(decltype(A)).name() << endl;
// 如果带参数列表,推导的是函数返回值的类型(此处只是推演,不会执行函数)
cout << typeid(decltype(A(0))).name() << endl;
return 0;
}
3、基于for的循环
这个循环底层实际是调用了arr的迭代器。所以只要是有迭代器的容器就可以用基于for的循环(自己定义的类实现了迭代器也可以用)
int main()
{
vector<int> arr = { 1, 2, 3, 4, 5 };
for (auto& num : arr)
{
//num是不需要解引用的
cout << num << endl;
}
return 0;
}
4、final与override
4.1. final
修饰虚函数,表示该虚函数不能再被继承
class Base
{
public:
virtual void Function() final
{ }
};
4.2 override
检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错(其实就是用来提醒你要记得重写)
class Base
{
public:
virtual void Function()
{ }
};
class Derive:public Base
{
public:
virtual void Function() override
{ }
};
5、智能指针
普通的指针如果指向了一段自己申请的空间,我们很容易忘记去释放,从而导致内存泄漏,所以C++11有了智能指针。
因为一个对象出了自己的作用域是会调用析构函数的,利用这一点可以封装智能指针,它的用法和普通指针一样,在析构函数里去释放申请的空间,这就自动的释放了空间了。这也就是利用了RALL(RALL是一种利用对象生命周期来控制程序资源(如内存、文件句柄、互斥量等等))的简单技术。
5.1 auto_ptr
原理:拷贝函数和赋值运算符重载函数内部都是:新的auto_ptr指向老的auto_ptr的资源,然后给老的auto_ptr置空
缺点:auto_ptr在被其他指针拷贝的时候,原来的指针就变成空的了,后面可能会导致引用空指针
auto_ptr<A> ptr = new A;
auto_ptr<A> copy(ptr);
5.2 unique_ptr
原理:unique_ptr在类里面将拷贝函数和赋值运算符重载函数都私有化或者 = delete,这样就防止unique_ptr被拷贝了
缺点:很简单粗暴,但是不实用
unique_ptr<A> ptr = new A;
unique_ptr<A> copy(ptr);//错误
5.3 shared_ptr
原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源。当有拷贝或者赋值时,会将引用计数+1。析构的时候,会将引用计数-1,如果引用计数==0,则释放资源
优点:不会浪费空间,也不会资源泄漏,还很灵活
缺点:循环引用的问题
shared_ptr<A> ptr = new A;
shared_ptr<A> copy1(ptr);
shared_ptr<A> copy2(ptr);
shared_ptr<A> copy3(ptr);
//都不会造成内存泄漏。每一个指针都会调用析构,但是只有最后一个调用的才会释放资源
6、新容器(unordered系列)
之前的map和set底层都是红黑树,而这个新系列的容器底层都是哈希表。前者查找效率是O(logN),后者查找效率是O(1)。unordered系列的容器主要是用来给不注重查找顺序而设计的高效容器,具体结构可以看我之前的哈希桶和红黑树的博客
7、委派构造函数
委派构造函数:就是指委派函数将构造的任务委派给目标构造函数来完成的一种类构造的方式
注意:构造函数不能同时”委派”和使用初始化列表
class A{
public:
// 目标构造函数
A()
: _a(0), _b(1)
{
}
// 委派构造函数
A(int c)
: A()
{
_c = c;
}
// 委派构造函数
A(char d)
: A()
{
_d = d;
}
private:
int _a;
int _b;
int _c;
char _d;
};
};
8、自定义类的函数控制
8.1 显式缺省函数
可以在默认函数定义或者声明时加上=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;
int main()
{
A a1(10);// 调用的自己写的够实在函数
A a2;// 调用的编译器默认生成的构造函数
a2 = a1;// 调用的编译器默认生成的赋值运算符重载函数
return 0;
}
8.2 删除默认函数
在函数声明加上=delete,就可以让这个函数没法使用,所以也称为删除函数,一般用于防拷贝之类的
class A {
public:
A(int a)
: _a(a)
{ }
// 禁止编译器生成默认的拷贝构造函数以及赋值运算符重载
A(const A&) = delete;
A& operator(const A&) = delete;
private:
int _a;
};
9、右值引用
9.1 定义
右值引用,顾名思义就是对右值的引用。C++11中,右值由两个概念组成:纯右值和将亡值。
纯右值:
纯右值是C++98中右值的概念,用于识别临时变量。比如:常量、一些运算表达式(比如:1+1)等。
将亡值:
声明周期将要结束的对象。比如:在值返回时的临时对象。
9.2 格式
类型&& 引用变量名字 = 实体;
注意:
- 与引用一样,右值引用在定义时必须初始化。
- 通常情况下,右值引用不能引用左值(左值就是:可以变的值,变量;也可以说不是右值的值)。
- 左值既可以引用左值也可以引用右值(一般加const)
int main()
{
int a = 10;
int&& ra; // 编译失败,没有进行初始化
int&& ra = a; // 编译失败,a是一个左值
const int&& ra = 10;// ra是匿名常量10的别名
return 0;
}
9.3 主要作用
作用1:与移动语义(将一个对象中资源移动到另一个对象中的方式)结合,减少无必要资源的开辟来提高代码的运行效率。
String&& GetString(char* str)
{
String str_temp(str);
return str_temp;
//返回值是右值引用,则在返回时候并不会销毁这个将亡值然后再拷贝这个将亡值,然后将拷贝的给s;而是直接将这个将亡值给s
}
int main()
{
String s(GetString("world"));
return 0;
}
作用2:拷贝的时候效率更高,更省空间。
class String
{
public:
String(char* str)
{
_str = new char[strlen(str) + 1];
strcpy(_str, str);
}
String(const String& s)
{
_str = new char[strlen(s._str) + 1];
strcpy(_str, s._str);
}
//移动语义的拷贝构造
String(String&& s) // 注意!!!String(const String&&);这样是不会构成移动语义的,而会走上面的拷贝构造
{
_str = s._str;
s._str = nullptr;
}
private:
char* _str;
};
String getString(char* str)
{
String tmp(str);
return tmp;
}
int main()
{
//getString返回的是一个将亡值
//如果没有移动语义的拷贝构造,那么会调用基本的拷贝构造,需要开空间然后赋值
String copy(getString("hello"));
//如果有移动语义的拷贝构造,会自动调用移动语义的拷贝构造,就不需要自己开空间
//因为这个返回的tmp十个将亡值,我们后面根本用不到,所以就采用这种高效的方法,如果后面要用到,就千万不要用这种方法
return 0;
}
9.4 std::move()
这个函数名字具有迷惑性,它并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值引用,通过右值引用使用该值,实现移动语义。 注意:被转化的左值,其声明周期并没有随着左右值的转化而改变,即std::move转化的左值变量不会被销毁。
它的作用主要是把某些你觉得后面不会再用到的变量当作右值去处理。
int main()
{
String s1("hello world");
String s2(move(s1));//s1后面用不到了,就把s1的资源更快的给s2
return 0;
}
10、lambda表达式
10.1 lambda表达式语法
lambda表达式书写格式:[capture-list] (parameters) mutable -> return-type { statement }
10.11 lambda表达式各部分说明
[capture-list] : 捕捉列表。该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。
(parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略
mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。
->return-type:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
{statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。
注意:
10.12 捕获列表说明
捕捉列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用。
[val]:表示值传递方式捕捉变量val
[=]:表示值传递方式捕获所有父作用域中的变量(包括this)
[&val]:表示引用传递捕捉变量val
[&]:表示引用传递捕捉所有父作用域中的变量(包括this)
[this]:表示值传递方式捕捉当前的this指针
10.13用例
int main()
{
// 最简单的lambda表达式, 该lambda表达式没有任何意义
[]{};
// 省略参数列表和返回值类型,返回值类型由编译器推导为int
int a = 3;
[=]{ return a + 3; };
// [=]表示把父作用域(main函数里)的所有变量以传值的形式传进去
// 省略了返回值类型,无返回值类型
int b = 4;
auto fun1 = [&](int c){ b = a + c; };
// [&]表示把父作用域(main函数里)的所有变量以引用的形式传进去,所以这里的lambda跑完后外面b的值是改变了的
fun1(10); // fun1变成了推导出来的函数类型,这是一个:参数为int,返回值为void的函数
cout << a << " " << b << endl;
// 各部分都很完善的lambda函数
auto fun2 = [=, &b](int c)->int{ return b += a + c; };
// [=, &b]表示把父作用域(main函数里)的,除了b以外的所有变量都以传值的形式传进去,b是以传引用的方式传进去;
//->int显式的告诉我们返回值是int,不写的话编译器会自己推到返回值类型(最好写,为了代码可读性)
cout << fun2(10) << endl;
// 复制捕捉x
int x = 10;
auto add_x = [x](int a) mutable { x *= 2; return a + x; };
// 如果不适用mutable,这个表达式是会有编译错误的,因为x是以传值的方式捕捉进来的,所以是有常性的,无法更改。当然这里的a是可以改变的,这是个函数参数。(所以要注意x和a的区别)
cout << add_x(10) << endl;
return 0;
}
10.14 注意事项
- 父作用域指包含lambda函数的语句块,lambda在的{ }里
- 语法上捕捉列表可由多个捕捉项组成,并以逗号分割。
比如:[=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量; [&,a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量 - 捕捉列表不允许变量重复传递,否则就会导致编译错误。
比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复 - 在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空。因此C++11中最简单的lambda函数为:[ ]{ }; 该lambda函数不能做任何事情。
- lambda表达式之间不能相互赋值,即使看起来类型相同
int main()
{
auto f1 = []{cout << "hello world" << endl; };
auto f2 = []{cout << "hello world" << endl; };
f1 = f2; // 编译失败
// 但是允许使用一个lambda表达式拷贝构造一个新的副本
auto f3(f2);
f3(); // 执行
// 可以将lambda表达式赋值给相同类型的函数指针
void(*PF)();// 创建了一个函数指针
PF = f2;
PF(); // 执行
return 0;
}
10.15 底层
实际在底层编译器对于lambda表达式的处理方式,完全就是按照函数对象的方式处理的,即:如果定义了一个lambda表达式,编译器会自动生成一个类,在该类中重载了operator()。也就是我们平常写的仿函数,我们写仿函数的时候只重载一下operator(),并不会重载operator=(),所以两个lambda表达式之间不可以赋值
11、线程库
11.1 线程的启动
C++线程库通过构造一个线程对象来启动一个线程,该线程对象中就包含了线程运行时的上下文环境,比如:线程函数、线程栈、线程起始状态等以及线程ID等,所有操作全部封装在一起,最后在底层统一传递给_beginthreadex() 创建线程函数来实现 (注意:_beginthreadex是windows中创建线程的底层c函数)。std::thread()创建一个新的线程可以接受任意的可调用对象类型(带参数或者不带参数),包括lambda表达式(带变量捕获或者不带),函数,以及函数指针。
#include<thread>
#include<iostream>
using namespace std;
int main()
{
int n1 = 500;
int n2 = 600;
thread t([&](int addNum){ n1 += addNum; n2 += addNum; }, 500);
// 这里传入的函数是捕获父作用域所有引用且带参数的lambda表达式,第二个传的是lambda里需要传的参数
t.join();
std::cout << n1 << ' ' << n2 << std::endl;
system("pause");
return 0;
}
void Fun(int& n1, int* n2)
{
n1 += 1;
*n2 += 1;
}
int main()
{
int n1 = 500;
int n2 = 600;
thread t1(Fun, n1, &n2);
// 这个和POSIX的创建线程更方便的是可以传入0个或多个参数,而POSIX的只能传入一个参数
t1.join();
std::cout << n1 << ' ' << n2 << std::endl;// 打印结果是500 601
// 因为编译器并不会知道这个线程函数执行完毕之前,n1的生命周期是否会结束,所以只能传值或者传地址
// 所以我们在传参的时候要确保参数的生命周期
system("pause");
return 0;
}
11.2 线程的结束
join():等待
join():会主动地等待线程的终止。在调用进程中join(),当新的线程终止时,join()会清理相关的资
源,然后返回,调用线程再继续向下执行。由于join()清理了线程的相关资源,thread对象与已销毁的线程就没有关系了,因此一个线程的对象每次你只能使用一次join()。注意:主线程在join的时候,会什么都不做,直到等待子线程结束后再往下执行。
detach():分离
detach:会从调用线程中分理出新的线程,之后不能再与新线程交互。分离的线程会在后台运行,其所有权和控制权将会交给c++运行库。同时,C++运行库保证,当线程退出时,其相关资源的能够正确的回收。注意:如果主线程先与子线程运行完,是不会结束子线程的,子线程是给系统托管了,只受系统影响。但是主线程退出了就表示进程退出了,子线程是被独立出去自己执行
void Fun()
{
while (1)
{
cout << "sleep ... ..." << endl;
Sleep(3000);
cout << "要退出了" << endl;
break;
}
}
int main()
{
thread t1(Fun);
t1.detach();
//t1.join();
Sleep(1000);
cout << "主线程 动了" << endl;//打印结果:sleep ... ...主线程 动了
// 请按任意键继续. . . 要退出了
system("pause");
return 0;
}
11.3 原子性操作变量
多线程最主要的问题是共享数据带来的问题(即线程安全)。如果共享数据都是只读的,那么没问题,因为只读操作不会影响到数据,更不会涉及对数据的修改,所以所有线程都会获得同样的数据。但是,当一个或多个线程要修改共享数据时,就会产生很多潜在的麻烦。
C++98
通过mutex库里的加锁和解锁可以保证访问数据的原子性,但是效率很差,因为一个线程有锁的时候,其他线程都会被阻塞
using namespace std;
#include <thread>
#include <mutex>
std::mutex m;
int sum = 1;
void fun(size_t num)
{
for (size_t i = 0; i < num; ++i)
{
m.lock();
sum++;
m.unlock();
}
}
C++11
通过原子类型来操作数据,这样操作不需要额外的调用加锁解锁函数,提高了效率
using namespace std;
#include <thread>
#include <atomic>
atomic_int sum{ 0 }; // 其实atomic_int就是一个封装的类,sums是一个对象
// 这里初始化的时候必须要用列表初始化
void fun(size_t num) {
for (size_t i = 0; i < num; ++i)
{
sum++;// 原子操作
}
}