列表初始化与右值引用

目录

C++11(列表初始化与右值引用)

列表初始化

initilaizer_list

auto

decltype

nullptr

智能指针

stl新增容器

右值引用

左值:

右值:

右值引用与左值引用的比较

左值引用的作用

右值引用的作用

万能引用

完美转发

完美转发的价值


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

初始化自定义类型

// initializer_list 初始化自定义类型必须要有对应的构造函数,如果没有写对应的构造函数,那么还是不能初始化
vector nums = {1,2,3,4,5};
// 上面这样是可以的,vector 有 initializer_list 的构造函数
​
// vector (initializer_list il,
//        const allocator_type& alloc = allocator_type());
// 如果没有对应的构造函数是不可以的
// 在 stl 库中,所有的容器都有 initializer_list 的构造函数
// 所以不仅是 vector 可以使用它来初始化,其他的容器也是可以的
map hash = {1, 2};// 可以这样初始化吗? 不可以,因为 map 里面存储的是一个 pair
map hash = { {1, 2} };// 这样才是正确的,同时也不是只能初始化一个值,可以加任意个数的值
// 也可以不加赋值符号
map 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 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>::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 nums = { 0,9,8,7,6,5,4,3,2,1 };
for (auto nu : nums)
{
    cout << nu << " ";
}
// 这样也是可以的
// 也可以遍历 map,但是 map 的迭代器的解引用是一个 pair
map 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

  • 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 后的左值

总结:

  1. 左值引用只能引用左值,不能引用右值,但是左值引用加 const 就可以引用右值

  2. 右值引用只能引用右值,不能引用左值,但是可以引用 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
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
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
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 的时候也需要进行完美转发

你可能感兴趣的:(开发语言,c++)