Chinaunix首页 | 论坛 | 博客
  • 博客访问: 2161452
  • 博文数量: 361
  • 博客积分: 10828
  • 博客等级: 上将
  • 技术积分: 4161
  • 用 户 组: 普通用户
  • 注册时间: 2010-01-20 14:34
文章分类

全部博文(361)

文章存档

2011年(132)

2010年(229)

分类: C/C++

2010-02-27 10:33:04

本人编程时间虽然不长。但现在觉得一些问题不能光从语法角度理解,能从在内存中是如何的角度考虑,似乎问题想得更清楚,更容易理解。那么,我们的程序从编译到运行是怎样一个过程呢。编译器把代码中的地址转换为可迁移的地址(如从这个模块开始的第14个byte处),链接或加载时将该地址转换为绝对地址(如0x70129)。而让一个应用程序运行,就是启动一个进程,操作系统赋予它地址空间,将相应的代码和数据加载到地址空间。进程的地址空间大致如下图所示:
       
      代码放到Text段内,全局和静态变量放到Data段内。程序的执行,就是在执行代码段中的代码,如果需要建临时变量等,就在Stack段中建立。上图没有反映多线程的情况。多线程会共享一个代码段和数据段,但是每个线程会有自己的堆栈。
      首先,编程的人通常都遇到过的,如访问冲突(TestMemory.exe 中的 0x004169bd 处未处理的异常: 0xC0000005: 写入位置 0x00000000 时发生访问冲突),堆栈破坏(Run-Time Check Failure #2 - Stack around the variable 'nArrTest' was corrupted.)。第一个是因为不是进程所有的地址空间我们都是可以访问的,第二个是因为某个变量的赋值超出了其合理的范围。要出现上述的错误,最少用两行代码就可以了,而且有多种方式。但如果我们是负责一个大型程序的一部分,修改别人的程序,或者自己的程序与别人的程序有某种方式的交互,有时候就不好查出错误在哪。但只要出现了就说明我们操作了非我们定义或声明要使用的内存。那么先在局部范围检查写内存的地方(赋值,拷贝等处),然后再检查一些成员变量,全局变量的写入,调用的类中成员函数的使用。
       不过,我觉得最好的方法来避免这些问题,还是养成一些好的习惯。比如说用宏,避免修改数组长度后,没有把程序中所有相应位置的都改过来;再有如使用指针前,检查是否为空,如函数开始处检查传进来的参数;与别的程序交互,或较大程序中,公用的一些宏放入一个文件中,等等。这其中,也要注意编译器升级后,有些数据类型长度变化。我就遇到过程序用2005编译后,出现了问题,是因为有个类型位数扩展了一倍。
       再有,以前看见介绍强制类型转换时,总看见有介绍各种转换的语法规则。但是我觉得,一切规则恐怕都源于,各种类型的数据是怎样的一个内存结构,还有就是未赋值的内存不一定是0。如果两个数据类型的所用的内存大小不同,那么转换时,就必然要考虑了。一个原本用四字节存放的数据能放到一个两字节的数据中吗?除非那四个字节没有都使用,前面的两个字节都是0。那么两个字节的数据转换到四个字节的数据就一定没问题了吗?还要看多出的那两个字节,编译器默认是怎么处理的。
       当然四个字数据节对四个字节字节数据,也不一定就合理,特别是数据类型之间的指针转换,因为可能会改变了编译器对指针所指向的内存单元的解释方式,如:
float fTest = 1234.56;
int *pnTest = (Int*)&fTest;
int nTest = 100;
float *pdwTest = (float*) &nTest;
       如果是值转换,考虑两者范围是否一样,符号是否一样,当然这些编译器会给出警告。就看你注意不注意。
       这一点不只对简单数据类型是如此的,对于复杂数据类型也是如此的。如基类和派生类之间的转换。定义的类对象在内存中就是由其成员变量组成,派生类比基类多定义了成员对象,则在内存中也多占了内存,而且基类的成员变量是在派生类成员变量之前的(在内存中)。所以基类指针可以指向派生类对象,因为内存中确实有一块可以认为是基类对象的内存,但派生类指针指向基类指针则不同了,内存里是一块合理的基类对象,和一块未知的内存。指针之间强制转换可以,但最好不是让编译器隐式转换(如:函数参数处),而用显式转换,并明确此后使用的是合法的内存。
 
        这里还要说明一下定义了虚函数的类对象。定义了虚函数的类对象,则在内存中其他成员对象前面加一个虚函数指针表(vptr),这里保存着不同类对象拥有的虚函数指针。下图是调试时的监视窗口,很好的反应这一内存中的现象。
 
       那么这里就要说明一下了,指针或引用可以引用类的对象或任意子类的对象。对象自身“很清楚”它实际上是哪个类的成员,所以只要方法用virtual声明,就会调用正确的方法(因为有虚函数表)。对非指针,非引用的对象,则不具有这个特性。这是因为,对于指针(引用类似于指针,但它没有明确的内存形式),它就是一个四字节的值,这个值指向的是一块内存,那么那块内存放的是基类对象则有基类的虚函数表(调用基类的函数),放的是派生类则有派生类的虚函数表(调用派生类的函数)。你定义多少个指针,不管这些指针是什么类型,你要给这些指针赋值,也是把一个已存在的对象的地址赋给它,你再做什么指针类型的强制转换也不会改变这个对象的内容。但是如果你定义的是非指针(引用类型),则不同,如下代码:
Sub mysub;//派生类
Super mysuper = mysub;//基类
mysuper.someMethod();//该函数是一个虚函数
这段代码调试时的监视图在上面已经给出,可以看出这时两个对象中虚函数表不同了。因为定义的Super是一个对象,不是指针,那么会分配新的内存,这块内存是按Super的内存结构分配(虚函数表也按Super类型填充),而此后再用赋值或复制构造函数时,都不会改变虚函数表中内容,只会改变显式声明的成员变量。因此这时它只会调用基类的函数。
       还有可以思考的就是,应用程序和动态库加载的方式。运行程序时,也就是开始一个进程,操作系统是先加载文件到内存(内存映射文件),创建主线程后再执行程序的代码。因此,我们遇到应用程序不能启动,那么可以调试看一下是否执行了App类的InitInstance 中的代码,执行了看是哪里执行失败,如果没有执行则说明在加载文件时就有问题,那么看看有没有哪个应用程序需要的dll没有加载成功,因为加载文件时除了应用程序还有其附属的动态库。      
       再有,就是线程同步的问题了。当然线程同步的方式有很多。但为什么要做同步呢,因为同属一个进程的线程共享进程的地址空间。如果我们有一个合理的内存地址就可以访问该内存,一块内存被两个线程同时写,或一个线程想知道某个特定时候某块内存的值,则需要线程同步了。
        还有就是内核对象了,因为在程序运行过程中,就是CPU和内存在起主要作用,因此进程,线程,线程同步的一些对象等这些对象,就只能在内存中开出一部分来维护他们的主要信息。而通常这些对象都是很关键的东西,如果允许用户随意访问,操作系统就很难管理了,所以这些对象的修改都要用系统提供的一些函数,而不能说获得一个指向他们的指针就任意修改.
        近日做了一个测试,主要测试函数的调用速度。一个测试是在一个类的某个函数内调用这个类的另一个函数,另一个测试是在一个应用程序内动态加载一个dll,获得这个dll的导出函数地址后,调用dll里的函数。这两个测试,都是执行该函数10000次后取平均时间,都用多媒体计时器保证精度。结果发现这两者的速度差别很小。后来想了想,也对。dll只要load后就在进程的地址空间里,获得了其导出函数的地址也就明确了其在进程地址空间的地址,那和第一个测试一样了,都是执行进程地址空间某处地址上的代码。如果说耗时间的话只在load动态库时耗时间。对于静态加载动态库,也一样,只不过静态加载是在应用程序刚开始运行时加载,并通过在dll的输出节中找到输出函数的地址。
 
阅读(1274) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~