C语言逆向学习基础课 第7课 函数参数传递与返回值陷阱

文章目录

    • 一、函数参数传递与返回值陷阱
      • [1.1 课程目标](#1.1 课程目标)
      • [1.2 核心知识点讲解](#1.2 核心知识点讲解)
        • [1.2.1 函数参数传递的两种方式(重点+易错点)](#1.2.1 函数参数传递的两种方式(重点+易错点))
        • [1.2.2 函数返回值的高频陷阱(重点+规避方法)](#1.2.2 函数返回值的高频陷阱(重点+规避方法))
      • [1.3 实战示例(综合错误排查)](#1.3 实战示例(综合错误排查))
      • [1.4 课后作业(实战巩固)](#1.4 课后作业(实战巩固))
      • [1.5 课程总结](#1.5 课程总结)
    • [二、上一节课作业答案 switch与goto语句的正确使用](#二、上一节课作业答案 switch与goto语句的正确使用)
      • [2.1 实战作业代码](#2.1 实战作业代码)
      • [2.2 代码功能说明](#2.2 代码功能说明)
      • [2.3 注意事项](#2.3 注意事项)

一、函数参数传递与返回值陷阱

1.1 课程目标

  1. 明确函数参数传递的两种核心方式(值传递、地址传递),区分二者的使用场景与易错点;

  2. 掌握函数返回值的正确使用规范,规避返回局部变量指针、返回值未校验等高频陷阱;

  3. 能独立排查并修正函数参数传递与返回值相关的代码错误,提升代码健壮性。

1.2 核心知识点讲解

1.2.1 函数参数传递的两种方式(重点+易错点)

函数参数传递本质是"值拷贝",分为两种形式,核心区别在于是否能修改实参的值,也是实战中最易混淆的点。

  1. 值传递(pass by value)
  • 定义:将实参的值拷贝一份传递给形参,形参是独立的局部变量,修改形参的值不会影响实参。

  • 易错点:误以为修改形参能改变实参,导致逻辑错误。

  • 示例代码(错误+正确对比):

c 复制代码
#include <stdio.h>

// 错误示例:试图通过值传递修改实参
void changeValue(int a) {
    a = 100; // 仅修改形参a,实参不受影响
}

// 正确示例:值传递仅用于获取实参值,不修改实参
int getSum(int a, int b) {
    return a + b; // 仅使用实参的值,返回计算结果
}

int main() {
    int num = 10;
    changeValue(num);
    printf("值传递后num = %d\n", num); // 输出:10(实参未改变)
    
    int sum = getSum(3, 5);
    printf("3+5的和 = %d\n", sum); // 输出:8(正确使用值传递)
    return 0;
}
  1. 地址传递(pass by address)
  • 定义:将实参的地址(指针)传递给形参,形参通过指针间接操作实参的内存空间,修改指针指向的值会影响实参。

  • 易错点:传递空指针、野指针;指针解引用前未校验;混淆"指针本身"与"指针指向的值"。

  • 示例代码(正确用法+易错规避):

c 复制代码
#include <stdio.h>

// 正确示例:通过地址传递修改实参的值
void changeValueByAddr(int *a) {
    // 易错点规避:先校验指针是否为空,避免空指针解引用
    if (a == NULL) {
        printf("错误:指针为空,无法操作!\n");
        return;
    }
    *a = 100; // 修改指针指向的实参值
}

int main() {
    int num = 10;
    changeValueByAddr(&num); // 传递实参的地址
    printf("地址传递后num = %d\n", num); // 输出:100(实参被修改)
    
    // 易错案例:传递空指针
    int *p = NULL;
    changeValueByAddr(p); // 输出错误提示,避免程序崩溃
    return 0;
}
1.2.2 函数返回值的高频陷阱(重点+规避方法)

函数返回值的核心陷阱集中在"返回非法值",导致程序崩溃、结果异常,以下是3个最高频陷阱及解决方案。

陷阱1:返回局部变量的指针(栈内存释放问题)

  • 原理:函数内的局部变量存储在栈内存中,函数执行结束后,栈内存会被系统释放,局部变量的地址变为"悬空地址",返回该地址会导致未定义行为(程序崩溃、输出乱码)。

  • 错误示例+正确修正:

c 复制代码
#include <stdio.h>
#include <stdlib.h>

// 错误示例:返回局部变量的指针
char *getStrError() {
    char str[] = "hello world"; // 局部变量,栈内存存储
    return str; // 返回栈内存地址,函数结束后地址失效
}

// 正确方案1:使用静态局部变量(存储在全局数据区,函数结束后不释放)
char *getStrRight1() {
    static char str[] = "hello world"; // 静态局部变量
    return str; // 安全返回,地址有效
}

// 正确方案2:使用堆内存分配(malloc),手动管理内存
char *getStrRight2() {
    char *str = (char *)malloc(12); // 堆内存分配,手动释放
    if (str == NULL) { // 规避陷阱:校验malloc返回值,避免空指针
        return NULL;
    }
    strcpy(str, "hello world");
    return str;
}

int main() {
    // 错误调用:输出乱码或程序崩溃
    char *p1 = getStrError();
    printf("错误返回值:%s\n", p1);
    
    // 正确调用1
    char *p2 = getStrRight1();
    printf("正确返回值1:%s\n", p2);
    
    // 正确调用2:记得手动释放堆内存,避免内存泄漏
    char *p3 = getStrRight2();
    if (p3 != NULL) {
        printf("正确返回值2:%s\n", p3);
        free(p3); // 手动释放堆内存
        p3 = NULL; // 规避野指针
    }
    return 0;
}

陷阱2:返回值未校验(忽略错误状态)

  • 易错点:调用有返回值的函数时,不校验返回值是否合法(如malloc返回NULL、文件操作返回-1),直接使用返回值导致程序异常。

  • 规避方法:调用函数后,先校验返回值,再进行后续操作(参考上面getStrRight2的调用示例)。

陷阱3:返回值类型与函数声明不一致

  • 易错点:函数声明返回int类型,但实际返回char、指针等其他类型,编译器可能报警告,运行时出现数据截断、行为异常。

  • 规避方法:严格保证函数声明、定义、返回值类型一致。

1.3 实战示例(综合错误排查)

以下代码包含2个高频错误(值传递误用、返回局部变量指针),请排查并修正:

c 复制代码
#include <stdio.h>

// 错误1:试图用值传递修改实参
void addOne(int a) {
    a++;
}

// 错误2:返回局部变量指针
int *getArray() {
    int arr[5] = {1,2,3,4,5};
    return arr;
}

int main() {
    int num = 5;
    addOne(num);
    printf("num = %d\n", num); // 预期输出6,实际输出5
    
    int *arr = getArray();
    for (int i = 0; i < 5; i++) {
        printf("%d ", arr[i]); // 输出乱码
    }
    return 0;
}

修正后代码:

c 复制代码
#include <stdio.h>
#include <stdlib.h>

// 修正1:使用地址传递修改实参
void addOne(int *a) {
    if (a == NULL) {
        return;
    }
    (*a)++; // 注意括号,优先级问题
}

// 修正2:使用堆内存分配返回数组
int *getArray() {
    int *arr = (int *)malloc(5 * sizeof(int));
    if (arr == NULL) {
        return NULL;
    }
    arr[0] = 1;
    arr[1] = 2;
    arr[2] = 3;
    arr[3] = 4;
    arr[4] = 5;
    return arr;
}

int main() {
    int num = 5;
    addOne(&num);
    printf("num = %d\n", num); // 输出6
    
    int *arr = getArray();
    if (arr != NULL) {
        for (int i = 0; i < 5; i++) {
            printf("%d ", arr[i]); // 输出1 2 3 4 5
        }
        free(arr);
        arr = NULL;
    }
    return 0;
}

1.4 课后作业(实战巩固)

  1. 编写一个函数,通过地址传递,交换两个整数的值,要求校验指针是否为空,避免空指针解引用。

  2. 编写一个函数,返回一个长度为10的字符串(内容为"0123456789"),要求使用堆内存分配,避免返回局部变量指针,调用后手动释放内存。

  3. 排查以下代码的错误(至少2个),并修正:

c 复制代码
#include <stdio.h>

char *getString() {
    char *str = "hello";
    return str;
}

void changeNum(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

int main() {
    char *p = getString();
    printf("%s\n", p);
    
    int x = 3, y = 5;
    changeNum(x, y);
    printf("x=%d, y=%d\n", x, y);
    return 0;
}

1.5 课程总结

  1. 参数传递:值传递仅传递值,不修改实参;地址传递传递指针,可修改实参,核心是"指针解引用"操作。

  2. 返回值陷阱:禁止返回局部变量指针(栈内存释放),优先使用静态变量或堆内存;调用函数必须校验返回值。

  3. 核心原则:指针使用前必校验(非空),堆内存分配后必释放,返回值必校验,避免野指针、空指针、内存泄漏。

二、上一节课作业答案 switch与goto语句的正确使用

2.1 实战作业代码

c 复制代码
#include <stdio.h>
#include <stdlib.h>

// 实战作业:模拟简易计算器(支持+、-、*、/、退出),规范使用switch,合理使用goto清理资源
int main() {
    int a, b;
    char op;
    int flag = 1; // 控制循环
    int *p = (int *)malloc(4); // 模拟需要清理的资源(堆内存)
    if (p == NULL) {
        printf("内存分配失败,程序退出!\n");
        return 1;
    }
    *p = 0; // 初始化资源
    
loop: // goto标签,仅用于统一清理资源
    while (flag) {
        printf("\n请输入运算式(格式:a op b,如3 + 5;输入q退出):");
        // 读取输入,校验输入合法性
        if (scanf("%d %c %d", &a, &op, &b) != 3) {
            // 处理输入错误,清空缓冲区
            while (getchar() != '\n');
            printf("输入格式错误,请重新输入!\n");
            goto loop; // 跳转至loop,重新输入,不清理资源
        }
        
        // 规范使用switch,每个case加break,避免穿透
        switch (op) {
            case '+':
                printf("%d + %d = %d\n", a, b, a + b);
                break; // 避免穿透到下一个case
            case '-':
                printf("%d - %d = %d\n", a, b, a - b);
                break;
            case '*':
                printf("%d * %d = %d\n", a, b, a * b);
                break;
            case '/':
                if (b == 0) {
                    printf("错误:除数不能为0!\n");
                    goto loop; // 重新输入
                }
                printf("%d / %d = %d\n", a, b, a / b);
                break;
            case 'q':
                flag = 0; // 退出循环
                break;
            default:
                printf("错误:不支持的运算符!\n");
                goto loop; // 重新输入
        }
    }
    
    // 统一清理资源(goto跳转至此,避免资源泄漏)
cleanup:
    if (p != NULL) {
        free(p);
        p = NULL;
    }
    printf("程序正常退出,资源已清理!\n");
    return 0;
}

2.2 代码功能说明

本代码模拟简易计算器,支持+、-、*、/四则运算及退出功能。规范使用switch语句,每个case后添加break避免穿透,通过default处理非法运算符;合理使用goto语句,仅用于输入错误时重新获取输入、程序退出时统一清理堆内存资源。代码包含输入合法性校验、除数不为0校验、内存分配校验,规避switch穿透、goto滥用、资源泄漏等陷阱,逻辑清晰,符合C语言实战规范。

2.3 注意事项

  1. switch语句:每个case后必须加break,除非有明确的穿透需求(本作业无穿透场景);default必须添加,处理非法输入,避免程序异常。

  2. goto语句:仅用于"统一清理资源"或"重新跳转至指定逻辑",禁止滥用(如用于循环跳转、逻辑跳转),避免代码可读性下降。

  3. 资源管理:堆内存分配后必须校验返回值,程序退出前统一释放,避免内存泄漏;goto标签cleanup用于集中清理资源,确保所有路径都能释放资源。

  4. 输入处理:输入格式错误时,需清空缓冲区,避免死循环;除数为0时,提示错误并重新输入,提升程序健壮性。

  5. 代码规范:变量初始化、指针校验、注释清晰,符合C语言实战开发习惯,便于后续维护和排查错误。

上一节课链接: C语言逆向学习基础课 第 6 课:switch与goto语句的正确使用

下一节课链接:C语言逆向学习基础课 第8课 函数原型与可变参数使用误区