什么是C++11

C++11标准是C++编程语言的第三个官方标准,正式名叫ISO/IEC 14882:2011 - Information technology – Programming languages – C++。在真是标准发布前,原名C++0x。它取代C++标准第二版ISO/IEC 14882:2003 - Programming languages – C++成为C++语言新标准

C++11是目前对C++语言的扩展和修正,C++11不仅包含核心语言的新机能,而且扩展了C++的标准程序库(STL),并引入了大部分的C++ Technical Report 1(TR1)程序库(数学的特殊函数除外)

C++11包含大量的新特性:包括lambda表达式,类型推到关键字auto、decltype和模版的大量改进

类型推导

auto

auto的自动类型推导用于从初始化表达式中推断出变量的数据类型。从这个意义上讲,auto并非一种”类型”声明,而是一个类型声明时的”占位符”,编译器在编译时期会将auto替换为变量实际的类型

#include <iostream>
#include <vector>

struct Test {
int a;
};

double func1() {
return 1.1;
}

void func2(std::vector<int> &tmp) {
for (auto i = tmp.begin(); i != tmp.end(); i++) {
// ...
}
}

int main() {
auto b = 1; // b的类型就是int
std::cout << "b = " << b << std::endl;

auto c = func1(); // c的类型就是double
std::cout << "c = " << c << std::endl;

Test d = {0};
auto e = d; // e的类型就是d

return 0;
}

auto的易错点:

  • 定义变量时,必须初始化
  • 函数形参在一些编译器中不可以是auto
  • auto变量不能作为自定义类型的成员变量
  • auto不可以作为数组的类型
  • 模版实例化类型不能是auto

decltype

decltype实际上有点像auto的反函数,auto可以让你声明一个变量,而decltype则可以从一个变量或表达式中得到其类型

#include <iostream>
#include <typeinfo> // typeid
#include <vector>

int main() {
int i;
decltype(i) j;
std::cout << typeid(j).name() << std::endl;

float a;
double b;
decltype(a + b) c;
std::cout << typeid(c).name() << std::endl;

std::vector<int> tmp;
decltype(tmp.begin()) k;
for (k = tmp.begin(); k != tmp.end(); k++) {
// ...
}

enum {
OK, Error
} flag; // 匿名类型枚举变量
decltype(flag) flag2;

return 0;
}

追踪返回类型

返回类型后置,在函数名和参数列表后面指定返回类型

#include <iostream>

int func1(int a, int b) {
return a + b;
}

auto func2(int a, int b) -> int {
return a + b;
}

auto func3(int a, double b) -> decltype(a + b) {
return a + b;
}

template<class T1, class T2>
auto mul(const T1 &t1, const T2 &t2) -> decltype(t1 * t2) {
return t1 * t2;
}

int main() {
int a = 10;
double b = 11.1;

auto c = func3(a, b);
std::cout << "c = " << c << std::endl;

auto d = mul(a, b);
std::cout << "d = " << d << std::endl;

return 0;
}

易用性改进

初始化

类内成员初始化

#include <iostream>
#include <string>

class A {
public:
A(int i) : a(i) { // 参数列表初始化
// a = i;
}

int a;
};

class B {
public:
int data1{1};
int data2 = 1;

A tmp{10};
std::string name{"santidadday"};
};

int main() {
B obj;
std::cout << obj.data1 << std::endl;
std::cout << obj.data2 << std::endl;
std::cout << obj.tmp.a << std::endl;
std::cout << obj.name << std::endl;

}

列表初始化

struct Test {
int a;
int b;
char name[50];
};

int main() {
struct Test tmp = {1, 2, "santidadday"};

int a = 1;
int b = {1}; // 列表初始化
int c{2};

int arr1[] = {1, 2, 3};
int arr2[]{1, 2, 3};

return 0;
}

基于范围的for循环

在C++中for循环可以使用基于范围的for循环

#include <iostream>

int main() {
int a[5] = {1, 2, 3, 4, 5};
for (int &e: a) {
e *= 2;
}

for (int &e: a) {
std::cout << e << " ";
}

std::cout << std::endl;

return 0;
}

使用基于范围的for循环,其for循环迭代的范围必须是可确定的

静态断言

C/C++提供了调试工具assert,这是一个宏,用于在运行阶段对断言进行检查。如果条件为真,执行程序;否则调用abort()

#include <iostream>
#include <cassert>

int main() {
bool flag = false;

// 如果条件为真, 程序正常执行; 如果条件为假, 终止程序提示错误
assert(flag = true); // include <cassert>
std::cout << "Hello World!" << std::endl;

return 0;
}

C++11增加了关键字static_assert,可用于编译阶段对断言进行测试

静态断言的好处:

  • 更早的报告错误,我们知道构建是早于运行的,更早的错误报告意味着开发成本的降低
  • 减少运行时开销,静态断言是编译期检测的,减少了运行时开销

static_assert(常量表达式, 提示字符串)

注意:只能是常量表达式,不能是变量

#include <iostream>

int main() {
// 该static_assert用来确保编译仅在32位的平台上进行,不支持64位平台
static_assert(sizeof(void *) == 4, "64-bit code generation is not supported.");
std::cout << "Hello World!" << std::endl;

return 0;
}

noexcept修饰符

void func3() throw(int, char) {    // 只能够抛出int和char类型的异常
// C++11已经弃用了这个声明
throw 0;
}

void BlockThrow() throw() { // 代表此函数不能抛出异常, 如果抛出就会异常
throw 1;
}

// 代表此函数不能抛出异常, 如果抛出就会异常
// C++11使用noexcept代替了throw()
void BlockThrowPro() noexcept {
throw 2;
}

nullptr

nullptr是为了解决原来C++中NULL的二义性问题而引进的一种新的类型。因为NULL实际上代表的是0

#include <iostream>

void func(int a) {
std::cout << __LINE__ << "a = " << a << std::endl;
}

void func(int *p) {
std::cout << __LINE__ << "p = " << p << std::endl;
}

int main() {
int *p1 = nullptr;
int *p2 = NULL;

if (p1 == p2) {
std::cout << "equal" << std::endl;
}

// int a = nullptr; // err编译失败, nullprt不能转型为int

// func(NULL);
func(nullptr);

return 0;
}

强类型枚举

C++11引入了一种新的枚举类型,即”枚举类”,又称为”强类型枚举”。声明强类型枚举非常简单,只需要在enum后面加上class或struct

enum Old{Yes, No};
enum class New1{Yes, No};
enum struct New2{Yes, No};

“传统”的C++枚举类型有一些缺点:它会在一个代码区间中抛出枚举类型成员(如果在相同的代买域中的两个枚举类型具有相同名字的枚举成员,这会导致命名冲突),它们会被隐式转换为整型,并且不可以指定枚举的底层数据类型

// 传统枚举
#include <iostream>

int main() {
enum Status {
Ok, Error
};
// enum Status2{Ok, Error};// err导致命名冲突, Status已经有成员叫Ok, Error


Status flag = Ok;
std::cout << sizeof(Ok) << std::endl;

return 0;
}
// 强类型枚举
#include <iostream>

int main() {
enum class Status {
Ok, Error
};
enum struct Status2 {
Ok, Error
};

// 必须添加枚举作用域
Status flag = Status::Ok;
std::cout << sizeof(Status::Ok) << std::endl;

// 强类型枚举可以指定成员变量的类型
enum struct Status3 : char {
Ok, Error
};
std::cout << sizeof(Status3::Ok) << std::endl;

return 0;
}

常量表达式

常量表达式主要是允许一些计算发生在编译时,即发生在代码编译而不是运行的时候

使用constexpr可以创建一个编译时的函数

constexpr int GetConst() {
return 3;
}

int main() {
int arr[GetConst()] = {0};
enum {
e1 = GetConst(), e2
};

constexpr int num = GetConst();

return 0;
}

constexpr函数的限制:

  • 函数中只能有一个return语句(有极少特例)
  • 函数必须返回值(不能是void函数)
  • 在使用前必须已有定义
  • return返回语句表达式不能使用非常量表达式函数、全局数据,且必须是一个常量表达式

常量表达式的构造函数有以下限制:

  • 函数体必须为空
  • 初始化列表只能由常量表达式来赋值

用户定义字面量

用户定义字面值或叫”自定义后缀”更加直观,主要作用是简化代码的读写

#include <iostream>

long double operator "" _mm(long double x) {
return x / 1000;
}

long double operator "" _m(long double x) {
return x;
}

long double operator "" _km(long double x) {
return x * 1000;
}

int main() {
std::cout << 1.0_mm << std::endl;
std::cout << 1.0_m << std::endl;
std::cout << 1.0_km << std::endl;

return 0;
}

// 根据C++11标准只有下面参数列表是合法的
char const *
unsigned long long
long double
char const *, size_t
wchar_t const *, size_t
char16_t const *, size_t
char32_t const *, size_t

最后四个对于字符串相当有用,因为第二个参数会自动推断字符串的长度

#include <iostream>

size_t operator "" _len(char const *str, size_t size) {
return size;
}

int main() {
std::cout << "santidadday"_len << std::endl;

return 0;
}

原生字符串字面值

原生字符串字面值(raw string literal)使用户书写的字符串”所见即所得”。C++11中原生字符串的声明相当简单,只需在字符串前加入前缀,即字母R,并在引号中使用括号左右标识,就可以声明该字符串字面量为原生字符串

#include <iostream>

int main() {
std::cout << R"(hello,\n
world)" << std::endl;
return 0;
}

类的改进

继承构造

C++11允许派生类继承基类的构造函数(默认构造函数、赋值构造函数、移动构造函数除外)

#include <iostream>

class A {
public:
A(int i) {
std::cout << "i = " << i << std::endl;
}

A(double d, int i) {

}

A(float f, int i, coust char *c) {

}
};

class B: public A {
public:
using A::A; // 继承构造函数
// ...
virtual void ExtraInterface() {

}
};

注意:

  • 继承的构造函数只能初始化基类中的成员变量,不能初始化派生类的成员变量
  • 如果基类的构造函数被声明为私有,或派生类是从基类中虚继承,那么不能继承构造函数
  • 一旦使用继承构造函数,编译器不会再为派生类生成默认构造函数

委托构造

和继承构造函数类似,委托构造函数也是C++11中对C++的构造函数的一项改进,其目的也是为了减少程序员书写构造函数的时间

如果一个类包含多个构造函数,C++11允许在一个构造函数中的定义中使用另一个构造函数,但这必须通过初始化列表进行操作

#include <iostream>

class Info {
public:
Info() : Info(1) {
// 委托构造函数
}

Info(int i) : Info(i, 'a') {
// 既是目标构造函数, 也是委托构造函数
}

Info(char e) : Info(1, e) {

}

private:
Info(int i, char e) : type(i), name(e) {
// 目标构造函数
}

int type;
char name;

// ...
};

继承控制:final和override

C++11之前一直没有继承控制关键字,禁用一个类的进步衍生比较麻烦

C++11添加了两个继承控制关键字:final和override

  • final阻止类的进一步派生和虚函数的进一步重写
  • override确保在派生类中声明的函数跟基类的虚函数有相同的签名
#include <iostream>

class B1 final {
}; // 此类不能被继承
// class D1: public B1 {}; // error!

class B {
public:
// virtual void func() override // error! 指定了重写但实际并没重写,没有基类
// {
// std::cout << __func__ << std::endl;
// }
virtual void f() const {
std::cout << __func__ << std::endl;
}

virtual void fun() {
std::cout << __func__ << std::endl;
}
};

class D : public B {
public:
virtual void f(int) // ok! 隐藏,由于没有重写同名函数B::f,在D中变为不可见
{
std::cout << "hiding: " << __func__ << std::endl;
}

// virtual void f() override // error! 指定了重写但实际并没重写,类型声明不完全相同
// {
// std::cout << __func__ << std::endl;
// }
virtual void fun() override final // ok! 指定了重写实际上也重写了,同时,指定为最终,后代类中不能再重写此虚函数
{
std::cout << __func__ << std::endl;
}
};

class D2 : public D {
public:
virtual void f() const // ok! 重写B::f(),同时,由于没有重写D::f(int),在D2中变不可见
{
std::cout << __func__ << std::endl;
}
// virtual void fun() // error! 基类的此虚函数被指定为最终,不能被重写,虽然没有显示指定"override"
// {
// std::cout << __func__ << std::endl;
// }
// virtual void fun() override // error! 基类的此虚函数被指定为最终,不能被重写
// {
// std::cout << __func__ << std::endl;
// }
};

类默认函数的控制:default和delete函数

default函数

C++的类有四类特殊成员函数,它们分别是:默认构造函数、析构函数、拷贝构造函数以及拷贝赋值运算符。这些类的特殊成员函数负责创建、初始化、销毁,或者拷贝类的对象。如果程序员没有显式地为一个类定义某个特殊成员函数,而又需要用到该特殊成员函数时,则编译器会隐式的为这个类生成一个默认的特殊成员函数

但是,如果程序员为类显式的自定义了非默认构造函数,编译器将不再会为它隐式地生成默认无参构造函数

class X {
public:
X() = default; // 让编译器提供一个默认的构造函数, 效率比用户写的高
X() {} // 手动定义默认构造函数

X(int i) {
a = i;
}

private:
int a;
};

int main() {
X obj; //必须手动定义默认构造函数X(){} 才能编译通过

return 0;
}

delete函数

为了能够让程序员显式的禁用某个函数,C++11标准引入了一个新特性:delete函数。程序员只需要在函数声明后加上 = delete就可以禁用该函数

class X {
public:
X();

X(const X &) = delete; // 声明拷贝构造函数为 deleted 函数
X &operator=(const X &) = delete; // 声明拷贝赋值操作符为 deleted 函数

void *operator new(size_t) = delete;
void *operator new[](size_t) = delete;
};

int main() {
X obj1;
X obj2 = obj1; // 错误,拷贝构造函数被禁用

X obj3;
obj3 = obj1; // 错误,拷贝赋值操作符被禁用

X *pa = new X; // 错误,new 操作符被禁用
X *pb = new X[10]; // 错误,new[] 操作符被禁用

return 0;
}

模板的改进

右尖括号>改进

在C++98/03的泛型编程中,模板实例化有一个很繁琐的地方,就是连续两个右尖括号(>>)会被编译解释成右移操作符,而不是模板参数表的形式,需要一个空格进行分割,以避免发生编译时的错误

template<int i>
class X {
};

template<class T>
class Y {
};

int main() {
Y<X<1> > x1; // ok, 编译成功
Y<X<2>> x2; // err, 编译失败, C++98/03

return 0;
};

在实例化模板时会出现连续两个右尖括号,同样static_cast、dynamic_cast、reinterpret_cast、const_cast表达式转换时也会遇到相同的情况。C++98标准是让程序员在>>之间填上一个空格,在C++11中,这种限制被取消了。在C++11标准中,要求编译器对模板的右尖括号做单独处理,使编译器能够正确判断出”>>”是一个右移操作符还是模板参数表的结束标记

模板的别名

#include <iostream>
#include <type_traits>

// 通过typedef给一个类型起别名,不能新建类型
typedef int int32;
// C++11方式
using my_int = int;

int main() {
// is_same判断2个类型是否一致, 如果是返回真, 否则返回假
std::cout << std::is_same<int32, my_int>::value << std::endl;
}

函数模板的默认模板参数

C++11之前类模板是支持默认的模板参数,却不支持函数模板的默认模板参数

// 普通函数的默认参数
void func(int a = 3) {

}

// 类模板支持默认的模板参数
template<class T>
class A {

};

// C++11才支持函数模板带默认的模板参数
template<class T = int>
void func2(T a) {

}

类模板的默认模板参数必须从右往左定义,数模板的默认模板参数则没有这个限定

template<class T, class T2 = int>
class A {

};

template<class T = int, class T2>
void func2(T a, T2 b) {

}

可变参数的模板

在C++11之前,类模板和函数模板只能含有固定数量的模板参数。C++11增强了模板功能,允许模板定义中包含0到任意个模板参数,这就是可变参数模板

可变参数模板和普通模板的语义是一样的,只是写法上稍有区别,声明可变参数模板时需要在typename或class后面带上省略号”…”

// 可变参数的模板函数, T叫模板参数包
template<class ... T>
void func(T... args) {
// args叫函数可变参数包
}

int main() {
func<int>(10);
func<int, int>(10, 20);
func<char, int>(10, 'a');
func<char, char *, int>('a', "abc", 10);

return 0;
}

省略号的作用有两个:

  • 声明一个参数包,这个参数包中可以包含0到任意个模板参数
  • 在模板定义的右边,可以将参数包展开成一个一个独立的参数

可变参数模板函数

可变参数模板函数的定义

#include <iostream>

template<class ... T>
void func(T ... args) {
// 可变参数模板函数
// sizeof...()
std::cout << "num = " << sizeof...(args) << std::endl;
}

int main() {
func(); // num = 0
func(1); // num = 1
func(2, 1.0); // num = 2

return 0;
}

参数包的展开

递归方式展开:

通过递归函数展开参数包,需要提供一个参数包展开的函数和一个递归终止函数

#include <iostream>

//递归终止函数
void debug() {
std::cout << "empty" << std::endl;
}

//展开函数
template<class T, class ... Args>
void debug(T first, Args ... last) {
std::cout << "parameter " << first << std::endl;
debug(last...);
}

int main() {
debug(1, 2, 3, 4);

// 运行结果:
// parameter 1
// parameter 2
// parameter 3
// parameter 4
// empty


return 0;
}

非递归方式展开:

#include <iostream>

template<class T>
void print(T arg) {
std::cout << arg << std::endl;
}

template<class ... Args>
void expand(Args ... args) {
int a[] = {(print(args), 0)...};
}

int main() {
expand(1, 2, 3, 4);

return 0;
}

expand函数的逗号表达式:(print(args), 0), 也是按照这个执行顺序,先执行print(args),再得到逗号表达式的结果0

可变参数模板类

继承方式展开参数包

可变参数模板类的展开一般需要定义2-3个类,包含类声明和特化的模板类:

  • 可变参数模板声明
  • 递归继承模板类
  • 边界条件
#include <iostream>

// 可变参数模板声明
template<class ... T>
class Car {
};

// 递归继承模板类
template<class Head, class ... Tail>
class Car<Head, Tail ...> : public Car<Tail ...> {
public:
Car() {
std::cout << "type = " << typeid(Head).name() << std::endl;
}
};

// 边界条件
template<>
class Car<> {
};

int main() {
Car<int, char *, double> bmw;

return 0;
}

模板递归和特化方式展开参数包

#include <iostream>

template<long ... nums>
struct Multiply;// 变长模板的声明

template<long first, long ... last>
struct Multiply<first, last ...> {
// 变长模板类
static const long val = first * Multiply<last...>::val;
};

template<>
struct Multiply<> {
// 边界条件
static const long val = 1;
};

int main() {
std::cout << Multiply<2, 3, 4, 5>::val << std::endl; // 120

return 0;
}

右值引用

左值引用、右值引用

左值、右值

在C语言中,我们常常会提起左值(lvalue)、右值(rvalue)这样的称呼。一个最为典型的判别方法就是,在赋值表达式中,出现在等号左边的就是“左值”,而在等号右边的,则称为“右值”

不过C++中还有一个被广泛认同的说法,那就是可以取地址的、有名字的就是左值,反之,不能取地址的、没有名字的就是右值。那么这个加法赋值表达式中,&a是允许的操作,但&(b + c)这样的操作则不会通过编译。因此a是一个左值,(b + c)是一个右值

相对于左值,右值表示字面常量、表达式、函数的非引用返回值等

左值引用、右值引用

左值引用是对一个左值进行引用的类型,右值引用则是对一个右值进行引用的类型

左值引用和右值引用都是属于引用类型。无论是声明一个左值引用还是右值引用,都必须立即进行初始化。而其原因可以理解为是引用类型本身自己并不拥有所绑定对象的内存,只是该对象的一个别名

左值引用是具名变量值的别名,而右值引用则是不具名(匿名)变量的别名

int &a = 2;       // 左值引用绑定到右值,编译失败, err
int b = 2; // 非常量左值
const int &c = b; // 常量左值引用绑定到非常量左值,编译通过, ok
const int d = 2; // 常量左值
const int &e = c; // 常量左值引用绑定到常量左值,编译通过, ok
const int &b = 2; // 常量左值引用绑定到右值,编程通过, ok

“const 类型 &”为 “万能”的引用类型,它可以接受非常量左值、常量左值、右值对其进行初始化

// 右值引用
int &&r1 = 22;
int x = 5;
int y = 8;
int &&r2 = x + y;

int c;
int &&d = c; // err, 不能把一个左值赋值给一个右值引用

移动语义

为什么需要移动语义

右值引用是用来支持转移语义的。转移语义可以将资源 ( 堆,系统对象等 ) 从一个对象转移到另一个对象,这样能够减少不必要的临时对象的创建、拷贝以及销毁,能够大幅度提高C++应用程序的性能。临时对象的维护 ( 创建和销毁 ) 对性能有严重影响

转移语义是和拷贝语义相对的,可以类比文件的剪切与拷贝,当我们将文件从一个目录拷贝到另一个目录时,速度比剪切慢很多

通过转移语义,临时对象中的资源能够转移其它的对象里

移动语义定义

在现有的C++机制中,我们可以定义拷贝构造函数和赋值函数。要实现转移语义,需要定义转移构造函数,还可以定义转移赋值操作符。对于右值的拷贝和赋值会调用转移构造函数和转移赋值操作符

如果转移构造函数和转移拷贝操作符没有定义,那么就遵循现有的机制,拷贝构造函数和赋值操作符会被调用

普通的函数和操作符也可以利用右值引用操作符实现转移语义

转移构造函数

#include <iostream>
#include <cstring>

class MyString {
public:
MyString(const char *tmp = "abc") {
// 普通构造函数
len = strlen(tmp); // 长度
str = new char[len + 1]; // 堆区申请空间
strcpy(str, tmp); // 拷贝内容

std::cout << "普通构造函数 str = " << str << std::endl;
}

MyString(const MyString &tmp) {
// 拷贝构造函数
len = tmp.len;
str = new char[len + 1];
strcpy(str, tmp.str);

std::cout << "拷贝构造函数 tmp.str = " << tmp.str << std::endl;
}

// 参数是非const的右值引用
MyString(MyString &&t) {
// 移动构造函数
str = t.str; // 拷贝地址,没有重新申请内存
len = t.len;

// 原来指针置空
t.str = nullptr;
std::cout << "移动构造函数" << std::endl;
}

MyString &operator=(const MyString &tmp) {
// 赋值运算符重载函数
if (&tmp == this) {
return *this;
}

// 先释放原来的内存
len = 0;
delete[]str;

// 重新申请内容
len = tmp.len;
str = new char[len + 1];
strcpy(str, tmp.str);

std::cout << "赋值运算符重载函数 tmp.str = " << tmp.str << std::endl;

return *this;
}

~MyString() {
// 析构函数
std::cout << "析构函数: ";
if (str != nullptr) {
std::cout << "已操作delete, str = " << str;
delete[]str;
str = nullptr;
len = 0;

}
std::cout << std::endl;
}

private:
char *str = nullptr;
int len = 0;
};

MyString func() {
// 返回普通对象,不是引用
MyString obj("santidadday");

return obj;
}

int main() {
MyString &&tmp = func(); // 右值引用接收

return 0;
}

拷贝构造函数类似,有几点需要注意:

  • 参数(右值)的符号必须是右值引用符号,即”&&”
  • 参数(右值)不可以是常量,因为我们需要修改右值
  • 参数(右值)的资源链接和标记必须修改,否则,右值的析构函数就会释放资源,转移到新对象的资源也就无效了

有了右值引用和转移语义,我们在设计和实现类时,对于需要动态申请大量资源的类,应该设计转移构造函数和转移赋值函数,以提高应用程序的效率

转移赋值函数

#include <iostream>
#include <cstring>

class MyString {
public:
MyString(const char *tmp = "abc") {
// 普通构造函数
len = strlen(tmp); // 长度
str = new char[len + 1]; // 堆区申请空间
strcpy(str, tmp); // 拷贝内容

std::cout << "普通构造函数 str = " << str << std::endl;
}

MyString(const MyString &tmp) {
// 拷贝构造函数
len = tmp.len;
str = new char[len + 1];
strcpy(str, tmp.str);

std::cout << "拷贝构造函数 tmp.str = " << tmp.str << std::endl;
}

// 参数是非const的右值引用
MyString(MyString &&t) {
// 移动构造函数
str = t.str; // 拷贝地址,没有重新申请内存
len = t.len;

// 原来指针置空
t.str = nullptr;
std::cout << "移动构造函数" << std::endl;
}

MyString &operator=(const MyString &tmp) {
// 赋值运算符重载函数
if (&tmp == this) {
return *this;
}

// 先释放原来的内存
len = 0;
delete[]str;

// 重新申请内容
len = tmp.len;
str = new char[len + 1];
strcpy(str, tmp.str);

std::cout << "赋值运算符重载函数 tmp.str = " << tmp.str << std::endl;

return *this;
}

//参数为非const的右值引用
MyString &operator=(MyString &&tmp) {
// 移动赋值函数
if(&tmp == this)
{
return *this;
}

//先释放原来的内存
len = 0;
delete []str;

//无需重新申请堆区空间
len = tmp.len;
str = tmp.str; //地址赋值
tmp.str = NULL;

std::cout << "移动赋值函数" << std::endl;

return *this;
}

~MyString() {
// 析构函数
std::cout << "析构函数: ";
if (str != nullptr) {
std::cout << "已操作delete, str = " << str;
delete[]str;
str = nullptr;
len = 0;

}
std::cout << std::endl;
}

private:
char *str = nullptr;
int len = 0;
};

MyString func() {
// 返回普通对象,不是引用
MyString obj("santidadday");

return obj;
}

int main() {
MyString tmp("abc"); // 右值引用接收
tmp = func();

return 0;
}

标准库函数std::move

既然编译器只对右值引用才能调用转移构造函数和转移赋值函数,而所有命名对象都只能是左值引用,如果已知一个命名对象不再被使用而想对它调用转移构造函数和转移赋值函数,也就是把一个左值引用当做右值引用来使用,怎么做呢?标准库提供了函数 std::move,这个函数以非常简单的方式将左值引用转换为右值引用

int a;
int &&r1 = a; // 编译失败
int &&r2 = std::move(a); // 编译通过

完美转发std::forward

完美转发适用于这样的场景:需要将一组参数原封不动的传递给另一个函数

“原封不动”不仅仅是参数的值不变,在 C++ 中,除了参数值之外,还有一下两组属性:左值/右值和const/non-const。完美转发就是在参数传递过程中,所有这些属性和参数值都不能改变,同时,而不产生额外的开销,就好像转发者不存在一样。在泛型函数中,这样的需求非常普遍

// 不使用完美转发
#include <iostream>

template<class T>
void func(const T &) {
std::cout << "const T &" << std::endl;
}

template<class T>
void func(T &) {
std::cout << "T &" << std::endl;
}

template<class T>
void forward_val(const T &tmp) {
func(tmp);
}

template<class T>
void forward_val(T &tmp) {
func(tmp);
}

int main() {
int a = 0;
const int &b = 1;

forward_val(a); // T &
forward_val(b); // const T &
forward_val(111); // const T &

return 0;
}
#include <iostream>

template<class T>
void func(const T &) {
std::cout << "const T &" << std::endl;
}

template<class T>
void func(T &) {
std::cout << "T &" << std::endl;
}

template<class T>
void forward_val(T &&tmp) {
// std::forward保存参数的左值, 右值属性
func(std::forward<T>(tmp));
}


int main() {
int a = 0;
const int &b = 1;

forward_val(a);
forward_val(b);
forward_val(111);

return 0;
}

智能指针

C++11中有unique_ptr、shared_ptr与weak_ptr等智能指针(smart pointer),定义在<memory>中。可以对动态资源进行管理,保证任何情况下,已构造的对象最终会销毁,即它的析构函数最终会被调用

unique_ptr

unique_ptr持有对对象的独有权,同一时刻只能有一个unique_ptr指向给定对象(通过禁止拷贝语义、只有移动语义来实现)

unique_ptr指针本身的生命周期:从unique_ptr指针创建时开始,直到离开作用域

离开作用域时,若其指向对象,则将其所指向的对象销毁(默认使用delete操作符,用户可指定其他操作符)

#include <iostream>
#include <memory>

class Test {
public:
~Test() {
std::cout << "析构" << std::endl;
}
};

int main() {
std::unique_ptr<int> up1(new int(11)); // 创建智能指针对象
std::cout << "*upl = " << *up1 << std::endl; // 重载了operator *()

{
// 测试作用域
std::unique_ptr<Test> up2(new Test); // 无需释放, 出作用域自动释放
up2 = nullptr; // 人为指定释放堆区空间
std::cout << "释放" << std::endl;
}

std::cout << std::endl;

return 0;
}

shared_ptr

shared_ptr允许多个该只能指针共享第”拥有”同一堆分配的内存,这通过引用计数(reference counting)实现,会记录有多少个shared_ptr共同指向一个对象,一旦最后一个这样的指针被销毁,也就是一旦某个对象的引用计数变为0,这个对象会被自动删除

#include <iostream>
#include <memory>

int main() {
std::shared_ptr<int> sp1(new int(22));
std::shared_ptr<int> sp2 = sp1;

std::cout << "count: " << sp2.use_count() << std::endl; // 打印引用计数

std::cout << "*sp1 = " << *sp1 << std::endl;
std::cout << "*sp2 = " << *sp2 << std::endl;

sp1.reset();
std::cout << "count: " << sp2.use_count() << std::endl;

std::cout << "*sp2 = " << *sp2 << std::endl;
sp1.reset();

std::cout << "count: " << sp2.use_count() << std::endl;

return 0;
}

weak_ptr

weak_ptr是为了配合shared_ptr而引入的一种智能指针来协助shared_ptr工作,它可以从一个shared_ptr或另一个weak_ptr对象构造,它的构造和析构不会引起计数的增加或减少。没有重载*和->但可以使用lock获得一个可用的shared_ptr对象

weak_ptr的使用更为复杂一点,它可以指向shared_ptr指针指向的对象内存,却并不拥有该内存,而使用weak_ptr成员lock,则可返回其指向内存的一个shared_ptr对象,且在所指向对象内存已经无效时,返回指针空值nullptr

#include <iostream>
#include <memory>

int main() {
std::shared_ptr<int> p1(new int(11));

std::shared_ptr<int> p2 = p1;

std::weak_ptr<int> wp = p1;

std::cout << "count = " << wp.use_count() << std::endl; // 2

std::shared_ptr<int> p3 = wp.lock();
std::cout << "count = " << wp.use_count() << std::endl; // 3

return 0;
}

闭包的实现

什么是闭包

闭包有很多种定义,一种说法是,闭包是带有上下文的函数。说白了,就是有状态的函数。更直接一些,是个类

一个函数,带上了一个状态,就变成了闭包。那”带上状态”又是什么意思?闭包有属于自己变量,这个变量的值是创建闭包的时候设置的,并在调用闭包的时候,可以访问这些函数

函数是代码,状态是一组变量,将代码和一组变量捆绑(bind),就形成了闭包

闭包的捆绑状态必须发生在运行时

闭包的实现

仿函数:重载operator()

#include <iostream>

class MyFunctor {
public:
MyFunctor(int tmp) : round(tmp) {

}

int operator()(int tmp) {
return tmp + round;
}

private:
int round;
};

int main() {
int round = 2;
MyFunctor f(round);
std::cout << "result = " << f(1) << std::endl;

return 0;
}

std::bind绑定器

std::function

在C++中,可调用实体主要包括:函数、函数指针、函数引用、可以隐式转换为函数指定的对象,或者实现了opetator()的对象

C++11中,新增加了一个std::function类模板,它是对C++中现有的可调用实体的一种类型安全的包裹。通过指定它的模板参数,它可以用统一的方式处理函数、函数对象、函数指针,并允许保存和延迟执行它们

#include <iostream>
#include <functional>

void func() {
// 普通全局函数
std::cout << __func__ << std::endl;
}

class Foo {
public:
static int foo_func(int a) {
// 类中静态函数
std::cout << __func__ << "(" << a << ") ->: ";
return a;
}
};

class Bar {
public:
int operator()(int a) {
// 仿函数
std::cout << __func__ << "(" << a << ") ->: ";
return a;
}
};

int main() {
// 绑定一个普通函数
std::function<void(void)> f1 = func;
f1();

// 绑定类中的静态函数
std::function<int(int)> f2 = Foo::foo_func;
std::cout << f2(111) << std::endl;

// 绑定一个仿函数
Bar obj;
f2 = obj;
std::cout << f2(222) << std::endl;

/*
运行结果:
func
foo_func(111) ->: 111
operator()(222) ->: 222
*/

return 0;
}

std::function对象最大的用处就是在实现函数回调,使用者需要注意,它不能被用来检查相等或者不相等,但是可以与NULL或者nullptr进行比较

std::bind

std::bind是这样一种机制,它可以预先把指定可调用实体的某些参数绑定到已有的变量,产生一个新的可调用实体,这种机制在回调函数的使用过程中也颇为有用

C++98中,有两个函数bind1st和bind2nd,它们分别可以用来绑定functor的第一个和第二个参数,它们都是只可以绑定一个参数,各种限制,使得bind1st和bind2nd的可用性大大降低

在C++11中,提供了std::bind,它绑定的参数的个数不受限制,绑定的具体哪些参数也不受限制,由用户指定,这个bind才是真正意义上的绑定

#include <iostream>
#include <functional>

void func(int x, int y) {
std::cout << x << " " << y << std::endl;
}

int main() {
std::bind(func, 1, 2)(); // 输出:1 2
std::bind(func, std::placeholders::_1, 2)(1); // 输出:1 2


std::bind(func, 2, std::placeholders::_1)(1); // 输出:2 1
std::bind(func, 2, std::placeholders::_2)(1, 2); // 输出:2 2
std::bind(func, std::placeholders::_1, std::placeholders::_2)(1, 2); // 输出:1 2
std::bind(func, std::placeholders::_2, std::placeholders::_1)(1, 2); // 输出:2 1

// err, 调用时没有第二个参数
// bind(func, 2, _2)(1);

return 0;
}

std::placeholders::_1是一个占位符,代表这个位置将在函数调用时,被传入的第一个参数所替代

std::bind和std::function配合使用

#include <iostream>
#include <functional>

class Test {
public:
int i = 0;

void func(int x, int y) {
std::cout << x << " " << y << std::endl;
}
};

int main() {
Test obj; // 创建对象

// 绑定成员函数
std::function<void(int, int)> f1 = std::bind(&Test::func, &obj, std::placeholders::_1, std::placeholders::_2);
f1(1, 2); // 输出:1 2

// 绑定成员变量
std::function<int &()> f2 = std::bind(&Test::i, &obj);
f2() = 123;
std::cout << obj.i << std::endl; // 结果为 123

return 0;
}

lambda表达式

lambda基础使用

lambda 表达式(lambda expression)是一个匿名函数,lambda表达式基于数学中的 λ 演算得名

C++11中的lambda表达式用于定义并创建匿名的函数对象,以简化编程工作

  • 函数对象参数:[],标识一个lambda的开始,这部分必须存在,不能省略。函数对象参数是传递给编译器自动生成的函数对象类的构造函数的。函数对象参数只能使用那些到定义lambda为止时lambda所在作用范围内可见的局部变量(包括lambda所在类的this)。函数对象参数有以下形式:
    • 空:没有使用任何函数对象参数
    • =:函数体内可以使用lambda所在作用范围内所有可见的局部变量(包括lambda所在类的this),并且是值传递方式(相当于编译器自动为我们按值传递了所有局部变量)
    • &:函数体内可以使用lambda所在作用范围内所有可见的局部变量(包括lambda所在类的this),并且是引用传递方式(相当于编译器自动为我们按引用传递了所有局部变量)
    • this:函数体内可以使用lambda所在类中成员变量
    • a:将a按值进行传递。按值进行传递时,函数体内不能修改传递进来的a的拷贝,因为默认情况下函数是const的。要修改传递进来的a的拷贝,可以添加mutable修饰符
    • &a:将a按引用进行传递
    • a,&b:将a按值传递,b按引用进行传递
    • =,&a,&b:除a和b按引用进行传递外,其他参数都按值进行传递
    • &,a,b:除a和b按值进行传递外,其他参数都按引用进行传递
  • 操作符重载函数参数:标识重载的()操作符的参数,没有参数时,这部分可以省略。参数可以通过按值(如:(a, b))和按引用(如:(&a, &b))两种方式进行传递
  • 可修改标识符:mutable声明,这部分可以省略。按值传递函数对象参数时,加上mutable修饰符后,可以修改按值传递进来的拷贝(注意是能修改拷贝,而不是值本身)
  • 错误抛出标识符:exception声明,这部分也可以省略。exception声明用于指定函数抛出的异常,如抛出整数类型的异常,可以使用throw(int)
  • 函数返回值:->返回值类型,标识函数返回值的类型,当返回值为void,或者函数体中只有一处return的地方(此时编译器可以自动推断出返回值类型)时,这部分可以省略
  • 函数体:{},标识函数的实现,这部分不能省略,但函数体可以为空

lambda与仿函数

lambda和仿函数有着相同的内涵——都可以捕获一些变量作为初始化状态,并接受参数进行运行。而事实上,仿函数是编译器实现lambda的一种方式,通过编译器都是把lambda表达式转化为一个仿函数对象。因此,在C++11中,lambda可以视为仿函数的一种等价形式

lambda类型

lambda表达式的类型在C++11中被称为”闭包类型”,每一个lambda表达式则会产生一个临时对象(右值)。因此,严格地将,lambda函数并非函数指针

不过C++11标准却允许lambda表达式向函数指针的转换,但提前是lambda函数没有捕获任何变量,且函数指针所示的函数原型,必须跟lambda函数函数有着相同的调用方式

#include <iostream>
#include <functional>

int main() {
// 使用std::function和std::bind来存储和操作lambda表达式
std::function<int(int)> f1 = [](int a) { return a; };
std::function<int()> f2 = std::bind([](int a) { return a; }, 123);
std::cout << "f1 = " << f1(123) << std::endl;
std::cout << "f2 = " << f2() << std::endl;

auto f3 = [](int x, int y) -> int { return x + y; }; // lambda表达式,没有捕获任何外部变量
typedef int (*PF1)(int x, int y); // 函数指针类型
typedef int (*PF2)(int x);

PF1 p1; // 函数指针变量
p1 = f3; // ok, lambda表达式向函数指针的转换
std::cout << "p1 = " << p1(3, 4) << std::endl;

PF2 p2;
// p2 = f3; // err, 编译失败,参数必须一致

decltype(f3) p3 = f3; // 需通过decltype获得lambda的类型
// decltype(f3) p4 = p1; // err 编译失败,函数指针无法转换为lambda

return 0;
}

线程

在C++11之前,C/C++一直是一种顺序的编程语言。顺序是指所有指令都是串行执行的,即在相同时刻,有且仅有单个CPU的程序计数器执行代码的代码段,并运行代码段中的指令。而C/C++代码也总是对应地拥有过一份操作系统赋予进程的包括堆、栈、可执行的(代码)及不可执行的(数据)在内的各种内存区域

而C++11中,一个相当大的变化就是引入了多线程的支持。这使得C/C++语言在进行线程编程时,不必依赖第三方库

线程使用

线程的创建

用std::thread穿件线程非常简单,只需要提供线程函数或函数对象即可,并且可以同时指定线程函数的参数

#include <iostream>
#include <thread>

void func1() {
std::cout << __func__ << std::endl;
}

void func2() {
std::cout << __func__ << std::endl;
}

int main() {
std::thread t1(func1);
std::thread t2(func2);

std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << __func__ << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));

}

线程还可以接受任意个数的参数

#include <iostream>
#include <thread>

void func(int a, char ch, const char *str) {
std::cout << "a = " << a << std::endl;
std::cout << "ch = " << ch << std::endl;
std::cout << "str = " << str << std::endl;
}


int main () {
std::thread t(func, 1, 'a', "santidadday");
std::this_thread::sleep_for(std::chrono::seconds(1));

return 0;
}

回收线程资源

std::thread::join等待线程结束(此函数会阻塞),并回收线程资源,如果线程函数有返回值

#include <iostream>
#include <thread>

void pause_thread(int n) {
std::this_thread::sleep_for(std::chrono::seconds(n));
std::cout << "pause of " << n << " seconds ended" << std::endl;
}

int main() {
std::cout << "Spawning 3 threads ... " << std::endl;
std::thread t1(pause_thread, 1);
std::thread t2(pause_thread, 2);
std::thread t3(pause_thread, 3);

std::cout << "Done spawning threads. Now waiting for them to join: " << std::endl;

t1.join();
t2.join();
t3.join();

std::cout << "All threads joined!" << std::endl;

return 0;
}

如果不希望线程被阻塞执行,可以调用线程的std::thread::detach,将线程和线程对象分离,让线程作为后台线程去执行。但需要注意的是,detach之后就无法在和线程发生联系了,比如detach之后就不能再通过join来等待执行完,线程何时执行完我们也无法控制

#include <iostream>
#include <thread>

void pause_thread(int n) {
std::this_thread::sleep_for(std::chrono::seconds(n));
std::cout << "pause of " << n << " seconds ended" << std::endl;
}

int main() {
std::cout << "Spawning 3 threads ... " << std::endl;
std::thread(pause_thread, 1).detach();
std::thread(pause_thread, 2).detach();
std::thread(pause_thread, 3).detach();
std::cout << "Done spawning threads." << std::endl;

std::cout << "(the main thread will now pause for seconds)" << std::endl;
pause_thread(5);

return 0;
}

获取线程ID和CPU核心数

#include <iostream>
#include <thread>

void func() {
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "func id = " << std::this_thread::get_id() << std::endl;
}

int main() {
std::thread t(func);
// 获取线程t的id
std::cout << "t.get_id = " << t.get_id() << std::endl;
// 主线程id
std::cout << "main id = " << std::this_thread::get_id() << std::endl;
// 获取CPU核心数, 失败返回0
std::cout << "cup num = " << std::thread::hardware_concurrency() << std::endl;

t.join();

return 0;
}

条件变量std::condition_variable

在生产者消费者模型中,如果我们使用双重检查锁会不断进行条件判断,大大降低了使用效率。在C++11中引入了std::condition_variable类解决效率问题,等待一个条件达成,需要和互斥量配合工作

wait()函数是std::condition_variable的一个成员函数,参数为互斥量和lambda表达式。如果lambda表达式返回值为false,那么wait()将解锁互斥量并阻塞在本行,堵塞到其他线程调用notify_one()成员函数为止;如果lambda表达式返回值为true那么wait()直接返回

#include <iostream>
#include <mutex>
#include <thread>
#include <list>

class A {
public:
void inMessage() {
for (int i = 0; i < 10000; i++) {
std::cout << "insert a num" << std::endl;

std::unique_lock<std::mutex> m(my_mutex);
mesRecvQueue.push_back(i);
my_cond.notify_one();
std::this_thread::sleep_for(std::chrono::seconds(1));
}
}

void outMessage() {
int command = 0;
while (true) {
std::unique_lock<std::mutex> m(my_mutex);

my_cond.wait(m, [this] {
if (!mesRecvQueue.empty()) {
return true;
}
return false;
});

command = mesRecvQueue.front();
mesRecvQueue.pop_front();
m.unlock(); // 智能锁提前解锁

std::cout << "out a num " << command << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
}
}

private:
std::list<int> mesRecvQueue;
std::mutex my_mutex;
std::condition_variable my_cond;
};

int main() {
A obj;

std::thread outMes(&A::outMessage, &obj);
std::thread inMes(&A::inMessage, &obj);

outMes.join();
inMes.join();

return 0;
}

notify_all()函数可以唤醒全部被该对象wait()函数阻塞线程

创建后台任务

std::async、std::future创建后台任务并返回值

std::async是个函数模板,用来启动一个异步任务,返回一个std::future对象,std::future是一个类模板

异步任务:自动创建一个线程并开始执行对应线程的入口函数

std::future对象中含有线程入口函数返回的结果(线程返回的结果),可以通过调用future对象的成员函数get()获取结果。也可以称为std::future提供了一种访问异步操作结果的机制,在线程执行完毕的时候可以获取到结果

#include <iostream>
#include <thread>
#include <future>

int func() {
std::cout << "func() start --- " << "threadid = " << std::this_thread::get_id() << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(5));
std::cout << "func() end --- " << "threadid = " << std::this_thread::get_id() << std::endl;
return 5;
}

int main() {
std::cout << "main start --- " << "threadid = " << std::this_thread::get_id() << std::endl;
std::future<int> result = std::async(func);
std::cout << "continue..." << std::endl;

std::cout << result.get() << std::endl; // 卡在此处等待func执行完毕, 拿到结果, 只能使用一次

// result.wait() 等待但无法拿到返回值
return 0;
}

可以通过额外向std::async()传递一个参数,该参数类型是std::launch类型(枚举类型)达到一些特殊目的

  • std::launch::deferred:表示线程入口函数调用被延迟到std::future的wait()或get()函数调用时才执行,没有创建新线程
  • std::launch::async:在调用async函数的时候就开始创建线程

std::packaged_task是个类模板,模板参数是各种调用对象,通过std::packaged_task来把各种调用对象包装起来,方便作为线程入口

std::promise是个类模板,能够在某线程中给它赋值,然后可以再其他线程中取出这个值

std::future_status枚举类型,可以给等待返回值时间设置超时

线程池

使用场景:服务器对每一个客户端请求都创建一个线程为客户提供服务,这样会导致程序不稳定,因此通过统一创建一堆线程进行循环利用管理的方式被称之为线程池

实现方式:程序启动时一次创建好一定数量的线程,当出现请求任务时从线程池中调用一个空闲的线程为之服务,任务结束时将线程中数据清空并重新进行利用

互斥量

为什么需要互斥量

在多任务操作系统中,同时运行的多个任务可能都需要使用同一种资源。这个过程优点类似于不同部门同时使用打印机

#include <iostream>
#include <thread>

// 打印机
void printer(const char *str) {
while(*str != '\0') {
std::cout << *str;
str++;
std::this_thread::sleep_for(std::chrono::seconds(1));
}

std::cout << std::endl;
}

// 线程一
void func1() {
const char *str = "hello";
printer(str);
}

// 线程二
void func2() {
const char *str = "world";
printer(str);
}

int main() {
std::thread t1(func1);
std::thread t2(func2);

t1.join();
t2.join();

return 0;
}
// 打印会出现顺序混乱: wheolrllod

独占互斥量std::mutex

互斥量的基本接口很相似,一般用法是通过lock()方法来阻塞线程,直到获得互斥量的所有权为止。在线程获得互斥量并完成任务之后,就必须使用unlock()来解除对互斥量的占用,lock()unlock()必须成对出现。try_lock()尝试锁定互斥量,如果成功则返回true,如果失败则返回false,它是非阻塞的

#include <iostream>
#include <thread>
#include <mutex>

std::mutex g_lock; // 全局互斥锁对象, #include<mutex>

// 打印机
void printer(const char *str) {
g_lock.lock();

while(*str != '\0') {
std::cout << *str;
str++;
std::this_thread::sleep_for(std::chrono::seconds(1));
}

std::cout << std::endl;

g_lock.unlock();
}

// 线程一
void func1() {
const char *str = "hello";
printer(str);
}

// 线程二
void func2() {
const char *str = "world";
printer(str);
}

int main() {
std::thread t1(func1);
std::thread t2(func2);

t1.join();
t2.join();

return 0;
}

使用std::lock_guard可以简化lock/unlock的写法,同时也更安全,因为lock_guard在构造时会自动锁定互斥量,而在退出作用域后进行析构时就会自动解锁,从而避免忘了unlock操作

#include <iostream>
#include <thread>
#include <mutex>

std::mutex g_lock; // 全局互斥锁对象, #include<mutex>

// 打印机
void printer(const char *str) {
std::lock_guard<std::mutex>locker(g_lock);

while(*str != '\0') {
std::cout << *str;
str++;
std::this_thread::sleep_for(std::chrono::seconds(1));
}

std::cout << std::endl;
}

// 线程一
void func1() {
const char *str = "hello";
printer(str);
}

// 线程二
void func2() {
const char *str = "world";
printer(str);
}

int main() {
std::thread t1(func1);
std::thread t2(func2);

t1.join();
t2.join();

return 0;
}

原子操作

所谓原子操作,取的就是”原子是最小的、不可分割的最小个体”的意义,它表示在多个线程访问同一个全局资源的时候,能够确保所有其他的线程都在同一时间内访问相同的资源。也就是他确保了在同一时刻只有唯一的线程对这个资源进行访问。这有点类似互斥对象对共享资源的访问的保护,但是原子操作更加接近底层,因而效率更高

#include <iostream>
#include <thread>

// 全局的结果数据
long total = 0;

// 点击函数
void func() {
for (int i = 0; i < 1000000; i++) {
// 对全局数据进行无锁访问
total += 1;
}
}

int main() {
clock_t start = clock(); // 计时开始

// 线程
std::thread t1(func);
std::thread t2(func);

t1.join();
t2.join();

clock_t end = clock(); // 计时结束
std::cout << "total = " << total << std::endl;
std::cout << "time = " << end - start << "ms" << std::endl;

return 0;
}

由于线程间对数据的竞争而导致每次运行的结果都不一样。因此,为了防止数据竞争问题,我们需要对total进行原子操作

#include <iostream>
#include <thread>
#include <mutex>

// 全局的结果数据
long total = 0;
std::mutex g_lock;

// 点击函数
void func() {
for (int i = 0; i < 1000000; i++) {
// 对全局数据进行无锁访问
g_lock.lock();
total += 1;
g_lock.unlock();
}
}

int main() {
clock_t start = clock(); // 计时开始

// 线程
std::thread t1(func);
std::thread t2(func);

t1.join();
t2.join();

clock_t end = clock(); // 计时结束
std::cout << "total = " << total << std::endl;
std::cout << "time = " << end - start << "ms" << std::endl;

return 0;
}

在新标准C++11,引入了原子的概念。如果我们在多个线程中对这些类型的共享资源进行操作,编译器将保证这些操作都是原子性的,也就是说,确保任意时刻只有一个线程对这个资源进行访问,编译器将保证多个线程访问这个共享资源的正确性。从而避免了锁的使用,提高效率

#include <iostream>
#include <thread>
#include <atomic>

// 原子数据类型
std::atomic<long> total = {0}; // 需要头文件#include<atomic>

// 点击函数
void func() {
for (int i = 0; i < 1000000; i++) {
// 对全局数据进行无锁访问
total += 1;
}
}

int main() {
clock_t start = clock(); // 计时开始

// 线程
std::thread t1(func);
std::thread t2(func);

t1.join();
t2.join();

clock_t end = clock(); // 计时结束
std::cout << "total = " << total << std::endl;
std::cout << "time = " << end - start << "ms" << std::endl;

return 0;
}

原子操作的实现跟普通数据类型相似,但是它能够在保证结果正确的前提下,提供比mutex等锁机制更好的性能