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 教程目录

✈️💬