电子产业一站式赋能平台

PCB联盟网

搜索
查看: 84|回复: 0
收起左侧

C++ 里的“数组”

[复制链接]

1077

主题

1077

帖子

1万

积分

论坛法老

Rank: 6Rank: 6

积分
11496
发表于 2024-4-16 08:30:00 | 显示全部楼层 |阅读模式

tdrgbix14gu6401335421.png

tdrgbix14gu6401335421.png
0 w' N" a  t* Z, g" p3 K; x
C 数组的问题; u" X8 C6 Q0 x$ h  K* D! O2 e

0 ^- Q8 C8 D0 {0 ]8 QC 里面就有数组。但是,C 数组具有很多缺陷,使用中有很多的陷阱。我们先来看一下其中的几个问题。
7 v7 J! k+ O( F' h0 L问题一:传参退化问题你可以一眼看出下面代码的问题吗?( t7 m8 {$ z+ L6 e" K
# X0 }& m- N# n5 J! ^! {* P
  • #define ARRAY_LEN(a) (sizeof(a) / sizeof((a)[0]))$ \3 Z; R7 }9 l" M! o
    void Test(int a[8]){    cout endl;}) P0 L# R; s7 r# R
    如果函数 Test 被调用的话,它的输出结果一般不是 8,而是 2。C 的老手一定能看出问题所在,但新手很容易就迷糊了。; z+ A9 O% h: C+ R: a+ t3 c
    幸运的是,编译器现在一般能直接对这个问题进行告警。你应该会见到类似下面这样的告警信息:5 Y! d. {# {: h0 F' M- z  R
    warning: ‘sizeof’ on array function parameter ‘a’ will return size of ‘int *’ [-Wsizeof-array-argument]1 W, _! n1 d+ \
    cout 2 l2 i7 Z2 o1 Z, }; `7 D( Y8 H6 W
    编译器会明确告诉你, a 被理解成了 int*,而不是数组。
    ! T$ f2 [6 h3 e9 w- S: E1 ^8 {问题二:复制问题跟上面退化问题紧密相关的一点是,C 数组不能被复制(所以传参有退化)。下面的代码无法通过编译:3 q' Z$ d  l7 U2 U9 c( |1 d

    . [) V' Y* \3 D9 y; |) S! m
  • int a[3] = {1, 2, 3};int b[3] = a;  // 不能编译b = a;         // 不能编译- u$ J/ S2 W! `* a7 p% v4 N
    复制和退化这两个问题是紧密相关的,但这种语言的不规则性还是带来了学习和理解上的困难。如果我们想要一个数组能够被复制,就得把它放到结构体(或联合体)里面去。这至少会带来语法上的不便。' u. Q* M. r+ g! J0 G$ E
    问题三:语法问题C 数组的语法设计也绝对称不上有良好的可读性。你能一眼看出下面两个声明分别是什么意思吗?8 d: r5 W: g- s
    # n% T6 R% |) q
  • int (*fpa[3])(const char*);int (*(*fp)(const char*))[3];
    ( E* j$ ?3 }# Z% L(下面会给出回答。)
    : n  j) M6 Y1 b, S: v* A问题四:动态问题最早的 C 数组大小是完全固定的,这实际上既不方便又不安全。当然,我们可以用 malloc 来动态分配内存,到了 C99 还可以用变长数组,但它们要么使用不够方便,要么长度不能在创建后变化(如动态增长)。这些问题使得 C 的代码里常常在不该使用定长数组的时候也使用了定长数组,并很容易导致安全问题,如缓冲区溢出* N3 v$ I/ r* o
    4 Y/ B  J2 H* ^7 z7 g

    hf4lzirzfv46401335521.png

    hf4lzirzfv46401335521.png
    8 Q8 b4 F  r9 h
    C++ 的解决方案0 `" g# c- A/ A) h) @$ h4 x
    * m9 \+ l  n; ]' S' [4 {; T
    C++ 有两种常用的替换 C 数组的方式:8 g) u/ c# P% W3 d4 G
    vector
    : T* o6 |# X% z4 Darray
    ' {9 Z2 L( Q- _4 `" H2 z- W
    vectorC++ 标准模板库(STL)的主要组成部分是:
    * F! v5 m" r" V2 m4 g" Y容器
    2 S9 b5 o3 X9 w) Z迭代器
    ) u" j4 r+ |& Q, h4 \算法
    8 r9 l; A5 [+ O0 d1 }; I& h$ Y函数对象% s2 c& F+ ^# I
    而说到容器,我们通常第一个讨论的就是vector。它的名字来源于数学术语,直接翻译是“向量”的意思,但在实际应用中,我们把它当成动态数组更为合适。Alex Stepanov 在设计 STL 时借鉴 Scheme 和 Common Lisp 语言起了这个名字,但他后来承认这是个错误——这个容器不是数学里的向量,名字起得并不好。它基本相当于 Java 的 ArrayList 和 Python 的list。C++ 里有更接近数学里向量的对象,名字是valarray(很少有人使用,我也不打算介绍)。, S! Z# U- B7 r# O
    vector 的成员在内存里连续存放。begin、end 成员函数返回的迭代器构成了一个半闭半开区间,而 front、back 成员函数则返回指向首项和尾项的引用,如下图所示:
    6 v  e6 J8 [5 v1 z! \

    11z1prndkl36401335621.png

    11z1prndkl36401335621.png
    8 Z2 \4 H% M3 v
    因为 vector 的元素放在堆上,它也自然可以受益于现代 C++ 的移动语义——移动 vector 具有很低的开销,通常只是操作六个指针而已。
    ! u- [) Q# X' a$ W下面的代码展示了 vector 的基本用法:
    # c. S: y9 b4 H/ p7 E4 @
    * Y3 w+ N1 S* S
  • vectorint> v{1, 2, 3, 4};v.push_back(5);v.insert(v.begin(), 0);for (size_t i = 0; i     cout ' ';  // 输出 0 1 2 3 4}cout '5 }1 L) V% o; N3 E0 Z
    ';
    6 Z1 W! L& }" i0 x$ \, X/ w7 D! {. J) Fint sum = 0;for (auto it = v.begin(); it != v.end(); ++it) {    sum += *it;}cout '# }4 p3 {" B9 u( i, o
    ';      // 输出 15# L" H/ d; H  x  l
    上面的代码里我们首先构造了一个内容为 {1, 2, 3, 4} 的 vector,然后在尾部追加一项 5,在开头插入一项 0。接下来,我们使用传统的下标方式来遍历,并输出其中的每一项。随即我们展示了 C++ 里通用的使用迭代器遍历的做法,对其中的内容进行累加。最后输出结果。* @9 d! ?( L+ J  p! `
    当一个容器存在 push_… 和 pop_… 成员函数时,说明容器对指定位置的删除和插入性能较高。vector 适合在尾部操作,这是它的内存布局决定的(它只支持 push_back 而不支持 push_front)。只有在尾部插入和删除时,其他元素才会不需要移动,除非内存空间不足导致需要重新分配内存空间。; V+ Y  b+ ^3 B' ~
    除了容器类的共同点,vector 允许下面的操作(不完全列表):
    ) t& o+ c9 i& T; N- L% T2 y可以使用中括号的下标来访问其成员
    ! k2 M$ \* M/ Q3 h) {8 C可以使用 data 来获得指向其内容的裸指针' {9 G& S( _6 @& q1 ~7 h
    可以使用 capacity 来获得当前分配的存储空间的大小,以元素数量计. N' R0 i2 d) h4 w) J0 C- |
    可以使用 reserve 来改变所需的存储空间的大小,成功后 capacity() 会改变# H  j& H1 l7 d; a0 l
    可以使用 resize 来改变其大小,成功后 size() 会改变) G: _2 I$ Q4 V) _) S: [
    可以使用 pop_back 来删除最后一个元素& E, y* r6 Q0 a1 r9 Q
    可以使用 push_back 在尾部插入一个元素/ A" s# A/ t, ~& Y1 o9 T8 p
    可以使用 insert 在指定位置前插入一个元素5 L  _- ~( X. G- ?) h
    可以使用 erase 在指定位置删除一个元素3 @. P6 z" x. [: T  `. f
    可以使用 emplace 在指定位置构造一个元素( |& c/ U7 U. u2 w5 q5 O& i/ v, c
    可以使用 emplace_back 在尾部新构造一个元素
    & ?! b' \: h/ l/ D" w
    大家可以留意一下 push_… 和 pop_… 成员函数。它们存在时,说明容器对指定位置的删除和插入性能较高。vector 适合在尾部操作,这是它的内存布局决定的。只有在尾部插入和删除时,其他元素才会不需要移动,除非内存空间不足导致需要重新分配内存空间。
    8 H- X5 u- f/ n% k  Q* w( h! D当 push_back、insert、reserve、resize 等函数导致内存重分配时,或当 insert、erase 导致元素位置移动时,vector 会试图把元素“移动”到新的内存区域。vector 的一些重要操作(如 push_back)试图提供强异常安全保证,即如果操作失败(发生异常)的话,vector 的内容完全不发生变化,就像数据库事务失败发生了回滚一样。如果元素类型没有提供一个保证不抛异常的移动构造函数,vector 此时通常会使用拷贝构造函数。因此,我们如果需要用移动来优化自己的元素类型的话,那不仅要定义移动构造函数(和移动赋值运算符,虽然 push_back 不要求),还应当将其标为 noexcept,或只在容器中放置对象的智能指针。' M: S& d' w) I2 Y  u
    C++11 开始提供的 emplace… 系列函数是为了提升容器的插入性能而设计的。如果你的代码里有 vector v; 和 v.push_back(Obj()),那把后者改成 v.emplace_back() ,v 的结果相同,而性能则有所不同——使用 push_back 会额外生成临时对象,多一次(移动或拷贝)构造和析构。如果是移动的情况,那会有小幅性能损失;如果对象没有实现移动的话,那性能差异就可能比较大了。——作为简单的使用指南,当且仅当我们见到 v.push_back(Obj(…)) 这样的代码时,我们就应当改为 v.emplace_back(…)。
    6 s2 ?; z5 A' I5 xarrayvector 解决了 C 数组的所有问题,但它毕竟不等价于 C 数组——堆内存分配的开销还是要比栈高得多。性能完全等同于 C 数组的 array 容器要到 C++11 才引入,虽然迟了点,但它最终在保留 C 数组性能的同时消除了前面列的头三个 C 数组的问题。
    * u+ ~4 V* \" [: M8 n首先,array 没有不会自动退化。如果你希望高效传参,就应当用标准的引用传参的方式,如 void foo(const array& a)。如果你希望把指针传给 C 接口,你也可以写 foo(a.data())。如果函数接口就是想复制一个小数组,那使用 void foo(array a) 这样的形式也完全没有问题。% t2 r0 H5 N9 }, {
    其次,跟上面的问题关联,array 有了合理的复制行为。下面的代码完全合法:
    1 R! n4 `# q/ S/ |9 H# |$ r- x; h- K1 c3 P5 M% H
  • arrayint, 3> a{1, 2, 3};arrayint, 3> b = a;  // OKb = a;                // OK5 b8 w5 W( h% B) E9 k% r- k2 z% y
    再次,从可读性角度,你来自己看一下你更喜欢读哪种风格的代码吧:
    % y# }, O# x& A9 U. l/ b5 [2 o3 j2 f
  • // 函数指针的数组int (*fpa[3])(const char*);arrayint (*)(const char*), 3> fpa;
    * W/ u0 z' Y+ n2 Z# H# x! c$ G* x  F// 返回整数数组指针的函数的指针int (*(*fp)(const char*))[3];arrayint, 3>* (*fp)(const char*);+ r# N4 G( I; {
    array 的好处还不止这些。由于它的接口跟其他的容器更一致,更容易被使用在泛型代码中。你也可以直接拿两个 array 来进行 ==、
    3 N0 `* i0 V5 @& B' U2 L0 n5 q0 q, s- p! V  G' x4 P# j

    hzkagdyk4mc6401335721.png

    hzkagdyk4mc6401335721.png

    9 b9 u7 s' W# _2 R( ?: V6 L我的培训课程
    " z, g" ^$ E, P& i/ z5 |
    , x% u) j/ A4 ?4 u+ l/ t# t) b
    9 G8 K" y4 f1 F
    《现代 C++ 实战》是一个我讲过很多次的培训课程,重点在 C++ 语言提供的“现代”特性上,包括了 C++ 的主要惯用法和常用新特性。我会在课程里讨论:
    . y) ]3 M9 X& ~- G$ Z5 c资源管理
    ) x) v# c) d  ?9 O7 C$ Y移动语义
    + m2 }6 q# p+ D5 U$ z. h7 j+ n智能指针
    : [6 ?3 T# q% m, N容器& L3 N/ J: g9 R4 q3 H+ r/ _
    迭代器6 j6 i/ D1 m1 R9 p8 U4 n# T
    现代 C++ 的易用性改进
    ) B- {$ ?: |/ P6 E3 v3 `模板- X3 l$ ^6 e6 g3 I
    ……. {$ \1 F! m$ t) C& g
    当然,课程是死的,课程里的交流、课后你自己的练习和拓展才是成长的关键。我希望我的课程能带给你一个看待 C++ 新视角,能在实践中加以应用;我希望你多多提出问题,由我来为你答疑解惑;我更希望你学完不是就那么结束了,而是牢牢记住一定要“学而时习之”,把课程的结束当成一个新的学习阶段的开始。只有这样,我的授课才不是白费力气。6 U' y0 l6 p% N, n, m+ K! s6 K
    作者 | 吴咏炜/ ]( q. u0 ?9 Z- A4 |/ B1 P6 G8 e
    出品 | 程序人生(ID:coder_life)( e7 Z% S. w4 ?9 c
    ——EOF——你好,我是飞宇,本硕均于某中流985 CS就读,先后于百度搜索、字节跳动电商以及携程等部门担任Linux C/C++后端研发工程师。% H; w9 a- w2 J% B7 p
    最近招聘季快到了,身边很多小伙伴都在摩拳擦掌、跃跃欲试,很多都打算看看新机会,这里推荐一个好朋友阿秀开发的互联网大厂面试真题解析网站,支持按照行业、公司、岗位、科目、考察时间等查看面试真题,有意者欢迎体验。! ?, ^0 M' [' ~4 m6 I0 d
    如果你明天就要面试了,那我建议你今晚来刷一刷这个网站,说不定就能遇到你明天的面试原题,目前已经有不少人在面试中遇到原题了,具体可以看下链接:字节跳动后端研发岗面试考察题目Top10、面试中局部性原理还真有用!
    8 L+ l; x* ]/ M1 c! }5 g

    uxv5eggdkmq6401335822.png

    uxv5eggdkmq6401335822.png
    ( L0 s1 w! L# m
    网址:https://top.interviewguide.cn/
    7 c; x6 ]: D' y6 k) I* M+ s
    同时,我也是知乎博主@韩飞宇,日常分享C/C++、计算机学习经验、工作体会,欢迎点击此处查看我以前的学习笔记&经验&分享的资源。
    1 U+ Q8 n2 n0 m) D5 R9 F1 Y- W: {我组建了一些社群一起交流,群里有大牛也有小白,如果你有意可以一起进群交流。
    / n( A* ^  r! p' i" r) }! f7 Z

    nff25zmdvjo6401335922.png

    nff25zmdvjo6401335922.png
    ) Z4 V. ]" R0 u* n
    欢迎你添加我的微信,我拉你进技术交流群。此外,我也会经常在微信上分享一些计算机学习经验以及工作体验,还有一些内推机会。% ^  F3 J2 a" c+ k; j$ D% U

    + {0 s8 E/ P' L( e# j

    ereqpeqfmlh6401336022.png

    ereqpeqfmlh6401336022.png

    9 T4 [! V: O0 H1 k  N1 f# U% m1 ?加个微信,打开另一扇窗- _5 V! C" L, X# g/ ]2 W9 A( j9 P2 k

    54lfrfqfzvu6401336122.gif

    54lfrfqfzvu6401336122.gif
  • 回复

    使用道具 举报

    发表回复

    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则


    联系客服 关注微信 下载APP 返回顶部 返回列表