C语言精要总结-指针系列(一)
原文地址:http://考虑到指针内容繁多,这里将指针作为一个系列,从简入繁,带着没有研究过指针的朋友,一点一点深挖并掌握这C语言的精华。初步计划如下
[local]1[/local]
此文为指针系列第一篇:
C语言精要总结-指针系列(一)
内存与地址
我们可以把内存看做一排连续的房间,每个房间(字节空间)都有一个房间号,房间号就是这个房间的地址,而且每个房间里都有八个位。
为了存储不同大小的值,多数时候我们要用连续几个房间来存储一个值,这时我们会用其中一个房间号来表示这一片连续的房间,至于这个房间号是第一个房间的房间号,还是最后一个房间的房间号,不同的机器有不同的规定。文中我们假设这个房间号是左起第一个房间的房间号,这样房间号(指针)其实就有了类型之分。
通过地址,计算机就可以操纵内存单元的内容,虽然在代码中,类似于*0x0048f93d = 'a';这样的表达式是合法的,但为了让代码看起来更友好,显然不能在代码里全部写数字地址。因此高级语言的编译器为我们实现了直接通过变量名来访问内存位置,当然硬件单元依然通过寻址来访问内存位置。
指针变量
指针变量本身也是变量,也需要在内存中占用一定的存储单元,只是其存储的值是其他变量的内存地址,分配的位置也可以是跟基本类型变量是连续的。(下图中一个长方形并不代表一个字节),如图:
用代码来描述即是
程序代码:
short shortVar = 10; int intVar = 80; short * p1 = & shortVar; int * p2 = & intVar;
上面的语句给出了指针定义(type *)和初始化(= &var)的方法。这里定义了一个名叫p1指向short类型的指针变量,并用shortVar的地址来初始化;定义了一个名叫p2指向int类型的指针变量,并用intVar的地址来初始化。当然,像下面这样直接用一个地址值来初始化指针也是合法的
int *p = (int *)0x0048f93d;
虽然这在我们看来是个地址值,但在编译器眼里,这是一个int类型的值,所以需要强制转换。这种写法,除非很明确这个地址时用来做什么的,否则不要这么做。
如果在定义一个指针变量时,还不确定用什么地址来初始化,则一定要初始化为NULL,这是一个空指针值,也是一个值为0的宏,它代表指针不指向任何位置。如果不给一个局部指针变量做任何初始化,它存储的将是一个不可预知的值,指向一个不可预知的位置,如果对这样一个指针变量进行操作,很容易引起异常中断。而对于全局变量,编译器会自动初始化为0。
解引用操作
解引用,又叫间接访问,即通过一个指针变量访问它所指向的地址的过程。这个解引用操作符便是单目操作符*。但注意对一个指针进行进行解引用,不一定是取值,也可能是写值,这取决于解引用表达式是作为左值(赋值符号左边)还是右值(赋值符号右边)。
例如对上述指针p1,p2进行解引用操作,如下代码
复制代码
程序代码:
printf("%d\n",* p1); // 10 printf("%d\n",* p2); // 80 *p1 = 1; *p2 = 8; printf("%d\n",* p1); // 1 printf("%d\n",* p2); // 8
那么像下面这个表达式做了什么呢?
*&shortVar = 1;
很显然,这是将1赋值给变量shortVar,根据右结合性,取地址(&)之后立即解引用(*),这其实多此一举,如果编译器不对这样的代码做优化,那将生成一些无意义的操作代码。
二级指针
二级指针,也叫指针的指针,也就是一个指向指针变量的指针。按照指针变量的定义方法(type * pVar),我们要定义一个指向整型指针变量的指针,应该像下面这样定义
int * * p2p = & p2;
没错,这就是定义一个指向整型指针的指针的定义方式。在内存中结构(假设分配的恰好是连续的)就如下图所示
很显然,二级指针变量依然也是一个指针变量,哪怕后面还有三级、四级指针变量,都始终是一个指针变量,对它进行解引用或者取地址,原理跟一级指针是一样的。比如对二级指针p2p进行一次解引用,将得到p2这个指针变量,再进行一次解引用将得到intVar这个变量,正如上图所示。
printf("%d\n",**p2p); // 80
二级指针跟二维数组名是有很大区别的,这会在后续的文章中指出。另外,如果对一个二级指针取地址,将得到一个三级地址,依次类推。
指针的大小
我们知道指针是用来存储地址值的,而分配给指针变量的空间,只用来存储地址值,而不会记录变量类型等信息,这跟普通变量是一样,它们被记录在编译器的符号表中。
既然指针变量自身的空间只存地址,那么不管什么类型的指针,它们占用的空间大小应该是一样的,那究竟应该分配多大的空间?这取决于CPU最大寻址地址的大小。为了保证指针变量能存下最大的寻址地址,应该给指针变量分配足以存储最大寻址地址的大小。
在32位CPU上,CPU最大寻址空间为2的32次方(4G),因此要存下最大的32位的地址值,需要为指针变量分配4个字节的空间,而在64位CPU中,为了能寻到2的64次方的内存空间,需要为指针变量分配8个字节的空间。
因此,编译器也充分考虑了这个问题。它可以控制分配的指针变量的空间大小。用VS在写Console Application时,默认编译的是32位 Console Application,这是为了保证程序的兼容性,以保证程序一定可以在32位和64位机器上运行,此时,vs编译器默为指针变量分配4个字节的空间。但是本人的笔记本是支持64位寻址的CPU,因此,本人用gcc version 5.1.0 (tdm64-1)编译出来的程序,指针变量分配了8个字节的空间。
后续文章中,如不明确指出,我们认为指针变量占4个字节的空间。
指针类型强制转换
在看怎么定义二级指针时,有读者可能考虑,为什么不能这样定义
(int *) * p2p = & p2;
乍一看可能没什么不对,但实际上,这个表达式并不是定义一个变量,而是在执行一个非法的赋值操作。假如前面已经定义过p2p这样的一个二级指针,这个表达式还真会做一些事情:
解引用p2p得到一个一级指针tmp
将一级指针tmp强制转换为一个指向int 类型的指针
取p2指针的地址(一个二级指针)赋值给一级指针tmp(注意是赋值给一级指针本身,而不是一级指针指向的变量)
显然这是不能执行成功的。但是它却告诉我们,指针是可以强制转换的。
但对指针类型强制转换,和普通数据类型会有些不一样:对指针类型强制转换,不会改变指针变量本身空间的大小及空间内存储的地址值,而只会修改符号表中的指针类型及其指向类型占用空间的大小值(为指针运算做准备)。
一起来图解一下下面这段代码
复制代码
1 // int a = 0x12345678;
2 // return *(char*)(&a) == (char) a;
3 int a = 0x12345678;
4 int * pa = &a ;
5 char * pch = (char *) pa;
6 char ch = (char) a;
7 printf("%x\n",*pch);
8 printf("%x\n",ch);
假如程序出现的变量按如下方式分配
对指针强制转换之后,pch存储的地址值跟pa存储的地址值时一样的,但是他们在编译器符号表中的类型是不一致的,因此指向的空间大小是不一样的,pa指向整个变量a,而pch指向变量a的第一个低字节。ch变量毫无疑问存储的将是78,因为对一个基本数据强制转换,只会取数据的低位。
很显然,如果按照图中所示,程序的第7行第8行将输出12和78。但实际上在本人的笔记本上,两次都是输出78。
这其实就是很经典的大端存储和小端存储的判别。如果按照图中所示,其实变量a是按大端模式存储(即低地址存高位)。而如果按照小端模式存储,则应该低地址存低数据位,如下图。
而上面那段代码,就是用来检测计算机是按大端存储还是按小端存储的,很显然,本人的笔记本按小端存储。
用这个程序想说明的是,对一个指针进行调整级别的强制转换再解引用,可能会引起一些兼容性问题,因为这取决于系统实现。
另外在程序中我们会经常看到void * 类型的指针,这样的指针主要是为了写通用的代码,你可以将任意类型的指针强制转换为void* 类型的指针,在之后要解引用的时候,再强制转换回正确的指针类型进行解引用。例如我们常见的c语言库函数qsort中的:int comparator ( const void * elem1, const void * elem2 );。