学生怎样建设网站,建设国外网站引流吗,两个wordpress,标志与设计C11
统一的列表初始化 在介绍这里的列表初始化之前#xff0c;首先我认为这是一个比较鸡肋的功能特性#xff0c;而且使用起来会和之前C98时有些区别。 // 首先可以定义普通的内置类型的变量int x1 1;int x2 { 1 };int x3{ 1 }; // 这样看起来着实有些怪int arry1[] { 1,…C11
统一的列表初始化 在介绍这里的列表初始化之前首先我认为这是一个比较鸡肋的功能特性而且使用起来会和之前C98时有些区别。 // 首先可以定义普通的内置类型的变量int x1 1;int x2 { 1 };int x3{ 1 }; // 这样看起来着实有些怪int arry1[] { 1,2,3,4,5 };int arry2[]{ 1,2,3,4,5 }; // 数组还可以这样定义如果是自定义类型定义可以如下
struct Point
{int _x;int _y;
};
int main()
{Point p1 { 1,2 };Point p2{ 2,2 };
}上面的定义更多看起来兼容了C语言如果有一个类成员为私有成员并且有自己的构造函数那么这个时候使用花括号其实就是在调用这个类的构造函数 class Date
{
private:int _hour;int _minute;int _second;
public:Date(int x 0, int y 0, int z 0):_hour(x),_minute(y),_second(z){}void Print(){cout _hour _minute _second;}
};
int main()
{Date d1{ 10,20,30 };d1.Print();return 0;
}在定义vector的时候可以如下定义
vectorint v1 { 1,2,3,4 };
vectorint v2{ 1,2,3,4,5,6,7 };但是我们之前在学习vector的时候并没有看到过类似第二行的初始化方式所以这里C11是如何实现这种方式的
C11为容器提供了一个初始化的方式initializer_list它作为构造函数的参数C11为STL中的不少容器添加了initializer_list作为参数的构造函数初始化对象就方便了很多。 initializer_list的成员函数中begin返回指向第一个位置的指针end返回指向最后一个位置的下一个位置的指针(nullptr)。具体来说就是initializer_list封装了一个常量数组然后支持迭代器返回数组的begin和end以方便对其进行遍历。
decltype
之前我们使用typeid可以拿到元素的类型的字符串但是这似乎是一个很鸡肋的功能因为它除了能打印什么都做不了。 auto il { 1,2,3,4,5 };cout typeid(il).name() endl;但是decltype的作用就不一样了可以参考下面这个例子
// decltype的一些使用场景
templateclass T1, class T2
void F(T1 t1, T2 t2)
{decltype(t1 * t2) ret t1 * t2;cout typeid(ret).name() endl;
}
void Func()
{F(1.2, 2);
}最后打印的结果是double 其次这里提一下nullptr和范围for都是C11新增的内容nullptr解决了之前NULL被定义成字面量0的问题范围for想必大家都很熟悉了这里就不多赘述了。 STL中的一些变化
C11在STL中添加了一些新容器并且对已有容器增加了一些新的接口函数。
除掉我们熟知的unordered_map和unordered_set有两个容器这里需要介绍一下array和forward_list array是一个固定大小的容器和vector相比vector是动态的数组。array对比的主要是静态数组那么与静态数组相比优势在哪儿呢 之前我们知道系统对普通数组的越界检查是一种抽查只能检查到临界位置的一些元素是否有越界问题array这里就对这个问题做出了很好的解决但是它任然是一个很鸡肋的容器并不会对容器中的元素进行初始化。 forward_list容器是一个单链表容器并没有提供尾插和尾删因为找尾的时间复杂度是很高的对比list唯一的优点就是每一个节点节省一个指针但是用起来真的差list好远 在vector容器中增加了cbegin和cend以及shrink_to_fit三个接口cbegin和cend返回const迭代器二shrink_to_fit是一个缩容接口一般情况都是异地缩容一般不要使用这个接口因为这个接口的使用会导致拷贝。在容器中还提供了许多右值引用相关的接口之后再谈。
在我之前的博客中继承和多态部分提到的final和override也是C11新增的部分如果一个虚函数不想被重写可以加final修饰也可以修饰一个类这个类叫最终类override是一个检查子类虚函数是否完成重写
右值引用 在引入右值引用之前先来谈一下什么是左值什么是右值 左值是一个数据表达式说人话就是可以获取它的地址也可以对其进行赋值左值是可以出现在复制符号的右边的但是右值不可以出现在赋值符号的左边 左值引用就是给左值起别名,左值引用也可以引用右值但是必须是const左值引用。 右值也是一个数据表达式如字面常量表达式返回值函数返回值传值返回等右值不能取地址 右值引用不可以引用左值但是如果**std::move(左值)**就可以被右值引用所引用 int main()
{double x 1.1, y 2.2;// 常见的右值10;x y;fmin(x, y);// 对右值的右值引用int rr1 10;double rr2 x y;double rr3 fmin(x, y);
}所以右值引用究竟有什么用呢和const左值引用有什么区别 引用出现的目的就是为了减少拷贝主要使用在函数传参我们之前的学习中使用引用传参是非常舒服的。但是如果我们想要传递一个参数作为返回值就有些麻烦了在栈上创建的内存出栈后会被销毁如果在堆上开辟空间又需要对空间进行回收在C11之前解决问题的方法就是输出型参数在传递进去的参数就是要改变的变量的引用或地址但是用起来总感觉怪怪的所以今天的右值引用就很好的解决了这个问题。
templateclass T
T func(const T x)
{T ret;// ...return ret;
}如果类似上面的这种写法是会报错的ret出了函数任然会被销毁来看下面这个例子
string to_string(int value)
{bool flag true;if (value 0){flag false;value 0 - value;}string str;while (value 0){int x value % 10;value / 10;str (0 x);}if (flag false)str -;std::reverse(str.begin(), str.end());return str;
}
int main()
{string str to_string(1234);return 0;
}这是一个传值返回的函数这里会造成两次拷贝构造编译器经过优化后只有一次拷贝构造.
在传递参数返回值的时候如果返回值不是很大会将返回值经过一次拷贝后返回寄存器中如果返回值比较大在main函数的函数栈帧边缘上开辟一段空间进行存放。
编译器在类似上面的这种写法可以直接进行优化只进行一次构造不会生成中间变量。
但是如果这么写呢
int main()
{string str;str to_string(1234);return 0;
}这样写VS编译器就不会再做出优化了产生临时变量进行两次拷贝构造。
C11对右值又分为纯右值和将亡值将亡值顾名思义就是即将要被销毁的值上面函数中我们所在函数栈帧中创建的变量str就算有编译器优化也需要最少一次拷贝构造那么右值引用的意义就是为了减少拷贝构造所带来的消耗。
在拷贝构造部分提供一个右值引用的版本
// 移动构造
string(string s):_str(nullptr)
{cout string(const string s) -- 移动拷贝 endl;swap(s);
}
// 拷贝构造
string(const string s):_str(nullptr),_capacity(0),_size(0)
{cout string(const string s) -- 深拷贝 endl; string tmp(s._str);swap(tmp);
}通过代码区分一下移动拷贝和拷贝构造移动构造将this指向的对象和s进行交换可以理解为函数栈帧中创建的且要返回的值是一个将亡值所以它匹配移动构造函数进行资源转移。 那么在有了移动构造之后编译器在没有进行优化时就会进行一次拷贝构造和一次移动构造在优化之后会直接移动构造并且将返回值直接识别为将亡值这一切都是编译器做的。 赋值运算符重载也是一样的除了实现拷贝复制还可以实现移动赋值
// 拷贝赋值
string operator(const string s1)
{cout string operator (string s) -- 拷贝赋值 endl;if (this s1){return *this;}delete[] _str;_str new char[s1._capacity 1];strcpy(_str, s1._str);_capacity s1._capacity;_size _capacity;return *this;
}
// 移动赋值
string operator(string s)
{cout string operator (string s) -- 移动赋值 endl;swap(s);return *this;
}所以移动构造的使用几乎是我们察觉不到的在设计类的时候设计对应的函数即可编译器会帮我们做好这一切相关的工作的。右值引用和左值引用减少拷贝的原理还不太一样左值引用是取别名直接起作用右值引用是间接起作用实现移动构造和移动赋值在拷贝的场景中如果是右值(将亡值)转移资源。
另外就是右值的插入如果要插入一个匿名对象在没有移动拷贝的时候进行的就是深拷贝有移动拷贝的情况下就节省了空间。
右值是不能取地址的但是给右值取别名后会导致右值被存储到特定的位置且可以取到该位置地址比如不能取字面常量10的地址但是字面常量被rr1引用后可以对rr1取地址也可以修改rr1可以理解为rr1指向的不是10本身修改rr1并不会影响10这个字面常量用const修饰rr1就可以防止其被修改 右值引用本身是左值。我们之前模拟实现过的list如果也想支持C11就要注意一个问题当参数作为右值引用传入进来进行资源转移后再将参数继续传下去在函数中继续使用该参数调用下一个函数就无法再保持参数的右值属性了 看下面这个例子
// 万能引用
templatetypename T
void func1(T t)
{cout func1(T t) endl;
}
void func2(int x)
{cout func2(int) endl;
}
void func3(int x)
{cout func3(int) endl;
}
int main()
{int x 10;func2(x);// func3(x);(会编译报错右值引用无法引用一个左值)func3(10);func1(x);func1(10);return 0;
}通过上面的代码我们可以发现在普通函数的调用中右值引用是不可以传递一个左值进去的但是在模板函数中是可以这么做的这里能实现是发生了引用折叠-更离谱的是这里还可以传递const左值和const右值但是带const的传值是不可以对参数进行修改的。
右值引用在一些情况下会有一些问题
void func(int x)
{ cout func(int) endl; }
void func(const int x)
{ cout func(const int) endl; }void func(int x)
{ cout func(int) endl; }
void func(const int x)
{ cout funcconst int) endl; }templatetypename T
void func1(T t)
{func(t);
}
int main()
{func1(10);int a 0;func1(a);func1(move(a));const int b 9;func1(b);func1(move(b));return 0;
}最后运行的结果如下 可以理解所有的参数无论是左值还是右值在传入func1函数后t实际上有左值属性所以在掉func函数的时候就相当于左值调用所以才有上面的打印。
这里就需要认识一个新的名词完美转发
templatetypename T
void func1(T t)
{// 完美转发 保持属性func(std::forwardT(t));
}在更改过代码后再运行试试 类的变化
移动构造和移动赋值
在上面我们提到了基于右值的移动构造和移动赋值C11中提供了默认生成的移动构造和移动赋值但是有一些需要注意的点 如果没有自己实现移动构造函数且没有实现析构函数拷贝构造拷贝赋值重载中的任意一个那么编译器就会自动生成一个默认移动构造。默认生成的移动构造函数对于内置类型成员会执行逐成员按字节拷贝自定义类型成员需要看这个成员是否实现移动构造如果实现了就调用移动构造没有实现就调用拷贝构造。 默认移动赋值和上面的移动构造完全类似 如果提供了移动构造和移动赋值编译器不会自动提供拷贝构造和拷贝赋值 类成员变量初始化
C11允许类在定义时给成员变量初始缺省值默认构造函数会使用这些缺省值初始化
default和delete关键字
默认生成的函数是有条件的有些情况下函数可能会因为某些原因没有默认生成比如提供了拷贝构造就不会生成移动构造了那么我们可以使用default关键字显示指定移动构造生成。
class Person
{public:Person(const char* name , int age 0):_name(name),_age(age){}Person(const Person x):_name(x._name())_age(x._age){}Person(Person p) default;private:mystring::string _name;int _age;
};delete关键字和default关键字正好相反delete可以禁止生成默认函数用法也是相似的。
lambda表达式 首先来说一下lambda表达式的应用场景C为了减少使用函数指针的使用函数指针确实是一个很烦人的东西就有了仿函数之后C11认为仿函数也不是那么好用于是就有了lambda表达式lambda本质就是一个可调用的对象之后我也会多次提到。 lambda书写表达式[capture-list] (parameters) mutable - return-type { statement }
这个书写表达式一看一脸懵第一部分[capture-list]是必须要写的第二部分是参数 (parameters) 没有参数可以不写第三部分 mutable 一般不需要第四部分是返回值 - return-type 一般都不写会自动推导最后一个部分是函数体必须要写 { statement }
// 进行int对象比较的lambdaauto compare [](int x, int y)-bool {return x y; };
// 也可以写成下面这样auto compare [](int x, int y){return x y};lambda本质是一个可调用的对象返回值auto是因为返回值是编译器生成的我们不知道返回值究竟是什么。
前面我只介绍了lambda表达式的语法组成但是各个部分究竟怎么用 auto compare [](int x, int y)-bool {return x y; };compare可以看作是一个对象这里可以给compare传递两个变量来进行大小的判断那么已有变量可不可以不进行传递直接使用呢
那就提到第一个部分[capture-list]捕捉列表编译器根据 [] 这个符号来判断接下来的代码是否为lambda函数同时捕捉列表可以捕捉上下文中的变量供lambda函数使用。如何使用
int a 0;
int b 1;
auto add2 [b](int x) {return x b;};
cout add2(a);上面的代码中在捕捉列表中捕捉变量b参数传递就可以只传一个。但是这里不能传递add2函数之后所创建变量。 第二条中[]的父作用域是函数体外的变量由于编译器由上到下扫描所以这里父作用域中的变量是不包括lambda函数之后所创建的变量的。
如果这里想要写一个lambda函数来交换两个变量怎么做
int a 0;
int b 1;
auto swap [a, b]()
{int tmp a;a b;b tmp;
};这里是无法编译通过的捕捉列表中的的变量本质上是拷贝过来的并且增加了const属性这也就是第三部分mutable的作用 编译并未报错但是没有真的交换变量所以验证了捕捉列表中的变量是拷贝。 将捕捉列表中的值改为引用就可以对变量进行交换了。 之前提到了lambda表达式本质上是实现了一个对象这里我们对lambda表达式进行底层的分析 在底层中编译器会自动帮我们生成一个类call部分就是定义构造函数可以发现构造了一个什么类是编译器自己决定的这也就是为什么返回值中我们要使用auto。
可变参数模板
在C11中引入了一个新的特性可变参数模板
在C语言中就有可变参数printf函数第一个参数为format第二个参数就是可变参数也就是说不确定有几个参数。 template class ...Args
void ShowList(Args... args)
{}Args是一个模板参数包args是一个函数形参参数包在STL的很多容器中也支持插入参数包 看一下可变参数模板的使用
void ShowList()
{cout endl;
}
// args参数包可以接收0-N个参数
template class T, class ...Args
void ShowList(T val, Args... args)
{cout val ;ShowList(args...);
}int main()
{ShowList(1);ShowList(1, 1.1);ShowList(1, 1.1, string(xxxxxx));return 0;
}上面的代码中ShowList函数中还有一个参数val他的类型是T也就将参数包中的参数一个一个传递给val然后再执行函数最后提供一个没有参数的ShowList函数。 上面是C容器和线程库中的一些函数对可变参数模板的使用可变参数模板的底层晦涩难懂但是用起来还是很好的。
这里举一个list的例子
listint list1;
list1.push_back(1);
list1.emplace_back(2);
list1.emplace_back();
for (auto e : list1)
{cout e ;
}
cout endl;上面的代码中list调用emplace_back和调用普通的push_back没有区别当调用emplace_back的时候如果没有传递参数默认构造一个值为0的对象。
listpairint, char mylist;
mylist.push_back(make_pair(1, a));
mylist.emplace_back(2, b);如果list中的元素是pair类型的就可以不写make_pair而直接使用参数包讲参数写入在第二行中调用make_pair构造一个对象后再通过拷贝构造或移动构造将值插入到list中而调用emplace_back则是直接通过递归调用直接构造。 可以发现emplace_back对左值没有任何优化空间在实现了移动构造后。emplace_back减少的只是一次浅拷贝所以emplace_back并没有想象中那么高效但没有实现移动构造差距就很大了。
包装器 如果我们想要实现一个让两个int类型的值相加的函数那么实现的方法有很多种 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类型变量的相加但函数是静态的和一个double类型变量的相加不同的函数调用起来有不同的方法那么包装器的作用就是将这些不同类型的函数进行包装使其调用统一。 使用包装器需要添加头文件functional
#include iostream
#include functionalusing namespace std;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()
{functionint(int, int) func1(f);cout func1(1, 2);functionint(int, int) func2 Functor();cout func2(1, 2);functionint(int, int) func3 [](const int a, const int b) { return a b; };// 静态成员函数的包装functionint(int, int) func4 Plus::plusi; // 类成员函数functionint(Plus, int, int) f6 Plus::plusd;cout f6(Plus(), 1, 2) endl;Plus plus;functionint(int, int) f7 [plus](int x, int y)-int { return plus.plusd(x, y); };return 0;
}包装器的本质是一个类模板 Ret是被调用函数的返回类型Args…是被调用函数的形参知道这些相信大家对上面func1和func2都可以很好地理解func3说明包装器还可以包装lambda表达式 func4和func5是对类内成员的包装可以发现包装这些函数的时候和包装普通函数不太一样在赋值的时候需要指定类域如果这个函数是静态函数类域前可以加取地址符也可以不加但是如果不是静态函数必须加取地址符另外就是在参数部分要在第一个参数之前添加类域这个参数。在调用这些被包装的类内的函数的时候需要在调用的时候传递类对象因为调用类内的函数需要this指针 func7则是使用lambad的捕获列表对类对象进行捕获这就说明其实包装器本身更在意的是函数参数和返回值是否正确。 bind
在包装器部分还有一个知识点是绑定绑定可以交换传入的两个函数的参数其次绑定也可以固定绑定参数。
int Plus(int x, int y)
{return x - y;
}class Sub
{
public:int sub(int a, int b){return a b;}
};
int main()
{functionint(int, int) func1 bind(Plus, placeholders::_1, placeholders::_2);cout func1(1, 2) endl;// 可以调整参数位置functionint(int, int) func2 bind(Plus, placeholders::_2, placeholders::_1);cout func2(1, 2) endl;functionbool(int, int) func3 bind(lessint(), placeholders::_1, placeholders::_2);// 固定绑定参数functionint(Sub, int, int) func4 Sub::sub;cout func4(Sub(), 2, 4) endl;functionint(int, int) func5 bind(Sub::sub, Sub(), placeholders::_1, placeholders::_2);cout func5(10, 20) endl;return 0;
}可以将绑定函数看作是一个通用的函数适配器它接受一个可调用对象生成一个新的可调用对象来适应原对象的参数列表 在参数部分placeholders_1就是第一个参数placeholders_2就是第二个参数并以此类推。 固定绑定参数就例如上面代码中如果包装一个类中的对象就需要在调用的时候加一个对象通过对象去调用在bind绑定参数后可以直接进行调用。