电子产业一站式赋能平台

PCB联盟网

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

嵌入式 C 语言,柔性数组的灵活运用。

[复制链接]

568

主题

568

帖子

4221

积分

四级会员

Rank: 4

积分
4221
发表于 2025-1-22 08:01:00 | 显示全部楼层 |阅读模式
我是老温,一名热爱学习的嵌入式工程师) j1 P% p0 ~# o
关注我,一起变得更加优秀!不知道各位工程师大佬在平时的嵌入式软件开发中,是否有使用过柔性数组(可能比较少?),关于柔性数组的灵活运用,且看下文。. N/ v( O0 Z, T7 N( O
零长度数组概念:众所周知, GNU/GCC 在标准的 C/C++ 基础上做了有实用性的扩展, 零长度数组(Arrays of Length Zero) 就是其中一个知名的扩展.9 q" J% [' @0 {) L1 q+ ~) i8 v$ s
多数情况下, 其应用在变长数组中, 其定义如下:+ n5 X1 P$ r  `, l
struct Packet
9 s/ T- E8 q2 u; ^1 @{2 A$ E' C. `8 D5 G9 u( c! e
    int state;1 S3 Q* J3 F6 `0 t) j
  int len;
' l+ {( Z1 x$ x  char cData[0]; //这里的0长结构体就为变长结构体提供了非常好的支持
5 L- D  B  P$ o9 Q9 e};  V2 R* M1 E( b; U6 K0 A" l
首先对 0 长度数组, 也叫柔性数组,做一个解释 :; i8 U3 i, R! D( L3 r) v
用途 : 长度为0的数组的主要用途是为了满足需要变长度的结构体;1 U8 a, W- C' g$ B
用法 : 在一个结构体的最后,声明一个长度为 0 的数组, 就可以使得这个结构体是可变长的. 对于编译器来说, 此时长度为 0 的数组并不占用空间, 因为数组名本身不占空间, 它只是一个偏移量, 数组名这个符号本身代表了一个不可修改的地址常量
. ^/ F- a3 J, z
(注意 : 数组名永远都不会是指针!), 但对于这个数组的大小, 我们可以进行动态分配。/ f5 V3 r1 ]8 v7 Y
注意 :如果结构体是通过 calloc、malloc 或 者 new 等动态分配方式生成,在不需要时要释放相应的空间。
  |% h6 f6 W1 v8 c4 o优点 :比起在结构体中声明一个指针变量、再进行动态分 配的办法,这种方法效率要高。因为在访问数组内容时,不需要间接访问,避免了两次访存。
/ E* ?# ]0 Y4 C& a4 ^" U缺点 :在结构体中,数组为 0 的数组必须在最后声明,使用上有一定限制。, }2 F1 [6 O  Q' |$ {! s
对于编译器而言, 数组名仅仅是一个符号, 它不会占用任何空间, 它在结构体中, 只是代表了一个偏移量, 代表一个不可修改的地址常量!, n3 f( K* e; Y8 i
0 长度数组的用途:我们设想这样一个场景, 我们在网络通信过程中使用的数据缓冲区, 缓冲区包括一个 len 字段和 data 字段, 分别标识数据的长度和传输的数据, 我们常见的有几种设计思路:
& t! M, ~$ ]: y8 J! C3 |+ y6 e定长数据缓冲区, 设置一个足够大小 MAX_LENGTH 的数据缓冲区1 k. I3 U* j7 k5 [1 p
设置一个指向实际数据的指针, 每次使用时, 按照数据的长度动态的开辟数据缓冲区的空间
& P0 i2 o  y2 Q1 y
我们从实际场景中应用的设计来考虑他们的优劣. 主要考虑的有, 缓冲区空间的开辟、释放和访问。- R5 d; K$ v0 [- I. {) T7 Q5 ]
1、定长包(开辟空间, 释放, 访问):比如我要发送 1024 字节的数据, 如果用定长包, 假设定长包的长度 MAX_LENGTH 为 2048, 就会浪费 1024 个字节的空间, 也会造成不必要的流量浪费:  V7 ]1 r9 J7 ]# ?" ^
数据结构定义:
/ r  j7 N2 `  N7 B! W4 Y* Y
//  定长缓冲区
% w$ i7 u8 I# A; c) e: pstruct max_buffer  k, _% O7 n5 P, d1 i. [5 y
{# U8 n0 D2 h% h' F% j' c0 N4 X
    int     len;    # Q3 s8 j( C0 x! l  ~
    char    data[MAX_LENGTH];
& E# h; a$ B1 Z0 U};数据结构大小:考虑对齐, 那么数据结构的大小 >= sizeof(int) + sizeof(char) * MAX_LENGTH
7 M2 A7 q0 y2 z6 [. v4 q
由于考虑到数据的溢出, 变长数据包中的 data 数组长度一般会设置得足够长足以容纳最大的数据, 因此 max_buffer 中的 data 数组很多情况下都没有填满数据, 因此造成了浪费。! e( N! ]/ g! W2 j7 e. \# t; s
数据包的构造:假如我们要发送 CURR_LENGTH = 1024 个字节, 我们如何构造这个数据包呢;一般来说, 我们会返回一个指向缓冲区数据结构 max_buffer 的指针:7 j: p- O+ r& q% ], x! g: i
//  开辟
7 \2 V6 u* ?; m$ kif ((mbuffer = (struct max_buffer *)malloc(sizeof(struct max_buffer))) != NULL)   
% G1 T/ {& N; U{
/ `' ~$ k( m/ ]1 U& \0 h  mbuffer->len = CURR_LENGTH;  T# A' C# i5 Z, L
  memcpy(mbuffer->data, "Hello World", CURR_LENGTH);
4 S9 s0 `3 ~$ e+ |6 x  printf("%d, %s- e: s: r7 o8 y. u
", mbuffer->len, mbuffer->data);
9 y. E% A6 J. c8 y% K+ O. Q! a6 ~; H}
7 ?6 {0 N' m# N' p2 r" R访问:这段内存要分两部分使用;前部分 4 个字节 p->len, 作为包头(就是多出来的那部分),这个包头是用来描述紧接着包头后面的数据部分的长度,这里是 1024, 所以前四个字节赋值为 1024 (既然我们要构造不定长数据包,那么这个包到底有多长呢,因此,我们就必须通过一个变量来表明这个数据包的长度,这就是 len 的作用);而紧接其后的内存是真正的数据部分, 通过 p->data, 最后, 进行一个 memcpy() 内存拷贝, 把要发送的数据填入到这段内存当中6 S5 k! }2 E: H& u. R2 y3 E/ W
释放:那么当使用完毕释放数据的空间的时候, 直接释放就可以了
4 L  f. ~( c; s+ U# |. [. ?) d
// 销毁# ^+ X+ @9 y- m" K* g
free(mbuffer);4 p* E* G0 m) f: w( ^1 }& Q
mbuffer = NULL;8 U4 z+ |  Z) E* q
2、小结:使用定长数组, 作为数据缓冲区, 为了避免造成缓冲区溢出, 数组的大小一般设为足够的空间 MAX_LENGTH, 而实际使用过程中, 达到 MAX_LENGTH 长度的数据很少, 那么多数情况下, 缓冲区的大部分空间都是浪费掉的
. j& Q9 E: |3 j1 \但是使用过程很简单, 数据空间的开辟和释放简单, 无需程序员考虑额外的操作2 [9 L0 h. g9 ~" C0 c
3、 指针数据包(开辟空间, 释放, 访问):如果你将上面的长度为 MAX_LENGTH 的定长数组换为指针, 每次使用时动态的开辟CURR_LENGTH 大小的空间, 那么就避免造成 MAX_LENGTH - CURR_LENGTH 空间的浪费, 只浪费了一个指针域的空间:$ V' }* P; O! Z3 g, _1 u
数据包定义:
' ]. J( F; L* p& t
struct point_buffer. I& V& d" x  f' d8 |. r
{2 N& r6 Y8 \5 S- c- G* R2 y& b
  int     len;
0 ]5 ]) g% C7 m: A% s9 ]  char    *data;0 D  S0 L; w! H1 w( u! _5 l7 M" F
};1 ~7 n9 T- ^: F
数据结构大小:考虑对齐, 那么数据结构的大小 >= sizeof(int) + sizeof(char *)
; {1 Z) w' ~2 p. J空间分配:但是也造成了使用在分配内存时,需采用两步
5 x* s: @) V5 Z" ~/ D3 E/ r! m& T
// =====================
  v4 C; g1 P0 ?: Z0 M// 指针数组  占用-开辟-销毁
  u8 u+ i8 w2 x  q, g; l// =====================, y  }# _9 c' D; S7 b* c
///  占用    * ?! l: k" ?. g! z) S3 b  o& E
printf("the length of struct test3:%d1 Q4 R: U4 O% ~+ p% l$ `
",sizeof(struct point_buffer));( \# |# q( t# P2 Y: ^  z2 b
///  开辟1 @. ~4 D% r. \0 ~( `2 u
if ((pbuffer = (struct point_buffer *)malloc(sizeof(struct point_buffer))) != NULL)
9 q# I/ F1 k8 m0 T2 ~/ F7 T{
% b5 k2 A  z. f3 v  pbuffer->len = CURR_LENGTH;
5 z. n( n) F  N6 H0 v4 y5 F! |- j5 c9 f  if ((pbuffer->data = (char *)malloc(sizeof(char) * CURR_LENGTH)) != NULL)
# a0 H4 d$ h8 E  {4 p6 R. ?* m5 R* O+ y) x+ `
      memcpy(pbuffer->data, "Hello World", CURR_LENGTH);# ]; ~' s7 W$ W! L- j
      printf("%d, %s+ T; k# H/ A3 C/ B$ ?) m
", pbuffer->len, pbuffer->data);5 `1 a8 ]* T6 T4 c
  }7 I6 F/ t* g0 O: l" H/ \7 N% r
}
0 y/ v5 s/ S8 Z; n2 ?& E9 F3 D6 c首先, 需为结构体分配一块内存空间;其次再为结构体中的成员变量分配内存空间。
5 p  O& f! z" m4 l$ R这样两次分配的内存是不连续的, 需要分别对其进行管理. 当使用长度为的数组时, 则是采用一次分配的原则, 一次性将所需的内存全部分配给它。
) ^2 _0 @0 n8 C释放:相反, 释放时也是一样的:, E0 @+ i( i( o$ u
/// 销毁
4 C7 ^  J" g1 z+ A9 Zfree(pbuffer->data);
  }- u: u3 H1 N: f2 M4 Efree(pbuffer);
, t/ O4 p! P+ `0 f! \pbuffer = NULL;: C9 x' a, F* n  F
小结:
8 l* a1 ?, }/ t1 k* d: y2 r- 使用指针结果作为缓冲区, 只多使用了一个指针大小的空间, 无需使用 MAX_LENGTH 长度的数组, 不会造成空间的大量浪费。
% V8 C2 ^( I5 M; S但那是开辟空间时, 需要额外开辟数据域的空间, 施放时候也需要显示释放数据域的空间, 但是实际使用过程中, 往往在函数中开辟空间, 然后返回给使用者指向 struct point_buffer 的指针, 这时候我们并不能假定使用者了解我们开辟的细节, 并按照约定的操作释放空间, 因此使用起来多有不便, 甚至造成内存泄漏。
& j( a0 O, T0 `* c; I3 ?8 t
4、变长数据缓冲区(开辟空间, 释放, 访问)定长数组使用方便, 但是却浪费空间, 指针形式只多使用了一个指针的空间, 不会造成大量空间分浪费, 但是使用起来需要多次分配, 多次释放, 那么有没有一种实现方式能够既不浪费空间, 又使用方便的呢?7 l. D- k  [% w3 X
GNU C 的 0 长度数组, 也叫变长数组, 柔性数组就是这样一个扩展。对于 0 长数组的这个特点,很容易构造出变成结构体,如缓冲区,数据包等等:
$ d- r, U, A( M0 k$ D数据结构定义:
+ |' I$ y& T/ @" ^
//  0长度数组
* J/ \' z& G$ p6 r- o+ K! ~struct zero_buffer
& _( j4 n  L0 V$ L{
) r2 A3 ^4 Y& ?+ D  int     len;
5 [% h- B- _* E5 q7 |6 `+ a  char    data[0];. N, b2 }+ B. M7 Z4 z8 `
};
% c, L9 w; W. K& A( N+ @数据结构大小:这样的变长数组常用于网络通信中构造不定长数据包, 不会浪费空间浪费网络流量, 因为 char data[0]; 只是个数组名, 是不占用存储空间的:
' A3 u9 L7 M( f4 I. e! V$ w' g
sizeof(struct zero_buffer) = sizeof(int)
, j# B6 j) }8 s) Y6 K$ q8 H) f开辟空间:那么我们使用的时候, 只需要开辟一次空间即可7 C; m: H& z" o/ ?; L# C; l
///  开辟' M1 Q: d; ^7 N9 S8 I" \$ j
if ((zbuffer = (struct zero_buffer *)malloc(sizeof(struct zero_buffer) + sizeof(char) * CURR_LENGTH)) != NULL)  Y; Q: E( X$ \% I6 f6 c
{/ D% Q! a- H' w9 D" M, b& A
    zbuffer->len = CURR_LENGTH;
9 i+ a/ {) W4 l$ K5 U. N0 g3 [5 A( X% w    memcpy(zbuffer->data, "Hello World", CURR_LENGTH);
# U5 ?5 L0 {1 h: ^' `& i    printf("%d, %s
9 N8 T' }  |$ @  m- H", zbuffer->len, zbuffer->data);- s& S3 a% w  t$ r
}" c) ?. F: m1 k0 n- d
释放空间:释放空间也是一样的, 一次释放即可0 M" q) D) `3 [
///  销毁3 n0 ^7 \2 V: L9 |+ ?6 x) E
free(zbuffer);
  `% @( U" O1 R1 _0 Nzbuffer = NULL;6 _$ B9 k  f% ^+ E1 G( L& E
总结:
1 r5 B2 k* E( }& v& |! o  d
// zero_length_array.c  r' J' U! U- w+ D, h2 W
#include # O' }0 Q# h* S0 u6 @# i
#include + t' s% J* m# S. X8 T  e3 M; h$ r
#define MAX_LENGTH      1024
; b" H7 u7 G/ x) Q#define CURR_LENGTH      5128 ~: V+ _( d3 ?" u
//  0长度数组
+ Z: N! b5 o* g- qstruct zero_buffer- L5 r4 j5 C" P1 {
{8 ^: M' \; O0 S
int     len;
' `# a$ o. w# {( f! bchar    data[0];
( U8 `' D1 w4 U0 p}__attribute((packed));
: S9 {% I6 V' p$ u9 V//  定长数组
5 {; `: o* d5 i$ S+ y2 Rstruct max_buffer
$ L. v5 w2 D4 T) \) G{5 \* v) I* i8 w0 b  E
int     len;
3 `! z% c+ m5 z9 r7 x  D7 P) p) tchar    data[MAX_LENGTH];( A8 D# D8 O4 o2 g3 V  e1 a( x
}__attribute((packed));
( |0 [- v# _% G6 I; X//  指针数组% r/ t; S9 U4 k4 r( p! ~$ J8 G
struct point_buffer
% [4 ?  t: U  m& Q{9 h* k  u5 ^+ j; L: s
int     len;1 Q9 ?& B* x9 B
char    *data;6 C( q$ E' O& {. s. q: G8 ?) G1 o: R
}__attribute((packed));; L5 O5 f4 Z2 T2 ~0 Q8 y: y5 C, j. X
int main(void)
: o, b7 {/ Y9 ^  Z* B4 R{
3 d- F0 e2 ]" H, Q) _+ X    struct zero_buffer  *zbuffer = NULL;3 X. |% O- }5 u
  struct max_buffer   *mbuffer = NULL;4 _: f; z6 q" m7 ^  B* a: S
  struct point_buffer *pbuffer = NULL;
! P( l) j/ x  }/ M7 Y8 I  // =====================
& |1 }1 N9 j$ `& z6 R  // 0长度数组  占用-开辟-销毁
# r$ R, d) q- R2 _5 z  // =====================' S4 Q4 v5 ~, b' @
  ///  占用! |6 ~# F/ J  g
  printf("the length of struct test1:%d# }: p4 A) W$ Y
",sizeof(struct zero_buffer));* h7 [, M5 w+ n
  ///  开辟! D+ C$ S3 _6 s& N5 D. d
  if ((zbuffer = (struct zero_buffer *)malloc(sizeof(struct zero_buffer) + sizeof(char) * CURR_LENGTH)) != NULL)) ?! _6 C. R5 T  C- W. @
  {. [! i1 ?3 i1 z5 n/ ~& o; S( w
      zbuffer->len = CURR_LENGTH;! e2 ^% V: c4 f! D2 t8 X) S# i
      memcpy(zbuffer->data, "Hello World", CURR_LENGTH);
: Z9 e& T3 M# _1 @      printf("%d, %s
+ ?  F& t. V: {0 {", zbuffer->len, zbuffer->data);
  o- `4 ?4 |! {, _/ ]% e  }  
+ G) g% D- G! R) e  ///  销毁/ s2 V2 ?: X- w. l/ F. r9 S! z1 J
  free(zbuffer);! e. h4 E: i% T! u3 l; _. }- H
  zbuffer = NULL;: ]' l" H' B6 V+ _* O/ v9 k$ i
  // =====================
2 k9 z* C& k  @4 u  // 定长数组  占用-开辟-销毁
* }$ Z3 h+ B/ i( M0 k5 B  // =====================/ M% D# X. _4 `! _; B+ [6 |
  ///  占用
5 S* c- D1 A5 n  O- Q0 v2 V  printf("the length of struct test2:%d3 m% R- i# O! T5 i( d
",sizeof(struct max_buffer));
9 I. r4 u: L: s- r7 d' u  ///  开辟
: Q; {0 o& P0 t6 K  if ((mbuffer = (struct max_buffer *)malloc(sizeof(struct max_buffer))) != NULL)
& z( I+ M+ x; a- b4 D. k  {3 J4 l6 f5 s: R' [
      mbuffer->len = CURR_LENGTH;
5 H. s) ?" q+ w1 \" n5 n% F      memcpy(mbuffer->data, "Hello World", CURR_LENGTH);
# ~  ^$ i$ D) ~* K1 r7 H/ U      printf("%d, %s7 n9 Q8 [" |, \  |' W$ B
", mbuffer->len, mbuffer->data);: t5 ^# \. P1 I4 t( L
  }/ E- F' M  F6 h4 l* L$ r
  /// 销毁
- W/ j  N4 u3 V+ ^3 s% e2 j: {  free(mbuffer);, `# t' h% @) g( b6 Y3 n+ X
  mbuffer = NULL;
  u1 h* i% D6 t, Z  // =====================4 _1 n' r" v. R* a- v3 i* I# Z
  // 指针数组  占用-开辟-销毁
" {1 d- u) A, i  // =====================
1 O/ X7 [4 W2 g6 s4 _6 D' _  ///  占用9 G! O9 @2 s, c& F/ W' ]# B
  printf("the length of struct test3:%d
  p" i, ~6 ]+ _0 A' x",sizeof(struct point_buffer));
9 w9 }. @3 Z$ s5 c' t' f  ///  开辟
; J  t  Y9 ~9 W# v  if ((pbuffer = (struct point_buffer *)malloc(sizeof(struct point_buffer))) != NULL)
/ v& ]: R; K; X" q4 H  {" k7 _7 P* \; Z- t. |
      pbuffer->len = CURR_LENGTH;& t7 q2 [; I6 X% D
      if ((pbuffer->data = (char *)malloc(sizeof(char) * CURR_LENGTH)) != NULL), A( i1 Q1 E3 [  e
    {7 u, C, [) P8 [9 l% W# v% |
        memcpy(pbuffer->data, "Hello World", CURR_LENGTH);2 P$ n0 k. a8 \! P
      printf("%d, %s
0 V* y1 k; q8 e: q2 L", pbuffer->len, pbuffer->data);
5 i2 X7 M! j- e: I    }
# m, H3 }+ r/ j: `; ^* i8 V. m8 o& s  }  C1 |: e: Q  \4 S" S
  /// 销毁
  H! g4 W  g9 |" O, L% _  free(pbuffer->data);
& u1 j1 W: x; \  free(pbuffer);
' W7 Q1 S& U( h  pbuffer = NULL;
7 q) @1 m: G/ m. _5 i8 Y( x: s  return EXIT_SUCCESS;) b9 M) u8 @3 P& R0 U$ m3 k  p
}9 x$ Q" w2 K, K2 p0 b; b

5 G8 ]# R! y2 V+ A+ @9 _2 y& j2 {7 L

qli2npwbrqk640362324.png

qli2npwbrqk640362324.png
2 G- Y+ b$ D' L) C

) m8 v% G  v7 G1 R) G$ i& K长度为 0 的数组并不占有内存空间, 而指针方式需要占用内存空间.. k/ x$ c% S3 D
对于长度为 0 数组, 在申请内存空间时, 采用一次性分配的原则进行; 对于包含指针的结构体, 在申请空间时需分别进行, 释放时也需分别释放.
0 i" I; ?2 O; v( c# O对于长度为 0 的数组的访问可采用数组方式进行
+ U5 u7 i& M* _3 j! S/ ^% }0 U/ }
GNU Document中 变长数组的支持:参考:, Q+ u5 C3 w1 x* N1 E9 j5 N5 m7 G
6.17 Arrays of Length Zero
2 z1 ~. Q" o; w0 m; R* }6 z5 pC Struct Hack – Structure with variable length array2 {) w$ o+ {( f8 j- A
在 C90 之前, 并不支持 0 长度的数组, 0 长度数组是 GNU C 的一个扩展, 因此早期的编译器中是无法通过编译的;对于 GNU C 增加的扩展, GCC 提供了编译选项来明确的标识出他们:
0 q) u2 T" O2 d0 s& n* m/ T' A  V-pedantic 选项,那么使用了扩展语法的地方将产生相应的警告信息
, X4 {0 t  O* u& T. h( f& d. G-Wall 使用它能够使 GCC 产生尽可能多的警告信息5 A  L, T/ x! T; c; l( c
-Werror, 它要求 GCC 将所有的警告当成错误进行处理
3 X, }6 c% y6 b' }0 Y+ ~
// 1.c
6 w) @7 |, `" b4 x) M#include
, G1 B; _* a) {& s" P6 X#include 0 H+ D# t/ _- y
int main(void)) o) E0 Y+ t5 F8 R: P
{
- m' N! m7 _9 }4 `- T) j    char a[0];
; d8 a3 N3 P6 S1 b1 {* W  printf("%ld", sizeof(a));
8 w- {& `; J1 e  t: V* k5 e  return EXIT_SUCCESS;2 q5 Z* o: C0 o# P% Q
}5 M% _" b! Z3 X0 @( ]" Y
我们来编译:
; M( c0 L- S) @: n1 ^) K  g1 j  c# 显示所有警告% H/ z. C! b6 C/ W
gcc 1.c -Wall
( _  F+ K$ L: o; Z9 q: W#none warning and error3 _! Q, o% B4 ~1 a0 G/ \3 d0 f
# 对GNU C的扩展显示警告6 I- t/ U* D% m
gcc 1.c -Wall -pedantic
1 @! e& r0 }% A9 |1 X; L! G# t# B9 `1.c: In function ‘main’:
- x7 j9 u- {! E1.c:7: warning: ISO C forbids zero-size array ‘a’
. G9 O; ^2 u& D# 显示所有警告同时GNU C的扩展显示警告, 将警告用 error 显示
& v/ c0 U! S  D% s3 zgcc 1.c -Werror -Wall -pedantic 2 T0 g& e3 S2 W0 o+ b
cc1: warnings being treated as errors1 Z  x8 b; v+ j. a
1.c: In function ‘main’:9 w0 w/ @- ?$ v- X, k( \0 ~" @7 m
1.c:7: error: ISO C forbids zero-size array ‘a’5 r; H5 H+ C; }! U
* G0 Y4 [; e& I) s" V

x5dod34ffci640362424.png

x5dod34ffci640362424.png

7 q* @# N* ^* g" A; o- u0长度数组其实就是灵活地运用数组指向的是其后面连续的内存空间:
3 W3 N1 X4 g0 istruct buffer4 w  X9 y- O! k7 G
{
; H" g$ |" h& J( ?' @  int     len;
$ S' l) D! r6 z  char    data[0];1 p3 K0 R( x/ P) }! q) I8 \+ H9 J
};
. o8 v9 \+ {5 G, N+ Q% S在早期没引入 0 长度数组的时候, 大家是通过定长数组和指针的方式来解决的, 但是:, p: E6 l" }/ J2 W$ p9 D
定长数组定义了一个足够大的缓冲区, 这样使用方便, 但是每次都造成空间的浪费) F) v( j) S0 Y: [: o% G* J
指针的方式, 要求程序员在释放空间时必须进行多次的 free 操作, 而我们在使用的过程中往往在函数中返回了指向缓冲区的指针, 我们并不能保证每个人都理解并遵从我们的释放方式。
6 O' c: x. B5 F5 t7 V所以 GNU 就对其进行了 0 长度数组的扩展. 当使用 data[0] 的时候, 也就是 0 长度数组的时候,0长度数组作为数组名, 并不占用存储空间。* A4 F; E* }( U
在 C99 之后,也加了类似的扩展,只不过用的是 char payload[] 这种形式(所以如果你在编译的时候确实需要用到 -pedantic参数,那么你可以将 char payload[0] 类型改成 char payload[] , 这样就可以编译通过了,当然你的编译器必须支持 C99 标准的,如果太古老的编译器,那可能不支持了)' E4 H4 u. ^1 e- Y  I; P
// 2.c payload
+ N+ b: \2 g) m$ D3 t#include #
' y4 {& b8 U' C6 v* ?8 jinclude
7 V9 e8 Z. L9 @struct payload3 h; B* T$ _. A+ |- ~
{3 ]! O4 s  Z* H5 z
    int   len;
0 s1 u& U2 W0 i! [" a2 x+ R: ~& E) M  char  data[];
' [# M& K! d: [% |1 v" _' j5 i4 J};) U& j+ e  f4 a" g, i, X  D
int main(void)2 B7 j; a  P# {+ W; u# m
{
7 }6 D8 \+ P8 S0 Z) _. z    struct payload pay;
' q/ a- N  g4 ^/ z  printf("%ld", sizeof(pay));
& e8 T# J1 Z6 p7 @' w  return EXIT_SUCCESS;
' v: S; r" p) P8 Z4 x: Y, n& ^+ m}
$ m! K4 @( D% N, C4 M. M* F使用 -pedantic 编译后, 不出现警告, 说明这种语法是 C 标准的6 _7 f/ T8 \' @  [* {/ J
gcc 2.c -pedantic -std=c99
& c% A% b2 f/ U: V5 T% o8 Y. x3 _8 S* D+ O5 T6 g

ajo2vddystc640362524.png

ajo2vddystc640362524.png
8 T# H* h* d* |/ x* s9 L
所以结构体的末尾, 就是指向了其后面的内存数据。因此我们可以很好的将该类型的结构体作为数据报文的头格式,并且最后一个成员变量,也就刚好是数据内容了.# S. ^/ z) h* q* Q% X* I) ]0 c
GNU 手册还提供了另外两个结构体来说明,更容易看懂意思:5 E% L- S9 G$ q
struct f1
& T/ d3 M4 P4 k6 u+ Q, a1 c{$ t' r6 F% N7 i' l
    int x;
: v3 [& \8 B/ H& `$ L  int y[];$ q" d# S  ], w7 r* Z) J4 ?; s
} f1 = { 1, { 2, 3, 4 } };' s# p3 v+ o- i/ X
struct f2
' U2 Z  O! ]0 e{6 J& d  c& h. ]- @$ Y
    struct f1 f1;8 V/ C4 x% J  d9 ~5 A
  int data[3];
: ?8 A6 L8 V% {} f2 = { { 1 }, { 5, 6, 7 } };
6 B7 O8 y) ]6 x, e' p" o, T我把 f2 里面的 2,3,4 改成了 5,6,7 以示区分。如果你把数据打出来。即如下的信息:
9 {; o/ ?+ N7 ]. Q( n# z3 Y8 wf1.x = 1
2 Y4 W" i( K# P/ E2 X# Af1.y[0] = 2
: x& F# a: u9 _$ j4 \  L( Zf1.y[1] = 3
4 Y& `. S- |" C$ `+ y2 K9 ]; zf1.y[2] = 4
8 x3 h3 a2 d4 T$ n( w9 f也就是 f1.y 指向的是 {2,3,4} 这块内存中的数据。所以我们就可以轻易的得到,f2.f1.y  指向的数据也就是正好 f2.data 的内容了。打印出来的数据:' Q4 {. ?2 e  W( Z, v% l
f2.f1.x = 14 v& C2 `" ^$ v6 n4 a: X9 T+ M
f2.f1.y[0] = 5
. f* r5 T: {: r" v& d. P* of2.f1.y[1] = 6
( t; e& H: w: T/ ff2.f1.y[2] = 75 w- o# Q, c- M) E0 C
如果你不是很确认其是否占用空间. 你可以用 sizeof 来计算一下。就可以知道 sizeof(struct f1)=4,也就是 int y[]其实是不占用空间的。但是这个 0 长度的数组,必须放在结构体的末尾。如果你没有把它放在末尾的话。编译的时候,会有如下的错误:& q8 v( @3 `% b$ D
main.c:37:9: error: flexible array member not at end of struct
, g# G' k1 d0 F0 O                    int y[];+ z, K5 d; ^# F, [! G& }
                            ^# R8 |1 I( l+ m- [  I' m1 ^+ k
到这边,你可能会有疑问,如果将 struct f1 中的 int y[] 替换成 int *y ,又会是如何?这就涉及到数组和指针的问题了. 有时候吧,这两个是一样的,有时候又有区别。2 Z3 W$ S$ [3 ~* g9 u$ G
首先要说明的是,支持 0 长度数组的扩展,重点在数组,也就是不能用 int *y 指针来替换。sizeof 的长度就不一样了。把struct f1 改成这样:  g: o8 s- G9 a& F
struct f3" t9 a; {8 _8 F3 i
{# Q0 O% C: Z5 l; k" t# ~" o% @# ]
    int x;
2 }" |4 _7 o' r2 v. S& V- l  int *y;0 e5 B* X, Q+ ]! @4 p: k* {
};
/ o1 Z. M3 I& a, r- d在 32/64 位下, int 均是 4 个字节,  sizeof(struct f1)=4,而 sizeof(struct f3)=16
+ p, g, G/ k1 w, S( Q% @1 Y; j因为int *y 是指针, 指针在 64 位下, 是 64 位的, sizeof(struct f3) = 16;如果在32位环境的话, sizeof(struct f3) 则是 8 了, sizeof(struct f1) 不变. 所以 int *y 是不能替代 int y[] 的;
; b* X( }8 @  \* I! d( r! Z代码如下:* i  Q- D  E4 K( q5 o! {* Z
// 3.c
! @7 W, f" w9 p# ^6 ?2 ?9 }2 c/ c#include 2 R" Z. J4 W  a. g8 E/ P' _1 \4 t. M2 }
#include . }- D" Q8 x$ I) e- E: X
struct f1
7 W! v0 Q+ k  N* E{7 Y/ e4 I$ S# [; L% l
    int x;) `& A3 G& B, H& ^3 T9 d
  int y[];2 b0 P/ V, b/ Z7 k: o
} f1 = { 1, { 2, 3, 4 } };
5 T8 R- ], \+ a/ C% W! b: Gstruct f2
& }) R& t# ], Q4 o* f5 l6 @{: S" ?6 I: L% Y; b" ?- w
    struct f1 f1;/ v; W1 C, s# ?6 T% D& X' o+ s
  int data[3];
0 Q7 F9 I4 m! E  ^, \5 l: @; P} f2 = { { 1 }, { 5, 6, 7 } };
+ F  d. E0 \6 ?' w. estruct f3
# }: o" x1 u! ^* [' N5 T9 p* b{
" n. K/ N8 ~+ n# v9 \' P- U    int x;
$ `8 b: H2 q. w$ c- h4 s  int *y;
; m: [# [# x: @5 G, k};# y6 g4 r8 A. }0 H$ Q% w
int main(void)
: v% H1 Y% m& |1 V{1 W7 m2 W* F( M5 O% s0 Y- k
    printf("sizeof(f1) = %d/ N, m2 a% u, K/ w/ b* J* p: m3 ]# `
", sizeof(struct f1));$ j% R0 l% n2 Y& F
  printf("sizeof(f2) = %d+ k8 @2 a( q& L. I4 |4 S/ q
", sizeof(struct f2));
9 i4 [8 ]" ^% a! J8 I1 k. Y  printf("szieof(f3) = %d
, t9 |, @6 b' D& L8 [0 X5 A", sizeof(struct f3));: `/ S- p2 {5 D9 @
  printf("f1.x = %d
( a" p; R+ s! ~4 y. T", f1.x);* I( P9 ]3 h' K* h* p! I! d1 t
  printf("f1.y[0] = %d4 L/ H/ O3 r! i9 c2 t/ O- x( H6 Q8 Q
", f1.y[0]);+ @, V* j# w; T& H/ Q
  printf("f1.y[1] = %d
2 A. h8 d" k0 ~' F- u", f1.y[1]);
" i: D+ l8 H" k- a& T2 C0 x2 C  printf("f1.y[2] = %d# t! }1 m( n: t3 ?' p: F0 |
", f1.y[2]);3 s) L, o& m. g4 o0 P1 ~
  printf("f2.f1.x = %d
) Z0 }4 R' U* G1 c0 P. h: h0 }1 u! p", f1.x);
. M+ E: [+ c6 w, W  printf("f2.f1.y[0] = %d
" K+ g  p: X3 O& H", f2.f1.y[0]);. D" @6 ]% E/ i
  printf("f2.f1.y[1] = %d
: P$ O3 S1 R% ?", f2.f1.y[1]);; B8 m! z4 G) m9 l
  printf("f2.f1.y[2] = %d& f9 r4 t: J9 x, n
", f2.f1.y[2]);
( h9 E( e; G3 A. p( P1 z  return EXIT_SUCCESS;
! E* w2 Z/ I. J- m& Y* y* v& n: h}
; @/ I! p2 w7 x7 D$ W. D2 c' N  _3 N- U, _% d6 n

rzu4trhxaoj640362624.png

rzu4trhxaoj640362624.png
8 p0 E, Y5 A0 ^8 @
0 长度数组的其他特征:1、为什么 0 长度数组不占用存储空间:0 长度数组与指针实现有什么区别呢, 为什么0长度数组不占用存储空间呢?" R% @3 O" @" ]1 e; d
其实本质上涉及到的是一个 C 语言里面的数组和指针的区别问题。char a[1] 里面的 a 和 char *b 的 b  相同吗?
2 t8 \$ i6 x* f《 Programming Abstractions in C》(Roberts, E. S.,机械工业出版社,2004.6)82页里面说:9 ]' ^/ {, y1 L
“arr is defined to be identical to &arr[0]”.8 R# b" u0 P6 v; m! Y
也就是说,char a[1] 里面的 a 实际是一个常量,等于 &a[0] 。而 char *b 是有一个实实在在的指针变量 b 存在。所以,a=b 是不允许的,而 b=a 是允许的。两种变量都支持下标式的访问,那么对于 a[0] 和 b[0]本质上是否有区别?我们可以通过一个例子来说明。* c. L( t) ~5 R3 g0 \9 Q
参见如下两个程序 gdb_zero_length_array.c和 gdb_zero_length_array.c:$ _4 w: x) g: p% D" d9 t4 U
//  gdb_zero_length_array.c
: d/ z, N( q: p; x% Y#include
: p1 N1 |9 k4 \4 s) O8 B#include 3 Q# g' ]2 C+ f: ~  L7 y4 V
struct str
. I: D2 F, Q9 Y# J{
8 Z1 ~( l+ ^, I# C    int len;
. G/ p: C/ [" b4 p& w2 ]8 ?  char s[0];$ V5 x# `+ ]0 R
};
( N' r. ^. |" S+ y* ~0 jstruct foo
& `4 O* k5 y& j3 E+ `( r& h{
6 R9 B1 h) q  S7 J    struct str *a;- V( ^: K/ G: C: ~+ n# y! N. K
};
# R7 ?. n/ D  C: [( ^int main(void)
7 K0 y" [$ z/ A; A1 `4 G{
# X5 x6 y* s/ A; J# n    struct foo f = { NULL };
) X. [. N. B5 Q- m  printf("sizeof(struct str) = %d- P2 `1 `& q0 D; H0 o& K: G0 `4 s
", sizeof(struct str));
) R- z% q" m! _/ J& r, a4 y  printf("before f.a->s.1 ?* `+ ~( E) I
");
7 @/ I/ y. E5 K  if(f.a->s)
  x7 K# _' i1 H- c  {* n/ e& R0 x& m9 q0 `& b4 h
      printf("before printf f.a->s.# R; _5 J2 s( ^8 {
");" |1 [5 g+ J3 l2 H1 @5 R0 y
    printf(f.a->s);  S  S6 Q5 o& g0 L2 M5 A% E
    printf("before printf f.a->s., E' g! v/ T6 _: X  a8 W* A
");
2 w2 ^* k% p/ [# R# m4 c) p' ^( i  }! i0 j1 s% A8 T0 E- ]
  return EXIT_SUCCESS;
8 N1 E7 N/ I3 _. G. N) Q6 s4 U! a3 x}1 r7 F3 r! p" J/ N) w& ?
5 j% ~# f7 J. X$ ~# z1 D

ruq3y42mxo1640362724.png

ruq3y42mxo1640362724.png
5 F3 o4 b) S  S- Y* [
: r; z6 h! `% W6 }# K, s% T
//  gdb_pzero_length_array.c
( F' Y& X! M4 e- F#include #7 S+ O. @6 T0 o6 \
include
% `3 p! @! I' ^8 O* f3 `struct str+ q  n) m; m) Q( H- L3 r3 p( s
{; v# A; A/ C; w# H; d% k! m% U8 @6 Q5 Q
    int len;
/ w- O$ \" E4 W( N  char *s;
9 n+ A( a# |9 Q( l6 {! W: T1 D};
# M' L; C; x: l' t' \struct foo
6 @4 t; t6 e- h: k/ g. |# \{
/ J  ]" K2 E7 E, J/ P7 \    struct str *a;/ a( ^' N/ N4 n) G
};4 W  Q  _% W+ E
int main(void)
% T( X  [" M* d. s9 e$ ~4 k{) B/ o- q9 u1 x, Z
    struct foo f = { NULL };
% f- q  K1 m* U& y5 h! V: c0 i9 k  printf("sizeof(struct str) = %d! e/ F4 P5 K: d
", sizeof(struct str));
3 J, x" R- _/ V4 M  printf("before f.a->s.4 A, J& q3 A" d
");
, P% x. K9 R# `0 S  if (f.a->s)+ f' h4 X* |- `' W/ o, x
  {* H% m! i; r" u7 F
      printf("before printf f.a->s.
5 `: f$ n+ g7 T( j/ o' F6 M" @% \6 c");, l2 Z# d5 t+ B: I
    printf(f.a->s);
( Y0 V% c) Z: }" y+ a4 |4 r    printf("before printf f.a->s.5 i4 d& n3 M- B8 S
");8 `; ]7 ~0 J5 r9 x7 @- T" |
  }
  p. ~+ R1 G3 T8 u! V    return EXIT_SUCCESS;! \7 @, X; L- a+ o) F' d) S* |
}
: R( F! k- X- g
4 a8 s4 V, v; c# ]

wp23a2m1dy5640362824.png

wp23a2m1dy5640362824.png

' ^2 Z# @' D( W) d. `# d9 @1 j. `
9 g( B) ?5 N: t9 P1 x可以看到这两个程序虽然都存在访问异常, 但是段错误的位置却不同+ {* Q' G( b0 E4 F9 G% E
我们将两个程序编译成汇编, 然后 diff 查看他们的汇编代码有何不同- b$ ~3 R. O( ]; A  v
gcc -S gdb_zero_length_array.c -o gdb_test.s
9 R5 ^6 k2 u9 Q% p& D% {* W- N8 Qgcc -S gdb_pzero_length_array.c -o gdb_ptest
/ @' ^$ H( S& ^4 R) i' l; i6 jdiff gdb_test.s gdb_ptest.s8 Y7 W. F9 K7 s9 O' G
1c1
  k1 v! o2 r. P4 t/ T"gdb_zero_length_array.c"
- `4 A9 D7 @5 y---; y8 Z1 f3 _  v" E) A# n' s
>   .file   "gdb_pzero_length_array.c"+ j4 x/ g6 ^. H9 U  E. N
23c232 J# L* L0 X5 n
   movl    $16, %esi) ^+ J" k* ^+ Y( R9 }
30c30+ z  J+ G4 h0 U
   movq    8(%rax), %rax
- x0 X( m( Q/ D7 E/ _2 N36c36
# B! o7 ?5 S5 y: l: U! ]   movq    8(%rax), %rax  # printf("sizeof(struct str) = %d$ e6 m$ y6 t) n, }7 E# P6 v
", sizeof(struct str));
- y3 F- O, q5 b1 l( T23c23) t& p; r6 C  E7 c
#printf("sizeof(struct str) = %d. o2 b& V% ?" ~* S
", sizeof(struct str));
6 N4 q( I$ G; w# T: }3 r---+ K8 t: W2 F- ?
>   movl    $16, %esi     #printf("sizeof(struct str) = %d
$ D  {( t  p2 ]# U# m! h4 d1 u1 i", sizeof(struct str));, [4 M; T3 U: ~( X3 q" }9 K
从 64 位系统中, 汇编我们看出, 变长数组结构的大小为 4, 而指针形式的结构大小为 16:" z4 A8 w- _3 g- U6 v$ a" t) F4 o
f.a->s( c4 M5 W# Q* J! X
30c30/36c366 K  {7 D3 g: A- R( \1 w+ Z7 ^
   movq    8(%rax), %rax
. |! u, a% g# k; _可以看到有:
7 f  ~+ j$ X! ?对于 char s[0] 来说, 汇编代码用了 addq 指令, addq $4, %rax
1 o. i8 ?0 ^" V0 |% N对于 char *s 来说,汇编代码用了 movq 指令, movq 8(%rax), %rax
  {2 n) N' h6 {$ [) t& |+ o- W
addq 对 %rax + sizeof(struct str), 即 str 结构的末尾即是 char s[0] 的地址, 这一步只是拿到了其地址, 而 movq 则是把地址里的内容放进去, 因此有时也被翻译为 leap 指令, 参见下一例子
& p; a% l- K0 y2 ?从这里可以看到, 访问成员数组名其实得到的是数组的相对地址, 而访问成员指针其实是相对地址里的内容(这和访问其它非指针或数组的变量是一样的):) w  W3 K: k% d
访问相对地址,程序不会 crash,但是,访问一个非法的地址中的内容,程序就会 crash。
4 b* j! m1 b9 U0 O
// 4-1.c
3 b+ ]  n4 Z* e9 q9 q0 Y5 }; _#include
: {4 w+ g0 p& Z- [% R7 k#include ! M2 j. H9 c% m0 f; l% x; N7 [7 v
int main(void)
7 L# N: U- K1 g% L5 v% q. Q9 H* P{
! ]9 p2 E& B% B5 a) {" h8 B! B    char *a;; [, o6 Q% L) |2 b: y6 U. ]& U
  printf("%p7 q; `9 V4 S; C) ~7 c4 a% @, m
", a);
8 _( M- x. g' Q5 v  K) K  return EXIT_SUCCESS;* Z' l$ o( ^) [
}
% ^9 i; `2 ?) O6 `  ~2 r* @' N1 @0 ^& f' B7 t- [1 i
4-2.c9 u+ U# \" H* I2 `  t5 [: v7 Y
#include 6 c1 N1 B/ X+ {4 U5 k
#include # \0 s" @1 a: ?2 w5 |( Q2 I; k5 m9 }
int main(void)" _6 r7 U6 v9 _- q2 V
{
$ ~; c, _& B/ A! M    char a[0];  X+ F, p5 I& o" j
  printf("%p7 A1 z8 u& j  P. j
", a);    return EXIT_SUCCESS;4 }# h$ E5 D0 j$ Y$ }
}1 q, N* A# h+ ?- {4 T0 V1 T
2 i5 Q; r+ F8 [7 I
$ diff 4-1.s 4-2.s
8 {# E4 y  D2 b0 V- H/ g1c1
) R6 K2 G! @$ y. E! @  z  L"4-1.c"& |* ?0 \3 h5 Z' M/ E
---
8 E$ A" r4 p3 _>       .file   "4-2.c"$ ?' F. `) {. I6 H; F6 u; Y
13c132 O* i5 ?7 L/ A+ L5 }
16, %esp
) Y6 |; K+ d' M5 L5 z( q---7 c6 L, h" A" F. }) A
>       subl    $32, %esp) p4 {% N8 P4 s+ V/ |4 R& _
15c158 v4 D* O+ s' n! j% G2 T; V
16(%esp), %eax, m8 J9 u8 h, F, \2 T2 o' B
---; W6 ^" r% F3 `
>       movl    28(%esp), %eax
! q7 L$ m6 k: M" v3 I! S对于 char a[0] 来说, 汇编代码用了 leal 指令, leal 16(%esp), %eax:2 b. V& d1 K& P! |/ t* b
对于 char *a 来说,汇编代码用了 movl 指令, movl 28(%esp), %eax
+ W- D) j( e: _* X
2、地址优化:// 5-1.c
: U, g. I! `9 E' y#include ' i4 H+ ^* E5 s. Q- z+ h' D
#include ! R, C2 K4 O. P
int main(void)2 E- A0 _) y9 ^, j3 K6 a+ T
{
! u5 Z: d# r0 I# P    char a[0];
$ c: u+ N- c* [1 C  printf("%p% e3 H$ O1 |# c6 T
", a);
4 Z4 @9 g% t- Y$ S# P& ~  char b[0];( M& B4 u) e' U) e
  printf("%p1 `9 A6 O( Q  a
", b);
8 e  x0 b& b3 _( g) V( C% |  n# S  return EXIT_SUCCESS;$ S& J: N0 l/ R% q% x
}
4 |1 Y, `7 {$ n5 s# y2 L7 C' u
0 g: r/ K, A' Z  g

1tebahiqjoa640362924.png

1tebahiqjoa640362924.png

4 m. c& K7 K; ^$ u2 a+ G由于 0 长度数组是 GNU C 的扩展, 不被标准库任可, 那么一些巧妙编写的诡异代码, 其执行结果就是依赖于编译器和优化策略的实现的.
- g3 x9 A) O1 W$ i8 i  w: z比如上面的代码, a 和 b 的地址就会被编译器优化到一处, 因为 a[0] 和 b[0] 对于程序来说是无法使用的, 这让我们想到了什么?
  R7 N9 O3 S; {+ u0 E编译器对于相同字符串常量, 往往地址也是优化到一处, 减少空间占用:/ S' ~$ z' p3 b0 c
//  5-2.c
# i6 d% S: R  b% i#include ; Y6 F6 b$ X; P+ |" G* Y0 d8 D8 w
#include
' ~( t) K5 i, F7 c5 [3 ?int main(void)# C, v0 r4 _; ^1 H1 N! @
{
0 X3 n6 K* k0 {# W5 Z    const char *a = "Hello";
: F5 H, W3 c# R2 F0 |. n$ z7 M  printf("%p
' h' _5 {9 N( p6 R+ _6 @) x) X* o( p. x", a);
/ M' V0 E5 z  G- R2 I$ |1 p  const char *b = "Hello";1 f3 }2 z9 ?1 ?; q. t# b& ~
  printf("%p
' ~8 _; I. r- g2 [- \", b);3 `. d; X! S) K' k0 U. L  F" _% S
  const char c[] = "Hello";, d/ G" Q5 x- t  q5 w
  printf("%p0 ~4 K: C: z5 m0 ?( ]; x. T
", c);
8 \' i, G% i# v* H5 ]7 p  return EXIT_SUCCESS;
. o, `- g8 d+ G' ?) U2 `0 T& m}
* ]  ~6 p! a% k1 |: t$ s" c' Q6 h; L# [; n

4svgattu54u640363024.png

4svgattu54u640363024.png
. e+ \5 l# T+ Z5 V' a

5 _2 s6 c+ D. y1 E1 E2 M5 \" \--- 文章来源于牛逼的工程师网友. `; f$ L' |$ w& {! }) i6 M
-END-6 K* R3 y2 q, ?0 f8 |
往期推荐:点击图片即可跳转阅读                                                               
/ Y0 S; H; ~  L" N. ^1 b2 v                                                                        9 C9 H( r2 v- G8 R. r. z
                                                                                2 ^3 T" F1 W3 _. l

hfvaxxlenvq640363124.jpg

hfvaxxlenvq640363124.jpg
- T5 j/ w# x$ ^$ d  W
                                                                                嵌入式软件,能用“低代码”的方式进行开发吗?
) u+ z9 X: L) A8 i0 z' k. a- @                                                                        ) D# S+ c8 X) {, I7 [/ ~
                                                                               
  ~1 r4 ~& K% H4 |( ?& D1 r

1mr0ta5vqoy640363225.jpg

1mr0ta5vqoy640363225.jpg
3 G2 ~& U+ j# m1 R# L$ @
                                                                                很大的一个坑,稍有不注意,又是一次硬件研发失误!$ E7 _& F0 l1 I% ]
                                                               
- C! ]( ]( F0 T9 y* I+ p                                                                        . d/ u5 `( C  R/ O2 s
                                                                                ! R0 e! `1 q( g

hd0j0sb3ptu640363325.jpg

hd0j0sb3ptu640363325.jpg

9 W$ P- V! T! q/ U  g                                                                                嵌入式 C 语言函数的返回值,也有其应用“潜规则”?
  e+ G  L- D  y" Q                                                                                # \1 F- s4 ], h$ @5 G) V
我是老温,一名热爱学习的嵌入式工程师
8 t+ r2 _; b: t4 x! N关注我,一起变得更加优秀!
( v' q4 j- A3 T  {8 ^5 E4 z

dzlsazv2icz640363425.jpg

dzlsazv2icz640363425.jpg
回复

使用道具 举报

发表回复

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

本版积分规则


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