; l$ H: w g$ K& F- G& Y2 s1 _0 g6 ?8 B4 O/ F
1.2.2指针所指向的类型3 g: Y) B& J/ C1 I/ g0 e
( @' E+ D% f6 O4 V1 }
当你通过指针来访问指针所指向的内存区时,指针所指向的类型决定了编译器将把那片内存区里的内容当做什么类型来看待。从语法的角度看,指针所指向的类型是指针声明语句中的指针名字和名字左边的指针声明符*去掉所剩下的部分。例如: * h9 T+ w: c( {- k# B# w0 E( i) k; z; J b& I7 T' A
int*ip; //指针所指向的类型是int5 f/ Q h1 \2 b
& c. L/ {! Y3 D8 R7 x8 {
char*ip; //指针所指向的类型是char 3 W$ U/ p7 y8 N$ s% d c! s" k1 O, _6 Z! m. a
int**ip; //指针所指向的类型是int*/ w* P6 W0 Y2 ~$ ^+ A
/ ?: @/ Z$ m/ I; B' j+ N% t1 \
int(*ip)[5]; //指针所指向的类型是int()[5]. e4 A. i& ]+ {, u
4 \$ u( E& [: ?: A
6 N+ O( U# e: U# ]! v* f
1.2.3指针的值(或称指针所指向的内存区)% F) M' u+ ~$ A6 C( `
9 y5 Q2 Z8 R" E; {
指针的值或者叫指针所指向的内存区或地址,是指针本身存储的数值,这个值将被编译器当作一个地址,而不是一个一般的数值。在32位程序里,所有类型的指针的值都是一个32位整数,因为32位程序里内存地址全都是32位长。 指针所指向的内存区就是从指针的值所代表的那个内存地址开始,长度为sizeof(指针所指向的类型)的一片内存区。以后,我们说一个指针的值是XX,就相当于说该指针指向了以XX为首地址的一片内存区域;我们说一个指针指向了某块内存区域,就相当于说该指针的值是这块内存区域的首地址。 g# \0 I C2 }9 V8 f! T
$ ?3 R$ P7 z$ A 指针所指向的内存区和指针所指向的类型是两个完全不同的概念。在上例中,指针所指向的类型已经有了,但由于指针还未初始化,所以它所指向的内存区是不存在的,或者说是无意义的。 ) i: U/ Q7 b! h [4 B6 H* |& B7 e! T5 S% Y9 B3 m* W
以后,每遇到一个指针,都应该问问:这个指针的类型是什么?指针指的类型是什么?该指针指向了哪里?( y3 h* l6 Q$ V, ^, Z" n
* O( ~/ l, N0 x
1.2.4指针本身所占有的内存区 S& o) V5 a3 ?- o1 d0 c& J2 @# a$ d" H$ v
指针本身所占有的内存区是指针本身占内存的大小,这个你只要用函数sizeof(指针的 : e: o5 _3 k/ \1 X4 U4 l) B% s) D5 s8 L# ]* X5 ]9 U
类型)测一下就知道了。在32位平台里,指针本身占据了4个字节的长度。 % C' h1 T' \% M- ]9 W2 K1 j* C, T4 T a: t
指针本身占据的内存这个概念在判断一个指针表达式是否是左值时很有用。0 O9 q0 E0 C# Y. }& B9 Z
" h4 X( B4 ]" ^9 \# QC/C++指针精髓(二)* U) g5 f( O8 u# Y
1.3指针与内存管理 . H4 K' Y1 Q4 d- u . J) h0 S# ~3 H5 G- G6 V 利用指针你可以将数据写入内存中的任意位置,但是,一旦你的程序中有一个野指针("wild“pointer),即指向一个错误位置的指针,你的数据就危险了—存放在堆中的数据可能会被破坏,用来管理堆的数据结构也可能会被破坏,甚至操作系统的数据也可能会被修改,有时,上述三种破坏情况会同时发生。所以合理的正确的分配指针的地址是非常重要的。3 f8 ?+ I0 q" V( j) s! b, g
1 h6 b. B4 Z. K. A6 j7 j! | 1.3.1内存分配的方式; T. T- q6 {7 K
1 D7 M: Y2 v/ }3 g 内存分配方式有三种: 9 o: U0 H% e7 S( R1 K- I% U. q3 z; y1 T9 i3 B
(1)从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static变量。 # s4 K% V+ p) `4 S# |$ u7 f, Z' u$ v# \ p K
(2)在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。 ' x& p" }4 b; N0 `* ]# k+ {7 L& q0 Z8 `" Q1 I% R
(3) 从堆上分配,亦称动态内存分配。程序在运行的时候用malloc或new申请任意多少的内存,程序员自己负责在何时用free或delete释放内存。动态内存的生存期由我们决定,使用非常灵活,但问题也最多,以下我们重点讲解动态内存分配。1 S, e' J. k$ a) N- e: E8 E
& C L! u+ U3 _4 H0 g! B 1.3.2 malloc/free 的使用要点 ! i$ {9 X+ j" j1 I" c. ] " _2 F6 G) J' O% x; { malloc与free是C/C++语言的标准库函数,它用于申请动态内存和释放内存。- r3 ?9 ~6 Y1 C* p e$ T) p+ ?9 K) c
& Z; O1 n* g1 E: N+ G1 x 函数malloc的原型如下: 9 h8 K/ F y) D/ i3 m M: W" H 3 o3 H0 G! K' V1 k6 M# D void * malloc(size_t size); % ]/ \0 M! R' W# a7 ` 6 x G' O! Q4 v8 W% ?/ [ 用malloc申请一块长度为length的整数类型的内存,程序如下: $ b: q8 ]5 V- N2 {% I9 @ 8 R! d9 x/ K$ u, e$ P: v7 z int *ip = (int *) malloc(sizeof(int) * length);1 H6 _9 @1 D2 z% G% M
2 B0 i* L( D' q& g* s" o R 我们应当把注意力集中在两个要素上:“类型转换”和“sizeof”。: Y, g$ E6 J& n2 W+ V9 u
" B% a. {# P) Q6 U; k
malloc函数返回值的类型是void *,所以在调用malloc时要显式地进行类型转换,将void * 转换成所需要的指针类型。 , I i; a, T2 Z' M : @4 E# ^: Q' w! C" R malloc函数本身并不识别要申请的内存是什么类型,它只关心内存的总字节数。例如int变量在16位系统下是2个字节,在32位下是4个字节;而float变量在16位系统下是4个字节,在32位下也是4个字节。这个你可以用sizeof(类型)去测试。4 _0 i% A5 @3 A" K5 S) E
8 _. S, @' v6 Y- c0 J' k, [, I" s- ~ 在malloc的“()”中使用sizeof运算符是良好的风格,但要当心有时我们会昏了头,写出 ip = malloc(sizeof(ip))这样的程序来。 Q) ~6 [7 D0 Q! s3 @0 ?* I9 \0 H" G7 q+ X J: U- r" A9 |
函数free的原型如下:8 @( U n( r A
9 F2 {6 a: O* ?
void free( void * memblock ); 1 A0 F7 j" A: Q! s( k 7 m% w% v1 V4 x 为什么free函数不象malloc函数那样复杂呢?这是因为指针p的类型以及它所指的内存的容量事先都是知道的,语句free(p)能正确地释放内存。如果p是NULL指针,那么free对p无论操作多少次都不会出问题。如果p不是NULL指针,那么free对p连续操作两次就会导致程序运行错误。 B) W* A# J0 o1 e2 ]
2 C+ {3 V& x$ _2 s( _) T 1.3.3 new/delete 的使用要点 % D& ~0 I/ C4 v+ M. k& V2 t * O+ h Y9 V# u1 x 对于非内部数据类型的对象而言,光用maloc/free无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free.8 ]5 {, [/ f, j4 V/ ]) H' f) g
/ g9 Z2 F" }6 W8 f7 V* G# A 因此C++语言需要一个能完成动态内存分配和初始化工作的运算符new,以及一个能完成清理与释放内存工作的运算符delete.注意new/delete不是库函数,只是C++的运算符。我们来看如下例子就知道怎么回事了。 9 @4 k3 }0 A$ A; j R4 N6 _2 X# ?* l) B- c7 d6 x
class Object : ]$ D( e' z& X# a' u! c) t+ w3 ]' h) V! N, E/ I8 S; I
{: ~5 \! d# p. k0 e5 R3 X
3 d) S% I" S$ `+ ^
public : 5 R6 q0 k' Z4 U3 W4 i" A/ r ; p8 W# a" B# c7 k3 Z9 Q Object(void){std::cout << “Initialization”<< std::endl; } : l3 E/ j$ n$ K6 d ) i* X6 D3 ^- G0 r: x ~Object(void){std::cout << “Destroy”<< std::endl; } 1 W, N2 f J' w1 x # p$ X/ s& x3 q# Q void Initialize(void){std:: cout << “Initialization”<< std::endl; }" @+ ]! ]& B0 X4 j# ~ S: @& d
% Y. |% P& u- Q+ g0 S4 ^0 ~ void Destroy(void){ std::cout << “Destroy”<< std::endl; } 8 Q& ?% \0 V8 V p+ K ; Z" |: A: a+ `: C5 \} " m r& a8 x5 A I5 e% T$ o3 O" F9 {; L3 ?: L( ?& N) t
void UseMallocFree(void) % \1 G& H6 I4 d* R8 V# J/ U5 j* o i
{# D* @* }8 h5 o0 d
: I |9 L1 p5 c+ ]" z
Object *ip = (Object *)malloc(sizeof(Object)); // 申请动态内存 9 ]6 a8 k, y# X x% c0 O; M" D/ H; h1 g' l# j4 V% E' P
ip->Initialize(); // 初始化' a* E+ |7 q6 w7 F6 i
" L6 @, H# b" ]$ ?1 d* `( g
//… ( N6 E5 ~) Q. q" \+ L! d' d2 K6 B+ L: Q L% I
ip->Destroy(); // 清除工作0 ^, G# h. {2 _ k
2 _, r% W# d9 ~# ]6 \5 u2 _: m3 b free(ip); // 释放内存 7 Q' D2 M) R: o1 M+ ~4 q5 `# L q# z( v& V. g# H
}$ J9 g& z% Z4 ~
( a' U$ V/ e* N# H6 V} / R7 }% b3 y; ^( @. l' {8 B# l8 D2 M, H& a
! [0 W, {8 I+ q1 ~% w8 ]4 o
! n& P) F$ [, _
, A4 h0 d8 ]1 Y: t( e" y. h
(3)为new和malloc设置异常处理函数。例如Visual C++可以用_set_new_hander函数为new设置用户自己定义的异常处理函数,也可以让malloc享用与new相同的异常处理函数。详细内容请参考C++使用手册。 * o" f2 D& _) ]# j7 g2 m' D/ ~& F, u! s8 `- _; ~: S9 o
有一个很重要的现象要告诉大家。对于32位以上的应用程序而言,无论怎样使用malloc与new,几乎不可能导致“内存耗尽”。因为32位操作系统支持“虚存”,内存用完了,自动用硬盘空间顶替。我不想误导读者,必须强调:不加错误处理将导致程序的质量很差,千万不可因小失大。6 { p4 P3 H4 G# Q& Z1 S& B
9 G, n5 W7 f9 v- e% l, R. U
1.3. 5杜绝“野指针”, E7 m: g& x) E0 ^. M
, g+ S/ `8 u# b4 P8 K6 ~: {) G “野指针”不是NULL指针,是指向“垃圾”内存的指针。人们一般不会错用NULL指针,因为用if语句很容易判断。但是“野指针”是很危险的,if语句对它不起作用。 “野指针”的原因主要有如下几种: " P5 |" k! }& Y5 j1 m( a# g/ N4 d% b* b
(1)指针变量没有被初始化。任何指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的,它会乱指一气。所以,指针变量在创建的同时应当被初始化,要么将指针设置为NULL,要么让它指向合法的内存。例如 4 [% ?7 O4 e# C2 |8 B4 k. N' U2 o# h$ L
char *ip = NULL;% R; c( O, y! n6 @3 J, @
* K: i2 m9 K/ c1 T3 u7 y. F
char *ip = new char; # o; y$ q5 S7 h/ l. l3 W: p' G0 D5 S L. F) F5 a" d
(2)指针ip被free或者delete之后,没有置为NULL,让人误以为ip是个合法的指针。; n5 l: G0 b8 y S" ]/ G# L1 I# h
/ j# L5 V; ]# k$ @& [
(3)指针操作超越了变量的作用范围。这种情况让人防不胜防,示例程序如下: ! s/ f0 S1 @. U3 K; e / u# r4 V) B7 e% G( h class A 4 x% N/ t2 Q W1 u. X. L3 x , ], C. R' L, T) k{3 o1 S' ]+ }( e: M7 `$ Y1 b
5 R) g" J2 b; | public: & \9 q9 P8 T) a * s- m* O) G0 H4 T/ K5 o8 N void Func(void){ std::cout << “Func of class A” << std::endl; } 8 O$ q0 D# K: a% P + D' P' A' }$ s5 M$ f6 U}; , D! _( d8 H2 l* \ @; F- J% n6 m- D1 g, |
void Test(void) 1 t4 y3 ]8 @5 U% h: x" n7 Z$ H8 V7 d/ ]5 z; j# {3 L2 s
{ / ], p- p; ^" ?. T1 I% e 5 K# Z& \$ c1 ~. i0 R A *p; 1 H$ d) X& [( U/ w+ o- N; e& x7 k
{ 1 C( a/ h% P* s) t 5 Z3 X! H3 B8 e/ X- n4 p A a;5 V( b; E! M3 a9 Q* C" K1 }/ B
9 C) ?7 d1 q/ i* p% Y7 }* v7 T
p = &a; // 注意 a 的生命期 ' Y3 j* x7 Q# \* f. b- y. Y 9 _ L9 v& W. F! U( x3 l } A" y1 w) T3 T: s! c
+ k x9 E3 M3 L8 f$ C( S/ Y6 i. g p->Func(); // p是“野指针”% c- w7 }. T- Z. V0 x( e+ _9 l( m
) s2 }, S3 N0 {7 m+ c8 ^ x}! v. f: Q; e0 {" l& A- T
6 S8 z' v4 v8 j6 \: n: T* X
$ C! w" d6 D3 E+ ?1 Y# u& m
函数Test在执行语句p->Func()时,对象a已经消失,而p是指向a的,所以p就成了“野指针”。但奇怪的是有些编译器运行这个程序时居然没有出错,这可能与编译器有关。 2 @) N' O! h& F- a7 J % v* g& H* G c7 J# V 1.3.6指针参数是如何传递内存的?- d% Q" s; m8 K$ _& J6 @4 {
% e/ q" w! X1 ~! J 如果函数的参数是一个指针,不要指望用该指针去申请动态内存。见如下例子:/ f( J6 S! P, }! F! ?
5 U4 l4 y. V, z7 }9 C F& ^7 Z7 X( g
void GetMemory(char *ip, int num)% S( Y1 g: @) ^6 c$ Q
( U5 I+ D5 g. D$ e{8 s; ]6 C' T. Q: G( f7 U4 k$ }
; N( d# k; E, J3 | p0 @" V
ip = (char *)malloc(sizeof(char) * num);, T6 e$ q. C. H/ C7 a7 s" ^" Q
2 y0 s- U1 r' O' x0 H4 l
} : D( B+ @, ^) D8 J, G- B" S# Z9 B" e6 x' k a% M5 v1 t
void Test(void) 0 N+ X" u u- X1 a " H X: u E. @* `( a9 Y8 E{ z0 ~+ R1 ?9 n z9 x/ g8 N5 c+ E
5 {5 \1 t7 ?2 t" |. B
char *str = NULL;9 G V$ Q( p) D# Y+ f7 l5 o; p* ~
/ ~; Y9 _4 q' m GetMemory(str, 100); // str 仍然为 NULL f6 p _$ b; B$ E 0 A8 g- n/ A/ z strcpy(str, "hello"); // 运行错误' _; j- y6 M( _3 u! Y6 u! w
4 O3 f& u' [; b/ d+ ?: Y7 }}0 X: _, @! b+ v" T2 K' E. d
2 [2 g% O! L6 Q& {3 b
4 P( h& T% b* W$ n) R
试图用指针参数申请动态内存. G- a( `9 g0 c- b6 z- R" N
% N; ]/ N" a4 Z8 N1 z4 [
毛病出在函数GetMemory中。编译器总是要为函数的每个参数制作临时副本,指针参数ip的副本是 _ip,编译器使 _ip = ip.如果函数体内的程序修改了_ip的内容,就导致参数ip的内容作相应的修改。这就是指针可以用作输出参数的原因。在本例中,_ip申请了新的内存,只是把_ip所指的内存地址改变了,但是ip丝毫未变。所以函数GetMemory并不能输出任何东西。事实上,每执行一次GetMemory就会泄露一块内存,因为没有用free释放内存。5 {2 N$ M& L- \# G
( n+ _( E7 w' M# R 如果非得要用指针参数去申请内存,那么应该改用“指向指针的指针(二级指针)”,见如下示例:& Y* t/ R2 m8 j' T" O
% s# S4 f7 c/ r1 R/ z void GetMemory(char **p, int num)0 o; r, s3 p$ i' p+ @$ u z
2 y& U8 V% u7 K {2 e
{ 5 Z M2 l [$ v& Z5 k ]6 O% T* r# a, ~6 w9 D
*ip = (char *)malloc(sizeof(char) * num);, K4 s3 w- l8 W: T1 g
+ A' `- m: M. V$ j2 ?% x' G% k 1.4.1.4把数组的首地址赋予指向数组的指针变量。例如: 4 q2 Q5 E* K! B8 }4 U2 ^- y- f1 i' m% n% R; {
int a[5],*pa;5 e' I0 o( F; t+ ~5 x
7 i" a8 b) r6 H8 R- O
pa=a; //数组名表示数组的首地址,故可赋予指向数组的指针变量pa $ m* ]6 O% O$ L3 O6 V5 I( s i( e- [9 ]" z7 Q3 J8 O: h! r
也可写为: 0 b8 z$ C( z& g( @% V; q ; {& N, ^# r4 ~8 u& G" D. Q: G pa=&a[0]; //数组第一个元素的地址也是整个数组的首地址也可赋予pa3 L/ j2 ]8 ~4 j% B0 T
7 T" A: A+ `9 U* Z1 S- | 当然也可采取初始化赋值的方法:9 l9 x2 c1 b% `& z" _" q' j5 Y6 }
$ y, {1 L3 I7 Y& z5 u2 H1 \2 o( d: P
int a[5],*pa=a;( {0 \, t$ I/ P1 B2 k& M
( c) I- {( J- q5 C1 k 以上是一些基本的数组赋值方法,后面我们会详细讨论指针在数组中的使用。 X' o3 ~; A% [$ m. v4 k1 m
; j2 z) O2 L; g+ d' n
1.4.1.5把字符串的首地址赋予指向字符类型的指针变量。例如:) B% ~1 Y& |: x
8 K$ L( h7 y( N5 p
char *pc;$ s* X8 M. U" j' v
1 G. b, H0 l% l, T4 D! h pc="c language";% U7 H$ k' A7 f) o
! K2 }& U3 v8 w. |( D
或用初始化赋值的方法写为: ]! _* m- o3 G : ` z' U2 n& s% A6 a/ Q6 I char *pc=" c language "; 2 `* T+ S' ?. z2 c% N * S C! M3 f" W: ~' v( I2 s* h 这里应说明的是并不是把整个字符串装入指针变量, 而是把存放该字符串的字符数组的首地址装入指针变量。 4 s% s! s. O) K; X, {( f 4 b( l9 F% Y! j# d6 D2 Q 1.4.1.6把函数的入口地址赋予指向函数的指针变量。例如:. N p/ B& s1 P( _$ n; O1 l
2 n5 b$ J9 u" o5 T
int (*pf)(); + @% e% u7 J) b* Y" N' ?/ B / S! p- D* | U& t- z pf=f; //f为函数名' S, d3 ^: L1 X0 R0 [" L6 l2 @
8 i6 ~2 o& w5 V) O7 f4 | 1.4.2加减运算1 A( Z) G8 x' y$ S B3 ~8 ?$ H; j
! |: C6 B3 E3 t/ J3 T
对于指向数组的指针变量,可以加上或减去一个整数n.设ip是指向数组a的指针变量,则ip+n,ip-n,ip++,++ip,ip——,——ip 运算都是合法的。指针变量加或减一个整数n的意义是把指针指向的当前位置(指向某数组元素)向前或向后移动n个位置。应该注意,数组指针变量向前或向后移动一个位置和地址加1或减1 在概念上是不同的。因为数组可以有不同的类型, 各种类型的数组元素所占的字节长度是不同的。如指针变量加1,即向后移动1 个位置表示指针变量指向下一个数据元素的首地址。而不是在原地址基础上加1.看如下例子: ! b2 w& }( V, Q* b 2 \; F E+ D$ @* p; E0 G p) {, {8 Z
char a[20]; & Q" z& ~" V3 a5 _
int*ip=a; / W* f6 i8 P8 ~
... 1 f# {% |5 s7 _& N
ip++; ) Q0 I$ ^2 W' ?+ x4 V . ?6 T1 x9 T5 }0 n
6 N! ]# `: z/ S! L
在上例中,指针ip的类型是int*,它指向的类型是int,它被初始化为指向整形变量a.接下来的第3句中,指针ip被加了1,编译器是这样处理的:它把指针ip的值加上了sizeof(int),在32位程序中,是被加上了4.由于地址是用字节做单位的,故ip所指向的地址由原来的变量a的地址向高地址方向增加了4个字节。, ]6 v, N5 ]/ O
) Y( F$ Y; }# Q. s) v3 a
由于char类型的长度是一个字节,所以,原来ptr是指向数组a的第0号单元开始的四个字节,此时指向了数组a中从第4号单元开始的四个字节。再看如下例子:6 P# }3 R2 Z" {. i
9 J& j' U; j1 D9 \( `8 C! D; p6 K5 ]" ], V4 s" r/ R$ f
char a[20]; $ V: O' V1 L- n. ^3 t e$ | int*ip=a; 7 z/ k4 t, O+ R$ D
... 6 t* n- c1 [ H% g; e ip+=5;4 t; z& d Y/ i4 ]
0 E* O' d& q w
/ X( H; n& L6 F6 u4 B$ n2 u& j 在这个例子中,ip被加上了5,编译器是这样处理的:将指针ip的值加上5乘sizeof(int),在32位程序中就是加上了5乘4=20.由于地址的单位是字节,故现在的ip所指向的地址比起加5后的ip所指向的地址来说,向高地址方向移动了20个字节。在这个例子中,没加5前的ip指向数组a的第0号单元开始的四个字节,加5后,ptr已经指向了数组a的合法范围之外了。虽然这种情况在应用上会出问题,但在语法上却是可以的。这也体现出了指针的灵活性。 4 s% m6 a+ O i [( W3 u% F6 R# l E& {# J5 [8 J9 }
如果上例中,ip是被减去5,那么处理过程大同小异,只不过ip的值是被减去5乘sizeof(int),新的ip指向的地址将比原来的ip所指向的地址向低地址方向移动了20个字节。1 D) C7 X5 O- U5 g