函数与数组---------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) 
{
    // 打开目标目录,失败时返回NULL
    DIR *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++;                          // 计数器加1
                printf("找到文件: %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.适合频繁插入删除的场景,比如任务队列,日志记录。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值