|
发表于 3 天前
|
查看: 106 |
回复: 0
C++,作为编程领域的老牌语言,自诞生至今已有 45 年之久。在漫长的岁月里,它不断进化,以应对各种新挑战。然而,不少开发者仍用旧眼光看待它,仿佛还停留在上个世纪。但其实,当代 C++ 在表达思想、性能、可靠性和可维护性等方面,都有了质的飞跃。今天,咱们就一起深入了解一下 21 世纪的 C++,看看它到底藏着哪些厉害的机制和特性。
一、现代 C++ 的魅力初体验
先来看一个简单的 C++ 程序,它能从输入中读取每一行内容,并只输出其中的唯一行:
import std;using namespace std;int main(){unordered_map<string, int> m;for (string line; getline(cin, line); )if (m[line]++ == 0)cout << line << '\n';}
是不是发现,和以前的 C++ 风格相比,这个程序没有了繁琐的内存分配 / 释放操作、尺寸定义、错误处理、类型转换、指针使用、不安全的下标操作,甚至连预处理指令#include都不见了。但它却高效地实现了功能,比很多程序员花时间手写的代码还要快。要是还想提升性能,也能进一步优化。这就是现代 C++ 的魅力,用简洁的代码实现强大的功能。
再看一个变体程序,它收集唯一行供后续使用:
import std;vector<string> collect_lines(istream& is){unordered_set s{from_range, istream_iterator<string>{is}};return vector{from_range, s};}auto lines = collect_lines(cin);
这里用了unordered_set来收集唯一行,最后返回一个vector。而且,编译器能自动推导vector的元素类型,是不是很方便?不过,这里的string还是存在复制操作,后面我们会讲讲怎么优化。这些小例子就是想让大家感受下现代 C++ 的风格,打破对它的固有认知。
二、C++ 的理想追求
C++ 一直以来都有着明确的目标,这些目标贯穿其发展历程:
- 直接表达思想:用代码清晰准确地表达开发者的意图。
- 静态类型安全:通过静态类型检查,在编译阶段发现类型相关的错误,提高代码的稳定性。
- 资源安全(无泄漏):确保程序中使用的资源(如内存、文件句柄等)都能正确释放,避免资源泄漏。
- 直接访问硬件:让开发者能够直接操作硬件资源,满足对性能要求极高的场景。
- 高性能(高效):生成的代码运行效率高,能充分利用系统资源。
- 可扩展(零开销抽象):在不增加额外运行时开销的前提下,方便地进行代码扩展和复用。
- 可维护(易理解代码):代码结构清晰,易于阅读和维护。
- 平台独立(可移植):编写的代码能在不同的操作系统和硬件平台上运行。
- 稳定(兼容):保证与早期版本的 C++ 兼容,让旧代码能在新环境中继续运行。
为了实现这些目标,C++ 不断发展,既有像类的构造函数和析构函数、异常处理、模板、std::vector这样的经典特性,也有模块、概念、Lambda 表达式、范围、constexpr和consteval、并发支持和并行算法、协程、std::shared_ptr等新特性。关键在于,要根据实际问题,合理组合使用这些特性和库功能。
三、资源管理:C++ 的关键能力
在 C++ 里,资源管理至关重要。资源可以是内存、锁、文件句柄等,为了避免资源泄漏,不能依赖手动释放资源。C++ 的基本做法是把资源绑定到一个句柄上,当句柄的作用域结束时,自动释放资源,这就是 RAII(Resource Acquisition Is Initialization)机制。
比如自定义的Vector类:
template<typename T>class Vector {public: Vector(initializer_list<T>); ~Vector();private: T* elem;int sz;};
这里的Vector就是一个资源句柄,它的构造函数负责分配内存和初始化元素,析构函数负责释放内存和销毁元素。使用时就像这样:
void fct(){ Vector<double> constants {1, 1.618, 3.14, 2.99e8}; Vector<string> designers {"Strachey", "Richards", "Ritchie"}; Vector<pair<string, jthread>> vp { {"producer",prod}, {"consumer",cons}};}
constants、designers和vp在创建时由构造函数初始化,作用域结束时由析构函数自动释放,而且这种初始化和释放是递归的,即使内部包含复杂的对象,也能妥善处理。
(一)控制对象生命周期
管理资源对象的生命周期对资源管理很关键。C++ 通过构造函数、析构函数、复制构造函数、移动构造函数等操作来控制对象生命周期:
- 构造函数:在对象首次使用前调用,用于建立类的不变量。
- 析构函数:在对象最后一次使用后调用,用于释放资源。
- 复制构造函数和赋值运算符:用于创建和赋值具有相同值的新对象。
- 移动构造函数和移动赋值运算符:用于在对象间移动资源,通常在不同作用域之间。
(二)消除冗余复制
回到collect_lines函数的例子,之前的版本存在string的复制操作,会影响性能。优化后可以这样写:
vector<string> collect_lines(istream& is){unordered_set s{from_range, istream_iterator<string>{is}};return vector{from_range, std::move(s)};}
这里通过std::move将set中的元素移动到vector中,避免了不必要的复制。在很多情况下,编译器还会进行 “复制省略” 优化,进一步提高效率。
(三)资源与错误处理
C++ 追求资源安全,在错误情况下也不能泄漏资源。基本规则是:不泄漏资源,不使资源处于无效状态。当检测到无法在本地处理的错误时,要将访问的对象置于有效状态,释放函数负责的资源,让调用链上的其他函数处理资源相关问题。所以,“裸指针” 不能可靠地用作资源句柄。
在处理错误时,C++ 有两种方式:对于常见且能在本地处理的错误,使用错误代码和测试;对于罕见且无法在本地处理的错误,使用异常。基于异常的错误处理不能和用作资源句柄的指针一起使用,要结合异常、RAII 和错误代码来实现可靠的错误处理。
四、模块化:告别旧方式,迎接新变革
C++ 从 C 继承来的预处理器虽然被广泛使用,但它给工具开发和编译器性能带来了不少问题。比如用头文件来实现模块化就存在不少弊端,#include指令会导致包含顺序不同可能有不同含义,而且具有传递性,会造成重复编译和微妙的依赖错误。
而 C++ 现在提供的模块功能就解决了这些问题。模块的导入是顺序无关的,相互独立,能有效避免依赖错误。模块只需要编译一次,大大提高了编译速度。比如一个包含大量代码的库,使用#include编译需要 1.5 秒,而使用模块导入只需要 0.062 秒,速度提升明显。就连标准库也都被做成了模块,像经典的 “hello world” 程序,使用import std;替换#include <iostream>后,编译时间从 0.87 秒骤降至 0.08 秒。
五、泛型编程:C++ 的强大武器
泛型编程是现代 C++ 的核心基础之一,它让代码能适用于多种类型,实现代码的简洁、高效和类型安全。C++ 通过模板来支持泛型编程,标准库中到处都有模板的身影,比如容器、算法、并发支持、内存管理、I/O、字符串和正则表达式等。
例如,我们可以写一个排序函数,能对所有符合标准的可排序范围进行排序:
void sort(Sortable_range auto& r);vector<string> vs;// … fill vs …sort(vs);array<int, 128> ai;// … fill ai …sort(ai);
编译器会检查参数类型是否符合Sortable_range的要求,如果不符合,在使用时就会报错。比如对list<int>进行排序就会出错,因为list不支持随机访问。
(一)概念:让泛型编程更强大
概念是 C++20 引入的编译时谓词,用于表达模板参数的要求。比如Sortable_range概念:
template<typename R>concept Sortable_range = random_access_range<R> && sortable<iterator_t<R>>;
它表示一个类型R要是Sortable_range,需要既是random_access_range,迭代器类型又要是sortable。概念还可以用 “使用模式” 直接指定类型的属性,而且我们可以用static_assert显式检查类型是否符合概念。
(二)编译时求值
在现代 C++ 中,很多简单函数都能在编译时求值,像constexpr、consteval和concept。例如:
constexpr auto jul = weekday(December/24/2024);
为了能在编译时求值,这些函数不能有副作用、访问非局部数据或存在未定义行为,但它们可以使用标准库的很多功能。编译时求值不仅提高了性能,还为代码带来了更多的可能性。
六、遵循准则,保障代码质量
虽然现代 C++ 有诸多优势,但升级代码并不容易,旧习惯也很难改,网上和教材中还有很多过时的信息。为了帮助开发者更好地使用现代 C++,避免陷入语言的 “死角”,人们制定了各种准则,其中 C++ 核心准则是比较有代表性的。
(一)准则的策略
C++ 核心准则采用 “子集 - 超集” 策略:先通过一些库抽象扩展语言,方便高效地使用准则;然后禁止使用低级、低效和易错的特性。这样得到的是更强大、安全、灵活和快速的 C++,同时还是 100% 符合 ISO 标准的 C++,在需要时依然可以使用那些底层特性。
(二)准则示例:不使用指针下标
指针没有关联的范围信息,使用指针下标容易引发内存安全和类型安全问题。比如:
void f(int* p, int n){for (int i = 0; i < n; i++) do_something_with(p[n]);}int a[100];// …f(a, 100);f(a, 1000);
这里如果n的值不合理,就会访问越界。而使用span可以解决这个问题:
void f(span<int> a){for (int& x: s) do_something_with(x);}int a[100];// …f(a); f({a, 1000});
span持有指针和元素个数,能进行范围检查,使用起来更安全、简洁。
(三)准则示例:不使用无效指针
有些容器(如vector)在操作时可能会重新分配元素位置,如果外部获取的指向容器元素的指针在重新分配后还被使用,就会出问题。通过遵循 C++ 的使用规则,利用本地静态分析可以防止指针无效化。
(四)强制执行:配置文件
准则虽然有用,但在大型代码库中很难完全遵循,所以强制执行很重要。我们把强制执行的一组连贯的准则规则称为 “配置文件”。目前计划的初始配置文件包括类型、生命周期、边界、算术等方面的检查,未来还会有针对特定应用领域的配置文件。配置文件主要在编译时进行检查,部分重要检查在运行时进行,而且可以根据需要在代码中显式请求或抑制某个配置文件的检查。
七、C++ 的未来展望
C++ 的发展由 ISO 标准委员会控制,目前有很多正在进行的工作,如异步计算模型、静态反射、SIMD、契约系统、函数式编程风格的模式匹配、通用单位系统等,虽然将这些不同的想法整合到一起是个挑战,但也为 C++ 的未来发展带来了无限可能。
八、总结C++
在不断发展进化,当代 C++ 在很多方面都比早期版本更接近理想状态,为开发者提供了更好的代码质量、类型安全、表达能力和性能,应用领域也更加广泛。但发展过程中也存在一些问题,比如人们对 C++ 的误解,以及工具支持的滞后。不过,C++ 的模型依然强大,包括静态类型系统、对内置类型和用户定义类型的平等支持、值和引用语义、系统且通用的资源管理、高效的面向对象编程、灵活高效的泛型编程、编译时编程、直接使用机器和操作系统资源、通过库支持并发等。希望大家能深入了解现代 C++,充分利用它的优势,编写出更优秀的代码。你对 C++ 的这些新特性有什么看法呢?欢迎在评论区留言分享!
<顺便吆喝一句,民族企业大厂,前后端测试捞人,感兴趣的来!→https://jsj.top/f/o38ijj>
|
温馨提示:
1.如果您喜欢这篇帖子,请给作者点赞评分,点赞会增加帖子的热度,评分会给作者加学币。(评分不会扣掉您的积分,系统每天都会重置您的评分额度)。
2.回复帖子不仅是对作者的认可,还可以获得学币奖励,请尊重他人的劳动成果,拒绝做伸手党!
3.发广告、灌水回复等违规行为一经发现直接禁言,如果本帖内容涉嫌违规,请点击论坛底部的举报反馈按钮,也可以在【 投诉建议】板块发帖举报。
|