C语言程序设计 - 作业与答案详解
本指南将按照C语言学习的典型顺序,从基础到进阶,分为多个章节,每个章节包含核心知识点回顾、典型作业题和详细答案解析。

第一章:C语言概述与入门
核心知识点回顾
- 程序结构:
#include(头文件包含)、main()函数(程序入口)、注释( 单行, 多行)。 - 基本输入输出:
printf()(格式化输出)、scanf()(格式化输入)。 - 数据类型:
int(整型)、float(单精度浮点型)、double(双精度浮点型)、char(字符型)。 - 变量与常量:变量的定义、初始化和赋值,常量(
const)。 - 基本运算符:算术运算符(, , , , )、赋值运算符()。
作业题
1:Hello World变体** 编写一个C程序,要求:
- 打印出自己的姓名和学号。
- 从键盘输入两个整数,计算它们的和并打印出来。
参考答案与解析
#include <stdio.h> // 1. 包含标准输入输出库,这是使用printf和scanf的前提
int main() {
// 2. 打印个人信息
// \n 是换行符,使下一次输出在新的一行开始
printf("姓名: 张三\n");
printf("学号: 20250001\n");
// 3. 定义变量
// int a, b; // 定义两个整型变量 a 和 b
// int sum; // 定义一个整型变量 sum 用来存储和
// 4. 从键盘输入
// scanf("%d %d", &a, &b);
// %d 表示要读取一个整数
// &a 是取a的地址,scanf需要知道把读到的数据存放到内存的哪个位置
// 5. 计算并输出
// sum = a + b;
// printf("%d + %d = %d\n", a, b, sum);
// --- 完整代码 ---
int a, b, sum;
printf("请输入两个整数,用空格隔开: ");
scanf("%d %d", &a, &b);
sum = a + b;
printf("它们的和是: %d\n", sum);
return 0; // 6. 程序正常结束,返回0
}
解题思路与易错点
- 思路:严格按照“包含头文件 -> 定义变量 -> 输入数据 -> 处理数据 -> 输出结果”的步骤来写程序。
- 易错点:
- 忘记
#include <stdio.h>:编译器会不认识printf和scanf,报错 "implicit declaration of function 'printf'"。 scanf中忘记取地址符&:例如写成scanf("%d %d", a, b);,这是最常见的初学者错误,会导致程序运行时崩溃或数据读入错误。- 变量未定义就使用:直接使用
sum = a + b;而没有先定义int sum;。 - 输出格式错误:
printf("和是: %d", sum);漏掉了换行符\n,导致下一次输出会紧跟在后面。
- 忘记
第二章:选择结构
核心知识点回顾
- 关系运算符:
>(大于)、<(小于)、>=(大于等于)、<=(小于等于)、(等于)、(不等于)。 - 逻辑运算符:
&&(逻辑与)、(逻辑或)、(逻辑非)。 if语句:if、if-else、if-else if-else。switch语句:用于多分支选择,注意case后的值必须是常量或常量表达式,以及break的使用。
作业题
2:判断成绩等级** 编写一个程序,要求用户输入一个0-100的整数成绩,然后输出对应的等级:

- 90-100: A
- 80-89: B
- 70-79: C
- 60-69: D
- 0-59: E
- 其他: 输入错误
参考答案与解析 (使用 if-else if)
#include <stdio.h>
int main() {
int score;
printf("请输入你的成绩 (0-100): ");
scanf("%d", &score);
// 使用 if-else if 结构处理多个区间
if (score >= 90 && score <= 100) {
printf("等级: A\n");
} else if (score >= 80 && score < 90) { // 注意这里的边界条件
printf("等级: B\n");
} else if (score >= 70 && score < 80) {
printf("等级: C\n");
} else if (score >= 60 && score < 70) {
printf("等级: D\n");
} else if (score >= 0 && score < 60) {
printf("等级: E\n");
} else {
// 处理所有不符合0-100范围的情况
printf("输入错误!成绩应在0到100之间,\n");
}
return 0;
}
解题思路与易错点
- 思路:使用
if-else if-else链条来依次判断分数所在的区间,判断的顺序很重要,通常从大到小或从小到大。 - 易错点:
- 逻辑运算符混淆:将
&&(与) 写成 (或)。if (score >= 90 || score <= 100)这个条件永远为真,因为所有成绩都小于等于100。 - 边界条件错误:
if (score >= 90)没有判断score <= 100,如果用户输入了150,程序也会判定为A级。 - 与 混淆:在
if条件中,if (score = 90)是赋值操作,会把90赋给score,并且表达式的值是90(非零),永远为真,应该使用if (score == 90)进行比较。 - 缺少
else处理非法输入:如果用户输入了负数或大于100的数,程序没有给出任何提示,这是不健壮的。
- 逻辑运算符混淆:将
第三章:循环结构
核心知识点回顾
for循环:适用于循环次数已知的情况,结构:for (初始化; 条件判断; 更新)。while循环:适用于循环次数未知,但循环条件明确的情况,结构:while (条件)。do-while循环:至少执行一次循环体,然后判断条件,结构:do { ... } while (条件);。break与continue:break跳出整个循环,continue跳过本次循环剩余语句,进入下一次循环。
作业题
3:求1到100的和**
使用 for 循环和 while 循环两种方式,计算1到100所有整数的和。
参考答案与解析
#include <stdio.h>
int main() {
int sum_for = 0;
int sum_while = 0;
int i;
// --- 方法一:使用 for 循环 ---
// 初始化 i=1, 循环条件 i<=100, 每次循环后 i++
for (i = 1; i <= 100; i++) {
sum_for += i; // 等价于 sum_for = sum_for + i;
}
printf("使用 for 循环计算 1 到 100 的和是: %d\n", sum_for);
// --- 方法二:使用 while 循环 ---
i = 1; // 初始化循环变量
while (i <= 100) {
sum_while += i;
i++; // 别忘了更新循环变量,否则会造成死循环!
}
printf("使用 while 循环计算 1 到 100 的和是: %d\n", sum_while);
return 0;
}
4:判断素数** 编写一个程序,输入一个正整数,判断它是否为素数(质数),素数是指只能被1和它本身整除的大于1的自然数。
参考答案与解析
#include <stdio.h>
#include <math.h> // 为了使用 sqrt() 函数
int main() {
int num, i;
int is_prime = 1; // 假设这个数是素数,用1(true)表示,0(false)表示
printf("请输入一个正整数: ");
scanf("%d", &num);
if (num <= 1) { // 1及以下的数不是素数
is_prime = 0;
} else {
// 优化:只需判断到 sqrt(num) 即可
for (i = 2; i <= sqrt(num); i++) {
if (num % i == 0) { // 如果能被i整除
is_prime = 0; // 则它不是素数
break; // 找到一个因子就可以提前结束了
}
}
}
if (is_prime) {
printf("%d 是一个素数,\n", num);
} else {
printf("%d 不是一个素数,\n", num);
}
return 0;
}
解题思路与易错点
- 思路:
- 素数定义:核心是“不能被2到num-1之间的任何数整除”。
- 算法优化:一个数
num如果不是素数,那么它必然有一个因子小于或等于sqrt(num),循环只需从2到sqrt(num)即可,大大减少了循环次数。 - 标志位法:使用一个变量(如
is_prime)作为标志,先假设是素数,一旦发现能被整除,就修改标志位并退出循环。
- 易错点:
- 循环范围错误:
for (i = 2; i < num; i++)虽然正确,但效率低。for (i = 2; i <= num / 2; i++)是一个不错的优化。for (i = 2; i <= sqrt(num); i++)是更优的写法。 - 边界值处理:忘记处理
num <= 1的情况,1不是素数。 - 忘记
break:如果不使用break,即使找到了因子,循环也会继续执行到sqrt(num),浪费了计算资源。 sqrt(num)的类型:sqrt()返回double类型,i是int,比较时会自动转换,但最好写成i <= (int)sqrt(num)以保证逻辑清晰。
- 循环范围错误:
第四章:数组
核心知识点回顾
- 数组定义:
数据类型 数组名[数组大小];。int scores[10];。 - 数组初始化:
int a[5] = {1, 2, 3, 4, 5};,部分初始化:int a[5] = {1, 2};剩余元素自动为0。 - 数组访问:通过下标(索引)访问,从0开始。
a[0]是第一个元素。 - 字符串:C语言中字符串是以
'\0'(空字符) 结尾的字符数组。char str[] = "hello";。
作业题
5:数组元素排序 编写一个程序,定义一个包含10个整数的数组,从键盘输入这10个数字,然后使用冒泡排序**将它们按从小到大的顺序排序,并输出排序后的结果。
参考答案与解析
#include <stdio.h>
#define N 10 // 定义一个常量N,方便修改数组大小
int main() {
int arr[N];
int i, j, temp;
// 1. 输入数组元素
printf("请输入 %d 个整数:\n", N);
for (i = 0; i < N; i++) {
scanf("%d", &arr[i]);
}
// 2. 冒泡排序
// 外层循环控制排序的趟数
for (i = 0; i < N - 1; i++) {
// 内层循环负责每趟中的比较和交换
// -i 是因为每排序一趟,最大的元素就会冒泡到最后,无需再比较
for (j = 0; j < N - 1 - i; j++) {
// 如果前一个数比后一个数大,则交换
if (arr[j] > arr[j + 1]) {
// 交换两个变量的值需要借助一个临时变量 temp
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
// 3. 输出排序后的数组
printf("排序后的数组是:\n");
for (i = 0; i < N; i++) {
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
解题思路与易错点
- 思路:
- 冒泡排序原理:重复地遍历要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来,遍历数列的工作是重复地进行,直到没有再需要交换的元素为止。
- 双层循环:外层循环控制排序的轮数(最多
N-1轮),内层循环负责在每一轮中进行相邻元素的比较和交换。
- 易错点:
- 数组下标越界:循环写成
for(i=0; i<=N; i++),会导致访问arr[N],这是数组之外的内存,会引发程序崩溃或不可预测的错误。 - 交换逻辑错误:
temp = arr[j]; arr[j] = arr[j+1]; arr[j+1] = temp;这三句的顺序不能错,如果写成arr[j] = arr[j+1]; arr[j+1] = arr[j];,两个值都会变成arr[j+1]的值,导致数据丢失。 - 内层循环范围错误:内层循环写成
for(j=0; j<N-1; j++)虽然能实现排序,但效率不高,优化后的for(j=0; j<N-1-i; j++)能减少不必要的比较。
- 数组下标越界:循环写成
第五章:函数
核心知识点回顾
- 函数定义:
返回类型 函数名(参数列表) { 函数体 }。 - 函数声明:
返回类型 函数名(参数列表);,通常放在文件开头。 - 函数调用:
函数名(实际参数);。 - 参数传递:C语言只有值传递(pass-by-value),实参的值被复制给形参,在函数内部修改形参不会影响实参。
- 递归:函数直接或间接地调用自身,必须有递归出口(base case)。
作业题
6:使用函数实现阶乘**
编写一个函数 long factorial(int n),用于计算一个非负整数 n 的阶乘(n!),然后在 main 函数中调用该函数,并打印结果。
参考答案与解析
#include <stdio.h>
// 函数声明
long factorial(int n);
int main() {
int num;
long result;
printf("请输入一个非负整数: ");
scanf("%d", &num);
// 调用函数
result = factorial(num);
printf("%d! = %ld\n", num, result);
return 0;
}
// 函数定义:计算阶乘
long factorial(int n) {
if (n < 0) {
return -1; // 用-1表示错误输入
}
if (n == 0 || n == 1) { // 递归出口/基本情况
return 1;
} else { // 递归步骤
return n * factorial(n - 1);
}
}
7:使用函数实现数组元素求和**
编写一个函数 int sum_array(int arr[], int size),用于计算一个整数数组的所有元素之和,并返回和,然后在 main 函数中调用该函数。
参考答案与解析
#include <stdio.h>
// 函数声明
int sum_array(int arr[], int size);
int main() {
int my_array[] = {10, 20, 30, 40, 50};
// sizeof(my_array) 得到整个数组占用的字节数
// sizeof(my_array[0]) 得到单个元素占用的字节数
// 两者相除即可得到数组元素个数
int array_size = sizeof(my_array) / sizeof(my_array[0]);
int total_sum;
total_sum = sum_array(my_array, array_size);
printf("数组的元素之和是: %d\n", total_sum);
return 0;
}
// 函数定义:数组求和
// int arr[] 实际上会被编译器处理为 int *arr,即一个指向整型的指针
int sum_array(int arr[], int size) {
int sum = 0;
int i;
for (i = 0; i < size; i++) {
sum += arr[i];
}
return sum;
}
解题思路与易错点
- 思路:
- 模块化设计:将一个复杂问题(如阶乘、数组求和)分解成一个独立的、可重用的功能模块(函数)。
- 参数传递数组:当函数需要接收一个数组时,形参通常写成
int arr[]或int *arr,编译器会将其视为指针,必须额外传递一个参数size来告知函数数组的大小,否则函数不知道何时停止遍历。
- 递归思想:阶乘是递归的经典例子。
n! = n * (n-1)!,必须定义一个不需要再递归就能直接返回结果的“基本情况”(0! = 1)。
- 易错点:
- 忘记函数声明:
main函数在sum_array函数之后定义,且没有在main前声明sum_array,编译器可能会报错。 - 递归无出口:写阶乘函数时,如果没有
if (n == 0) return 1;这句,factorial(n)会不断调用factorial(n-1),直到栈溢出(Stack Overflow)。 - 数组大小未知:在
sum_array函数内部,如果只写int sum_array(int arr[]),函数内部无法通过sizeof(arr)得到数组大小,sizeof(arr)会返回指针的大小(通常是4或8字节),而不是数组的大小。
- 忘记函数声明:
第六章:指针
核心知识点回顾
- 指针定义:
数据类型 *指针名;。int *p;,p是一个指向整型数据的指针。 - 取地址与解引用:
&是取地址运算符, 是解引用(或称间接寻址)运算符。 - 指针与数组:数组名
arr通常会“退化”为其首元素的地址,即&arr[0]。p = arr;是合法的,*(p+i)等价于arr[i]。 - 指针作为函数参数:通过指针传递,可以实现函数修改外部变量的值(模拟引用传递)。
作业题
8:使用指针交换两个变量的值*
编写一个函数 `void swap(int a, int *b),该函数使用指针来交换两个整数的值,在main` 函数中调用它来验证。
参考答案与解析
#include <stdio.h>
// 函数声明
void swap(int *a, int *b);
int main() {
int x = 10, y = 20;
printf("交换前: x = %d, y = %d\n", x, y);
// 调用函数,传递变量的地址
swap(&x, &y);
printf("交换后: x = %d, y = %d\n", x, y);
return 0;
}
// 函数定义:使用指针交换值
void swap(int *a, int *b) {
int temp;
// *a 解引用指针a,得到它指向的变量的值
temp = *a;
*a = *b;
*b = temp;
}
解题思路与易错点
- 思路:
- 为什么需要指针:C语言是值传递。
swap函数写成void swap(int a, int b),main中的x和y的值不会被改变,函数内部交换的是a和b(它们是x和y的副本)。 - 如何实现:要修改外部变量,必须传递它的地址,在函数内部,通过解引用操作符 来访问和修改这个地址上存储的值。
- 为什么需要指针:C语言是值传递。
- 易错点:
- 混淆指针和指针指向的值:在
swap函数内部,操作的是*a和*b,而不是a和b。a和b本身是地址,交换地址没有意义。 - 忘记解引用:写成
temp = a; a = b; b = temp;这只是交换了指针a和b本身,main函数中的x和y毫无变化。 - 函数返回值错误:交换两个变量的值不需要返回值,所以函数返回类型应为
void。
- 混淆指针和指针指向的值:在
第七章:结构体与文件操作
核心知识点回顾
- 结构体:
struct,用于将不同类型的数据组合成一个整体。struct Student { int id; char name[20]; };。 - 结构体变量定义与访问:
struct Student s1;,访问成员用 运算符:s1.id。 - 文件操作:
fopen(): 打开文件,返回FILE*指针。fprintf(),fscanf(): 格式化读写文件。fputc(),fgetc(): 字符读写。fputs(),fgets(): 字符串读写。fclose(): 关闭文件。
作业题
9:学生信息管理(结构体与文件)**
定义一个学生结构体,包含学号(int)、姓名(char[20])和成绩(float),编写程序实现:
- 从键盘输入3个学生的信息,并将它们保存到文件
students.txt中。 - 从文件
students.txt中读取所有学生信息,并打印到屏幕上。
参考答案与解析
#include <stdio.h>
#include <string.h>
// 1. 定义学生结构体
struct Student {
int id;
char name[20];
float score;
};
int main() {
struct Student stu[3];
int i;
// --- 第一部分:写入文件 ---
printf("请输入3个学生的信息(学号 姓名 成绩):\n");
FILE *fp_write = fopen("students.txt", "w"); // "w" 表示以写入模式打开文件,如果文件不存在则创建,存在则清空
if (fp_write == NULL) {
printf("无法打开文件 students.txt 进行写入!\n");
return 1;
}
for (i = 0; i < 3; i++) {
scanf("%d %s %f", &stu[i].id, stu[i].name, &stu[i].score);
// 将学生信息格式化写入文件
fprintf(fp_write, "%d %s %.2f\n", stu[i].id, stu[i].name, stu[i].score);
}
fclose(fp_write); // 写完后关闭文件
printf("学生信息已成功写入 students.txt\n");
// --- 第二部分:读取文件 ---
printf("\n--- 从文件读取的学生信息 ---\n");
FILE *fp_read = fopen("students.txt", "r"); // "r" 表示以只读模式打开文件
if (fp_read == NULL) {
printf("无法打开文件 students.txt 进行读取!\n");
return 1;
}
struct Student read_stu;
// 循环读取文件,直到文件末尾
while (fscanf(fp_read, "%d %s %f", &read_stu.id, read_stu.name, &read_stu.score) != EOF) {
// fscanf 返回成功读取的项数,读到文件末尾时返回 EOF
printf("学号: %d, 姓名: %s, 成绩: %.2f\n", read_stu.id, read_stu.name, read_stu.score);
}
fclose(fp_read); // 读完后关闭文件
return 0;
}
解题思路与易错点
- 思路:
- 数据建模:使用
struct将一个学生的相关信息(学号、姓名、成绩)封装成一个数据类型,使程序逻辑更清晰。 - 文件I/O流程:
- 写入:
fopen("w")-> 循环scanf获取数据 -> 循环fprintf写入文件 ->fclose。 - 读取:
fopen("r")-> 循环fscanf读取数据 -> 处理数据 ->fclose。
- 写入:
- 错误处理:每次
fopen后都应该检查返回值是否为NULL,以判断文件是否成功打开,这是健壮编程的重要习惯。
- 数据建模:使用
- 易错点:
- 文件打开模式错误:用
"w"模式去读取一个不存在的文件会失败,用"r"模式去写入一个文件也会失败。 - 忘记关闭文件:程序结束后,操作系统会回收资源,但养成良好的
fclose习惯可以确保数据立即从缓冲区写入磁盘,并释放文件句柄资源。 fscanf循环条件错误:读取文件时,应该使用while (fscanf(...) != EOF)来判断是否到达文件末尾,而不是for (i=0; i<3; i++),因为文件中的数据量可能和预期不符。- 结构体成员访问错误:访问结构体成员用 运算符,如
stu.id,如果是指向结构体的指针,才用->运算符。
- 文件打开模式错误:用
总结与建议
- 动手实践:看懂答案和亲手写出代码是两码事,一定要自己动手敲代码、编译、运行、调试。
- 理解原理:不要死记硬背代码,对于每个知识点,多问自己几个“为什么”,为什么
scanf要&?为什么数组下标从0开始?为什么交换值要用指针? - 调试能力:学会使用编译器的错误提示信息,当程序运行结果不对时,学会使用
printf在关键位置打印变量值,或者使用专业的调试器单步跟踪,观察程序执行流程和变量变化。 - 代码规范:从开始就养成良好的编程习惯,比如有意义的变量名、适当的注释、合理的代码缩进。
希望这份详细的指南能对你的C语言学习有所帮助!祝你学习顺利!
