目录
C++11(初始化列表与右值引用)
-
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)
initilaizer_list
-
前面说了一个列表初始化,还有一个是 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<value_type> 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 可以由后面的值自动推导类型
-
而且有了 auto 后还有了范围for
auto自动推导类型:
int b = 10;
auto a = b;
cout << typeid(a).name() << endl;
// 打印类型查看
int
// 也不仅可以这样推,还可以推导表达式
auto c = a + b;// 实际上这样推导也是正确的
// 不仅可以推导内置类型,还可以推导自定义类型
// 当我们在写迭代器的时候
std::unordered_map<std::string, std::vector<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<string> vs{ "a", "b", "c", "d", "e" };
for (string& str : vs)
{
cout << str << endl;
}
decltype
-
decltype可以将变量的类型声明为表达式的类型
-
auto 可以自动推导类型,但是必须要是后面的值推导,如果后面没有值,则不可以
-
而 typeid().name() 是可以将变量的类型打印出来,并不能用来声明
decltype声明类型:
int a = 10;
decltype(a) b;
cout << typeid(b).name() << endl;
// 输出结果
int
// decltype 不仅可以用变量来声明,还可以使用表达式
decltype(10 + 20) c;
nullptr
-
在前面的 NULL 实际上是宏定义的结果,他们将 0 定义为了 NULL,所以在有些场景下是有问题的。
-
而 nullptr 就是为了表示空指针
智能指针
-
这个会在后面说,这个内容比较多
stl新增容器
-
在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<T>(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<string> 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<string>(str)); .... }
-
push_back 里面会调用一个 new list_node 然后将 string 给 list_node。
-
list_node 在 new 的时候调用了构造函数, list_node 也写了右值版本。
-
list_node(string&& str) :_prev(nullptr) ,_next(nullptr) ,_val(forward<string>(str)) {}
-
但是此时传到 push_back 里面的变量此时虽然是右值引用的变量,但是实际上是左值,如果直接调用 list_node 的拷贝构造,那么一定会调用到左值版本的,所以传过去的时候还需要对其进行完美转发。
-
而传过去之后, list_node 在进行 val 的构造的时候,会调用拷贝构造,所以在传给 value 的时候也需要进行完美转发