C11 列表初始化、左/右值引用、移动语义、可变参数模版

本文详细介绍了C++11的新特性,包括统一的列表初始化、声明方式、右值引用和移动语义、新的类功能以及可变参数模板。统一列表初始化提供更统一安全的方式;右值引用解决左值引用短板;新类功能完善资源管理;可变参数模板提升代码灵活性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

一、统一的列表初始化

1、{}初始化

基本类型的初始化

类对象的初始化

自定义类型及构造函数

复合类型和聚合体

2、std::initializer_list

3、模拟实现vector花括号操作

二、声明

1、自动类型推断 - auto

2、类型推导关键字 - decltype

3、空指针常量 - nullptr

4、范围for循环

三、右值引用和移动语义

1、左值引用和右值引用

左值与左值引用

右值与右值引用

2、左值引用与右值引用比较

3、左值引用的使用场景

4、左值引用的短板:

5、右值引用使用场景

6、移动构造

7、移动赋值

8、右值引用引用左值及其一些更深入的使用场景分析

9、万能引用

10、完美转发

11、模拟实现list push_back右值引用

完整代码:

关键讲解:

四、新的类功能

1、原始默认成员函数(C++98/03)

2、C++11引入的新默认成员函数

3、自动生成与手动定义

移动构造函数的自动生成规则与行为

移动赋值运算符的自动生成与行为

手动定义的影响

4、强制生成默认函数的关键字default

4、禁止生成默认函数的关键字delete

5、emplace

五、可变参数模板

1、概念

2、递归函数方式展开参数包

3、逗号表达式展开参数包

4、示例:求和函数模板


一、统一的列表初始化

1、{}初始化

在C++98中,标准已经规定可以利用花括号{}对数组或结构体成员进行统一的初始化操作。例如:
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 a = 10;      // 赋值初始化
int b(10);       // 直接构造初始化
int arr[] = {1, 2, 3}; // 数组初始化
  • C++11列表初始化方式:
int a{10};      // 列表初始化,类型安全,避免窄化转换
int b{10};      // 同样是列表初始化,更清晰地表示意图
int arr[]{1, 2, 3}; // 列表初始化数组,更统一

类对象的初始化

假设有一个类 Point,传统方式可能这样初始化:

class Point {
public:
    int x, y;
};

Point p1;      // 默认初始化,成员x和y的值不确定
Point p2 = {1, 2}; // 老式初始化,如果类定义了适当的构造函数
  • C++11列表初始化 提供了更明确的初始化,尤其是对于有默认值的成员:
class Point {
public:
    int x = 0, y = 0; // 成员有默认值
};

Point p1{};      // 明确的值初始化,x和y都被初始化为0
Point p2{1, 2};  // 直接列表初始化,x=1, y=2

自定义类型及构造函数

考虑一个类,其中构造函数接受一个 std::initializer_list

class Vector {
public:
    Vector(std::initializer_list<int> list) : elements(list.begin(), list.end()) {}
private:
    std::vector<int> elements;
};

// 传统方式难以直接实现这样的初始化
  • C++11列表初始化 支持直接使用初始化列表构造对象:
Vector v{1, 2, 3, 4}; // 直接使用列表初始化向量

复合类型和聚合体

对于复合类型或聚合体(如结构体),列表初始化更加明显地展示了初始化过程:

struct Color {
    int r, g, b;
};

// 传统初始化
Color c1;      // 不安全,成员未初始化
Color c2 = {255, 127, 0}; // 初始化,但依赖于编译器支持

// C++11列表初始化
Color c3{};      // 明确的值初始化,所有成员初始化为0
Color c4{255, 127, 0}; // 清晰地初始化每个成员

从上述例子可以看出,C++11列表初始化提供了更统一、明确和类型安全的初始化方式,特别在处理复杂类型和确保所有成员都得到适当初始化方面表现出色。

2、std::initializer_list

std::initializer_list 是C++11引入的一种特殊类型,它能够存储一组指定类型的常量值序列,并提供迭代器访问这些值。下面是一个示例说明其类型:

int main()
{
    // il 的类型即为 std::initializer_list<int>
    auto il = { 10, 20, 30 };
    cout << typeid(il).name() << endl;
    return 0;
}

std::initializer_list 主要用于简化构造函数参数的传递,尤其是在初始化容器对象时。C++11标准库中的许多容器如 std::vectorstd::liststd::map 等都增加了接受 std::initializer_list 类型参数的构造函数,使得我们可以方便地用花括号列表来初始化容器。

例如:

int main()
{
    // 使用 std::initializer_list 初始化容器
    std::vector<int> v = { 1, 2, 3, 4 };
    std::list<int> lt = { 1, 2 };

    // 使用 std::initializer_list 初始化 map,其中每个元素都会被解释为 pair
    std::map<std::string, std::string> dict = { { "sort", "排序" }, { "insert", "插入" } };

    // 同样支持大括号对容器进行赋值操作
    v = { 10, 20, 30 };

    return 0;
}

3、模拟实现vector花括号操作

为了使我们自定义的 `bit::vector` 支持花括号初始化以及赋值操作,可以如下实现:

namespace bit
{
    template <class T>
    class vector {
    public:
        typedef T* iterator;

        // 构造函数接收 std::initializer_list 参数
        vector(std::initializer_list<T> l)
        {
            _start = new T[l.size()];
            _finish = _start + l.size();
            _endofstorage = _start + l.size();

            iterator vit = _start;
            typename std::initializer_list<T>::iterator lit = l.begin();

            // 遍历并复制 initializer_list 中的元素到新建的 vector 中
            while (lit != l.end()) {
                *vit++ = *lit++;
            }
        }

        // 赋值运算符重载,接收 std::initializer_list 参数
        vector<T>& operator=(std::initializer_list<T> l) {
            // 创建临时对象,并用新的 initializer_list 初始化
            vector<T> tmp(l);

            // 通过 swap 技术完成赋值(避免自我赋值问题)
            std::swap(_start, tmp._start);
            std::swap(_finish, tmp._finish);
            std::swap(_endofstorage, tmp._endofstorage);

            return *this;
        }

    private:
        iterator _start;
        iterator _finish;
        iterator _endofstorage;
    };
}

构造函数

  1. 动态分配足够存储 l 中元素的连续内存空间。
  2. 初始化 _start 指针指向开始位置,_finish 和 _endofstorage 指针指向结束位置(这里假设没有预留额外空间)。
  3. 使用迭代器遍历 initializer_list,并将其中的元素依次复制到新分配的内存空间内。

赋值运算符重载

  1. 创建一个临时 vector<T> 对象 tmp,并使用 l 初始化它。
  2. 通过 std::swap() 函数交换当前对象(*this)和临时对象 tmp 的内部数据成员 _start_finish 和 _endofstorage。这样,原先对象的数据就被临时对象的新数据所取代。
  3. 返回当前对象的引用 *this,以便支持链式赋值。

二、声明

在C++11中,引入了多种简化声明的方式,特别是在模板编程中大大提高了效率和可读性。

1、自动类型推断 - auto

自动类型推断 - auto 在C++98中,auto关键字表示变量具有局部作用域和自动存储期,但由于局部变量默认就是自动存储类型,所以当时的auto显得并不重要。然而,在C++11中,auto的含义发生了改变,它被用来实现自动类型推断。这意味着当你声明一个变量并使用auto时,编译器会根据初始化表达式自动推断出该变量的类型,因此必须进行显式初始化。

示例:

int main()
{
    int i = 10;
    auto p = &i;  // p 的类型被推断为 int*
    auto pf = strcpy; // pf 的类型被推断为 char* (*)(char*, const char*)

    cout << typeid(p).name() << endl;
    cout << typeid(pf).name() << endl;

    map<string, string> dict = { {"sort", "排序"}, {"insert", "插入"} };
    // 使用auto简化迭代器声明
    // map<string, string>::iterator it = dict.begin();
    auto it = dict.begin(); // it 的类型被推断为 map<string, string>::iterator

    return 0;
}

2、类型推导关键字 - decltype

`decltype` 关键字允许你声明变量的类型与指定表达式的结果类型相同,这对于理解复杂的模板类型或推断函数返回类型极其有用。

// decltype 的应用场景
template <class T1, class T2>
void F(T1 t1, T2 t2)
{
    decltype(t1 * t2) ret; // ret 的类型由 t1 和 t2 相乘决定
    cout << typeid(ret).name() << endl;
}

int main()
{
    const int x = 1;
    double y = 2.2;
    decltype(x * y) ret; // ret 的类型被推断为 double
    decltype(&x) p;      // p 的类型被推断为 int*

    cout << typeid(ret).name() << endl;
    cout << typeid(p).name() << endl;

    F(1, 'a'); // 根据传入参数推断 ret 的类型

    return 0;
}

3、空指针常量 - nullptr

在C++98及之前版本中,通常使用宏`NULL`表示空指针,但`NULL`被定义为整数值0,这可能会引发混淆,因为在某些上下文中0可以同时代表指针常量和整数值。为了解决这个问题并提高代码清晰度和安全性,C++11引入了`nullptr`关键字,专用于表示空指针。
// 在C++11及以上版本中,建议使用nullptr代替NULL
void* ptr = nullptr;

4、范围for循环

范围for循环 C++11引入了范围for循环,这是一种更简洁的方式来遍历任何可迭代的对象,包括但不限于STL容器。它的语法格式如下:

for (declaration : range-expression)
    statement

例如,对于上述提到的dict容器,我们可以用范围for循环遍历:

for (const auto& pair : dict)
{
    cout << pair.first << ": " << pair.second << endl;
}

在这个循环中,编译器会自动获取dict的迭代器,并按顺序遍历容器中的每个元素,无需手动管理迭代器。每次迭代时,pair会被隐式声明为一个引用,引用当前迭代到的键值对。

三、右值引用和移动语义

1、左值引用和右值引用

在C++11中,引入了右值引用的概念,对于我们之前熟悉的引用,现在称之为左值引用。无论是左值引用还是右值引用,它们本质上都是为对象取了一个别名。

左值与左值引用

左值是指可以出现在赋值符号左侧的表达式,例如变量名或者解引用后的指针。它们可以有持续的地址,并可以被赋新的值。特别地,被const修饰后的左值不能被重新赋值,但我们仍可以获取其地址。左值引用正是为这些左值取的一个别名。

常见的左值包括:

  • 变量(如 int a;
  • 解引用后的指针(如 *ptr;
  • 引用(如 int& ref = a;
  • 数组名称(如 int arr[5];,数组名本身是一个左值,指向数组的第一个元素)

示例代码:

int main() {
    int* p = new int(0); // p是左值
    int b = 1; // b是左值
    const int c = 2; // c是左值,虽然不能赋新值,但可以取地址
    // 下面是对左值的左值引用
    int*& rp = p;
    int& rb = b;
    const int& rc = c;
    int& pvalue = *p; // 解引用得到左值
    return 0;
}
  1. int* p = new int(0); 这行代码创建了一个指向整数的指针 p,并通过 new int(0) 分配了一块动态内存,并初始化这块内存的值为 0p 是一个指向这块内存的指针,是一个左值(即可以出现在赋值表达式的左边)。

  2. int b = 1; 这行代码声明了一个整型变量 b 并初始化其值为 1b 是一个左值,可以直接对其进行读写操作。

  3. const int c = 2; 这行代码声明了一个 const 整型变量 c 并初始化其值为 2c 是一个左值,但由于它是 const 的,所以不能改变它的值,但仍然可以取它的地址。

  4. int*& rp = p; 这行代码声明了一个指向整型指针的引用 rp,并将它绑定到指针 p 上。这里的 rp 是一个左值引用,指向的是 p。也就是说,rpp 指向相同的内存地址,改变 rp 也会改变 p

  5. int& rb = b; 这行代码声明了一个指向整型变量的引用 rb,并将它绑定到变量 b 上。这里的 rb 是一个左值引用,指向的是 b。改变 rb 的值也会改变 b 的值。

  6. const int& rc = c; 这行代码声明了一个指向 const 整型变量的引用 rc,并将它绑定到 const 变量 c 上。这里的 rc 是一个指向 const 类型的左值引用,意味着它不能通过 rc 改变 c 的值,但可以读取 c 的值。

  7. int& pvalue = *p; 这行代码声明了一个指向整型变量的引用 pvalue,并将它绑定到指针 p 所指向的内存地址上的值(即 *p)。这里的 pvalue 是一个左值引用,指向的是 p 指向的内存中的值。改变 pvalue 也会改变 *p 的值。

右值与右值引用

右值通常是不能出现在赋值符号左侧的表达式,比如字面量、表达式的计算结果或者是函数的返回值(此处不包括返回左值引用的情况)。右值通常不能取地址。右值引用就是对这些右值的引用,为它们取了别名。

常见的右值包括:

  • 字面量(如 10"Hello"
  • 表达式的计算结果(如 a + b
  • 函数的返回值(如 std::max(a, b)

右值引用是一种特殊的引用类型,它用于引用右值。右值引用的定义语法如下:

Type&& ref;

其中 Type 是引用的类型,ref 是引用的名字。右值引用可以绑定到右值,也可以绑定到左值,但在大多数情况下,它用于绑定到临时对象。

示例代码:

int main() {
    double x = 1.1, y = 2.2;
    // 下面是常见的右值
    10;
    x + y;
    fmin(x, y);
    // 下面是对右值的右值引用
    int&& rr1 = 10;
    double&& rr2 = x + y;
    double&& rr3 = fmin(x, y);
    // 下面的尝试会引发编译错误,因为右值不能出现在赋值符号左侧
    // 10 = 1;
    // x + y = 1;
    // fmin(x, y) = 1;
    return 0;
}

需要注意,右值本身是不能取地址的,但一旦我们为右值创建了一个右值引用(相当于给右值取了一个别名),这个引用的右值就可以被存储在一个具体位置,从而可以获取其地址,并且可以被修改。如果我们希望防止这种修改,可以通过使用const修饰的右值引用来实现。

int main() {
    double x = 1.1, y = 2.2;
    int&& rr1 = 10;
    const double&& rr2 = x + y;
    rr1 = 20;    // 正确,可以修改rr1
    rr2 = 5.5;   // 错误,不能修改const修饰的右值引用
    return 0;
}

虽然右值引用的概念可能初看起来比较抽象,但它在现代C++编程中,尤其是实现移动语义和优化临时对象的处理方面发挥着重要作用。

2、左值引用与右值引用比较

左值引用要点:

  1. 左值引用(non-const左值引用)仅能绑定到持久的、可命名的左值对象,例如变量名、数组元素等。例如,在例子中,我们能成功创建一个引用ra1指向左值变量a,但不能将一个左值引用绑定到右值(如字面量10)上。

    int a = 10;
    int& ra1 = a; // 正确,ra1成为了a的别名
    // int& ra2 = 10; // 错误,无法将右值绑定到左值引用
  2. 然而,const左值引用则具有更高的灵活性,它既能绑定到左值对象,也能绑定到右值。在示例中,我们可以创建一个const左值引用ra3绑定到右值字面量10,同时也能绑定到左值变量a

    const int& ra3 = 10; // 正确,ra3绑定到右值
    const int& ra4 = a; // 正确,ra4成为a的const别名

右值引用要点:

  1. 右值引用专为右值设计,它只能绑定到临时对象或者将要销毁的对象(即右值)。例如,我们可以创建一个右值引用r1直接绑定到右值字面量10上。

    int&&am
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Han同学

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值