您的位置:首页 > 编程语言 > C语言/C++

用汇编的眼光看C++(之指针)

2012-10-14 21:13 417 查看
/article/1797746.html

【 声明:版权所有,欢迎转载,请勿用于商业用途。 联系信箱:feixiaoxing @163.com】

指针是我们在C/C++中经常遇到的一种数据类型。指针用的好,可以提高代码的可读性;但是如果使用不恰当,反而会造成很大的麻烦。指针,也就是指向某一种数据类型的地址。这种类型很多,它可以是编程语言自带的类型,比如说int、long、short、char、float、double、int;也可是是指向某一种自定义数据类型,可以使union、struct或者是class;甚至指向的数据类型本身即是指针,比如说int*、char*、short**;当然指针还可以是指向一片内存,表示具有一定长度的起始地址,比如说int(*pData)[4];最后,指针还可以是函数指针,直接指向函数运行的第一个字节。

(1)普通数据类型指针

普通数据类型指针相对概念比较简单,它表示指向的区域就是普通数据类型的空间。我们可以看下面一段示例代码:

[cpp]
view plaincopyprint?

43: int m = 10;
004012F8 mov dword ptr [ebp-4],0Ah
44: char* p = (char*) &m;
004012FF lea eax,[ebp-4]
00401302 mov dword ptr [ebp-8],eax
45: float* f = (float*) &m;
00401305 lea ecx,[ebp-4]
00401308 mov dword ptr [ebp-0Ch],ecx
46: short* s = (short*) &m;
0040130B lea edx,[ebp-4]
0040130E mov dword ptr [ebp-10h],edx
47: *p = 2;
00401311 mov eax,dword ptr [ebp-8]
00401314 mov byte ptr [eax],2
48: *f = 2.4f;
00401317 mov ecx,dword ptr [ebp-0Ch]
0040131A mov dword ptr [ecx],4019999Ah
49: *s = 10;
00401320 mov edx,dword ptr [ebp-10h]
00401323 mov word ptr [edx],offset process+46h (00401326)

43:       int  m = 10;
004012F8   mov         dword ptr [ebp-4],0Ah
44:       char* p = (char*) &m;
004012FF   lea         eax,[ebp-4]
00401302   mov         dword ptr [ebp-8],eax
45:       float* f = (float*) &m;
00401305   lea         ecx,[ebp-4]
00401308   mov         dword ptr [ebp-0Ch],ecx
46:       short* s = (short*) &m;
0040130B   lea         edx,[ebp-4]
0040130E   mov         dword ptr [ebp-10h],edx
47:       *p = 2;
00401311   mov         eax,dword ptr [ebp-8]
00401314   mov         byte ptr [eax],2
48:       *f = 2.4f;
00401317   mov         ecx,dword ptr [ebp-0Ch]
0040131A   mov         dword ptr [ecx],4019999Ah
49:       *s = 10;
00401320   mov         edx,dword ptr [ebp-10h]
00401323   mov         word ptr [edx],offset process+46h (00401326)

上面的一段代码出现了四种数据类型,三种指针,我们可以一一梳理一下。m、p、f、s都是函数内部的临时变量,因为指针也是一种数据类型,它保存的数据不再是一种char或者是short、int数据,而是一种地址。所以我们对p、f、s进行复制的时候,都是把m的地址一一拷贝给他们的。所以虽然指针类型不同,实际上p、f、s的数值是一样的。下面对指针指向的空间进行数据赋值的时候,就和指针类型相关了。一般来说,如果指针为char类型,那么计算就局限在指针指向的那一个字节里面;如果指针是int类型,那么运算的范围就是指针指向的连续4个字节;当然如果指针是数据结构体或者是class类型,那么指针操作的内存区域就更大了。所以,我们返回到代码的时候发现,*p=2只是操作了一个byte,*f=2.4f的时候,操作的是四个byte、也就是dword,*s=10的时候,赋值的就是一个word,这里0x401326处其实就是数据0x00
0A。函数内的变量进过这一番折腾之后,m数值还是10吗?大家可以好好思考一下?(其实是0x4019000a)

(2)函数类型指针

下面是一段有趣的代码,可以查看函数的地址。

[cpp]
view plaincopyprint?

void add()
{
printf("hello!\n");
}

void process(int* q)
{
int* address = (int*)add;

__asm{
call address
}
}

void add()
{
printf("hello!\n");
}

void process(int* q)
{
int* address = (int*)add;

__asm{
call address
}
}

这段代码使用了嵌入式汇编,但是理解上面没有什么困难,感兴趣的同学可以直接拷贝到VC上面进行编译,当然还要加上头文件和main函数。通过代码,我们可以发现其实address就是一个地址,call address其实和call add是一样的。

(3)指针的指针

指针的指针,其实就是说我们指针指向的数据类型本身就是指针。但是,总之一句,指针是地址,那么指向地址数据的指针本身也是地址。

[cpp]
view plaincopyprint?

46: int* pp = &p;
004012FF lea eax,[ebp-4]
00401302 mov dword ptr [ebp-8],eax
47: int** ppp = &pp;
00401305 lea ecx,[ebp-8]
00401308 mov dword ptr [ebp-0Ch],ecx
48: int*** pppp = &ppp;
0040130B lea edx,[ebp-0Ch]
0040130E mov dword ptr [ebp-10h],edx
49: assert(sizeof(p) == 4);
50: assert(sizeof(pp) == 4);
51: assert(sizeof(ppp) == 4);
52: assert(sizeof(ppp) == 4);

46:       int* pp = &p;
004012FF   lea         eax,[ebp-4]
00401302   mov         dword ptr [ebp-8],eax
47:       int** ppp = &pp;
00401305   lea         ecx,[ebp-8]
00401308   mov         dword ptr [ebp-0Ch],ecx
48:       int*** pppp = &ppp;
0040130B   lea         edx,[ebp-0Ch]
0040130E   mov         dword ptr [ebp-10h],edx
49:       assert(sizeof(p) == 4);
50:       assert(sizeof(pp) == 4);
51:       assert(sizeof(ppp) == 4);
52:       assert(sizeof(ppp) == 4);

指针是一种保存地址的数据类型,那么指针的指针也是一种地址数据,保存的也是地址。以此类推,指针的指针的指针呢。。。。。。上面的代码已经清楚地说明了这一点。虽然pp、ppp、pppp自身的意义有所差别,但是他们保存的数据却很简单,也就是堆栈的地址。pppp->ppp->pp->p,大家如果看懂了这个关系,就不会觉得指针很复杂了。如果还是不甚了了,可以把指针内容和指针地址分别打印出来,就会发现他们的区别了。下面就是打印的结果:

[cpp]
view plaincopyprint?

p = 0xa, &p = 0x12ff20
pp = 0x12ff20, &pp = 0x12ff1C
ppp = 0x12ff1C, &ppp = 0x12ff18
pppp = 0x12ff18, &pppp = 0x12ff14

【 声明:版权所有,欢迎转载,请勿用于商业用途。 联系信箱:feixiaoxing @163.com】

(4)指针和引用

引用是C++和语言的区别之一。其实本质上说两者是一致的。朋友们可以看下面两段代码。

a)指针和指针的函数代码

[cpp]
view plaincopyprint?

void add_point(int* q)
{
(*q)++;
}

void add_ref(int& q)
{
q++;
}

void add_point(int* q)
{
(*q)++;
}

void add_ref(int& q)
{
q++;
}


b)函数的调用代码

[cpp]
view plaincopyprint?

56: int m = 10;
004012E8 mov dword ptr [ebp-4],0Ah
57: add_point(&m);
004012EF lea eax,[ebp-4]
004012F2 push eax
004012F3 call @ILT+45(process) (00401032)
004012F8 add esp,4
58: add_ref(m);
004012FB lea ecx,[ebp-4]
004012FE push ecx
004012FF call @ILT+50(add_ref) (00401037)
00401304 add esp,4
59: return 1;
00401307 mov eax,1
60: }

56:       int m = 10;
004012E8   mov         dword ptr [ebp-4],0Ah
57:       add_point(&m);
004012EF   lea         eax,[ebp-4]
004012F2   push        eax
004012F3   call        @ILT+45(process) (00401032)
004012F8   add         esp,4
58:       add_ref(m);
004012FB   lea         ecx,[ebp-4]
004012FE   push        ecx
004012FF   call        @ILT+50(add_ref) (00401037)
00401304   add         esp,4
59:       return 1;
00401307   mov         eax,1
60:   }

分析一下,我们发现其实函数add_point和函数add_ref实现的功能,都是对输入的数据进行自增处理。只不过处理的时候,一个函数的入参是指针,一个函数的入参是引用。在函数调用的地方,大家可以发现指针和引用居然是一样的。首先看add_point,第一句获取m的地址复制给eax,第二句压栈处理,第三句调用函数add_point,第四句出栈回溯。同样看一下add_ref,第一句获取m的地址复制给ecx,第二句ecx压栈处理,第三句调用函数add_ref,第四句堆栈回溯处理。相信看到这里,大家就明白C++的前辈们为什么鼓励大家多多使用引用了。

(5)指针和结构体

我们在学习数据节点的时候,相信大家都学习过这样的一个数据结构定义:

[cpp]
view plaincopyprint?

typedef struct _NODE
{
int data;
struct _NODE* next;
}NODE;

typedef struct _NODE
{
int data;
struct _NODE* next;
}NODE;

当时,我们都不明白这个结构体是什么意思?其实这个定义完全修改成这样:

[cpp]
view plaincopyprint?

typedef struct _NODE
{
int data;
void* next;
}NODE;

typedef struct _NODE
{
int data;
void* next;
}NODE;

这两个数据结构体其实是完全一致的。第一个数据保存数据,第二个数据为指针,内容为某一个数据类型的地址。这种确定的地址和void*类型的地址类型是一样的。只不过前面一种更加直接。后面一种地址的固然方便,但是使用的时候每一次都需要进行转换,很是麻烦。如果大家感兴趣,不妨是接着看下面一道题目:

[cpp]
view plaincopyprint?

typedef struct _NODE
{
struct _NODE* next;
}NODE;

typedef struct _NODE
{
struct _NODE* next;
}NODE;

我们既可以把节点NODE的地址看是NODE*,也可以堪称是NODE**,两者之间有差别吗?(其实没有区别

linux 内核代码上面有一种计算偏移值的方法,大家可以参考一下:

[cpp]
view plaincopyprint?

int offset = (int)&(((NODE*)(0))->next);

int offset = (int)&(((NODE*)(0))->next);

(6)class指针

class指针比较复杂,不过大家可以从一个小范例看出一些端倪:

[cpp]
view plaincopyprint?

class fruit
{
public:
fruit() {}
~fruit() {}
void print() {printf("fruit!\n");}
};

class apple : public fruit
{
public:
apple() {}
~apple() {}
void print() {printf("apple!\n");}
};

void process()
{
fruit f;
apple* a = (apple*)&f;
a->print();
fruit* b = &f;
b->print();
}

class fruit
{
public:
fruit() {}
~fruit() {}
void print() {printf("fruit!\n");}
};

class apple : public fruit
{
public:
apple() {}
~apple() {}
void print() {printf("apple!\n");}
};

void process()
{
fruit f;
apple* a = (apple*)&f;
a->print();
fruit* b = &f;
b->print();
}

熟悉C++的朋友可以很快知道这道题目的答案了,那么为什么a和b都指向同一个地址,使用了相同的print函数,但是结果不同。我想这主要是因为两者数据类型不同的缘故。一旦指针和某一种数据类型绑在了一起,那么这个指针的所有行为事实上都已经被这种类型的数据所限定了。普通数据类型是这样,自定义的class类型也是这样。只要不是在print函数前面加上virtual,我们就会发现两个print的调用都是硬编码,和普通的函数调用无异。所以说,指针离不开数据类型,离开具体类型的地址是没有意义的。就像void*是可以原谅的,但是void却是万万不能接受的。下面的汇编代码很好的说明了这一点。

[cpp]
view plaincopyprint?

65: fruit f;
0040132D lea ecx,[ebp-10h]
00401330 call @ILT+35(fruit::fruit) (00401028)
00401335 mov dword ptr [ebp-4],0
66: apple* a = (apple*)&f;
0040133C lea eax,[ebp-10h]
0040133F mov dword ptr [ebp-14h],eax
67: a->print();
00401342 mov ecx,dword ptr [ebp-14h]
00401345 call @ILT+0(apple::print) (00401005)
68: fruit* b = &f;
0040134A lea ecx,[ebp-10h]
0040134D mov dword ptr [ebp-18h],ecx
69: b->print();
00401350 mov ecx,dword ptr [ebp-18h]
00401353 call @ILT+25(fruit::print) (0040101e)

65:       fruit f;
0040132D   lea         ecx,[ebp-10h]
00401330   call        @ILT+35(fruit::fruit) (00401028)
00401335   mov         dword ptr [ebp-4],0
66:       apple* a = (apple*)&f;
0040133C   lea         eax,[ebp-10h]
0040133F   mov         dword ptr [ebp-14h],eax
67:       a->print();
00401342   mov         ecx,dword ptr [ebp-14h]
00401345   call        @ILT+0(apple::print) (00401005)
68:       fruit* b = &f;
0040134A   lea         ecx,[ebp-10h]
0040134D   mov         dword ptr [ebp-18h],ecx
69:       b->print();
00401350   mov         ecx,dword ptr [ebp-18h]
00401353   call        @ILT+25(fruit::print) (0040101e)


(全文完)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: