
说到指针,估计还是有很多小伙伴都还是云里雾里的,有点“知其然,而不知其所以然”。但是,不得不说,学了指针,C语言才能算是入门了。指针是C语言的「精华」,可以说,对对指针的掌握程度,「直接决定」了你C语言的编程能力。 在讲指针之前,我们先来了解下变量在「内存」中是如何存放的。 在程序中定义一个变量,那么在程序编译的过程中,系统会根据你定义变量的类型来分配「相应尺寸」的内存空间。那么如果要使用这个变量,只需要用变量名去访问即可。 通过变量名来访问变量,是一种「相对安全」的方式。因为只有你定义了它,你才能够访问相应的变量。这就是对内存的基本认知。但是,如果光知道这一点的话,其实你还是不知道内存是如何存放变量的,因为底层是如何工作的,你依旧不清楚。 那么如果要继续深究的话,你就需要把变量在内存中真正的样子是什么搞清楚。内存的最小索引单元是1字节,那么你其实可以把内存比作一个超级大的「字符型数组」。在上一节我们讲过,数组是有下标的,我们是通过数组名和下标来访问数组中的元素。那么内存也是一样,只不过我们给它起了个新名字:地址。每个地址可以存放「1字节」的数据,所以如果我们需要定义一个整型变量,就需要占据4个内存单元。 那么,看到这里你可能就明白了:其实在程序运行的过程中,完全不需要变量名的参与。变量名只是方便我们进行代码的编写和阅读,只有程序员和编译器知道这个东西的存在。而编译器还知道具体的变量名对应的「内存地址」,这个是我们不知道的,因此编译器就像一个桥梁。当读取某一个变量的时候,编译器就会找到变量名所对应的地址,读取对应的值。 初识指针和指针变量那么我们现在就来切入正题,指针是个什么东西呢? 所谓指针,就是内存地址(下文简称地址)。C语言中设立了专门的「指针变量」来存储指针,和「普通变量」不一样的是,指针变量存储的是「地址」。 定义指针指针变量也有类型,实际上取决于地址指向的值的类型。那么如何定义指针变量呢: 很简单:类型名* 指针变量名 char* pa;//定义一个字符变量的指针,名称为paint* pb;//定义一个整型变量的指针,名称为pb; U. w2 i c7 P# H: d3 c float* pc;//定义一个浮点型变量的指针,名称为pc5 e% N; ~$ N8 L" M- n1 o/ W% r 注意,指针变量一定要和指向的变量的类型一样,不然类型不同可能在内存中所占的位置不同,如果定义错了就可能导致出错。 取地址运算符和取值运算符获取某个变量的地址,使用取地址运算符&,如: char* pa = &a;int* pb = &f;" ?0 _& t* B4 c 如果反过来,你要访问指针变量指向的数据,那么你就要使用取值运算符*,如: printf("%c, %d\n", *pa, *pb);这里你可能发现,定义指针的时候也使用了*,这里属于符号的「重用」,也就是说这种符号在不同的地方就有不同的用意:在定义的时候表示「定义一个指针变量」,在其他的时候则用来「获取指针变量指向的变量的值」。 直接通过变量名来访问变量的值称之为直接访问,通过指针这样的形式访问称之为间接访问,因此取值运算符有时候也成为「间接运算符」。 比如: //Example 01; G. u3 _, H7 ?% t" {//代码来源于网络,非个人原创' [$ I' w! i1 Q& D( D9 T #include <stdio.h>5 Q) s& Z1 y7 t int main(void) { char a = 'f'; int f = 123;& \0 ~4 C1 \' s- E: Z; r char* pa = &a; int* pf = &f; printf("a = %c\n", *pa); printf("f = %d\n", *pf); *pa = 'c'; *pf += 1; printf("now, a = %c\n", *pa);9 [2 R" P9 j+ k( m! e* x& S printf("now, f = %d\n", *pf);* X, K/ W# r- {- m+ \ printf("sizeof pa = %d\n", sizeof(pa));9 J8 j1 s* b) q4 F. T2 ] printf("sizeof pf = %d\n", sizeof(pf)); ' B5 D' ?" l: l' k& b printf("the addr of a is: %p\n", pa); printf("the addr of f is: %p\n", pf);9 I4 L% A4 m0 {& t return 0;7 ?9 ~$ a# L2 E' l9 _ }2 C1 h. A- H6 O/ K3 I! \6 t 程序实现如下: //Consequence 011 v o2 A4 }# c: G. ua = f f = 123 now, a = c now, f = 124 sizeof pa = 4: \& [( h6 j3 e. I8 P/ { sizeof pf = 4 the addr of a is: 00EFF97F' d, }* B7 y+ f f+ t- ` the addr of f is: 00EFF970$ S% [9 |7 ?, |* p 避免访问未初始化的指针void f() { int* a; *a = 10; } 像这样的代码是十分危险的。因为指针a到底指向哪里,我们不知道。就和访问未初始化的普通变量一样,会返回一个「随机值」。但是如果是在指针里面,那么就有可能覆盖到「其他的内存区域」,甚至可能是系统正在使用的「关键区域」,十分危险。不过这种情况,系统一般会驳回程序的运行,此时程序会被「中止」并「报错」。要是万一中奖的话,覆盖到一个合法的地址,那么接下来的赋值就会导致一些有用的数据被「莫名其妙地修改」,这样的bug是十分不好排查的,因此使用指针的时候一定要注意初始化。 指针和数组有些读者可能会有些奇怪,指针和数组又有什么关系?这俩货明明八竿子打不着井水不犯河水。别着急,接着往下看,你的观点有可能会改变。 数组的地址我们刚刚说了,指针实际上就是变量在「内存中的地址」,那么如果有细心的小伙伴就可能会想到,像数组这样的一大摞变量的集合,它的地址是啥呢? 我们知道,从标准输入流中读取一个值到变量中,用的是scanf函数,一般貌似在后面都要加上&,这个其实就是我们刚刚说的「取地址运算符」。如果你存储的位置是指针变量的话,那就不需要。 //Example 02int main(void) { int a; int* p = &a;+ c6 w) B9 {0 y D printf("请输入一个整数:");/ r/ {3 [# K2 _ scanf("%d", &a);//此处需要&% H5 r1 E6 ?/ u( ?/ g5 ^ printf("a = %d\n", a);* s2 H5 Z$ c. N& U2 ~: p printf("请再输入一个整数:");3 w. C& O& e! S3 @2 }" K% m5 r scanf("%d", p);//此处不需要& printf("a = %d\n", a);3 p h7 ]% L- [' H' E+ m0 ]/ C 1 s+ C6 L* Z8 l; G0 g0 ? return 0; }* P: k' G/ C' |4 k5 C 程序运行如下: //Consequence 02 `# L' @: r0 }) p$ H请输入一个整数:1 a = 12 V7 r9 o; U, b8 i 请再输入一个整数:2 a = 2 在普通变量读取的时候,程序需要知道这个变量在内存中的地址,因此需要&来取地址完成这个任务。而对于指针变量来说,本身就是「另外一个」普通变量的「地址信息」,因此直接给出指针的值就可以了。 试想一下,我们在使用scanf函数的时候,是不是也有不需要使用&的时候?就是在读取「字符串」的时候: //Example 032 `; E3 r" x8 g" P* B#include <stdio.h> int main(void) { char url[100]; \: Q' r7 C" _. O7 \- d9 \ url[99] = '\0'; printf("请输入TechZone的域名:");- {' ]- J# {: G# A! }6 ~ scanf("%s", url);//此处也不用& printf("你输入的域名是:%s\n", url);2 l5 A0 n* }, b9 W. `8 [' ? return 0;7 C, r, Y: h1 T } 程序执行如下: //Consequence 03请输入TechZone的域名:www.techzone.ltd. p+ [2 T; ]6 [ 你输入的域名是:www.techzone.ltd 因此很好推理:数组名其实就是一个「地址信息」,实际上就是数组「第一个元素的地址」。咱们试试把第一个元素的地址和数组的地址做个对比就知道了: //Example 03 V2#include <stdio.h>& \2 u6 S9 M3 R5 {. { int main(void)3 i! a2 V" n( V$ a { char url[100];% k4 `2 ]) t8 a printf("请输入TechZone的域名:");6 [3 m$ F0 B0 J7 _ url[99] = '\0'; scanf("%s", url);8 r* q+ C% r7 u2 L. T$ } printf("你输入的域名是:%s\n", url);3 r! E. \4 @5 T Q' e) x$ P, ~: V! G printf("url的地址为:%p\n", url);& w7 p- ^9 e# z2 x. c printf("url[0]的地址为:%p\n", &url[0]); 2 m4 o3 R1 }8 v if (url == &url[0]) { printf("两者一致!");& K( j8 E2 [- O9 e$ X4 Q } else! r! H/ L: h4 B- M5 ^ {% W Q( p+ z2 [& M printf("两者不一致!"); } return 0; }: o) m; d9 G$ j1 @3 l. y 程序运行结果为: //Comsequense 03 V2! y$ _- {! v/ Q4 B* L请输入TechZone的域名:www.techzone.ltd- v; o+ V2 ~$ @' {8 h& J 你输入的域名是:www.techzone.ltd! @- a. Q9 B: u url的地址为:0063F804 url[0]的地址为:0063F804 两者一致! 这么看,应该是实锤了。那么数组后面的元素也就是依次往后放置,有兴趣的也可以自己写代码尝试把它们输出看看。 指向数组的指针刚刚我们验证了数组的地址就是数组第一个元素的地址。那么指向数组的指针自然也就有两种定义的方法: ...char* p;4 [+ w! D& r. s+ m! m! { //方法1+ V4 U6 i$ @) o7 w1 C- s# W/ Z: p p = a;# M p( m5 F1 G# O4 t7 ~ //方法2! }- t, Z7 _' V9 v, I/ H ^# w$ D p = &a[0]; 指针的运算 当指针指向数组元素的时候,可以对指针变量进行「加减」运算,+n表示指向p指针所指向的元素的「下n个元素」,-n表示指向p指针所指向的元素的「上n个元素」。并不是将地址加1。 如: //Example 04#include <stdio.h>4 X1 [4 e) W. Y' e: a int main(void) { int a[] = { 1,2,3,4,5 };0 s N( X1 K# P: g7 y4 X int* p = a;" J7 n4 ~) ^2 `0 b [( t printf("*p = %d, *(p+1) = %d, *(p+2) = %d\n", *p, *(p + 1), *(p + 2)); printf("*p -> %p, *(p+1) -> %p, *(p+2) -> %p\n", p, p + 1, p + 2);: n! j( s! G# i0 k2 G4 U& M return 0; }7 k" }9 s/ R j, U2 w' `2 m3 i 执行结果如下: //Consequence 04- ~' x7 f& L$ g& _5 h. s1 c*p = 1, *(p+1) = 2, *(p+2) = 3 *p -> 00AFF838, *(p+1) -> 00AFF83C, *(p+2) -> 00AFF840 有的小伙伴可能会想,编译器是怎么知道访问下一个元素而不是地址直接加1呢? 其实就在我们定义指针变量的时候,就已经告诉编译器了。如果我们定义的是整型数组的指针,那么指针加1,实际上就是加上一个sizeof(int)的距离。相对于标准的下标访问,使用指针来间接访问数组元素的方法叫做指针法。 其实使用指针法来访问数组的元素,不一定需要定义一个指向数组的单独的指针变量,因为数组名自身就是指向数组「第一个元素」的指针,因此指针法可以直接作用于数组名: ...+ Y1 |. D# [ g4 _printf("p -> %p, p+1 -> %p, p+2 -> %p\n", a, a+1, a+2); printf("a = %d, a+1 = %d, a+2 = %d", *a, *(a+1), *(a+2)); ... 执行结果如下: p -> 00AFF838, p+1 -> 00AFF83C, p+2 -> 00AFF8409 s+ n! m6 P# ?2 j/ C' ib = 1, b+1 = 2, b+2 = 3- i; J+ H3 _9 e1 P) v; B: | 现在你是不是感觉,数组和指针有点像了呢?不过笔者先提醒,数组和指针虽然非常像,但是绝对「不是」一种东西。 甚至你还可以直接用指针来定义字符串,然后用下标法来读取每一个元素: //Example 05//代码来源于网络 #include <stdio.h> #include <string.h>- j8 V+ ~3 X' n0 F+ B int main(void)+ ]* C1 l$ P7 N { g6 w7 J1 D+ @" b) q char* str = "I love TechZone!";$ M% _0 J x( ? int i, length; length = strlen(str);1 r9 U8 n R3 M' U5 Q for (i = 0; i < length, i++)4 l8 F, f2 R7 W9 }4 Y { printf("%c", str); }" w4 B3 K- ]1 S printf("\n"); return 0;" S" d, b9 B# _ } 程序运行如下: //Consequence 05: S4 w9 N) k ^" g2 A! O, dI love TechZone! 在刚刚的代码里面,我们定义了一个「字符指针」变量,并且初始化成指向一个字符串。后来的操作,不仅在它身上可以使用「字符串处理函数」,还可以用「下标法」访问字符串中的每一个字符。 当然,循环部分这样写也是没毛病的: ...for (i = 0, i < length, i++) {7 P; O* B7 X" Z- e printf("%c", *(str + i));& r8 h) `1 f$ x( V( }; s0 K } 这就相当于利用了指针法来读取。 指针和数组的区别刚刚说了许多指针和数组相互替换的例子,可能有的小伙伴又开始说:“这俩货不就是一个东西吗?” 随着你对指针和数组越来越了解,你会发现,C语言的创始人不会这么无聊去创建两种一样的东西,还叫上不同的名字。指针和数组终究是「不一样」的。 比如笔者之前看过的一个例子: //Example 06//代码来源于网络 #include <stdio.h>. L5 [, F: x Z) Z. C) m int main(void) { char str[] = "I love TechZone!";+ r m9 P( V- A6 u$ g+ Q! b int count = 0; while (*str++ != '\0') { count++; } printf("总共有%d个字符。\n", count); return 0; } 当编译器报错的时候,你可能会开始怀疑你学了假的C语言语法: //Error in Example 06错误(活动) E0137 表达式必须是可修改的左值 错误 C2105 “++”需要左值 我们知道,*str++ != ‘\0’是一个复合表达式,那么就要遵循「运算符优先级」来看。具体可以回顾《C语言运算符优先级及ASCII对照表》。 str++比*str的优先级「更高」,但是自增运算符要在「下一条语句」的时候才能生效。所以这个语句的理解就是,先取出str所指向的值,判断是否为\0,若是,则跳出循环,然后str指向下一个字符的位置。 看上去貌似没啥毛病,但是,看看编译器告诉我们的东西:表达式必须是可修改的左值 ++的操作对象是str,那么str到底是不是「左值」呢? 如果是左值的话,那么就必须满足左值的条件。 ❝ 第一点,数组名str是可以满足的,因为数组名实际上就是定位数组第一个元素的位置。但是第二点就不满足了,数组名实际上是一个地址,地址是「不可以」修改的,它是一个常量。如果非要利用上面的思路来实现的话,可以将代码改成这样: //Example 06 V2& v$ O, Z* C$ P- m& q9 {$ k% r- M6 L2 B//代码来源于网络. ^8 d, y( c/ m; p/ s2 W' d0 P #include <stdio.h>+ B' q0 |- c/ m# u; I: v int main(void)* P x% r% j' q$ W1 h. L0 @ { char str[] = "I love TechZone!";3 D$ J% J: W9 T char* target = str; int count = 0; while (*target++ != '\0') { count++;& L$ J* D! m& a! G } printf("总共有%d个字符。\n", count);# b; g7 B" F4 d" B2 b 1 V7 F0 v% ^. P$ h8 K' a; i4 _ return 0;+ i, w0 l3 F6 T I4 n } 这样就可以正常执行了: //Consequence 06 V24 i& ~- w3 V& N& ^总共有16个字符。. ~& d( w1 i3 K E7 y 这样我们就可以得出:数组名只是一个「地址」,而指针是一个「左值」。 指针数组?数组指针?看下面的例子,你能分辨出哪个是指针数组,哪个是数组指针吗? int* p1[5];6 A5 l; \+ z }6 b( bint(*p2)[5]; 单个的我们都可以判断,但是组合起来就有些难度了。 答案: int* p1[5];//指针数组, Z2 d6 k1 ?! `- i( o" d8 I; w! y# xint(*p2)[5];//数组指针/ T9 h! _' }% f6 u# q 我们挨个来分析。 指针数组数组下标[]的优先级是最高的,因此p1是一个有5个元素的「数组」。那么这个数组的类型是什么呢?答案就是int*,是「指向整型变量的指针」。因此这是一个「指针数组」。 那么这样的数组应该怎么样去初始化呢? 你可以定义5个变量,然后挨个取地址来初始化。 不过这样太繁琐了,但是,并不是说指针数组就没什么用。 比如: //Example 07#include <stdio.h>2 r0 p" F7 a1 }& z, K3 ^' x int main(void); s! R7 c6 C1 s" y4 X {$ k! t) S( g1 k2 s' i4 v( _ char* p1[5] = {, W/ K6 C% }: l! d" B+ E; s "人生苦短,我用Python。", "PHP是世界上最好的语言!", "One more thing...",- q; `1 r0 p' C s& L "一个好的程序员应该是那种过单行线都要往两边看的人。", "C语言很容易让你犯错误;C++看起来好一些,但当你用它时,你会发现会死的更惨。": x; ], m1 a" K; ^0 h! f }; int i; for (i = 0; i < 5; i++)/ j8 J, s! E! R2 _# P: w { printf("%s\n", p1); } return 0;7 i' M- K1 ?1 M0 p } 结果如下: //Consequence 07; X- B6 B) W3 S: A: g Z人生苦短,我用Python。6 d$ U1 l6 t/ h% @; L PHP是世界上最好的语言!( @- }- i5 Y6 X1 l k# C One more thing...6 m" e, D: z1 B+ H! v! h3 C* f6 N* @ 一个好的程序员应该是那种过单行线都要往两边看的人。 C语言很容易让你犯错误;C++看起来好一些,但当你用它时,你会发现会死的更惨。 这样是不是比二维数组来的更加直接更加通俗呢? 数组指针()和[]在优先级里面属于「同级」,那么就按照「先后顺序」进行。 int(*p2)将p2定义为「指针」, 后面跟随着一个5个元素的「数组」,p2就指向这个数组。因此,数组指针是一个「指针」,它指向的是一个数组。 但是,如果想对数组指针初始化的时候,千万要小心,比如: //Example 08" l6 n1 v" U; f+ N; P! |, w* U#include <stdio.h> int main(void) {) b0 w$ d$ T+ S! e9 F& l. Q# w int(*p2)[5] = {1, 2, 3, 4, 5};, R+ B E( E9 _) m# v0 W int i;# \; T- p/ b& }8 _) P% F p7 V for (i = 0; i < 5; i++) { printf("%d\n", *(p2 + i));7 v* u# k7 J( @8 M+ x7 l2 y4 |7 n* X } return 0; } Visual Studio 2019报出以下的错误: //Error and Warning in Example 08错误(活动) E0146 初始值设定项值太多 错误 C2440 “初始化”: 无法从“initializer list”转换为“int (*)[5]” 警告 C4477 “printf”: 格式字符串“%d”需要类型“int”的参数,但可变参数 1 拥有了类型“int *”* T1 j8 V5 h0 [! w! s3 p 这其实是一个非常典型的错误使用指针的案例,编译器提示说这里有一个「整数」赋值给「指针变量」的问题,因为p2归根结底还是指针,所以应该给它传递一个「地址」才行,更改一下: //Example 08 V2#include <stdio.h>' z7 V/ i4 E: y3 E+ T/ K1 E$ z2 T: F int main(void). S( e$ p# a! U# V2 M {' V+ \+ C- F) e. h& G1 M int temp[5] = {1, 2, 3, 4, 5}; int(*p2)[5] = temp; int i; for (i = 0; i < 5; i++)) U& f* g* L7 p$ Y" q ?; R; u {) M0 `! F2 @ O/ p printf("%d\n", *(p2 + i));9 i! b& I; M) r( z& \( M$ R* t } return 0;- a* S$ J: a& l/ Y( v. u3 ~ } //Error and Warning in Example 08 V2 错误(活动) E0144 "int *" 类型的值不能用于初始化 "int (*)[5]" 类型的实体" ]/ o# k n, J% x$ `; f 错误 C2440 “初始化”: 无法从“int [5]”转换为“int (*)[5]” 警告 C4477 “printf”: 格式字符串“%d”需要类型“int”的参数,但可变参数 1 拥有了类型“int *”* G% ~" n. x ]4 v2 l 可是怎么还是有问题呢? 我们回顾一下,指针是如何指向数组的。 int temp[5] = {1, 2, 3, 4, 5};int* p = temp; 我们原本以为,指针p是指向数组的指针,但是实际上「并不是」。仔细想想就会发现,这个指针实际上是指向的数组的「第一个元素」,而不是指向数组。因为数组里面的元素在内存中都是挨着个儿存放的,因此只需要知道第一个元素的地址,就可以访问到后面的所有元素。 但是,这么来看的话,指针p指向的就是一个「整型变量」的指针,并不是指向「数组」的指针。而刚刚我们用的数组指针,才是指向数组的指针。因此,应该将「数组的地址」传递给数组指针,而不是将第一个元素的地址传入,尽管它们值相同,但是「含义」确实不一样: //Example 08 V3//Example 08 V2 #include <stdio.h>. A, k y* j$ V# V. ]! r int main(void)0 r; P8 M. H% Y% S' { { int temp[5] = {1, 2, 3, 4, 5}; int(*p2)[5] = &temp;//此处取地址 int i; for (i = 0; i < 5; i++)5 f' z/ _; I4 _! c4 s0 I& ` { printf("%d\n", *(*p2 + i));' ~) K: a* L' D/ v* _3 y }6 ~6 ?* H! K5 C; r1 J. f return 0; }$ A2 J" q. Q4 D8 P$ h: W: g 程序运行如下: //Consequence 08/ B* `% b1 M9 ^5 v G1& `9 a3 \: p4 z; _ S$ B2 K 23 h9 K. A4 O( N4 O- v4 C 3; V% H: X" n9 x, u8 T9 ^$ G3 F1 { 4 5- \3 T4 [( |' O* L 指针和二维数组 在上一节《C语言之数组》我们讲过「二维数组」的概念,并且我们也知道,C语言的二维数组其实在内存中也是「线性存放」的。 假设我们定义了:int array[4][5] arrayarray作为数组的名称,显然应该表示的是数组的「首地址」。由于二维数组实际上就是一维数组的「线性拓展」,因此array应该就是指的指向包含5个元素的数组的指针。 如果你用sizeof()去测试array和array+1的话,就可以测试出来这样的结论。 *(array+1)首先从刚刚的问题我们可以得出,array+1同样也是指的指向包含5个元素的数组的指针,因此*(array+1)就是相当于array[1],而这刚好相当于array[1][0]的数组名。因此*(array+1)就是指第二行子数组的第一个元素的地址。 *(*(array+1)+2)有了刚刚的结论,我们就不难推理出,这个实际上就是array[1][2]。是不是感觉非常简单呢? 总结一下,就是下面的这些结论,记住就好,理解那当然更好: *(array + i) == array6 h- B4 M' [( \8 Y* o- o. F# a/ v*(*(array + i) + j) == array[j] *(*(*(array + i) + j) + k) == array[j][k] ...$ b/ A- p9 d2 X8 C 数组指针和二维数组 我们在上一节里面讲过,在初始化二维数组的时候是可以偷懒的: int array[][3] = {{1, 2, 3}, {4, 5, 6}; i2 i) N+ o3 x4 f- i* Y }; 刚刚我们又说过,定义一个数组指针是这样的: int(*p)[3];那么组合起来是什么意思呢? int(*p)[3] = array;通过刚刚的说明,我们可以知道,array是指向一个3个元素的数组的「指针」,所以这里完全可以将array的值赋值给p。 其实C语言的指针非常灵活,同样的代码用不同的角度去解读,就可以有不同的应用。 那么如何使用指针来访问二维数组呢?没错,就是使用「数组指针」: //Example 09#include <stdio.h> int main(void) {3 X- Y3 y1 \3 }* z: f int array[3][4] = {% `2 H3 v5 @. b& d; E' R {0, 1, 2, 3}, {4, 5, 6, 7}, {8, 9, 10, 11}: e0 C1 s! Y8 m1 }" K! } };" V1 ^9 s7 T+ H$ G+ u int(*p)[4]; int i, j; p = array;& ^* \( {0 W' j* s- v2 |; f for (i = 0, i < 3, i++) {2 s# ?* W' A; h( l. h* A' ~$ p for (j = 0, j < 4, j++)2 Y: B$ j Y$ M1 ^7 q0 ? {& m* c$ j( M! q% N, r3 P' A; u: p printf("%2d ", *(*(p+i) + j)); }7 i. `( s8 I! \# d printf("\n");+ [8 f Z& [# C) w& t% y }1 N1 l9 \$ E/ Z+ ~- B5 e; W+ W) m/ ~ return 0; } 运行结果: //Consequence 090 1 2 3& H# E5 D2 d/ K- \1 A2 Y% w 4 5 6 7) v: C+ A# v) i( g 8 9 10 11: O* Q7 X( F( b: J' c% l% Z void指针 void实际上是无类型的意思。如果你尝试用它来定义一个变量,编译器肯定会「报错」,因为不同类型所占用的内存有可能「不一样」。但是如果定义的是一个指针,那就没问题。void类型中指针可以指向「任何一个类型」的数据,也就是说,任何类型的指针都可以赋值给void指针。 将任何类型的指针转换为void是没有问题的。但是如果你要反过来,那就需要「强制类型转换」。此外,不要对void指针「直接解引用」,因为编译器其实并不知道void指针会存放什么样的类型。 //Example 10#include <stdio.h>9 E- {3 u$ O" U7 L8 m- c1 X) K1 L int main(void)+ ]" t3 I- G- h, J' \ {0 ]9 N6 E. j" j& R int num = 1024;9 x6 @* F1 y% u P5 X, L int* pi = #8 y: Q& U4 d1 c0 u; Z5 Q, z char* ps = "TechZone"; void* pv; 8 A7 z7 c5 [; \1 x6 O) x/ T9 F pv = pi;) D: t2 M$ U& n- n. @: H/ f1 E printf("pi:%p,pv:%p\n", pi, pv); printf("*pv:%d\n", *pv); pv = ps;3 g0 ~7 A2 M/ M* m printf("ps:%p,pv:%p\n", ps, pv);& F# {1 c( ]) R+ g- q: e printf("*pv:%s\n", *pv);& V- v3 o3 Y% P } 这样会报错: //Error in Example 10错误 C2100 非法的间接寻址+ Z0 z$ `" ~( e8 J9 {' D 错误 C2100 非法的间接寻址* |( N( j+ H0 u' _ `' l; O 如果一定要这么做,那么可以用「强制类型转换」: //Example 10 V2#include <stdio.h>0 a- e$ x7 h+ z* K int main(void); ^) B5 K! c: r {6 T% B6 V7 S! q; X# W) D int num = 1024; int* pi = ## I+ p/ K# t+ y4 u- y8 P char* ps = "TechZone";2 C6 m! P/ d5 T0 @5 ]' Q5 T4 J void* pv; pv = pi; printf("pi:%p,pv:%p\n", pi, pv);' v+ _/ t; }) P; ^0 Y8 r! C- e8 Y printf("*pv:%d\n", *(int*)pv); . Q/ G! U, |9 z8 U% j$ e pv = ps;) r- C- @1 ~4 G8 v8 n/ }6 l3 I printf("ps:%p,pv:%p\n", ps, pv); printf("*pv:%s\n", pv); } 当然,使用void指针一定要小心,由于void指针几乎可以「通吃」所有类型,所以间接使得不同类型的指针转换变得合法,如果代码中存在不合理的转换,编译器也不会报错。 因此,void指针能不用则不用,后面讲函数的时候,还可以解锁更多新的玩法。 NULL指针在C语言中,如果一个指针不指向任何数据,那么就称之为「空指针」,用「NULL」来表示。NULL其实是一个宏定义: #define NULL ((void *)0)在大部分的操作系统中,地址0通常是一个「不被使用」的地址,所以如果一个指针指向NULL,就意味着不指向任何东西。为什么一个指针要指向NULL呢? 其实这反而是一种比较指的推荐的「编程风格」——当你暂时还不知道该指向哪儿的时候,就让它指向NULL,以后不会有太多的麻烦,比如: //Example 11+ y c0 F; ~9 A" s6 q4 B#include <stdio.h> int main(void)' `% Y" a& `9 l3 j1 t5 i% Q {) f: S4 V' I: k1 P; L' E% S8 g int* p1;0 V, `3 s% v* Q; \( i int* p2 = NULL;! @6 F7 _7 c: B/ {% p; U* d printf("%d\n", *p1);- {8 [& j) y% @# l( [ printf("%d\n", *p2); return 0; }" [' {' v, Q/ y! c1 R, E 第一个指针未被初始化。在有的编译器里面,这样未初始化的变量就会被赋予「随机值」。这样指针被称为「迷途指针」,「野指针」或者「悬空指针」。如果后面的代码对这类指针解引用,而这个地址又刚好是合法的话,那么就会产生莫名其妙的结果,甚至导致程序的崩溃。因此养成良好的习惯,在暂时不清楚的情况下使用NULL,可以节省大量的后期调试的时间。 指向指针的指针开始套娃了。其实只要你理解了指针的概念,也就没什么大不了的。 //Example 12#include <stdio.h>4 V# _* V% y4 g) \2 q+ F V. ` int main(void). l- e" Q/ G6 ]" u0 J { int num = 1;" a& Z( ^0 H3 g' e% A int* p = #0 I! `: @0 s, ]( E) x* Z int** pp = &p; printf("num: %d\n", num);, X6 F y( U9 l3 s; ~ printf("*p: %d\n", *p);- D3 j6 _) W0 n* E9 @ printf("**p: %d\n", **pp); printf("&p: %p, pp: %p\n", &p, pp); printf("&num: %p, p: %p, *pp: %p\n", &num, p, *pp); return 0; }% b6 O( H8 e9 G* A: a$ G& i h 程序结果如下: //Consequence 12num: 1 *p: 1" |8 o$ S6 m! N! R' u4 E' a **p: 1, f) e4 g$ Z5 g% p& [ &p: 004FF960, pp: 004FF960: j* S" O! P3 @- u &num: 004FF96C, p: 004FF96C, *pp: 004FF96C; r1 z) ?2 f$ @9 h2 R; S- F/ h' k 当然你也可以无限地套娃,一直指下去。不过这样会让代码可读性变得「很差」,过段时间可能你自己都看不懂你写的代码了。 指针数组和指向指针的指针那么,指向指针的指针有什么用呢? 它可不是为了去创造混乱代码,在一个经典的实例里面,就可以体会到它的用处: char* Books[] = {"《C专家编程》", "《C和指针》", "《C的陷阱与缺陷》",# x! m* n% @4 r$ _7 t "《C Primer Plus》",2 \0 t% H3 M& h9 u "《Python基础教程(第三版)》"1 M4 k, O2 o8 b' p };+ c0 w" K" D/ Z! ?2 ^* D8 d 然后我们需要将这些书进行分类。我们发现,其中有一本是写Python的,其他都是C语言的。这时候指向指针的指针就派上用场了。首先,我们刚刚定义了一个指针数组,也就是说,里面的所有元素的类型「都是指针」,而数组名却又可以用指针的形式来「访问」,因此就可以使用「指向指针的指针」来指向指针数组: ...+ o) ^) {$ c3 N2 f: o3 n0 s( nchar** Python; char** CLang[4];% \* Z# E& n' }2 J Python = &Books[5];, k' ?$ q6 Z/ T) o3 o7 ]7 v CLang[0] = &Books[0]; CLang[1] = &Books[1];- f U: _/ z2 {" u, } CLang[2] = &Books[2];* T7 g! u; e$ Y; ?9 Y: g CLang[3] = &Books[3]; ... 因为字符串的取地址值实际上就是其「首地址」,也就是一个「指向字符指针的指针」,所以可以这样赋值。 这样,我们就利用指向指针的指针完成了对书籍的分类,这样既避免了浪费多余的内存,而且当其中的书名要修改,只需要改一次即可,代码的灵活性和安全性都得到了提升。 常量和指针常量,在我们目前的认知里面,应该是这样的: 520, 'a'& W" ?" P4 ~9 a- ~) b6 b7 _或者是这样的: #define MAX 10002 ` r E8 p+ J, J#define B 'b' 常量和变量最大的区别,就是前者「不能够被修改」,后者可以。那么在C语言中,可以将变量变成像具有常量一样的特性,利用const即可。 const int max = 1000;const char a = 'a';0 M, I" t" I8 F; i( _; t 在const关键字的作用下,变量就会「失去」本来具有的可修改的特性,变成“只读”的属性。 指向常量的指针强大的指针当然也是可以指向被const修饰过的变量,但这就意味着「不能通过」指针来修改它所引用的值。总结一下,就是以下4点: ❝常量指针指向非常量的常量指针 指针本身作为一种「变量」,也是可以修改的。因此,指针也是可以被const修饰的,只不过位置稍稍「发生了点变化」: ...int* const p = # ...% W% O6 _8 ^) v; f, c v 这样的指针有如下的特性: ❝指向常量的常量指针 在定义普通变量的时候也用const修饰,就得到了这样的指针。不过由于限制太多,一般很少用到: ...int num = 100; const int cnum = 200; const int* const p = &cnum; ... |