C Hello World Again

发布于 · 修订 · 约 4 分钟

本文是我给地空学院的同学做的 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. 数组和函数都不能被拷贝(不能整体赋值)。
  2. 因为第 1 点,数组和函数都不能做函数的返回值,但函数可以返回指向数组的指针或指向函数的指针。
  3. 数组和函数都可以写在函数的形参里,但因为第 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 学习经验#

  1. 多写一些简单的程序做实验,验证自己的想法,从实践中学习。
  2. 多写代码,多写代码,多写代码。
  3. 找一些好书看,不要看谭浩强之类的。
  4. 学会使用搜索引擎。
  5. 不要害怕写代码,其实真的不难。这属于技术活,写得越多越熟练。
  6. 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 和指针》。