a我考网

 找回密码
 立即注册

QQ登录

只需一步,快速开始

扫一扫,访问微社区

查看: 86|回复: 1

[C语言] C语言之指针综合谈

[复制链接]
发表于 2012-7-31 21:48:08 | 显示全部楼层 |阅读模式
  概述- S% V# P' }9 D! k# e% h; x/ e  l! t
  Joel Spolsky认为,对指针的理解是一种aptitude,不是通过训练就可以达到的。虽然如此,我还是想谈一谈这个C/C++语言中最强劲也是最容易出错的要素。
& Q9 z6 i7 o1 L0 R* p  鉴于指针和目前计算机内存结构的关联,很多C语言比较本质的特点都孕育在其中,因此,本篇和第六、第七两篇我都将以指针为主线,结合在实际中遇到的问题,来详细谈谈关于指针的几个重要方面。
6 A- \9 l' W  i6 |  指针类型的本质分析
  ]1 X, W6 ^# a3 j0 ?) J& o5 Q3 S" q  1、指针的本质
1 D4 `, H5 @6 L7 I- }  ]8 Q! J# y  指针的本质:一种复合的类型。下面我将以下面几个作为例子进行展开分析:
( G( ~. E" h" d& w: i  a)、int *p;! k3 t/ N; G- S" c: G& h9 R
  b)、int p;( \6 O8 o8 U' h9 s
  c)、int ;
9 j. T: x1 w/ T- s5 p  d)、int ;
- O1 t& W% _6 T$ W* T; I0 ]* I% s  分析:
% @, `' ^+ M& Y+ G+ @- U! U4 k  所谓的类型就是具有某种特征的东东,比如类型char,它的特征就是它所占据的内存为1个字节, 指针也很类似,指针所指向的值也占据着内存中的一块地址,地址的长度与指针的类型有关,比如对于char型指针,这个指针占据的内存就是1个字节,因此指针也是一种类型,但我们知道指针本身也占据了一个内存空间地址,地址的长度和机器的字长有关,比如在32位机器中,这个长度就是4个字节,因此指针本身也同样是一种类型,因此,我们说,指针其实是一种复合的类型,! W1 P  s6 p( T+ x
  好了,现在我们可以分析上面的几个例子了。# h7 {, ?6 A) I0 W0 W. [
  假设有如下定义:: X. w' O6 b" f# C3 f
  int nValue;
' I( O  P2 S( K4 z, @1 x% N  那么,nValue的类型就是int,也就是把nValue这个具体变量去掉后剩余的部分,因此,上面的4个声明可以类比进行分析:3 T( U8 p; h" A8 h: V& a, Y
  a)、int *
3 b/ Z; {- j+ n: H( n  *代表变量(指针本身)的值是一个地址,int代表这个地址里面存放的是一个整数,这两个结合起来,int *定义了一个指向整数的指针,类推如下:* {1 @" l: l. E1 Z4 V! p
  b)、int
9 [! L, v5 y( e8 l/ {2 D  指向一个指向整数的指针的指针。9 f7 I& H0 x# a5 u% Z. x
  c)、int5 e" g( O5 k. s0 y' j+ u) Y
  指向一个拥有三个整数的数组的指针。
6 ]. q# L# ~" V9 x4 f6 j: z  d)、int
1 R4 E" _: z' ?- n5 F" M" r  指向一个函数的指针,这个函数参数为空,返回值为整数。) N" J* D1 O% V$ r. W6 @
  分析结束,从上面可以看出,指针包括两个方面,一个是它本身的值,是一个内存中的地址;另一个是指针所指向的物,是这个地址中所存放着具有各种各样意义的。
5 W: \+ H: ]6 ]. z# t  2、对指针本身值的分析
' P5 s8 H+ m1 P# X$ @0 M6 j! p) G  下面例子考察指针本身的值(环境为32位的计算机):
; h; y. [. ]2 b( R/ ]6 }  void *p = malloc;$ G6 m" _5 |# _6 j7 m
  请计算sizeof= ?
; i7 u7 l& s# X: B7 p  @7 C' A/ ^  char str = “Hello” ;) b5 }, k3 b( n8 A) p+ v* [
  char *p = str ;$ Z* S' h* l9 x, g9 A# R% m
  请计算sizeof= ?  w7 V! n: r, Z" o& z
  void Func
8 \4 M* E# s: m# a. \  分析:上面的例子,答案都是4,因为从上面的讨论可以知道,指针本身的值对应着内存中的一个地址,它的size只与机器的字长有关(即它是由系统的内存模型决定的),在32位机器中,这个长度是4个字节。& o" g7 X5 R2 K3 ^/ y' B: {
  3、对指针所指向物的分析
7 i( m& K. B& ~: s: l, z  现在再对指针这个复合类型的第二部分,指针所指向物的意义进行分析。' y6 B$ K6 h: }  n: z; Q' o
  上面我们已经得到了指针本身的类型,那么将指针本身的类型去掉 “*”号就可得到指针所指向物的类型,分别, N- a' w. B3 _" C/ S  `0 W
  a)、int
0 v4 a6 a( q- Q- A3 f  所指向物是一个整数。8 K2 B" Q* u# ?
  b)、int*
  c1 T( t+ b# Y" a. R  所指向物是一个指向整数的指针。$ A( k" G/ a* L% d) D# b
  c)、int
# r% F$ O& U% l# E0 W: ]  为空,可以去掉,变为int ,所指向物是一个拥有三个整数的数组。+ e' U$ {; p) d# M
  d)、int
  ^. S2 \# \  m4 c  第一个为空,可以去掉,变为int ,所指向物是一个函数,这个函数的参数为空,返回值为整数。
& @2 k% B' D/ U  4、附加分析
2 ?0 x1 @+ J" ~. p1 N5 |! ~  另外,关于指针本身大小的问题,在C++中与C有所不同,这里我也顺带谈一下。
% J& b0 d; v, O. f0 @1 X9 x$ q3 x  在C++中,对于指向对象成员的指针,它的大小不一定是4个字节,这主要是因为在引入多重虚拟继承以及虚拟函数的时候,有些附加的信息也需要通过这个指针进行传递,因此指向对象成员的指针会增大,不论是指向成员,还是成员函数都是如此,具体与编译器的实现有关,你可以编写个很小的C++程序去验证一下。另外,对一个类的静态成员(static member,可以是静态成员变量或者静态成员函数)来说,指向它的指针只是普通的函数指针,而不是一个指向类成员的指针,所以它的大小不会增加,仍旧是4个字节。: ]; C- E+ }4 ?5 f4 [2 P4 E. J
  指针运算符&和*
0 b& z  P/ X0 L) r$ {" u  “&和*”,它们是一对相反的操作,’&’取得一个物的地址(也就是指针本身),’*’得到一个地址里放的物(指针所指向的物)。这个东西可以是值(对象)、函数、数组、类成员(class member)等等。
" a* F! B1 A! e( S/ B  参照上面的分析我们可以很好地理解&与*。+ x1 E5 `4 B/ I) B/ v
  使用指针的好处?
# l4 u9 t/ M& [; r  a. O( N" ?  关于指针的本质和基本的运算符我们讨论过了,在这里,我想再笼总地谈一谈使用指针的必要性和好处,为我们今后的使用和对后面篇章的理解做好铺垫。简而言之,指针有以下好处:5 }: A" n) n: o7 G1 M4 l
  1)、方便使用动态分配的数组。5 b$ Y1 Z7 F: \/ R
  这个解释我放在本系列第六篇中进行讲解。0 e% Z2 l! O, a" F* f
  2)、对于相同类型(甚至是相似类型)的多个变量进行通用。/ d3 J6 t; X/ F- c
  就是用一个指针变量不断在多个变量之间指来指去,从而使得非常应用起来非常灵活,不过,这招也比较危险,需要小心使用:因为出现错误的指针是中非常忌讳的事情。6 @6 J* m$ _, Z: L, U" `
  3)、变相改变一个函数的值传递特性。
; }9 X$ s: N+ g  说白了,就是指针的传地址作用,将一个变量的地址作为参数传给函数,这样函数就可以修改那个变量了。
! w4 z/ [0 b# w, d3 Y  4)、节省函数调用代价。
; V& t2 |( L+ j3 m, \  我们可以将参数,尤其是大个的参数(例如结构,对象等),将他们地址作为参数传给函数,这样可以省去编译器为它们制作副本所带来的空间和时间上的开销。
回复

使用道具 举报

 楼主| 发表于 2012-7-31 21:48:09 | 显示全部楼层

C语言之指针综合谈

  5)、动态扩展结构。) {" `  \' u. {
  因为指针可以动态地使用malloc/new生成堆上的内存,所以在需要动态扩展结构的时候,非常有用;比如对于树、链表、Hash表等,这几乎是必不可少的特性。
* u& @3 @! L) `  O' L  6)、与目前计算机的内存模型相对应,可按照内存地址进行直接存取,这使得C非常适合于一些较底层的应用。
) l4 e/ k8 Y$ ~8 G! `  这也是C/C++指针一个强大的优点,我会在后面讲述C语言的底层操作时,较详细地介绍这个优点的应用。
- }$ P7 z" {* i6 h9 L6 s: P$ G  7)、遍历数组。% F; s; t, V7 I" K4 W. Z( i
  据个例子来说吧,当你需要对字符串数组进行操作时,想一想,你当然要用字符串指针在字符串上扫来扫去。
4 @3 Y5 z3 l$ N  t- r; w8 r8 u% ]7 d  …实在太多了,你可以慢慢来补充^_^。
. k, H! p: n7 J% o  指针本身的相关问题
3 \: p7 y$ R, F9 t) m# {7 M  1、问题:空指针的定义
; [; Q0 S' Z& ]+ r  曾经看过有的.h文件将NULL定义为0L,为什么?
5 s8 B' u' [. v6 R: j  答案与分析:: |: S  x  @7 ~( n( V1 S, D
  这是一个关于空指针宏定义的问题。指针在C语言中是经常使用的,有时需要将一个指针置为空指针,例如在指针变量初始化的时候。
: f. c7 a# A+ Z: F  C语言中的空指针和Pascal或者Lisp语言中的NIL具有相同的地位。那如何定义空指针呢?下面的语句是正确的:
# ^! N: M- D9 o4 \9 E" f$ t  char *p1 = 0;
* J) c& ?) d* x2 p. N9 N  int *p2;0 U. V$ r. Y" e
  if
( e8 y& p4 u2 m' R0 d, E  p2 = 0;
! _( x" Y. u0 Q6 j; Z/ R- Q# U2 b  也就是说,在指针变量的初始化、赋值、比较操作中,0会被编译器理解为要将指针置为空指针。至于空指针的内部表示是否是0,则随不同的机器类型而定,不过通常都是0。但是在另外一些场合下,例如函数的参数原型是指针类型,函数调用时如果将0作为参数传入,编译器则不能将其理解为空指针。此时需要明确的类型转换,例如:
6 A6 v# m+ L# T& _, x& E& g5 T  void func ;' `, b2 Q1 q" J
  func 0);
/ w* Y# W  m& G' Y  R" \  一般情况下,0是可以放在代码中和指针关联使用的,但是有些程序员(数量还不少呦!也许就包括你在内)不喜欢0的直白,认为其不能表示作为指针的特殊含义,于是要定义一个宏NULL,来明确表示空指针常量。这也是对的,人家C语言标准就明确说:“ NULL应该被定义为与实现相关的空指针常量”。但是将NULL定义成什么样的值呢?我想你一定见过好几种定义NULL的方法:/ l3 U/ A0 m) O( j1 E
  #define NULL 02 @; f: m7 l, Z) R' S" h
  #define NULL 0
) t0 `+ m4 R4 j- A6 [. x  j  #define NULL 0
+ l+ z& x* x+ b9 Y3 e# m  在我们使用的绝大多数计算系统上,例如PC,上述定义是能够工作的。然而,世界上还有很多其它种类的计算机,其CPU也不是Intel的。在某些系统上,指针和整数的大小和内部表示并不一致,甚至不同类型的指针的大小都不一致。为了避免这种可移植性问题,0L是一种最为安全的、最妥帖的定义方式。0L的含义是: “值为0的整数常量表达式”。这与C语言给出的空指针定义完全一致。因此,建议采用0L作为空指针常量NULL的值。& {. O. f8 W, w4 e$ s6 N
  其实 NULL定义值,和操作系统的的平台有关, 将一个指针定义为 NULL, 其用意是为了保护操作系统,因为通过指针可以任何一块地址, 但是,有些是不许一般用户的,比如操作系统的核心。 当我们通过一个空的指针去方位时,系统会提示非法, 那么系统又是如何知道的呢??
8 w1 j$ N& a1 L9 a# o  V# S/ R! i  以windows2000系统为例, 该系统规定系统中每个进程的起始地址(0x00000000)开始的某个地址范围内是存放系统的,用户进程无法, 所以当用户用空指针(0)时,其实的就是0x00000000地址的系统,由于该地址是受系统保护的,所以系统会提示错误。) I4 h- K9 P% E6 g! z7 O$ k
  这也就是说NULL值不一定要定义成0,起始只要定义在系统的保护范围的地址空间内,比如定义成都会起到相同的作用,但是为了考虑到移植性,普遍定义为0 。0 P7 c% u5 N  L" E, r9 L
  2、问题:与指针相关的规则&规则分析6 ?# l" Y5 v: S* A7 O3 Z' I
  指针既然这么重要,而且容易出错,那么有没有方法可以很好地减少这些指针相关问题的出现呢?7 ~5 X! w5 N8 c- ?8 [; O0 d- f
  答案与分析:
) J8 h+ o2 I- X) l  减少出错的根本是彻底理解指针。* [* o1 K# C4 i) y0 _
  在方法上,遵循一定的编码规则可能是最立竿见影的方法了,下面我来阐述一下与指针相关的规则:# \  T% F' }- y5 `0 s
  1) 未使用的指针初始化为NULL 。
( G0 J* \( p) |) `" p% w8 z  2) 在给指针分配空间前、分配后均应作判断。
3 |" g* h4 r5 ~! `6 B. a) B, j  3) 指针所指向的内容删除后也要清除指针本身。
5 T2 D9 B6 N1 g- r# w* h  Q% {8 j  要牢记指针是一个复合的结构这个本质,所以我们不论初始化和清除都要同时兼顾指针本身(上述规则1,3)和指针所指向的内容(上述规则2,3)这两个方面。% U9 D; D6 p3 o$ M# E: {5 o
  遵循这些规则可以有效地减少指针出错,我们来看下面的例子:
: J+ B, v8 w: D5 V8 G8 h  void Test
, r" F- _) o1 p/ G, M  }4 q  S1 O6 u; Z
  请问运行Test函数会有什么样的结果?% h9 y8 ^8 o7 A
  答:9 _* m2 ?8 M& j% U# p5 G  S
  篡改动态内存区的内容,后果难以预料,非常危险。因为free;之后,str成为野指针,if语句不起作用。2 h% C( R1 n1 T0 R
  如果我们牢记规则3,在free后增加语句:& b/ y$ j8 V$ E2 ]. {! c
  str = NULL;
0 y) c2 S! r& h8 l  那么,就可以防止这样的错误发生。
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

Archiver|手机版|小黑屋|Woexam.Com ( 湘ICP备18023104号 )

GMT+8, 2024-5-17 12:48 , Processed in 0.204724 second(s), 23 queries .

Powered by Discuz! X3.4 Licensed

© 2001-2017 Comsenz Inc.

快速回复 返回顶部 返回列表