在C语言中,指针是最难掌握的知识点之一,不过在平常的教材中都会比较详细地讲解指针,如果大家想深入学习C中指针的详细知识,推荐大家可以看一下《C和指针》,也可以参考《C专家编程》或者《C陷阱与缺陷》,这些书对C语言的讲解是十分深入的,比基本的教材讲的深入一些,可以看一下。
言归正传,本编文章的主要目的是和大家分享一个我在做项目时遇到的一个问题,也就是当指针作为函数参数,并且想通过这个函数为这个指针分配内存的时候出现的一个问题,在初学者看来,这个可能没什么问题,也很自然地在使用,但是这个问题曾经导致我做的项目出现了很大的BUG,最终经过一些调试手段才找到这个问题,下面通过代码和叙述进行详细地讲解。
我们经常会遇到这样的函数,比如说:为一个指针分配内存并初始化其内存中的变量,这个指针一般都是一个较为完善的结构体指针或者其他复杂的指针,这个函数一般命名为:mallocAndInit(); 具体的形式下面会给出来,那么下面我们先来看一下这个函数的第一个版本,大家可以先看看“版本1”有没有错误。
// 版本1
#include
#include
#include
#define LEN 20
typedef struct person
{
char name[LEN];
int age;
}person;
// 返回0表示失败,返回1表示成功
int mallocAndInit(person *pPs)
{
pPs = (person*)malloc(sizeof(person));
if (NULL == pPs)
{
// 内存分配失败
fprintf(stderr, "Malloc failed.\n");
return 0;
}
// 初始化
pPs->age = 0;
strcpy(pPs->name, "");
return 1;
}
void main()
{
person *pTest = NULL;
if (!mallocAndInit(pTest))
{
fprintf(stderr, "Error.\n");
exit(1);
}
// 这里会出错,你看出来为什么了吗?
printf("The person's name is %s, age is %d.\n", pTest->name, pTest->age);
if (pTest != NULL)
{
free(pTest);
pTest = NULL;
}
}
“版本1”中定义了一个person结构体,其中有几点需要说明一下:
1. 数组的长度一般习惯采用宏定义,方便后续项目较大时的修改;
2. 程序中尽量有一些错误控制,即考虑完善一些,例如上面的内存分配是否成功的判断以及函数调用后是否达到预期目的的判断;
3. 使用typedef简化定义;
4. 使用NULL == pPs,而不是pPs == NULL是一种良好的编程习惯,这在有些面试中也会被问到,之所以这样是因为当将“==”误写成“=”时,前者直接编译出错,后者则可以运行,这种隐蔽的错误很可能导致整个项目出现混乱,这种BUG有时候也很难被发现,所以这个习惯很重要,一句话就是“判断时,等号左边写常量,等号右边写变量,防止少写一个等号带来的严重BUG”;
5. 最后一定记得使用完指针要释放内存,否则会出现内存泄露等问题。
回到我们的主题,“版本1”中出现了一个严重的错误,并且很难被发现,编译时不会报错,但是运行时就会出现错误,出现这个情况建议大家可以单步调试看看这个指针到底有没有被分配到内存。
为什么会出错呢?我们来分析一下,我们要搞清楚一个问题:指针变量作为函数参数传递的时候,当你需要修改指针变量本身的地址值的时候,相当于我们常说的“值传递”,大家都知道,值传递是不会改变实参的值的,常见的例子就是用一个函数交换两个变量的值,如果大家对这一点还有疑问,建议大家可以查看基本的教材中函数形参和实参相关的章节,会有详细的介绍。说回来,对于需要修改指针的地址值的函数实参,我们相当于是“值传递”,所以呢,我们调用mallocAndInit()函数后实际上main函数中的指针pPs还是未分配内存的指针,这个时候我们调用printf函数去访问这个指针的内容肯定是错误的,因为这相当于访问了一个未知的内存,这是一种很危险的操作。要引起重视。
总结来说就是一句话,当你需要通过函数修改函数参数中指针参数的地址值时,这就相当于是指针的值传递操作,和普通的变量值传递是一样的结果,而当你只是通过函数访问或修改函数参数中指针参数的内容(例如修改person中的age的值)时,这就相当于地址传递,实参结果可以被修改并传递出函数。
下面给出两种解决方案:
1. 在函数内部对指针分配内存后 ,通过返回该指针来获得分配后的地址,见“版本2”;
2. 给函数传递二级指针来修改一级指针的地址值,这样会比较麻烦,见“版本3”。
// 版本2
#include
#include
#include
#define LEN 20
typedef struct person
{
char name[LEN];
int age;
}person;
// 返回NULL表示失败,返回地址表示成功
person* mallocAndInit(person *pPs)
{
pPs = (person*)malloc(sizeof(person));
if (NULL == pPs)
{
// 内存分配失败
fprintf(stderr, "Malloc failed.\n");
return NULL;
}
// 初始化
pPs->age = 0;
strcpy(pPs->name, "");
return pPs; // 注意这里的变化!!!
}
void main()
{
person *pTest = NULL;
// 注意这里的变化!!!
if (!(pTest = mallocAndInit(pTest)))
{
fprintf(stderr, "Error.\n");
exit(1);
}
// 这里不会再出错
printf("The person's name is %s, age is %d.\n", pTest->name, pTest->age);
// 释放内存并还原
if (pTest != NULL)
{
free(pTest);
pTest = NULL;
}
}
// 版本3
#include
#include
#include
#define LEN 20
typedef struct person
{
char name[LEN];
int age;
}person;
// 返回0表示失败,返回1表示成功
int mallocAndInit(person **ppPs)
{
*ppPs = (person*)malloc(sizeof(person));
if (NULL == *ppPs)
{
// 内存分配失败
fprintf(stderr, "Malloc failed.\n");
return 0;
}
// 初始化
(*ppPs)->age = 0;
strcpy((*ppPs)->name, "");
return 1;
}
void main()
{
person *pTest = NULL;
person **pptest = &pTest; // 二级指针要指向准备使用的一级指针!!!
// 这里通过二级指针获取到了函数内部为一级指针分配的内存,即改变了一级指针的地址
if (!mallocAndInit(pptest))
{
fprintf(stderr, "Error.\n");
exit(1);
}
// 这里不会再出错
printf("The person's name is %s, age is %d.\n", pTest->name, pTest->age);
// 释放内存并还原
if (pTest != NULL)
{
free(pTest);
pTest = NULL;
pptest = NULL;
}
}
总之,C语言中的指针确实比较令人头疼,经常会因为指针出现各种各样的问题,所以大家要时刻保持警惕,遇到指针要高度细心。
上面的程序大家可以直接复制粘贴到VS中,可以直接运行,都是经过我验证后才发出来的,有不懂的地方欢迎大家留言,我看到后会及时回复,这是第一篇写得比较多,比较仔细,还希望大家可以细细品味,有需要交流的朋友欢迎留言,谢谢。