目录
{}初始化
{}初始化大家一定不陌生,经常会用{}对数组或者结构体元素进行统一的列表初始值设定,例如:
struct A
{
int _a1;
int _a2;
};
void test5()
{
int arr[10] = { 9,8,7,6,5,4,3,2,1,0 };
A aa = { 1,2 };
}
在c++11标准中,“加强了”{}的“战斗力”,可用于所有的内置类型和自定义类型,等号(=)可以根据心情省略。
void Test1()
{
int x1 = 10;
//使用列表初始化,也可以省略等号
int x2{ 20 };
int arr1[5]{ 2,4,6,8,10 };
int arr2[5]{0};
}
这里请“上古神器”出山对{}初始化进行一下检测:
哈哈哈哈哈,“神器”好像没有学习这个“技能”,不会支持这样的写法。
●对于{}的使用, 应用到new表达式中也是比较便捷的:
void Test2()
{
int* arr1 = new int[5]{1,3,4,7,9};
}
●创建对象时使用列表初始化方式调用构造函数初始化:
class Date
{
public:
Date(int year = 2001,int month = 10,int day = 1)
:_year(2001)
,_month(10)
,_day(1)
{}
private:
int _year;
int _month;
int _day;
};
void test2()
{
//传统写法
Date d1(1990,2,3);
//c++11支持的新玩法
Date d2 = {2022,9,18};
Date d3{ 2023,4,1 };
}
initializer_list
void test3()
{
auto il = { 520,13,14 };
cout << typeid(il).name() << endl;
}
typeid是操作符,不是函数。可以使用 typeid(变量).name() 获知变量类型名称。
std::initializer_list一般是作为构造函数的参数,这样初始化容器对象就更方便了。也可以作为operator=的参数,这样就可以用大括号赋值。
void Test4()
{
auto il1 = {1,3,5,7};
vector<int> v1(il1);
vector<int> v2{2,4,6,8};
vector<int> v3;
v3 = il1;
map<string, string> dict = { {"张飞", "三国演义"}, {"猪八戒", "西游记"} };
}
关键字
decltype
decltype关键字的功能是可以将变量的类型声明为表达式指定的类型:如下述代码,decltype关键字识别x+y表达式的类型为double,将ret的类型声明为了double。
void Test5()
{
int x = 520;
double y = 13.14;
decltype(x + y) ret = x+y;
cout << ret << endl;
cout << typeid(ret).name() << endl;
}
nullptr
C++中NULL被定义成字面量0,0既能表示指针常量,又能表示整形常量。所以C++11中新增了nullptr,用于表示空指针。
观察下述代码,首先在vs2020下进行测试:
void Fun(int x)
{
cout << "Fun1" << endl;
}
void Fun(int* p)
{
cout << "Fun2" << endl;
}
void Test6()
{
cout << typeid(NULL).name() << endl;
cout << typeid(nullptr).name() << endl;
Fun(nullptr);
Fun(NULL);
}
编译通过了,并且通过打印可以看出NULL和nullptr类型的区别。
同样是上述的代码,在Linux下进行测试:
编译报错,提示我们test(NULL)调用不明确!
小总结:不要在纠结了,当你要表示空指针的时候,使用nullptr!
新增容器
unorder_map和set在上篇文章中进行了一些介绍,使用方法和map/set基本相同,底层结构用的是哈希结构。这里简单的介绍下array和forward_list。
array
array的模板参数有两个,T表示要存储的数据类型,N表示array存储空间的大小。也就是array的空间是指定好的,你可以将其理解为静态数组。
void test1()
{
array<int, 5> a{4,3,2,1,0};
for (auto e : a)
{
cout << e << " ";
}
}
forward_list
C++11 新添加的一类容器,其底层实现和 list 容器一样,采用的也是链表结构,只不过 forward_list 使用的是单链表,而 list 使用的是双向链表。
void Test2()
{
forward_list<int> ll;
ll.push_front(1);
ll.push_front(2);
ll.push_front(3);
ll.push_front(4);
for (auto e : ll)
{
cout << e << " ";
}
}
右值引用
引用的概念在之前的文章中就提到过了,简单的理解就是对已存在变量取了一个别名。如下述代码示例,对表示数据的表达式进行了引用(变量名或者解引用的指针)。
void Test3()
{
int a = 10;
int& A = a;
int* arr = new int[5]{ 1,2,3,4,5 };
int*& Parr = arr;
int& Ppa = *Parr;
const int b = 20;
const int& B = b;
}
C++11新增了右值引用的语法特性,这一特性增加后,上述的引用我们称为左值引用!右值引用也是数据的表达式,如字面量、表达式返回值等。例如下述代码示例:
int add(int x, int y)
{
return x + y;
}
void Test4()
{
double a = 520, b = 13.14;
int&& x = 10;
int&& y = (a + b);
int&& z = add(a, b);
}
●区分左值和右值。
左值:是一个表示数据的表达式(如变量名或解引用的指针),可以对其进行取地址和赋值操作。const修饰符后的左值,不能给他赋值,但是可以取它的地址。
右值:也是一个表示数据的表达式(如字面常量,表达式返回值,函数返回值(左值返回除外)等等)右值不能取地址,右值不能出现在赋值符号的左边。右值还分为纯右值(内置类型表达式的值)和将亡值(自定义类型的表达式的值)。小提示:左右值的判别千万不要根据其在等号(=)左右的位置。
●左值引用和右值引用。
左值引用即引用左值,对左值取别名。右值引用即引用右值,对右值取别名。
左值引用只能引用左值,但const左值引用既能引用左值,也能引用右值。
右值引用能引用右值,不能引用左值。但是可以引用move(返回一个右值引用)后的左值。
void test5() { int a = 10; //const左值引用可以引用左值,也可以引用右值 const int& A1 = a; const int& B = 100; //右值引用可以引用move后的左值 int&& A2 = move(a); }
右值不能被取地址,但是给右值取别名后,会导致右值被存储到一个特定的位置,并且可以取到该位置的地址。也可以修改该别名引用的内容。如果不想被修改,应该加const。
int main() { //右值引用 int&& a = 10; cout << "修改前" << a << endl; //对别名a取地址 cout << &a << endl; //修改a引用的内容 a = 20; cout <<"修改后" << a << endl; return 0; }
●左值引用的价值和短板。
优势:做参数和做返回值都可以提高效率(减少拷贝)。
为了更好的观察上述文字描述,这里准备一段代码(以string类为例):代码中包含了一些打印信息,但是在测试的过程中,会根据情况屏蔽或者放开打印的提示信息,使打印出的提示更简洁!
class string { public: string(const char* str = "") :_size(strlen(str)) , _capacity(_size) { //cout << "string(const char* str = "")构造函数" << endl; _str = new char[_capacity + 1]; strcpy(_str, str); } void swap(string& s) { std::swap(_str, s._str); std::swap(_size, s._size); std::swap(_capacity, s._capacity); } //拷贝构造 string(const string& s) { cout << "string(const string& s)拷贝构造" << endl; //根据s._str调用构造函数 string tmp(s._str); swap(tmp); } //赋值重载 string& operator=(const string& s) { cout << "string& operator=(string s)赋值重载" << endl; //根据s调用拷贝构造 string tmp(s); //把拷贝好的tmp交换给this swap(tmp); return *this; } private: char* _str = nullptr; size_t _size = 0; size_t _capacity = 0; };
测试左值引用做参数和返回值的优势:
void func1(string s) { cout << "传值调用" << endl; } void func2(const string& s) { cout << "传引用调用" << endl; } void String_test() { string s1("Hello World"); //func1(s1); //func2(s1); }
string fun1(string& s) { cout << "返回值" << endl; return s; } string& fun2(string& s) { cout << "返回引用" << endl; return s; } void String_test() { string s1("Hello World"); //fun1(s1); //fun2(s1); }
短板(未解决的问题):如果函数返回对象是局部变量,出了作用域就不存在了。这种场景只能传值返回。
string& fun1() { string str; cout << "返回局部变量str" << endl; return str; }
针对上述情况,只能传值返回!
●右值引用的价值。
右值引用的价值之一就是补齐上述的短板,例如上述代码中的局部变量str,它是一个左值但是编译器优化将其识别成右值且是一个将亡值。这样的对象对其进行拷贝大可不必,直接吸取其“资源”是一个不错的选择。
移动构造(吸星大法)
在上述string中提供一个移动构造:
//移动构造 string(string&& s) { cout << "string(const string&& s)移动构造" << endl; swap(s); }
string fun1() { string str; cout << "返回局部变量str" << endl; return str; } void String_test() { string s1 = fun1(); }
移动赋值
//移动赋值 string& operator=(string&& s) { cout << "string& operator=(string&& s)赋值重载" << endl; swap(s); return *this; }
string fun1() { string str("Hi"); cout << "返回局部变量str" << endl; return str; } void String_test() { string s1("hello world!"); s1 = fun1(); }
为了效果简介,屏蔽掉了构造函数的打印。通过测试可以发现,调用了一次移动构造和移动赋值,移动构造的调用过程和上述一致,fun1的函数返回值赋值给s1又调用了移动赋值。
小总结:右值引用和左值引用减少拷贝的原理不太一样,左值引用是直接起作用,而右值引用是间接起作用,实现移动构造和移动重载,在拷贝的场景中,如果是右值(将亡值),转移资源。
万能引用和完美转发
万能引用
先来看一段这样的代码:
void fun1(int& val) { cout << "fun1(int& val)" << endl; } void fun1(int&& val) { cout << "fun1(int&& val)" << endl; } void test2() { int a = 10; fun1(a); fun1(20); }
上述代码和简单,fun1构成了函数重载,传左值调用int&,传有值调用int&&。c++比较重要的一环就是泛型编程,在模板中有万能引用的概念。
template<class T> void universal(T&& val) { //... }
上述代码中的T&&表示的就不在单单表示右值引用了而是万能引用,T的类型是右值就是右值引用,如果是左值&&就折叠成一个变成左值引用。
需要注意的是,模板的万能引用只是提供了能够同时接收左值引用和右值引用的能力,但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值。也就是说无论是左值引用还是右值引用,val的属性在universal函数中都会退化成左值(如果不这样话“吸星大法”就没法用了)。
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<class T> void universal(T&& val) { Fun(val); } void test3() { //右值引用 universal(100); int a = 10; //左值 universal(a); universal(move(a)); const int b = 20; //const左值 universal(b); //congst右值 universal(move(b)); }
推演实例化图解:
测试结果和我们描述的一致,val的属性在universal函数中都会退化成左值,但是有些场景我们是需要val保证其属性的,在模板中使用move肯定是不能解决这个问题的,基于这样的背
景接下来介绍完美转发!
完美转发
std::forward 完美转发在传参的过程中保留对象原生类型属性!
还是上述代码,仅仅做如下的改动:
template<class T> void universal(T&& val) { Fun(forward<T>(val)); }
这样一来,在传递的过程中就保证了左值或者有值的属性。