C语言的一种错误传参做法

多次遇过的一个错误传参做法。

C/C++中的堆和栈

  • 栈区由编译器自动分配和释放,存放函数的参数值,局部变量值等,下面称之为系统栈,因为由编译器环境系统为其分配。
  • 堆区由程序员通过malloc, realloc, calloc分配和释放,若程序员申请堆空间后不free,程序结束时可能由操作系统回收。堆区的内存空间分配类似数据结构中的链表和散列表结合,在可用的内存/虚拟内存空间中寻找一块足够大的、满足malloc等分配函数要求的空间,标记该块空间为已使用,并返回该块空间的首地址。
  • 全局区/静态区存放全局变量和静态变量,其中在代码中初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后该区域由系统释放。
  • 文字常量区,存放const常量、字符串等。程序结束后由系统释放。
  • 程序代码区,存放函数体的二进制代码。

按值传递

C语言没有C++的按引用传递,要在子函数中修改父函数里声明的局部变量,需要将该变量的地址作为参数传入子函数,在子函数中通过类似(*str)的方法修改str指向的内存中存储的值。

一个简单的测试如下

1
2
3
4
5
6
7
8
9
void func1( int a ){
a = 1;
}
int main(){
int a = 2;
func1(a); /* 传递a的拷贝 */
printf("%d\n", a);
return 0;
}

测试结果a=2,在调用func1时参数为按值传递,传入的a是main函数中声明的a变量的一个拷贝,将其称为a’。在系统栈中,a属于main函数在栈中的空间,而a’属于func1在系统栈中的空间。在func1中修改a’不会影响到main函数中a的内存域。正确的传参做法应修改如下。

1
2
3
4
5
6
7
8
9
void func1( int * a ){	/* 接收的参数类型为int *,代表一个指向int型的指针,也可认为是一个指向内存中int型大小空间开头的地址 */
*a = 1; /* 将a中存储的地址指向的内存空间赋值为1,若int为4个字节,则该块内存赋值后为 0x00000001 */
}
int main(){
int a = 2;
func1(&a); /* 传递a的地址的拷贝 */
printf("%d\n",a);
return 0;
}

测试结果a=1。注意此处调用func1时仍然是按值传递,但func1接收的参数类型不再是int,而是int * ,即指向int类型空间的地址,main函数里func1(&a),是传入了一份(&a)的拷贝。在系统栈中,&a指向了main函数中变量a所占的内存地址。上面正确传参做法中注释部分,&a其实也可以视为一个变量,这个变量的类型是int * ,是一个指向int的指针。因而向func1传入的(&a)的拷贝(&a)’,其实就相当于传入了一个int * 类型变量的拷贝。(&a)’和(&a)存的值是相同的,但它们分别占据了系统栈中不同的内存空间,如果修改上述程序如下:

1
2
3
4
5
6
7
8
9
10
void func1( int * a, int * b ){
a = b;
}
int main(){
int a = 1, b = 2;
printf("%d %d\n", &a, &b);
func1( &a, &b);
printf("%d %d\n", &a, &b);
return 0;
}

得到的运行结果不定(在不同时间、不同计算机上执行,内存地址不同),但格式类似如下:

1
2
3
4
[forec@forec ~]$ gcc wrongparameter2.c -o t
[forec@forec ~]$ ./t
493409228 493409224
493409228 1

上面运行结果说明,main函数中的&a在执行func1前后没有变化,这和最初给出的错误传参做法是一致的,将父函数中a的地址&a作为参数传入子函数,子函数无法修改&a的值(也就是main里变量a的地址),只能修改&a指向的内存块的值

错误传参测试

一维数组在子函数中动态分配

下面给出一种错误的一维数组动态分配示例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <stdio.h>
#include <stdlib.h>
void func1(int * str1){
int i;
puts("starting malloc str1 with 10 * int ...");
str1 = (int*)malloc(sizeof(int)*10);
printf("In func1, after malloc, the value of str1 is %d\n", str1 );
for ( i = 0 ; i < 10 ; i++ )
str1[i] = i;
puts("In func1, the value of str1[] is");
for ( i = 0 ; i < 10 ; i++ )
printf("%d ",str1[i]);
putchar('\n');
}
int main(){
int * str, i;
puts("Before func1, the value of array str is");
for ( i = 0 ; i < 10 ; i++ )
printf("%d ", str[i] );
putchar('\n');
printf("In main function, str is %d\n", str );
func1(str);
printf("After func1, str is %d\n", str );
puts("After func1, the value of array str is");
for ( i = 0 ; i < 10 ; i++ )
printf("%d ", str[i] );
putchar('\n');
return 0;
}

执行上面的代码,运行结果如下

1
2
3
4
5
6
7
8
9
10
Before func1, the value of array str is
1 0 -649909861 32764 0 0 -649909857 32764 -649909841 32764
In main function, str is -649916608
starting malloc str1 with 10 * int ...
In func1, after malloc, the value of str1 is 30027792
In func1, the value of str1[] is
0 1 2 3 4 5 6 7 8 9
After func1, str is -649916608
After func1, the value of array str is
1 0 -649909861 32764 0 0 -649909857 32764 -649909841 32764

在代码中,为了突出区分str和传给func1的str的拷贝,将func1的参数用str1表示。可以看出,str的值(str是一个int * 变量,其值就是指向的int型的指针)在func1前后都没有变化,func1函数中变化的仅仅是str的拷贝str1。因此上面代码的传参方法有误。可行的传参方法有下面两种,下面的第一种是正确的,第二种在C语言中可行,因为C语言不存在垃圾回收,但这种习惯不好,容易造成野指针。并且在支持垃圾回收的语言中,函数结束后会释放函数执行过程中产生的垃圾。

可行做法1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void func1(int ** str1){	/* 要为int *类型赋值,传入int** */
int i;
puts("starting malloc str1 with 10 * int ...");
*str1 = (int*)malloc(sizeof(int)*10);
printf("In func1, after malloc, the value of str1 is %d\n", *str1 );
for ( i = 0 ; i < 10 ; i++ )
(*str1)[i] = i;
puts("In func1, the value of str1[] is");
for ( i = 0 ; i < 10 ; i++ )
printf("%d ",(*str1)[i]);
putchar('\n');
}
int main(){
int * str, i;
......
func1(&str); /* 传入指针str所在的地址 */
......
return 0;
}

上面代码和之前代码的不同之处在于,func1的接收参数变为了 int ** 类型,是一个指向int型指针的指针,因此向func1传入str的地址,就可以在func1里修改str的值。在func1中,要修改main中str的值,只需要对( * str1)进行操作。运行结果如下。

1
2
3
4
5
6
7
8
9
10
Before func1, the value of array str is
1 0 -1242502757 32767 0 0 -1242502753 32767 -1242502737 32767
In main function, str is -1242510416
starting malloc str1 with 10 * int ...
In func1, after malloc, the value of str1 is 28885008
In func1, the value of str1[] is
0 1 2 3 4 5 6 7 8 9
After func1, str is 28885008
After func1, the value of array str is
0 1 2 3 4 5 6 7 8 9

可行做法2

1
2
3
4
5
6
7
8
9
10
11
int * func1( int * str ){
......
return str;
}
int main(){
int * str, i;
......
str = func1( str );
......
return 0;
}

为func1设一个int *类型的返回值,之所以func1对str1的修改无法影响str,是因为str1仅仅是str的拷贝。因此在func1结束时把str1返回,并赋值给str。运行结果和上面的做法1相同。

二维数组在子函数中动态分配

这类错误引发的主要场景在二维数组,尤其是结构体数组的分配上。考虑下面测试代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <stdio.h>
#include <stdlib.h>
void func2(int ** str2){
str2 = (int**)malloc(sizeof(int*)*10);
printf("In func2, the value of str2 is %d\n",str2);
int i, j;
for ( i = 0; i < 10; i++){
str2[i] = (int *)malloc(sizeof(int)*10);
printf(" The value of str2[%d] is %d\n", i, str2[i]);
printf("And the value of the array is ");
for ( j = 0 ; j < 10 ; j++ ){
str2[i][j] = i * 10 + j;
printf("%d ", str2[i][j]);
}
putchar('\n');
}
}
int main(){
int ** str2; /* str2 is stored in stack, which was not initialized */
int i, j ;
printf("Before run func2, the str2 is %d\n", str2);
func2(str2);
printf("After run func2, return to main, the str2 is %d\n", str2);
for ( i = 0 ; i < 10 ; i++ )
printf("str2[i] is %d\n", str2[i]);
puts("The value of total str2 is :");
for ( i = 0 ; i < 10 ; i++ ){
for ( j = 0 ; j < 10 ; j++ )
printf("%d ", str2[i][j]);
putchar('\n');
}
return 0;
}

运行结果如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
Before run func2, the str2 is -1427732048
In func2, the value of str2 is 10940432
The value of str2[0] is 10940528
And the value of the array is 0 1 2 3 4 5 6 7 8 9
The value of str2[1] is 10940576
And the value of the array is 10 11 12 13 14 15 16 17 18 19
The value of str2[2] is 10940624
And the value of the array is 20 21 22 23 24 25 26 27 28 29
The value of str2[3] is 10940672
And the value of the array is 30 31 32 33 34 35 36 37 38 39
The value of str2[4] is 10940720
And the value of the array is 40 41 42 43 44 45 46 47 48 49
The value of str2[5] is 10940768
And the value of the array is 50 51 52 53 54 55 56 57 58 59
The value of str2[6] is 10940816
And the value of the array is 60 61 62 63 64 65 66 67 68 69
The value of str2[7] is 10940864
And the value of the array is 70 71 72 73 74 75 76 77 78 79
The value of str2[8] is 10940912
And the value of the array is 80 81 82 83 84 85 86 87 88 89
The value of str2[9] is 10940960
And the value of the array is 90 91 92 93 94 95 96 97 98 99
After run func2, return to main, the str2 is -1427732048
str2[i] is 1
str2[i] is -1427727973
str2[i] is 0
str2[i] is -1427727969
str2[i] is -1427727953
str2[i] is -1427727942
str2[i] is -1427727925
str2[i] is -1427727831
str2[i] is -1427727796
str2[i] is -1427727780
The value of total str2 is :
段错误 (核心已转储)

可以看出,main中的str2并没有受func2执行的影响。联系上面一维数组的动态分配,可以联想到,要么让func2返回str2的值,要么传入str2的地址,而func2接收int * 类型的参数。

正确做法1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void func2(int *** str2){
*str2 = (int**)malloc(sizeof(int*)*10);
printf("In func2, the value of str2 is %d\n",*str2);
int i, j;
for ( i = 0; i < 10; i++){
(*str2)[i] = (int *)malloc(sizeof(int)*10);
printf(" The value of str2[%d] is %d\n", i, (*str2)[i]);
printf("And the value of the array is ");
for ( j = 0 ; j < 10 ; j++ ){
(*str2)[i][j] = i * 10 + j;
printf("%d ", (*str2)[i][j]);
}
putchar('\n');
}
}

int main(){
int ** str2, i, j;
......
func2(&str2);
......
return 0;
}

正确做法2

1
2
3
4
int ** func2(int ** str2){
......
return str2;
}

野指针

对malloc分配的堆空间,free对应的首地址后一定要将指针变量赋为NULL。如p = ( int * )malloc(sizeof(int) * 10),若free(p),则一定要 p = NULL,否则p指向的内存空间已经释放,而p指向了垃圾内存,成为野指针。再次调用p会引发不允许的内存访问。


原创作品,允许转载,转载时无需告知,但请务必以超链接形式标明文章原始出处(https://forec.github.io/2015/12/28/parameter-error/) 、作者信息(Forec)和本声明。

分享到