前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >C++ 里的“数组”

C++ 里的“数组”

作者头像
C语言与CPP编程
发布2024-04-16 13:03:26
1000
发布2024-04-16 13:03:26
举报
文章被收录于专栏:c语言与cpp编程c语言与cpp编程

C 数组的问题

C 里面就有数组。但是,C 数组具有很多缺陷,使用中有很多的陷阱。我们先来看一下其中的几个问题。

问题一:传参退化问题

你可以一眼看出下面代码的问题吗?

代码语言:javascript
复制
#define ARRAY_LEN(a) (sizeof(a) / sizeof((a)[0]))

void Test(int a[8])
{
    cout << ARRAY_LEN(a) << endl;
}
代码语言:javascript
复制

如果函数 Test 被调用的话,它的输出结果一般不是 8,而是 2。C 的老手一定能看出问题所在,但新手很容易就迷糊了。

幸运的是,编译器现在一般能直接对这个问题进行告警。你应该会见到类似下面这样的告警信息:

warning: ‘sizeof’ on array function parameter ‘a’ will return size of ‘int *’ [-Wsizeof-array-argument] cout << ARRAY_LEN(a) << endl;

编译器会明确告诉你, a 被理解成了 int*,而不是数组。

问题二:复制问题

跟上面退化问题紧密相关的一点是,C 数组不能被复制(所以传参有退化)。下面的代码无法通过编译:

代码语言:javascript
复制
int a[3] = {1, 2, 3};
int b[3] = a;  // 不能编译
b = a;         // 不能编译
代码语言:javascript
复制

复制和退化这两个问题是紧密相关的,但这种语言的不规则性还是带来了学习和理解上的困难。如果我们想要一个数组能够被复制,就得把它放到结构体(或联合体)里面去。这至少会带来语法上的不便。

问题三:语法问题

C 数组的语法设计也绝对称不上有良好的可读性。你能一眼看出下面两个声明分别是什么意思吗?

代码语言:javascript
复制
int (*fpa[3])(const char*);
int (*(*fp)(const char*))[3];
代码语言:javascript
复制

(下面会给出回答。)

问题四:动态问题

最早的 C 数组大小是完全固定的,这实际上既不方便又不安全。当然,我们可以用 malloc 来动态分配内存,到了 C99 还可以用变长数组,但它们要么使用不够方便,要么长度不能在创建后变化(如动态增长)。这些问题使得 C 的代码里常常在不该使用定长数组的时候也使用了定长数组,并很容易导致安全问题,如缓冲区溢出

C++ 的解决方案

C++ 有两种常用的替换 C 数组的方式:

  • vector
  • array

vector

C++ 标准模板库(STL)的主要组成部分是:

  • 容器
  • 迭代器
  • 算法
  • 函数对象

而说到容器,我们通常第一个讨论的就是vector。它的名字来源于数学术语,直接翻译是“向量”的意思,但在实际应用中,我们把它当成动态数组更为合适。Alex Stepanov 在设计 STL 时借鉴 Scheme 和 Common Lisp 语言起了这个名字,但他后来承认这是个错误——这个容器不是数学里的向量,名字起得并不好。它基本相当于 Java 的 ArrayList 和 Python 的list。C++ 里有更接近数学里向量的对象,名字是valarray(很少有人使用,我也不打算介绍)。

vector 的成员在内存里连续存放。beginend 成员函数返回的迭代器构成了一个半闭半开区间,而 frontback 成员函数则返回指向首项和尾项的引用,如下图所示:

因为 vector 的元素放在堆上,它也自然可以受益于现代 C++ 的移动语义——移动 vector 具有很低的开销,通常只是操作六个指针而已。

下面的代码展示了 vector 的基本用法:

代码语言:javascript
复制
vector<int> v{1, 2, 3, 4};
v.push_back(5);
v.insert(v.begin(), 0);
for (size_t i = 0; i < v.size(); ++i) {
    cout << v[i] << ' ';  // 输出 0 1 2 3 4
}
cout << '\n';

int sum = 0;
for (auto it = v.begin(); it != v.end(); ++it) {
    sum += *it;
}
cout << sum << '\n';      // 输出 15
代码语言:javascript
复制

上面的代码里我们首先构造了一个内容为 {1, 2, 3, 4}vector,然后在尾部追加一项 5,在开头插入一项 0。接下来,我们使用传统的下标方式来遍历,并输出其中的每一项。随即我们展示了 C++ 里通用的使用迭代器遍历的做法,对其中的内容进行累加。最后输出结果。

当一个容器存在 push_…pop_… 成员函数时,说明容器对指定位置的删除和插入性能较高。vector 适合在尾部操作,这是它的内存布局决定的(它只支持 push_back 而不支持 push_front)。只有在尾部插入和删除时,其他元素才会不需要移动,除非内存空间不足导致需要重新分配内存空间。

除了容器类的共同点,vector 允许下面的操作(不完全列表):

  • 可以使用中括号的下标来访问其成员
  • 可以使用 data 来获得指向其内容的裸指针
  • 可以使用 capacity 来获得当前分配的存储空间的大小,以元素数量计
  • 可以使用 reserve 来改变所需的存储空间的大小,成功后 capacity() 会改变
  • 可以使用 resize 来改变其大小,成功后 size() 会改变
  • 可以使用 pop_back 来删除最后一个元素
  • 可以使用 push_back 在尾部插入一个元素
  • 可以使用 insert 在指定位置前插入一个元素
  • 可以使用 erase 在指定位置删除一个元素
  • 可以使用 emplace 在指定位置构造一个元素
  • 可以使用 emplace_back 在尾部新构造一个元素

大家可以留意一下 push_…pop_… 成员函数。它们存在时,说明容器对指定位置的删除和插入性能较高。vector 适合在尾部操作,这是它的内存布局决定的。只有在尾部插入和删除时,其他元素才会不需要移动,除非内存空间不足导致需要重新分配内存空间。

push_backinsertreserveresize 等函数导致内存重分配时,或当 inserterase 导致元素位置移动时,vector 会试图把元素“移动”到新的内存区域。vector 的一些重要操作(如 push_back)试图提供强异常安全保证,即如果操作失败(发生异常)的话,vector 的内容完全不发生变化,就像数据库事务失败发生了回滚一样。如果元素类型没有提供一个保证不抛异常的移动构造函数,vector 此时通常会使用拷贝构造函数。因此,我们如果需要用移动来优化自己的元素类型的话,那不仅要定义移动构造函数(和移动赋值运算符,虽然 push_back 不要求),还应当将其标为 noexcept,或只在容器中放置对象的智能指针。

C++11 开始提供的 emplace… 系列函数是为了提升容器的插入性能而设计的。如果你的代码里有 vector<obj> v;v.push_back(Obj()),那把后者改成 v.emplace_back()v 的结果相同,而性能则有所不同——使用 push_back 会额外生成临时对象,多一次(移动或拷贝)构造和析构。如果是移动的情况,那会有小幅性能损失;如果对象没有实现移动的话,那性能差异就可能比较大了。——作为简单的使用指南,当且仅当我们见到 v.push_back(Obj(…)) 这样的代码时,我们就应当改为 v.emplace_back(…)

array

vector 解决了 C 数组的所有问题,但它毕竟不等价于 C 数组——堆内存分配的开销还是要比栈高得多。性能完全等同于 C 数组的 array 容器要到 C++11 才引入,虽然迟了点,但它最终在保留 C 数组性能的同时消除了前面列的头三个 C 数组的问题。

首先,array 没有不会自动退化。如果你希望高效传参,就应当用标准的引用传参的方式,如 void foo(const array<int, 100>& a)。如果你希望把指针传给 C 接口,你也可以写 foo(a.data())。如果函数接口就是想复制一个小数组,那使用 void foo(array<short, 3> a) 这样的形式也完全没有问题。

其次,跟上面的问题关联,array 有了合理的复制行为。下面的代码完全合法:

代码语言:javascript
复制
array<int, 3> a{1, 2, 3};
array<int, 3> b = a;  // OK
b = a;                // OK
代码语言:javascript
复制

再次,从可读性角度,你来自己看一下你更喜欢读哪种风格的代码吧:

代码语言:javascript
复制
// 函数指针的数组
int (*fpa[3])(const char*);
array<int (*)(const char*), 3> fpa;

// 返回整数数组指针的函数的指针
int (*(*fp)(const char*))[3];
array<int, 3>* (*fp)(const char*);
代码语言:javascript
复制

array 的好处还不止这些。由于它的接口跟其他的容器更一致,更容易被使用在泛型代码中。你也可以直接拿两个 array 来进行 ==、< 之类的比较,结果不是 C 数组的无聊指针比较,而是真正的逐元素比较!

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2024-04-15,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 C语言与CPP编程 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 问题一:传参退化问题
  • 问题二:复制问题
  • 问题三:语法问题
  • 问题四:动态问题
  • C++ 的解决方案
    • vector
      • array
      相关产品与服务
      容器服务
      腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档