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

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

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

1.1. 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++ 最近版本的图书。

1.2. 使用 Vector 快速了解 C++11

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

一个很有用(且简单)的起点是不太起眼的 vector, 它位于 std 命名空间的 vector 头文件中。CppReference 提供了一个概述, 告诉我们 vector 是一个序列容器, 封装了动态大小的数组。因此, vector 包含了一个连续的元素序列, 我们可以根据需要调整 vector 的大小。vector 本身是一个类模板, 因此它需要一个类型, 例如 std::vector。我们可以使用 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 展示其内容。我们使用流插入操作符<<来显示这些元素。我们基于 vector 的 size 编写一个 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。这段代码可以在编译器探索器上找到。

1.3. 类模板参数推断

让我们做一些更有意思的事情, 并学习一下现代的 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);

注意我们不需要再声明了。因为从 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 以来引入的一些便利特性。

1.4. 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 版本, 它会为我们找到对应的 begin 和 end:

std::ranges::iota(sums, 1);

C++ 23 还没有得到广泛的支持, 所以可能需要等到你的编译器提供 range 版本。numeric 和 algorithm 头文件中的很多算法都有两个版本, 其中一个需要一对输入迭代器(即 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 的数学网站阐述了出现这种现象的原因。

1.5. 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? '.':'*'; });

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

1.6. 总结

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

#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++ 的人来说, 这是一个不错的起点!

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

云满笔记

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值