概述- 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, \ 我们可以将参数,尤其是大个的参数(例如结构,对象等),将他们地址作为参数传给函数,这样可以省去编译器为它们制作副本所带来的空间和时间上的开销。 |