当前位置: 首页 > news >正文

C++11

目录

  • C++11
    • 1.列表初始化
      • 1.1{}初始化
      • 1.2initializer_list
    • 2.范围for循环
    • 3.变量类型推导
      • 3.1auto
      • 3.2decltype
    • 4.final和override
    • 5.nullptr
    • 6.新增容器
    • 7.智能指针
    • 8.右值引用和移动语义
      • 8.1左值引用和右值引用
      • 8.2左值引用和右值引用的比较
      • 8.3右值引用使用的场景和意义
      • 8.4emplace_back
      • 8.5完美转发
    • 9.lambda表达式
      • 9.1lambda表达式的由来
      • 9.2lambda表达式
      • 9.3lambda表达式的原理
    • 10.类的新特性
    • 11.可变参数模版
    • 12.包装器
      • 12.1function包装器
      • 12.2bind包装器
    • 13.线程库
      • 13.1线程库基本操作
      • 13.2线程函数参数
      • 13.3多线程操作

C++11

C++11更新了很多新的语法和玩法,这里不是全部的内容,这里只讲了一些重要的,并且在这里面,也只有

1.列表初始化

1.1{}初始化

在C++98中,标准允许使用花括号{}对数组或者结构体元素进行统一的列表初始值设定。比如:

struct Point
{int _x;int _y;
};int main()
{int array1[] = { 1, 2, 3, 4, 5 };int array2[5] = { 0 };Point p = { 1, 2 };return 0;
}

C++11扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自定义的类型,使用初始化列表时,可添加等号(=),也可不添加

struct Point
{int _x;int _y;
};
int main()
{int x1 = 1;int x2{ 2 };int array1[]{ 1, 2, 3, 4, 5 };int array2[5]{ 0 };Point p{ 1, 2 };// C++11中列表初始化也可以适用于new表达式中int* pa = new int[4]{ 0 };return 0;
}

创建对象时也可以使用列表初始化方式调用构造函数初始化

class Date
{
public:Date(int year, int month, int day):_year(year), _month(month), _day(day){cout << "Date(int year, int month, int day)" << endl;}
private:int _year;int _month;int _day;
};int main()
{Date d1(2022, 1, 1); // old style// C++11支持的列表初始化,这里会调用构造函数初始化Date d2{ 2022, 1, 2 };Date d3 = { 2022, 1, 3 };return 0;
}

这些都很简单

1.2initializer_list

initializer_list的官方文档

也可以直接将其类型打印出来:

int main()
{// the type of il is an initializer_list auto il = { 10, 20, 30 };cout << typeid(il).name() << endl;return 0;
}

initializer_list使用场景:

std::initializer_list一般是作为构造函数的参数,C++11对STL中的不少容器就增加

std::initializer_list作为参数的构造函数,这样初始化容器对象就更方便了。也可以作为operator=的参数,这样就可以用大括号赋值

例子:

int main()
{vector<int> v = { 1,2,3,4 };list<int> lt = { 1,2 };// 这里{"sort", "排序"}会先初始化构造一个pair对象map<string, string> dict = { {"sort", "排序"}, {"insert", "插入"} };// 使用大括号对容器赋值v = {10, 20, 30};return 0
}

让模拟实现的vector也支持{}初始化和赋值

template<class T>
class vector {
public:typedef T* iterator;vector(initializer_list<T> l){_start = new T[l.size()];_finish = _start + l.size();_endofstorage = _start + l.size();iterator vit = _start;typename initializer_list<T>::iterator lit = l.begin();while (lit != l.end()){*vit++ = *lit++;}//for (auto e : l)//   *vit++ = e;}vector<T>& operator=(initializer_list<T> l) {vector<T> tmp(l);std::swap(_start, tmp._start);std::swap(_finish, tmp._finish);std::swap(_endofstorage, tmp._endofstorage);return *this;}
private:iterator _start;iterator _finish;iterator _endofstorage;};

2.范围for循环

这个就很简单了,只要支持迭代器的容器就都支持范围for循环,这里就举一个简单的例子,这个范围for循环也已经使用过很多次了。

int main()
{vector<int> v = {1, 2, 3, 4, 5};for(auto n : v) //要注意,如果容器的元素拷贝的代码太大就要给&{cout << n << " ";}cout << endl;return 0;
}

3.变量类型推导

3.1auto

在C++98中auto是一个存储类型的说明符,表明变量是局部自动存储类型,但是局部域中定义局部的变量默认就是自动存储类型,所以auto就没什么价值了。C++11中废弃auto原来的用法,将其用于实现自动类型腿断。这样要求必须进行显示初始化,让编译器将定义对象的类型设置为初始化值的类型。

int main()
{int i = 10;auto p = &i; auto pf = strcpy;cout << typeid(p).name() << endl;cout << typeid(pf).name() << endl;map<string, string> dict = { {"sort", "排序"}, {"insert", "插入"} };//map<string, string>::iterator it = dict.begin();auto it = dict.begin();return 0;
}

3.2decltype

关键字decltype将变量的类型声明为表达式指定的类型。

// decltype的一些使用使用场景
template<class T1, class T2>
void F(T1 t1, T2 t2)
{decltype(t1 * t2) ret;cout << typeid(ret).name() << endl;
}int main()
{const int x = 1;double y = 2.2;decltype(x * y) ret; // ret的类型是doubledecltype(&x) p;  // p的类型是int*cout << typeid(ret).name() << endl;cout << typeid(p).name() << endl;F(1, 'a');return 0;
}

4.final和override

这两个关键字在c++多态的学习中已经详细的讲解了,忘记了就去复习

这里大概总结一下:

被final修饰的类无法被继承

被final修饰的函数无法被重写

override:检查子类虚函数是否重写了父类的某个虚函数,如果没有重写编译报错

5.nullptr

由于C++中NULL被定义成字面量0,这样就可能回带来一些问题,因为0既能指针常量,又能表示整形常量。所以出于清晰和安全的角度考虑,C++11中新增了nullptr,用于表示空指针

#ifndef NULL
#ifdef __cplusplus
#define NULL   0
#else
#define NULL   ((void *)0)
#endif
#endif

6.新增容器

重点就是unordered_set和unordered_map【效率更高。底层是哈希表】

容器中的一些新方法

如果我们再细细去看会发现基本每个容器中都增加了一些C++11的方法,但是其实很多都是用得比较少的。

比如提供了cbegin和cend方法返回const迭代器等等,但是实际意义不大,因为begin和end也是可以返回const迭代器的,这些都是属于锦上添花的操作。

实际上C++11更新后,容器中增加的新方法最后用的插入接口函数的右值引用版本

这个右值引用版本在某种情况下是能够通过移动拷贝来提高效率的,具体是怎么提高的可以看后面右值引用的学习

7.智能指针

在后面专门学习一个章节

8.右值引用和移动语义

早在c++98就提出了引用的概念,但是此时的引用可以被称作左值引用

8.1左值引用和右值引用

传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,所以从现在开始我们之前学习的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名

什么是左值?什么是左值引用?

左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址+可以对它赋值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名

什么是右值?什么是右值引用?

右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。右值引用就是对右值的引用,给右值取别名。

int main()
{// 左值一般是一个变量,或者解引用后的指针// 右值一般是一个常量,一个表达式返回值,或者一个函数返回值// 不管是左值引用还是右值引用,作用都是取别名!	int a = 10; // 10是一个右值,a是一个左值int& rp = a; // rp是一个左值引用, rp也是左值int x = 1, y = 2;int&& b = 10; //b是一个右值引用, b是左值int&& s = x + y; // s也是右值引用,s是左值, x+y是一个右值// 但是左值引用不是只能引用左值, 只要带const就可以引用右值了const int& c = 10; // 不带const就会报错// 同样的,右值引用也不是只能引用右值, 只要将左值move(移动处理)就可以引用int&& d = move(a); return 0;
}

8.2左值引用和右值引用的比较

左值引用总结:

  1. 左值引用只能引用左值,不能引用右值。

  2. 但是const左值引用既可引用左值,也可引用右值

右值引用总结:

  1. 右值引用只能右值,不能引用左值。

  2. 但是右值引用可以move以后的左值。

8.3右值引用使用的场景和意义

在c++11中,将右值分为了两种类型:

  1. 纯右值(内置类型的常量或临时对象)
  2. 将亡值(自定义类型的临时对象)

重点就是这个将亡值!可以说右值引用的使用场景就和将亡值有很大的关系

下面来举个例子来理解右值引用的使用场景和意义:

class String
{
public:String(const char* str = ""){// 深拷贝_str = new char[strlen(str) + 1]; //向堆区申请空间strcpy(_str, str);}String(const String& s){cout << "String(const String& s)深拷贝" << endl;_str = new char[strlen(s._str) + 1];strcpy(_str, s._str);}// 如果给的是右值,那么就调用右值的拷贝构造【其实这里就是移动拷贝】String(String&& s) // 传过来的一定是一个将亡值:_str(nullptr){// 由于将亡值的生命周期即将结束,因此这里不在需要深拷贝,直接浅拷贝swap(_str, s._str); //浅拷贝cout << "String(String&& s)移动拷贝\n";}~String(){delete[] _str;_str = nullptr;}private:char* _str;
};String f(const char* str)
{String tmp(str);return tmp; //这里tmp作为函数返回值,肯定是右值,并且String还是自定义类型的临时对象,说明是将亡值
}int main()
{// 左值一般是一个变量,或者解引用后的指针// 右值一般是一个常量,一个表达式返回值,或者一个函数返回值// 不管是左值引用还是右值引用,作用都是取别名!	// 下面是探究右值引用的使用场景和意义String s1("左值");String s2(s1);//这里可能会编译器优化导致f返回的不是自定义类型的临时对象,导致无法调用拷贝构造String s3(f("右值")); //看不到现象的话,理解意思即可// 因此,这里传了一个将亡值,s3就不在需要深拷贝,而是识别到将亡值这个右值,对应到为浅拷贝的拷贝构造// 这样就可以提高效率,不用每次都深拷贝的拷贝构造String s4(move(s1)); //如果s3看不到,这里一定看的到。// 这里直接强制将s1变成右值——将亡值,要注意,s1经过这次操作之后,就会在拷贝构造里被交换为空return 0;
}

执行结果如下:
image-20250422002822926

而移动赋值也是一样的,就是移动赋值运算符重载,和移动拷贝构造是同理的。都是当检测到自定义类型是临时对象,即将亡值之后,采用移动拷贝(浅拷贝),提高效率

结论:右值引用本身没有多大意义,但是右值引用实现了移动拷贝!只要涉及到深拷贝的类,都可以在类中搞一个移动拷贝,通过判断是左值还是右值,来判断是要深拷贝还是移动拷贝,从而提高效率

8.4emplace_back

template <class... Args>
void emplace_back(Args&&... args); 这里是一个可变参数模版

网上有种说法:一定要使用emplace_back,因为效率高

其实这样说是不够严谨的,要具体情况具体分析

下面是例子:

int main()
{// 有很多人说一定要使用emplace_back,因为效率高// 其实这样说是不够严谨的,要具体情况具体分析// 就拿vector类型的push_back和emplace_back来比较vector<string> v;string s1 = "左值";v.push_back("右值"); //这里的"右值"会隐式类型转化为string,这里就发生了一次构造,是临时对象,为右值// 调用的为void push_back (value_type&& val)。提升效率,移动拷贝v.push_back(s1); //这里为左值,调用的push_back为void push_back (const value_type& val);//而如果为emplace_back又是如何呢//template <class... Args>//void emplace_back(Args&&... args); 这里是一个可变参数模版v.emplace_back("右值");v.emplace_back(s1);// 尽管这两个都调用了emplace_back,但是实际上s1并没有被转移,作为一个左值,不敢随便移动拷贝// 而"右值"是右值,采取的是移动拷贝,这里会减少深拷贝,"右值"会被转移// 因此v.emplace_back(s1);并没有比v.push_back(s1);高效到哪里去//实际上emplace_back真正的价值是在于其可变参数模版上vector<pair<string, string>> vp;vp.push_back(make_pair("右值", "右值")); //调用了函数,函数返回值是右值vp.emplace_back(make_pair("右值", "右值"));vp.emplace_back("右值", "右值"); //这里不再需要手动构造一个pair,直接就有对应的模版参数帮助完成return 0;
}

8.5完美转发

其实就是正常情况下,右值引用在经过多次参数传递后,会丢失其右值的属性。导致无法正确调用应该调用的函数,这个时候就要使用完美转发

完美转发在传参的过程中保留对象原生类型属性

注意:

模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。

模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力,

但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值,

我们希望能够在传递过程中保持它的左值或者右值的属性, 就需要用我们下面学习的完美转发

例子:

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; }// std::forward<T>(t)在传参的过程中保持了t的原生类型属性。
template<typename T>
void PerfectForward(T&& t)
{Fun(std::forward<T>(t));
}
int main()
{PerfectForward(10);           // 右值int a;PerfectForward(a);            // 左值PerfectForward(std::move(a)); // 右值const int b = 8;PerfectForward(b);      // const 左值PerfectForward(std::move(b)); // const 右值return 0;
}

完美转发实际中的使用场景:

template<class T>
struct ListNode
{ListNode* _next = nullptr;ListNode* _prev = nullptr;T _data;
};template<class T>
class List
{typedef ListNode<T> Node;
public:List(){_head = new Node;_head->_next = _head;_head->_prev = _head;}void PushBack(T&& x){//Insert(_head, x);Insert(_head, std::forward<T>(x));}void PushFront(T&& x){//Insert(_head->_next, x);Insert(_head->_next, std::forward<T>(x));}void Insert(Node* pos, T&& x){Node* prev = pos->_prev;Node* newnode = new Node;newnode->_data = std::forward<T>(x); // 关键位置// prev newnode posprev->_next = newnode;newnode->_prev = prev;newnode->_next = pos;pos->_prev = newnode;}void Insert(Node* pos, const T& x){Node* prev = pos->_prev;Node* newnode = new Node;newnode->_data = x; // 关键位置// prev newnode posprev->_next = newnode;newnode->_prev = prev;newnode->_next = pos;pos->_prev = newnode;}private:Node* _head;
};int main()
{List<string> lt;lt.PushBack("1111");lt.PushFront("2222");return 0;
}

9.lambda表达式

9.1lambda表达式的由来

要学习lambda表达式,就得先知道仿函数的用途并且理解其使用场景

下面是最简单的一个仿函数的使用场景:

template<class K>
bool g(const K& x1, const K& x2)
{return x1 > x2;
}
int main()
{// 其实lambda表达式和仿函数是有关系的// 下面是仿函数最简单的使用例子int array[] = { 4,1,8,5,3,7,0,9,2,6 };// 默认按照小于比较,排出来结果是升序std::sort(array, array + sizeof(array) / sizeof(array[0]));// 如果需要降序,需要改变元素的比较规则, 这里用了一个仿函数,但是这里不一定要传仿函数,传函数进去也是可以的// 因为仿函数的本质就是一个结构体里面对()进行了重载//std::sort(array, array + sizeof(array) / sizeof(array[0]), greater<int>());std::sort(array, array + sizeof(array) / sizeof(array[0]), g<int>);return 0;
}

如果待排序元素为自定义类型,需要用户定义排序时的比较规则:

// 这个结构体代表货物
struct Goods
{string _name; // 名字double _price; // 价格int _evaluate; // 评价Goods(const char* str, double price, int evaluate):_name(str), _price(price), _evaluate(evaluate){}
};struct ComparePriceLess
{bool operator()(const Goods& gl, const Goods& gr){return gl._price < gr._price;}
};struct CompareEvaluateLess
{bool operator()(const Goods& gl, const Goods& gr){return gl._evaluate < gr._evaluate;}
};struct ComparePriceGreater
{bool operator()(const Goods& gl, const Goods& gr){return gl._price > gr._price;}
};struct CompareEvaluateGreater
{bool operator()(const Goods& gl, const Goods& gr){return gl._evaluate > gr._evaluate;}
};struct CompareNameGreater
{bool operator()(const Goods& gl, const Goods& gr){return gl._name > gr._name; //实际上对于字符串比较的逻辑不应该这么简单,但是这里只是为了理解仿函数要写很多个而已}
};int main()
{// 而之前学习的仿函数一般都是用来处理一些自定义类型的比较运算// 比如排序,对一个自定义类型进行排序,不能采取重载</>/=等运算符,因为只能比较一种情况,代码兼容性太差// 因此要用仿函数来实现自定义类型的比较运算,下面来给一个具体的例子// 就像上面的货物结构体,在对它进行排序的时候只能,挑选其中一个属性进行排序,因此对应每种情况都要写一个仿函数// 而对应上述结构体一共要写6个仿函数!vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2,3 }, { "菠萝", 1.5, 4 } };sort(v.begin(), v.end(), ComparePriceGreater());sort(v.begin(), v.end(), CompareEvaluateGreater());sort(v.begin(), v.end(), CompareNameGreater());// 这样太麻烦了,因此c++11提出了lambda表达式return 0;
}

随着C++语法的发展,人们开始觉得上面的写法太复杂了,每次为了实现一个algorithm算法,都要重新去写一个类,如果每次比较的逻辑不一样,还要去实现多个类,特别是相同类的命名,这些都给编程者带来了极大的不便。因此,在C++11语法中出现了Lambda表达式。

9.2lambda表达式

lambda表达式书写格式:[capture - list](parameters) mutable -> return-type{statement}

  • [capture-list] :捕捉队列——该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数捕捉列表能够捕捉上下文中的变量供lambda函数使用
  • (parameters):参数列表——与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略
  • **mutable:**默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。【一般来说用不到这个】
  • ->returntype:返回值类型——用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导
  • statement:函数体——在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。

下面是简单的使用lambda表达式的例子:

	// 对于lambda表达式,可以这样理解,一个被定义在函数内部的匿名函数// lambda表达式的定义方式和正常函数不太一样,规则如下:【如果看不懂就看博客】// [capture - list](parameters) mutable ->return-type{ statement}[] {}; //这就是最简单lambda表达式int a = 1, b = 2;// 实现功能为a+b的lambda表达式auto add1 = [](int x1, int x2)->int {return x1 + x2; }; //这是不捕捉的定义方式add1(a, b); //这样用起来和函数就很像了auto add2 = [a, b]()->int {return a + b; }; //直接捕捉a和b//auto add2 = [=]()->int {return a + b; }; //全部捕捉,不推荐add2(); //这个函数就不用传参了,也没有定义形参// 实现功能为swap的lambda表达式// 不捕捉auto swap1 = [](int& x1, int& x2){ //这里函数体内没有返回值,这个->return-type可以不用写int x = x1;x1 = x2;x2 = x;};swap1(a, b);//捕捉auto swap2 = [&a, &b]() { //这里这个&不是取地址,是捕捉int x = a;a = b;b = x;};swap2();

了解了基础了lambda表达式之后,就可以来尝试用lambda表达式来替代仿函数了

//了解了基础了lambda表达式之后,就可以来尝试用lambda表达式来替代仿函数了
// 代替仿函数不要捕捉实现lambda表达式,用起来要和函数差不多
auto price_greater = [](const Goods& gl, const Goods& gr) {return gl._price > gr._price;};
auto name_greater = [](const Goods& gl, const Goods& gr) {return gl._name > gr._name;};
auto evaluate_greater = [](const Goods& gl, const Goods& gr) {return gl._evaluate > gr._evaluate;};sort(v.begin(), v.end(), price_greater);
sort(v.begin(), v.end(), evaluate_greater);
sort(v.begin(), v.end(), name_greater);//但是这也不是用lambda表达式代替仿函数最喜欢的写法,最常用的写法,是直接将lambda表达式写到参数里
sort(v.begin(), v.end(), [](const Goods& gl, const Goods& gr) {return gl._price > gr._price;}
);
sort(v.begin(), v.end(), [](const Goods& gl, const Goods& gr) {return gl._name > gr._name; }
);
sort(v.begin(), v.end(), [](const Goods& gl, const Goods& gr) {return gl._evaluate > gr._evaluate; }
);

可以看到,用lambda表达式来代替仿函数,代码就会更加简短和易懂

下面是使用lambda表达式的时候要注意的一些点:

捕捉列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用

  • [var]:表示值传递方式捕捉变量var

  • [=]:表示值传递方式捕获所有父作用域中的变量(包括this)

  • [&var]:表示引用传递捕捉变量var

  • [&]:表示引用传递捕捉所有父作用域中的变量(包括this)

  • [this]:表示值传递方式捕捉当前的this指针

注意:

a. 父作用域指包含lambda函数的语句块

b. 语法上捕捉列表可由多个捕捉项组成,并以逗号分割

比如:[=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量

[&,a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量

c. 捕捉列表不允许变量重复传递,否则就会导致编译错误

d. 在块作用域以外的lambda函数捕捉列表必须为空

e. 在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都 会导致编译报错。

f. lambda表达式之间不能相互赋值,即使看起来类型相同

比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复

9.3lambda表达式的原理

lambda表达式底层还是依靠仿函数来实现的!

每一个lambda表达式在定义的时候,其实编译器内部是为每一个lambda表达式生成了一个叫lambda_uuid的类,这个类就是仿函数类,然后重载接着重载operator()!

而这个uuid是根据算法来计算得出的,保证了每一个lambda表达式生成的仿函数类的名字都不相同

下面来看一个例子:

int main()
{int a = 1, b = 2;auto add = [](int x, int y)->int {return x + y; };cout << add(a, b) << endl;// lambda表达式的底层还是依靠仿函数来实现的// 从<lambda_1>::operator()就可以看出来,这里调用的实际上是一个仿函数// 实际上编译器会根据这个lambda表达式来生成一个叫lambda_uuid的类,这是那个仿函数的类,然后operator()// 这个uuid是根据算法得来的,保证每个lambda表达式生成的lambda仿函数类的名字都不一样// 因此[](int x, int y)->int {return x + y; }实际上是生成了一个匿名的仿函数对象// add就是仿函数类型的对象,这里这个仿函数类的名称就是lambda_1。/*00007FF65D73199B  lea         rax, [rbp + 124h]00007FF65D7319A2  mov         rdi, rax00007FF65D7319A5 xor eax, eax00007FF65D7319A7  mov         ecx, 100007FF65D7319AC  rep stos    byte ptr[rdi]cout << add(a, b) << endl;00007FF65D7319AE  mov         r8d, dword ptr[b]00007FF65D7319B2  mov         edx, dword ptr[a]00007FF65D7319B5  lea         rcx, [add]00007FF65D7319B9  call        `main'::`2': : <lambda_1>::operator() (07FF65D731860h)*/return 0;
}

10.类的新特性

默认成员函数

原来C++类中,有6个默认成员函数:

  1. 构造函数

  2. 析构函数

  3. 拷贝构造函数

  4. 拷贝赋值重载

  5. 取地址重载

  6. const 取地址重载

最后重要的是前4个,后两个用处不大。默认成员函数就是我们不写编译器会生成一个默认的。

C++11 新增了两个:移动构造函数和移动赋值运算符重载。

针对移动构造函数和移动赋值运算符重载有一些需要注意的点如下:

  • 如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。

  • 如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)

  • 如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。

class Person
{
public:Person(const char* name = "", int age = 0):_name(name), _age(age){}/*Person(const Person& p):_name(p._name),_age(p._age){}*//*Person& operator=(const Person& p){if(this != &p){_name = p._name;_age = p._age;}return *this;}*//*~Person(){}*/
private:bit::string _name;int _age;
};int main()
{Person s1;Person s2 = s1;Person s3 = std::move(s1);Person s4;s4 = std::move(s2);return 0;
}

c++11还提供了指定编译器生成默认构造函数(default)

在类内部可以禁止该类的拷贝构造和 = 重载被调用的功能(delete)

class A
{
public:// 这里不管写了什么构造函数,是带参还是不带参,是拷贝构造还是直接构造,// 都是一样的,编译器不会在为这个类生成默认的无参构造函数//A(const A& a) //拷贝构造//{//	_a = a._a;//}// 但是如果我们不想自己实现构造函数,就可以直接让编译器指定生成默认构造函数A() = default; //指定编译器生成默认构造函数//c++11提供了复用delete关键字,在类内部可以禁止该类的拷贝构造和 = 重载被调用A(const A& a) = delete;A& operator=(const A& a) = delete;private:int _a;
};int main()
{//先来看一些类的新功能// 首先就是,一旦一个类实现了一个构造函数,那么编译器将不会为该类生成默认构造函数A aa; //会发现无法构造aa对象出来,因为没有显式定义构造函数,而编译器也没有生成默认构造函数//A aaa(aa);// 如果用户想禁止类外部能够调用类内部的拷贝构造和=重载// c++11提供了复用delete关键字,在类内部可以禁止该类的拷贝构造和=重载被调用return 0;
}

11.可变参数模版

C++11的新特性可变参数模板能够让您创建可以接受可变参数的函数模板和类模板,相比C++98/03,类模版和函数模版中只能含固定数量的模版参数,可变模版参数无疑是一个巨大的改进。然而由于可变模版参数比较抽象,使用起来需要一定的技巧,所以这块还是比较晦涩的。现阶段呢,掌握一些基础的可变参数模板特性就够用了,

下面就是一个基本可变参数的函数模板

// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
template <class ...Args>
void ShowList(Args... args)
{}

上面的参数args前面有省略号,所以它就是一个可变模版参数,我们把带省略号的参数称为“参数包”,它里面包含了0到N(N>=0)个模版参数。我们无法直接获取参数包args中的每个参数的,只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特点,也是最大的难点,即如何展开可变模版参数。

由于语法不支持使用args[i]这样方式获取可变参数,所以我们的用一些奇招来一一获取参数包的值

  • 递归函数方式展开参数包

// 递归终止函数
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, 'A');ShowList(1, 'A', std::string("sort"));return 0;
}
  • 逗号表达式展开参数包

这种展开参数包的方式,不需要通过递归终止函数,是直接在expand函数体中展开的, printarg不是一个递归终止函数,只是一个处理参数包中每一个参数的函数。这种就地展开参数包的方式实现的关键是逗号表达式。我们知道逗号表达式会按顺序执行逗号前面的表达式。

expand函数中的逗号表达式:(printarg(args), 0),也是按照这个执行顺序,先执行printarg(args),再得到逗号表达式的结果0。同时还用到了C++11的另外一个特性——初始化列表,通过初始化列表来初始化一个变长数组, {(printarg(args), 0)…}将会展开成((printarg(arg1),0), (printarg(arg2),0), (printarg(arg3),0), etc… ),最终会创建一个元素值都为0的数组int arr[sizeof…(Args)]。由于是逗号表达式,在创建数组的过程中会先执行逗号表达式前面的部分printarg(args)打印出参数,也就是说在构造int数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在数组构造的过程展开参数包

template <class T>
void PrintArg(T t)
{cout << t << " ";
}//展开函数
template <class ...Args>
void ShowList(Args... args)
{int arr[] = { (PrintArg(args), 0)... };cout << endl;
}int main()
{ShowList(1);ShowList(1, 'A');ShowList(1, 'A', std::string("sort"));return 0;
}

12.包装器

12.1function包装器

function包装器 也叫作适配器。C++中的function本质是一个类模板,也是一个包装器。

那function的用途是什么呢?是用来解决什么场景的呢?来看下面这段代码:

// ret = func(x);
// 上面func可能是什么呢?那么func可能是函数名?函数指针?函数对象(仿函数对象)?
// 也有可能是lamber表达式对象?所以这些都是可调用的类型!如此丰富的类型,可能会导致模板的效率低下!
// 为什么呢?我们继续往下看template<class F, class T>
T useF(F f, T x)
{static int count = 0;cout << "count:" << ++count << endl;cout << "count:" << &count << endl;return f(x);
}double f(double i)
{return i / 2;
}struct Functor
{double operator()(double d){return d / 3;}
};int main()
{// 函数名cout << useF(f, 11.11) << endl;// 函数对象cout << useF(Functor(), 11.11) << endl;// lamber表达式cout << useF([](double d)->double { return d / 4; }, 11.11) << endl;return 0;
}

通过上面的程序验证,我们会发现useF函数模板实例化了三份。

包装器可以很好的解决上面的问题

//std::function在头文件<functional>
// 类模板原型如下
template <class T> function;     // undefinedtemplate <class Ret, class... Args>
class function<Ret(Args...)>;
//模板参数说明:
//Ret : 被调用函数的返回类型
//Args…:被调用函数的形参

下面是function包装器的基础使用方法:

// 使用方法如下:
#include <functional>
int f(int a, int b)
{return a + b;
}
struct Functor
{
public:int operator() (int a, int b){return a + b;}
};
class Plus
{
public:static int plusi(int a, int b){return a + b;}double plusd(double a, double b){return a + b;}};int main()
{// 函数名(函数指针)std::function<int(int, int)> func1 = f;cout << func1(1, 2) << endl;// 函数对象std::function<int(int, int)> func2 = Functor();cout << func2(1, 2) << endl;// lamber表达式std::function<int(int, int)> func3 = [](const int a, const int b){return a + b; };cout << func3(1, 2) << endl;// 类的成员函数std::function<int(int, int)> func4 = &Plus::plusi;cout << func4(1, 2) << endl;std::function<double(Plus, double, double)> func5 = &Plus::plusd;cout << func5(Plus(), 1.1, 2.2) << endl;return 0;
}

有了包装器,如何解决模板的效率低下,实例化多份的问题呢?

#include <functional>
template<class F, class T>
T useF(F f, T x)
{static int count = 0;cout << "count:" << ++count << endl;cout << "count:" << &count << endl;return f(x);
}double f(double i)
{return i / 2;
}struct Functor
{double operator()(double d){return d / 3;}
};int main()
{// 函数名std::function<double(double)> func1 = f;cout << useF(func1, 11.11) << endl;// 函数对象std::function<double(double)> func2 = Functor();cout << useF(func2, 11.11) << endl;// lamber表达式std::function<double(double)> func3 = [](double d)->double { return d /4; };cout << useF(func3, 11.11) << endl;return 0;
}

12.2bind包装器

std::bind函数定义在头文件中,是一个函数模板,它就像一个函数包装器(适配器)接受一个可调用对象(callable object),生成一个新的可调用对象来“适应”原对象的参数列表。一般而言,我们用它可以把一个原本接收N个参数的函数fn,通过绑定一些参数,返回一个接收M个(M可以大于N,但这么做没什么意义)参数的新函数。同时,使用std::bind函数还可以实现参数顺序调整等操作

bind包装器的原型如下:

// 原型如下:
template <class Fn, class... Args>
/* unspecified */ bind (Fn&& fn, Args&&... args);
// with return type (2) 
template <class Ret, class Fn, class... Args>
/* unspecified */ bind (Fn&& fn, Args&&... args);

可以将bind函数看作是一个通用的函数适配器,它接受一个可调用对象,生成一个新的可调用对

象来“适应”原对象的参数列表。

调用bind的一般形式:auto newCallable = bind(callable,arg_list);

其中,newCallable本身是一个可调用对象,arg_list是一个逗号分隔的参数列表,对应给定的callable的参数。当我们调用newCallable时,newCallable会调用callable,并传给它arg_list中的参数

arg_list中的参数可能包含形如_n的名字,其中n是一个整数,这些参数是“占位符”,表示newCallable的参数,它们占据了传递给newCallable的参数的“位置”。数值n表示生成的可调用对象中参数的位置:_1为newCallable的第一个参数,_2为第二个参数,以此类推

下面是使用例子:

// 使用举例
#include <functional>
int Plus(int a, int b)
{return a + b;
}class Sub
{
public:int sub(int a, int b){return a - b;}
};int main()
{//表示绑定函数plus 参数分别由调用 func1 的第一,二个参数指定std::function<int(int, int)> func1 = std::bind(Plus, placeholders::_1, placeholders::_2);//auto func1 = std::bind(Plus, placeholders::_1, placeholders::_2);//func2的类型为 function<void(int, int, int)> 与func1类型一样//表示绑定函数 plus 的第一,二为: 1, 2auto func2 = std::bind(Plus, 1, 2);cout << func1(1, 2) << endl;cout << func2() << endl;Sub s;// 绑定成员函数std::function<int(int, int)> func3 = std::bind(&Sub::sub, s, placeholders::_1, placeholders::_2);// 参数调换顺序std::function<int(int, int)> func4 = std::bind(&Sub::sub, s, placeholders::_2, placeholders::_1);cout << func3(1, 2) << endl;cout << func4(1, 2) << endl;return 0;
}

13.线程库

13.1线程库基本操作

在C++11之前,涉及到多线程问题,都是和平台相关的,比如windows和linux下各有自己的接口,这使得代码的可移植性比较差(在c++11出来之前。c++98采取的是条件编译来解决不同平台的编译问题)。C++11中最重要的特性就是对线程进行支持了,使得C++在并行编程时不需要依赖第三方库,而且在原子操作中还引入了原子类的概念。要使用标准库中的线程,必须包含< thread >头文件。C++11中线程类

具体的知识和简单接口的使用这里就不演示了,这里直接给一个线程操作的代码例子:

这个代码有线程互斥和lambda表达式和线程操作的结合。还有无锁操作

#include<iostream>
#include<thread> 
#include<mutex>
#include<vector>
using namespace std;int x = 0; //为了保护这个全局变量,线程应该要互斥,也就是线程之间要串行访问该资源
// 有关多线程的知识,可以复习Linux的多线程,线程库的线程操作都是一样的,都是对底层系统调用接口的封装mutex m; //锁不能定义线程的执行流中,因为锁是用来保护临界资源的,要保证能被所有线程看到void Add(int x)
{m.lock(); //加锁for (int i = 1; i <= x; i++){//如果把锁加载for循环里面的话,会导致锁的粒度过小,反而会导致线程频繁的切换,浪费时间。//因此即便并行`,速度也不如串行x++;}m.unlock();//解锁
}int main()
{/*thread t1(Add, 1000000);thread t2(Add, 1000000);t1.join(); //主线程必须要等待线程t2.join();cout << x << endl;*/// 这里还可以让lambda表达式和线程一起玩// 经过上述操作,我们知道在c++的线程操作中,要在构造线程对象的时候传一个函数指针给他// 因此这样就可以玩lambda表达式了// 假设要创建n个线程,并都执行一个任务,对一个x累加m次int n, m;cin >> n >> m;vector<thread> v;atomic<int> x = 0; //该x变量的++,--等操作会变成原子操作。// 这种做法叫做CAS——无锁操作for (int i = 1; i <= n; i++){//这里要注意,这里是构造了一个匿名对象,是一个右值v.push_back(thread([&x](int count)->void {for (int i = 1; i <= count; i++){x++;}}, m)); }//对n个线程都要等待for (auto& t : v){cout << t.get_id() << "join()" << endl;t.join();}cout << "x: " << x << endl;return 0;
}

注意:

  1. 线程是操作系统中的一个概念,线程对象可以关联一个线程,用来控制线程以及获取线程的状态

  2. 创建一个线程对象后,没有提供线程函数,该对象实际没有对应任何线程。

  3. 当创建一个线程对象后,并且给线程关联线程函数,该线程就被启动,与主线程一起运行。线程函数一般情况下可按照以下三种方式提供:

    • 函数指针

    • lambda表达式

    • 函数对象

  4. thread类是防拷贝的,不允许拷贝构造以及赋值,但是可以移动构造和移动赋值,即将一个线程对象关联线程的状态转移给其他线程对象,转移期间不意向线程的执行。

  5. 可以通过jionable()函数判断线程是否是有效的,如果是以下任意情况,则线程无效

    • 采用无参构造函数构造的线程对象

    • 线程对象的状态已经转移给其他线程对象

    • 线程已经调用jion或者detach结束

面试题:并发与并行的区别?

13.2线程函数参数

线程函数的参数是以值拷贝的方式拷贝到线程栈空间中的,因此:即使线程参数为引用类型,在线程中修改后也不能修改外部实参,因为其实际引用的是线程栈中的拷贝,而不是外部实参

#include <thread>
void ThreadFunc1(int& x)
{x += 10;
}
void ThreadFunc2(int* x)
{*x += 10;
}int main()
{int a = 10;// 在线程函数中对a修改,不会影响外部实参,因为:线程函数参数虽然是引用方式,	但其实际引用的是线程栈中的拷贝thread t1(ThreadFunc1, a);t1.join();cout << a << endl;// 如果想要通过形参改变外部实参时,必须借助std::ref()函数thread t2(ThreadFunc1, std::ref(a);t2.join();cout << a << endl;// 地址的拷贝thread t3(ThreadFunc2, &a);t3.join();cout << a << endl;return 0;
}

注意:如果是类成员函数作为线程参数时,必须将this作为线程函数参数。

13.3多线程操作

一个经典的场景:一个线程打印奇数,一个线程打印偶数

	// 一个经典的场景:一个线程打印奇数,一个线程打印偶数。【注意要依次打印】int sum = 100; //范围是100之内mutex mtx1, mtx2; //两个锁分别给两个线程加锁,锁的目的是为了配合条件变量// 用两个条件变量来实现线程同步condition_variable ct1; condition_variable ct2;// t1线程打印奇数thread t1([&]()->void {for (int i = 1; i <= sum; i += 2){if (i != 1) //如果没有条件限制,那么就会导致两个线程都阻塞{unique_lock<mutex> lock1(mtx1);ct1.wait(lock1); //等偶数打印之后来唤醒自己}cout << "tid: " << this_thread::get_id() << ": t1: " << i << endl;// 打印一个奇数之后就通知t2线程打印偶数ct2.notify_one(); //通知t2}});//t2线程打印偶数thread t2([&](){ //省略->voidfor (int i = 2; i <= sum; i+=2){// 如果打印完一个偶数之后,又进来该线程就要等待t1线程打印奇数之后唤醒自己unique_lock<mutex> lock2(mtx2); //这样是为了RAII,这里不细谈,等学习了智能指针之后就懂了ct2.wait(lock2); //先等待奇数打印出来,在唤醒t2线程cout << "tid: " << this_thread::get_id() << ": t2: " << i << endl;//打印偶数之后就通知t1线程打印奇数ct1.notify_one();}});// 如果没有条件变量和互斥锁,会导致一个问题————竞争终端,即打印的顺序发生冲突,并不是我们理想的打印顺序。// 这里其实早就学了,就是Linux多线程的知识————生产者消费者模型t1.join();t2.join();

这里放的是正确的版本,懒得搞错误版本一步步纠错到正确版本了。hh,有点懒了属于是

代码的执行结果如下:

image-20250423231950379

可以看到就是依次输出,没有竞争终端的现象出现了,这是做好了线程的同步与互斥的结果,这是程序员的任务。多线程不写好是很容易出现bug的

http://www.xdnf.cn/news/180559.html

相关文章:

  • 自然语言处理之机器翻译:Statistical Machine Translation(SMT)的评估方法解析与创新实践
  • 小集合 VS 大集合:MySQL 去重计数性能优化
  • 常用第三方库:sqflite数据库应用
  • Python语言基础知识详解:数据类型及运算
  • 【MQ篇】RabbitMQ之消费失败重试!
  • 2、Linux操作系统下,ubuntu22.04版本安装搜狗输入法
  • <PLC><汇川><工控>汇川PLC实现光纤缠绕设备
  • ollama的若干实践
  • Step1X-Edit: A practical framework for general image editing
  • PaddleX的安装
  • Moment 在 JavaScript 中解析、校验、操作、显示日期和时间
  • web 开发中,前端部署更新后,该怎么通知用户刷新
  • 新闻数据接口开发指南:从多源聚合到NLP摘要生成
  • 一些可用于监控服务器响应时间稳定性的工具
  • 【神经网络与深度学习】端到端方法和多任务学习
  • 来自B站AIGC科技官的“vLLM简介“视频截图
  • 音频转base64
  • 基于c++的LCA倍增法实现
  • log4cpp进阶指南
  • Dart中一个类实现多个接口 以及Dart中的Mixins
  • NestJS + Kafka 秒杀系统完整实践总结
  • 大语言模型的“模型量化”详解 - 04:KTransformers MoE推理优化技术
  • Android 理清 Gradle、AGP、Groovy 和构建文件之间的关系
  • 打孔包地解决PCB的串扰问题
  • 03_多线程任务失败解决方案
  • C#学习第19天:多线程
  • 关于 Web 服务器的五个案例
  • AI 应用同质化:一场看不见的资源 “吞噬战”
  • 人机鉴权和机机鉴权
  • Day26 -php开发05 -搭建个人博客三种实现:自己写前后端 套用现成模板 调用第三方模板引擎smarty 及三种方法的缺点