最近在整理哈工大SSE的C语言编程练习时,遇到几个很有意思的题目,涉及字符串处理、数学计算和基础算法。这些题目虽然基础,但包含了C语言编程中常见的坑点和技巧。今天就把我的解题过程和经验分享给大家,特别是对刚接触C语言的同学应该会很有帮助。
字符串逆序是C语言中经典的数组操作题。题目要求我们编写一个Inverse函数,接收两个字符数组参数,将第一个数组的内容逆序存入第二个数组。
关键点在于理解字符数组的索引关系。假设原始字符串"hello"长度为5,那么逆序后的字符串应该是"olleh"。通过观察可以发现,逆序后的第i个字符对应原始字符串的第(len-i-1)个字符。
c复制#include <stdio.h>
#include <string.h>
void Inverse(char str[], char ptr[]) {
int len = strlen(str);
for(int i = 0; i < len; i++) {
ptr[len-i-1] = str[i];
}
ptr[len] = '\0'; // 必须添加字符串结束符
}
int main() {
char str[2000], ptr[2000];
printf("Input a string:");
gets(str);
Inverse(str, ptr);
printf("Inversed results:%s\n", ptr);
return 0;
}
重要提示:使用gets()函数存在缓冲区溢出风险,在实际项目中建议使用fgets()替代。
忘记字符串结束符:这是最常见的错误。C语言字符串以'\0'结尾,逆序操作后必须手动添加,否则printf可能输出乱码。
数组越界访问:确保循环条件正确,特别是当i=0时,ptr[len-1]应该是第一个字符。
输入处理:gets()会读取整行输入,包括空格,但不会读取换行符。如果使用fgets(),需要注意它会保留换行符。
调试时可以添加临时打印语句:
c复制printf("Copying str[%d]='%c' to ptr[%d]\n", i, str[i], len-i-1);
C语言中可以使用%e格式说明符输出科学计数法形式的浮点数。例如:
c复制double num = 142000000;
printf("%e", num); // 输出1.420000e+08
也可以直接在代码中使用科学计数法表示:
c复制double num = 1.42e8; // 等同于142000000
经典的棋盘麦粒问题:第一个格子放1粒麦子,第二个放2粒,第三个放4粒,依此类推,问64个格子总共需要多少麦子。
c复制#include <stdio.h>
#include <math.h>
int main() {
double total = 0;
for(int i = 0; i < 64; i++) {
total += pow(2, i);
}
printf("Total grains needed: %e\n", total);
return 0;
}
计算结果约为1.84e19,这个数字非常大,因此必须使用double类型来存储。
处理大数计算时要注意:
数据类型选择:int类型通常只有32位,最大约2e9,不够存储结果。unsigned long long最大约1.8e19,勉强够用但建议直接用double。
精度损失:浮点数运算可能存在精度问题,对于精确计算应考虑使用大整数库。
输出格式:%e默认显示6位小数,可以使用%.2e控制小数位数。
C语言中字符数组如果不初始化,内容是不确定的。对于需要作为字符串使用的数组,必须确保以'\0'结尾。
两种正确的初始化方式:
c复制char result[2000] = ""; // 全部初始化为0
// 或者
result[0] = '\0'; // 仅设置第一个字符为结束符
使用strtol函数可以方便地将十六进制字符串转换为long型整数:
c复制#include <stdlib.h>
char hex[] = "1A3F";
double decimal = (double)strtol(hex, NULL, 16);
注意事项:
处理字符串拼接时,要跟踪当前写入位置:
c复制char result[2000] = "";
char *current = result;
// 拼接多个字符串
strcpy(current, "Hello");
current += strlen("Hello");
strcpy(current, " World");
这种方法比反复调用strcat效率更高,因为strcat每次都要从头开始查找字符串结尾。
折半查找(二分查找)要求数组必须是有序的。算法步骤:
c复制int binarySearch(int arr[], int size, int target) {
int left = 0;
int right = size - 1;
while(left <= right) {
int mid = left + (right - left)/2; // 防止溢出
if(arr[mid] == target) {
return mid;
} else if(arr[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return -1; // 未找到
}
常见的错误和修正方法:
循环条件:应该是while(left <= right)而不是while(left < right),否则可能漏掉边界情况。
中间值计算:使用mid = left + (right - left)/2而不是(left + right)/2,可以防止整数溢出。
边界调整:找到中间值后,新的范围应该排除mid位置,所以是mid+1或mid-1。
返回值:未找到时应返回一个特殊值(如-1),而不是未定义的值。
调试时可以打印查找范围:
c复制printf("Searching from %d to %d, mid=%d\n", left, right, mid);
回到题目中提到的国际象棋麦粒问题,完整实现如下:
c复制#include <stdio.h>
#include <math.h>
void printGrains() {
double grains = 1;
double total = 0;
for(int i = 1; i <= 64; i++) {
total += grains;
printf("Square %2d: %.0f grains, total: %.0f\n", i, grains, total);
grains *= 2;
}
printf("\nFinal total in scientific notation: %e\n", total);
}
int main() {
printGrains();
return 0;
}
这个程序会输出每个棋盘格子的麦粒数和累计总数,最后以科学计数法显示总和。
几个关键点:
在实际编写这些程序时,我总结了几个有用的经验:
防御性编程:对所有用户输入进行验证,比如检查字符串长度是否超过数组大小。
单元测试:为每个函数编写测试用例,特别是边界情况(如空字符串、单个字符等)。
调试输出:在复杂逻辑处添加临时打印语句,帮助理解程序执行流程。
代码复用:将常用操作封装成函数,如安全的字符串输入函数。
版本控制:使用git等工具管理代码版本,方便回溯和比较不同实现。
例如,一个更安全的字符串输入函数可以这样实现:
c复制void safeInput(char *buffer, int size) {
if(fgets(buffer, size, stdin)) {
// 去除可能的换行符
char *newline = strchr(buffer, '\n');
if(newline) *newline = '\0';
} else {
buffer[0] = '\0'; // 清空缓冲区
}
}
这些练习虽然基础,但涵盖了C语言编程的核心概念。通过反复练习和思考,可以深入理解指针、数组、内存管理等重要概念。