问题的背景:
之前在写c语言实验课作业的时候,老师布置的是用struct类型来创建一个Student变量,包括姓名、学号、性别和成绩,并初始化一个Student类的数组然后随机化学生的初始成绩,来完成对学生成绩的排序和信息的输出。
我想作为既然把学生作为一个struct类型来看待,并且还要实现学生成绩的排序与信息展示,那么完全可以把Struct类型升级成C++里的那种Class类型,让Student作为一个类拥有自己的数据成员和成员函数,即一个学生可以拥有姓名、学号、性别和成绩这四种数据;同时可以有显示所有信息和对成绩排序的方法。于是我想到了用函数指针来实现struct类型中的成员函数。
问题的发生:
在造构造函数和析构函数的轮子时,出现了严重的问题。为了近似地实现构造函数的功能(其实没什么必要2333,因为这点数据量等程序结束后交给操作系统一次性回收完全没啥问题,纯粹为了实现一下本人的好奇心),我选择把Student的数据成员全部设置成对应的指针变量,然后用构造函数给这些指针变量申请堆内存,最后在析构函数内释放这些堆内存。但是写下来之后运行的时候vs报了指针为空的异常。
最后排查下来以后,才发现是构造函数出了问题,在构造函数内申请的堆内存并没有成功地返回到主函数中,进而这些数据成员指针在初始化为空指针后就算执行了构造函数,指针也仍然为空。进一步排查后发现,是指针传递过程中犯了一些基础性的错误,导致本来应该使用地址传递的地方使用了值传递,因此构造函数内申请堆内存后主函数的指针并未因此指向之前开辟的堆内存。
对当时情形的代码简化及对比:
//此处代码里,若把堆内存申请方式从malloc/free换成c++中的new/delete,结果也是一样的
#include <cstdio>
#include <cstdlib>
void ApplyHeapMemory1(int* _p1) //传指针,无返回值
{
_p1 = (int*)malloc(3 * sizeof(int));
for (int i = 0; i < 3; i++)
{
_p1[i] = i;
}
}
int* ApplyHeapMemory2(int* _p2) //传指针,有返回值
{
_p2 = (int*)malloc(3 * sizeof(int));
for (int i = 0; i < 3; i++)
{
_p2[i] = i;
}
return _p2;
}
void ApplyHeapMemory3(int** const _p3) //传指针的指针,无返回值
{
*_p3 = (int*)malloc(3 * sizeof(int));
for (int i = 0; i < 3; i++)
{
(*_p3)[i] = i;
}
}
void display(int* _p)
{
if (_p != NULL)
{
for (int i = 0; i < 3; i++)
{
printf("%d\n", _p[i]);
}
}
else
{
printf("Null pointer!\n");
}
}
void destruct(int* _p)
{
if (_p != NULL)
{
free(_p);
}
}
int main()
{
int* ptr1 = NULL, * ptr2 = NULL, * ptr3 = NULL;
ApplyHeapMemory1(ptr1);
display(ptr1); //输出“Null pointer!”,如果直接调用这个指针会报nullptr的异常
ptr2 = ApplyHeapMemory2(ptr2);
display(ptr2); //正常输出0 1 2;可以正常修改指针指向内存中存储的数据
ApplyHeapMemory3(&ptr3);
display(ptr3); //同上,正常输出0 1 2;可以正常修改指针指向内存中存储的数据
destruct(ptr1);
destruct(ptr2);
destruct(ptr3);
return 0;
}
这里我犯的错误恰好是想用指针作为参数传到构造函数内,却忽略了形参和实参在结合时发生的传参过程。一般而言,函数参数在形实结合时发生的是值传递,即实参把自己的值拷贝给了函数声明时用于接收参数的形参,之后流程执行到函数体内后形参和实参就“断开了联系”,被调函数体内对形参的操作无法影响调用函数中实参的状态,此谓“值传递”。与之对应的就是地址传递,即把实参对应的地址传给函数的形参(此时形参为指针类型),形参接受了实参的地址之后,通过对指针的寻址操作找到形参所存地址对应的变量,然后就能通过指针运算符“ * ”来对实参的值进行操作,从而实现改变实参的目的。
这里本质上而言,地址传参也是一种值传递,只不过这里传递的值变成了地址,其实过程仍旧是形参把自己的地址值拷贝给了实参(可以用取地址操作检验一下)。所以ApplyHeapMemory1这个函数出问题的原因,就是应该采用地址传递的地方却使用了值传递,作为指针变量的形参_p1只是拷贝了ptr1的值,在拷贝这个值以后又存了新开辟堆内存的地址,最后形参_p1的生命周期随着函数体执行的结束而结束,而主函数中ptr1仍被置空。显然,ApplyHeapMemory1所实现的和我预想的,让传进去的ptr1去指向一块申请出来的堆内存,结果相去甚远。
感想:
这个问题我最后采用了函数ApplyHeapMemory3的方法解决了。同时在自己造class的过程中,除了这一个与指针相关的问题,我还出过:
- 向函数体内传入指针,并让指针指向被调函数中的局部变量,函数调用结束后指针成为了野指针,出现了奇奇怪怪的输出结果
- 申请动态数组以后对堆内存越界写入,报异常
- 指针先是指向开辟的堆内存,尔后指向栈上的变量,最后还对指针进行了free,报异常,并且异常点跳到了反汇编的页面,出现了int 3中断
尽管有些坑是老师上课强调过的,但在实践中有时候一不留神又会犯下这些错误。踩过这些坑后,才发现c的指针虽然灵活,也能实现各种神奇的操作,但是危险性和不确定性特别大,隐患多多(毕竟vs静态检查不报错,而且编译的时候也不一定报错),今后在使用指针的时候一定要从所指对象(特别是对象的生存周期)、越界检查进行仔细检查。听说c++中针对指针安全性问题,推出了一些智能指针类,目前还没学到和接触到,以后接触到了再细品。
新手上路,请多关照,如有错误或者不同的想法,欢迎讨论:D