C 语言练习题(15 题)
学了语法得动手写才算真的会。这一页给 15 道精心挑选的练习题,按难度分三级——基础 6 题(变量 / 运算 / 控制流 / 函数)、进阶 5 题(指针 / 动态内存 / 结构体)、实战 4 题(综合应用)。每题都含题目描述、提示、可折叠的参考答案,答案都是完整的、本地 gcc 一编就能跑的程序。
使用建议:先别急着展开答案。读题后自己用本地编辑器(或 在线 C 编译器)试着写,卡住了再看提示,实在写不出再对照参考答案。C 语言对内存和指针的要求最严苛——能不参考地写对,才算真的入门。
基础(6 题):变量 / 运算 / 控制流 / 函数
这一组覆盖最基础的语言元素:循环、数组遍历、函数定义。目标是训练"把思路翻译成 printf 和 for"的基本功。
第 1 题:翻转字符串
题目:写一个函数 void reverse(char *s),原地翻转字符数组(不要调用 strrev——Linux/glibc 标准库没有这个函数)。比如 "hello" → "olleh"。
提示:双指针——头 i、尾 n-1-i,向中间逼近并交换;循环条件 i < n/2。注意实参必须是可写的字符数组(不能是字符串字面量)。
参考答案
#include <stdio.h>
#include <string.h>
// 原地翻转:双指针 i(头)与 n-1-i(尾)向中间逼近并交换
void reverse(char *s) {
int n = strlen(s);
for (int i = 0; i < n / 2; i++) {
char t = s[i];
s[i] = s[n - 1 - i];
s[n - 1 - i] = t;
}
}
int main(void) {
char s[] = "hello"; // 注意:必须是可写的数组,不能写成 char *s = "hello"
reverse(s);
printf("%s\n", s); // 输出: olleh
return 0;
}
// 提示:C 标准库没有 strrev(Linux/glibc 不提供);两头向中间交换最省事。第 2 题:判断素数
题目:写一个函数 int is_prime(int n),判断正整数 n 是否为素数。返回 1 表示是素数,0 表示不是。
提示:小于 2 的都不是素数;只需检查从 2 到 sqrt(n) 的整除性——超过平方根的因子必然配对一个更小的、已经检查过的因子。用 #include <math.h> 引入 sqrt。
参考答案
#include <stdio.h>
#include <math.h>
// 只需检查到 sqrt(n):超过的因子必然配对一个更小的、已检查过的因子
int is_prime(int n) {
if (n < 2) return 0;
for (int i = 2; i <= (int)sqrt((double)n); i++) {
if (n % i == 0) return 0;
}
return 1;
}
int main(void) {
printf("%d\n", is_prime(7)); // 1
printf("%d\n", is_prime(10)); // 0
printf("%d\n", is_prime(1)); // 0
printf("%d\n", is_prime(2)); // 1
return 0;
}
// 编译: gcc ex2.c -lm (-lm 链接数学库)第 3 题:FizzBuzz
题目:用 printf 打印 1 到 15。3 的倍数打印 Fizz,5 的倍数打印 Buzz,同时是 3 和 5 的倍数打印 FizzBuzz,其余打印数字本身。
提示:if / else if / else 的判断顺序很关键——必须先判"同时是 3 和 5 的倍数"(即 15 的倍数),否则会被前面的分支截胡。
参考答案
#include <stdio.h>
int main(void) {
for (int i = 1; i <= 15; i++) {
if (i % 15 == 0) // 必须先判 15,否则会被 3 或 5 截胡
printf("FizzBuzz\n");
else if (i % 3 == 0)
printf("Fizz\n");
else if (i % 5 == 0)
printf("Buzz\n");
else
printf("%d\n", i);
}
return 0;
}
// 输出:
// 1
// 2
// Fizz
// 4
// Buzz
// Fizz
// 7
// 8
// Fizz
// Buzz
// 11
// Fizz
// 13
// 14
// FizzBuzz第 4 题:求数组最大值
题目:写一个函数 int max_of(int arr[], int n),返回长度为 n 的整型数组的最大值。
提示:C 数组不保存长度,n 必须作为参数单独传入;用 sizeof(a) / sizeof(a[0]) 在定义处计算元素个数。这是"打擂台"算法——变量初始化为第一个元素,遇到更大的就更新。
参考答案
#include <stdio.h>
// C 数组不保存长度,n 必须作为参数单独传入
int max_of(int arr[], int n) {
int m = arr[0];
for (int i = 1; i < n; i++) {
if (arr[i] > m) m = arr[i];
}
return m;
}
int main(void) {
int a[] = {3, 1, 4, 1, 5, 9, 2, 6};
int n = sizeof(a) / sizeof(a[0]); // 数组元素个数 = 总字节 / 单元素字节
printf("%d\n", max_of(a, n)); // 输出: 9
return 0;
}第 5 题:数组求和
题目:写一个函数 int sum_of(int arr[], int n),返回数组所有元素之和。
提示:累加变量初始化为 0,for 循环遍历一遍加上即可。注意整型溢出——元素很大时考虑改用 long long 累加。
参考答案
#include <stdio.h>
int sum_of(int arr[], int n) {
int total = 0;
for (int i = 0; i < n; i++) total += arr[i];
return total;
}
int main(void) {
int a[] = {1, 2, 3, 4, 5};
int n = sizeof(a) / sizeof(a[0]);
printf("%d\n", sum_of(a, n)); // 输出: 15
return 0;
}第 6 题:计算阶乘
题目:写函数 long factorial(int n),返回 n!。要求 factorial(0) == 1。同时用循环和递归两种写法实现。
提示:循环版用一个累乘变量,从 2 乘到 n;递归版更贴近数学定义 n! = n * (n-1)!。注意 int 装不下 13!,所以返回类型用 long。
参考答案
#include <stdio.h>
// 方法 1:循环(推荐,无栈开销)
long factorial(int n) {
long r = 1;
for (int i = 2; i <= n; i++) r *= i;
return r;
}
// 方法 2:递归(更贴近数学定义,但 n 太大会栈溢出)
long factorial_rec(int n) {
if (n <= 1) return 1;
return n * factorial_rec(n - 1);
}
int main(void) {
printf("%ld\n", factorial(5)); // 120
printf("%ld\n", factorial(0)); // 1
printf("%ld\n", factorial_rec(6)); // 720
return 0;
}进阶(5 题):指针 / 动态内存 / 结构体
这一组是 C 语言的核心难点——指针。从最基础的"用指针交换两个数"出发,逐步过渡到指针遍历、动态内存、字符串指针、结构体排序。每道题都要亲手敲一遍,看懂不等于会写。
第 7 题:用指针交换两个整数
题目:写函数 void swap(int *a, int *b),通过指针交换两个整数的值。要求调用方 swap(&x, &y) 后 x 和 y 真的变了。
提示:C 函数默认传值,直接传 int 改的只是副本,外部看不到效果;必须传地址(指针),函数内用 *a 解引用修改原变量。
参考答案
#include <stdio.h>
// C 函数默认传值:不传指针的话改的只是副本,外部看不到
void swap(int *a, int *b) {
int t = *a; // *a:解引用,取指针指向的值
*a = *b;
*b = t;
}
int main(void) {
int x = 3, y = 8;
swap(&x, &y); // &x:取 x 的地址
printf("x=%d, y=%d\n", x, y); // 输出: x=8, y=3
return 0;
}第 8 题:用指针遍历数组求和
题目:写函数 int sum_ptr(const int *p, int n),用指针当游标遍历数组求和(不要用下标 p[i])。
提示:*p 取当前元素,p++ 移动到下一个 int(int 占 4 字节时指针自动加 4,这就是"指针运算");const 表示函数不会修改数组内容。
参考答案
#include <stdio.h>
// 把指针当游标:*p 取当前元素,p++ 移动到下一个 int
int sum_ptr(const int *p, int n) {
int total = 0;
for (int i = 0; i < n; i++) {
total += *p;
p++;
}
return total;
}
int main(void) {
int a[] = {10, 20, 30, 40};
printf("%d\n", sum_ptr(a, 4)); // 输出: 100
return 0;
}
// 提示:数组名 a 在表达式里会退化为首元素指针,所以 sum_ptr(a, 4) 合法。第 9 题:动态内存(malloc + free)
题目:用 malloc 申请一个长度为 n 的动态整型数组,填入平方值并打印,最后 free 释放。
提示:#include <stdlib.h>;malloc(n * sizeof(int)) 返回 void *,需强转成 int *;务必检查返回值是否为 NULL(内存可能申请失败);用完 free 防止内存泄漏。
参考答案
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int n = 5;
// malloc 申请 n 个 int 的连续字节,返回 void*,强转成 int*
int *arr = (int *)malloc(n * sizeof(int));
if (arr == NULL) { // 务必检查:内存可能申请失败
printf("内存不足\n");
return 1;
}
for (int i = 0; i < n; i++) arr[i] = i * i;
for (int i = 0; i < n; i++) printf("%d ", arr[i]);
printf("\n");
free(arr); // 用完必须 free,否则内存泄漏
arr = NULL; // 好习惯:释放后置空,防止后续误用
return 0;
}
// 输出: 0 1 4 9 16第 10 题:手写 my_strlen
题目:不调用 string.h 的 strlen,自己写 size_t my_strlen(const char *s),返回字符串长度(不计结束符 \0)。
提示:C 字符串以 '\0' 隐式结尾,但 strlen 不把它算进长度;用 while (*s != '\0') 边走边数。const char * 表示只读指针。
参考答案
#include <stdio.h>
// const char *:只读指针,函数内不修改字符串
size_t my_strlen(const char *s) {
size_t n = 0;
while (*s != '\0') { // 一直走到字符串结束符 '\0'
n++;
s++;
}
return n;
}
int main(void) {
printf("%zu\n", my_strlen("hello")); // 5
printf("%zu\n", my_strlen("")); // 0
return 0;
}
// %zu 是 size_t 的正确格式符;\0 是 C 字符串的隐式结束符。第 11 题:结构体数组按分数排序
题目:定义结构体 Student(含 name 和 score),用 qsort 把一个学生数组按分数降序排列。
提示:qsort(数组, 元素数, 单元素字节数, 比较函数);比较函数签名固定为 int(const void *, const void *),返回负数表示 a 排在 b 前(降序就让 sb->score - sa->score)。
参考答案
#include <stdio.h>
#include <stdlib.h>
typedef struct {
char name[16];
int score;
} Student;
// qsort 的比较函数:返回负数表示 a 排在 b 前
int cmp(const void *a, const void *b) {
const Student *sa = (const Student *)a;
const Student *sb = (const Student *)b;
return sb->score - sa->score; // 降序:后者减前者
}
int main(void) {
Student arr[] = {
{"小明", 78}, {"小红", 95}, {"小刚", 62}, {"小丽", 88}
};
int n = sizeof(arr) / sizeof(arr[0]);
qsort(arr, n, sizeof(Student), cmp);
for (int i = 0; i < n; i++)
printf("%s: %d\n", arr[i].name, arr[i].score);
return 0;
}
// 输出:
// 小红: 95
// 小丽: 88
// 小明: 78
// 小刚: 62实战(4 题):综合应用
这一组每题都会综合用到多个知识点——结构体、链表、文件 I/O、函数指针——做出一个接近真实场景的小程序。建议每题都完整跑一遍,再想想怎么扩展。
第 12 题:学生成绩管理
题目:定义 Student 结构体(姓名、数学、语文、均分),实现"录入 → 计算均分 → 按均分降序排序 → 打印"全流程。
提示:结构体字段用 .name 直接访问,通过指针用 ->name;C 中结构体可以直接赋值(编译器自动 memcpy),所以冒泡排序里 Student t = a[j] 是合法的。
参考答案
#include <stdio.h>
typedef struct {
char name[16];
int math, chinese;
double avg;
} Student;
void fill_avg(Student *s) {
s->avg = (s->math + s->chinese) / 2.0; // -> 通过指针访问字段
}
// 冒泡排序,按均分降序
void sort_by_avg(Student a[], int n) {
for (int i = 0; i < n - 1; i++)
for (int j = 0; j < n - 1 - i; j++)
if (a[j].avg < a[j + 1].avg) {
Student t = a[j]; // 结构体可直接赋值(C 自动 memcpy)
a[j] = a[j + 1];
a[j + 1] = t;
}
}
int main(void) {
Student cls[] = {
{"小明", 90, 80, 0},
{"小红", 95, 88, 0},
{"小刚", 70, 100, 0}
};
int n = 3;
for (int i = 0; i < n; i++) fill_avg(&cls[i]);
sort_by_avg(cls, n);
for (int i = 0; i < n; i++)
printf("%d. %s 均分 %.1f\n", i + 1, cls[i].name, cls[i].avg);
return 0;
}
// 输出:
// 1. 小红 均分 91.5
// 2. 小明 均分 85.0
// 3. 小刚 均分 85.0第 13 题:单链表(建表 + 打印 + 反转)
题目:用 struct Node 实现一个单链表,支持头插法建表、顺序打印、链表反转。
提示:节点用 malloc 分配;头插法是新节点 next 指向当前头,新节点变头;反转用三指针 prev / cur / next——先存 next = cur->next,再让 cur->next = prev,最后 prev 和 cur 都前进一步。
参考答案
#include <stdio.h>
#include <stdlib.h>
typedef struct Node {
int val;
struct Node *next;
} Node;
// 头插法:新节点插到链表头部
Node *push_front(Node *head, int v) {
Node *node = (Node *)malloc(sizeof(Node));
node->val = v;
node->next = head;
return node;
}
void print_list(Node *head) {
for (Node *p = head; p; p = p->next)
printf("%d -> ", p->val);
printf("NULL\n");
}
// 三指针反转:prev / cur / next
Node *reverse(Node *head) {
Node *prev = NULL, *cur = head;
while (cur) {
Node *next = cur->next; // 先存下下一个
cur->next = prev; // 当前指向前一个
prev = cur; // prev 前进
cur = next; // cur 前进
}
return prev; // 新头是 prev
}
int main(void) {
Node *head = NULL;
int a[] = {1, 2, 3, 4};
for (int i = 0; i < 4; i++) head = push_front(head, a[i]);
print_list(head); // 4 -> 3 -> 2 -> 1 -> NULL
head = reverse(head);
print_list(head); // 1 -> 2 -> 3 -> 4 -> NULL
return 0;
}第 14 题:文件读写 + 统计行数
题目:先写一个 3 行文本到 demo.txt,再读回来统计文件行数。
提示:fopen("path", "w") 写,"r" 读;fgetc(in) 每次读一个字符,返回 EOF 表示文件结束;遇到 '\n' 计数器加 1,并要处理"末行无换行符"的边界情况。
参考答案
#include <stdio.h>
int main(void) {
// 1. 先写一个测试文件
FILE *out = fopen("demo.txt", "w");
if (!out) { perror("fopen write"); return 1; }
fprintf(out, "第一行\n第二行\n第三行\n");
fclose(out);
// 2. 再读回来统计行数
FILE *in = fopen("demo.txt", "r");
if (!in) { perror("fopen read"); return 1; }
int lines = 0, ch, last = '\n';
while ((ch = fgetc(in)) != EOF) {
if (ch == '\n') lines++;
last = ch;
}
if (last != '\n') lines++; // 最后一行无换行符也要算一行
fclose(in);
printf("行数: %d\n", lines); // 输出: 行数: 3
return 0;
}
// 提示:fgetc 每次读一个字符;EOF 是文件结束标志;perror 打印系统错误信息。第 15 题:简易计算器(函数指针数组分派)
题目:实现一个支持 + - * / 的简易计算器,用函数指针数组分派四种运算。
提示:先定义 4 个签名相同的函数 int(int, int);再声明数组 int (*ops[4])(int, int) = {add, sub, mul, dvd};通过下标 ops[i](a, b) 调用。switch 也能做,但加新运算时数组更易扩展。
参考答案
#include <stdio.h>
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int mul(int a, int b) { return a * b; }
int dvd(int a, int b) { return b == 0 ? 0 : a / b; } // 防除零
int main(void) {
// 函数指针数组:4 个元素都是 int(*)(int,int),按符号索引调用
int (*ops[4])(int, int) = { add, sub, mul, dvd };
char syms[] = "+-*/";
int a = 10, b = 3;
for (int i = 0; i < 4; i++)
printf("%d %c %d = %d\n", a, syms[i], b, ops[i](a, b));
return 0;
}
// 输出:
// 10 + 3 = 13
// 10 - 3 = 7
// 10 * 3 = 30
// 10 / 3 = 3
// 提示:函数指针数组的核心是"统一函数签名";switch 也能做,但加新运算时数组更易扩展。练习建议
不要只看答案。把每道题在本地或在线环境亲手敲一遍,改一改输入试试边界情况(空数组、负数、超长字符串、NULL 指针)。C 语言尤其要练"段错误"调试——一旦指针或下标越界,程序就崩,排错能力是 C 程序员的硬通货。
试着扩展:做完第 11 题结构体排序,能不能改成按姓名升序?做完第 13 题单链表,能不能加上"指定位置插入"和"删除节点"?做完第 15 题计算器,能不能加上幂运算和括号?每个扩展都是一次小型的真实开发。
更多练习资源:推荐 LeetCode 上用 C 刷题(虽然比 C++ 慢,但训练指针最有效);遇到不懂的先查 cppreference 的 C 文档(最权威,有中文版)。
← 上一篇 C 实战项目
返回 C 教程目录