函数参数传递方式,经典GetMemory问题分析

函数参数传递方式

先说结论:三种传递方式,1.按值传递 2.通过指针模拟”按引用传递“ 3.数组作为参数的特殊情况

1.按值传递

函数调用时,是将实参的值复制给形参。形参和实参是内存中两个独立的变量,修改形参的值不会影响实参。

void modify(int x) {
    x = 10;  // 修改形参x的值
}

int main() {
    int a = 5;
    modify(a);
    printf("%d", a);  // 输出5,实参a未被修改
    return 0;
}
2.通过指针模拟”按引用传递“

虽然c语言没有真正的引用传递,但可以通过传递指针(内存地址的值)来间接修改实参的值。

此时传递的是 指针变量的值(地址的拷贝),但可以通过地址访问同一块内存。

void swap(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;  // 通过指针修改实参的值
}

int main() {
    int x = 1, y = 2;
    swap(&x, &y);      // 传递x和y的地址
    printf("%d %d", x, y);  // 输出2 1,实参被修改
    return 0;
}
3.数组作为参数的特殊情况

数组作为函数参数时,会退化为指向数组首元素的指针。传递的是数组首地址的拷贝,所以可以通过指针修改原数组的内容。

void modifyArray(int arr[], int size) {
    arr[0] = 100;  // 修改数组首元素的值
}

int main() {
    int a[] = {1, 2, 3};
    modifyArray(a, 3);
    printf("%d", a[0]);  // 输出100,原数组被修改
    return 0;
}

注意事项:

结构体的传递:直接传递结构体会导致完整拷贝(按值传递),会影响性能。通常使用指针传递结构体

void modifyStruct(struct MyStruct *s) {
    s->value = 10;  // 通过指针修改结构体成员
}

指针变量的值传递:如果需要修改指针本身(如动态内存分配),需传递指针的指针(int **p)

void changePointer(int *p) {
    p = NULL;  // 修改的是形参p的值,不影响实参的指针
}

 

明白以上概念后来看几道题

问题一
//分析以下代码的问题
void GetMemory1(char *p)
{
    p = (char*)malloc(100);
}
 
void Test1(void)
{
    char *str = NULL;
    GetMemory1(str);       //把str传进去,str是一个指针
    strcpy(str, "hello world");
    printf(str);
}

问题:

  1. GetMemory1(str); 传入的是 str指针的拷贝,也就是地址的拷贝。在GetMemory1函数中,修改的是 局部指针变量 p 的指向,使其指向新分配的 100字节内存,但这不会影响外部的 str指针。所以,函数返回后,str指针仍指向NULL,strcpy试图向NULL地址写数据,导致段错误。

  2. GetMemory1函数中分配的内存永远无法被释放,因为函数返回后,局部变量被销毁,分配的内存地址丢失,造成内存泄漏

解决方案:

  1. 使用二级指针,传入指针的地址

    void GetMemory1(char **p) { // 参数改为指向指针的指针
        *p = (char*)malloc(100); // 解引用修改外部指针
    }
    
    void Test1() {
        char *str = NULL;
        GetMemory1(&str); // 传递str的地址
        strcpy(str, "hello world");
        printf("%s", str);
        free(str); // 记得释放内存
    }
  2. 通过返回值传递内存地址

    char* GetMemory1() {
        return (char*)malloc(100);
    }
    
    void Test1() {
        char *str = GetMemory1(); // 直接接收返回值
        strcpy(str, "hello world");
        printf("%s", str);
        free(str);
    }

问题二
char *GetMemory2(void)
{
    char p[] = "hello world";
    return p;
}
 
void Test2(void)
{
    char *str = NULL;
    str = GetMemory2();
    printf(str);
}

问题:

  1. GetMemory2函数中定义了一个局部字符数组 p,存储在栈内存中。当该函数执行完毕返回时,其栈帧被销毁,p数组的内存被释放,此时返回的 指针p 指向的是一个已被回收的栈的内存地址(悬空指针)。然后 str指针指向了这个悬空指针,在 printf(str)时,试图访问已释放的内存,导致不可预测的结果(可能输出乱码,崩溃或看似正常运行)

  2. char p[] = "hello world";会在栈上创建一个字符数组,并将字符串的内容复制到该数组中。但是该数组是临时创建的(生命周期仅限函数执行期间),与字符串字面量(”hello world“,存储在 .rodata段,地址固定,生命周期为整个程序)的地址无关。

解决方案:

  1. 直接返回字符串字面量的地址(直接指向常量区)

    char* GetMemory2(void) {
        return "hello world"; // 字符串字面量存储在静态存储区,程序生命周期内有效
    }
    
    void Test2(void) {
        char *str = GetMemory2();
        printf("%s", str); // 输出正常
    }
  2. 使用 static关键字 延长数组声明周期

    char* GetMemory2(void) {
        static char p[] = "hello world"; // static 使数组存储在静态区,生命周期持续到程序结束
        return p;
    }
    
    void Test2(void) {
        char *str = GetMemory2();
        printf("%s", str); // 输出正常
    }
  3. 动态分配堆内存

    char* GetMemory2(void) {
        char* p = (char*)malloc(12); // 分配堆内存
        strcpy(p, "hello world");
        return p;
    }
    
    void Test2(void) {
        char *str = GetMemory2();
        printf("%s", str);
        free(str); // 必须手动释放内存!
    }
问题三
void GetMemory3(char **p, int num)
{
   *p = (char*)malloc(num);
}
 
void Test3(void)
{
   char *str = NULL;
   GetMemory3(&str, 100);
   strcpy(str, "hello");
   printf(str);
}

问题:可以打印hello,但仍存在问题

  1. 未检查malloc是否成功,malloc(num)可能失败(内存不足时返回NULL),若直接对NULL指针进行strcpy操作,会导致段错误

  2. 使用 printf(str)存在格式字符串漏洞,若str中包含格式化字符串(%s,%d),printf会将其误认为格式说明符,导致未定义行为(如读取无效内存地址)。

  3. 内存泄漏,malloc后没有free。

问题四
void Test4(void)
{ 
    char *str = (char *)malloc(100);
    strcpy(str, "hello");
    free(str);
    if(str != NULL)
    {
        strcpy(str, "hello");
        printf(str);
    }   
}

问题:

  1. 悬空指针与访问已释放内存。free(str)释放了str指向的内存,但不会将指针置为NULL,此时str成悬空指针(指向无效内存)。也就是说指针还是指向那个地址,只是从那个地址开始的100字节的内存被清空了。所以if(str != NULL)永远成立,strcpy(str, "hello") 和 printf(str) 操作了已释放的内存,属于未定义行为,可能导致崩溃。

  2. 格式字符串漏洞,问题三中说过。

问题五
char *GetMemory5(void)
{
     return "hello world";
}
 
void Test5(void)
{
    char *str = NULL;
    str = GetMemory5();
    printf(str);
}

问题:可以打印hello world,但仍存在问题

  1. 返回的字符串字面量不可修改,若后续尝试修改内容(str[0] = 'H'),会导致未定义行为。可以使用const修饰str,明确表示不能修改。

  2. 格式字符串漏洞,问题三中说过。

问题六
swap(int* p1, int* p2)
{
 int *p;
 *p = *p1;
 *p1 = *p2;
 *p2 = *p;
}

问题:

指针p 没有初始化,是野指针,它指向一个随机地址。*p = *p1会向随机地址写入数据,可能会导致程序崩溃或静默数据被破坏。

总结
int a = 0; //全局初始化区,放入.data段
char *p1;  //全局未初始化区,放入.bss段
int main()
{
     int b;               //栈,栈上空间自动分配,自动回收
     char s[] = "abc";    //栈
     char *p2;            //栈
     char *p3 = "123456"; // 123456\0是字面量存储在.rodata段,p3存储在栈上。
     static int c =0;    //全局(静态)初始化区
     p1 = (char *)malloc(10);    //堆  
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值