论坛: 编程破解 标题: C程序优化 复制本贴地址    
作者: seny11 [seny11]    论坛用户   登录
C程序优化 - 算法篇
liyuming1978(原作)

I.从小处说起:

先说说一些小地方先:

① 比如n/2写为n>>1这个是常用的方法,不过要注意的是这两个不是完全等价的!因为:如果n=3的话,n/2=1;n>>1=1;但是,如果n=-3的话,n/2=-1;n>>1=-2所以说在正数的时候,他们都是向下取整,但是负数的时候就不一样了。(在JPG2000中的整数YUV到RGB变换一定要使用>>来代替除法就是这个道理)

② 还有就是a=a+1要写为a++; a=a+b要写为a+=b(估计一般用VB的才会写a=a+1 :P)

③ 将多种运算融合:比如a[i++];就是先访问a[i],再令i加1;从汇编的角度上说,这个确实是优化的,如果写为a[i],和i++的话,有可能就会有两次的对i变量的读,一次写(具体要看编译器的优化能力了),但是如果a[i++]的话,就一定只读写i变量一次。不过这里有一个问题要注意:在条件判断内的融合一定要小心,比如:(idct变换中的0块判断,陈王算法)

if (!((x1 = (blk[8*4]<<8)) | (x2 = blk[8*6]) | (x3 = blk[8*2]) | (x4 = blk[8*1]) | (x5 = blk[8*7]) | (x6 = blk[8*5]) | (x7 = blk[8*3])))

  在条件判断中融合了赋值语句,但是实际上如果条件为真的话,是不需要这些赋值语句的,也就是说当条件真的时候,多了一些垃圾语句,这些是在h263源码上的问题,虽然这些垃圾语句使得计算0块的时候,时间增加了30%,但是由于idct仅仅占1%的时间,0块又仅仅30%~70%的时间,所以这些性能损失是没有什么关系的。(这是后来我用汇编改写源码的时候得到的结论)。这里也说明了,程序优化一定重点在最耗时的地方。对于不耗时的代码优化是没有太大的实用意义的。

II.以内存换速度:

  天下总是难有双得的事情,编程也是一样,大多数情况,速度同内存(或者是性能,比如说压缩性能什么的)是不可兼得的。目前程序加速的常用算法一个大方面就是利用查表来避免计算(比如在jpg有huffman码表,在YUV到RGB变换也有变换表)这样原来的复杂计算现在仅仅查表就可以了,虽然浪费了内存,不过速度显著提升,还是很划算的。在数据库查询里面也有这样的思想,将热点存储起来以加速查询。 现在介绍一个简单的例子,(临时想的,呵呵):比如,在程序中要经常(一定要是经常!)计算1000到2000的阶乘,那么我们可以使用一个数组a[1000]先把这些值算好,保留下来,以后要计算1200!的时候,查表a[1200-1000]就可以了。

III.化零为整

  由于零散的内存分配,以及大量小对象建立耗时很大,所以对它们的优化有时会很有效果,比如上一篇我说的链表存在的问题,就是因为大量的零散内存分配。现在就从一个vb的程序说起,以前我用vb给别人编小程序的时候,(呵呵,主要是用vb编程比vc快,半天就可以写一个)在使用MSFlexGrid控件的时候(就是一个表格控件),发现如果一行一行的增加新行,刷新速度十分的慢,所以我就每次增加100行,等到数据多到再加新行的时候,再加100行,这样就“化零为整”了,使用这样的方法,刷新的速度比原来快了n倍!其实这样的思想应用很多,如:程序运行的时候,其实就占用了一定的空间,后来的小块内存分配是先在这个空间上的,这就保证了内存碎片尽可能的少,同时加快运行速度。 IV.条件语句或者case语句将最有可能的放在前面

优化效果不明显。想得到就用吧,想不到就算了。

V.为了程序的可读性,不去做那些编译器可以做的或者优化不明显的处理:

  这个是很重要的,一个普通程序的好坏,主要是它的可读性,可移植性,可重用性,然后才是它的性能。所以,如果编译器本身可以帮助我们优化的话,我们就没有必要写那些大家都不怎么看得懂的东西。比如a=52(结束)-16(起始);这样写可能是因为在别人读程序的时候,一下就明白了a的含义。我们不用写为a=36,因为编译器是会帮我们算出来的。

IV.具体情况具体分析:

  具体情况具体分析,这是放之四海而皆准的真理。没有具体的分析,就不能针对问题灵活应用解决的办法。下面我就说说分析的方法。即如何找到程序的耗时点:(从最简单的办法说起,先说明一个函数GetTickCount(),这个函数在头尾各调用一次,返回值相减就是程序的耗时,精确到1ms)

① 对于认为是比较耗时的函数,运行两次,或者将函数内部的语句注释掉(要保证程序可以运行),看看多(或者少了)多少时间。这个办法简单不精确。 ② 每个地方都用GetTickCount()函数测试时间,注意GetTickCount()只能精确到ms。一般的小于10ms就不太精确了。

③ 使用另外一个函数QueryPerformanceCounter(&Counter)和QueryPerformanceFrequency(&Frequency),前面计算cpu时钟周期,后面是cpu频率相除就是时间。不过如果你要精确到这一步的话,建议将进程设置为最高级别,防止它被阻塞。

  最后讲讲我处理的一个程序:程序要求我忘了,反正里面有一个函数,函数里面有一个大的循环,循环内部的处理比较耗时。结果最初程序表现出来的状况是开始还很快,越到后面越慢;我在跟踪程序中变量的时候,发现最初的循环在循环几次后就跳出了,而后面的循环次数越来越多。找到了为什么慢的原因,就可以对症下药了,我的处理是每次循环不是从头开始,而是从上一次循环跳出的地方开始左右循环(因为可能下一次循环跳出的地方别上一次的小,所以也要遍历前面的),这样程序的速度在后面也很快了。我讲这个的道理就是在实际运用中,要具体的分析程序慢的真正原因,才能达到最佳的优化效果。

------------------
C程序优化 - 内存篇
liyuming1978(原作)

I.优化数组的寻址

  在编写程序时,我们常常使用一个一维数组a[M×N]来模拟二维数组a[N][M],这个时候访问a[]一维数组的时候:我们经常是这样写a[j×M+i](对于a[j][i])。这样写当然是无可置疑的,但是显然每个寻址语句j×M+i都要进行一次乘法运算。现在再让我们看看二维数值的寻址,说到这里我们不得不深入到C编译器在申请二维数组和一维数组的内部细节上�D�D实际在申请二位数组和一维数组,编译器的处理是不一样的,申请一个a[N][M]的数组要比申请一个a[M×N]的数组占用的空间大!二维数组的结构是分为两部分的:

① 是一个指针数组,存储的是每一行的起始地址,这也就是为什么在a[N][M]中,a[j]是一个指针而不是a[j][0]数据的原因。

② 是真正的M×N的连续数据块,这解释了为什么一个二维数组可以象一维数组那样寻址的原因。(即a[j][i]等同于(a[0])[j×M+i])

  清楚了这些,我们就可以知道二维数组要比(模拟该二维数组的)一维数组寻址效率高。因为a[j][i]的寻址仅仅是访问指针数组得到j行的地址,然后再+i,是没有乘法运算的!

所以,在处理一维数组的时候,我们常常采用下面的优化办法:(伪码例子)

int a[M*N];
int *b=a;
for(…){
  b[…]=…;
  …………
  b[…]=…;
  b+=M;
}

  这个是遍历访问数组的一个优化例子,每次b+=M就使得b更新为下一行的头指针。当然如果你愿意的话,可以自己定义一个数组指针来存储每一行的起始地址。然后按照二维数组的寻址办法来处理一维数组。不过,在这里我建议你干脆就直接申请一个二维数组比较的好。下面是动态申请和释放一个二维数组的C代码。

int get_mem2Dint(int ***array2D, int rows, int columns) // h.263源代码
{
  int i;
  if((*array2D = (int**)calloc(rows, sizeof(int*))) == NULL) no_mem_exit(1);
  if(((*array2D)[0] = (int* )calloc(rows*columns,sizeof(int ))) == NULL) no_mem_exit(1);
  for(i=1 ; i<rows ; i++)
    (*array2D)[i] = (*array2D)[i-1] + columns ;
  return rows*columns*sizeof(int);
}

void free_mem2D(byte **array2D)
{
  if (array2D){
  if (array2D[0]) free (array2D[0]);
  else error ("free_mem2D: trying to free unused memory",100);
  free (array2D);
  } else{
    error ("free_mem2D: trying to free unused memory",100);
  }
}

  顺便说一下,如果你的数组寻址有一个偏移量的话,不要写为a[x+offset],而应该为 b=a+offset,然后访问b[x]。

  不过,如果你不是处理对速度有特别要求的程序的话,这样的优化也就不必要了。记住,如果编普通程序的话,可读性和可移值性是第一位的。


II.从负数开始的数组

  在编程的时候,你是不是经常要处理边界问题呢?在处理边界问题的时候,经常下标是从负数开始的,通常我们的处理是将边界处理分离出来,单独用额外的代码写。那么当你知道如何使用从负数开始的数组的时候,边界处理就方便多了。下面是静态使用一个从-1开始的数组:

int a[M];

int *pa=a+1;

  现在如果你使用pa访问a的时候就是从-1到M-2了,就是这么简单。(如果你动态申请a的话,free(a)可不要free(pa)因为pa不是数组的头地址)


III.我们需要链表吗

  相信大家在学习《数据结构》的时候,对链表是相当熟悉了,所以我看有人在编写一些耗时算法的时候,也采用了链表的形式。这样编写当然对内存的占用(似乎)少了,可是速度呢?如果你测试:申请并遍历10000个元素链表的时间与遍历相同元素的数组的时间,你就会发现时间相差了百倍!(以前测试过一个算法,用链表是1分钟,用数组是4秒钟)。所以这里我的建议是:在编写耗时大的代码时,尽可能不要采用链表!

  其实实际上采用链表并不能真正节省内存,在编写很多算法的时候,我们是知道要占用多少内存的(至少也知道个大概),那么与其用链表一点点的消耗内存,不如用数组一步就把内存占用。采用链表的形式一定是在元素比较少,或者该部分基本不耗时的情况下。

  (我估计链表主要慢是慢在它是一步步申请内存的,如果能够象数组一样分配一个大内存块的话,应该也不怎么耗时,这个没有具体测试过。仅仅是猜想 :P)

-----------------
C程序优化 - I/O篇
liyuming1978(原作)

  如果有文件读写的话,那么对文件的访问将是影响程序运行速度的一大因素。提高文件访问速度的主要办法有两个:一是采用内存映射文件,二是使用内存缓冲。下面是一组测试数据(见《UNIX环境高级编程》3.9节),显示了用18种不同的缓存长度,读1,468 ,802字节文件所得到的结果。

缓冲大小 用户CPU(秒) 系统CPU(秒) 时钟时间(秒) 循环次数(秒)
1 23.8 397.9 423.4 1 468 802
2 12.3 202.0 215.2 734 401
4 6.1 100.6 107.2 367 201
8 3.0 50.7 54.0 183 601
16 1.5 25.3 27.0 91 801
32 0.7 12.8 13.7 45 901
64 0.3 6.6 7.0 22 951
128 0.2 3.3 3.6 11 476
256 0.1 1.8 1.9 5 738
512
0.0 1.0 1.1 2 869
1 024 0.0 0.6 0.6 1 435
2 048 0.0 0.4 0.4 718
4 096 0.0 0.4 0.4 359
8 192 0.0 0.3 0.3 180
16 384 0.0 0.3 0.3 90
32 768 0.0 0.3 0.3 45
65 536 0.0 0.3 0.3 23
131 072 0.0 0.3 0.3 12

  可见,一般的当内存缓冲区大小为8192的时候,性能就已经是最佳的了,这也就是为什么在H.263等图像编码程序中,缓冲区大小为8192的原因(有的时候也取2048大小)。使用内存缓冲区方法的好处主要是便于移植,占用内存少,便于硬件实现等。下面是读取文件的C伪码:

int Len ;
BYTE buffer[8192] ;
ASSERT(buffer==NULL) ;
If buffer is empty
{
  Len=read(File,buffer,8192) ;
  If(len==0) No data and exit ;
}


  但是如果内存比较大的时候,采用内存映射文件可以达到更佳性能,并且编程实现简单。内存映射的具体使用说明见msdn October 2001中的Platform SDK:Documentation―Base Services―File Storage―File Mapping。

下面是一点建议:
① 内存映射文件不能超过虚拟内存的大小,最好也不要太大,如果内存映射文件接近虚拟内存大小的时候,反而会大大降低程序的速度(其实是因为虚拟内存不足导致系统运行效率降低),这个时候,可以考虑分块映射,但是我觉得如果这样,还不如直接使用内存缓冲来得直接一些。

② 可以将两种方法统一使用,如我在编大图像文件数据处理的时候(因为是Unix工作站,内存很大GB单位)使用了内存映射文件,但是为了最佳性能,也使用了一行图像缓存,这样在读取文件中数据的时候,就保证了仅仅是顺序读写(内存映射文件中,对顺序读写有专门的优化)。

③ 在写文件的时候使用内存映射文件要有一点小技巧:应该先创建足够大的文件,然后将这个文件映射,在处理完这个文件的时候,用函数SetFilePointer和SetEndOfFile来对文件进行截尾。

④ 对内存映射文件进行操作与对内存进行操作类似(使用起来就象数组一样),那么如果有大块数据读写的时候,切记使用memcpy()函数(或者CopyMemory()函数)


  总之,如果要使用内存映射文件,必须:1.处理的文件比较的小,2.处理的文件很大,但是运行环境内存也很大,并且一般在运行该程序的时候不运行其他消耗内存大的程序,同时用户对速度有特别的要求,而且对内存占用没有什么要求。如果以上两个条件不满足的时候,建议使用内存缓冲区的办法。





地主 发表时间: 04-05-02 23:20

回复: seny11 [seny11]   论坛用户   登录
轻松学指针 www.ptdot.net      指针实验室整理与维护


第一章。指针的概念

指针是一个特殊的变量,它里面存储的数值被解释成为内存里的一个地址。

要搞清一个指针需要搞清指针的四方面的内容:指针的类型,指针所指向的 类型,指针的值或者叫指针所指向的内存区,还有指针本身所占据的 内存区。让 我们分别说明。 先声明几个指针放着做例子: 例一: (1)int*ptr; (2)char*ptr; (3)int**ptr; (4)int(*ptr)[3]; (5)int*(*ptr)[4]; 如果看不懂后几个例子的话,请参阅我前段时间贴出的文章<<如何 理解c和c ++的复杂类型声明>>。

1。指针的类型。 从语法的角度看,你只要把指针声明语句里的指针名字去掉,剩下的部分就 是这个指针的类型。这是指针本身所具有的类型。让我们看看例一中各个指针的 类型: (1)int*ptr;//指针的类型是int* (2)char*ptr;//指针的类型是char* (3)int**ptr;//指针的类型是int** (4)int(*ptr)[3];//指针的类型是int(*)[3] (5)int*(*ptr)[4];//指针的类型是int*(*)[4] 怎么样?找出指针的类型的方法是不是很简单?

2。指针所指向的类型。 当你通过指针来访问指针所指向的内存区时,指针所指向的类型决定了编译 器将把那片内存区里的内容当做什么来看待。 从语法上看,你只须把指针声明语句中的指针名字和名字左边的指针声明符 *去掉,剩下的就是指针所指向的类型。例如: (1)int*ptr;//指针所指向的类型是int (2)char*ptr;//指针所指向的的类型是char (3)int**ptr;//指针所指向的的类型是int* (4)int(*ptr)[3];//指针所指向的的类型是int()[3] (5)int*(*ptr)[4];//指针所指向的的类型是int*()[4] 在指针的算术运算中,指针所指向的类型有很大的作用。 指针的类型(即指针本身的类型)和指针所指向的类型是两个概念。当你对C越 来越熟悉时,你会发现,把与指针搅和在一起的"类型"这个概念分成"指针的 类型"和"指针所指向的类型"两个概念,是精通指针的关键点之一。我看了不 少书,发现有些写得差的书中,就把指针的这两个概念搅在一起了,所以看起书 来前后矛盾,越看越糊涂。

3。指针的值,或者叫指针所指向的内存区或地址。 指针的值是指针本身存储的数值,这个值将被编译器当作一个地址,而不是 一个一般的数值。在32位程序里,所有类型的指针的值都是一个32位整数,因为 32位程序里内存地址全都是32位长。 指针所指向的内存区就是从指针的值所代表的那个内存地址开始,长度为si zeof(指针所指向的类型)的一片内存区。以后,我们说一个指针的值是XX,就相 当于说该指针指向了以XX为首地址的一片内存区域;我们说一个指针指向了某块 内存区域,就相当于说该指针的值是这块内存区域的首地址。 指针所指向的内存区和指针所指向的类型是两个完全不同的概念。在例一中 ,指针所指向的类型已经有了,但由于指针还未初始化,所以它所指向的内存区 是不存在的,或者说是无意义的。 以后,每遇到一个指针,都应该问问:这个指针的类型是什么?指针指向的 类型是什么?该指针指向了哪里? 4。指针本身所占据的内存区。 指针本身占了多大的内存?你只要用函数sizeof(指针的类型)测一下就知道 了。在32位平台里,指针本身占据了4个字节的长度。 指针本身占据的内存这个概念在判断一个指针表达式是否是左值时很有用。


第二章。指针的算术运算

指针可以加上或减去一个整数。指针的这种运算的意义和通常的数值的加减 运算的意义是不一样的。例如: 例二: 1。Chara[20]; 2。Int*ptr=a; … … 3。Ptr++; 在上例中,指针ptr的类型是int*,它指向的类型是int,它被初始化为指向整 形变量a。接下来的第3句中,指针ptr被加了1,编译器是这样处理的:它把指针 ptr的值加上了sizeof(int),在32位程序中,是被加上了4。由于地址是用字节做 单位的,故ptr所指向的地址由原来的变量a的地址向高地址方向增加了4个字节。 由于char类型的长度是一个字节,所以,原来ptr是指向数组a的第0号单元开始的 四个字节,此时指向了数组a中从第4号单元开始的四个字节。 我们可以用一个指针和一个循环来遍历一个数组,看例子: 例三: 例三: intarray[20]; int*ptr=array; … //此处略去为整型数组赋值的代码。 … for(i=0;i<20;i++) { (*ptr)++; ptr++; } 这个例子将整型数组中各个单元的值加1。由于每次循环都将指针ptr加1,所 以每次循环都能访问数组的下一个单元。 再看例子: 例四: 1。Chara[20]; 2。Int*ptr=a; … … 3。Ptr+=5; 在这个例子中,ptr被加上了5,编译器是这样处理的:将指针ptr的值加上5 乘sizeof(int),在32位程序中就是加上了5乘4=20。由于地址的单位是字节,故 现在的ptr所指向的地址比起加5后的ptr所指向的地址来说,向高地址方向移动了 20个字节。在这个例子中,没加5前的ptr指向数组a的第0号单元开始的四个字节 ,加5后,ptr已经指向了数组a的合法范围之外了。虽然这种情况在应用上会出问 题,但在语法上却是可以的。这也体现出了指针的灵活性。 如果上例中,ptr是被减去5,那么处理过程大同小异,只不过ptr的值是被减 去5乘sizeof(int),新的ptr指向的地址将比原来的ptr所指向的地址向低地址方 向移动了20个字节。

总结一下,一个指针ptrold加上一个整数n后,结果是一个新的指针ptrnew, ptrnew的类型和ptrold的类型相同,ptrnew所指向的类型和ptrold所指向的类型 也相同。Ptrnew的值将比ptrold的值增加了n乘sizeof(ptrold所指向的类型)个字 节。就是说,ptrnew所指向的内存区将比ptrold所指向的内存区向高地址方向移 动了n乘sizeof(ptrold所指向的类型)个字节。 一个指针ptrold减去一个整数n后,结果是一个新的指针ptrnew,ptrnew的类 型和ptrold的类型相同,ptrnew所指向的类型和ptrold所指向的类型也相同。Pt rnew的值将比ptrold的值减少了n乘sizeof(ptrold所指向的类型)个字节,就是说 ,ptrnew所指向的内存区将比ptrold所指向的内存区向低地址方向移动了n乘siz eof(ptrold所指向的类型)个字节。

第三章。运算符&和*

这里&是取地址运算符,*是…书上叫做"间接运算符"。 &a的运算结果是一个指针,指针的类型是a的类型加个*,指针所指向的类型 是a的类型,指针所指向的地址嘛,那就是a的地址。 *p的运算结果就五花八门了。总之*p的结果是p所指向的东西,这个东西有这 些特点:它的类型是p指向的类型,它所占用的地址是p所指向的地址。 例五: inta=12; intb; int*p; int**ptr; p=&a;//&a的结果是一个指针,类型是int*,指向的类型是int,指向的地址 是a的地址。 *p=24;//*p的结果,在这里它的类型是int,它所占用的地址是p所指向的地 址,显然,*p就是变量a。 ptr=&p;//&p的结果是个指针,该指针的类型是p的类型加个*,在这里是int **。该指针所指向的类型是p的类型,这里是int*。该指针所指向的地址就是指针 p自己的地址。 *ptr=&b;//*ptr是个指针,&b的结果也是个指针,且这两个指针的类型和所 指向的类型是一样的,所以用&b来给*ptr赋值就是毫无问题的了。 **ptr=34;//*ptr的结果是ptr所指向的东西,在这里是一个指针,对这个指 针再做一次*运算,结果就是一个int类型的变量。

第四章。指针表达式。

一个表达式的最后结果如果是一个指针,那么这个表达式就叫指针表达式。 下面是一些指针表达式的例子: 例六: inta,b; intarray[10]; int*pa; pa=&a;//&a是一个指针表达式。 int**ptr=&pa;//&pa也是一个指针表达式。 *ptr=&b;//*ptr和&b都是指针表达式。 pa=array; pa++;//这也是指针表达式。 例七: char*arr[20]; char**parr=arr;//如果把arr看作指针的话,arr也是指针表达式 char*str; str=*parr;//*parr是指针表达式 str=*(parr+1);//*(parr+1)是指针表达式 str=*(parr+2);//*(parr+2)是指针表达式

由于指针表达式的结果是一个指针,所以指针表达式也具有指针所具有的四 个要素:指针的类型,指针所指向的类型,指针指向的内存区,指针自身占据的 内存。 好了,当一个指针表达式的结果指针已经明确地具有了指针自身占据的内存 的话,这个指针表达式就是一个左值,否则就不是一个左值。 在例七中,&a不是一个左值,因为它还没有占据明确的内存。*ptr是一个左 值,因为*ptr这个指针已经占据了内存,其实*ptr就是指针pa,既然pa已经在内 存中有了自己的位置,那么*ptr当然也有了自己的位置。

第五章。数组和指针的关系

如果对声明数组的语句不太明白的话,请参阅我前段时间贴出的文章<<如何 理解c和c++的复杂类型声明>>。 数组的数组名其实可以看作一个指针。看下例: 例八: intarray[10]={0,1,2,3,4,5,6,7,8,9},value; … … value=array[0];//也可写成:value=*array; value=array[3];//也可写成:value=*(array+3); value=array[4];//也可写成:value=*(array+4); 上例中,一般而言数组名array代表数组本身,类型是int[10],但如果把a rray看做指针的话,它指向数组的第0个单元,类型是int*,所指向的类型是数 组单元的类型即int。因此*array等于0就一点也不奇怪了。同理,array+3是一个 指向数组第3个单元的指针,所以*(array+3)等于3。其它依此类推。

例九: 例九: char*str[3]={ "Hello,thisisasample!", "Hi,goodmorning.", "Helloworld" }; chars[80]; strcpy(s,str[0]);//也可写成strcpy(s,*str); strcpy(s,str[1]);//也可写成strcpy(s,*(str+1)); strcpy(s,str[2]);//也可写成strcpy(s,*(str+2)); 上例中,str是一个三单元的数组,该数组的每个单元都是一个指针,这些指 针各指向一个字符串。把指针数组名str当作一个指针的话,它指向数组的第0号 单元,它的类型是char**,它指向的类型是char*。 *str也是一个指针,它的类型是char*,它所指向的类型是char,它指向的地 址是字符串"Hello,thisisasample!"的第一个字符的地址,即'H'的地址。 str+1也是一个指针,它指向数组的第1号单元,它的类型是char**,它指向 的类型是char*。 *(str+1)也是一个指针,它的类型是char*,它所指向的类型是char,它指向 "Hi,goodmorning."的第一个字符'H',等等。

下面总结一下数组的数组名的问题。声明了一个数组TYPEarray[n],则数组 名称array就有了两重含义:第一,它代表整个数组,它的类型是TYPE[n];第二 ,它是一个指针,该指针的类型是TYPE*,该指针指向的类型是TYPE,也就是数组 单元的类型,该指针指向的内存区就是数组第0号单元,该指针自己占有单独的内 存区,注意它和数组第0号单元占据的内存区是不同的。该指针的值是不能修改的 ,即类似array++的表达式是错误的。 在不同的表达式中数组名array可以扮演不同的角色。 在表达式sizeof(array)中,数组名array代表数组本身,故这时sizeof函数 测出的是整个数组的大小。 在表达式*array中,array扮演的是指针,因此这个表达式的结果就是数组第 0号单元的值。Sizeof(*array)测出的是数组单元的大小。 表达式array+n(其中n=0,1,2,….。)中,array扮演的是指针,故arr ay+n的结果是一个指针,它的类型是TYPE*,它指向的类型是TYPE,它指向数组第 n号单元。故sizeof(array+n)测出的是指针类型的大小。 例十: intarray[10]; int(*ptr)[10]; ptr=&array; 上例中ptr是一个指针,它的类型是int(*)[10],他指向的类型是int[10] ,我们用整个数组的首地址来初始化它。在语句ptr=&array中,array代表数组本 身。

本节中提到了函数sizeof(),那么我来问一问,sizeof(指针名称)测出的究 竟是指针自身类型的大小呢还是指针所指向的类型的大小?答案是前者。例如:

int(*ptr)[10]; 则在32位程序中,有: sizeof(int(*)[10])==4 sizeof(int[10])==40 sizeof(ptr)==4 实际上,sizeof(对象)测出的都是对象自身的类型的大小,而不是别的什么 类型的大小。 第六章。指针和结构类型的关系

可以声明一个指向结构类型对象的指针。 例十一: structMyStruct { inta; intb; intc; } MyStructss={20,30,40};//声明了结构对象ss,并把ss的三个成员初始 化为20,30和40。 MyStruct*ptr=&ss;//声明了一个指向结构对象ss的指针。它的类型是 MyStruct*,它指向的类型是MyStruct。 int*pstr=(int*)&ss;//声明了一个指向结构对象ss的指针。但是它的 类型和它指向的类型和ptr是不同的。

请问怎样通过指针ptr来访问ss的三个成员变量? 答案: ptr->a; ptr->b; ptr->c; 又请问怎样通过指针pstr来访问ss的三个成员变量? 答案: *pstr;//访问了ss的成员a。 *(pstr+1);//访问了ss的成员b。 *(pstr+2)//访问了ss的成员c。 呵呵,虽然我在我的MSVC++6.0上调式过上述代码,但是要知道,这样使用p str来访问结构成员是不正规的,为了说明为什么不正规,让我们看看怎样通过指 针来访问数组的各个单元: 例十二: intarray[3]={35,56,37}; int*pa=array; 通过指针pa访问数组array的三个单元的方法是: *pa;//访问了第0号单元 *(pa+1);//访问了第1号单元 *(pa+2);//访问了第2号单元 从格式上看倒是与通过指针访问结构成员的不正规方法的格式一样。 所有的C/C++编译器在排列数组的单元时,总是把各个数组单元存放在连续的 存储区里,单元和单元之间没有空隙。但在存放结构对象的各个成员时,在某种 编译环境下,可能会需要字对齐或双字对齐或者是别的什么对齐,需要在相邻两 个成员之间加若干个"填充字节",这就导致各个成员之间可能会有若干个字节 的空隙。 所以,在例十二中,即使*pstr访问到了结构对象ss的第一个成员变量a,也 不能保证*(pstr+1)就一定能访问到结构成员b。因为成员a和成员b之间可能会有 若干填充字节,说不定*(pstr+1)就正好访问到了这些填充字节呢。这也证明了指 针的灵活性。要是你的目的就是想看看各个结构成员之间到底有没有填充字节, 嘿,这倒是个不错的方法。 通过指针访问结构成员的正确方法应该是象例十二中使用指针ptr的方法。

第七章。指针和函数的关系

可以把一个指针声明成为一个指向函数的指针。 intfun1(char*,int); int(*pfun1)(char*,int); pfun1=fun1; …. …. inta=(*pfun1)("abcdefg",7);//通过函数指针调用函数。 可以把指针作为函数的形参。在函数调用语句中,可以用指针表达式来作为 实参。 例十三: intfun(char*); inta; charstr[]="abcdefghijklmn"; a=fun(str); … … intfun(char*s) { intnum=0; for(inti=0;i{ num+=*s;s++; } returnnum; } 这个例子中的函数fun统计一个字符串中各个字符的ASCII码值之和。前面说 了,数组的名字也是一个指针。在函数调用中,当把str作为实参传递给形参s后 ,实际是把str的值传递给了s,s所指向的地址就和str所指向的地址一致,但是 str和s各自占用各自的存储空间。在函数体内对s进行自加1运算,并不意味着同 时对str进行了自加1运算。

第八章。指针类型转换

当我们初始化一个指针或给一个指针赋值时,赋值号的左边是一个指针,赋 值号的右边是一个指针表达式。在我们前面所举的例子中,绝大多数情况下,指 针的类型和指针表达式的类型是一样的,指针所指向的类型和指针表达式所指向 的类型是一样的。 例十四: 1。Floatf=12.3; 2。Float*fptr=&f; 3。Int*p; 在上面的例子中,假如我们想让指针p指向实数f,应该怎么搞?是用下面的 语句吗? p=&f; 不对。因为指针p的类型是int*,它指向的类型是int。表达式&f的结果是一 个指针,指针的类型是float*,它指向的类型是float。两者不一致,直接赋值的 方法是不行的。至少在我的MSVC++6.0上,对指针的赋值语句要求赋值号两边的类 型一致,所指向的类型也一致,其它的编译器上我没试过,大家可以试试。为了 实现我们的目的,需要进行"强制类型转换": p=(int*)&f; 如果有一个指针p,我们需要把它的类型和所指向的类型改为TYEP*和TYPE, 那么语法格式是: (TYPE*)p; 这样强制类型转换的结果是一个新指针,该新指针的类型是TYPE*,它指向的 类型是TYPE,它指向的地址就是原指针指向的地址。而原来的指针p的一切属性都 没有被修改。

一个函数如果使用了指针作为形参,那么在函数调用语句的实参和形参的结 合过程中,也会发生指针类型的转换。 例十五: voidfun(char*); inta=125,b; fun((char*)&a); … … voidfun(char*s) { charc; c=*(s+3);*(s+3)=*(s+0);*(s+0)=c; c=*(s+2);*(s+2)=*(s+1);*(s+1)=c; } } 注意这是一个32位程序,故int类型占了四个字节,char类型占一个字节。函 数fun的作用是把一个整数的四个字节的顺序来个颠倒。注意到了吗?在函数调用 语句中,实参&a的结果是一个指针,它的类型是int*,它指向的类型是int。形 参这个指针的类型是char*,它指向的类型是char。这样,在实参和形参的结合过 程中,我们必须进行一次从int*类型到char*类型的转换。结合这个例子,我们可 以这样来想象编译器进行转换的过程:编译器先构造一个临时指针char*temp, 然后执行temp=(char*)&a,最后再把temp的值传递给s。所以最后的结果是:s的 类型是char*,它指向的类型是char,它指向的地址就是a的首地址。

我们已经知道,指针的值就是指针指向的地址,在32位程序中,指针的值其 实是一个32位整数。那可不可以把一个整数当作指针的值直接赋给指针呢?就象 下面的语句: unsignedinta; TYPE*ptr;//TYPE是int,char或结构类型等等类型。 … voidfun(char*s) { charc; c=*(s+3);*(s+3)=*(s+0);*(s+0)=c; c=*(s+2);*(s+2)=*(s+1);*(s+1)=c; } } 注意这是一个32位程序,故int类型占了四个字节,char类型占一个字节。函 数fun的作用是把一个整数的四个字节的顺序来个颠倒。注意到了吗?在函数调用 语句中,实参&a的结果是一个指针,它的类型是int*,它指向的类型是int。形 参这个指针的类型是char*,它指向的类型是char。这样,在实参和形参的结合过 程中,我们必须进行一次从int*类型到char*类型的转换。结合这个例子,我们可 以这样来想象编译器进行转换的过程:编译器先构造一个临时指针char*temp, 然后执行temp=(char*)&a,最后再把temp的值传递给s。所以最后的结果是:s的 类型是char*,它指向的类型是char,它指向的地址就是a的首地址。

我们已经知道,指针的值就是指针指向的地址,在32位程序中,指针的值其 实是一个32位整数。那可不可以把一个整数当作指针的值直接赋给指针呢?就象 下面的语句: unsignedinta; TYPE*ptr;//TYPE是int,char或结构类型等等类型。 … … a=20345686; ptr=20345686;//我们的目的是要使指针ptr指向地址20345686(十进制 ) ptr=a;//我们的目的是要使指针ptr指向地址20345686(十进制) 编译一下吧。结果发现后面两条语句全是错的。那么我们的目的就不能达到 了吗?不,还有办法: unsignedinta; TYPE*ptr;//TYPE是int,char或结构类型等等类型。 … … a=某个数,这个数必须代表一个合法的地址; ptr=(TYPE*)a;//呵呵,这就可以了。 严格说来这里的(TYPE*)和指针类型转换中的(TYPE*)还不一样。这里的(TYP E*)的意思是把无符号整数a的值当作一个地址来看待。 上面强调了a的值必须代表一个合法的地址,否则的话,在你使用ptr的时候 ,就会出现非法操作错误。

想想能不能反过来,把指针指向的地址即指针的值当作一个整数取出来。完 全可以。下面的例子演示了把一个指针的值当作一个整数取出来,然后再把这个 整数当作一个地址赋给一个指针: 例十六: inta=123,b; int*ptr=&a; char*str; b=(int)ptr;//把指针ptr的值当作一个整数取出来。 str=(char*)b;//把这个整数的值当作一个地址赋给指针str。

好了,现在我们已经知道了,可以把指针的值当作一个整数取出来,也可以 把一个整数值当作地址赋给一个指针。

第九章。指针的安全问题

看下面的例子: 例十七: chars='a'; int*ptr; ptr=(int*)&s; *ptr=1298; 指针ptr是一个int*类型的指针,它指向的类型是int。它指向的地址就是s的 首地址。在32位程序中,s占一个字节,int类型占四个字节。最后一条语句不但 改变了s所占的一个字节,还把和s相临的高地址方向的三个字节也改变了。这三 个字节是干什么的?只有编译程序知道,而写程序的人是不太可能知道的。也许 这三个字节里存储了非常重要的数据,也许这三个字节里正好是程序的一条代码 ,而由于你对指针的马虎应用,这三个字节的值被改变了!这会造成崩溃性的错 误。 让我们再来看一例: 例十八: 1。Chara; 2。Int*ptr=&a; … … 3。Ptr++; 4。*ptr=115; 该例子完全可以通过编译,并能执行。但是看到没有?第3句对指针ptr进行 自加1运算后,ptr指向了和整形变量a相邻的高地址方向的一块存储区。这块存储 区里是什么?我们不知道。有可能它是一个非常重要的数据,甚至可能是一条代 码。而第4句竟然往这片存储区里写入一个数据!这是严重的错误。所以在使用指 针时,程序员心里必须非常清楚:我的指针究竟指向了哪里。 在用指针访问数组的时候,也要注意不要超出数组的低端和高端界限,否则 也会造成类似的错误。 在指针的强制类型转换:ptr1=(TYPE*)ptr2中,如果sizeof(ptr2的类型)大 于sizeof(ptr1的类型),那么在使用指针ptr1来访问ptr2所指向的存储区时是安 全的。如果sizeof(ptr2的类型)小于sizeof(ptr1的类型),那么在使用指针ptr1

来访问ptr2所指向的存储区时是不安全的。至于为什么,读者结合例十七来想一 想,应该会明白的


这篇文章摘自网易广州社区的C语言版精华区。作者girlrong是以前C语言版版主,她乐于助人,虚心诚恳,颇受网友欢迎。只可惜现在已退隐江湖了。
发信人: girlrong (阿蓉), 信区: C
标 题: 我眼中的指针

发信站: 网易虚拟社区 (Mon Aug 2 16:12:02 1999), 站内信件

  作者序

为初学者服务。这是我的帖子的宗旨。我也是个初学者(强调了无数遍了) ,我以我的理解把初学者觉得难懂的东西用浅显的语言写出来。由于小学时语文没学好,所以竭尽全力也未必能达到这个目的。尽力而为吧。

指针是c和c++中的难点和重点。我只精通dos下的basic。c语言的其它各种特性,在basic中都有类似的东西。只有指针,是baisc所不具备的。指针是c的灵魂。

我不想重复大多数书上说得很清楚的东西,我只是把我看过的书中说得不清楚或没有说,而我又觉得我理解得有点道理的东西写出来。我的目的是:

1。通过写这些东西,把我脑袋中关于c的模糊的知识清晰化。

2。给初学者们一点提示。

3。赚几个经验值。(因为贴这些东西没有灌水之嫌啊)

 

第一章。指针的概念

指针是一个特殊的变量,它里面存储的数值被解释成为内存里的一个地址。要搞清一个指针需要搞清指针的四方面的内容:指针的类型,指针所指向的类型,指针的值或者叫指针所指向的内存区,还有指针本身所占据的内存区。让我们分别说明。

先声明几个指针放着做例子:

例一:

(1)int *ptr;

(2)char *ptr;

(3)int **ptr;

(4)int (*ptr)[3];

(5)int *(*ptr)[4];

如果看不懂后几个例子的话,请参阅我前段时间贴出的文章<<如何理解c和c

++的复杂类型声明>>。

 

1。 指针的类型。

从语法的角度看,你只要把指针声明语句里的指针名字去掉,剩下的部分就是这个指针的类型。这是指针本身所具有的类型。让我们看看例一中各个指针的类型:

(1)int *ptr; //指针的类型是int *

(2)char *ptr; //指针的类型是char *

(3)int **ptr; //指针的类型是 int **

(4)int (*ptr)[3]; //指针的类型是 int(*)[3]

(5)int *(*ptr)[4]; //指针的类型是 int *(*)[4]

怎么样?找出指针的类型的方法是不是很简单?

 

2。指针所指向的类型。

当你通过指针来访问指针所指向的内存区时,指针所指向的类型决定了编译器将把那片内存区里的内容当做什么来看待。

从语法上看,你只须把指针声明语句中的指针名字和名字左边的指针声明符*去掉,剩下的就是指针所指向的类型。例如:

(1)int *ptr; //指针所指向的类型是int

(2)char *ptr; //指针所指向的的类型是char

(3)int **ptr; //指针所指向的的类型是 int *

(4)int (*ptr)[3]; //指针所指向的的类型是 int()[3]

(5)int *(*ptr)[4]; //指针所指向的的类型是 int *()[4]

在指针的算术运算中,指针所指向的类型有很大的作用。

指针的类型(即指针本身的类型)和指针所指向的类型是两个概念。当你对C越来越熟悉时,你会发现,把与指针搅和在一起的“类型”这个概念分成“指针的类型”和“指针所指向的类型”两个概念,是精通指针的关键点之一。我看了不少书,发现有些写得差的书中,就把指针的这两个概念搅在一起了,所以看起书来前后矛盾,越看越糊涂。

3。 指针的值,或者叫指针所指向的内存区或地址。

指针的值是指针本身存储的数值,这个值将被编译器当作一个地址,而不是一个一般的数值。在32位程序里,所有类型的指针的值都是一个32位整数,因为32位程序里内存地址全都是32位长。

指针所指向的内存区就是从指针的值所代表的那个内存地址开始,长度为sizeof(指针所指向的类型)的一片内存区。以后,我们说一个指针的值是XX,就相当于说该指针指向了以XX为首地址的一片内存区域;我们说一个指针指向了某块内存区域,就相当于说该指针的值是这块内存区域的首地址。

指针所指向的内存区和指针所指向的类型是两个完全不同的概念。在例一中,指针所指向的类型已经有了,但由于指针还未初始化,所以它所指向的内存区是不存在的,或者说是无意义的。

以后,每遇到一个指针,都应该问问:这个指针的类型是什么?指针指向的类型是什么?该指针指向了哪里?

4。 指针本身所占据的内存区。

指针本身占了多大的内存?你只要用函数sizeof(指针的类型)测一下就知道了。在32位平台里,指针本身占据了4个字节的长度。

指针本身占据的内存这个概念在判断一个指针表达式是否是左值时很有用。  

第二章。指针的算术运算

 

指针可以加上或减去一个整数。指针的这种运算的意义和通常的数值的加减运算的意义是不一样的。例如:

例二:

1。 char a[20];

2。 int *ptr=a;

...

...

3。 ptr++;

在上例中,指针ptr的类型是int*,它指向的类型是int,它被初始化为指向整形变量a。接下来的第3句中,指针ptr被加了1,编译器是这样处理的:它把指针ptr的值加上了sizeof(int),在32位程序中,是被加上了4。由于地址是用字节做单位的,故ptr所指向的地址由原来的变量a的地址向高地址方向增加了4个字节。

由于char类型的长度是一个字节,所以,原来ptr是指向数组a的第0号单元开始的四个字节,此时指向了数组a中从第4号单元开始的四个字节。

我们可以用一个指针和一个循环来遍历一个数组,看例子:

例三:

int array[20];

int *ptr=array;

...

//此处略去为整型数组赋值的代码。

...

for(i=0;i<20;i++)

{

(*ptr)++;

ptr++;

}

这个例子将整型数组中各个单元的值加1。由于每次循环都将指针ptr加1,所以每次循环都能访问数组的下一个单元。再看例子:

例四:

1。 char a[20];

2。 int *ptr=a;

...

...

3。 ptr+=5;

在这个例子中,ptr被加上了5,编译器是这样处理的:将指针ptr的值加上5乘sizeof(int),在32位程序中就是加上了5乘4=20。由于地址的单位是字节,故现在的ptr所指向的地址比起加5后的ptr所指向的地址来说,向高地址方向移动了20个字节。在这个例子中,没加5前的ptr指向数组a的第0号单元开始的四个字节,加5后,ptr已经指向了数组a的合法范围之外了。虽然这种情况在应用上会出问题,但在语法上却是可以的。这也体现出了指针的灵活性。

如果上例中,ptr是被减去5,那么处理过程大同小异,只不过ptr的值是被减去5乘sizeof(int),新的ptr指向的地址将比原来的ptr所指向的地址向低地址方向移动了20个字节。

总结一下,一个指针ptrold加上一个整数n后,结果是一个新的指针ptrnew,ptrnew的类型和ptrold的类型相同,ptrnew所指向的类型和ptrold所指向的类型也相同。ptrnew的值将比ptrold的值增加了n乘sizeof(ptrold所指向的类型)个字节。就是说,ptrnew所指向的内存区将比ptrold所指向的内存区向高地址方向移动了n乘sizeof(ptrold所指向的类型)个字节。一个指针ptrold减去一个整数n后,结果是一个新的指针ptrnew,ptrnew的类型和ptrold的类型相同,ptrnew所指向的类型和ptrold所指向的类型也相同。ptrnew的值将比ptrold的值减少了n乘sizeof(ptrold所指向的类型)个字节,就是说,ptrnew所指向的内存区将比ptrold所指向的内存区向低地址方向移动了n乘sizeof(ptrold所指向的类型)个字节。

 

第三章。运算符&和*

 

这里&是取地址运算符,*是...书上叫做“间接运算符”。&a的运算结果是一个指针,指针的类型是a的类型加个*,指针所指向的类型是a的类型,指针所指向的地址嘛,那就是a的地址。*p的运算结果就五花八门了。总之*p的结果是p所指向的东西,这个东西有这些特点:它的类型是p指向的类型,它所占用的地址是p所指向的地址。

例五:

int a=12;

int b;

int *p;

int **ptr;

p=&a;//&a的结果是一个指针,类型是int*,指向的类型是int,指向的地址是a的地址。

*p=24;//*p的结果,在这里它的类型是int,它所占用的地址是p所指向的地址,显然,*p就是变量a。

ptr=&p;//&p的结果是个指针,该指针的类型是p的类型加个*,在这里是int**。该指针所指向的类型是p的类型,这里是int*。该指针所指向的地址就是指针p自己的地址。

*ptr=&b;//*ptr是个指针,&b的结果也是个指针,且这两个指针的类型和所指向的类型是一样的,所以?amp;b来给*ptr赋值就是毫无问题的了。

**ptr=34;//*ptr的结果是ptr所指向的东西,在这里是一个指针,对这个指针再做一次*运算,结果就是一个int类型的变量。

 

第四章。指针表达式。

 

一个表达式的最后结果如果是一个指针,那么这个表达式就叫指针表达式。下面是一些指针表达式的例子:

例六:

int a,b;

int array[10];

int *pa;

pa=&a;//&a是一个指针表达式。

int **ptr=&pa;//&pa也是一个指针表达式。

*ptr=&b;//*ptr和&b都是指针表达式。

pa=array;

pa++;//这也是指针表达式。

例七:

char *arr[20];

char **parr=arr;//如果把arr看作指针的话,arr也是指针表达式

char *str;

str=*parr;//*parr是指针表达式

str=*(parr+1);//*(parr+1)是指针表达式

str=*(parr+2);//*(parr+2)是指针表达式

由于指针表达式的结果是一个指针,所以指针表达式也具有指针所具有的四个要素:指针的类型,指针所指向的类型,指针指向的内存区,指针自身占据的内存。

好了,当一个指针表达式的结果指针已经明确地具有了指针自身占据的内存的话,这个指针表达式就是一个左值,否则就不是一个左值。 在例七中,&a不是一个左值,因为它还没有占据明确的内存。*ptr是一个左值,因为*ptr这个指针已经占据了内存,其实*ptr就是指针pa,既然pa已经在内存中有了自己的位置,那么*ptr当然也有了自己的位置。

 

第五章。数组和指针的关系

 

如果对声明数组的语句不太明白的话,请参阅我前段时间贴出的文章<<如何理解c和c++的复杂类型声明>>。 数组的数组名其实可以看作一个指针。看下例:

例八:

int array[10]={0,1,2,3,4,5,6,7,8,9},value;

...

...

value=array[0];//也可写成:value=*array;

value=array[3];//也可写成:value=*(array+3);

value=array[4];//也可写成:value=*(array+4);

上例中,一般而言数组名array代表数组本身,类型是int [10],但如果把array看做指针的话,它指向数组的第0个单元,类型是int *,所指向的类型是数组单元的类型即int。因此*array等于0就一点也不奇怪了。同理,array+3是一个指向数组第3个单元的指针,所以*(array+3)等于3。其它依此类推。

例九:

char *str[3]={

"Hello,this is a sample!",

"Hi,good morning.",

"Hello world"

};

char s[80];

strcpy(s,str[0]);//也可写成strcpy(s,*str);

strcpy(s,str[1]);//也可写成strcpy(s,*(str+1));

strcpy(s,str[2]);//也可写成strcpy(s,*(str+2));

上例中,str是一个三单元的数组,该数组的每个单元都是一个指针,这些指针各指向一个字符串。把指针数组名str当作一个指针的话,它指向数组的第0号单元,它的类型是char**,它指向的类型是char *。

*str也是一个指针,它的类型是char*,它所指向的类型是char,它指向的地址是字符串"Hello,this is a sample!"的第一个字符的地址,即'H'的地址。 str+1也是一个指针,它指向数组的第1号单元,它的类型是char**,它指向的类型是char *。

*(str+1)也是一个指针,它的类型是char*,它所指向的类型是char,它指向"Hi,good morning."的第一个字符'H',等等。

下面总结一下数组的数组名的问题。声明了一个数组TYPE array[n],则数组名称array就有了两重含义:第一,它代表整个数组,它的类型是TYPE [n];第二,它是一个指针,该指针的类型是TYPE*,该指针指向的类型是TYPE,也就是数组单元的类型,该指针指向的内存区就是数组第0号单元,该指针自己占有单独的内存区,注意它和数组第0号单元占据的内存区是不同的。该指针的值是不能修改的,即类似array++的表达式是错误的。

在不同的表达式中数组名array可以扮演不同的角色。

在表达式sizeof(array)中,数组名array代表数组本身,故这时sizeof函数测出的是整个数组的大小。

在表达式*array中,array扮演的是指针,因此这个表达式的结果就是数组第0号单元的值。sizeof(*array)测出的是数组单元的大小。

表达式array+n(其中n=0,1,2,....。)中,array扮演的是指针,故array+n的结果是一个指针,它的类型是TYPE*,它指向的类型是TYPE,它指向数组第n号单元。故sizeof(array+n)测出的是指针类型的大小。

例十:

int array[10];

int (*ptr)[10];

ptr=&array;

上例中ptr是一个指针,它的类型是int (*)[10],他指向的类型是int [10],我们用整个数组的首地址来初始化它。在语句ptr=&array中,array代表数组本身。

本节中提到了函数sizeof(),那么我来问一问,sizeof(指针名称)测出的究竟是指针自身类型的大小呢还是指针所指向的类型的大小?答案是前者。例如:

int (*ptr)[10];

则在32位程序中,有:

sizeof(int(*)[10])==4

sizeof(int [10])==40

sizeof(ptr)==4

实际上,sizeof(对象)测出的都是对象自身的类型的大小,而不是别的什么类型的大小。  

第六章。指针和结构类型的关系

 

可以声明一个指向结构类型对象的指针。

例十一:

struct MyStruct

{

int a;

int b;

int c;

}

MyStruct ss={20,30,40};//声明了结构对象ss,并把ss的三个成员初始化为20,30和40。

MyStruct *ptr=&ss;//声明了一个指向结构对象ss的指针。它的类型是

MyStruct*,它指向的类型是MyStruct。

int *pstr=(int*)&ss;//声明了一个指向结构对象ss的指针。但是它的类型和它指向的类型和ptr是不同的。

请问怎样通过指针ptr来访问ss的三个成员变量?

答案:

ptr->a;

ptr->b;

ptr->c;

又请问怎样通过指针pstr来访问ss的三个成员变量?

答案:

*pstr;//访问了ss的成员a。

*(pstr+1);//访问了ss的成员b。

*(pstr+2)//访问了ss的成员c。

呵呵,虽然我在我的MSVC++6.0上调式过上述代码,但是要知道,这样使用pstr来访问结构成员是不正规的,为了说明为什么不正规,让我们看看怎样通过指针来访问数组的各个单元:

例十二:

int array[3]={35,56,37};

int *pa=array;

通过指针pa访问数组array的三个单元的方法是:

*pa;//访问了第0号单元

*(pa+1);//访问了第1号单元

*(pa+2);//访问了第2号单元

从格式上看倒是与通过指针访问结构成员的不正规方法的格式一样。

所有的C/C++编译器在排列数组的单元时,总是把各个数组单元存放在连续的存储区里,单元和单元之间没有空隙。但在存放结构对象的各个成员时,在某种编译环境下,可能会需要字对齐或双字对齐或者是别的什么对齐,需要在相邻两个成员之间加若干个“填充字节”,这就导致各个成员之间可能会有若干个字节的空隙。

所以,在例十二中,即使*pstr访问到了结构对象ss的第一个成员变量a,也不能保证*(pstr+1)就一定能访问到结构成员b。因为成员a和成员b之间可能会有若干填充字节,说不定*(pstr+1)就正好访问到了这些填充字节呢。这也证明了指针的灵活性。要是你的目的就是想看看各个结构成员之间到底有没有填充字节,嘿,这倒是个不错的方法。

通过指针访问结构成员的正确方法应该是象例十二中使用指针ptr的方法。

 

第七章。指针和函数的关系

 

 

可以把一个指针声明成为一个指向函数的指针。

int fun1(char*,int);

int (*pfun1)(char*,int);

pfun1=fun1;

....

....

int a=(*pfun1)("abcdefg",7);//通过函数指针调用函数。

可以把指针作为函数的形参。在函数调用语句中,可以用指针表达式来作为实参。

例十三:

int fun(char*);

int a;

char str[]="abcdefghijklmn";

a=fun(str);

...

...

int fun(char*s)

{

int num=0;

for(int i=0;i<strlen(s);i++)

{

num+=*s;s++;

}

return num;

}

这个例子中的函数fun统计一个字符串中各个字符的ASCII码值之和。前面说了,数组的名字也是一个指针。在函数调用中,当把str作为实参传递给形参s后,实际是把str的值传递给了s,s所指向的地址就和str所指向的地址一致,但是str和s各自占用各自的存储空间。在函数体内对s进行自加1运算,并不意味着同时对str进行了自加1运算。

 

第八章。指针类型转换

 

当我们初始化一个指针或给一个指针赋值时,赋值号的左边是一个指针,赋值号的右边是一个指针表达式。在我们前面所举的例子中,绝大多数情况下,指针的类型和指针表达式的类型是一样的,指针所指向的类型和指针表达式所指向的类型是一样的。

例十四:

1。 float f=12.3;

2。 float *fptr=&f;

3。 int *p;

在上面的例子中,假如我们想让指针p指向实数f,应该怎么搞?是用下面的语句吗?

p=&f;

不对。因为指针p的类型是int*,它指向的类型是int。表达式&f的结果是一个指针,指针的类型是float*,它指向的类型是float。两者不一致,直接赋值的方法是不行的。至少在我的MSVC++6.0上,对指针的赋值语句要求赋值号两边的类型一致,所指向的类型也一致,其它的编译器上我没试过,大家可以试试。为了实现我们的目的,需要进行“强制类型转换”:

p=(int*)&f;

如果有一个指针p,我们需要把它的类型和所指向的类型改为TYEP*和TYPE,那么语法格式是:

(TYPE*)p;

这样强制类型转换的结果是一个新指针,该新指针的类型是TYPE*,它指向的类型是TYPE,它指向的地址就是原指针指向的地址。而原来的指针p的一切属性都没有被修改。

一个函数如果使用了指针作为形参,那么在函数调用语句的实参和形参的结合过程中,也会发生指针类型的转换。

例十五:

void fun(char*);

int a=125,b;

fun((char*)&a);

...

...

void fun(char*s)

{

char c;

c=*(s+3);*(s+3)=*(s+0);*(s+0)=c;

c=*(s+2);*(s+2)=*(s+1);*(s+1)=c;

}

注意这是一个32位程序,故int类型占了四个字节,char类型占一个字节。函数fun的作用是把一个整数的四个字节的顺序来个颠倒。注意到了吗?在函数调用语句中,实?amp;a的结果是一个指针,它的类型是int *,它指向的类型是int。形参这个指针的类型是char*,它指向的类型是char。这样,在实参和形参的结合过程中,我们必须进行一次从int*类型到char*类型的转换。结合这个例子,我们可以这样来想象编译器进行转换的过程:编译器先构造一个临时指针 char*temp,然后执行temp=(char*)&a,最后再把temp的值传递给s。所以最后的结果是:s的类型是char*,它指向的类型是char,它指向的地址就是a的首地址。

我们已经知道,指针的值就是指针指向的地址,在32位程序中,指针的值其实是一个32位整数。那可不可以把一个整数当作指针的值直接赋给指针呢?就象下面的语句:

unsigned int a;

TYPE *ptr;//TYPE是int,char或结构类型等等类型。

...

...

a=20345686;

ptr=20345686;//我们的目的是要使指针ptr指向地址20345686(十进制)

ptr=a;//我们的目的是要使指针ptr指向地址20345686(十进制)

编译一下吧。结果发现后面两条语句全是错的。那么我们的目的就不能达到了吗?不,还有办法:

unsigned int a;

TYPE *ptr;//TYPE是int,char或结构类型等等类型。

...

...

a=某个数,这个数必须代表一个合法的地址;

ptr=(TYPE*)a;//呵呵,这就可以了。

严格说来这里的(TYPE*)和指针类型转换中的(TYPE*)还不一样。这里的(TYPE*)的意思是把无符号整数a的值当作一个地址来看待。

上面强调了a的值必须代表一个合法的地址,否则的话,在你使用ptr的时候,就会出现非法操作错误。

想想能不能反过来,把指针指向的地址即指针的值当作一个整数取出来。完全可以。下面的例子演示了把一个指针的值当作一个整数取出来,然后再把这个整数当作一个地址赋给一个指针:

例十六:

int a=123,b;

int *ptr=&a;

char *str;

b=(int)ptr;//把指针ptr的值当作一个整数取出来。

str=(char*)b;//把这个整数的值当作一个地址赋给指针str。

好了,现在我们已经知道了,可以把指针的值当作一个整数取出来,也可以把一个整数值当作地址赋给一个指针。

 

第九章。指针的安全问题

看下面的例子:

例十七:

char s='a';

int *ptr;

ptr=(int*)&s;

*ptr=1298;

指针ptr是一个int*类型的指针,它指向的类型是int。它指向的地址就是s的首地址。在32位程序中,s占一个字节,int类型占四个字节。最后一条语句不但改变了s所占的一个字节,还把和s相临的高地址方向的三个字节也改变了。这三个字节是干什么的?只有编译程序知道,而写程序的人是不太可能知道的。也许这三个字节里存储了非常重要的数据,也许这三个字节里正好是程序的一条代码,而由于你对指针的马虎应用,这三个字节的值被改变了!这会造成崩溃性的错误。让我们再来看一例:

例十八:

1。 char a;

2。 int *ptr=&a;

...

...

3。 ptr++;

4。 *ptr=115;

该例子完全可以通过编译,并能执行。但是看到没有?第3句对指针ptr进行自加1运算后,ptr指向了和整形变量a相邻的高地址方向的一块存储区。这块存储区里是什么?我们不知道。有可能它是一个非常重要的数据,甚至可能是一条代码。而第4句竟然往这片存储区里写入一个数据!这是严重的错误。所以在使用指针时,程序员心里必须非常清楚:我的指针究竟指向了哪里。

在用指针访问数组的时候,也要注意不要超出数组的低端和高端界限,否则也会造成类似的错误。

在指针的强制类型转换:ptr1=(TYPE*)ptr2中,如果sizeof(ptr2的类型)大于sizeof(ptr1的类型),那么在使用指针ptr1来访问ptr2所指向的存储区时是安全的。如果sizeof(ptr2的类型)小于sizeof(ptr1的类型),那么在使用指针ptr1来访问ptr2所指向的存储区时是不安全的。至于为什么,读者结合例十七来想一想,应该会明白的。


请写出以下程序的运行结果:

#include<stdio.h>
int *p;
pp(int a,int *b);
main()
{
int a=1,b=2,c=3;
p=&b;
pp(a+c,&b);
printf("(1)%d%d%d\n",a,b,*p);
}
pp(int a,int *b)
{int c=4;
*p=*b+c;
a=*p-c;
printf("(2)%d%d%d\n",a,*b,*p);
}

这道题出得很好,它涉及好几方面的知识,如果你能一次宰龆裕�你的水平在初哥中就算不低�?br> (起码高过我啦,我吾系好掂格咋)



B1层 发表时间: 04-05-02 23:22

回复: seny11 [seny11]   论坛用户   登录
一个绝好的大型软件ISO下载FTP站!
【分享】一个绝好的大型软件ISO下载FTP站!

内容: http://bbs.downsurf.com 主论坛
ftp://list:list@soft.downsurf.com 软件站24小时开放高速下载

已经更新超过600G的软件
七台高级光纤服务器
水晶报表9.2中文版等
每日更新0day破解软件
最新程序设计工具及组件
以ISO为主
...

最新水晶报表下载列表:
最新水晶报表下载列表:
+---Seagate.Crystal.Enterprise.v10.0 英文版 625M 注册码 NEW
+---Seagate.Crystal.Reports.Professional.v10.0.0.533英文版 140M 注册码 NEW
+---Seagate.Crystal.Reports.Professional v9.2.2.634英文版 395M 注册码 NEW
+---Seagate.Crystal.Report.Application.Server.v9.2.1.148英文版 477M 注册码 NEW
+---Seagate.Crystal.Enterprise.v9.2.Standard英文标准企业版 640M 注册码 NEW
+---Seagate.Crystal.Reports.Professional.v9.2英文版 364M 注册码 NEW
+---Seagate.Crystal.Reports.Advanced.v9.2中文版 488M 注册码 HOT
+---Seagate.Crystal.Reports.Advanced.v9.2英文版 465M 注册码
+---Seagate.Crystal.Analysis.Professional.v9.2多语言版 156M 注册码 NEW
+---Seagate.Crystal.Enterprise.Report.Application.Server.v9.2英文版 448M 注册码
+---Seagate.Crystal.Analysis.Professional.v8.5英文专业版 223M 注册码
+---Seagate.Crystal.Reports.Professional.v8.5英文专业版 368M 已授权
+---Seagate.Crystal.Reports.Developer.v8.5英文开发版 586M 已授权
+---Seagate.Crystal.Reports.Professional.v8.0英文专业版 233M 已授权
+---Seagate.Crystal.Reports.Developer.v8.0英文开发版 238M 注册码
+---Seagate.Crystal.Reports.Professional.v7.0英文专业版 128M 注册码

├最新大型原版更新软件列表:
├─SAP ERP 4.6D 全套 10CD
├─用友ERP-U8.5 1CD
├─用友ERP-U8.50SP 1CD
├─用友财务软件V8.20 1CD
├─金蝶K3系列V9.4.1企业完全版 2CD
├─金蝶财务K3系统 V9.3 1CD
├─通达网络智能办公系统2.4 1CD
├─万千网络办公系统 1CD
├─海天移动办公系统7.0 1CD
├─天心企业全能管理软件 Fas2000 1CD
├─领航OA2002 1CD
├─讯为通用办公与客户管理系网络专业版本7.23 1CD
├─正方网络办公自动化系统2002版 1CD
├─联达动力办公助手V4.0 1CD
├─视频电话会议全站源码 1CD
├─速达3000XP6.51 网络版 1C
├─V2 Conference 视屏会议3.7 1CD
├─清华大学计算机系网络课程 31CD
├─张孝祥IT课堂-JAVA就业培训教程 8CD
├─新概念英语Flash版-全套(1-4) 4CD
├─ORACLE内部培训视频教学 22CD
├─Oracle9i.Database.Release.2 4CD
├─Oracle9i学习资料 3CD
├─微软认证MSCE2000认证实录完整版 12CD
├─微软认证高级技术培训MCSE多媒体教程 4CD
├─北邮网络讲座 3CD
├─北邮在线TCP-IP教程 2CD
├─北邮在线教程 4CD
├─新概念英语新版课本同步讲解辅导 全套(1-4)
├─阶梯英语课程单元 12CD
├─新东方4+1系列 全套
├─新东方雅思 1CD
├─海天上网管理系统 1CD
├─科利华校园网应用平台(高级版) 1CD
├─圆方家居设计系统2003 V3.0 1CD
├─作曲大师2004专业版 1CD
├─CISCO思科教学课件全集 1CD
├─CISCO专家组中文培训 1CD
├─C及C++语言程序设计入门 1CD
├─IBM学习资料 1CD
├─JAVA开发全面通 1CD
├─JSP网络应用教程2003 1CD
├─Java语言讲座 2CD
├─Linux系统讲座 1CD
├─MBA全套教材 1CD
├─Microsoft 9月中文资料光盘 1CD
├─Microsoft 10月中文资料光盘 1CD
├─Unix系统讲座 3CD
├─薄冰高级英语语法 1CD
├─北大听讲座第二辑 1CD
├─程序员2003年合订本随附光盘 1CD
├─程序员大本营.NET开发 2CD
├─计算机网络 吉大版 7CD
├─剑鹰会员光盘 4CD
├─路由协议基础 1CD
├─清华大学计算机主干课程讲义 1CD
├─如何编写优秀程序 2CD
├─数据结构C++ 复旦版全套
├─微软12月份月度中文资料光盘 1CD
├─张孝祥IT课堂-JAVA片断 1CD
├─浙大远程-英语语法多媒体教学 2CD
├─指南针股票多媒体教学《天狼之眼》 18VCD
├─IBM.Lotus.Domino.Release.6.多国语言版 6CD
├─IBM Tivoli 入侵检测简体中文版 7CD
├─IBM Tivoli 实战系列教程 3CD
├─IBM Tivoli 网络备份 1CD
├─IBM Tivoli 远程控制 1CD
├─IBM Tivoli 资产管理 2CD
├─IBM.LOTUS.NOTES.CLIENT.DESIGN.ADMIN.V6.5 1CD
├─IBM.Lotus.Domino.Server.V6.5.1.Windows 1CD
├─IBM.Lotus.Enterprise.Script.Builder.v3.0 1CD
├─IBM.Notes.Domino.Designer.and.Admin.Clients.V6.5.1.Windows 1CD
├─IBM.RATIONAL.CLEARCASE.V2003.06.00.Multilanguage 1CD
├─IBM.RATIONAL.CLEARQUEST.V2003.6.436 1CD
├─IBM.RATIONAL.REQUISITE.PRO.V2003.6.436 1CD
├─IBM.RATIONAL.ROBOT.V2003.6.ISO 1CD
├─IBM.RATIONAL.ROSE.ALL.EDITIONS.V2003.6.12 2CD
├─IBM.RATIONAL.ROSE.C++.EDITION.PROFESSIONAL.V2003.6 1CD
├─IBM.RATIONAL.ROSE.XDE.DEVELOPER.PLUS.FOR.DOTNET.V2003.6.12 2CD
├─IBM.RATIONAL.XDE.DEVELOPER.FOR.JAVA.V2003.6.12 1CD
├─IBM.RATIONALl.SUITE.V2003.06.00.Multilanguage 3CD
├─IBM.WEBSPHERE.PORTAL.SERVER.V5.0 2CD
├─IBM.WebSphere.Application.Server.Enterprices.V5.0.2.Multilanguage 1CD
├─IBM.WebSphere.Studio.Application.Developer.v5.1 3CD
├─IBM.WebSphere.Studio.Voice.Toolkit.v4.2.MULTiLANGUAGE 1CD
├─Microsoft.Office.Access.2003.Developer.Extensions 1CD
├─Microsoft.Visual.Studio.Tools.for.the.Microsoft.Office.System.2003 1CD
├─Microsoft.Windows.XP.SP1.Driver.Development.Kit.WinXP 1CD
├─Microsoft.CRM.v1.2.Sales.for.Outlook 1CD
├─Microsoft.CRM.v1.2.Server 1CD
├─ANSOFT.HFSS.V9.1 1CD
├─BEA.WEBLOGIC.PLATFORM.V8.1.SP2 1CD
├─CA Unicenter TNG 简体中文企业版 1CD
├─LanStar.2000.Visua.Class.多媒体教学网络系统 1CD
├─Microsoft..Office.Live.Communications.Server.2003英文版 1CD
├─SCO OpenServer 5.0.7 1CD
├─MAGIC.WINMAILSERVER 邮件服务器 1CD
├─MBA全套教材 2CD
├─SOLAIRS 9.0 2CD
├─TapeWare.6.30SP1B多国语言版 1CD
├─海百合课件专家V8.0城域网版-免服务器安装版 1CD
├─经理人常犯的11个错误 (余世维) 11CD
├─实用加密解密高手捷径 2CD
├─天正建筑6.0 1CD
├─微软高校.NET比赛作品 1CD
├─VERITAS.BACKUP.EXEC.V9.1 1CD
├─XILINX.ISE.V6.1I 2CD




B2层 发表时间: 04-05-02 23:35

回复: lida1818 [lida1818]   论坛用户   登录
详读,收藏,十分感谢!解决了我不少疑问!再次感谢!

B3层 发表时间: 04-05-02 23:42

回复: seny11 [seny11]   论坛用户   登录
书籍下载:http://www.ptdot.net/down/index.asp?classid=4

B4层 发表时间: 04-05-03 00:14

回复: sniper167 [sniper167]   论坛用户   登录
收藏,十分感谢!

B5层 发表时间: 04-05-03 11:29

回复: ziaichen [ziaichen]   论坛用户   登录
我帮你顶!

B6层 发表时间: 04-05-03 15:00

论坛: 编程破解

20CN网络安全小组版权所有
Copyright © 2000-2010 20CN Security Group. All Rights Reserved.
论坛程序编写:NetDemon

粤ICP备05087286号