动画网站欣赏,外贸网站怎么做推广,上海企业网站备案,网站后台无法访问文章目录 前言#x1f4da;一、C11的历史发展#x1f4d6;1.1 C11 之前的背景#x1f4d6;1.2 C11 的发展历程#x1f4d6;1.3 C11 的主要设计目标#x1f4d6;1.4 C11 的主要特性#x1f4d6;1.5 C11 的影响 #x1f4da;二、统一的列表初始化#x1f4d6;2.1 基本列表… 文章目录 前言一、C11的历史发展1.1 C11 之前的背景1.2 C11 的发展历程1.3 C11 的主要设计目标1.4 C11 的主要特性1.5 C11 的影响 二、统一的列表初始化2.1 基本列表初始化2.2 使用初始化列表初始化 STL 容器2.3 类构造函数中的列表初始化2.4 列表初始化防止隐式窄化转换2.5 默认初始化2.6 聚合类型的列表初始化 三、decltype——编译时获取表达式的类型3.1 如何声明一个未知类型变量3.2 获取自定义类的成员函数类型 四、左、右值引用和移动语义4.1 什么是左值右值1. 左值Lvalue2. 右值Rvalue3. 左值和右值的主要区别 4.2 左值引用和右值引用1. 左值引用Lvalue Reference2. 右值引用Rvalue Reference3. 左值引用与右值引用的区别4. 左值引用能否给右值取别名?5. 右值引用能否给左值取别名?总结 4.3 右值引用的使用场景和意义1. 移动构造函数2. 移动赋值运算符3. 为什么要使用 noexcept 五、完美转发5.1 模板中的万能引用5.2 forwardT完美转发 结语 前言
在现代C编程中性能优化和资源管理一直是开发者追求的目标。C11引入的右值引用rvalue reference和移动语义move semantics为解决这些问题提供了强有力的工具。通过右值引用我们能够更高效地处理临时对象而移动语义的引入则进一步优化了对象的资源转移和管理。在这篇文章中我们将深入探索右值引用和移动语义的核心概念、实现原理以及它们在实际开发中的应用场景。 一、C11的历史发展
C11 是 C 标准的一次重大更新于 2011 年发布。它引入了许多新的特性和改进使得 C 更加现代化、高效且易于使用。C11 的发布可以说是 C 语言的一次**“复兴”**在过去几十年中标准化组织ISO对 C 语言的不断完善起到了重要作用。
1.1 C11 之前的背景
在 C11 之前C 的最新标准是 C98 和它的技术修正C03。C98 于 1998 年发布建立了 C 的基础特性如模板、标准模板库STL、异常处理等。C03 是对 C98 的一些小修订主要是修复了 C98 中的缺陷并未引入新的语言特性。随着计算机硬件的快速发展和软件开发需求的变化C98 和 C03 逐渐显得陈旧无法满足更高效、更现代化的软件开发需求。
1.2 C11 的发展历程
C11 的标准化过程可以追溯到 2002 年。C 标准委员会ISO/IEC JTC1/SC22/WG21开始对 C 语言进行改进的讨论目标是让 C 语言更加高效和现代化同时保持其核心的性能和灵活性。在这个过程中多个 C 提案被提出委员会从这些提案中选取了对语言发展最为重要的部分进行标准化。经过近 10 年的讨论和修改最终在 2011 年发布了 C11 标准。
1.3 C11 的主要设计目标
C11 的设计目标主要包括以下几个方面
提高程序性能和效率为了解决大型项目中的性能瓶颈C11 引入了移动语义、智能指针、多线程支持等特性。改进编程体验C11 提供了许多语法改进使得代码更简洁清晰如自动类型推导auto、范围 for 循环、初始化列表等。增强可移植性和可维护性通过标准库的扩展和新功能C11 提供了更好的跨平台支持和更高的代码可维护性。
1.4 C11 的主要特性
C11 引入了大量新特性使得 C 语言得到了显著的改进。以下是一些主要特性
右值引用和移动语义通过右值引用T和 std::move 实现移动语义优化了资源管理和对象拷贝。自动类型推导auto 关键字可以自动推导变量类型使代码更加简洁。智能指针std::shared_ptr、std::unique_ptr 和 std::weak_ptr 解决了原始指针的内存管理问题。Lambda 表达式引入了 Lambda 表达式使得 C 具备了更现代化的函数式编程能力。多线程支持标准库引入了 std::thread、std::mutex 等多线程工具为并发编程提供了标准化的支持。新容器如 std::unordered_map 和 std::array 等丰富了 C 的数据结构。范围 for 循环更简洁的循环语法便于遍历容器。constexpr引入 constexpr 关键字支持编译时常量计算提升了程序的执行效率。初始化列表统一的初始化语法提供了更灵活的初始化方式。空指针常量引入 nullptr 代替原来的 NULL避免类型不安全的问题。静态断言static_assert 允许在编译期进行断言检查提高了代码的健壮性。
1.5 C11 的影响
C11 的发布使得 C 语言变得更加强大和现代化成为工业级开发的主流选择之一。以下是 C11 的主要影响
编程范式的改变C11 引入了许多现代化的语法和特性使得编程范式从传统的面向对象逐渐向更简洁和高效的函数式编程发展。提高了开发效率通过更简洁的语法和自动化的内存管理工具如智能指针C11 提高了开发效率降低了代码复杂度。促进了后续标准的制定C11 的成功促使标准委员会在此基础上继续改进推出了 C14、C17、C20 和 C23使 C 语言保持了活力和竞争力。
二、统一的列表初始化
在 C11 中列表初始化List Initialization是一种新的初始化方式它允许使用花括号 {} 来初始化变量和对象。这种方式提供了更一致和灵活的初始化方法避免了一些潜在的错误。列表初始化主要有以下几种形式
2.1 基本列表初始化
最常见的列表初始化形式是直接用 {} 初始化变量或对象。这种方式可以应用于内置类型、类类型和数组。
int a{5}; // a 初始化为 5
double b{3.14}; // b 初始化为 3.14
int arr[3]{1, 2, 3}; // 初始化数组2.2 使用初始化列表初始化 STL 容器
C11 允许通过列表初始化来直接构造 STL 容器。
#include vector
#include iostream
using namespace std;int main() {vectorint vec{1, 2, 3, 4}; // 使用列表初始化向量for (int val : vec) {cout val ; // 输出: 1 2 3 4}return 0;
}2.3 类构造函数中的列表初始化
C11 引入了 std::initializer_list使得可以通过列表初始化构造类对象。为此类需要实现接受 std::initializer_list 的构造函数。
#include initializer_list
#include iostream
using namespace std;class MyClass {
public:MyClass(initializer_listint list) {for (auto val : list) {cout val ;}cout endl;}
};int main() {MyClass obj{1, 2, 3, 4}; // 使用列表初始化return 0;
}在这个例子中MyClass 接受一个 std::initializer_listint 类型的参数可以在初始化时传入多个值。
2.4 列表初始化防止隐式窄化转换
列表初始化可以防止某些类型转换错误例如浮点数到整数的窄化转换从而提高代码的安全性。C11 标准规定列表初始化不允许隐式的窄化转换。
int x{3.14}; // 错误3.14 是 double不能隐式转换为 int上面代码会报错因为 3.14 是 double 类型而列表初始化不允许将 double 隐式转换为 int。
2.5 默认初始化
通过列表初始化可以直接实现默认初始化使用 {} 直接初始化没有提供具体值。
int x{}; // x 初始化为 0
double y{}; // y 初始化为 0.0
std::string s{}; // s 初始化为空字符串2.6 聚合类型的列表初始化
对于聚合类型如数组、struct可以使用列表初始化为其成员赋值。
多参数构造函数的隐式类型转换
struct Point {Point(int x, int y):_x(x),_y(y){}int _x;int _y;
};Point p{10, 20}; // 使用列表初始化 struct 成员explicit关键字避免隐式类型转换带来的不确定性
struct Point {explicit Point(int x, int y):_x(x),_y(y){}int _x;int _y;
};Point p{10, 20}; // 会报错三、decltype——编译时获取表达式的类型
3.1 如何声明一个未知类型变量
总所周知在C98之中有这样一个运算符名叫typeid它可以查看任何变量或者函数的类型例如
int main() {int i 10;auto p i;auto pf malloc;cout typeid(p).name() endl;cout typeid(pf).name() endl;return 0;
}很显然typeid推出的类型只能看不能用当然你也可以将其打印出来再确定它的类型之后手动声明或定义新变量不过这样未免显得有点太繁琐。于是我们想到经常使用的auto关键字
int i 10;
auto p i; // 只能定义变量 可以发现使用auto必须要给左值添加一个右值用来推导类型可有些时候我们只想声明先不想赋值该怎么办C11推出了一个新的关键字叫做decltype用于在编译时获取表达式的类型。它允许开发者在不显式指定类型的情况下获取变量或表达式的类型信息从而提高代码的灵活性和可维护性。
基本用法
decltype 的基本语法是 decltype(expression)其中 expression 是一个有效的 C 表达式或者变量。编译器会分析表达式或者变量的类型并将其作为 decltype 的结果类型。
示例
int main() {auto pf malloc;decltype(pf) pf2;cout typeid(pf2).name() endl;return 0;
}3.2 获取自定义类的成员函数类型
decltype 可以用于获取类成员变量的类型这在使用模板和泛型编程时非常有用。
templateclass Func
class B{
private:Func _f;
};int main() {auto pf malloc;Bdecltype(pf) b1;cout b1-type: typeid(b1).name() endl;const int x 1;double y 2.2;Bdecltype(x * y) b2;cout b2-type: typeid(b2).name() endl;return 0;
}四、左、右值引用和移动语义
4.1 什么是左值右值
在C中左值Lvalue和右值Rvalue是表达式类型的重要概念。它们决定了表达式的“值类别”即表达式的结果可以用于什么类型的操作比如赋值、地址取用等。
1. 左值Lvalue
左值LvalueLocator value是一个可以取地址的表达式表示一个持久的、可命名的存储位置。它可以出现在赋值运算符的左边也就是说它是可以被赋值的对象。
特点具有持久性可多次访问。示例变量、数组元素、解引用的指针等。用法左值通常用于表示可以被修改的对象但需要注意的是有些左值可能是const的即使是左值也不能修改。
示例
int x 10; // x 是一个左值可以赋值
int* p x; // 可以取 x 的地址在这里x就是左值因为我们可以取它的地址并在后续操作中多次使用它。
2. 右值Rvalue
右值RvalueRead value是一个不持久的、临时的值通常是表达式的结果。它不能取地址通常出现在赋值的右侧。右值通常是字面量、临时对象或是表达式的计算结果不能重复使用。
特点通常为临时值只在表达式中短暂存在。示例字面量如10、表达式如x y、临时对象。用法右值不能直接取地址不能在后续操作中重复使用除非绑定到右值引用。
示例
int y 5 3; // 5 3 是一个右值
int z y * 2; // y * 2 是一个右值这里5 3 和 y * 2是右值它们是表达式的计算结果不能取地址。
3. 左值和右值的主要区别
特性左值Lvalue右值Rvalue持久性是持久性的是临时性的可赋值性可以出现在赋值运算符的左边通常不能出现在赋值的左边取地址可以取地址不能取地址用途可多次访问的对象通常为表达式结果或临时值(将亡值)
4.2 左值引用和右值引用
在C中左值引用和右值引用是两种不同的引用类型主要用于资源管理、性能优化和控制对象的生命周期。它们分别是为左值持久对象和右值临时对象设计的。
1. 左值引用Lvalue Reference
左值引用T是C中最常见的引用类型用于引用变量、对象等持久化的左值通常用于需要在多个地方访问和修改同一对象的情况。
定义T例如int ref x;绑定对象左值引用只能绑定到左值上如变量、数组元素等。典型用途 传递和修改函数参数。提高效率避免函数参数的拷贝。提供统一的接口来操作对象。
示例
void updateValue(int ref) {ref 20; // 修改原始对象
}int main() {int x 10;updateValue(x); // 传递左值引用直接修改 xcout x; // 输出 20
}在这个例子中updateValue函数使用左值引用来修改传入的参数x避免了不必要的拷贝。
2. 右值引用Rvalue Reference
右值引用T是C11引入的一种新型引用类型用于绑定到右值如临时对象或表达式的计算结果。右值引用允许在编程中直接使用和操作临时对象是实现移动语义的关键。
定义T例如int rref 5;绑定对象右值引用只能绑定到右值临时值上比如常量、表达式结果 函数的传值返回值不能是左值引用返回值等。典型用途 实现移动语义右值引用可以通过转移资源而非复制资源来优化程序性能。避免不必要的拷贝右值引用允许在需要生成临时对象的地方避免对象拷贝从而提高效率。
示例实现移动语义
class MyClass {
public:int* data;MyClass() : data(new int[1000]) {}// 移动构造函数MyClass(MyClass other) : data(other.data) {other.data nullptr; // 转移资源}~MyClass() { delete[] data; }
};MyClass createMyClass() {MyClass temp;return temp; // 返回右值触发移动构造
}在这里createMyClass函数返回一个临时对象右值可以通过移动构造函数实现资源转移避免拷贝从而提高性能。
3. 左值引用与右值引用的区别
特性左值引用T右值引用T绑定对象只能绑定到左值只能绑定到右值常见用途函数参数传递和修改、避免拷贝移动语义、转移资源所有权、优化性能示例int ref x;int rref 5 3;用法限制不能绑定右值不能直接绑定左值需std::move转换
4. 左值引用能否给右值取别名?
在C中左值引用不能直接绑定到右值。通常情况下左值引用T只能绑定到左值而不是右值。右值是临时的、短暂存在的值而左值引用需要绑定到一个持久的、可以命名的对象因此不能直接给右值取别名。
间接方式通过 const 左值引用绑定右值
不过const左值引用const T可以绑定到右值。这是因为 const 左值引用不会修改绑定对象的值允许在函数中引用临时对象或字面量等右值。使用 const T 可以间接为右值取别名。
示例
void print(const int ref) {cout ref endl;
}int main() {print(10); // 10 是右值但可以绑定到 const int 上
}在这个例子中字面量 10 是右值但可以通过 const int 引用传递给 print 函数。通过这种方式可以间接地为右值取一个别名。
5. 右值引用能否给左值取别名?
右值引用不能直接给左值取别名。右值引用T的设计初衷是用于绑定右值即临时对象来实现移动语义。因此右值引用只能绑定到右值不能直接绑定到左值。
但是通过 std::move 可以实现
如果希望将左值转化为右值引用可以使用 std::move 将左值转换成右值来绑定到右值引用。std::move 不会真正移动数据只是将左值“视为”右值以便能够绑定到右值引用。
示例
void process(int rref) {cout Processing value: rref endl;
}int main() {int x 10; // x 是一个左值process(move(x)); // 将 x 转换为右值引用可以绑定到 int
}在这个例子中std::move(x)将左值x转换为右值引用从而能够绑定到右值引用参数rref上。
为什么右值引用不直接绑定左值
右值引用的目的是为了避免拷贝通过资源转移提升效率而左值通常是需要继续使用的持久对象不适合绑定到右值引用右值引用的绑定会引导资源转移导致左值状态不可预测。因此设计上不允许右值引用直接绑定左值除非明确使用 std::move 来告知编译器。 总结 左值引用只能引用左值不能引用右值 但是const左值引用既可以引用左值也可以引用右值 右值引用只能引用右值不能引用左值 但是右值引用可以引用move以后的左值
4.3 右值引用的使用场景和意义
在 C11 中为了提高程序的性能增加了移动构造函数和移动赋值运算符它们使对象的资源可以从一个对象“移动”到另一个对象而不是进行深拷贝。这样可以显著减少不必要的内存分配和复制尤其是对于动态分配资源的类如包含指针的类而言。
1. 移动构造函数
移动构造函数的作用是通过“移动”资源来构造一个新对象而不是“复制”资源。这意味着资源的所有权将从源对象转移到目标对象而源对象在移动后通常会处于“空”或“无效”的状态但仍然可析构。
移动构造函数的定义使用右值引用 通常在构造函数声明中使用以下形式
class MyClass {
public:MyClass(MyClass other) noexcept; // 移动构造函数
};假设我们有一个简单的类 MyClass包含一个动态分配的数组指针
#include iostream
#include string
using namespace std;class MyClass {
public:string data;// 普通构造函数MyClass(const string str) : data(str) {}// 移动构造函数MyClass(string str) noexcept : data(move(str)) {cout Move constructor called\n;}
};int main() {string temp Hello;MyClass obj(move(temp)); // 调用移动构造函数cout temp after move: temp endl; // temp 可能为空return 0;
}在上面的例子中std::move(temp) 将 temp 转换为右值触发移动构造函数将 temp 的资源移动到 obj 中。这避免了深拷贝提高了效率。
输出结果 2. 移动赋值运算符
移动赋值运算符用于在赋值操作中转移资源的所有权。它通常用于将一个临时对象或不再需要的对象的资源“移动”到另一个已存在的对象上。
移动赋值运算符同样使用右值引用 并返回当前对象的引用 *this
class MyClass {
public:MyClass operator(MyClass other) noexcept; // 移动赋值运算符
};在前面的 MyClass 基础上我们可以实现移动赋值运算符
class MyClass {
public:string data;// 普通构造函数MyClass(const string str) : data(str) {}// 移动构造函数MyClass(string str) noexcept : data(move(str)) {cout Move constructor called\n;}// 移动赋值运算符重载MyClass operator(MyClass other) noexcept {if (this ! other) {data move(other.data); // 使用 move 将资源移动other.data.clear(); // 清空 other 的 datacout Move Assigned called\n;}return *this;
}
};int main() {string temp Hello;MyClass obj(move(temp)); // 调用移动构造函数MyClass obj2(World);obj move(obj2); // 调用移动赋值运算符cout obj2.data after move: obj2.data endl; // obj2.data 可能为空return 0;
}在这个示例中
移动赋值运算符首先检查对象是否为自赋值。如果不是自赋值则释放当前对象的资源将 other.data 转移到 this-data并将 other.data 置空防止重复释放。
输出结果 3. 为什么要使用 noexcept
通常在移动构造函数和移动赋值运算符中添加 noexcept表示该操作不会抛出异常。这是因为许多标准库容器会检查移动操作是否为 noexcept以决定是否使用移动操作。
五、完美转发
5.1 模板中的万能引用
在C11的模板编程中****代表万能引用既能接收左值又能接收右值。我们以下面的代码为例分析一下 在模板中的意义
代码示例
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; }// 万能引用既可以接收左值又可以接收右值
templatetypename T
void PerfectForward(T t){Fun(t);
}
int main(){PerfectForward(10); // 右值int a;PerfectForward(a); // 左值PerfectForward(move(a)); // 右值const int b 8;PerfectForward(b); // const 左值PerfectForward(move(b)); // const 右值return 0;
}注意看我定义了4个Fun函数用来判断PerfectForward函数接收的左值或者右值能否在Fun函数中持续左值或者右值的状态。
输出结果 让人意想不到的是打印出来的竟然全部都是左值引用这究竟是怎么一回事呢我们先拿第一行代码PerfectForward(10); 解释一下PerfectForward 的形参 t 接收了一个右值 10 这里是右值引用。不过在函数体中调用了 Fun(t); 这一语句**而此时的 t 却是完完全全的一个左值因为右值引用变量的属性会被编译器识别成左值否则在移动构造的场景下无法完成资源转移必须要修改。**所以 Fun() 函数只会调用 void Fun(int x) 。main 函数中的其他关于右值的语句也都是犯了这样一个错误当然左值不受影响。**总的来说引用类型的唯一作用就是限制了接收的类型后续使用中都退化成了左值。**为了防止这种错误的大面积发生C11做出了相应调整增加了一个函数模板叫做std::forwardT主要用于实现 完美转发perfect forwarding。它可以根据参数的类型是左值还是右值保留参数的值类别即左值或右值并转发给另一个函数。
5.2 forwardT完美转发
std::forwardT 的主要作用是保持传入参数的值类别左值或右值并正确地转发给接收方函数。它通常用于模板函数中使得可以处理并转发任意的值类别。它的使用场景是右值引用和模板参数的结合。
对于以上代码我们就可以进行更改啦:
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; }templatetypename T
void PerfectForward(T t) {// t是左值引用保持左值属性// t是右值引用保持右值属性Fun(forwardT(t));
}
int main() {PerfectForward(10); // 右值int a;PerfectForward(a); // 左值PerfectForward(move(a)); // 右值const int b 8;PerfectForward(b); // const 左值PerfectForward(move(b)); // const 右值return 0;
}输出结果 这下就能一一对上了。 结语
右值引用与移动语义是C11标准中的重要组成部分它们不仅提升了程序的执行效率也为开发者提供了更灵活的资源管理手段。在理解和掌握这些特性后您将能够编写出更加高效和优雅的代码。未来在C的学习和使用中希望您能将这些新特性融入实践享受现代C的强大魅力
今天的分享到这里就结束啦如果觉得文章还不错的话可以三连支持一下17的主页还有很多有趣的文章欢迎小伙伴们前去点评您的支持就是17前进的动力