个人主页:企鹅不叫的博客
专栏
- C语言初阶和进阶
- C项目
- Leetcode刷题
- 初阶数据结构与算法
- C++初阶和进阶
- 《深入理解计算机操作系统》
- 《高质量C/C++编程》
- Linux
⭐️ 博主码云gitee链接:代码仓库地址
⚡若有帮助可以【关注+点赞+收藏】,大家一起进步!
【初阶与进阶C++详解】第一篇:C++入门知识必备
【初阶与进阶C++详解】第二篇:C&&C++互相调用(创建静态库)并保护加密源文件
【初阶与进阶C++详解】第三篇:类和对象上(类和this指针)
【初阶与进阶C++详解】第四篇:类和对象中(类的六个默认成员函数)
【初阶与进阶C++详解】第五篇:类和对象下(构造+static+友元+内部类
【初阶与进阶C++详解】第六篇:C&C++内存管理(动态内存分布+内存管理+new&delete)
【初阶与进阶C++详解】第七篇:模板初阶(泛型编程+函数模板+类模板+模板特化+模板分离编译)
【初阶与进阶C++详解】第八篇:string类(标准库string类+string类模拟实现)
【初阶与进阶C++详解】第九篇:vector(vector接口介绍+vector模拟实现+vector迭代器区间构造/拷贝构造/赋值)
【初阶与进阶C++详解】第十篇:list(list接口介绍和使用+list模拟实现+反向迭代器和迭代器适配)
【初阶与进阶C++详解】第十一篇:stack+queue+priority_queue+deque(仿函数)
【初阶与进阶C++详解】第十二篇:模板进阶(函数模板特化+类模板特化+模板分离编译)
【初阶与进阶C++详解】第十三篇:继承(菱形继承+菱形虚拟继承+组合)
【初阶与进阶C++详解】第十四篇:多态(虚函数+重写(覆盖)+抽象类+单继承和多继承)
【初阶与进阶C++详解】第十五篇:二叉树搜索树(操作+实现+应用KVL+性能+习题)
【初阶与进阶C++详解】第十六篇:AVL树-平衡搜索二叉树(定义+插入+旋转+验证)
【初阶与进阶C++详解】第十七篇:红黑树(插入+验证+查找)
【初阶与进阶C++详解】第十八篇:map_set(map_set使用+multiset_multimap使用+模拟map_set)
【初阶与进阶C++详解】第十九篇:哈希(哈希函数+哈希冲突+哈希表+哈希桶)
【初阶与进阶C++详解】第二十篇:unordered_map和unordered_set(接口使用+模拟实现)
【初阶与进阶C++详解】第二十一篇:哈希应用(位图实现应用+布隆过滤器增删查优缺点+海量数据面试题)
C++11是2011年更新的C++新语法,C++11能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全,不仅功能更强大,而且能提升程序员的开发效率。
使用初始化列表初始化时,可以初始化数组和自定义类型,同时可以省略=====,这些用法不常用
struct Point{ int _a; int _b; }; int x1 = 1; //新写法 int x2{ 2 }; //原来写法 int arr1[] = { 1,2,3,4 }; //C++11支持写法 int arr2[]{ 1,2,3,4,5,6 }; //C++11 自定义类型列表初始化 Point p{ 1, 2 };
使用更多是在new(传送门)对象的时候,初始化对象,同时还有部分自定义类型和内置类型也有新写法
//旧写法 int *p1 = new int(3);//开辟一个int的空间,并初始化为3赋值给p1 int *p2 = new int[3];//开辟3个int的空间,不进行初始化 //C++11写法,用多个花括号的方式进行批量初始化 Point* p4 = new Point(1, 3); Point* p4 = new Point[2]{ {1, 2}, {3, 4} }; //下面第一二种写法相同,利用{}直接调用构造函数 //第一种是旧写法,第二三种是新写法 Point p1(1, 3); Point p2{1, 3}; Point p3 = { 1, 3 };//对类来说,会进行隐式类型转换+调用构造函数 //内置类型也可以这样写 vector<int> v1 = { 1,2,3,4 }; vector<int> v2{ 1,2,3,4 };//不建议这么写
initializer_list
一般是作为构造函数的参数(本质就是{}),C++11对STL中的不少容器就增加initializer_list
作为参数的构造函数,支持多个对象初始化auto il = { 10, 20, 30 }; cout << typeid(il).name() << endl;//initializer_list
模拟实现:初始化,同时开对应size()大小空间
//initializer_list构造 vector(const initializer_list<T>& il) :_start(nullptr), _finish(nullptr), _endofstorage(nullptr) { reserve(il.size());//扩容 for (auto e : il) { push_back(e); } }
auto声明的类型必须要进行初始化,否则编译器无法推导出auto的实际类型。auto不能作为函数的参数和返回值,且不能用来直接声明数组类型(传送门)
int a = 10; auto pa = &a;
decltype是根据表达式的实际类型推演出定义变量时所用的类型。且还可以使用推导出来的类型进行变量声明。
decltype不可以作为函数的参数,编译时推导类型
int Add(int x, int y) { return x + y; } int a = 10, b = 20; int sum = a * b; decltype(a + b) num; // 用decltype自动推演sum的实际类型,输出int cout << "type(sum) " << typeid(sum).name() << endl; // 用decltype自动推演a+b的实际类型,输出int cout << "type(a+b) " << typeid(num).name() << endl; // 不带参数,推导函数类型,输出int cout << "type(&sum) " << typeid(decltype(Add)).name() << endl;
typeid只能查看类型,不能用其结果类定义类型,typeid只能推导类型,但是不能使用类型声明和定义变量
int a = 9; cout << "type(a) " << typeid(a).name() << endl;
左值:
- 一个表示数据的表达式,可以取地址和赋值,左值引用不能引用右值,左值可以出现在赋值符号的左边,也可以出现在赋值符号的右边
- const修饰后的左值不可以赋值,但是可以取地址,可以引用左值也可以引用右值
- 左值引用用符号
&
右值:
一个表示数据的表达式,右值不能取地址,右值引用不能引用左值(可以move后引用左值),右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边比如:表达式返回值
A+B
、字面常量10
如果表达式运行结果或单个变量是一个引用,则认为是右值
右值引用的符号是
&&
总结:左值可以取地址,右值不能取地址
move:当需要用右值引用引用一个左值时,可以通过move来获得绑定到左值上的右值引,可以把左值换成右值,但不能把右值转左值,则可能资源被掠夺导致该对象失效
被转化的左值,其生命周期并没有随着左值的转化而改变,即std::move转化的左值变量value不会被销毁。move告诉编译器:我们有⼀个左值,但我们希望像⼀个右值⼀样处理它。
STL中也有另一个move函数,就是将一个范围中的元素搬移到另一个位置。
int main() { String s1("123"); //输出移动拷贝,s1变成一个右值,所以会调用移动构造去构造s2,这样s1的资源就被转移给了s2,s1本身也没有资源了 String s2(move(s1)); return 0; }
- 纯右值:基本类型的常量或临时对象,如:a+b,字面常量
- 将亡值:自定义类型的临时对象用完自动完成析构,如:函数以值的方式返回一个对象
//纯右值 10; a+b; Add(a,b); //将亡值 string s1("1111");//"1111"是将亡值 string s2 = to_string(1234);//to_string的return对象是一个将亡值 string s3 = s1+"hello" //相加重载的return对象是一个将亡值 move(s1);//被move之后的s1也是将亡值
左值引用就是对左值的引用,针对出了作用域不会销毁的变量进行引用返回,引用传参,减少形参拷贝代价
int b = 1; const int c = 2; // 以下是对上面左值的 左值引用 int& rb = b; const int& rc = c;
下面是常见的右值和右值引用,如果表达式运行结果或单个变量是一个引用,则认为是右值
int Add(int x, int y) { return x + y; } double x = 1.1, y = 2.2; // 常见的右值 10; x + y; Add(x, y); // 以下都是对右值的右值引用 int&& rr1 = 10; double&& rr2 = x + y; int&& ret = Add(3, 4);// 函数的返回值是一个临时变量,是一个右值
不能直接对右值进行
取地址/赋值
操作,但是在右值引用过后,便可以对引用值进行取地址/赋值
操作,给右值取别名后,会导致右值被存储到特定位置,且可以取到该位置的地址int&& rr1 = 1; cout << rr1 << endl; rr1 = 2; cout << rr1 << endl; int* p = &rr1; *p = 3; cout << rr1 << endl; //上面依次输出1 2 3
左值引用:做参数和做返回值都可以提高效率
右值引用:提高
移动构造/移动赋值
等深拷贝场景的效率
右值引用的构造函数/赋值重载称作
移动构造/移动赋值
,直接拿取对象资源,避免多次拷贝造成时间和空间的损失
调用下面这个string类,下面会输出三次深拷贝,造成浪费,如果使用左值引用,出了函数作用域之后,临时对象会被销毁,进行赋值的时候会出现利用str别名拷贝,但str是一个已经销毁的对象
class String { public: string(const char* str = "") :_size(strlen(str)) , _capacity(_size) { //cout << "string(char* str)" << endl; _str = new char[_capacity + 1]; strcpy(_str, str); } void swap(string& s) { ::swap(_str, s._str); ::swap(_size, s._size); ::swap(_capacity, s._capacity); } // 移动构造 string(string&& s) :_str(nullptr) , _size(0) , _capacity(0) { cout << "string(string&& s) -- 资源转移" << endl; swap(s); } // 拷贝构造 string(const string& s) :_str(nullptr) , _size(0) , _capacity(0) { cout << "string(const string& s) -- 深拷贝" << endl; string tmp(s._str); swap(tmp); } // 移动赋值 string& operator=(string&& s) { cout << "string& operator=(string&& s) -- 资源转移" << endl; swap(s); return *this; } // 赋值重载 string& operator=(const string& s) { cout << "string& operator=(string s) -- 深拷贝" << endl; string tmp(s); swap(tmp); return *this; } ~string() { delete[] _str; _str = nullptr; } char& operator[](size_t pos) { assert(pos < _size); return _str[pos]; } private: char* _str; size_t _size; size_t _capacity; // 不包含最后做标识的\0 }; String func(String& str) { String tmp(str); return tmp; } int main() { String s1("123"); String s2(s1); String s3(func(s1)); return 0; }
因为返回的临时对象是一个右值,所以会调用上面的移动构造的代码对返回的临时对象进行构造,本质是资源进行转移,此时tmp指向的是一块空的资源。最后返回的临时对象拷贝构造给s3时还是调用了移动构造,两次移动构造被编译器优化为一个。可以看出的是这里解决的是减少接受函数返回对象时带来的拷贝,极大地提高了效率。
// 移动构造 string(string&& s) :_str(nullptr) , _size(0) , _capacity(0) { // 对于将亡值,内部做移动拷贝 cout << "string(string&& s) -- 资源转移" << endl; swap(s); }
可以看出的是func返回的临时对象是通过移动赋值给s2的,也是一次移动赋值,复用了之前已经写好的一个
swap
函数,实现了一个“现代写法”的构造,直接交换了二者的资源。避免深拷贝带来的副作用// 移动赋值 string& operator=(string&& s) { cout << "string& operator=(string&& s) -- 资源转移" << endl; swap(s); return *this; }
模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值,传入的值,不管是左值还是右值,传入后,都被当作左值。
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); //fun(std::move(t));//利用move将左值转换成右值 //Fun(std::forward
(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; }
问题:我们此时想调用右值怎么办,但是我们不能用move来解决问题,使用move变成右值后,数据容易被拿走
解决:使用完美转发能够在传递过程中保持它的左值或者右值的属性,使用
forward
函数进行完美转发template<typename T> void PerfectForward(T&& t){ Fun(std::forward<T>(t)); }
构造函数
析构函数
拷贝构造函数
拷贝赋值重载
取地址重载
const 取地址重载
移动构造函数
拷贝赋值函数
最后两个是新增的,具有以下特点
如果没有实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。
默认生成的移动构造函数,对于内置类型成员会按照字节序进行浅拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
如果没有实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。
默认生成的移动构造函数,对于内置类型成员会按照字节序进行浅拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。
class Test{ private: string _s; int _a; public: Test(const string& s="", int a=0) :_s(s), _a(a) {} }; void DefaultMoveCopy(){ Test t1;//构造 拷贝构造 构造 cout << endl; TestB t2 = t1;//拷贝构造 构造 cout << endl; TestB t3 = std::move(t1); //移动拷贝 cout << endl; TestB t4; t4 = std::move(t2);//构造 拷贝构造 构造 移动赋值重载 cout << endl; }
调用对应强制生成或者删除默认成员函数
class Person { public: Person(const char* name = "", int age = 0) :_name(name) ,_age(age) {} Person(Person&& p) = default;// 强制生成默认的 Person& operator=(Person&& p) = default; Person(Person& p) = delete;// 强制删除默认的 ~Person() {} private: Simulation::string _name; int _age; };
这就是一个可变的模板参数,允许一个函数有多个参数,且不要求是相同类型,使用
sizeof
即可查看参数的个数template <class... Args> void emplace_back (Args&&... args){ // 参数个数 cout << sizeof...(args) << endl; }
- Args和args前面有省略号,所以它们是可变参数,带省略号的参数称为“参数包”,它里面包含了0到N(N>=0)个模版参数
- Args是一个
模板参数包
,args是一个函数形参参数包
//一个无参的同名函数,用作参数包递归的结尾,当参数包中的参数个数为0时,调用无参的ShowList void ShowList() {} //当参数包中只有一个参数的时候,调用对应的单参函数 //args中第一个参数作为val传参,参数包中剩下的参数作为新的参数包传参 template <class T> void ShowList(const T & val) { cout << val << "->" << typeid(val).name() << " end" << endl; } template <class T, class ...Args> void ShowList(const T & val, Args... args) { cout << "参数个数" << sizeof...(args) << endl; cout << val << "->" << typeid(val).name() << endl; // 递归调用ShowList,当参数包中的参数个数为0时,调用上面无参的ShowList ShowList(args...); } void test() { ShowList(1, 'x', string("123")); }
如果一个参数包中都是一个的类型,那么可以使用该参数包对数组进行列表初始化,参数包会展开,然后对数组进行初始化,可以利用列表初始化数组时,展开参数包的特性,再与一个逗号表达式结合使用,可以展开都会表达式中的参数.
template <class T> int PrintArg(const T& t) { cout << t << " "; return 0; } template <class ...Args> void ShowList(Args... args) { //错误写法 //只适用于所有参数都是相同类型的情况,如果是不同类型则会报错 //int arr[] = { args... }; int arr[] = { PrintArg(args)... }; cout << endl; } //都会表达式会按顺序执行,先执行第一个函数,然后把最后的0值赋给数组,这样就把参数包展开了 //展开 //(PrintArg(arg1), 0),(PrintArg(arg2), 0),(PrintArg(arg3), 0),(PrintArg(arg4), 0)
emplace_back可以支持可变参数包,然后调用定位new,使用参数包对空间进行初始化
template <class... Args> //万能引用,实参是左值,参数包的这个形参就是左值引用,实参是右值,参数包的这个形参就是右值引用 void emplace_back (Args&&... args); //灵活使用 list<pair<int, string>> lt; lt.emplace_back(1, "hehe");
push_back接受的是一个参数包,参数不能够匹配,所以无法使用
list<pair<int, string>> lt; lt.push_back({ 2, "haha" });
- 对于右值对象,emplace_back是先构造一个对象,移动构造,push_back也是如此
- 对于左值对象,都是先构造一个左值对象,再拷贝构造
- 对于参数包,emplace_back直接使用参数包用定位new构造
对于自定义类型排序,我们可以用sort,并且可以通过仿函数使用自定义类型比较
#include
//sort #include //greater/less int main() { int arr[]={1,3,2,5,4}; int sz=sizeof(arr)/sizeof(arr[0]); //默认升序 std::sort(arr,arr+sz); //降序传入仿函数greater std::sort(arr,arr+sz,greater<int>()); } 对于自定义类型,我们需要自己实现一个对应的仿函数
struct Goods { string _name; // 名字 double _price; // 价格 int _evaluate; // 评价 Goods(const char* str, double price, int evaluate) :_name(str) , _price(price) , _evaluate(evaluate) {} }; //价格降序 struct CompPriceGreater{ bool operator()(const Goods& g1, const Goods& g2) { return g1._price > g2._price; } }; //价格升序 struct CompPriceLess { bool operator()(const Goods& g1, const Goods& g2) { return g1._price < g2._price; } }; int main () { sort(v.begin(), v.end(), ComparePriceLess()); sort(v.begin(), v.end(), ComparePriceGreater()); }
处理的对象有很多不同的成员变量的时候,就需要实现非常非常多的仿函数
[capture-list](parameters)mutable -> return-type{statement}
- [capture-list]捕捉列表,用于编译器判断是否是lambda表达式,同时捕捉该表达式所在域的变量以供函数使用
- (parameters)参数,和函数的参数一致。如果不需要传参则可连带()一起省略
- mutable默认情况下捕捉列表捕捉的参数是const修饰的,该关键字的作用是取消const使其可修改
- -> return-type函数返回值类型,返回类型明确的情况下可以省略,由编译器自动推导
- {statement}函数体。除了可以使用传入的参数,还可以使用捕捉列表获取的参数
下面可以看到,可以省略函数返回值类型,编译器会自动推导,或者我们可以指定返回值类型
int x = 1, y = 2; auto Add = [](int x, int y) {return x + y; }; //auto Add = [](int x, int y) ->int{return x + y; }; cout<< Add(x, y)<<endl;//输出3
捕捉了函数作用域里面的局部变量
x/y
,直接在lambda
表达式内部使用,因为不需要传入参数,所以直接把参数()
和返回值类型一并省略掉int x = 1, y = 2; auto Add = [x, y]{return x+y }; cout<< Add(x, y)<<endl;//输出3
[var]:表示值传递方式捕捉变量var
[=]:表示值传递方式捕获所有父作用域中的变量(包括this)
[&var]:表示引用传递捕捉变量var
[&]:表示引用传递捕捉所有父作用域中的变量(包括this)
注意:lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都会报错,lambda函数生成的是一个对象,lambda表达式之间不能相互赋值
//lambda表达式之间不能相互赋值,编译器会将他们是别成其他类型 auto f1 = []{cout << "hello world" << endl; }; auto f2 = []{cout << "hello world" << endl; }; //f1 = f2;
下面这个函数不加mutable是无法使用的,因为参数默认是const类型,如果不用()mutable我们不能对其修改
//mutable 只是让传值捕捉变量const属性去掉了,这个关键字使用的时候必须带上函数参数的() auto Swap2 = [a, b]()mutable{ int tmp = a; a = b; b = tmp; };
下面是引用方式捕获部分和捕获全部对象,当然可以修改参数
// 用引用的方式捕捉 auto Swap2 = [&x, &y]{ int tmp = x; x = y; y = tmp; }; int c =2, d=3, e=4, f=5, g=6, ret; // 传引用捕捉全部对象 auto Func2 = [&]{ ret = c + d*e / f + g; };
下面是传值捕获全部对象
int c =2, d=3, e=4, f=5, g=6, ret; // 传值捕捉全部对象 auto Func1 = [=]{ return c + d*e / f + g; };
下面可以用单独引用的方式修改ret,语法上捕捉列表由多个捕获对象组成,用逗号分割
int c =2, d=3, e=4, f=5, g=6, ret; // 混着捕捉 auto Func3 = [c, d, &ret]{ ret = c + d; }; // ret传引用捕捉 其他全部传值捕捉 auto Func4 = [=, &ret]{ ret = c + d*e / f + g; };
对自定义价格修改如下
vector<Goods> v = { Goods( "苹果", 2.1, 5 ), { "香蕉", 3, 4 }, { "橙子", 2.2, 3 }, { "菠萝", 1.5, 4 } }; //价格升序 sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2){ return g1._price < g2._price; }); cout << endl; //价格降序 sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2){ return g1._price > g2._price; }); cout << endl; //名字排序 sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2){ return g1._evaluate < g2._evaluate; });
lambda的底层就是把自己转成了一个仿函数供我们调用,就是在类中重载了operator()运算符的类对象
function包装器,也叫作适配器。C++中的function本质是一个类模板,也是一个包装器。头文件< functional >
class AddClass { public: static int Addi(int a, int b) { return a + b; } double Addd(double a, double b) { return a + b; } }; int func(int a, int b) { return a + b; } //仿函数 struct Functor { int operator()(int a, int b) { return a + b; } }; void TestFunction1() { // 普通函数 function<int(int, int)> func1 = func; cout << func1(10, 20) << endl; // 仿函数 function<int(int, int)> func2 = Functor(); cout << func2(10, 20) << endl; // 类中static成员函数,,可加&可不加 function<int(int, int)> func3 = AddClass::Addi; cout << func3(100, 200) << endl; // 类中非静态成员函数,要加上&,要AddClass()的匿名对象来适配包装器 function<double(AddClass, double, double)> func4 = &AddClass::Addd; cout << func4(AddClass(), 100.11, 200.11) << endl; // lambda表达式 function<int(int, int)> func5 = [](int a, int b) {return a + b; }; cout << func5(100, 200) << endl; }
在头文件< functional >中,函数适配器,接受一个可调用对象,生成一个新的可调用对象配对原参数列表,现参数顺序调整
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为第二个参数
placeholders
是用来占位的,代表这里的参数需要用户手动传入,而_1
代表传入的第一个参数,_2
就是传入的第二个参数,同时可以利用 placeholders 调整参数个数// 调整可调用对象的参数个数和顺序 // _1 _2 ... 表示你要自己传的那些参数,_1表示第一个参数传给_1 // 2个参数 function<int(int, int)> func1 = bind(&AddClass::Addii,AddClass(), placeholders::_1, placeholders::_2); cout << func1(100, 200) << endl;//输出300 // 1个参数 function<int(int)> func2 = bind(&AddClass::Addii, AddClass(), 10, placeholders::_1); cout << func1(100, 200) << endl;//输出210