函数与数组---------C语言经典题目(1)
函数的参数传递
解释值传递和指针传递的区别。
值传递:
函数接收的是原始数据的副本,在函数内部对参数的修改不会影响原始数据。
指针传递:
函数接收的是原始数据的内存地址(指针)。通过指针可以直接访问和修改原始数据。
总结:
值传递是“复制数据”,函数内修改不影响外部。
指针传递时“传递数据地址”,可通过地址直接修改外部数据。
如何通过指针实现指针传递?
在C语言中,这也就是说向函数传递一个指针的地址(即指针的指针),让函数可以直接操作外部指针变量本身。
#include <stdio.h>
#include <stdlib.h>// 函数声明:接收指针的指针,用于修改原始指针变量
void allocate_memory(char **ptr, int size)
{// 分配指定大小的内存*ptr = (char *)malloc(size);if (*ptr == NULL) {printf("内存分配失败\n");return;}// 在分配的内存中存储数据sprintf(*ptr, "Hello, world!");
}int main()
{char *str = NULL; // 初始化为NULL的指针// 传递指针的地址(&str)给函数allocate_memory(&str, 20); // 分配20字节内存if (str != NULL) {printf("指针指向的内容: %s\n", str); // 输出:Hello, world!free(str); // 释放内存str = NULL; // 置空指针防止野指针}return 0;
}
一个函数传入一个参数,怎么返回两个参数
一般来说,函数只能有一个返回值,如果需要让一个函数返回两个参数,可以通过指针作为输出参数。比如说:
#include <stdio.h>// 函数通过指针参数返回两个值
void return_two_values(int a, int *sum, int *product)
{*sum = a + 2; // 修改sum指针指向的变量*product = a * 2; // 修改product指针指向的变量
}int main() {int x = 3;int sum, product;return_two_values(x, &sum, &product); // 传入变量地址printf("Sum: %d\nProduct: %d\n", sum, product);return 0;
}
函数参数中的int *sum 和int *product是指针,用于接收调用者传入的变量地址。
通过*sum和*product直接操作原变量的内存空间,实现返回多个值的效果。
递归函数
你用过递归吗?
用过,递归是一种函数在其定义中直接或间接调用自身的编程技巧,比如说计算阶乘,可以有
当 n > 0 时,fun(n) = n * fun(n - 1),终止条件是fun(0) = 1。再比如可以用递归的思想来遍历一下某个项目目录下所有的C语言源文件(.c文件),可以用以下代码:
int count_files(const char *dir_path, const char *ext)
{// 打开目标目录,失败时返回NULLDIR *dir = opendir(dir_path);struct dirent *entry; // 用于存储目录项信息的结构体指针struct stat stat_buf; // 用于存储文件/目录状态信息int count = 0; // 初始化文件计数器// 处理目录打开失败的情况if (!dir) {perror("无法打开目录"); // 打印系统错误信息return 0; // 目录不可访问时返回0}// 遍历目录中的所有条目while ((entry = readdir(dir)) != NULL){// 跳过当前目录(.)和父目录(..),避免无限递归if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) {continue;}// 构建完整路径(目录路径 + 条目名称)char full_path[PATH_MAX];snprintf(full_path, sizeof(full_path), "%s/%s", dir_path, entry->d_name);// 获取文件/目录的状态信息,失败时跳过当前条目if (lstat(full_path, &stat_buf) == -1) {perror("获取文件状态失败"); // 打印获取状态失败的错误信息continue;}// 判断当前条目是否为目录if (S_ISDIR(stat_buf.st_mode)) {// 递归处理子目录,统计结果累加到计数器count += count_files(full_path, ext);}// 判断当前条目是否为普通文件else if (S_ISREG(stat_buf.st_mode)) {// 查找文件名中最后一个点的位置(扩展名起始位置)char *dot = strrchr(entry->d_name, '.');// 检查是否存在扩展名且与目标扩展名匹配(不区分大小写)if (dot && strcasecmp(dot, ext) == 0) {count++; // 计数器加1printf("找到文件: %s\n", full_path); // 打印文件路径}}}// 关闭目录流,释放资源closedir(dir);return count; // 返回统计结果
}int main() {// 配置遍历参数const char *start_dir = "/project/source"; // 起始遍历的根目录const char *target_ext = ".c"; // 目标文件扩展名(C语言源文件)// 执行统计并获取结果int total = count_files(start_dir, target_ext);// 打印最终统计结果printf("总计 %d 个 %s 文件\n", total, target_ext);return 0;
}
递归函数的优缺点是什么?
优点是可以简化代码,提高可读性,将复杂问题拆解为规模更小的子问题,每个子问题的解决逻辑一致。比如快速排序算法通过递归划分区间,代码结果清晰,易于理解。
缺点是每次递归调用都会在栈中创建新的函数帧,空间复杂度为O(n),性能开销比较大,且有栈溢出的风险。
如何避免递归中的无限循环?
主要是确保递归过程中具备明确的终止条件。
比如阶乘中,终止条件是n == 0或n == 1,此时直接返回1,不再继续递归。
计算斐波那契数列时,若输入负数,应该返回错误码或默认值,不再继续递归。
int fibonacci(int n)
{if (n < 0) // 处理非法输入{ return -1; // 或报错}if (n == 0 || n == 1) // 终止条件{return n;}return fibonacci(n - 1) + fibonacci(n - 2);
}
这个代码是通过递归方法计算斐波那契数列的第n项。
数组
数组下标越界会导致什么问题?
数组下标越界会引发未定义行为(Undefined Behavior)。
当越界访问的内存地址属于不可读写区域,会触发段错误,导致程序直接崩溃。
int a[5];
int *p = a - 1; // 越界读取前一个地址(假设为无效地址)
printf("%d\n", *p); // 触发段错误
当写入越界时,可能覆盖相邻内存中的数据:
int a[3] = {1, 2, 3};
arr[5] = 100; // 越界写入,可能覆盖相邻内存中的其他变量
如何避免?
访问数组前确保下标在数组范围内;
使用安全函数,比如用strncpy替代strcpy。
多维数组在内存中的存储方式是什么?
遵循“行优先”原则,即按行依次存储每一行的元素,同一行内的元素在内存中连续排列。
比如二维数组a[m][n],可以看作a是一个指针,指向包含n个元素的一维数组,a的类型为
int(*)[n]。
什么是字符串
字符串是由字符组成的序列,并且以空字符“\0”(ASCII码值为0)作为结尾标志的一维字符数组。
字符串在内存中占用连续的字节空间,末尾的‘\0’不计入字符串的实际长度,但用于标记字符串的结束。
比如:字符串“hello”,在内存中实际存储为h e l l o \0,共六个字节。
字符串有两种类型:
字符串常量:直接用双引号括起来的:“abc”,存放在程序的只读数据段,内容不可修改。
字符串变量:可以通过字符数组或字符指针来定义和操作:
char a[] = "hello world"; // 字符数组,存储在栈区,内容可修改
char *p = "hello world"; // 指针指向字符串常量,不可通过p修改内容
数组和链表的区别?
数组:
是由相同类型元素组成的连续内存块,元素在内存中按顺序排列,通过下标(索引)来访问元素,如a[0]、a[1]。
1.定义时需指定固定长度(静态数组)、或通过动态内存分配(如malloc)指定长度(动态数组)。但长度一旦确定,后续难以灵活改变。
2.可以随机访问,通过下标直接计算内存地址。
3.插入删除效率低,若在中间插入或删除元素,需移动后续所有元素。
4.适合需要频繁随机访问元素,且元素数量固定或可预估,比如存储学生成绩,配置参数等。
链表:
由 节点 组成的链式结构,每个节点包含数据域和指针域(指向下一个节点或前一个节点,单向、双向链表)。
1.节点在内存中不连续,通过指针链接,无需预先指定总长度,可以动态添加或删除节点。
2.只能顺序访问,需从表头或表尾开始遍历。
3.插入删除效率高,只需修改指针指向,无需移动其他节点。
4.适合频繁插入删除的场景,比如任务队列,日志记录。