C Hello World Again
本文是我给地空学院的同学做的 C 语言讲座的讲稿。
指针与数组#
a[i]、*(a + i) 和 i[a] 是一回事:下标运算 a[i] 本来就定义为 *(a + i),而加法可以交换。
int (*x)[10] 与 int *x[10] 的区别:前者是一个指针,指向长度为 10 的 int 数组;后者是一个长度为 10 的数组,每个元素都是指向 int 的指针。
指针与数组可以互换着用:
int a[10];
for (int i = 0; i < 10; ++i)
{
a[i] = 0;
}
for (int *p = a; p != a + 10; ++p)
{
printf("%d\n", *p);
}C 风格的字符串#
字符串的表示#
char s[100] = "Hello";char *s = "Hello";
// 这是一个特殊的行为:
// 注意 int *x = 2; 是完全错误的,但这个语句是对的。
// 编译器会开辟一块内存来存放 "Hello",并让 s 指向它。
// 但注意:这个 "Hello" 是不可修改的,下面的语句是未定义行为,
// 通常会因为写只读内存而崩溃:
s[1] = 'a';printf("%c\n", "abc"[2]); // 输出 c
printf("%s\n", "abc" + 1); // 输出 bc
// 这里的 "abc" 是数组 {'a', 'b', 'c', '\0'},在表达式里退化成指向首元素的指针char s[100] = {'a', 'b', 'c'}; // 剩下的元素都是 0,所以它也是以 '\0' 结尾的字符串char *s = (char *)malloc(sizeof(char) * 10);
*s++ = 'a';
*s++ = 'b';
*s++ = 'c';
*s = '\0'; // malloc 得到的内存没有初始化,要自己写上结尾的 '\0'
printf("%s", s - 2); // 输出 bc
printf("%s", s - 1); // 输出 c
free(s - 3); // 释放 malloc 申请的内存,必须传回 malloc 返回的那个地址字符串拷贝#
void strcpy1(char *a, const char *b) // 把 b 拷贝到 a
{
while (*b)
{
*a++ = *b++;
}
*a = '\0';
}
int main(void)
{
char a[100];
char b[100];
while (scanf("%99s", b) == 1)
{
strcpy1(a, b);
printf("%s\n", a);
}
}求值顺序#
printf("%c%c%c\n", getchar(), getchar(), getchar());
输入 abc,用 GCC 编译出来的程序(x86-64)输出 cba,因为 GCC 在这里是从右向左对参数求值的;而用 Clang 编译出来的程序输出 abc。C 标准没有规定函数参数的求值顺序,所以不要写依赖它的代码。
变量作用域#
{
int x = 2;
}
printf("%d", x); // 编译错误:x 只在上面的花括号里可见int x = 8;
{
int x = 3;
printf("%d", x); // 输出 3,里层的 x 遮住了外层的 x
}用宏实现 max 函数#
#define MAX(x, y) x > y ? x : y
考虑 MAX(1 != 2, 3),它展开为 1 != 2 > 3 ? 1 != 2 : 3。由于 != 的优先级低于 >,上式为 (1 != (2 > 3)) ? (1 != 2) : 3,即 (1 != 0) ? 1 : 3,结果为 1。而它本来应该是 MAX(1, 3) = 3。
因此给参数加上括号:
#define MAX(x, y) (x) > (y) ? (x) : (y)
但考虑 3 + MAX(1, 2),它展开为 3 + (1) > (2) ? (1) : (2),即 4 > 2 ? 1 : 2,结果为 1,而实际上应该是 5。
因此继续修改这个宏,给整体也加上括号:
#define MAX(x, y) ((x) > (y) ? (x) : (y))
但考虑 MAX(i++, j++),展开后为 ((i++) > (j++) ? (i++) : (j++)):比较时 i 和 j 各自增一次,被选中的那个还会再自增一次。例如 i = 1, j = 2 时,结果是 3 而不是 2,并且 j 变成了 4。
为此可以这么定义宏:
#define MAX(x, y) ({ \
__typeof__(x) _x = (x); \
__typeof__(y) _y = (y); \
_x > _y ? _x : _y; \
})
这个宏先把两个参数各求值一次,存进临时变量 _x 与 _y 再比较,从而使得 MAX(i++, j++) 符合要求。注意 ({ ... })(语句表达式)和 __typeof__ 都是 GCC 扩展,Clang 也支持,但不是标准 C。
函数与数组的相似点#
| 数组 | 函数 | |
|---|---|---|
| 声明 | int x[10] | int sum(int, int) |
| 类型 | int [10] | int (int, int) |
| 指向它的指针 | int (*x)[10] | int (*sum)(int, int) |
| 指针的类型 | int (*)[10] | int (*)(int, int) |
数组和函数的共同点:
- 数组和函数都不能被拷贝(不能整体赋值)。
- 因为第 1 点,数组和函数都不能做函数的返回值,但函数可以返回指向数组的指针或指向函数的指针。
- 数组和函数都可以写在函数的形参里,但因为第 1 点,编译器会对其做处理:如果形参类型是数组,实际的形参类型会转换成元素类型的指针,例如
void fun(int arr[5])等价于void fun(int *arr);如果形参类型是函数,实际的形参类型会转换成对应的函数指针类型,例如void fun(int test())等价于void fun(int (*test)())。
如何返回一个数组#
用结构体包装一下:
#include <stdio.h>
#define MAXNUM 1000
struct Array
{
int a[MAXNUM];
};
struct Array DoubleIt(struct Array x)
{
struct Array y;
for (int i = 0; i < MAXNUM; ++i)
{
y.a[i] = x.a[i] << 1; // 移位的优先级比加减还低,和别的运算混用时要加括号
}
return y;
}
int main(void)
{
struct Array a = {{1, 2, 3, 4, 5}};
struct Array b = DoubleIt(a);
for (int i = 0; i < 5; ++i)
{
printf("%d\n", b.a[i]);
}
}推荐阅读#
- 《C 专家编程》
- 《C 陷阱与缺陷》
- 《征服 C 指针》
- 《C 和指针》
以上几本在图书馆应该都能借到。特别推荐《征服 C 指针》,它是日本最受欢迎的 C 语言书籍之一,写得很好。
C 学习经验#
- 多写一些简单的程序做实验,验证自己的想法,从实践中学习。
- 多写代码,多写代码,多写代码。
- 找一些好书看,不要看谭浩强之类的。
- 学会使用搜索引擎。
- 不要害怕写代码,其实真的不难。这属于技术活,写得越多越熟练。
- C 是比较偏向底层的语言,如果有计算机硬件和底层相关的知识,会更好理解。
2026 年 10 月修订。 几处更正:
- 求值顺序:原文说 GCC 一定从右向左求值。实际上 C 标准没有规定函数参数的求值顺序;GCC 16 在 x86-64 上的确输出
cba,但 Clang 23 输出abc。 MAX(i++, j++):原文说i和j都会自增两次,实际上只有被选中的那个会自增两次。语句表达式版本改用__typeof__,不再只能比较int,并注明它是 GCC 扩展。strcpy1:原来的版本没有写结尾的'\0',而是去清空目标缓冲区里残留的字符,对没有初始化的数组会越界读写;改成了直接写'\0'。scanf加了长度限制和返回值检查。malloc的例子补上了结尾的'\0',否则printf("%s")会读到未初始化的内存。- 修改字符串字面量是未定义行为,不保证报错;
void fun(int arr*)改为void fun(int *arr);移位运算符优先级的注释改得更准确。 - 推荐书目合并成一个列表,删去了书名无法确认的《明解 C 指针》,《C 与指针》更正为中译本书名《C 和指针》。