C++从诞生到现在一直是一门主流的编程语言,期间经历了多次更行迭代,最近的一次大版本更新就是C++11,而现在大部分公司也把C++11作为主流的应用版本。有人说C++现在越来越不像C++了,一部分原因就是C++11更行了很多重要的新东西,当然也有部分是比较鸡肋的,所以今天就把最重要最常用的新特性给大家罗列出来,去讲解清楚。
在C++11出来之前,我们只见过也是最常用{}
初始化只是数组或者结构体,例如:
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出来之后可以用列表初始化各种类型,还可以不加=
int main()
{
//日期类
Date d1(2023, 1, 1);
Date d2 = { 2023,1,1 };
Date d3{2023,1,1};
int x1 = 1;
//可以用{}初始化变量或者数组,可以不加=
int x2 = { 2 };
int x3{ 3 };
int arr[]{ 1,2,3,4,5 };
int arr2[5]{ 0 };
vector<int> v1 = { 1,1,1,1 };
vector<int> v2{ 1,1,1,1 };
list<int> ls1 = { 1,2,3,4,5 };
list<int> ls2{ 1,2,3,4,5 };
map<int, int> m1 = { {1,1},{2,2} };
map<int, int> m2 { {1,1},{2,2} };
return 0;
}
为什么这么多的容器或者类型都支持列表初始化呢?是通过什么方式让内置、自定义类型和容器都支持列表初始化呢?
在C++11中这些容器的构造函数中,都支持了用initializer_list类型的构造函数,下面这是一部分,基本上所有的容器都支持了列表初始化。
这是一个很方便的语法特性,值得我们去学习使用。
C++11标准中新增了auto关键字,可以用于声明变量,其作用是自动推导变量的类型。
使用auto声明变量时,编译器会根据右边表达式的类型自动推断出变量的类型,并将其类型推导为所初始化的表达式的类型。例如:
auto i = 10; // 推导为 int 类型
auto d = 2.3; // 推导为 double 类型
auto s = "hello"; // 推导为 const char* 类型(C风格字符串)
需要注意的是,auto声明的变量必须要进行初始化,否则无法推导出变量的类型,编译会报错。
auto还可以与特定修饰符一起使用,如const、&和*等。例如:
const auto *p = &x; // 推导为const int*
auto &a = x; // 推导为int&
auto b = &x; // 推导为int*
因此,通过结合auto和各种修饰符,可以方便地定义各种类型的变量,从而简化代码,并提高程序的可读性和可维护性。
还可以验证上面的列表是否真的为initializer_list类型
注:typeid().name()是一个推导变量类型的函数。
C++11标准中新增了一个decltype操作符,可以用于在不需要实际执行表达式的情况下获取表达式的类型。
不过什么是右值,什么是左值?我们需要先把这个概念搞清楚。可能很多人觉得在赋值符号左边的就是左值,在赋值符号右边的就是右值,这种想法是错误的。
什么是左值?什么是左值引用?
左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址+可以对它赋值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名。
//我们之前学习的引用就是左值引用
int a=0;//a是左值
int* p = &a;//p也是左值,因为他们都是变量,都可以取到地址。
int& b = a;//左值引用
什么是右值?什么是右值引用?
右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。右值引用就是对右值的引用,给右值取别名。
int a = 1;
int b = 2;
//int& ret = (a+b);//这段代码是错误的,因为a+b是一个表达式,这个表达式会先产生一个临时对象,然后赋值给ret,这个临时对象具有常性,这是一种权限的放大,所以不能引用
const int& ret = (a+b);//加const就可以了,左值引用引用右值
int&& ret = (a+b);//右值引用右值
int&& ret1 = 10;
//那么右值能不能引用左值呢?
int a = 10;
//int&& ret = a; //这段代码是错误的,右值不能直接引用左值,但是move之后可以了
int&& ret = std::move(a);
//只有右值引用才能调用move函数,因为它是将对象的资源所有权转移给目标对象,而左值在转移资源所有权时,会使得源对象变为无效状态,这是不符合语义的。
//要保证目标对象有足够的空间来接收资源。如果目标对象的类型与源对象的类型不同,需要进行类型转换。
//所以move的使用要慎重
左值引用和右值引用的总结:
- 左值引用只能引用左值,不能引用右值。
- 但是const左值引用既可引用左值,也可引用右值
- 右值引用只能右值,不能直接引用左值。
- 但是右值引用可以move以后的左值,也可以通过模板来间接引用左值。
那么右值引用真正的意义是什么?
右值引用的真正意义是移动语义(资源的移动),利用临时对象或者表达式的特性,避免资源的重复分配或者拷贝,从而提高程序的执行效率。
右值分为:
例如:
string s1("hello");
string s2("world");
string ret1 = s1; //调用拷贝构造进行深拷贝
string ret2 = s1+s2; //这个表达式的返回值是一个右值,也是将亡值,因为s1+s2的结果会放到一个临时对象的空间内 然后深拷贝到ret2。
这个将亡值都要消失之前,还要进行一次拷贝,如果是自定义类型无伤大雅,如果是自定义类型就可能需要深拷贝,这是一种资源浪费。右值引用之后,就可以指向这个将亡值的空间,减少拷贝。
移动构造
而在C++11中STL所有容器用右值引用实现了移动构造,让自定义类型减少拷贝,提升效率。移动构造本质是将参数右值的资源窃取过来,占位已有,那么就不用做深拷贝了,就是窃取别人的资源来构造自己,所以它叫做移动构造。所以移动构造是针对自定义类型设计的,对内置类型意义不大。例如:
这里列举了一部分,那么移动构造怎么实现的呢?例如:
这个string类的代码不完整,主要是为了观察移动构造和拷贝构造的区别。
class string
{
public:
string(const char* str = "")
:_size(strlen(str))
, _capacity(_size)
{
_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(string&& s)
:_str(nullptr)
{
cout << "string(string&& s) -- 移动拷贝" << endl;
swap(s);
}
private:
char* _str;
size_t _size;
size_t _capacity; // 不包含最后做标识的\0
};
这个函数的返回值是一个string,当这个返回值返回结果时,会先构建一个临时对象空间,把str拷贝到临时空间,然后从临时空间拷贝到返回的ret变量中,但是新一点的编译器会做优化,把两次拷贝减少为一次,str直接拷贝给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 ret = to_string(12345);
return 0;
}
还有一个和移动构造作用一样的是移动赋值,移动赋值运算符重载和移动构造的关系就像 拷贝构造和拷贝赋值运算符重载的关系是一样的。
//以上面的string类为例
string& operator=(string&& str)
{
swap(str);
}
我们看一下下面这段代码,最终的输出结果是什么?可以认真思考一下。
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);
}
int main()
{
PerfectForward(100); //右值
int a;
PerfectForward(a); // 左值
PerfectForward(std::move(a)); // 右值
const int b = 8;
PerfectForward(b); // const 左值
PerfectForward(std::move(b)); // const 右值
return 0;
}
这是最终的输出结果,可能很多小伙伴有一些疑问,为什么右值可以引用左值,为什么都是左值引用?
答案是不会,因为通过模板可以引用折叠。
万能引用(引用折叠):既可以引用左值,也可以引用右值
左值和右值有一个本质的区别就是左值可以取地址,右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可
以取到该位置的地址
第一次传入100的时候,100是一个右值,但是通过右值引用之后,t就是一个有空间的左值了,再调用Fun函数,就会输出左值引用。这其实是一种属性丢失,如果想让t继续保持右值属性,可以用forward()。
C++类中,有6个成员函数不写,会被自动生成:
构造函数
析构函数
拷贝构造函数
拷贝赋值重载
取地址重载
const 取地址重载
如果不了解类和对象特性的,可以看看我之前详解的类和对象特性,毕竟类和对象是很重要的基础:[类和对象](http://t.csdn.cn/KlNqO)
C++11中新增了移动构造和移动拷贝赋值,这两个成员函数也是可以被自动生成的,但是自动生成是有一下条件的:
- 如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员变量是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
- 如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)
- 如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。
移动构造默认生成的条件还是有些苛刻的,C++11中增加了default
关键字可以让类的成员函数被强制生成。
//例如强制生成默认的移动构造
class string
{
public:
string(const char* str = "")
:_size(strlen(str))
, _capacity(_size)
{
_str = new char[_capacity + 1];
strcpy(_str, str);
}
// 移动构造
string(string&& s) = default;
private:
char* _str;
size_t _size;
size_t _capacity; // 不包含最后做标识的\0
};
有强制生成成员函数,也有禁止自动生成成员函数,用delete
关键字。主要还是针对默认生成的成员函数。
class string
{
public:
string(const char* str = "")
:_size(strlen(str))
, _capacity(_size)
{
_str = new char[_capacity + 1];
strcpy(_str, str);
}
// 移动构造
string(string&& s) = delete;
private:
char* _str;
size_t _size;
size_t _capacity; // 不包含最后做标识的\0
};
C语言中有可变参数,例如printf。可变参数主要是STL库里面的类模板会用,我们以了解为主。
C++11中新增了可变参数模板,下面就是可变参数模板的写法
// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
template <class ...Args>
void ShowList(Args... args)
{}
int main()
{
ShowList();
ShowList(1);
ShowList(1, 'a');
ShowList(12, "sdf", 23.0);
return 0;
}
可以传不同类型或者相同类型的若干参数,那么怎么解析这个参数包呢?
template <class ...Args>
void ShowList(Args... args)
{
cout << sizeof...(args) << endl;//代表传进去的参数个数
}
怎么显示打印这些可变参数呢?
1. 递归方式
void ShowList()
{
cout << endl;
}
//新增一个模板参数和函数形参,利用递归的思维解决。
template <class T,class ...Args>
void ShowList(const T& val,Args... args)
{
cout << val << " ";
ShowList(args...);//当参数包为0的时候,调用对应的无参函数
}
int main()
{
ShowList();
ShowList(1);
ShowList(1, 'a');
ShowList(12, "sdf", 23.0);
return 0;
}
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;
}
学习lambda表达式之前,我还是想要给大家举个例子来说明为什么学习lambda表达式,它能给我们带来什么好处。
例子:
在C++98中,如果想要对一个Goods类进行排序,可以使用std::sort方法,但是需要自己写一个类或者函数来定义比较规则。
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 ComparePriceGreater
{
bool operator()(const Goods& gl, const Goods& gr)
{
return gl._price > gr._price;
}
};
int main()
{
vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2, 3 }, { "菠萝", 1.5, 4 } };
sort(v.begin(), v.end(), ComparePriceLess());
sort(v.begin(), v.end(), ComparePriceGreater());
}
随着C++语法的发展,人们开始觉得上面的写法太复杂了,每次为了实现一个algorithm算法,都要重新去写一个类,如果每次比较的逻辑不一样,还要去实现多个类,特别是相同类的命名,这些都给编程者带来了极大的不便。因此,在C++11语法中出现了Lambda表达式。
我们先见识一下lambda表达式的写法,然后给大家解释什么意思
int main()
{
vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2, 3 }, { "菠萝", 1.5, 4 } };
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2)->bool { return g1._price < g2._price; });
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2)->bool { return g1._price > g2._price; });
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2)->bool { return g1._evaluate < g2._evaluate; });
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2)->bool { return g1._evaluate > g2._evaluate; });
}
很明显sort函数的第三个参数就是lambda表达式,我们解析一下每个部分是什么意思
lambda表达式书写格式:[capture-list] (parameters) mutable -> return-type { statement }
lambda表达式各部分说明
- [capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。
- (parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略
- mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。
- ->returntype:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
- {statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。
注意:
在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空。因此C++11中最简单的lambda函数为:[]{}; 该lambda函数不能做任何事情。
这些都是一些死板的概念,想要更加深刻的理解lambda表达式,我们还需借助观察几个例子来理解:
int x = 1, y = 0;
auto swap = [](int x, int y) {
int tem = x;
x = y;
y = tem;
};
cout << x <<" "<< y << endl;
大家觉得这个函数能完成x和y的交换吗?答案是不能,因为表达式内的空间是一块独立的,和一个函数一样,只不过lambda是一个匿名函数。
所以把参数列表变成引用类型就可以了。
[capture-list]捕捉列表内可以捕获上下文的数据,然后表达式内可以用,那么下面这种方式可以完成交换吗?
//传值捕捉
int x = 1, y = 0;
auto swap2 = [x, y]()
{
int tmp = x;
x = y;
y = tmp;
};
swap2();
cout << x << " " << y << endl;
首先这个代码会报错,报错原因如下图。因为默认情况下,lambda函数总是一个const函数,所以需要加上mutable。
但是加上mutable也不能完成交换,因为这是传值捕捉,就是相当于拷贝给表达式。
所以捕获列表也需要引用捕捉,才能完成对x和y的交换并且可以不加mutable
所以捕捉列表有两种捕捉方式,引用捕捉和传值捕捉。lambda还支持对所有的参数进行全部捕捉。例如:
//对所有参数进行引用捕捉
auto swap = [&](){};
//对所有参数进行传值捕捉
auto swap = [=](){};
//混合捕捉
auto swap = [&x,y](){};
//对x传值捕捉,其他引用捕捉
auto swap = [&,x](){};
//对x引用捕捉,其他传值捕捉
auto swap = [&x,=](){};
这就是C++11最常用的一些特性,希望对您有所帮助。