指针解密
( 李俊 )
其实指针并那么神秘 , 只是人们把它越弄越神秘了 , 在一些 C 或 C++ 书中总是把指针谈的不清楚或是不够深入和透彻 , 我写此文的意在帮你迅速掌握指针的本质 , 这篇文章假设你不是指针菜鸟 , 并比较了解指针的基本运用 .
了解基本指针概念 :
指针的类型是地址 , 这个大家应该都明白 , 也就是说它只能存放地址 ,OK! 我们先来理解三个极为重要的概念 :
1, 指针变量的值 ( 指针变量所在的内存单元 )
2, 指针变量的地址 ( 指针变量所在的内存单元地址 )
3, 指针变量的值 ( 地址 ) 所在的内存单元 ( 也就是通常人们说的指针指向的内存单员 )
( 图 1)
地址 :0x6FE3 地址 :0x23EF
上图已经很清楚了 , 指针变量中保存的是 0x23EF, 在 0x23EF 中存的是个数据 2343;
下面用几个概念上的实际例子来加深理解 :( 很具代表性也是经常弄糊涂的例子 )
* 注 : 为了清晰 , 所有例子代码中地址均是付与缺定的整型 ( 如 :char *pc = 0x201E), 大家
在实际写代码时请千万不要这么做 , 因为会引起段错误 . 也叫段违规 .
( 例 1)
main(){
char *a = 0x0005,*b=a;
*b = 'a';
*++b = 'b'; /* 先把 b 指的地址 +1, 再为这个新地址指的单元附上 ’b’, 即 :*0x0006=’b’*/
printf("a=%d, b=%d, *a=%c, *b=%c\n",a,b,*a,*b);
*b++ = 'c'; /* 先把 ’c’ 附给 0x0006, 在将此地址 +1, 并其 b 指向此地址 */
printf("a=%d, b=%d, *a=%c, *b=%c\n",a,b,*(a+1),*b);
getch();
}
输出 : a=5, b=6, *a=a, *b=b
a=5, b=7, *a=c, *b = r
解说 : 从上面例子我们发现 b 本身再怎么变 , 是不会有关系的 , 直到 b 要改变 a 或 a 将要指的内存时才发生变化 , 这也是我为什么要打印 (a+1) 这个地址值的原因 , 其实这个地址也就是 0x0006, 因为上句是先附了这块地址 ’c’ 之后在将指针 +1 的 , OK, 加 1 后 b 实际上指向了 0x0007 这块地址 , 这块内存中存放的东西是不确定的 , 因为我们没为它附过任何值 , 所以最后 *b 打印出来是 ’r’.
( 例 2)
test(char *ta){
*++ta = 'b';
}
main(){
char *a = 0x0005;
*a = 'a';
printf("a=%d,*a=%c\n",a,*a);
test(a);
printf("a=%d,*a=%c\n",a,*a);
getch();
}
输出 : a=5, *a=a
a=5, *a=a
解说 : 是不是很出乎意料呀 ? 其实这个例子和上面的一个例子差不多 , 但多了一个知识点 , 其实在传递参数数时是把实参附给了一个函数内的临时变量 ( 形参 ), 所以上面的代码可以想象为 :
main(){
char *a = 0x0005;
char *ta;
*a = 'a';
printf("a=%d,*a=%c\n",a,*a);
ta = a;
*++ta = 'b'; /*test(a)*/
printf("a=%d,*a=%c\n",a,*a);
getch();
}
很容易理解了吧 , 就可以将方法的形参想象成方法的局部变量 , 调用时实参将值附给这个局部变量 .
现在对指针有更深一层的理解了吧 ; 下面我们再看些复杂的例子 ! 其实指针的复杂不在于写的如何复杂 ,
而在于你对内存的理解程度和编译器为你做了些什么 .
( 例 3)
testA(char **pc){++*pc;}
testB(char *pc){++*pc;}
main(){
char *c=0x0004, **pc=&c ;
printf("testA_f=%i\n",c);
testA(pc);
printf("testA_A=%i\n",c);
testB(&c);
printf("testB_A=%i\n",c);
getch();
}
输出 : testA_f =5
testA_A=6
testB_A =7
解说 : 这个例子有点复杂 , 想说明的是在什么地方需要使用到指针的指针和如何去使用 , 简单来讲指针的指针多数被用在需要在调用方法后 , 实参的值也发生改变 . 其实理解它也不难 , 你只要将指针指向的那个指针也想成一个普通的变量就 OK 了 ,( 例如 : char c=’c’,*pc=&c,**pcc=&pc; 你把 pc 想象成一个普通变量就象 c 一样 ) 这样一来就很容易理解了 , 那么 *pcc 实际上就是取了 pc 的值 ( 即 : 变量 c 的地址 ), 而 **pcc 实际上是取了 c 的值 ( 即 : 字符 ’c’), 现在看上面的例子很简单了吧 !!
( 特例 , 勿仿 )
( 小例 1)
main()
{
int i=45,*p = 45;
*p =345;
printf("%i",*(int*)i);
getch();
}
( 小例 2)
int* RT_32[3];
main()
{
int *p = 45;
*p = 345;
*RT_32[1] = 45;
printf("%i",**(int**)RT_32[1]);
getch();
}
输出结果都是 : 345
这个应用非常高级 , 是我在写一个 VM 时发现的 , 这个功能非常强 , 也就是说它能实现将一个整型变量的值做为一个地址去取这个地址所对应内存单元中的值 , 这种应用一般在 VC 中是行不同的 ,TC2.0 可以 , 还要注意的是这种应用很危险 , 你必需首先知道这个整型中存的是什么数 , 还要知道将这个数当成地址来取的值是什么 . 建议非不得以 , 不要去用此方法 .
数组和指针 :
数组和指针是 C 语言中对于指针方面争论最多的话题 , 现在我们来分析一下数组和指针到底有什么区别 .
( 区别 1)
文件 1 中 :
Int test[100];
文件 2 中 :
Extern int *test;
以上是编译不可以通过的 , 从这就证明数组和指针是存在区别的 .
( 区别 2)
数组在声明时必需给出大小 , 而指针理论上可以动态增加大小 . 而且对内存的操作更为灵活 .
( 区别 3)
比如有 char a[4] = “abcd”; char *p;
a[2];
数组的取值操作 : 1, 编译器将 a 符号用地址 9980 取代 .
2, 运行时步骤 1: 取下标的值 2, 将它与 9980 相加 .
3, 运行时步骤 2: 取地址 (9980+2) 的内容 .
*(p+2)
指针取值操作 : 1, 编译器将 p 符合用地址 4624 取代 .
2, 运行时步骤 1: 取地址 4624 的内容 ( 比如 5081).
3, 运行时步骤 2: 取出 (5081+1) 的内容 .
很明显指针多了一个寻址地址操作 , 要先找到 p 的地址 , 再找到 p 地址中存放的地址 , 最后再找到这个地址 +2 的地址内容 .
看看在编译器中的区别 :
Char a[4]; 被翻译成 9980; 实际动作 : *(9980+2)
Char *p; 被编译成 4624; 实际动作 : *(*(4624)+2)
那我们再来研究一下当你定义为指针 , 但以数组方式引用会发生什么 .
如 : Char *p = “abcd”;
取值操作 : 1, 编译器将 p 符合用地址 4624 取代 .
2, 运行时步骤 1: 取地址 4624 的内容 ( 比如 5081).
3, 运行时步骤 2: 取的下标值 2, 并将它内容与 5081 相加 .
4, 运行时步骤 3: 取地址 5081+2 的内容 .
其实数组和指针的根本区别就是 , 指针是间接访问数据 , 首先取得指针的内容 , 把它当做地址 , 然后从这个地址取数据 , 而数组是直接访问数据 ,a[i], 只是简单的以 a+i 为地址取得数据 . 指针通常用与动态数据结构 , 而数组一般用与存储固定数目且数据类型相同的元素 .
了解函数调用 :
OK, 相信学过汇编或懂点编译原理的人都知道在函数调用时是用堆栈来保存临时变量的 , 对 , 它们的生命周期在函数被返回后也就被释放了 , 我们来用一段小代码证明一下吧 .
( 例 4):
void *vp;
testA(){
char *c = 10;
(char*)vp = &c; /* 实际 vp 指向 [SP+2]->vp*/
}
testB(){
char *c = 20; /*vp 所指的单元被改变 */
}
main(){
testA();
printf("vpA=%i\n",*(char*)vp);
testB();
printf("vpB=%i\n",*(char*)vp);
getch();
}
输出结果 : 10
20
这段代码证明了堆栈段的存在 .
破坏堆栈段和代码段都很简单 , 看一段小代码 .
( 例 5):
(*pf)();
void *ps;
f()
{
char c = 40;
(char*)ps = &c;
printf("f=%i\n",c);
}
main()
{
pf = ps;
f();
ps = pf;
f();
pf();
getch();
}
函数指针 ( 高级篇 )
也许大家会决定 , 我下面这个例子比较复杂 , 但你必需理解它才能理解后面的例子 .
下面这个例子描述了函数指针的转型调用 , 可以说很象是 C++ 中的重载 , 但在一些
方面要比重载更强 , 因为它可以把函数的返回类型的不同做为不同类型的依据 .
( 例 6):
(*pf)();
f1(){
printf("f1\n");
}
f2(char c){
printf("f2=%i\n",c);
}
char f3(char c){
return c;
}
f4(int i,char c){
printf("f4=%i,%d\n",i,c);
}
main()
{
pf = f1;
pf();
pf = f2;
(void (*)(char))pf(34);
pf = f3;
printf("f3=%i\n",(char (*)(char))pf(100));
pf=f4;
(void (*)(int,char))pf(200,120);
getch();
}
呵呵 ! 很强悍吧 . 四次转型 , 实现了绑定 4 个方法 .
我们再来看一个实现函数指针数组的例子 . 一般用它实现有限状态机 .
( 例 7):
#define MAX_TEST 4
void a(){printf("function a by call!\n");}
void b(){printf("function b by call!\n");}
void c(){printf("function c by call!\n");}
void d(){printf("function d by call!\n");}
void (*test[MAX_TEST])() = {a, b, c, d}; /* 声明并初始化函数指针数组 */
main()
{
int i;
for(i=0; i<MAX_TEST; i++)
test[i]();
getch();
}
函数指针很有趣 , 你可以向以下这种模式来调用函数 (*test[i])(); 你甚至还可以 (*****test[i])();
很郁闷吧 . 最重要不要把自己的思维搞乱 , 你认为怎么样最符合你的理解 , 你就怎么样去调用它 .
我个人还是比较倾向于用 test[i](); 因为从我学 C 开始 , 我就将函数名理解为指向函数的入口地址 .
最后一例子很有意思 , C 中能不能象 C++ 一样实现类呢 ? 就用这个例子做个小小的启发吧 . 没真正
实现对象的实例化 , 仅仅是绑定了一些函数 .( 仅供参考 )
( 例 8):
#define NEW(n) n->Build=building;n->Build(myclass,add,printsum,closeClass,n)
typedef struct myClass
{
int a;
int b;
void (*my)();
int (*Add)(int,int);
void (*PutSum)(int);
void (*myClose)();
void (*Build)(void (*n)(), int (*ad)(int,int),void (*p)(int),void (*c)(), struct myClass*);
} CLASS;
void myclass(){
printf("create myclass!!!\n");
}
int add(int a,int b){
return a+b;
}
void printsum(int c){
printf("sum = %i\n",c);
}
void closeClass(){
printf("close myclass!!!\n");
}
void building(void (*n)(), int (*ad)(int,int),void (*p)(int),void (*c)(), CLASS* mc)
{
mc->my = n;
(*mc).Add = ad;
(*mc).PutSum = p;
(*mc).myClose = c;
}
void main()
{
int sum;
CLASS *myobj;
NEW(myobj);
myobj->my();
sum = myobj->Add(myobj->a = 5, myobj->b = 10);
myobj->PutSum(sum);
myobj->myClose();
free(myobj);
getch();
}
再来看一个模拟继承的特性 .
( 例 9)
#define NEW(n) n->Build=building;n->Build(myclass,add,printsum,closeClass,n)
#define SUBNEW(n) n->Build=subbuilding;n->Build(div,n)
typedef struct myClass
{
int a;
int b;
void (*my)();
int (*Add)(int,int);
void (*PutSum)(int);
void (*myClose)();
void (*Build)(void (*n)(), int (*ad)(int,int),void (*p)(int),void (*c)(), struct myClass*);
} CLASS;
typedef struct subMyClass
{
CLASS *baseClass;
int (*Div)(int);
void (*Build)(int (*d)(int), struct subMyClass* sc);
} SUBCLASS;
void myclass(){
printf("create myclass!!!\n");
}
int add(int a,int b){
return a+b;
}
void printsum(int c){
printf("sum = %i\n",c);
}
void closeClass(){
printf("close myclass!!!\n");
}
void building(void (*n)(), int (*ad)(int,int),void (*p)(int),void (*c)(), CLASS* mc)
{
mc->my = n;
(*mc).Add = ad;
(*mc).PutSum = p;
(*mc).myClose = c;
}
int div(int a)
{
return (a/2);
}
void subbuilding(int (*d)(int), SUBCLASS* sc)
{
building(myclass,add,printsum,closeClass,sc->baseClass);
(*sc).Div = d;
}
void main()
{
int sum;