C/C++中的指针操作是一个令人抓狂的问题,这几天在温习林锐的《高质量C++C编程指南》,里面的内存管理这一章对我受益匪浅。看到下面的一段内容,不禁和作者提出相同的疑问:该程序不出错是因为编译器的原因吗?并在网上查找相关资料。
源码1:
#include <iostream> using namespace std; class A { public: void Func(void) { cout<<"Func of class A"<<endl; } }; void Test(void) { A *p; { A a; p = &a; // 注意 a 的生命期 } p->Func(); // p是“野指针” } int main() { Test(); return 0; }
为了探索这个问题,我分别在CodeBlocks、VC++6.0和VS2010上运行这段代码,发现并无错误,所以,这个并非编译器的原因,那么到底是什么原因呢?
1、我们知道在Test函数中内部用花括号括起来的部分是一个局部作用范围,因此,该语句块拥有局部的作用域,在里面定义的局部对象a,是一个存放在栈中的自动变量,一旦离开了这个作用域(离开花括号),就不复存在了。可以通过简单的单步调试,跟踪对象a的变化,实践证明,这个想法是对的。
2、一般来说,一个对象所占的空间大小只取决于该对象中数据成员所占的空间,而与成员函数无关。通过下面一段代码可以验证
源码2:
#include <iostream> using namespace std; class A { int i; char chr; }; class B { int i; char chr; void fun() { int j = 1; cout<<j<<endl; } }; int main() { A a; B b; cout<<"sizeof A = "<<sizeof(a)<<endl; cout<<"sizeof B = "<<sizeof(b)<<endl; return 0; }
,在类A和类B中都有一个int和char类型的成员变量,根据内存对齐,取其中较大的int变量的字节数4作为倍数,所以sizeof(A) = 2 * 4 = 8字节。尽管类B中有成员函数fun,但是该对象b所占用空间并没有计算fun函数的空间。
3、那么成员函数放在哪里?对象是怎么调用成员函数的呢?
先来看一个图片:
通过图片可以看出,对象实例的成员函数(非静态成员函数)有一个公用函数代码存储空间,对象通过this指针调用公用函数代码中的成员函数,这样可以减少存储空间的使用。
小结:1) 不论成员函数在类内定义还是在类外定义,成员函数的代码段都用同一种方式存储;
2) 函数代码是存储在对象空间之外的;
3) 通常所说的“某某对象的成员函数”,是从逻辑的角度而言的;而成员函数的存储方式,是从物理的角度而言的,二者是不矛盾的。
4、返回原来讨论的问题,既然成员函数存放在公用函数代码段里面,而在源码1中,指针p 是保存了在花括号里面的对象a的地址的,对象a销毁了,我们不能使用变量a,但是它的地址是不会改变的,编译器只是销毁了对象a这个变量,并没有销毁对象a原来所在存储区域(当然是在栈里面的存储区域)中的成员变量和成员函数,所以,这里得到的指针p,还是指向花括号中定义的对象a的首地址,这就是ptr就是原来的对象a的this指针,当使用p->Func();语句调用成员函数的时候,自然就可以通过this指针在公用函数代码段中找到对应的成员函数,执行该函数(我猜想在公用函数代码段中应该维护了一个this指针与成员函数的对照表)。
5、错误的调用返回了正确的结果,着实令人头痛。显然,应该坚决杜绝这样的调用方法。对于野指针(不是NULL指针,是指向被释放的或者访问受限内存的指针),我们使用delete或者free释放存储空间的时候,把指针p赋值为NULL是一个好习惯,这样,通过if(p != NULL)语句就可以判断该指针是否是合法的。
扩展:
上述研究的内容是针对一个对象实例的,假设是一个int类型或者字符串类型,结果是否一样呢。来看一段代码。
源码3:
#include <iostream> using namespace std; char *getstr(int flag) { if(flag == 1) { char *str = "12345";//字符串"12345"存放在文字常量区 return str; } else { char str[] = "ABCDE";//字符数组 return str; } } int *getint() { int i = 2014; return &i; } int main() { //字符串类型测试 cout<<"字符串类型测试(字符串):"<<getstr(1)<<endl; cout<<"字符串类型测试(字符数组):"<<getstr(2)<<endl; //char *str = getstr(2);//使用指针str保存返回的getstr(2)指针,打印出来的是乱码,说明指针没有正确返回 //使用getstr(2)打印确实正常的,如下: for(int i = 0; i < 6; i++) { cout<<"str"<<i<<": "<<getstr(2)[i]<<endl; } cout<<endl<<"****************分割线******************"<<endl<<endl; //整型类型测试 cout<<"整型类型测试:"<<*getint()<<endl; return 0; }
运行结果(VC++6.0):
1) 对于整型变量来说,与之前的说法是一致的,getint方法中变量i在退出函数时便不能使用了,但是返回的地址所指向的存储区域内容并没有改变依旧是2014;
2) char *str = "12345";这种定义方式的字符串"12345"是存放在文字常量区的,该字符串不能修改,其生命周期和整个程序的生命周期一样长,但是依旧不建议这么编写代码。
3) char str[] = "ABCDE";这里定义的是一个字符数组,这是一种直观的初始化方式,注意这样不代表字符串"ABCDE"是存放在文字常量区,而是等价于char str[] = {'A', 'B', 'C', 'D', 'E', '\0'};;离开了getstr函数,字符数组str便不复存在,这里返回的指针不一定是原先字符串"ABCDE"的首地址,这里打印出来的地址是乱码,但是单个取值却没有问题。个人猜想:数组名和指针不是一个概念,getstr函数中分配字符数组空间的时候,还要存储数据的类型等信息,返回的也是数组名str,然后离开了getstr函数,数组str不存在了,返回的“数组名”是没有意义的,因此打印出来的是乱码。
总结:
离开了作用域的变量,变量被销毁,变量所在存储区域的内容并没有被系统自动实时释放和回收;也许该存储区域只是做了“可分配”标记。
注:不能因为系统没有实时回收这些变量而存在侥幸心理,熟练使用指针并对内存管理有清晰的理解才是王道。
C/C++中的内存管理学问太深,这篇博客是小弟个人拙见,也许漏洞百出,还是希望各路高手指教,不胜感激!
本文版权归作者和CSDN所有,欢迎转载,转载请注明出处:http://blog.csdn.net/thebestdavid/article/details/23165597