中国最大网站建设商,官方微信公众号,江门市蓬江区最新发布,网站建设公司没有业务C特性 1. 列表初始化1.1 {}初始化1.2 initializer_list 2. 声明2.1 auto2.2 typeid2.3 decltype2.4 nullptr 3. STL3.1 新容器3.2 新接口 4. 右值引用5. 移动构造与移动赋值6. lambda表达式7. 可变参数模板8. 包装器9. bind 1. 列表初始化
1.1 {}初始化
C11支持所有内置类型和… C特性 1. 列表初始化1.1 {}初始化1.2 initializer_list 2. 声明2.1 auto2.2 typeid2.3 decltype2.4 nullptr 3. STL3.1 新容器3.2 新接口 4. 右值引用5. 移动构造与移动赋值6. lambda表达式7. 可变参数模板8. 包装器9. bind 1. 列表初始化
1.1 {}初始化
C11支持所有内置类型和自定义类型使用{}初始化并且可以不写。
struct A
{A(int a, int b):_a(a),_b(b){}int _a;int _b;
};
int main()
{int a 0;int b { 1 };int c{ 2 };int d[] { 1,2,3,4 };int e[]{ 5,6,7,8 };//本质都是调用构造函数多参数的构造函数支持隐式类型转换。A aa(1, 2);A bb { 3,4 };A cc{ 5,6 };return 0;
}但为了一定的可读性在日常定义中还是不要省略。
问题
vectorint v {1,2};这是{}初始化吗显然不是这是vector的构造。那为什么vector支持{}这种构造这与initializer_list有关。
1.2 initializer_list C11新增一个类initializer_list它用常量数组来初始化。此类型的对象由编译器根据初始化列表声明自动构建初始化列表声明是一个用逗号分隔的元素列表用大括号括起来initializer_list int il { 1 , 2 }。本质还是调用initializer_list的构造函数。 C11后一些容器增加了initializer_list为参数的构造这样初始化容器更方便如 这也是为什么vector支持{1,2}直接构造。因为vector有一个构造支持initializer_list构造所以v调用的是initializer_list的构造。 2. 声明
2.1 auto
auto可以实现自动类型推断这就意味着auto定义的变量必须初始化。
2.2 typeid
typeid(变量).name();//得到变量类型但只是字符串只能看不能用。2.3 decltype
当想单纯定义一个变量而不想初始化时就可以用decltype。decltype可以推导出对象或者表达式的类型可以再定义变量。 综上 typeid推出的类型是字符串只能看不能用decltype可以推出对象的类型可以再定义变量或者作为模板实参auto通过赋值自动推到类型。
2.4 nullptr
C中NULL既可以是0也可以是空指针。 func调用的是第一个func在预处理阶段被替换成0。 为了解决这个问题C11新增了nullptr表示空指针。 3. STL
3.1 新容器 array静态数组 int main()
{int arr1[10] { 0,1,2,3,4,5,6,7,8,9 };arrayint, 10 arr2 { 0,1,2,3,4,5,6,7,8,9 };//数组和静态数组的真正区别在于对越界的处理arr1[12] 0;//指针解引用arr2[12] 0;//调用operator[]函数内部会检查会报错
}同样是会对越界报错vector也可以进行扩容所以这个array有点鸡肋。 forward_list单链表 forward_list支持在任何位置的下一个节点的插入和删除因为它是单链表不如list有前后指针。这也是它唯一的优点节省一个指针。 unordered_set和unordered_map已经介绍过这里就不赘述。
3.2 新接口
迭代器 const修饰的正向迭代器和反向迭代器但是普通迭代器也可以返回const修饰的迭代器所以这几个接口还是用处不大。
所有容器均支持{}列表初始化的构造函数。所有容器均新增了emplace系列。新容器增加了移动构造和移动赋值。 4. 右值引用
什么是左值引用什么是右值引用
可以获得它的地址就是左值不可以获得它的地址就是右值。左值引用就是给左值取别名右值引用就是给右值取别名。左值可以在等号左边也可以在等号右边右值不可以在等号左边。
int main()
{//abc都是左值int* a new int(0);int b 1;const int c 2;//以下就是右值10;//常量b c;//表达式add(b , c);//函数返回值不是左值引用返回//左值引用int r1 b;//右值引用int r2 10;int r3 b c;return 0;
}拓展 内置类型的右值叫做纯右值自定义类型的右值叫做将亡值生命周期要结束。
左值引用能否给右值取别名右值引用能否给左值取别名
int main()
{//int r1 10;//(×)const int r1 10;//(√)double x 0.1;double y 0.2;const double r2 x y;//(√)//左值引用可以给右值取别名但必须constint a 1;//int r3 a;//(×)int r3 move(a);//(√)//右值引用可以给左值取别名但必须move
}拓展
//重载和调用歧义问题
void func1(int x)
{cout void func1(int x) endl;
}
void func1(int x)
{cout void func1(int x) endl;
}
void func2(const int x)
{cout void func2(const int x) endl;
}
void func2(int x)
{cout void func2(int x) endl;
}
int main()
{int a 1;func1(a);func1(1);int b 2;func2(b);func2(a b);
}func1构成重载func2也构成重载且不存在调用歧义编译器会调用更匹配的有右值引用就会调用右值引用版本。
左值引用的应用和缺点
左值引用可以用来做参数和返回值可以减少拷贝提高效率。
string func(string s)
{for (auto e : s){e;}return s;
}
int main()
{string str(1234);string ret func(str);cout str endl;
}但如果func返回的是一个局部变量出了作用域就销毁就不能左值引用返回只能传值返回这样就得调用拷贝构造。如何减少拷贝用右值引用。
右值引用的应用
1以一个简洁的string库的string太复杂了为例
namespace zn
{class string{public:typedef char* iterator;iterator begin(){return _str;}iterator end(){return _str _size;}string(const char* str ):_size(strlen(str)), _capacity(_size){//cout string(char* str) endl;_str new char[_capacity 1];strcpy(_str, str);}// s1.swap(s2)void swap(string s){::swap(_str, s._str);::swap(_size, s._size);::swap(_capacity, s._capacity);}// 拷贝构造string(const string s):_str(nullptr){cout string(const string s) -- 深拷贝 endl;string tmp(s._str);swap(tmp);}// 赋值重载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];}void reserve(size_t n){if (n _capacity){char* tmp new char[n 1];strcpy(tmp, _str);delete[] _str;_str tmp;_capacity n;}}void push_back(char ch){if (_size _capacity){size_t newcapacity _capacity 0 ? 4 : _capacity * 2;reserve(newcapacity);}_str[_size] ch;_size;_str[_size] \0;}//string operator(char ch)string operator(char ch){push_back(ch);return *this;}const char* c_str() const{return _str;}private:char* _str;size_t _size;size_t _capacity; // 不包含最后做标识的\0};
}
zn::string func()
{zn::string ret(nihao);return ret;
}
int main()
{zn::string str;str func();return 0;
}移动赋值 func函数传值返回返回的ret是右值将亡值生命周期结束所以可以重载赋值用右值引用接受参数。
// 赋值重载
string operator(const string s)
{cout string operator(string s) -- 深拷贝 endl;string tmp(s);swap(tmp);return *this;
}
// 移动赋值
// 为什么叫移动赋值因为是将别人的资源转移过来同时将自己的资源转移过去。
string operator(string s)
{cout string operator(string s) -- 移动赋值 endl;swap(s);return *this;
}func如果返回左值就调用赋值如果返回右值就调用移动赋值。移动拷贝交换了资源旧资源会被将亡值释放减少了拷贝提高了效率。 移动拷贝
// 拷贝构造
string(const string s):_str(nullptr)
{cout string(const string s) -- 深拷贝 endl;string tmp(s._str);swap(tmp);
}a. 拷贝构造 在拷贝构造的情况下如果编译器优化就只有一次深拷贝如果不优化就有两次深拷贝。 如果想要再减少拷贝次数提高效率可以使用移动构造来实现。 b. 移动构造
// 移动构造
string(string s):_str(nullptr), _size(0), _capacity(0)
{cout string(string s) -- 移动语义 endl;swap(s);
}编译器优化后直接交换将亡值的内容让它顺便释放旧的资源。以上的优化都是基于传值返回如果函数是传引用返回就没有这些优化。 既然有了移动拷贝我们再来看看移动赋值的例子发现它还可以再优化。
2场景二move函数将左值变成右值
int main()
{zn::string str(nihao);zn::string str2(str);
}str是左值str2会调用拷贝构造。但我想要让其调用移动构造抢走str的资源该如何做用move修饰str2。
int main()
{zn::string str(nihao);zn::string str2(move(str));
}str被move强制转换成右值右值调用移动构造。 3场景三STL容器插入接口函数也增加了右值引用版本容器的插入接口如果插入对象是右值可以利用移动构造转移资源给数据结构中的对象也可以减少。
完美转发
引用类型的唯一作用就是限制了接收的类型后续使用中都退化成了左值。不理解这句话没关系先看例子。 首先介绍下什么是万能引用模板中的不是接收右值的意思其代表万能引用既可以接收右值也可以接收左值。实参是左值它就是左值引用实参是右值它就是右值引用。 例子
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; }
templatetypename T
void PerfectForward(T t)
{Fun(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;
}为什么会出现这种情况我们平时都忽略了右值的另一种特征不能修改。既然右值不可修改那么在移动构造和移动赋值的时候是如何交换资源的答案是在用右值引用接收时右值的属性就发生了变化它可以被修改所以此时的右值是左值属性的。 rr能取地址且地址和r的地址相同但rr却是右值引用对右值取别名。原因是右值引用变量的属性会被编译器识别成左值。 回到例子Func接收的参数虽然是右值但它的属性早已变成左值属性所以调用的函数是左值引用。 结论 由此我们可以得出结论模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力但是引用类型的唯一作用就是限制了接收的类型后续使用中都退化成了左值。 问题 那如何保持住右值的右值属性那就得用到完美转发。 完美转发 forward完美转发在对象的传参过程保持对象原生类型的属性。
// std::forwardT(t)在传参的过程中保持了t的原生类型属性。
templatetypename T
void PerfectForward(T t)
{Fun(std::forwardT(t));
}在传参过程中保持t的属性t是左值引用就保持左值属性t是右值属性就保持右值属性。 应用场景 在一些容器的插入中我们通常需要保持插入数据的右值属性这样就可以调用移动构造和移动赋值或者就不用深拷贝就可以减少拷贝提高效率。
templateclass T
class List
{//...void PushBack(T x){//Insert(_head, x);Insert(_head, std::forwardT(x));}void Insert(Node* pos, T x){Node* prev pos-_prev;Node* newnode new Node;newnode-_data std::forwardT(x); // 关键位置// prev newnode posprev-_next newnode;newnode-_prev prev;newnode-_next pos;pos-_prev newnode;}void Insert(Node* pos,const T x){//……}
} 5. 移动构造与移动赋值
C11新增了两个默认成员函数移动构造和移动赋值。在上面我们已经了解到它们的原理现在来了解它们的一些细节。
深拷贝的类需要实现自己实现移动构造和移动赋值浅拷贝的类就不需要自己实现。如果没有自己实现移动构造和移动赋值且没有实现析构、拷贝构造、赋值重载中任一个编译器就会默认生成移动构造和移动赋值。默认生成的移动构造和移动赋值对于内置类型实现值拷贝对于自定义类型如果有移动构造和移动赋值就调用其移动 构造和移动赋值没有就调用拷贝构造和赋值重载。一个类要写这三个函数析构函数 、拷贝构造、拷贝赋值重载说明这个类是深拷贝的类这三个函数是三位一体的通常一起实现。如果没有实现这三个说明是浅拷贝的类那就用编译器默认生成的析构、拷贝构造、赋值重载和移动构造和移动赋值如果手动实现这三个说明是深拷贝的类就需要自己手动实现移动构造和移动赋值。强制生成成员函数可以用default修饰。
string(string p) default;6. lambda表达式
为什么会有lambda表达式
如果我们要对自定义类型进行排序我们可以根据自定义类型的成员进行排序且可以排升序也可以排降序。
struct book
{string _name;int _id;double _price;
};
struct CompareByIdGreater
{bool operator()(const book b1, const book b2){return b1._id b2._id;}
};
struct CompareByIdLess
{bool operator()(const book b1, const book b2){return b1._id b2._id;}
};
int main()
{vectorbook lib { {活着,1,30.4},{皮囊,2,24.9},{我与地坛,3,33.5}};//要求对书按照_id进行排升序需要写仿函数sort(lib.begin(), lib.end(), CompareByIdGreater());//要求对书按照_id进行排降序需要写仿函数sort(lib.begin(), lib.end(), CompareByIdLess());//要求对书按照价格进行排降序需要写仿函数//……//要求对书按照价格进行排升序需要写仿函数//……//要求对书按照书名进行排降序需要写仿函数//……//……return 0;
}对一个自定义类型的各个成员排序得专门写多个类进行排序而且还得排升序和降序还得再实现多个类。这样实现太复杂了所以就有lambda表达式。
//按照_id进行排降序
sort(lib.begin(), lib.end(), [](const book b1, const book b2) {return b1._id b2._id; });
//按照_id进行排升序
sort(lib.begin(), lib.end(), [](const book b1, const book b2) {return b1._id b2._id; });
//按照价格进行排降序
sort(lib.begin(), lib.end(), [](const book b1, const book b2) {return b1._price b2._price; });
//按照价格进行排升序
sort(lib.begin(), lib.end(), [](const book b1, const book b2) {return b1._price b2._price; });lambda表达式怎么用 捕捉列表
1[ var ]传值捕捉变量var。因为是传值捕捉捕捉列表中的var是外面var的拷贝又因为lambda函数是const函数捕捉过来的变量不能修改如果想要修改必须mutable但修改的也是var的拷贝。 2[ var ]传引用捕捉变量var。 3[ ]传值捕捉父作用域所有的变量包括this。这里的父作用域是指包含lambda表达式的代码块。 4[ ]传引用捕捉父作用域所有的变量包括this。 5在捕捉列表中可以多次捕捉然后用逗号分隔开。[ ,var ]传值捕捉var传引用捕捉其他变量。[ ,var ]传引用捕捉var传值捕捉其他变量。不能重复捕捉如[ ,var ]。 6lambda表达式只能捕捉父作用域的局部变量不能捕捉父作用域以外的变量。
例子以及一些重要事项
int Add(int x, int y)
{return x y;
}
int main()
{//1.lambda表达式是匿名函数对象和函数指针、仿函数一样都可以作为对象使用。auto add [](int x, int y)-int {return x y; };cout add(1, 1) endl;cout endl;//2.函数体内可以有多条语句auto swap [](int x, int y) {int temp x;x y;y temp;};//3.在lambda函数体内不能调用局部函数如add但可以调用全局函数。//如果想要调用局部变量就用捕捉列表捕捉。//auto func1 [](int x, int y) {return add(x, y); }; ×auto func1 [](int x, int y) {return Add(x, y); };//auto func1 [ ](int x, int y) {return add(x, y); }; √cout func1(1, 2) endl;cout endl;//4.捕捉列表int a 1;int b 2;auto func2 [a, b]() { return a b; };cout func2() endl;auto func3 [a, b]()mutable {a;b;return a b; };cout a b func3() endl;cout a: a b: b endl;//5.最简单的lambda表达式没有任何意义[] {};
}结果
lambda表达式的原理
在前面我们已经发现lambda表达式与仿函数的使用方法一样现在就来看看它与仿函数的关系。
class CompareGreater
{
public:bool operator()(const int x, const int y){return x y;}
};
int main()
{CompareGreater cg;//cg是仿函数也叫函数对象本质是类对象通过重载()可以像函数一样使用int x 1;int y 2;int ret1 cg(x, y);auto func [](int x, int y) {return x y; };//lambda表达式int ret2 func(x, y);
}查看lambda表达式的底层代码后发现有一个类调用operator()。 也就是说lambda表达式的底层其实是仿函数编译器在我们定义lambda表达式时自动生成一个类并且重载了operator()。 问题 如果lambda表达式相同编译器生成的类是否相同 如图由相同的lambda表达式生成的类是不同的因为后面的那串字符串。圈起来的字符串是UUID通用唯一识别码每个lambda都会自动生成且通过UUID被识别为不同的类。 7. 可变参数模板
在以前C只支持固定数目的模板参数C11有了可变参数模板就可以实现类模板参数和函数参数的传参自由想传多少就传多少。
//1.Args/args是参数的意思。
//2.Args是模板参数包args是函数参数包
//3.参数包内可以有任意个参数0到N
//4.第一个参数会被T提取剩余的参数会形成参数包没有T也是可以的
templateclass T,class ...Args
void Print(T value,Args ...args)//利用模板参数包定义一个函数参数包
{cout value endl;
}那如何将参数包内的参数解析出来。args本质是将参数放入一个数组但不能通过args[i]提取。
templateclass T, class ...Args
void Print(T value, Args ...args)
{cout value endl;//这个写法很奇怪但是它的作用是获得参数包的参数个数Print传4个实参一个被value接收剩下三个放到参数包中cout sizeof...(args) endl;for (size_t i 0; i sizeof...(args); i){cout args[i] endl;}}
int main()
{Print(1,3.14,x,nihao);
}但是我们发现好像不能直接获得参数包的参数。
1通过递归方式展开参数包
templateclass T
void Print(T value)
{//这个函数的作用是接受参数包的最后一个参数同时作为递归结束条件。cout value endl;
}
templateclass T, class ...Args//加一个参数T通过T将参数包的参数一个一个取出来
void Print(T value, Args ...args)
{cout value ;Print(args...);//这个传参也很奇怪
}
int main()
{Print(1,3.14,x,nihao);
}如果不传参数该怎么办
//不传参数就直接调用这个函数同时如果参数包参数个数为0也会调用这个函数作为递归结束的标志
void Print()
{cout endl;
}
templateclass T, class ...Args
void Print(T value, Args ...args)
{cout value ;Print(args...);
}
int main()
{Print();Print(1,3.14,x,nihao);
}2逗号表达式 这是在网上看到的大佬的写法
void Print()//应付不传参数的情况
{cout endl;
}
templateclass T
int _Print(T value)
{cout value ;return 0;//为什么要返回0用来给数组初始化。
}
templateclass ...Args
void Print(Args ...args)
{int arr[] { _Print(args)... };// []没有给定大小编译器就会去推到底有多少个就会展开...参数包的剩余参数// 例如有两个参数就会展开为{(_Print(args,0),(_Print(args),0)}展开后就会调用_Print。cout endl;
}
int main()
{Print();Print(1,3.14,x,nihao);
}emplace和emplace_back就应用到参数包 以list为例这两个函数的作用都是构造插入元素。
int main()
{list pairint, zn::string lt;lt.emplace_back(10, sort);lt.emplace_back(make_pair(20, sort));lt.push_back(make_pair(30, sort));lt.push_back({ 40, sort });return 0;
}8. 包装器 什么是包装器function是一个类模板实例化出的对象可以用来包装可调用对象函数指针、仿函数、lambda表达式。 为什么要有包装器它是如何进行包装的
//包装器
//f是可调用对象可以是函数指针可以是仿函数也可以是lambda表达式
templateclass F,class T
void func(F f,T x,T y)
{static int count 0;//用来标记func是否有实例化多份cout count endl;f(x, y);
}
//函数
int Add1(int x, int y)
{return x y;
}
//仿函数
struct Add2
{int operator()(int x, int y){return x y;}
};
int main()
{//lamda表达式auto Add3 [](int x, int y) {return x y; };//函数指针func(Add1, 1, 1);//仿函数func(Add2(), 1, 1);//lambda表达式func(Add3, 1, 1);cout endl;
};func会被实例化出3份效率较低。如果我只想实例化一份func该怎么办用到function。 functionint(int, int) add1 Add1;functionint(int, int) add2 Add2();functionint(int, int) add3 Add3;func(add1, 2, 2);func(add2, 2, 2);func(add3, 2, 2);如图func被实例化成一份因为是传参是传包装器。 9. bind
band绑定是一个函数模板是一个函数适配器。它接收可调用对象和可变参数返回可调用对象。它可以用来调整函数的参数顺序和增加函数参数等。 一般格式
auto newCallable bind(callable,arg_list);newCallable是适配出的可调用对象它的参数绑定了callable的参数在arg_list中newCallable的占位符绑定了callable的参数。这个格式看不出什么直接看例子。
例子bind的作用
1调整函数参数顺序 2减少函数参数 当一个函数有多个参数且一些参数是固定的这时我们就可以把这些参数绑死适配出少参数的可调用对象。
//例如c不变我们在绑定时就可以把c绑死
int func(int a, int b, int c)
{return a - b c;
}
int main()
{functionint(int, int) f1 bind(func, placeholders::_1, placeholders::_2, 3);cout f1(1,2) endl;//还可以调整参数顺序functionint(int, int) f2 bind(func, placeholders::_2, placeholders::_1, 3);cout f2(1, 2) endl;
}3绑定成员函数
class Sub
{
public:static int sub(int a, int b){return a - b;}int ssub(int a, int b){return a - b;}
};
int main()
{// 绑定静态成员函数需要指定类域functionint(int, int) func3 bind(Sub::sub,placeholders::_1, placeholders::_2);// 绑定成员函数需要一个具体的可调用对象需要创建一个类对象Sub s;//functionint(int, int) func4 bind(Sub::ssub, placeholders::_1, placeholders::_2);//(×)functionint(int, int) func4 bind(Sub::ssub, s , placeholders::_1, placeholders::_2);functionint(int, int) func5 bind(Sub::ssub, Sub(), placeholders::_1, placeholders::_2);
}