【C++】13. C++11新特性【上】

目录

C++11简介

一、列表初始化

1.大括号初始化

[2. initializer_list](#2. initializer_list)

二、右值引用和移动语义

[1. 左值和右值](#1. 左值和右值)

[2. 左值引用和右值引用](#2. 左值引用和右值引用)

3.右值引用的的意义

4.移动构造和移动赋值

[1. 为什么需要移动语义?先看拷贝的代价](#1. 为什么需要移动语义?先看拷贝的代价)

[2. 移动构造函数](#2. 移动构造函数)

[3. 移动赋值函数](#3. 移动赋值函数)

[4. 显式声明:= default / = delete](#4. 显式声明:= default / = delete)

5.引用折叠

5.完美转发

[1. 问题:普通转发会"丢失"右值属性](#1. 问题:普通转发会"丢失"右值属性)

[2. 解决:用 forward 还原左右值属性](#2. 解决:用 forward 还原左右值属性)

[3. forward 为什么能做到?三件套原理](#3. forward 为什么能做到?三件套原理)

三、lambda

[lambda 表达式各部分说明](#lambda 表达式各部分说明)

捕捉列表

①显示捕捉

②隐式捕捉

③混合捕捉

捕捉列表的其他要点


C++11简介

C++11 是 C++ 发展过程中一次具有里程碑意义的版本更新。相比之前的 C++98标准,C++11 不仅对原有语言特性进行了完善,还加入了大量新的语法特性和标准库组件,使 C++ 在保持高性能、灵活性以及底层控制能力的同时,代码编写也变得更加简洁、高效和现代化。

C++11 能更好地用于系统开发和库开发、语言更加泛华和简单化、更加稳定和安全,不仅功能更强大,而且能提升程序员的开发效率,公司实际项目开发中也用得比较多。

一、列表初始化

1.大括号初始化

在c++98中,允许数组 和结构体使用大括号初始化,例如:

C++11想统一初始化方式,试图实现一切对象皆可用**{}** 初始化,{}初始化也叫做列表初始化

内置类型支持,自定义类型也支持。自定义类型本质是类型转换,中间会产生临时对象(依赖于构造函数),最后优化了以后变成直接构造。

{}初始化的过程中,可以省略掉=。

2. initializer_list

上面的初始化已经很方便了,但是对象容器初始化还是不太方便,比如一个 vector 对象,我想用N个值去构造初始化,那么我们得实现很多个构造函数才能支持。所以这是很不方便的。

C++11库中提出了一个 std::initializer_list 的类,这个类的本质是底层开一个数组,将数据拷贝过来,std::initializer_list 内部有两个指针分别指向数组的开始和结束。

std::initializer_list 支持迭代器遍历。

cpp 复制代码
#include <iostream>
#include <vector>
#include <list>
#include <map>
using namespace std;

int main()
{
    vector<int> v = { 1,2,3,4,5 };
    list<double> l = { 3.6,5.9,4.8 };
    map<string, int> m = { {"hello", 1}, {"nihao", 3} };
    return 0;
}

//这里赋值使用的大括号及其内部元素,即是initializer_list。

二、右值引用和移动语义

1. 左值和右值

左值: 一般是一个持久存在的表达式,有明确内存地址可以进行取地址操作

右值:指那些临时性的 ,即将被销毁的 ,以及常量表达式,没有明确的内存地址,不能进行取地址操作

2. 左值引用和右值引用

左值引用是给左值取别名,右值引用是给右值取别名。举个例子:

cpp 复制代码
int main()
{
	int a = 0;
	int& r1 = a; //左值引用
	int&& r2 = 1; //右值引用
	return 0;
}

左值引用不能直接引用右值,需要加上const;右值引用不能直接引用左值,需要将左值使用move函数包装起来(move将左值转化为右值引用)。示例:

cpp 复制代码
int main()
{
	const int& r1 = 1; //左值引用引用右值
 
	int a = 0;
 
	//右值引用引用左值
	int&& r2 = (int&&)a; //强转
	int&& r3 = std::move(a); //使用move函数
	return 0;
}

3.右值引用的的意义

const左值引用和右值引用都可以延长临时对象的生命周期,直至引用的生命周期结束,但const左值引用所引用的对象不可被修改;而通过右值引用,可以修改所引用的临时对象,因为右值引用本身是左值。

cpp 复制代码
int main()
{
	const int& r1 = 1 + 3;
 
	r1++; // 报错,表达式必须是可修改的左值
 
	int&& r2 = 1 + 3;
 
	r2++; // 可以修改
	return 0;
}

func1 使用左值引用传参,可以减少对象拷贝,提高效率。func2 也可以返回左值引用,但要注意对象的生命周期:如果引用的是函数内部的局部对象,那么函数执行结束后对象就被销毁了,返回的引用也就失效了,因此这种情况不能使用左值引用返回。

str 是函数内部的局部对象,函数结束后就会被销毁。因此这里不能使用左值引用返回,否则返回的引用会指向一个已经被销毁的对象,形成悬空引用。这里使用按值返回是没有问题的。

局部对象出作用域就会销毁,所以会拷贝构造一个临时对象,然后再拷贝构造给ret。不过vs2022编译器会优化,连续的构造、拷贝构造会优化为直接构造。

4.移动构造和移动赋值

原来 C++ 类中,有 6 个默认成员函数:构造函数,析构函数,拷贝构造函数,拷贝赋值重载,取地址重载,const 取地址重载。

而 C++11 新增了两个:移动构造函数和移动赋值运算符重载。

移动构造的本质是指针转移(把别人的资源"窃"过来),不是再深拷贝一份------适合临时对象(右值),性能提升巨大。

1. 为什么需要移动构造?先看拷贝的代价

cpp 复制代码
std::string s1 = "一串很长的字符串...";          // 假设底层 char[1000000]
std::string s2 = s1;                              // 深拷贝:开新空间、复制 100w 字节
std::string s3 = std::move(s1);                   // 移动:s1 把指针"给"s3,自己置空
// 结果:s1 = "",s2 和 s3 各持一份数据,但 s3 这份是"窃"来的,没花复制成本

2. 移动构造函数

构造函数的一种重载,专门处理"用右值初始化新对象"。

cpp 复制代码
class String {
private:
    char* _str;
    size_t _size;
    size_t _capacity;
public:
    // 移动构造(窃取------把别人指针拿过来)
    String(String&& s) {                // 第一个参数必须是右值引用 T&&
        _str = s._str;                  // 1. 直接把对方的指针拿过来
        _size = s._size;
        _capacity = s._capacity;
        s._str = nullptr;               // 2. 关键一步:把对方置空(防止它析构时把资源也释放掉)
    }
    ~String() { delete[] _str; }
};

3. 移动赋值函数

已存在的对象 = 另一个对象,分两种情况:左值用拷贝赋值(operator=),右值用移动赋值(operator&&)。

cpp 复制代码
// 移动赋值(参数用值传递,内部 swap,天然异常安全)
String& operator=(String s) {            // 形参 s 是实参的拷贝(这里要触发移动构造)
    swap(s);                             // 1. 把 this 的资源和 s 交换
    return *this;                        // 2. s 离开作用域时,析构的是 this 原来的资源
}
// 移动版(C++11 之后常写为更明显的右值引用版本)
String& operator=(String&& s) {
    if (this != &s) {                    // 自赋值检查
        delete[] _str;                   // 先释放自己原有的资源
        _str = s._str;
        _size = s._size;
        _capacity = s._capacity;
        s._str = nullptr;
    }
    return *this;
}
  • 你没写 :析构函数、拷贝构造、拷贝赋值、移动构造、移动赋值 中的任何一个 ,编译器就不再自动生成移动构造和移动赋值。
  • 反过来:你 写了其他四个 (析构/拷贝构造/拷贝赋值/普通赋值),没碰 移动相关的,编译器默认生成移动版本。

为什么这么反直觉?因为移动是"窃资源",如果你的类需要自己写析构(说明有资源要管)或者写拷贝构造(说明深拷贝逻辑特殊),编译器就不敢默认生成移动版本,怕行为对不上。

4. 显式声明:= default / = delete

写法 含义
String(String&&) = default; 强制编译器生成默认移动构造(即便你写了析构)
String& operator=(String&&) = default; 同上,移动赋值
String(String&&) = delete; 显式禁止 移动构造(std::move(s) 编译报错)
String& operator=(String&&) = delete; 同上,移动赋值

= default 常用于"我虽然写了析构/拷贝,但移动我想要编译器默认行为就够了"的场景;= delete 常用于"我这个类不能被移动(资源特殊)"的禁止场景。

5.引用折叠

C++ 规定引用不能引用引用 (不存在"引用的引用"),所以当模板推导出 T& &&T&& & 这种"引用套引用"的类型时,编译器要按规则折叠成一个引用 ------这就是引用折叠

cpp 复制代码
template <typename T>
void f(T&& x) { }

int a = 10;
f(a);            // 传左值 → T 推导为 int&  → 参数是 int& && → 折叠为 int&
f(std::move(a)); // 传右值 → T 推导为 int   → 参数是 int&&      → 保持右值引用

5.完美转发

完美转发 = 保留参数的左/右值属性------传进来是左值就给下家左值,是右值就给下家右值,既不丢也不改。

1. 问题:普通转发会"丢失"右值属性

std::movea 变成了右值,但一旦进入 wrapper,arg 就是一个有名字的变量(C++ 标准:有名字的就是左值),右值属性就丢了。这就是为什么要"完美转发"。

2. 解决:用 forward 还原左右值属性
3. forward 为什么能做到?三件套原理

①模板形参 T&& 是万能引用(forwarding reference)------它能根据实参类型自动推导:

  • 传左值 aT 推导为 int&
  • 传右值 10move(a)T 推导为 int

② forward 内部靠引用折叠把 T 还原

复制代码
// std::forward 的实现本质(C++11/14简化版)
template <class T>
T&& forward(typename remove_reference<T>::type& arg);

// C++11 完整版还有第二个重载(用于显式 T 时传右值场景)
template <class T>
T&& forward(typename remove_reference<T>::type&& arg);

三、lambda

lambda 是 C++11 引入的匿名函数对象 ,可以写在任何需要"一段可调用逻辑"的地方------编译器在底层会把它翻译成一个operator() 的匿名类(即仿函数),捕获的变量变成这个匿名类的成员。

lambda 表达式书写格式:[capture-list] (parameters) mutable -> return-type { statement }

lambda 表达式各部分说明

  • capture‑list:捕捉列表 该列表总是出现在 lambda 函数的开始位置,编译器**根据[]**来判断接下来的代码是否为 lambda 函数,捕捉列表能够捕捉上下文中的变量供 lambda 函数使用。
  • (parameters):参数列表 与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略。
  • mutable 默认情况下,lambda 函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)
  • ->return‑type:返回值类型 用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
  • {statement}:函数体 在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。
cpp 复制代码
#include <iostream>
using namespace std;
int main()
{
    //简单的lambda表达式
    auto f = [](int x, int y)->int {return x + y; };
    //调用
    cout << f(1, 2) << endl;
    return 0;
}

在定义lambda表达式时,我们要遵守如下规则:

  1. 捕捉列表的" "不能省略。

  2. lambda表达式的参数为空时,参数列表(包括"( )")可以省略。

  3. 返回值类型(包括"->")可以省略。

  4. 若返回值类型已写,那么就算没有参数,参数列表的"( )"也不能省略。

  5. 函数体的"{ }"不能省略。

捕捉列表

lambda表达式的函数内部**只能使用参数列表的变量,**这时要想使用外部变量,就需要依靠捕捉列表了。捕捉列表分为三种,显示捕捉,隐式捕捉,混合捕捉

①显示捕捉

显示捕捉就是在捕捉列表里写自己要使用的变量。

cpp 复制代码
int main()
{
	int a = 1;
	int b = 3;

	auto f = [a](int x)
		{
			//return a + b + x; // 报错,b不能使用
			return a + x; //捕捉列表中写明了a,可以使用a

		};    

	cout << f(1) << endl;
	return 0;

}

这里a的值不能修改,因为是传值捕捉,若要修改就得变成传引用捕捉[&a]了

②隐式捕捉

隐式捕捉就显得十分便利:它的机制是函数体内部使用了哪些变量就捕捉哪些变量。自动捕捉

auto f1 = = {}; // 隐式的传值捕捉

auto f2 = \& {}; // 隐式的传引用捕捉

③混合捕捉

如果我们需要在隐式捕捉的基础上,确保其中某些值可以被修改或不能被修改,那么就可以使用混合捕捉。

cpp 复制代码
auto f1 = [=, &a] {}; // 其他变量使用传值捕捉,a使用传引用捕捉

auto f2 = [&, a] {}; // 其他变量使用传引用捕捉,a使用传值捕捉

混合捕捉列表的第一个位置必须是=或&。

捕捉列表的其他要点

  1. 全局变量和静态变量不需要,也不能通过捕捉列表进行捕捉。

    Lambda 表达式可以在作用域允许的情况下直接访问它们。

  2. 对于值捕捉,Lambda 会将外部变量的值复制一份,保存到 Lambda 对象内部。

    默认情况下,这些通过值捕捉得到的成员不能在 Lambda 函数体内修改。

    如果希望修改 Lambda 内部保存的副本,同时不影响外部原变量 ,可以使用 mutable 关键字。

cpp 复制代码
int main()
{
    int a = 0;

    auto f = [=]() mutable -> int
    {
        a++;        // 修改的是 Lambda 内部保存的 a 的副本
        return a;
    };

    cout << f() << endl;  // 1
    cout << a << endl;    // 0,外部的 a 没有改变

    return 0;
}

注意:在 C++11~C++20 中,使用 mutable 时,即使 Lambda 没有参数,也需要写出空参数列表 ()。

相关推荐
SomeB1oody1 小时前
【RustyML入门】7.4. 按需裁剪与模块化集成
开发语言·后端·机器学习·rust·教程
wabs6661 小时前
关于栈【力扣1047. 删除字符串中的所有相邻重复项的思考】
数据结构·c++·算法·leetcode··代码随想录
'2061 小时前
**写了个自动整理下载视频的脚本:按片名归档、自动重命名、生成目录索引
开发语言·python·ffmpeg·音视频·cctv
xys_6782 小时前
深挖C语言:动态内存管理
c语言·开发语言
粥里有勺糖2 小时前
视野修炼-技术周刊第130期 | 光影效果
前端·javascript·github
OPEN-F2 小时前
Python进阶教程:项目工程化与虚拟环境
开发语言·python
吴声子夜歌2 小时前
Java面试——基础
java·开发语言·面试
橘色的喵2 小时前
PySide6 工业上位机的实时帧链、零拷贝与跨语言架构
c++·架构·图像·pyside
小岛前端2 小时前
AI Skills 已经封神,但新的问题却越来越严重!
前端·后端·github