选了《嵌入式程序设计》这门课,听了两节课,感觉还不错。课上,老师谈及了一些 C 语言的东西,感觉很(特)有(无)趣(语),尝试回忆并记录一下。
内存对齐
1 | struct { |
输出是?
先扯一点别的:sizeof
是一个关键字…很久很久以前,一直把它看作函数…它的返回值是 size_t
类型,这是啥类型?我也说不清,不是 C 的基本数据类型。支持 C99 标准的话,最好使用 %zu
来转义就对了,u
means __unsigned__。Stack Overflow 上的这问题挺好。
正确的答案是 9 或 10 或 12。Why?
先看看 9 怎么来的:
float
类型很简单,啥平台都是 4 Bytesint
是 2 或 4 Bytes。但只有 16 位的计算机是 2,32 / 64 位都是 4 Bytes。这里说的- 至于
char
,没见过不是 1 Byte 的 - 1 + 4 + 4 = 9
- 恭喜你,算出来了,数学真好;)
至于 10 与 12,不得不说起编译器优化。这个结构体里面的数据类型最大是 4 Bytes,所以会按照 4 字节进行内存对齐,这个术语叫做[data structure alignment](Data structure alignment);大多数 C 编译器默认会进行此优化的,因此最终的大小就是 4*3=12 Bytes。至于 10 Bytes,则是将内存按照 2 Byts 进行对齐(GCC 的编译参数增加 -fpack-struct=2
)。
多说几句:GCC 支持用 __attribute__
为变量、类型、函数、标签指定特殊属性。这些不是编程语言标准里的内容,而属于编译器对语言的扩展。比如 aligned
与 packed
为变量、类型、函数、标签指定特殊属性。这些不是编程语言标准里的内容,而属于编译器对语言的扩展。比如
aligned
属性最常用在变量声明上。它的作用是告诉 GCC,为变量分配内存时,要分配在对齐的内存地址上。比如:
1 | int x __attribute__ ((aligned (16))) = 0; |
告诉编译器把变量x分配在16字节对齐的内存地址上,而非默认的4字节对齐。
packed
属性的主要目的是让编译器更紧凑地使用内存。当它用于变量时,告诉编译器该变量应该有尽可能小的对齐,也就是 1 字节对齐。当它用于结构体时,相当于给该结构体的每个成员加上了 packed
属性,这时该结构体将占用尽可能少的内存。比如:
1 | struct { |
更多内容参考GCC中的aligned和packed属性12。
参数传递
1 | void get(char *s){ |
输出是?
哭丧脸,第一眼没看出来,纠结 strcpy
去了。后来确认: strcpy
除了复制字符串以外,还会复制字符串的结尾符 '\0'
,即 sizeof(str) = strlen(str) + 1
个字符。而 strncpy
,让它复制多少就复制多少,复制得比拥有的还多咋办? '\0'
填充呗。__切记,一定要为 '\0'
留下空间__。
还得说一下 NULL
这个东西。
1 | // libio.h |
这就是 C 语言中的 NULL
。这文章给了解释。简单地说,并非所有的语言实现用 0
代表 NULL
,标准 C 语言(C99 或 C11)中标注 NULL pointer的使用会导致未定义行为、段错误。
这是计算机语言中的参数传递问题。C 语言中的参数传递是按值传递,就是把参数复制一份传递再给调用的函数。所以,因为 get
参数接收的是字符指针的值(而不是地址),所以函数返回后,实参的值并不会改变。要想改变,要么使用全局变量,要么传递指针的指针(即 `void get(char *s[])),或者接收返回值。
汇编层面的解释:
1 | # test.c |
注: edi —— 存储传递给函数的第一个参数
the difference between char s[] and char *s in C
满脑门子黑线,这我真的是第一次听说…
1 | char *get1(void){ |
你说这俩有啥不同?
先说说数据段(Data segment, 也叫 Text segment)这个东西。
一般来说,一个计算机程序的内存使用情况如图所示:
- 栈从上往下增长,存储临时变量之类
- 堆从下往上增长,动态分配的空间来源于此
- bss 段是为初始化的变量占用的空间,比如
static int i
这种 - data 段是初始化变量占用的空间,比如
char string[] = "Hello World"
,其值"Hello World"
即存储在这里 - code 段,用于存储代码。摊手,没法解释,就是代码
简洁地说,get1()
正确,get2()
的行为是不可预料的。get1()
中的变量 s
的值是 bss 段中的某个地址;而 get2()
中的变量 s
指向堆栈中的某个地址,char s[] = "Hello World";
的行为就是:在堆栈中分配一个数组,然后把 "Hello World"
复制到这数组中,数组的起始地址赋给 s
。
参考What is the difference between char s[] and char *s in C?
宏函数
恩,这是留下的作业。
C 语言中宏的一个有趣的用法:宏可以称为函数,感觉就像内联函数(Inline function)差不多。比如:
1 | // 一个宏, 将 4 个 unsigned char 型变量合成一个 unsigned 型变量 |
有意思的是,宏函数也可以有返回值,先看这样一行代码:
1 | if((ch = getchar()) == 'c') |
经常可以见到类似的。C 语言中,每个表达式都是有值的,如上,不是么?
…半月前在 Linux Kernel 中看到过这样的用法,但我现在给忘了…循环与跳转不是表达式,所以那示例我不会改写…留作思考题,跑
03.05 添加
注: 考虑以下的代码:
1 | // 1 |
宏不使用小括号扩起来的话,可能会有隐患,比如上例——优先级的原因,外加 x / y
可能是表达式…
感谢 Silver Bullet 指出,ckj 的帮助~