文章目录
-
- 一、函数参数传递与返回值陷阱
-
- [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 核心知识点讲解
1.2.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;
}
- 地址传递(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 课后作业(实战巩固)
-
编写一个函数,通过地址传递,交换两个整数的值,要求校验指针是否为空,避免空指针解引用。
-
编写一个函数,返回一个长度为10的字符串(内容为"0123456789"),要求使用堆内存分配,避免返回局部变量指针,调用后手动释放内存。
-
排查以下代码的错误(至少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 课程总结
-
参数传递:值传递仅传递值,不修改实参;地址传递传递指针,可修改实参,核心是"指针解引用"操作。
-
返回值陷阱:禁止返回局部变量指针(栈内存释放),优先使用静态变量或堆内存;调用函数必须校验返回值。
-
核心原则:指针使用前必校验(非空),堆内存分配后必释放,返回值必校验,避免野指针、空指针、内存泄漏。
二、上一节课作业答案 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 注意事项
-
switch语句:每个case后必须加break,除非有明确的穿透需求(本作业无穿透场景);default必须添加,处理非法输入,避免程序异常。
-
goto语句:仅用于"统一清理资源"或"重新跳转至指定逻辑",禁止滥用(如用于循环跳转、逻辑跳转),避免代码可读性下降。
-
资源管理:堆内存分配后必须校验返回值,程序退出前统一释放,避免内存泄漏;goto标签cleanup用于集中清理资源,确保所有路径都能释放资源。
-
输入处理:输入格式错误时,需清空缓冲区,避免死循环;除数为0时,提示错误并重新输入,提升程序健壮性。
-
代码规范:变量初始化、指针校验、注释清晰,符合C语言实战开发习惯,便于后续维护和排查错误。
上一节课链接: C语言逆向学习基础课 第 6 课:switch与goto语句的正确使用
下一节课链接:C语言逆向学习基础课 第8课 函数原型与可变参数使用误区