cv.cm

编码 · 编程语言

C 语言入门教程(零基础)

阅读约 9 分钟

简短回答

C 是一门小巧、快速的编译型语言,通过指针直接访问内存。在 .c 文件中编写 main,用 gcc hello.c -o hello 编译,再运行 ./hello。核心概念包括:固定大小的类型、函数、数组和以空字符结尾的字符串、指针与地址、用 malloc 和 free 手动管理内存,以及通过返回值处理错误。

打开二进制转换

C 语言是什么,用来做什么

C 由 Dennis Ritchie 于 20 世纪 70 年代初在贝尔实验室创建,用来编写 Unix 操作系统。它由 ISO 标准化(主要版本有 C99、C11、C17 和 C23)。C 编译为机器码,运行时开销极小,几乎可以在任何地方运行。它被用于操作系统内核(Linux)、嵌入式固件、数据库(SQLite)、语言运行时(CPython 就是用 C 写的)以及高性能库。学习 C 还能让你明白内存和计算机究竟是如何工作的。

安装编译器并运行 Hello, world

在 Linux 上安装 gcc(例如 sudo apt install build-essential);在 macOS 上运行 xcode-select --install 获得 clang(也可通过 gcc 命令调用);在 Windows 上使用 MSYS2、WSL 或 Visual Studio。

#include <stdio.h>
int main(void) {
    printf("Hello, world\n");
    return 0;
}
gcc -Wall -Wextra -std=c17 hello.c -o hello
./hello
# Hello, world

#include 会引入头文件中的声明,这里引入的是标准输入输出库。main 返回 0 表示成功。编译时务必开启警告(-Wall -Wextra)。

变量与基本类型

每个变量都有一个类型,它决定了变量的大小和含义。除非标准另有规定,大小取决于平台;<stdint.h> 提供了 int32_t 这样宽度确定的类型。

类型典型大小示例printf 格式
char1 字节char c = 'A';%c
int4 字节int n = 42;%d
long long8 字节long long big = 1LL << 40;%lld
double8 字节double x = 3.14;%f
bool1 字节bool ok = true; (C99 <stdbool.h>)%d
size_t64 位下为 8 字节size_t len = sizeof(arr);%zu
#include <stdio.h>
int main(void) {
    int count = 7;
    double avg = count / 2;     /* 3.0: integer division first */
    double avg2 = count / 2.0;  /* 3.5 */
    printf("%d %.1f %.1f\n", count, avg, avg2);  /* 7 3.0 3.5 */
    printf("%zu\n", sizeof(int));                /* 4 on most systems */
    return 0;
}

流程控制与函数

函数必须先声明再使用。参数按值传递:函数收到的是一份副本。

#include <stdio.h>
int square(int x) {
    return x * x;
}
int main(void) {
    int score = 72;
    if (score >= 90) {
        printf("A\n");
    } else if (score >= 70) {
        printf("B\n");              /* B */
    } else {
        printf("C\n");
    }
    for (int i = 0; i < 3; i++) {
        printf("%d ", square(i));   /* 0 1 4 */
    }
    printf("\n");
    int n = 10;
    while (n > 1) n /= 2;
    printf("%d\n", n);              /* 1 */
    return 0;
}

C 还有 do/while、带 case 标签的 switch(别忘了 break),以及主要用于错误路径中清理资源的 goto。

数组与字符串

数组是一段连续存放的固定数量元素。C 不检查边界,也不保存长度,所以需要单独传入长度。字符串是以零字节 '\0' 结尾的 char 数组。

#include <stdio.h>
#include <string.h>
int sum(const int *values, size_t n) {
    int total = 0;
    for (size_t i = 0; i < n; i++) total += values[i];
    return total;
}
int main(void) {
    int nums[] = {3, 1, 2, 5};
    size_t n = sizeof nums / sizeof nums[0];
    printf("%zu items, sum %d\n", n, sum(nums, n));  /* 4 items, sum 11 */
    char name[16] = "Ada";
    strncat(name, " Lovelace", sizeof name - strlen(name) - 1);
    printf("%s (%zu chars)\n", name, strlen(name));  /* Ada Lovelace (12 chars) */
    return 0;
}

数组传给函数时会退化为指向首元素的指针,这就是 sum 接收 const int * 和一个长度的原因。

标志性特性:指针

指针保存另一个值的内存地址。&x 得到 x 的地址,*p 读取或写入地址 p 处的值。指针让函数能够修改调用者的变量、无需复制就能共享大块数据,并能构建链式数据结构。

#include <stdio.h>
void swap(int *a, int *b) {
    int tmp = *a;
    *a = *b;
    *b = tmp;
}
int main(void) {
    int x = 1, y = 2;
    int *p = &x;
    *p = 10;                      /* changes x */
    swap(&x, &y);
    printf("x=%d y=%d\n", x, y);  /* x=2 y=10 */
    int arr[3] = {7, 8, 9};
    int *q = arr;
    printf("%d %d\n", *(q + 1), q[2]);  /* 8 9: pointer arithmetic */
    return 0;
}

用 malloc 和 free 管理动态内存

局部变量存放在栈上,函数返回后就消失。对于大小在运行时才能确定、或需要比函数存活更久的数据,请用 malloc 在堆上分配,并用 free 释放。每一次成功的 malloc 都要对应恰好一次 free。

#include <stdio.h>
#include <stdlib.h>
int main(void) {
    size_t n = 5;
    int *squares = malloc(n * sizeof *squares);
    if (squares == NULL) {
        fprintf(stderr, "out of memory\n");
        return 1;
    }
    for (size_t i = 0; i < n; i++) squares[i] = (int)(i * i);
    printf("%d\n", squares[4]);  /* 16 */
    free(squares);
    squares = NULL;              /* avoid using a dangling pointer */
    return 0;
}

相关函数有 calloc(分配并清零)和 realloc(调整大小)。可以用 struct 把相关的值组合在一起,例如 struct point { int x; int y; };。

错误处理

C 没有异常。函数通过返回值报告错误(通常是 -1、NULL 或状态码),许多库函数还会设置全局变量 errno。每个可能失败的返回值都要检查。

#include <errno.h>
#include <stdio.h>
#include <string.h>
int main(void) {
    FILE *f = fopen("missing.txt", "r");
    if (f == NULL) {
        fprintf(stderr, "cannot open: %s\n", strerror(errno));
        return 1;   /* cannot open: No such file or directory */
    }
    fclose(f);
    return 0;
}

一个完整的小程序:行数与单词计数器

这个程序类似一个小型 wc:读取命令行指定的文件,并输出行数、单词数和字节数。

#include <ctype.h>
#include <stdio.h>
int main(int argc, char *argv[]) {
    if (argc != 2) {
        fprintf(stderr, "usage: %s FILE\n", argv[0]);
        return 1;
    }
    FILE *f = fopen(argv[1], "r");
    if (f == NULL) {
        perror(argv[1]);
        return 1;
    }
    long lines = 0, words = 0, bytes = 0;
    int c, in_word = 0;
    while ((c = fgetc(f)) != EOF) {
        bytes++;
        if (c == '\n') lines++;
        if (isspace(c)) {
            in_word = 0;
        } else if (!in_word) {
            in_word = 1;
            words++;
        }
    }
    fclose(f);
    printf("%ld lines, %ld words, %ld bytes\n", lines, words, bytes);
    return 0;
}

用 gcc -Wall -Wextra wc.c -o wc 编译,再运行 ./wc notes.txt。注意 c 是 int 而不是 char,这样它才能容纳特殊值 EOF。

新手常见错误

  • 在条件中用 = 代替 ==。警告选项(-Wall)能发现大多数此类情况。
  • 给字符串缓冲区定大小时,忘了给结尾的 '\0' 留位置。
  • 使用 gets 或不限长度的 scanf("%s", buf)。请改用 fgets(buf, sizeof buf, stdin)。
  • 返回指向局部变量的指针,而该变量在函数返回后已不存在。
  • printf 的格式与类型不匹配,例如对 size_t 使用 %d。

接下来学什么

把 cppreference.com 的 C 部分当作语言和标准库的参考,想看标准本身可以阅读 ISO 工作组(WG14)的页面。多练习结构体、链表、头文件,以及用 make 构建的多文件程序。调试器(gdb 或 lldb)以及 Valgrind 或 AddressSanitizer 能帮你节省大量时间。之后可以探索 C++ 或 Rust,它们建立在相同的思想之上,并提供更多安全保障。

常见问题

现在还值得学 C 吗?

值得。它仍然用于操作系统、嵌入式设备和对性能要求苛刻的库,而且能让你理解内存、指针以及编译后代码的运行方式,对学习其他任何语言都有帮助。

C 和 C++ 有什么区别?

C++ 最初是 C 的扩展,增加了类、模板、引用、异常和庞大的标准库。C 则保持小巧,是过程式的。大多数 C 代码与合法的 C++ 很接近,但两者是各自独立的标准。

为什么我的 C 程序会因段错误(segmentation fault)崩溃?

它访问了不属于自己的内存,通常是通过空指针、未初始化的指针或已释放的指针,或者数组下标越界。用调试器或 AddressSanitizer 运行,就能找到具体出错的那一行。

应该使用哪个 C 标准?

C17 被当前的编译器广泛支持,C23 是最新的标准。请显式指定版本,例如 -std=c17,让行为保持一致。

全部编码教程