目录
C++11 是C++跟新了很多有用内容的一个版本,其中包括了列表初始化,initialiazer_list,右值引用等等。
下面一起看一下C++11跟新的内容。
在C语言中,数组、结构体都可以用花括号来初始化,也就是这个{}。
但是C++的类却不支持那样初始化,C++的类只能是单参数的类才能隐式类型转化,而列表初始化,也可以叫做多参数的隐式类型转换。
有了花括号初始化后,花括号不仅可以初始化数组,还可以初始化类。
花括号初始化内置类型:
- int a = { 10 };
- int b{ 20 };
花括号初始化还可以省略掉中间的赋值符号,但是我认为内置类型没有必要这样初始化。
数组初始化:
- int ptr1[] = { 0, 9 };
- int ptr2[]{ 8, 7 };
类初始化:
- struct A
- {
- A(int a)
- :_a(a)
- {}
-
- int _a;
- };
-
- // 单参数的类支持隐式类型转换,所以这样是可以的
- A aa = 10;
- // 如果不想隐式类型转换,那么可以加 explicit
- struct A
- {
- explicit A(int a)
- :_a(a)
- {}
-
- int _a;
- };
-
- void test1()
- {
- A aa = 10;
- }
- // 可以看一下报错
- /*
- “初始化”: 无法从“int”转换为“A” test_2023_09_24
- */
多参数隐式类型转化:
多参数隐式类型转化,也就是前面说的花括号初始化类。
- struct B
- {
- B(int b1, int b2)
- :_b1(b1), _b2(b2)
- {}
-
- int _b1;
- int _b2;
- };
- // 显然多参数的自定义类型是不支持下面这样书写的
- // B b = 1, 2;
- // 但是是C++11更新后,就支持多参数的隐式类型转化了
-
- B b = { 1, 2 };
- B b1 { 1, 2 };
-
- // 不过实质上,都是调用了构造函数
- struct B
- {
- B(int b1, int b2)
- :_b1(b1), _b2(b2)
- {
- cout << "B(int b1, int b2)" << endl;
- }
-
- int _b1;
- int _b2;
- };
-
- void test1()
- {
- B b = { 1, 2 };
- B b1{ 1, 2 };
- }
- // 下面调用该函数
- // 下面是输出结果
- B(int b1, int b2)
- B(int b1, int b2)
前面说了一个列表初始化,还有一个是 initializer_list ,这两个很容易混淆
initializer_list 是一个类,而它的类型就是 一个 {},里面可以有任意个数的单一类型的元素
有了它,那么就可以用它来构造对象
initializer_list 对象:
- auto il = { 1, 2, 3, 4 };
- // 它的实际类型就是 initializer_list
- // 打印看一下类型
- cout << typeid(il).name() << endl;
- // 下面是输出结果
- class std::initializer_list<int>
初始化自定义类型:
- // initializer_list 初始化自定义类型必须要有对应的构造函数,如果没有写对应的构造函数,那么还是不能初始化
- vector<int> nums = {1,2,3,4,5};
- // 上面这样是可以的,vector 有 initializer_list 的构造函数
-
- // vector (initializer_list
il, - // const allocator_type& alloc = allocator_type());
- // 如果没有对应的构造函数是不可以的
- // 在 stl 库中,所有的容器都有 initializer_list 的构造函数
- // 所以不仅是 vector 可以使用它来初始化,其他的容器也是可以的
- map<int, int> hash = {1, 2};// 可以这样初始化吗? 不可以,因为 map 里面存储的是一个 pair
- map<int, int> hash = { {1, 2} };// 这样才是正确的,同时也不是只能初始化一个值,可以加任意个数的值
- // 也可以不加赋值符号
- map<int, string> hash1{ {1, "a"}, {2, "b"}, {3, "c"} };
- // 其实这里有两层初始化,第一层是构造pair,而二层是使用pair构造map
没有实现 initializer_list 的是不可以使用它来构造对象的:
- // A 类型是前面用过的,并没有写对应的构造函数
- A a = { 1, 2, 3, 4 };// 所以这样是无法初始化的
- // 看一下报错
- /*
- 初始化”: 无法从“initializer list”转换为“A” test_2023_09_24
- */
- A a{1}// 这个是单参数/多参数的隐式类型转换
- vector<int> nums{1,2,3};// 这个是 initializer_list的构造函数
其实这个之前介绍过
auto 可以由后面的值自动推导类型
而且有了 auto 后还有了范围for
auto自动推导类型:
- int b = 10;
- auto a = b;
- cout << typeid(a).name() << endl;
- // 打印类型查看
- int
- // 也不仅可以这样推,还可以推导表达式
- auto c = a + b;// 实际上这样推导也是正确的
-
- // 不仅可以推导内置类型,还可以推导自定义类型
- // 当我们在写迭代器的时候
- std::unordered_map
int>>::iterator it;// 当我们要写这么一个类型的时候,太长了,但是我们也可以是用 auto 来自动推导
语法糖:
- // 实际上我认为 auto 最好用的就是范围for,也叫做语法糖
- // 它可以遍历很多容器,只要有迭代器就可以使用语法糖
- int a[] = { 1,2,3,4,5,6,7,8,9 };
- for (auto nu : a)
- {
- cout << nu << " ";
- }
- // 可以这样遍历数组 a,因为这里的数组 a 的下标就是原生的迭代器
- // 既然原生的迭代器可以,那么自己写的迭代器也是可以的
- vector<int> nums = { 0,9,8,7,6,5,4,3,2,1 };
- for (auto nu : nums)
- {
- cout << nu << " ";
- }
- // 这样也是可以的
- // 也可以遍历 map,但是 map 的迭代器的解引用是一个 pair
- map<int, int> hash{ {1,10}, {2, 20}, {3, 30} };
- for (auto kv : hash)
- {
- cout << kv.first << " : " << kv.second << endl;
- }
-
- // 其实范围for,也就是利用 auto 自动推导类型,其实范围for的底层也就是替换成了迭代器,所以只要有迭代器就可以泡范围for
- // 范围for的类型那里不一定要写 auto,这里主要是不明确里面是什么类型,如果知道具体类型,也可以写具体类型
- vector
vs{ "a", "b", "c", "d", "e" }; - for (string& str : vs)
- {
- cout << str << endl;
- }
decltype可以将变量的类型声明为表达式的类型
auto 可以自动推导类型,但是必须要是后面的值推导,如果后面没有值,则不可以
而 typeid().name() 是可以将变量的类型打印出来,并不能用来声明
decltype声明类型:
- int a = 10;
- decltype(a) b;
- cout << typeid(b).name() << endl;
- // 输出结果
- int
-
- // decltype 不仅可以用变量来声明,还可以使用表达式
- decltype(10 + 20) c;
在前面的 NULL 实际上是宏定义的结果,他们将 0 定义为了 NULL,所以在有些场景下是有问题的。
而 nullptr 就是为了表示空指针
这个会在后面说,这个内容比较多
在C++11中 stl 新增了一些容器
在新增的容器中,有两个是我们非常好用的 unordered_map,unordered_set
还新增了一个 array 和 forward_list ,但是这两个几乎不常用,所以不多解释
右值引用,我们很容易想起之前说的引用,而之前说的引用都是左值引用
那么左值和右值是什么?
左值就是可以被取地址,大概率可以被修改值的值,叫做左值
右值就是不能被取地址的值
- // 左值
- int a = 10; // a 就是一个左值
- int* Pa = &a;
- a = 20;// 可以被取地址,也可以被修改
-
- const int c = 100;// const int c 也是一个左值,可以被取地址
- const int* Pc = &c;// 但是不能被修改
-
- cout << &("hello world");// 常量字符串也是左值,可以被取地址
- // 右值
- int a = 10, b = 20;
- a + b; // a + b 就是常见的右值,因为 a + b 会有一个返回值,该返回值不能被取地址
-
- int func()
- {
- int a = 99;
- return a;
- }
- func();// 函数的传值范围也是右值,不能被取地址
- // 左值引用
- int a = 10;
- int& b = a;// 左值引用引用左值
-
- // 左值引用引用右值
- int& c = 1 + 2; // 这样是不可以的
- const int& c = 1 + 2; // 但是左值引用加 const 就可以引用右值
-
- // 右值引用
- int&& x = 10;// 右值引用引用右值
-
- // 右值引用引用左值
- int&& y = a;// 这样是不可以的
- int&& y = move(a);// 但是可以引用 move 后的左值
总结:
左值引用只能引用左值,不能引用右值,但是左值引用加 const 就可以引用右值
右值引用只能引用右值,不能引用左值,但是可以引用 move 后的左值
可以用作传参
可以用作返回值
意义:减少拷贝
右值其实可以分为两种:纯右值、将亡值
纯右值:内置类型的右值就是纯右值
将亡值:自定义类型的右值就是将亡值
- void func1(string str)
- {
- string s(str);
- cout << s << endl;
- }
- // 在这种情况下,如果传值的话,怎么办?
- func1("hello");// 首先是值传递,而 string 在值传递的过程中会发生拷贝构造,就浪费了资源,但是在C++11 中,就可以将,将亡值的资源换给自己,然后在将自己没用的资源给将亡值,让将亡值销毁的时候带走
- void swap(string& str)
- {
- char* tmp = _a;
- _a = str._a;
- str._a = tmp;
- _size = str._size;
- _capacity = str._capacity;
- }
- string(const string& str)
- {
- _a = new char[str._size + 1];
- for (int i = 0; i < str._size; ++i)
- {
- _a[i] = str._a[i];
- }
- _size = str._size;
- _capacity = str._capacity;
- _a[_size] = '\0';
- cout << "string(const string& str) 深拷贝" << endl;
- }
-
- string(string&& str)
- {
- swap(str);
- cout << "string(string&& str) 浅拷贝" << endl;
- }
- string& operator=(string str)
- {
- swap(str);
- return *this;
- cout << "string& operator=(string str) 深拷贝" << endl;
- }
- string& operator=(string&& str)
- {
- swap(str);
- return *this;
- cout << "string& operator=(string&& str) 浅拷贝" << endl;
- }
上面是自己实现的一个string,可以测试一下深浅拷贝
- void func(lxy::string str) // 传值深拷贝
- {
-
- }
-
-
- int main()
- {
- lxy::string s("hello");
- func(s);
- }
- // 查看输出结果
- string(const string& str) 深拷贝
- lxy::string func()
- {
- lxy::string str("hello");
-
- return str;
- }
-
-
- int main()
- {
- lxy::string ret = func();
- }
- // 如果在没有写移动构造和移动赋值的情况下会发生几次拷贝构造呢?
- // 这里来看一下
- // 1. 首先在 func 栈帧里面会有一个临时变量 str,然后返回该 str ,str 出了栈帧就会销毁,所以返回的是 str 的拷贝,返回之后,又会通过返回值构造一个对象,所以是两次深拷贝,但是由于连续的两次深拷贝实在是效率低下,所以编译器也做了优化,就是两次连续的深拷贝会被优化为一次,所以看一下结果
- string(const string& str) 深拷贝
- // 但是这也是针对两次连续的,如果不连续呢?
- // 那么就只能是两次深拷贝了
-
- int main()
- {
- lxy::string ret;
- ret = func();
- }
- // 查看结果
- string(const string& str) 深拷贝
- string& operator=(string str) 深拷贝;
- // 所以通过编译器的优化可以减少两次连续的深拷贝
-
- // 将移动构造和移动赋值放出来
- // 那么就只会有浅拷贝
- int main()
- {
- lxy::string ret = func();
- }
- // 查看结果
- string(string&& str) 浅拷贝;// 浅拷贝里面,我们只是 swap 了两个对象里面的几个内置类型,所以代价很低
- // 在看一下下面的这种写法
- int main()
- {
- lxy::string ret;
- ret = func();
- }
- // 这样写也会调用两次浅拷贝
- string(string&& str) 浅拷贝
- string& operator=(string&& str) 浅拷贝
下面先看一段代码
- template<class T>
- void fun(T&& a)
- {
-
- }
-
- void test9()
- {
- int a = 10;
- const int b = 10;
-
- fun(a);
- fun(b);
-
- fun(10);
- fun(move(b));
- }
这段代码 fun 能被调用成功吗?
这里的 fun 的参数是 T&& ,下面其中有 左值、 const 左值、右值、 const 右值 , 那么电泳会怎么样?是编译报错还是调用出现问题,或者是正常调用?
其实是调用正常的,因为如果在模板这里的话,函数模板会自己推导类型,如果是左值的话,就会推成左值,也叫作引用折叠,如果是右值那么就推的是右值。
既然可以调用,那么看下面一段代码:
- void fun1(int& a)
- {
- cout << "fun1(int& a)" << endl;
- }
-
- void fun1(const int& a)
- {
- cout << "fun1(const int& a)" << endl;
- }
-
- void fun1(int&& a)
- {
- cout << "fun1(int&& a)" << endl;
- }
-
- void fun1(const int&& a)
- {
- cout << "fun1(const int&& a)" << endl;
- }
-
-
- template<class T>
- void fun(T&& a)
- {
- fun1(a);
- }
-
- void test9()
- {
- int a = 10;
- const int b = 10;
-
- fun(a);
- fun(b);
-
- fun(10);
- fun(move(b));
- }
这段代码里面 fun 函数调用 fun1 函数,会正确调用到吗?
看一下结果:
- fun1(int& a)
- fun1(const int& a)
- fun1(int& a)
- fun1(const int& a)
全都调用到左值引用了?为什么?
其实右值引用的变量是左值,下面看一下:
- int&& a = 10;
- cout << &a << endl;
- a = 100;
- cout << a << endl;
结果:
- 0077F8D4
- 100
其实右值不仅不可以被取地址,还不能被修改,但是这里看到右值引用的变量不仅可以被修改还可以被取地址,所以右值引用的变量是左值,所以上面的调用都会调用到左值,但是如果想要传过去依旧是右值呢?
可以通过完美转发来实现:
- void fun1(int& a)
- {
- cout << "fun1(int& a)" << endl;
- }
-
- void fun1(const int& a)
- {
- cout << "fun1(const int& a)" << endl;
- }
-
- void fun1(int&& a)
- {
- cout << "fun1(int&& a)" << endl;
- }
-
- void fun1(const int&& a)
- {
- cout << "fun1(const int&& a)" << endl;
- }
-
-
- template<class T>
- void fun(T&& a)
- {
- //完美转发
- fun1(forward
(a)); - }
-
- void test9()
- {
- int a = 10;
- const int b = 10;
-
- fun(a);
- fun(b);
-
- fun(10);
- fun(move(b));
- }
还是上面的那一段代码,但是在 fun 函数调用 fun1 的时候,传值的时候我们对a进行了 forward (完美转发),完美转发过的值,本来是什么类型,那么就是什么类型。
下面继续看一下结果:
- fun1(int& a)
- fun1(const int& a)
- fun1(int&& a)
- fun1(const int&& a)
上面我们知道了右值引用的价值,实际上完美转发就是可以将右值引用的价值发挥到极致:
- list
ls; - ls.push_back("hello world");
上面有这么一段代码,其中我们的 string 是有一定构造的,而 list 也有移动构造的版本:
在插入的时候 hello world 会隐式类型转换变成 string,但是这个 string 是一个将亡值。
由于 list 的 push_back 也有自己的右值引用版本,所以此时 push_back 就会调用到右值版本。
- void push_back(string&& str)
- {
- list_node* newnode = new list_node(forward
(str)); - ....
- }
push_back 里面会调用一个 new list_node 然后将 string 给 list_node。
list_node 在 new 的时候调用了构造函数, list_node 也写了右值版本。
- list_node(string&& str)
- :_prev(nullptr)
- ,_next(nullptr)
- ,_val(forward
(str)) - {}
但是此时传到 push_back 里面的变量此时虽然是右值引用的变量,但是实际上是左值,如果直接调用 list_node 的拷贝构造,那么一定会调用到左值版本的,所以传过去的时候还需要对其进行完美转发。
而传过去之后, list_node 在进行 val 的构造的时候,会调用拷贝构造,所以在传给 value 的时候也需要进行完美转发