当前位置: 首页 > news >正文

C++11新特性(列表初始化与右值引用折叠与完美转发)

c++11

  • 列表初始化
    • c++98的{}
    • c++11的{}
    • std::initializer_list
  • 右值引用和移动语义
    • 左值和右值的概念
    • 左值引用和右值引用
    • 引用延长临时对象生命周期
    • 左值和右值的参数匹配
    • 左值引用的主要使用场景
    • 移动构造和移动赋值
    • 引用折叠
    • 完美转发

列表初始化

c++98的{}

在C++98中,大括号 {} 的使用主要有以下几种场景:

  1. 代码块
    大括号用于定义一个代码块,通常在控制结构(如 if、for、while 等)或函数定义中使用。
if (condition) {// 代码块doSomething();
}
  1. 函数定义
    函数体由大括号括起来。
void myFunction() {// 函数体
}
  1. 类和结构体定义
    类和结构体的成员变量和成员函数也由大括号括起来。
class MyClass {
public:int x;void myMethod() {// 方法体}
};
  1. 初始化列表(C++98 限制)
    在C++98中,可以使用大括号进行数组或结构体的初始化。
int arr[5] = {1, 2, 3, 4, 5};struct Point {int x;int y;
};Point p = {10, 20};
  1. 范围(Scope)管理
    大括号可以创建新的作用域,这对于管理变量的生命周期很有用。
{int temp = 5;// temp 在这个作用域内有效
}
// temp 在这里不可见
  1. C++98 对 {} 的局限
    C++98 对大括号的支持相对有限,没有像 C++11 引入的统一初始化(Uniform Initialization)那样的功能。

c++11的{}

在 C++11 中,大括号 {} 的使用引入了一些重要的变化和新特性,尤其是在初始化和类型安全方面。以下是主要变化:

  1. 统一初始化(Uniform Initialization)
    C++11 引入了统一初始化语法,可以使用大括号来初始化各种类型的变量,包括基本类型、结构体、类和数组。这种语法帮助避免了一些常见的错误。
int x{5};              // 初始化基本类型
std::vector<int> vec{1, 2, 3};  // 初始化容器
struct Point { int x, y; };
Point p{10, 20};      // 初始化结构体
  1. 列表初始化(List Initialization)
    使用大括号进行列表初始化可以避免窄化转换(narrowing conversion)。例如:
// 这将会导致编译错误,避免窄化转换
int x{2.5};  // 错误:不能从 double 转换为 int
  1. 初始化数组
    在 C++11 中,数组的初始化也可以使用统一初始化语法:
int arr[]{1, 2, 3, 4, 5};  // C++11 引入的数组初始化
  1. nullptr
    虽然 nullptr 不是直接与 {} 相关,但它与统一初始化一起使用时,可以提供更好的类型安全:
int* ptr{nullptr};  // 安全的空指针初始化
  1. lambda 表达式
    在 C++11 中,引入了 lambda 表达式,虽然大括号在这里用作 lambda 的函数体,但这也是一种新的使用方式。
auto func = []() {std::cout << "Hello, World!" << std::endl;
};  // 大括号定义 lambda 的主体
  1. std::initializer_list
    C++11 引入了 std::initializer_list,允许使用大括号初始化自定义类型,方便处理多个值的初始化。
class MyClass {
public:MyClass(std::initializer_list<int> list) {for (auto& item : list) {std::cout << item << " ";}}
};// 使用大括号初始化
MyClass obj{1, 2, 3, 4};
  1. 案例
#include<iostream>
#include<vector>
using namespace std;
struct Point
{int _x;int _y;
};
class Date
{ public:Date(int year = 1, int month = 1, int day = 1):_year(year), _month(month), _day(day){cout << "Date(int year, int month, int day)" << endl;} Date(const Date& d):_year(d._year), _month(d._month), _day(d._day){cout << "Date(const Date& d)" << endl;}
private:int _year;int _month;int _day;
};
// ⼀切皆可⽤列表初始化,且可以不加=int main()
{// C++98⽀持的int a1[] = { 1, 2, 3, 4, 5 };int a2[5] = { 0 };Point p = { 1, 2 };// C++11⽀持的// 内置类型⽀持int x1 = { 2 };// ⾃定义类型⽀持// 这⾥本质是⽤{ 2025, 1, 1}构造⼀个Date临时对象// 临时对象再去拷⻉构造d1,编译器优化后合⼆为⼀变成{ 2025, 1, 1}直接构造初始化d1// 运⾏⼀下,我们可以验证上⾯的理论,发现是没调⽤拷⻉构造的Date d1 = { 2025, 1, 1};// 这⾥d2引⽤的是{ 2024, 7, 25 }构造的临时对象const Date& d2 = { 2024, 7, 25 };// 需要注意的是C++98⽀持单参数时类型转换,也可以不⽤{}Date d3 = { 2025};Date d4 = 2025;// 可以省略掉=Point p1 { 1, 2 };int x2 { 2 };Date d6 { 2024, 7, 25 };const Date& d7 { 2024, 7, 25 };// 不⽀持,只有{}初始化,才能省略=// Date d8 2025;vector<Date> v;v.push_back(d1);v.push_back(Date(2025, 1, 1));// ⽐起有名对象和匿名对象传参,这⾥{}更有性价⽐v.push_back({ 2025, 1, 1 });return 0;
}

std::initializer_list

std::initializer_list 是 C++11 中引入的一个非常有用的特性,允许你使用大括号 {} 进行初始化,特别适用于需要接收多个元素的构造函数、函数参数或其他需要集合的情况。

基本概念
std::initializer_list 是一个轻量级的类模板,提供了一个可迭代的常量数组,可以用来接收初始化的元素。

用法示例

  1. 构造函数
    你可以在类的构造函数中使用 std::initializer_list 来接收多个值。
#include <iostream>
#include <initializer_list>
#include <vector>class MyVector {
public:MyVector(std::initializer_list<int> list) {for (auto& value : list) {vec.push_back(value);}}void print() const {for (const auto& value : vec) {std::cout << value << " ";}std::cout << std::endl;}private:std::vector<int> vec;
};int main() 
{MyVector mv{1, 2, 3, 4, 5};  // 使用 initializer_listmv.print();                  // 输出: 1 2 3 4 5return 0;
}
  1. 函数参数
    std::initializer_list 也可以用作函数的参数,让函数能够接收多个输入。
void printNumbers(std::initializer_list<int> numbers) {for (auto& number : numbers) {std::cout << number << " ";}std::cout << std::endl;
}int main() {printNumbers({10, 20, 30});  // 使用 initializer_listreturn 0;
}
  1. 注意事项
    std::initializer_list 是常量类型,因此你不能修改它的内容。
    由于它是一个轻量级的类,可以在需要的地方使用,避免了额外的内存分配。

右值引用和移动语义

左值和右值的概念

左值和右值是 C++ 中的重要概念,用于区分表达式的值类型。它们在理解资源管理和对象生命周期时尤为关键。

左值 (Lvalue)

  • 定义:左值是指可以被赋值的对象,可以出现在赋值操作符的左侧。左值有持久的存储地址,可以在程序中多次访问。
  • 特点:
    有名字,并且在内存中有一个具体的位置。
    可以被修改,例如变量。
  • 示例:
int a = 10;  // 'a' 是一个左值
a = 20;      // 可以将值赋给左值 'a'

右值 (Rvalue)

  • 定义:右值是指不具名的临时对象,通常是表达式的结果,不能出现在赋值操作符的左侧。右值通常没有持久的存储地址。
  • 特点:
    通常是临时的、不可修改的值。
    可以是字面量、运算结果或函数返回值等。
  • 示例:
int a = 10;        // 10 是一个右值
a = a + 5;        // 'a + 5' 也是一个右值

结合与应用

在 C++11 中,引入了右值引用(&&),使得可以更有效地处理临时对象,优化移动语义,从而提高性能。简单来说左值就是可以取地址的,而右值不可以。

案例

#include<iostream>
using namespace std;
int main()
{// 左值:可以取地址// 以下的p、b、c、*p、s、s[0]就是常⻅的左值int* p = new int(0);int b = 1;const int c = b;*p = 10;string s("111111");s[0] = 'x';cout << &c << endl;cout << (void*)&s[0] << endl;// 右值:不能取地址double x = 1.1, y = 2.2;// 以下⼏个10、x + y、fmin(x, y)、string("11111")都是常⻅的右值10;x + y;fmin(x, y);string("11111");//cout << &10 << endl;//cout << &(x+y) << endl;//cout << &(fmin(x, y)) << endl;//cout << &string("11111") << endl;return 0;
}

左值引用和右值引用

  • Type& r1 = x; Type&& rr1 = y; 第⼀个语句就是左值引⽤,左值引⽤就是给左值取别名,第⼆个就是右值引⽤,同样的道理,右值引⽤就是给右值取别名。

  • 左值引⽤不能直接引⽤右值,但是const左值引⽤可以引⽤右值

  • 右值引⽤不能直接引⽤左值,但是右值引⽤可以引⽤move(左值)

  • template typename remove_reference::type&& move (T&&arg);

  • move是库⾥⾯的⼀个函数模板,本质内部是进⾏强制类型转换,还涉及⼀些引⽤折叠的知识。

  • 需要注意的是变量表达式都是左值属性,也就意味着⼀个右值被右值引⽤绑定后,右值引⽤变量变量表达式的属性是左值。

下面来解释下这句话。

1, 变量表达式都是左值

这意味着所有的变量(如 int a = 10; 中的 a)都是左值,因为它们有一个持久的存储位置,可以被赋值和修改。

2,右值被右值引用绑定

右值引用是 C++11 引入的特性,表示可以绑定到右值的引用,通常用 && 表示。比如:

int&& r = 10; // 10 是一个右值

在这里,r 是一个右值引用,它可以绑定到临时值 10。

3, 右值引用变量的属性是左值

一旦一个右值被绑定到右值引用,虽然这个引用最初绑定的是一个右值,但 r 现在本身是一个可以在赋值中使用的对象。在这种情况下,右值引用 r 的性质变为左值,因为它现在有一个具体的名称和存储位置。

具体示例

int&& r = 10; // r 绑定到右值 10
int a = std::move(r); // std::move(r) 是一个右值,但 r 是左值

在上面的例子中,std::move(r ) 使得 r 的值能够被移动(即转移资源),但 r 本身是一个左值。

template <class _Ty>
remove_reference_t<_Ty>&& move(_Ty&& _Arg)
{ // forward _Arg as movablereturn static_cast<remove_reference_t<_Ty>&&>(_Arg);
}#include<iostream>
using namespace std;
int main()
{// 左值:可以取地址// 以下的p、b、c、*p、s、s[0]就是常⻅的左值int* p = new int(0);int b = 1;const int c = b;*p = 10;string s("111111");s[0] = 'x';double x = 1.1, y = 2.2;// 左值引⽤给左值取别名int& r1 = b;int*& r2 = p;int& r3 = *p;string& r4 = s;char& r5 = s[0];// 右值引⽤给右值取别名int&& rr1 = 10;double&& rr2 = x + y;double&& rr3 = fmin(x, y);string&& rr4 = string("11111");// 左值引⽤不能直接引⽤右值,但是const左值引⽤可以引⽤右值const int& rx1 = 10;const double& rx2 = x + y;const double& rx3 = fmin(x, y);const string& rx4 = string("11111");// 右值引⽤不能直接引⽤左值,但是右值引⽤可以引⽤move(左值)int&& rrx1 = move(b);int*&& rrx2 = move(p);int&& rrx3 = move(*p);string&& rrx4 = move(s);string&& rrx5 = (string&&)s;// b、r1、rr1都是变量表达式,都是左值cout << &b << endl;cout << &r1 << endl;cout << &rr1 << endl;// 这⾥要注意的是,rr1的属性是左值,所以不能再被右值引⽤绑定,除⾮move⼀下int& r6 = r1;// int&& rrx6 = rr1;int&& rrx6 = move(rr1);return 0;
}

引用延长临时对象生命周期

在这里插入图片描述

  1. 基本概念

在 C++ 中,当你创建一个临时对象(通常是一个右值),其生命周期通常非常短暂。在表达式结束后,临时对象会被销毁。但当这个临时对象被一个常量引用(const T&)或者右值引用绑定时,它的生命周期会被延长,直到该引用的作用域结束。

  1. 示例
const std::string& str = "Hello, World!"; // "Hello, World!" 是一个临时对象
std::cout << str; // 在这里可以使用 str,因为临时对象的生命周期被延长

在这个例子中,字符串字面量 “Hello, World!” 是一个临时对象,正常情况下会在语句结束后销毁。然而,由于它被一个 const std::string& 引用绑定,临时对象的生命周期被延长,直到 str 的作用域结束(例如,在函数返回时)。

  1. 适用场景

引用延长生命周期主要用于以下几种情况:

返回值优化:当函数返回一个临时对象时,可以通过返回一个常量引用来延长其生命周期。

避免不必要的复制:使用常量引用可以避免对临时对象的拷贝,提高性能。

  1. 注意事项

不可修改:使用常量引用只能读取临时对象的值,不能修改。

作用域管理:引用的作用域决定了临时对象的有效性,如果引用超出其作用域,临时对象会被销毁,使用引用将导致未定义行为。

左值和右值的参数匹配

在 C++ 中,函数可以接受不同类型的参数,包括左值引用和右值引用。以下是几种常见的参数类型及其匹配规则:

  • 左值引用(T&):只能绑定到左值。
  • 右值引用(T&&):只能绑定到右值。
  • 常量左值引用(const T&):可以绑定到左值和右值,因为常量左值引用允许对临时对象的绑定。
  • 常量右值引用(const T&&):理论上不常用,因为右值引用本身就是临时对象,通常不需要常量修饰。

案例

#include <iostream>
#include <utility> // for std::movevoid process(int& x) {std::cout << "Lvalue reference: " << x << std::endl;
}void process(int&& x) {std::cout << "Rvalue reference: " << x << std::endl;
}void process(const int& x) {std::cout << "Const lvalue reference: " << x << std::endl;
}int main() {int a = 10;process(a);           // 绑定到左值,调用 int& 版本process(20);         // 绑定到右值,调用 int&& 版本process(std::move(a)); // 绑定到右值,调用 int&& 版本process(30);         // 绑定到右值,调用 int&& 版本process(static_cast<const int&>(20)); // 绑定到常量左值引用return 0;
}

选择规则

  • 在函数重载时,C++ 使用以下规则来确定哪个版本的函数将被调用:

  • 精确匹配:如果有左值和右值的重载,C++ 会优先选择精确匹配的版本。

  • const 修饰符:如果没有左值引用匹配,const 左值引用会被考虑。

  • 临时对象:对于右值,右值引用会优先匹配。

左值引用的主要使用场景

左值引用在 C++ 中有许多重要的应用场景:

修改已有对象:左值引用可以用来修改函数外部的变量。例如,当你希望在函数中改变传入的参数值时,可以使用左值引用。

void increment(int& x) {x++;
}int main() {int a = 5;increment(a); // a 现在是 6
}

作为返回值:函数可以返回左值引用,从而允许链式调用。

int& getElement(std::vector<int>& vec, size_t index) {return vec[index];
}

避免复制:通过使用左值引用,可以避免对象的复制,提高性能,特别是在处理大对象时。

缺点
1,左值引用只能绑定到具有持久性地址的对象,不能绑定到临时对象。这意味着无法直接修改临时值或表达式的结果。

2,左值引用可以被赋值,但不能用于移动语义。这意味着使用左值引用的函数不能轻易地转移资源的所有权,从而可能导致不必要的资源开销。

移动构造和移动赋值

移动构造(Move Constructor)

概念
移动构造函数是一种特殊的构造函数,用于通过移动一个对象的资源来初始化另一个对象,而不是复制资源。这种方式适用于临时对象或即将被销毁的对象。

语法
移动构造函数的定义通常采用以下形式:

ClassName(ClassName&& other) noexcept {// 转移资源this->data = other.data;other.data = nullptr; // 使原对象的指针失效
}

示例

class MyString {
public:MyString(const char* str) {data = new char[strlen(str) + 1];strcpy(data, str);}// 移动构造函数MyString(MyString&& other) noexcept {data = other.data;other.data = nullptr; // 使其他对象的指针失效}~MyString() {delete[] data;}private:char* data;
};

移动赋值(Move Assignment Operator)

概念
移动赋值运算符是一种特殊的赋值运算符,用于将一个对象的资源转移到另一个对象,而不是复制资源。这在赋值操作中非常有用,尤其是在处理临时对象时。

语法
移动赋值运算符的定义通常采用以下形式:

ClassName& operator=(ClassName&& other) noexcept {if (this != &other) { // 防止自我赋值delete[] data; // 释放当前资源data = other.data; // 转移资源other.data = nullptr; // 使原对象的指针失效}return *this;
}

示例

class MyString {
public:MyString(const char* str) {data = new char[strlen(str) + 1];strcpy(data, str);}// 移动赋值运算符MyString& operator=(MyString&& other) noexcept {if (this != &other) {delete[] data; // 释放当前资源data = other.data; // 转移资源other.data = nullptr; // 使其他对象的指针失效}return *this;}~MyString() {delete[] data;}private:char* data;
};

意义

  • 性能提升:通过移动构造和移动赋值,C++ 可以避免不必要的深拷贝,显著提升性能,尤其是在处理大对象或复杂数据结构时。

  • 资源管理:移动语义提供了一种简洁且高效的方式来管理动态资源,降低内存分配和释放的频率。

  • 支持临时对象:移动构造和移动赋值使得临时对象的使用变得更加高效,避免了临时对象的拷贝。

引用折叠

有人可能会想到,能不能套娃引用的引用?确实可以,不过直接这样写是报错的,需要通过取巧的方式,这种情况就会引发引用折叠。

在这里插入图片描述

通过typedef的方式或者模板才能引用叠加,不过这种情况下,叠加的引用算什么引用呢?
在这里插入图片描述

引用叠加的情况

通过模板或typedef中的类型操作可以构成引⽤的引⽤时,这C++11给出了⼀个引⽤折叠的规则:右值引⽤的右值引⽤折叠成右值引⽤,所有其他组合均折叠成左值引⽤。

  • 下⾯的程序中很好的展⽰了模板和typedef时构成引⽤的引⽤时的引⽤折叠规则。

  • 像f2这样的函数模板中,T&& x参数看起来是右值引⽤参数,但是由于引⽤折叠的规则,他传递左值时就是左值引⽤,传递右值时就是右值引⽤,有些地⽅也把这种函数模板的参数叫做万能引⽤。

  • Function(T&&t)函数模板程序中,假设实参是int右值,模板参数T的推导int,实参是int左值,模板参数T的推导int&,再结合引⽤折叠规则,就实现了实参是左值,实例化出左值引⽤版本形参Function,实参是右值,实例化出右值引⽤版本形参的Function。

// 由于引⽤折叠限定,f1实例化以后总是⼀个左值引⽤
template<class T>
void f1(T& x)
{}
// 由于引⽤折叠限定,f2实例化后可以是左值引⽤,也可以是右值引⽤
template<class T>
void f2(T&& x)
{}
int main()
{typedef int& lref;typedef int&& rref;int n = 0;lref& r1 = n; // r1 的类型是 int&lref&& r2 = n; // r2 的类型是 int&rref& r3 = n; // r3 的类型是 int&rref&& r4 = 1; // r4 的类型是 int&&// 没有折叠->实例化为void f1(int& x)f1<int>(n);f1<int>(0); // 报错// 折叠->实例化为void f1(int& x)f1<int&>(n);f1<int&>(0); // 报错// 折叠->实例化为void f1(int& x)f1<int&&>(n);f1<int&&>(0); // 报错// 折叠->实例化为void f1(const int& x)f1<const int&>(n);f1<const int&>(0);// 折叠->实例化为void f1(const int& x)f1<const int&&>(n);f1<const int&&>(0);// 没有折叠->实例化为void f2(int&& x)f2<int>(n); // 报错f2<int>(0);// 折叠->实例化为void f2(int& x)f2<int&>(n);f2<int&>(0); // 报错// 折叠->实例化为void f2(int&& x)f2<int&&>(n); // 报错f2<int&&>(0);return 0;
}template<class T>
void Function(T&& t)
{int a = 0;T x = a;//x++;cout << &a << endl;cout << &x << endl << endl;
} 
int main()
{// 10是右值,推导出T为int,模板实例化为void Function(int&&t)Function(10); // 右值int a;// a是左值,推导出T为int&,引⽤折叠,模板实例化为void Function(int& t)Function(a); // 左值// std::move(a)是右值,推导出T为int,模板实例化为void Function(int&& t)Function(std::move(a)); // 右值const int b = 8;// a是左值,推导出T为const int&,引⽤折叠,模板实例化为void Function(const int&t)// 所以Function内部会编译报错,x不能++Function(b); // const 左值// std::move(b)右值,推导出T为const int,模板实例化为void Function(const int&& t)// 所以Function内部会编译报错,x不能++Function(std::move(b)); // const 右值return 0;
}

完美转发

Function(T&&t)函数模板程序中,传左值实例化以后是左值引⽤的Function函数,传右值实例化以后是右值引⽤的Function函数。

  • 变量表达式都是左值属性,也就意味着⼀个右值被右值绑定
    后,右值引⽤变量表达式的属性是左值
    ,也就是说Function函数中t的属性是左值,那么我们把t传递给下⼀层函数Fun,那么匹配的都是左值引⽤版本的Fun函数。这⾥我们想要保持t对象的属性,就需要使⽤完美转发实现。

  • 完美转发forward本质是⼀个函数模板,他主要还是通过引⽤折叠的⽅式实现,下⾯⽰例中传递给Function的实参是右值,T被推导为int,没有折叠,forward内部t被强转为右值引⽤返回;传递Function的实参是左值,T被推导为int&,引⽤折叠为左值引⽤,forward内部t被强转为左值引⽤返回。

template <class _Ty>
_Ty&& forward(remove_reference_t<_Ty>& _Arg) noexcept
{ // forward an lvalue as either an lvalue or an rvaluereturn static_cast<_Ty&&>(_Arg);
} 
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 Function(T&& t)
{Fun(t);//Fun(forward<T>(t));
} 
int main()
{// 10是右值,推导出T为int,模板实例化为void Function(int&& t)Function(10); // 右值int a;// a是左值,推导出T为int&,引⽤折叠,模板实例化为void Function(int& t)Function(a); // 左值// std::move(a)是右值,推导出T为int,模板实例化为void Function(int&& t)Function(std::move(a)); // 右值const int b = 8;// a是左值,推导出T为const int&,引⽤折叠,模板实例化为void Function(const int&	t)Function(b); // const 左值// std::move(b)右值,推导出T为const int,模板实例化为void Function(const int&&	t)Function(std::move(b)); // const 右值return 0;
}

http://www.mrgr.cn/news/59678.html

相关文章:

  • flink使用hikariCP数据库连接池,导致连接泄露
  • 2024.10.27 直接插入排序 非递归后序遍历(复杂版)
  • 5G IMS开户需要哪些信息
  • 防止keeplived脑裂
  • 【leetcode】动态规划
  • 程序员数学:用Python学透线性代数和微积分 中文目录
  • #【2024年10月26日更新】植物大战僵尸杂交本V2.6更新内容与下载
  • Python毕业设计选题:基于Django+Vue的图书馆管理系统
  • Docker 实践与应用举例教程:从入门到精通
  • python实现放烟花效果庆祝元旦
  • 若依框架部署到服务器后头像资源访问404
  • 生产环境直接热编译
  • 近似线性可分支持向量机 代码实现
  • 使用代理服务器后sse数据合并问题
  • C++的继承和多态
  • 基于用户体验的在线相册管理平台创新设计与实现
  • Linux日志分析-应急响应实战笔记
  • CSS中为特定的元素设置背景图片(Top1,Top1,Top3)
  • 记一次内存溢出排查(dump文件)
  • RHCE nfs作业
  • C++11实践指北
  • OpsAny社区版与专业版的主要区别是什么
  • 农作物大豆病虫害识别分类数据集(猫脸码客第227期)
  • 基于IMX6ULL开发板LCD点阵显示字符学习
  • C++——string的模拟实现(上)
  • Fofa搜索技巧(理论加实践的整理)