引言
C语言作为计算机科学与技术专业的基础编程语言,其重要性不言而喻。胡晓静教授编写的《C语言程序设计实验指导书》是许多高校计算机专业学生的重要学习资料。本文将针对该指导书中的典型课后习题进行详细解析,并分享实用的编程实战技巧,帮助读者更好地掌握C语言编程。
第一部分:基础语法习题解析
1.1 数据类型与变量定义
典型习题:编写程序,定义不同数据类型的变量并输出其占用的内存空间。
解题思路: 本题考察对C语言基本数据类型的理解,以及sizeof运算符的使用。
代码实现:
#include <stdio.h>
int main() {
// 定义各种基本数据类型的变量
char c = 'A';
int i = 100;
float f = 3.14;
double d = 2.71828;
short s = 32767;
long l = 1000000;
// 使用sizeof运算符输出各数据类型占用的内存空间
printf("char类型占用空间: %zu字节\n", sizeof(c));
printf("int类型占用空间: %zu字节\n", sizeof(i));
printf("float类型占用空间: %zu字节\n", sizeof(f));
printf("double类型占用空间: %zu字节\n", sizeof(d));
printf("short类型占用空间: %zu字节\n", sizeof(s));
printf("long类型占用空间: %zu字节\n", sizeof(l));
return 0;
}
代码解析:
sizeof是C语言中的单目运算符,用于计算数据类型或变量所占用的内存字节数%zu是printf函数中用于输出size_t类型(sizeof运算符的返回类型)的格式说明符- 不同系统环境下,某些数据类型(如
long)的大小可能不同,但char总是1字节
1.2 运算符与表达式
典型习题:编写程序,模拟简单的计算器功能,实现加、减、乘、除四种运算。
解题思路: 本题考察运算符的使用和基本的输入输出处理,需要注意除法运算的精度问题和除零错误处理。
代码实现:
#include <stdio.h>
int main() {
double num1, num2, result;
char operator;
printf("请输入表达式(如:3+5):");
scanf("%lf %c %lf", &num1, &operator, &num2);
switch(operator) {
case '+':
result = num1 + num2;
printf("%.2lf + %.2lf = %.2lf\n", num1, num2, result);
break;
case '-':
result = num1 - num2;
printf("%.2lf - %.2lf = %.2lf\n", num1, num2, result);
break;
case '*':
result = num1 * num2;
printf("%.2lf * %.2lf = %.2lf\n", num1, num2, result);
break;
case '/':
if(num2 != 0) {
result = num1 / num2;
printf("%.2lf / %.2lf = %.2lf\n", num1, num2, result);
} else {
printf("错误:除数不能为零!\n");
}
break;
default:
printf("错误:无效的运算符!\n");
}
return 0;
}
代码解析:
- 使用
scanf读取用户输入时,%lf用于读取double类型数据,%c用于读取字符 switch语句根据运算符执行不同的计算逻辑- 除法运算前必须检查除数是否为零,避免运行时错误
- 使用
%.2lf控制输出精度,保留两位小数
第二部分:流程控制习题解析
2.1 循环结构应用
典型习题:编写程序,计算1到100之间所有能被3整除但不能被5整除的数的和。
解题思路: 本题考察循环结构和条件判断的综合应用,需要遍历1-100的数字,使用取模运算符进行条件筛选。
代码实现:
#include <stdio.h>
int main() {
int sum = 0;
// 方法1:使用for循环
for(int i = 1; i <= 100; i++) {
if(i % 3 == 0 && i % 5 != 0) {
sum += i;
}
}
printf("方法1结果:%d\n", sum);
// 方法2:使用while循环
int j = 1;
sum = 0; // 重置sum
while(j <= 100) {
if(j % 3 == 0 && j % 5 != 0) {
sum += j;
}
j++;
}
printf("方法2结果:%d\n", 100);
return 0;
}
代码解析:
i % 3 == 0判断数字是否能被3整除i % 5 != 0判断数字是否不能被5整除- 两个条件用逻辑与
&&连接,表示必须同时满足 sum += i等价于sum = sum + i,用于累加符合条件的数字
2.2 嵌套循环与图形输出
*典型习题:编写程序,使用星号()输出一个直角三角形图案。
解题思路: 本题考察嵌套循环的使用,外层循环控制行数,内层循环控制每行输出的星号数量。
代码实现:
#include <stdio.h>
int main() {
int rows;
printf("请输入三角形的行数:");
scanf("%d", &rows);
// 输出直角三角形
for(int i = 1; i <= rows; i++) {
for(int j = 1;1 j <= i; j++) {
printf("*");
}
printf("\n"); // 每行结束换行
}
return 0;
}
代码解析:
- 外层循环
i从1到rows,表示当前是第几行 - 内层循环
j从1到i,表示当前行需要输出的星号数量 - 每行输出完成后必须换行,否则所有星号会输出在同一行
- 嵌套循环是图形输出编程的核心技巧
第三部分:数组与字符串习题解析
3.1 一维数组应用
典型习题:编写程序,找出一个整型数组中的最大值和最小值。
解题思路: 本题考察数组遍历和极值查找算法,可以使用遍历比较法或先排序再取首尾元素的方法。
代码实现:
#include <stdio.h>
#include <limits.h> // 包含INT_MAX和INT_MIN的定义
int main() {
int arr[] = {23, 45, 12, 67, 34, 89, 5, 78};
int n = sizeof(arr) / sizeof(arr[0]);
// 方法1:遍历比较法
int max = arr[0];
int min = arr[0];
for(int i = 1; i < n; i++) {
if(arr[i] > max) {
max = arr[i];
}
if(arr[i] < min) {
min = arr[i];
}
}
printf("数组元素:");
for(int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
printf("\n最大值:%d\n", max);
printf("最小值:%d\n", min);
// 方法2:使用标准库函数(需要排序)
// 这里仅作演示,实际应用中方法1更高效
// qsort(arr, n, sizeof(int), compare);
return 0;
}
代码解析:
sizeof(arr) / sizeof(arr[0])是计算数组元素个数的常用技巧- 初始化
max和min为数组第一个元素,避免使用极值导致的逻辑错误 - 遍历数组时从第二个元素开始(索引为1)
- 每次比较更新最大值和最小值
3.2 字符串处理
典型习题:编写程序,实现字符串的逆序输出。
解题思路: 本题考察字符串处理和数组操作,可以使用双指针法或逐个字符交换的方法。
代码实现:
#include <stdio.h>
#include <string.h>
int main() {
char str[100];
int len, i;
printf("请输入一个字符串:");
gets(str); // 注意:gets函数不安全,实际应用中应使用fgets
len = strlen(str);
// 方法1:双指针交换法
for(i = 0; i < len / 2; i++) {
char temp = str[i];
str[i] = str[len - 1 - i];
str[len - 1 - i] = temp;
}
printf("逆序结果:%s\n", str);
// 方法2:使用标准库函数(需要额外空间)
// char reversed[100];
// for(i = 0; i < len; i++) {
// reversed[i] = str[len - 1 - i];
// }
// reversed[len] = '\0';
// printf("逆序结果:%s\n", reversed);
return 0;
}
代码只需:
strlen函数用于获取字符串长度(不包括结尾的’\0’)- 双指针法通过交换首尾对称位置的字符实现逆序
- 循环次数为
len / 2,因为只需交换一半元素 - 注意:实际开发中应避免使用
gets,改用fgets(str, sizeof(str), stdin)
第四部分:函数与指针习题解析
4.1 函数定义与调用
典型习题:编写函数,计算一个整数的阶乘,并用主函数调用验证。
解题思路: 本题考察函数的定义、声明和调用,以及递归算法的应用。
代码实现:
#include <stdio.h>
// 函数声明
long factorial(int n);
int main() {
int num;
printf("请输入一个正整数:");
scanf("%d", &num);
if(num < 0) {
printf("错误:阶乘只能计算非负整数!\n");
} else {
long result = factorial(num);
printf("%d! = %ld\n", num, result);
}
return 0;
}
// 递归实现阶乘函数
long factorial(int n) {
if(n == 0 || n == 1) {
return 1;
} else {
return n * factorial(n - 1);
}
}
代码解析:
- 函数声明放在
main函数之前,告诉编译器函数的存在 - 递归基例:当n为0或1时,直接返回1
- 递归关系:n! = n * (n-1)!
- 使用
long类型防止阶乘结果溢出(但仍有上限) - 递归算法简洁但效率较低,对于大数计算应使用迭代方法
4.2 指针与数组
典型习题:编写函数,使用指针实现数组元素的逆序。
解题思路: 本题考察指针运算和数组操作,通过指针算术实现数组元素的访问和交换。
代码实现:
#include <stdio.h>
// 使用指针逆序数组
void reverse_array(int *arr, int len) {
int *start = arr;
int *end = arr + len - 1;
int temp;
while(start < end) {
temp = *start;
*start = *end;
*end = temp;
start++;
end--;
}
}
int main() {
int arr[] = {1, 2, 3, 4, 5, 6, 7, 8};
int n = sizeof(arr) / sizeof(arr[0]);
printf("原始数组:");
for(int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
printf("\n");
reverse_array(arr, n);
printf("逆序数组:");
for(int i = 0;1 i < n; i++) {
printf("%d ", arr[i]);
}
printf("\n");
return 0;
代码解析:
int *arr表示接收数组的首地址arr + len - 1指向数组最后一个元素*start和*end是解引用操作,获取指针指向的值start++和end--是指针算术运算,分别向后和向前移动指针- 当
start >= end时,说明所有元素已交换完成
第五部分:结构体与文件操作习题解析
5.1 结构体应用
典型习题:定义一个学生结构体,包含学号、姓名、成绩,编写程序实现学生成绩的录入和查询。
解题思路: 本题考察结构体的定义、数组使用和基本的数据管理操作。
代码实现:
#include <stdio.h>
#include <string.h>
#define MAX_STUDENTS 50
#define NAME_LEN 20
// 定义学生结构体
struct Student {
int id;
char name[NAME_LEN];
float score;
};
int main() {
struct Student students[MAX_STUDENTS];
int count = 0;
int choice;
while(1) {
printf("\n=== 学生成绩管理系统 ===\n");
printf("1. 录入学生信息\n");
printf("2. 查询学生成绩\n");
printf("3. 显示所有学生\n");
printf("0. 退出\n");
printf("请选择:");
scanf("%d", &choice);
switch(choice) {
case 1: // 录入
if(count >= MAX_STUDENTS) {
printf("错误:学生数量已达上限!\n");
break;
}
printf("请输入学号、姓名、成绩(空格分隔):");
scanf("%d %s %f", &students[count].id, students[count].name, &students[count].score);
count++;
printf("录入成功!\n");
break;
case 2: // 查询
int search_id;
printf("请输入要查询的学号:");
scanf("%d", &search_id);
int found = 0;
for(int i = 0; i < count; i++) {
if(students[i].id == search_id) {
printf("学号:%d,姓名:%s,成绩:%.1f\n",
students[i].id, students[i].name, students[i].score);
found = 1;
break;
}
}
if(!found) {
printf("未找到该学号的学生!\n");
}
break;
case 3: // 显示所有
if(count == 0) {
printf("暂无学生信息!\n");
} else {
printf("学号\t姓名\t成绩\n");
for(int i = 0; i < count; i++) {
printf("%d\t%s\t%.1f\n", students[i].id, students[i].name, students[i].score);
}
}
break;
case 0:
printf("感谢使用,再见!\n");
return 0;
default:
printf("无效选择,请重新输入!\n");
}
}
return 0;
}
代码解析:
struct Student定义了包含三个成员的结构体students[MAX_STUDENTS]是结构体数组,用于存储多个学生信息students[count].id使用点运算符访问结构体成员- 程序使用菜单驱动方式,通过循环实现简单的交互式操作
- 查询功能通过遍历结构体数组实现线性查找
5.2 文件操作
典型习题:编写程序,将学生信息写入文件,并从文件中读取显示。
解题思路: 本题考察文件读写操作,需要掌握fopen、fprintf、fscanf、fclose等文件操作函数。
代码实现:
#include <stdio.h>
#include <stdlib.h>
struct Student {
int id;
char name[20];
float score;
};
void write_to_file() {
FILE *fp;
struct Student s;
fp = fopen("students.txt", "w");
if(fp == NULL) {
printf("错误:无法打开文件!\n");
return;
}
printf("请输入学生信息(学号 姓名 成绩),输入0结束:\n");
while(1) {
scanf("%d", &s.id);
if(s.id == 0) break;
scanf("%s %f", s.name, &s.score);
fprintf(fp, "%d %s %.1f\n", s.id, s.name, s.score);
}
fclose(fp);
printf("数据已保存到students.txt\n");
}
void read_from_file() {
FILE *fp;
struct Student s;
fp = fopen("students.txt", "r");
if(fp == NULL) {
printf("错误:文件不存在或无法打开!\n");
return;
}
printf("\n从文件读取的学生信息:\n");
printf("学号\t姓名\t成绩\n");
while(fscanf(fp, "%d %s %f", &s.id, s.name, &s.score) != EOF) {
printf("%d\t%s\t%.1f\n", s.id, s.name, s.score);
}
fclose(fp);
}
int main() {
int choice;
printf("1. 写入文件\n2. 读取文件\n请选择:");
scanf("%d", &choice);
if(choice == 1) {
write_to_file();
} else if(choice == 2) {
read_from_file();
} else {
printf("无效选择!\n");
}
return 0;
}
代码解析:
fopen("students.txt", "w")以写模式打开文件,若文件不存在则创建fprintf将格式化数据写入文件,格式与printf类似fscanf从文件读取格式化数据,返回成功读取的项目数EOF是文件结束标志,fscanf读到文件末尾时返回EOF- 必须调用
fclose关闭文件,释放系统资源
第六部分:编程实战技巧分享
6.1 代码规范与可读性
技巧1:有意义的命名
// 不好的命名
int a;
float b;
void func1(int x, int y);
// 好的命名
int student_count;
float average_score;
void calculate_grade(int score, int total);
技巧2:合理使用注释
/*
* 函数名:calculate_average
* 功能:计算数组元素的平均值
* 参数:arr - 数组指针,len - 数组长度
* 返回值:平均值,如果数组为空返回0
*/
float calculate_average(float *arr, int len) {
if(len <= 0) return 0.0;
float sum = 0;
for(int i = 0; i < len; i++) {
sum += arr[i];
}
return sum / len;
}
6.2 调试技巧
技巧3:使用调试宏
#ifdef DEBUG
#define DEBUG_PRINT(fmt, ...) \
fprintf(stderr, "[DEBUG] %s:%d: " fmt "\n", __FILE__, __LINE__, ##__VA_ARGS__)
#else
#define DEBUG_PRINT(fmt, ...)
#endif
// 使用示例
int main() {
int x = 10;
DEBUG_PRINT("x = %d", x);
return 0;
}
技巧4:防御性编程
// 检查指针是否为空
void safe_print(const char *str) {
if(str == NULL) {
printf("错误:空指针!\n");
return;
}
printf("%s\n", str);
}
// 检查数组越界
int get_element(int *arr, int len, int index) {
if(index < 0 || index >= len) {
printf("错误:数组索引越界!\n");
return -1;
}
return arr[index];
}
6.3 性能优化
技巧5:避免不必要的函数调用
// 不好的写法:循环内调用strlen
for(int i = 0; i < strlen(str); i++) {
// 处理字符串
}
// 好的写法:预先计算长度
int len = strlen(str);
for(int i = 0; i < len; i++) {
// 处理字符串
}
技巧6:使用位运算优化
// 判断奇偶性(位运算更快)
int is_even(int n) {
return (n & 1) == 0; // 与运算判断最后一位
}
// 交换两个变量(无需临时变量)
int a = 5, b = 10;
a ^= b;
b ^= a;
a ^= b;
6.4 常见错误避免
技巧7:初始化变量
// 错误:未初始化变量
int sum; // 值不确定
for(int i = 0; i < 10; i++) {
sum += i; // 结果不可预测
}
// 正确:初始化变量
int sum = 0;
for(int i = 0; i < 10; i++) {
sum += i;
}
技巧8:注意运算符优先级
// 错误:混淆优先级
if(a & 1 == 0) // 实际:a & (1 == 0),结果总是0
// 正确:使用括号
if((a & 1) == 0) // 判断a是否为偶数
第七部分:高级编程技巧
7.1 动态内存管理
典型应用:动态数组的实现
#include <stdio.h>
#include <stdlib.h>
// 动态数组结构体
typedef struct {
int *data;
int size;
int capacity;
} DynamicArray;
// 初始化动态数组
void init_dynamic_array(DynamicArray *da, int initial_capacity) {
da->data = (int*)malloc(initial_capacity * sizeof(int));
if(da->data == NULL) {
printf("内存分配失败!\n");
exit(1);
}
da->size = 0;
da->capacity = initial_capacity;
}
// 添加元素
void push_back(DynamicArray *da, int value) {
if(da->size >= da->capacity) {
// 扩容:容量翻倍
da->capacity *= 2;
da->data = (int*)realloc(da->data, da->capacity * sizeof(int));
if(da->data == NULL) {
printf("内存重新分配失败!\n");
exit(1);
}
}
da->data[da->size++] = value;
}
// 释放内存
void free_dynamic_array(DynamicArray *da) {
free(da->data);
da->data = NULL;
da->size = da->capacity = 0;
}
int main() {
DynamicArray da;
init_dynamic_array(&da, 5);
// 添加元素测试扩容
for(int i = 0; i < 10; i++) {
push_back(&da, i * 10);
}
printf("动态数组内容:");
for(int i = 0; i < da.size; i++) {
printf("%d ", da.data[i]);
}
printf("\n当前容量:%d,大小:%d\n", da.capacity, da.size);
free_dynamic_array(&da);
return 0;
}
代码解析:
malloc用于动态分配内存,realloc用于调整已分配内存大小- 当数组满时,容量翻倍(Amortized analysis)
- 必须检查内存分配是否成功(返回NULL表示失败)
- 使用完毕必须调用
free释放内存,避免内存泄漏
7.2 函数指针的应用
典型应用:实现回调函数和通用算法
#include <stdio.h>
#include <stdlib.h>
// 比较函数类型
typedef int (*compare_func)(const void*, const void*);
// 通用排序函数(模拟qsort)
void bubble_sort(void *base, size_t nmemb, size_t size, compare_func cmp) {
char *arr = (char*)base;
char temp[size];
for(size_t i = 0; i < nmemb - 1; i++) {
for(size_t j = 0; j < nmemb - 1 - i; j++) {
// 比较相邻元素
if(cmp(arr + j * size, arr + (j + 1) * size) > 0) {
// 交换元素
memcpy(temp, arr + j * size, size);
memcpy(arr + j * size, arr + (j + 1) * size, size);
memcpy(arr + (j + 1) * size, temp, size);
}
}
}
}
// 整数比较函数
int int_compare(const void *a, const void *b) {
return (*(int*)a - *(int*)b);
}
// 浮点数比较函数
int float_compare(const void *a, const void *b) {
float diff = (*(float*)a - *(float*)b);
return (diff > 0) - (diff < 0);
}
int main() {
// 测试整数排序
int int_arr[] = {64, 34, 25, 12, 22, 11, 90};
size_t n = sizeof(int_arr) / sizeof(int_arr[0]);
bubble_sort(int_arr, n, sizeof(int), int_compare);
printf("排序后整数数组:");
for(size_t i = 0; i < n; i++) {
printf("%d ", int_arr[i]);
}
printf("\n");
// 测试浮点数排序
float float_arr[] = {3.14, 2.71, 1.41, 1.73, 0.58};
size_t m = sizeof(float_arr) / sizeof(float_arr[0]);
bubble_sort(float_arr, m, sizeof(float), float_compare);
printf("排序后浮点数数组:");
for(size_t i = 0; i < m; i++) {
printf("%.2f ", float_arr[i]);
}
printf("\n");
return 0;
}
代码解析:
typedef定义函数指针类型,简化复杂声明void*和size_t使函数通用,可处理任意类型数据char*类型转换用于字节级内存操作memcpy用于安全地复制内存块- 函数指针允许在运行时决定调用哪个比较函数
第八部分:综合项目实战
8.1 项目:简易通讯录管理系统
项目需求: 实现一个基于文件的通讯录,支持联系人增删改查、分组管理、数据持久化等功能。
完整代码实现:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#define MAX_CONTACTS 100
#define NAME_LEN 50
#define PHONE_LEN 20
#define EMAIL_LEN 50
#define GROUP_LEN 20
#define FILENAME "address_book.dat"
// 联系人结构体
typedef struct {
int id;
char name[NAME_LEN];
char phone[PHONE_LEN];
char email[EMAIL_LEN];
char group[GROUP_LEN];
} Contact;
// 通讯录结构体
typedef struct {
Contact contacts[MAX_CONTACTS];
int count;
} AddressBook;
// 函数声明
void load_contacts(AddressBook *book);
void save_contacts(const AddressBook *book);
void add_contact(AddressBook *book);
void delete_contact(AddressBook *book);
void search_contact(const AddressBook *book);
void display_contacts(const AddressBook *book);
void display_by_group(const AddressBook *book);
void edit_contact(AddressBook *book);
int get_unique_id();
int main() {
AddressBook book;
book.count = 0;
load_contacts(&book);
int choice;
do {
printf("\n=== 通讯录管理系统 ===\n");
printf("1. 添加联系人\n");
printf("2. 删除联系人\n");
printf("3. 编辑联系人\n");
printf("4. 查找联系人\n");
printf("5. 显示所有联系人\n");
printf("6. 按分组显示\n");
printf("0. 退出并保存\n");
printf("请选择:");
if(scanf("%d", &choice) != 1) {
// 清除错误输入
while(getchar() != '\n');
choice = -1;
}
switch(choice) {
case 1: add_contact(&book); break;
case 2: delete_contact(&book); break;
case 3: edit_contact(&book); break;
case 4: search_contact(&book); break;
case 5: display_contacts(&book); break;
case 6: display_by_group(&book); break;
case 0: save_contacts(&book); break;
default: printf("无效选择!\n");
}
} while(choice != 0);
return 0;
}
// 从文件加载联系人
void load_contacts(AddressBook *book) {
FILE *fp = fopen(FILENAME, "rb");
if(fp == NULL) {
printf("未找到数据文件,将创建新通讯录。\n");
return;
}
book->count = fread(book->contacts, sizeof(Contact), MAX_CONTACTS, fp);
fclose(fp);
printf("已加载 %d 个联系人。\n", book->count);
}
// 保存联系人到文件
void save_contacts(const AddressBook *book) {
FILE *fp = fopen(FILENAME, "wb");
if(fp == NULL) {
printf("错误:无法保存数据!\n");
return;
}
fwrite(book->contacts, sizeof(Contact), book->count, fp);
fclose(fp);
printf("数据已保存,再见!\n");
}
// 生成唯一ID
int get_unique_id() {
static int id = 1000;
return ++id;
}
// 添加联系人
void add_contact(AddressBook *book) {
if(book->count >= MAX_CONTACTS) {
printf("错误:通讯录已满!\n");
return;
}
Contact *c = &book->contacts[book->count];
c->id = get_unique_id();
printf("请输入姓名:");
scanf("%s", c->name);
printf("请输入电话:");
scanf("%s", c->phone);
printf("请输入邮箱:");
scanf("%s", c->email);
printf("请输入分组:");
scanf("%s", c->group);
book->count++;
printf("添加成功!联系人ID:%d\n", c->id);
}
// 删除联系人
void delete_contact(AddressBook *book) {
int id;
printf("请输入要删除的联系人ID:");
scanf("%d", &id);
int index = -1;
for(int i = 0; i < book->count; i++) {
if(book->contacts[i].id == id) {
index = i;
break;
}
}
if(index == -1) {
printf("未找到ID为%d的联系人!\n", id);
return;
}
// 移动元素覆盖要删除的元素
for(int i = index; i < book->count - 1; i++) {
book->contacts[i] = book->contacts[i + 1];
}
book->count--;
printf("删除成功!\n");
}
// 查找联系人
void search_contact(const AddressBook *book) {
char keyword[NAME_LEN];
printf("请输入姓名关键词:");
scanf("%s", keyword);
int found = 0;
printf("\n搜索结果:\n");
printf("ID\t姓名\t电话\t邮箱\t分组\n");
for(int i = 0; i < book->count; i++) {
if(strstr(book->contacts[i].name, keyword) != NULL) {
printf("%d\t%s\t%s\t%s\t%s\n",
book->contacts[i].id,
book->contacts[i].name,
book->contacts[i].phone,
book->contacts[i].email,
book->contacts[i].group);
found = 1;
}
}
if(!found) {
printf("未找到匹配的联系人!\n");
}
}
// 显示所有联系人
void display_contacts(const AddressBook *book) {
if(book->count == 0) {
printf("通讯录为空!\n");
return;
}
printf("\n所有联系人(共%d个):\n", book->count);
printf("ID\t姓名\t电话\t邮箱\t分组\n");
for(int i = 0; i < book->count; i++) {
printf("%d\t%s\t%s\t%s\t%s\n",
book->contacts[i].id,
book->contacts[i].name,
book->contacts[i].phone,
book->contacts[i].email,
book->contacts[i].group);
}
}
// 按分组显示
void display_by_group(const AddressBook *book) {
if(book->count == 0) {
printf("通讯录为空!\n");
return;
}
char group[GROUP_LEN];
printf("请输入分组名称:");
scanf("%s", group);
int found = 0;
printf("\n分组 '%s' 的联系人:\n", group);
printf("ID\t姓名\t电话\t邮箱\n");
for(int i = 0; i < book->count; i++) {
if(strcmp(book->contacts[i].group, group) == 0) {
printf("%d\t%s\t%s\t%s\n",
book->contacts[i].id,
book->contacts[i].name,
book->contacts[i].phone,
book->contacts[i].email);
found = 1;
}
}
if(!found) {
printf("该分组下无联系人!\n");
}
}
// 编辑联系人
void edit_contact(AddressBook *book) {
int id;
printf("请输入要编辑的联系人ID:");
scanf("%d", &id);
int index = -1;
for(int i = 0; i < book->count; i++) {
if(book->contacts[i].id == id) {
index = i;
break;
}
}
if(index == -1) {
printf("未找到ID为%d的联系人!\n", id);
return;
}
Contact *c = &book->contacts[index];
printf("当前信息:\n");
printf("姓名:%s\n", c->name);
printf("电话:%s\n", c->phone);
printf("邮箱:%s\n", c->email);
printf("分组:%s\n", c->group);
printf("\n输入新信息(直接回车保持原值):\n");
char buffer[100];
printf("新姓名:");
getchar(); // 消耗之前的换行符
fgets(buffer, sizeof(buffer), stdin);
if(buffer[0] != '\n') {
buffer[strcspn(buffer, "\n")] = 0;
strcpy(c->name, buffer);
}
printf("新电话:");
fgets(buffer, sizeof(buffer), stdin);
if(buffer[0] != '\n') {
buffer[strcspn(buffer, "\n")] = 0;
strcpy(c->phone, buffer);
}
printf("新邮箱:");
fgets(buffer, sizeof(buffer), stdin);
if(buffer[0] != '\n') {
buffer[strcspn(buffer, "\n")] = 0;
strcpy(c->email, buffer);
}
printf("新分组:");
fgets(buffer, sizeof(buffer), stdin);
if(buffer[0] != '\n') {
buffer[strcspn(buffer, "\n")] = 0;
strcpy(c->group, buffer);
}
printf("编辑成功!\n");
}
项目解析:
- 数据结构:使用结构体组织联系人信息,结构体数组存储多个联系人
- 文件持久化:使用二进制文件读写(
fread/fwrite)实现数据持久化 - 内存管理:静态分配内存,避免动态内存复杂性
- 用户交互:菜单驱动界面,提供完整的CRUD操作
- 错误处理:检查文件打开失败、数组越界、输入错误等情况
- 唯一ID:使用静态变量生成唯一ID,确保联系人标识唯一性
第九部分:C语言编程最佳实践
9.1 代码风格指南
1. 缩进与空格
// 良好的格式
void example_function(int param1, int param2) {
if(param1 > 0 && param2 < 10) {
for(int i = 0; i < param1; i++) {
printf("Value: %d\n", i);
}
}
}
// 避免的格式
void example_function(int param1,int param2){
if(param1>0&¶m2<10){
for(int i=0;i<param1;i++){
printf("Value:%d\n",i);}}}
2. 命名约定
- 变量名:
lowerCamelCase或snake_case - 常量名:
UPPER_SNAKE_CASE - 函数名:
lowerCamelCase或snake_case - 类型名:
UpperCamelCase(结构体、枚举)
9.2 安全编程
1. 避免缓冲区溢出
// 危险:gets函数
char buf[10];
gets(buf); // 输入超过10字符会导致溢出
// 安全:fgets
char buf[10];
fgets(buf, sizeof(buf), stdin); // 自动截断,安全
// 更安全:scanf限制宽度
char buf[10];
scanf("%9s", buf); // 最多读取9字符+1个'\0'
2. 空指针防护
// 安全的内存分配检查
int *arr = malloc(size * sizeof(int));
if(arr == NULL) {
fprintf(stderr, "内存分配失败!\n");
exit(EXIT_FAILURE);
}
// 使用后释放并置空
free(arr);
arr = NULL; // 防止悬垂指针
9.3 可移植性考虑
1. 使用标准类型
// 不可移植:假设int为32位
int x = 0xFFFFFFFF;
// 可移植:使用标准类型
#include <stdint.h>
int32_t x = 0xFFFFFFFF;
uint8_t byte = 255;
2. 处理字节序
// 检测系统字节序
int is_big_endian(void) {
union {
uint32_t i;
uint8_t c[4];
} test = {0x01020304};
return test.c[0] == 0x01;
}
// 网络字节序转换
#include <arpa/inet.h>
uint32_t host_long = 0x12345678;
uint32_t network_long = htonl(host_long); // 主机到网络
uint32_t back_to_host = ntohl(network_long); // 网络到主机
9.4 性能优化技巧
1. 循环优化
// 优化前:每次循环计算长度
for(int i = 0; i < strlen(str); i++) {
// 处理
}
// 优化后:缓存长度
int len = strlen(str);
for(int i = 0; i < len; i++) {
// 处理
}
// 更进一步:指针遍历
char *p = str;
while(*p) {
// 处理 *p
p++;
}
2. 减少函数调用开销
// 内联函数(C99)
static inline int max(int a, int b) {
return a > b ? a : b;
}
// 宏定义(预处理时替换)
#define MAX(a, b) ((a) > (b) ? (a) : (b))
第十部分:常见面试题解析
10.1 经典面试题
问题1:什么是野指针?如何避免?
// 野指针示例
int *p; // 未初始化,指向随机地址
*p = 10; // 危险!可能导致程序崩溃
// 正确做法
int *p = NULL; // 初始化为NULL
p = malloc(sizeof(int));
if(p != NULL) {
*p = 10;
free(p);
p = NULL; // 释放后置空
}
问题2:static关键字的作用?
// 1. 限制作用域:文件内的全局变量/函数
static int file_var = 0; // 只在本文件可见
static void helper() { } // 只在本文件可调用
// 2. 保持生命周期:函数内的静态变量
void counter() {
static int count = 0; // 只初始化一次
count++;
printf("%d\n", count);
}
问题3:const关键字的作用?
// 1. 定义常量
const int MAX = 100;
// 2. 保护参数
void print(const char *str) { // 保证函数内不修改str
printf("%s\n", str);
}
// 3. 常量指针
const int *p1; // 指向常量的指针(值不能改)
int *const p2; // 常量指针(指向不能改)
const int *const p3; // 指向常量的常量指针
10.2 指针面试题
问题4:分析以下代码的输出
#include <stdio.h>
int main() {
int arr[] = {1, 2, 3, 4, 5};
int *p = arr;
printf("%d\n", *p++); // 输出1,p指向arr[1]
printf("%d\n", (*p)++); // 输出2,arr[1]变为3
printf("%d\n", *++p); // 输出4,p指向arr[3]
printf("%d\n", ++*p); // 输出5,arr[3]变为5
return 0;
}
解析: 考察运算符优先级和结合性,*和++的组合需要仔细分析。
问题5:实现strlen函数
// 方法1:循环计数
size_t my_strlen(const char *str) {
if(str == NULL) return 0;
size_t len = 0;
while(str[len] != '\0') {
len++;
}
return len;
}
// 方法2:指针运算
size_t my_strlen(const char *str) {
if(str == NULL) return 0;
const char *p = str;
while(*p) p++;
return p - str;
}
// 方法3:递归(不推荐,仅作演示)
size_t my_strlen(const char *str) {
return *str ? 1 + my_strlen(str + 1) : 0;
}
结语
通过本文的详细解析和实战技巧分享,相信读者对C语言程序设计有了更深入的理解。从基础语法到高级特性,从简单习题到综合项目,我们系统地梳理了C语言的核心知识点。
学习建议:
- 动手实践:光看不练假把式,所有代码都要亲自敲一遍
- 调试习惯:学会使用gdb调试器,理解程序运行过程
- 阅读源码:阅读优秀的开源C项目代码
- 持续学习:C语言博大精深,需要长期积累
推荐学习资源:
- 《C程序设计语言》(K&R)
- 《C陷阱与缺陷》
- 《C专家编程》
- GCC编译器文档
- Linux系统编程手册
希望本文能帮助你在C语言学习道路上更进一步!如有疑问,欢迎交流讨论。
