50万奖金+官方证书,深圳国际金融科技大赛正式启动,点击报名 了解详情
写点什么

C++ 变化太大!该重新学习这门语言了

作者:Frances Buontempo

  • 2023-07-10
    北京
  • 本文字数:4589 字

    阅读完需:约 15 分钟

C++变化太大!该重新学习这门语言了

C++是一门古老但不断演进的语言。你几乎可以使用它来做任何事情,而且可以在很多地方找到它的身影。实际上,C++的发明者 Bjarne Stroustrup 将其描述为一切事物的隐形基础。有时,它可以深入到另外一门语言的库中,因为 C++可以用于性能关键的路径中。它可以在小型的嵌入式系统中运行,也可以为视频游戏提供动力。你的浏览器可能正在使用它。C++几乎无处不在!

C++为何如此重要


迄今为止,C++已经存在了很长的时间,但是其变化也是非常大的,尤其是 2011 年之后。当时,推出了一个名为 C++11 的新标准,标志着一个频繁更新的时代正式开启。如果你从 C++11 就没有使用过 C++,那么你有很多东西需要补习,这要从哪里开始呢?


该语言是需要编译的,面向特定的架构,如 PC、大型机、嵌入式设备、定制硬件,或者你想到的其他东西。如果你需要代码在不同类型的机器上运行,那需要重新编译它。这有缺点也有优点。不同的配置会带来更多的维护工作,但编译到特定架构能够让你“因地制宜(down to the metal)”,从而获得速度方面的优势。


不管你的目标是哪种平台,均需要一个编译器。你还需要一个编辑器或集成开发环境(IDE)来编写 C++代码。ISOCpp给出了一个资源清单,包括 C++编译器。Gnu 编译器集(Gnu compiler collection,gcc)、Clang 和 Visual Studio 均有免费版本。你甚至可以使用Matt Godbolt的编译器探索器,在浏览器上尝试基于各种编译器的代码。编译器可能支持不同版本的 C++,所以必须在编译器标记中说明你所需要的版本,例如 g++的-std=c++23或 Visual Studio 的/std:c++latest。ISOCpp 网站上有一个FAQ区域,概述了最近的一些变化,包括 C++11 和 C++14,以及整体的概览。另外,还有多本关于 C++最近版本的图书。

使用 Vector 快速了解 C++11


如果你已经被落下了,那么大量的资源可能会让你不知所措。但是,我们可以通过一个小例子来理解一些基础知识。停下来,亲自动手试一试往往是最好的学习方法。因此,我们从简单基础的东西开始吧!


一个很有用(且简单)的起点是不太起眼的vector,它位于std命名空间的vector头文件中。CppReference 提供了一个概述,告诉我们vector是一个序列容器,封装了动态大小的数组。因此,vector包含了一个连续的元素序列,我们可以根据需要调整 vector 的大小。vector本身是一个类模板,因此它需要一个类型,例如std::vector<int>。我们可以使用push_back将一个条目添加到 vector 的尾部。C++11 引入了一个名为emplace_back的新方法,该方法取值来构造一个新的条目。对于int,代码看上去是一样的:


std::vector<int> numbers;numbers.push_back(1);numbers.emplace_back(1);
复制代码


如果我们有比int更复杂的东西,那么就可能在 emplace 版本中获得性能方面的收益,因为 emplace 版本可以就地构造条目,从而避免对其进行复制。


C++11 引入了_r-value 引用_和_移动语义(move semantics)_来避免不必要的复制。潜在的性能改善是 C++11 的驱动力之一,后续的版本都是在此基础上进行的。为了解释什么是 r-value 引用,我们可以考虑前面样例中的push_back方法。它有两个重载形式,其中一个会接受一个常量引用,即const T&值,另外一个接受一个 r-value 引用,即T&&值。第二个版本会将元素移动到 vector 中,这可以避免复制临时对象。与之类似,emplace_back的签名通过 r-value 引用来获取参数,Args&&…,同样允许移动参数而无需复制。移动语义是一个很大的话题,我们只是接触到了它的皮毛。如果你想了解更多详情的话,Thomas Becker 在 2013 年撰写了一篇很好的文章,介绍了它的细节。


我们创建一个vector并在其中放置几个条目,然后使用来自iostream头文件的std::cout展示其内容。我们使用流插入操作符<<来显示这些元素。我们基于vectorsize编写一个for循环,并使用操作符[]来访问每个元素:


#include <iostream>#include <vector>
void warm_up(){ std::vector<int> numbers; numbers.push_back(1); numbers.emplace_back(1); for(int i=0; i<numbers.size(); ++i) { std::cout << numbers[i] << ' '; } std::cout << '\n';}
int main(){ warm_up();}
复制代码


该代码会显示两个 1。这段代码可以在编译器探索器上找到。

类模板参数推断

让我们做一些更有意思的事情,并学习一下现代的 C++。我们构建几个数字三角,会发现它们之间存在一个模式。数字三角的值是 1,3,6,10……它们分别由 1,1+2,1+2+3,1+2+3+4,……相加而成。如果我们这些斯诺克球架起来,就可以组成一个三角形,它也因此得名:



如果再增加一排,我们就会再增加六个斯诺克球。再加一排就会增加七个,以此类推。


为了得到数字 1,2,3 等,我们可以构建一个充满 1 的 vector,然后将这些数字相加。我们可以直接创建一个 vector,比如 18 个 1,而不必再增加另一个循环。我们说明想要多少个元素,然后再指明它的值:


   std::vector numbers(18, 1);
复制代码


注意我们不需要再声明<int>了。因为从 C++17 开始,_类模板参数推断(CTAD)就已经实现了。编译器可以推断出我们指的是int,因为我们要求的值是 1,这是一个int。如果我们需要显示 vector,那么可以使用_基于 range 的 for 循环。此时,我们不必使用基于 vector 索引的传统for循环,而是声明一个类型,甚至可以使用新的关键字auto,告诉编译器判断类型,然后是冒号和容器:


   for (auto i : numbers)    {        std::cout << i << ' ';    }    std::cout << '\n';   
复制代码


CTAD 和基于 range 的for循环是 C++11 以来引入的一些便利特性。

Range

有了由“1”组成的 vector,我们就可以包含numeric头文件,并使用部分的和来填充一个新的vector,如 1,1+1,1+1+1……,这样就有了 1,2,3……我们需要声明新vector的类型,因为这里要从一个空的vector开始,如果没有任何值可供使用,那么编译器将无法推断其类型。partial_sum需要开头和结尾的数字,最后我们需要使用back_inserter,这样目标 vector 会根据需要增长:


    #include <algorithm>    std::vector numbers(18, 1);    std::vector<int> sums;    std::partial_sum(numbers.begin(), numbers.end(),        std::back_inserter(sums));
复制代码


这样我们就得到了 1 到 18 的数字,均包含边界值。我们已经完成了数字三角的部分工作,但是 C++现在可以让我们的代码更加简洁。C++11 引入了iota函数,也位于numeric头文件中,它能够用不断增加的值填充一个容器:


std::vector<int> sums(18);std::iota(sums.begin(), sums.end(), 1);
复制代码


实际上,C++23 引入了一个 range 版本,它会为我们找到对应的beginend


  std::ranges::iota(sums, 1);
复制代码


C++23 还没有得到广泛的支持,所以可能需要等到你的编译器提供 range 版本。numericalgorithm头文件中的很多算法都有两个版本,其中一个需要一对输入迭代器(即first and last),另一个则是 range 版本,只需要接受容器即可。ranges 重载正在逐渐添加到标准 C++中。ranges 提供的功能远远超过我们这里避免声明两个迭代器的场景。我们可以过滤和转换输出,将这些东西连接在一起,并使用视图来避免复制数据。ranges 支持惰性计算,所以视图的内容会在需要的时候才评估计算出来。Ivan Čukić的Functional Programming in C++一书在这方面提供了更多的细节(书中还包含更多的内容)。


我们需要做的最后一件事就是形成数字三角。查看 vector 的部分和:


   std::partial_sum(sums.begin(), sums.end(), sums.begin());
复制代码


我们已经得到了想要的数字三角,即 1,3,6,10,15……171。


我们注意到,有些算法有 ranges 版本,那我们可以尝试一个。前两个三角数字是 1 和 3 是奇数,然后是两个偶数 6 和 10。这个模式是不是可持续的呢?如果我们对 vector 进行转换,用点号“.”来标记奇数,用星号“*”来标记偶数,就能看出最终结果。我们可以声明一个新的 vector 来存放转换结果。对于每个数字,仅需要一个字符,所以我们需要一个char类型的vector


std::vector<char> odd_or_even.
复制代码


我们可以编写一个简短的函数,它会获取一个 int 并返回对应的字符:


char flag_odd_or_even(int i){    return i % 2 ? '.' : '*';}
复制代码


如果i % 2的值不为零,这就是一个奇数,所以我们返回.,否则,返回*。我们可以在来自algorithm头文件的transform函数中使用这个自己的函数。最初的版本需要一对输入迭代器(first 和 last)、一个输出迭代器和一个_一元函数(unary function)_,该函数会接受一个输入,就像我们的flag_odd_or_even函数这样。C++20 引入了一个 ranges 版本,它能够接受一个输入源,而不是一对迭代器,另外还需要一个输出迭代器和一元函数。这意味着我们可以通过如下方式来转换先前生成的和:


   std::vector<char> odd_or_even;    std::ranges::transform(sums,        std::back_inserter(odd_or_even),        flag_odd_or_even);
复制代码


输出将会如下所示:


. . * * . . * * . . * * . . * * . .
复制代码


看上去,我们确实是不断地得到两个奇数,然后是两个偶数。Stack Exchange 的数学网站阐述了出现这种现象的原因

Lambdas

我们使用另一个新的 C++特性对我们的代码做最后的改进。如果我们想要看一下实际的转换代码的话,那需要要转移到另外一个地方才能看到这个一元函数都做了些什么。


C++11 引入了匿名函数或lambda表达式的特性。它们看起来与有名称的函数类似,将参数放在括号中,将函数主体放到花括号中,但是它们没有名字,不需要返回类型,并且有一个用[]表示的捕获组:


[](int i) { return i%2? '.':'*'; }
复制代码


如果与有名称的函数进行对比,会看到两者的相似性:


char flag_odd_or_even(int i){ return i % 2 ? '.' : '*'; }
复制代码


我们可以在捕获组中声明变量,这会给我们一个_闭包_。这些内容超出了本文的范围,但是在函数式编程中它们是非常强大和常见的。


如果我们将一个 lambda 分配给一个变量,


auto lambda = [](int i) { return i % 2 ? '.' : '*'; };
复制代码


那么,我们就可以像调用有名称的函数那样调用它:


lambda(7);
复制代码


这个特性允许我们使用 lambda 重写转换调用:


    std::ranges::transform(sums,        std::back_inserter(odd_or_even),        [](int i) { return i%2? '.':'*'; });
复制代码


这样的话,我们就可以在一个地方看到转换函数,而不必再去查看其他的地方了。

总结

将所有的内容组合在一起,就形成了如下的代码:


#include <algorithm>#include <iostream>#include <numeric>#include <vector>
int main(){ std::vector<int> sums(18); std::iota(sums.begin(), sums.end(), 1); std::partial_sum(sums.begin(), sums.end(), sums.begin());
std::vector<char> odd_or_even; std::ranges::transform(sums, std::back_inserter(odd_or_even), [](int i) { return i%2? '.':'*'; });
for (auto c : odd_or_even) { std::cout << c << ' '; } std::cout << '\n';}
复制代码


我们使用了 ranges、lambda 和基于 range 的for循环,浏览了移动语义,并练习了对 vector 的使用。对于首次重回 C++的人来说,这是一个不错的起点!


你可以在编译器探索器中尝试上述的代码


作者简介:

Frances Buontempo 有多年的 C++经验,还有过使用 Python 和其他各种语言的经验。她曾发表过关于 C++的演讲,并且是 ACCU 的 Overload 杂志的编辑。她有数学背景,为 PragProg 写了一本关于遗传算法和机器学习的书,并且正在为 Manning 写一本名为 C++ Bookcamp 的 C++书,以帮助那些被现代 C++落下的人迎头赶上。


原文链接:

Relearning C++ After C++11

2023-07-10 08:004933

评论

发布
暂无评论
发现更多内容

(1)长安链学习笔记-启动长安链

室内LED显示屏应该怎么选择?这5点注意事项必须考虑在内

Dylan

LED显示屏 户内led显示屏

Python 入门指南之深入 Python 流程控制

海拥(haiyong.site)

7月月更

NFTScan 开发者平台推出多链 NFT 数据 Pro API 服务

NFT Research

NFT 研发团队

浅谈网络安全之文件上传

网络安全学海

网络安全 信息安全 渗透测试 WEB安全 漏洞挖掘

「小程序容器技术」,是噱头还是新风口?

ToB行业头条

【Unity】升级版·Excel数据解析,自动创建对应C#类,自动创建ScriptableObject生成类,自动序列化Asset文件

萧然🐳

游戏开发 Unity 7月月更 Excel工具

新一代云原生消息队列(一)

技术小生

云原生 消息队列 7月月更

安全保护能力是什么意思?等保不同级别保护能力分别是怎样?

行云管家

等保 等级保护 安全保护能力

gRPC三种Java客户端性能测试实践

FunTester

让我们,从头到尾,通透网络I/O模型

C++后台开发

网络编程 IO多路复用 C++后台开发 网络io模型 C++开发

存币生息理财dapp系统开发案例演示

开发微hkkf5566

长安链学习笔记-证书研究之证书模式

长安链

云原生混部最后一道防线:节点水位线设计

阿里巴巴中间件

阿里云 云原生 中间件 混部

Efficient ETL Testing

Bright

数据开发 ETL 大数据开发 EasySQL

面试题:AOF重写机制,redis面试必问!!!

知识浅谈

redis 底层原理

体验Python剪辑视频以及相关问题解决,一劳永逸!

迷彩

Python Moviepy视频剪辑处理 7月月更

深度解读 RocketMQ 存储机制

阿里巴巴中间件

阿里云 RocketMQ 云原生 中间件 消息队列

TiFlash 源码阅读(四)TiFlash DDL 模块设计及实现分析

PingCAP

让 Rust 库更优美的几个建议!你学会了吗?

非凸科技

rust API

低代码平台中的数据连接方式(上)

Baidu AICLOUD

前端 低代码 数据格式 数据通信 爱速搭

🚩🚩🚩建议收藏!!Flutter状态管理插件哪家强?请看岛上码农的排行榜!

岛上码农

flutter ios 安卓 移动端开发 7月月更

spark调优(二):UDF减少JOIN和判断

怀瑾握瑜的嘉与嘉

spark 7月月更

同构+跨端,懂得小程序+kbone+finclip就够了!

Speedoooo

小程序 跨端开发 小程序容器 kbone web同构

Redis 持久化机制

知识浅谈

redis 面试题

COSCon'22 社区召集令来啦!Open the World,邀请所有社区一起拥抱开源,打开新世界~

开源社

开源

前置机是什么意思?主要作用是什么?与堡垒机有什么区别?

行云管家

堡垒机 前置机

MetaForce原力元宇宙开发搭建丨佛萨奇2.0系统开发

开发微hkkf5566

TDengine 社区问题双周精选 | 第二期

TDengine

数据库 tdengine 时序数据库

C++变化太大!该重新学习这门语言了_编程语言_InfoQ精选文章