编码 · 编程语言
C 语言入门教程(零基础)
简短回答
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 格式 |
|---|---|---|---|
char | 1 字节 | char c = 'A'; | %c |
int | 4 字节 | int n = 42; | %d |
long long | 8 字节 | long long big = 1LL << 40; | %lld |
double | 8 字节 | double x = 3.14; | %f |
bool | 1 字节 | bool ok = true; (C99 <stdbool.h>) | %d |
size_t | 64 位下为 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,让行为保持一致。