1.类的成员函数
类实例化为对象,数据成员是根据对象的首地址来偏移的,而对象的成员函数在内存中是公用的,但在C++或者C写代码时,却可以根据对象加操作符.或者->来进行访问,这在底层到底是如何实现的呢? 不同类的相同函数名的函数在内存中有不同的实现。C++中的函数在编译时会根据命名空间、类、参数签名等信息进行重新命名,形成新的函数名。
2.this指针
this指针具有如下特点:
名称属性:标识符this表示。 类型属性:classname* const 值属性:表示当前调用该函数对象的首地址。 作用域:this指针是编译器默认传给类中非静态函数的隐含形参,其作用域在非静态成员函数的函数体内。 链接属性:在类作用域中,不同类的非静态成员函数中,this指针变量的链接属性是内部的,但其所指对象是外部的,即this变量是不同的实体,但指向对象是同一个。 存储类型:this指针是由编译器生成,当类的非静态成员函数的参数个数一定时,this指针存储在ECX寄存器中;若该函数参数个数未定(可变参数函数),则存放在栈中。 this指针并不是对象的一部分,this指针所占的内存大小是不会反映在sizeof操作符上的。this指针的类型取决于使用this指针的成员函数类型以及对象类型。
类的成员函数默认第一个参数为T* const register this。
3.类成员函数指针
C++编译器在代码编译阶段会对类对象调用的成员函数进行静态绑定(虚函数进行动态绑定),类成员函数的地址在代码编译时就确定,类成员函数地址可以使用成员函数指针进行保存。 成员函数指针定义语法如下:
ReturnType (ClassName::* pointerName) (ArgumentLList);
ReturnType:成员函数返回类型 ClassName: 成员函数所属类的名称 Argument_List: 成员函数参数列表 pointerName:指针名称
class Test
{
public:
void print()
{
cout << "Test::print" << endl;
}
};
成员函数指针语法极其严格:
- 不能使用括号:例如&(Test::print)不对。
- 必须有限定符:例如&print不对,即使在类ClassName作用域内也不行。
- 必须使用取地址符号:直接写Test::print不行,必须写:&Test::print。
Test类的成员函数print的函数指针声明及初始化如下:
void (Test::* pFunc)() = &Test::print;
通常,为了简化代码,使用typedef关键字。
typedef void (Test::*pFunc)();
pFunc p = &Test::print;
可以通过函数指针调用成员函数,示例代码如下:
#include <iostream>
using namespace std;
class Test
{
public:
void print()
{
cout << "Test::print" << endl;
}
};
int main(int argc, char *argv[])
{
void (Test::* pFunc)() = &Test::print;
Test test;
(test.*pFunc)();
Test* pTest = &test;
(pTest->*pFunc)();
return 0;
}
上述代码中,.*pFunc将pFunc绑定到对象test,->*pFunc绑定pFunc到pTest指针所指向的对象。在 (obj.*fptr) 和 (p->*fptr) 两边的括号是语法所强制要求的。成员函数名不是常规函数指针(保存的是某个确切地址),非虚成员函数指针保存的是成员函数在内存中的地址,虚成员函数指针保存的是虚表调用选项,详情查看汇编代码分析。
3.1.常规成员函数转换
#include <iostream>
class Foo {
public:
int f(char *c = 0) {
std::cout << "Foo::f()" << std::endl;
return 1;
}
};
class Bar {
public:
void b(int i = 0) {
std::cout << "Bar::b()" << std::endl;
}
};
class FooDerived : public Foo {
public:
int f(char *c = 0) {
std::cout << "FooDerived::f()" << std::endl;
return 1;
}
};
int main(int argc, char *argv[]) {
typedef int (Foo::*FPTR) (char*);
typedef void (Bar::*BPTR) (int);
typedef int (FooDerived::*FDPTR) (char*);
FPTR fptr = &Foo::f;
BPTR bptr = &Bar::b;
FDPTR fdptr = &FooDerived::f;
fptr = static_cast<int(Foo::*)(char*)>(fdptr);
Bar obj;
( obj.*(BPTR) fptr )(1);
}
Bar obj; ( obj.*(BPTR) fptr)(1);
尽管我们想要调用的是 Bar::b() ,但是 Foo::f() 却被调用了,因为fptr是静态绑定(翻译君注:这里的静态绑定,即指在编译阶段,fptr的值已经确定了,所以即使进行强制转换,依然调用的是Foo类的f()函数)。(请围观成员函数调用和 this指针)
3.2.虚函数情形
相同代码,只是将函数转为虚函数。
#include <iostream>
class Foo {
public:
virtual int f(char *c = 0) {
std::cout << "Foo::f()" << std::endl;
return 1;
}
};
class Bar {
public:
virtual void b(int i = 0) {
std::cout << "Bar::b()" << std::endl;
}
};
class FooDerived : public Foo {
public:
int f(char *c = 0) {
std::cout << "FooDerived::f()" << std::endl;
return 1;
}
};
int main(int argc, char *argv[]) {
typedef int (Foo::*FPTR) (char*);
typedef void (Bar::*BPTR) (int);
FPTR fptr = &Foo::f;
BPTR bptr = &Bar::b;
FooDerived objDer;
(objDer.*fptr)(0);
Bar obj;
( obj.*(BPTR) fptr )(1);
}
如我们所看到的,当成员函数是虚函数的时候,成员函数能够具有多态性并且现在调用的是 FooDerived::f() ,而且 Bar::b() 也能被正确调用了。因为 “一个指向虚成员的指针能在不同地址空间之间传递,只要二者使用的对象布局一样” (C++ Bjarne Stroustrup 的 《C++程序设计语言》 )。当成员函数是虚函数的时候,编译器会生成虚函数表,来保存虚函数的地址。只要成员函数在类中的相对地址一样,即使是在不同类之间,成员函数指针也能相互转换。这是和非虚函数之间的最大不同,因此,运行时的行为也是不同的。
3.3.成员函数指针数组及其应用
成员函数指针的一个重要应用就是根据输入来生成响应事件,下面的 Printer 类和指针数组 pfm 展示了这一点:
#include <stdio.h>
#include <string>
#include <iostream>
class Printer {
public:
void Copy(char *buff, const char *source) {
strcpy(buff, source);
}
void Append(char *buff, const char *source) {
strcat(buff, source);
}
};
enum OPTIONS { COPY, APPEND };
typedef void(Printer::*PTR) (char*, const char*);
void working(OPTIONS option, Printer *machine,
char *buff, const char *infostr) {
PTR pmf[2] = { &Printer::Copy, &Printer::Append };
switch (option) {
case COPY:
(machine->*pmf[COPY])(buff, infostr);
break;
case APPEND:
(machine->*pmf[APPEND])(buff, infostr);
break;
}
}
int main() {
OPTIONS option;
Printer machine;
char buff[40];
working(COPY, &machine, buff, "Strings ");
working(APPEND, &machine, buff, "are concatenated!");
std::cout << buff << std::endl;
}
3.4.成员函数调用和 this 指针
Foo *const this = p;
void Foo::f(Foo *const this) {
std::cout << "Foo::f()" << std::endl;
}
所以,不管p的值是神马,函数 Foo::f 都可以被调用,就像一个全局函数一样!p被作为 this 指针并当作参数传递给了函数。而在我们的例子中 this 指针并没有被解引用,所以,编译器放了我们一马(翻译君表示,这其实跟编译器没有关系,即使我们在成员函数中使用this指针,编译照样能通过,只不过在运行时会crash)。假如我们想知道成员变量 _i 的值呢?那么编译器就需要解引用 this 指针,这只有一个结果,那就是我们的好兄弟——未定义行为(undefined behavior)。对于一个虚函数调用,我们需要虚函数表来查找正确的函数,然后, this 指针被传递给这个函数。
4.成员函数和成员虚函数汇编分析
在VS2013下,新建控制台程序,然后输入如下代码:
#include <cstdio>
using namespace std;
class X {
public:
int get1() {
return 1;
}
virtual int get2() {
return 2;
}
virtual int get3() {
return 3;
}
};
int main() {
X x;
X* xp = &x;
int(X::*gp1)() = &X::get1;
int(X::*gp2)() = &X::get2;
int(X::*gp3)() = &X::get3;
printf("gp1 = %lu\n", gp1);
printf("gp2 = %lu\n", gp2);
printf("gp3 = %lu\n", gp3);
(x.*gp1)();
(xp->*gp1)();
(x.*gp2)();
(xp->*gp2)();
(x.*gp3)();
(xp->*gp3)();
}
在main入口处下断点,然后查看菜单:调试>窗口>反汇编。关闭符号,有利于理解。为了减少代码长度,将部分无关汇编代码省略。代码如下所示:
__setdefaultprecision:
000D1005 jmp 000D3730
__setargv:
000D100A jmp 000D3040
X::get3:
000D100F jmp 000D1510
X::`vcall'{4}':
000D1014 jmp 000D1483
......
X::`vcall'{0}':
000D1131 jmp 000D1488
......
X::get1:
000D1159 jmp 000D1490
......
_main:
000D1168 jmp 000D1550
X::get2:
000D116D jmp 000D14D0
......
X::X:
000D1440 push ebp
000D1441 mov ebp,esp
000D1443 sub esp,0CCh
000D1449 push ebx
000D144A push esi
000D144B push edi
000D144C push ecx
000D144D lea edi,[ebp+FFFFFF34h]
000D1453 mov ecx,33h
000D1458 mov eax,0CCCCCCCCh
000D145D rep stos dword ptr es:[edi]
000D145F pop ecx
000D1460 mov dword ptr [ebp-8],ecx
000D1463 mov eax,dword ptr [ebp-8]
000D1466 mov dword ptr [eax],0D685Ch
000D146C mov eax,dword ptr [ebp-8]
000D146F pop edi
000D1470 pop esi
000D1471 pop ebx
000D1472 mov esp,ebp
000D1474 pop ebp
000D1475 ret
......
X::`vcall'{4}':
000D1483 mov eax,dword ptr [ecx]
000D1485 jmp dword ptr [eax+4]
X::`vcall'{0}':
000D1488 mov eax,dword ptr [ecx]
000D148A jmp dword ptr [eax]
000D148C int 3
000D148D int 3
000D148E int 3
000D148F int 3
--- e:\work\currentproject\微博\consoleapplication1\consoleapplication1\consoleapplication1.cpp
1: // ConsoleApplication1.cpp : 定义控制台应用程序的入口点。
2: //
3:
4:
5:
6: using namespace std;
7:
8: class X {
9: public:
10: int get1() {
000D1490 push ebp
000D1491 mov ebp,esp
000D1493 sub esp,0CCh
000D1499 push ebx
000D149A push esi
000D149B push edi
000D149C push ecx
000D149D lea edi,[ebp+FFFFFF34h]
000D14A3 mov ecx,33h
000D14A8 mov eax,0CCCCCCCCh
000D14AD rep stos dword ptr es:[edi]
000D14AF pop ecx
000D14B0 mov dword ptr [ebp-8],ecx
11: return 1;
000D14B3 mov eax,1
12: }
000D14B8 pop edi
000D14B9 pop esi
000D14BA pop ebx
000D14BB mov esp,ebp
000D14BD pop ebp
000D14BE ret
--- e:\work\currentproject\微博\consoleapplication1\consoleapplication1\consoleapplication1.cpp
13: virtual int get2() {
000D14D0 push ebp
000D14D1 mov ebp,esp
000D14D3 sub esp,0CCh
000D14D9 push ebx
000D14DA push esi
000D14DB push edi
000D14DC push ecx
000D14DD lea edi,[ebp+FFFFFF34h]
000D14E3 mov ecx,33h
000D14E8 mov eax,0CCCCCCCCh
000D14ED rep stos dword ptr es:[edi]
000D14EF pop ecx
000D14F0 mov dword ptr [ebp-8],ecx
14: return 2;
000D14F3 mov eax,2
15: }
000D14F8 pop edi
000D14F9 pop esi
000D14FA pop ebx
000D14FB mov esp,ebp
000D14FD pop ebp
000D14FE ret
--- e:\work\currentproject\微博\consoleapplication1\consoleapplication1\consoleapplication1.cpp
16: virtual int get3() {
000D1510 push ebp
000D1511 mov ebp,esp
000D1513 sub esp,0CCh
000D1519 push ebx
000D151A push esi
000D151B push edi
000D151C push ecx
000D151D lea edi,[ebp+FFFFFF34h]
000D1523 mov ecx,33h
000D1528 mov eax,0CCCCCCCCh
000D152D rep stos dword ptr es:[edi]
000D152F pop ecx
000D1530 mov dword ptr [ebp-8],ecx
17: return 3;
000D1533 mov eax,3
18: }
000D1538 pop edi
000D1539 pop esi
000D153A pop ebx
000D153B mov esp,ebp
000D153D pop ebp
000D153E ret
--- 无源文件 -----------------------------------------------------------------------
000D153F int 3
000D1540 int 3
000D1541 int 3
000D1542 int 3
000D1543 int 3
000D1544 int 3
000D1545 int 3
000D1546 int 3
000D1547 int 3
000D1548 int 3
000D1549 int 3
000D154A int 3
000D154B int 3
000D154C int 3
000D154D int 3
000D154E int 3
000D154F int 3
--- e:\work\currentproject\微博\consoleapplication1\consoleapplication1\consoleapplication1.cpp
19: };
20:
21: int main() {
000D1550 push ebp
000D1551 mov ebp,esp
000D1553 sub esp,100h
000D1559 push ebx
000D155A push esi
000D155B push edi
000D155C lea edi,[ebp+FFFFFF00h]
000D1562 mov ecx,40h
000D1567 mov eax,0CCCCCCCCh
000D156C rep stos dword ptr es:[edi]
000D156E mov eax,dword ptr ds:[000D9000h]
000D1573 xor eax,ebp
000D1575 mov dword ptr [ebp-4],eax
22: X x;
000D1578 lea ecx,[ebp-0Ch]
000D157B call 000D1145
23: X* xp = &x;
000D1580 lea eax,[ebp-0Ch]
000D1583 mov dword ptr [ebp-18h],eax
24: int(X::*gp1)() = &X::get1;
000D1586 mov dword ptr [ebp-24h],0D1159h
25: int(X::*gp2)() = &X::get2;
000D158D mov dword ptr [ebp-30h],0D1131h
26: int(X::*gp3)() = &X::get3;
000D1594 mov dword ptr [ebp-3Ch],0D1014h
27: /*********************输出各个成员函数指针的值*****************/
28: printf("gp1 = %lu\n", gp1);
000D159B mov esi,esp
000D159D mov eax,dword ptr [ebp-24h]
000D15A0 push eax
000D15A1 push 0D6868h
000D15A6 call dword ptr ds:[000DA11Ch]
000D15AC add esp,8
000D15AF cmp esi,esp
000D15B1 call 000D1163
29: printf("gp2 = %lu\n", gp2);
000D15B6 mov esi,esp
000D15B8 mov eax,dword ptr [ebp-30h]
000D15BB push eax
000D15BC push 0D6878h
000D15C1 call dword ptr ds:[000DA11Ch]
000D15C7 add esp,8
000D15CA cmp esi,esp
000D15CC call 000D1163
30: printf("gp3 = %lu\n", gp3);
000D15D1 mov esi,esp
000D15D3 mov eax,dword ptr [ebp-3Ch]
000D15D6 push eax
000D15D7 push 0D6888h
000D15DC call dword ptr ds:[000DA11Ch]
000D15E2 add esp,8
000D15E5 cmp esi,esp
000D15E7 call 000D1163
31:
32: /********************用成员函数指针调用虚函数*******************/
33: (x.*gp1)();
000D15EC mov esi,esp
000D15EE lea ecx,[ebp-0Ch]
000D15F1 call dword ptr [ebp-24h]
000D15F4 cmp esi,esp
000D15F6 call 000D1163
34: (xp->*gp1)();
000D15FB mov esi,esp
000D15FD mov ecx,dword ptr [ebp-18h]
000D1600 call dword ptr [ebp-24h]
000D1603 cmp esi,esp
000D1605 call 000D1163
35: (x.*gp2)();
000D160A mov esi,esp
000D160C lea ecx,[ebp-0Ch]
000D160F call dword ptr [ebp-30h]
000D1612 cmp esi,esp
000D1614 call 000D1163
36: (xp->*gp2)();
000D1619 mov esi,esp
000D161B mov ecx,dword ptr [ebp-18h]
000D161E call dword ptr [ebp-30h]
000D1621 cmp esi,esp
000D1623 call 000D1163
37: (x.*gp3)();
000D1628 mov esi,esp
000D162A lea ecx,[ebp-0Ch]
000D162D call dword ptr [ebp-3Ch]
000D1630 cmp esi,esp
000D1632 call 000D1163
38: (xp->*gp3)();
000D1637 mov esi,esp
000D1639 mov ecx,dword ptr [ebp-18h]
000D163C call dword ptr [ebp-3Ch]
000D163F cmp esi,esp
000D1641 call 000D1163
39: }
000D1646 xor eax,eax
000D1648 push edx
000D1649 mov ecx,ebp
000D164B push eax
000D164C lea edx,ds:[000D1678h]
000D1652 call 000D109B
000D1657 pop eax
000D1658 pop edx
000D1659 pop edi
000D165A pop esi
000D165B pop ebx
000D165C mov ecx,dword ptr [ebp-4]
000D165F xor ecx,ebp
000D1661 call 000D1028
000D1666 add esp,100h
000D166C cmp ebp,esp
000D166E call 000D1163
000D1673 mov esp,ebp
000D1675 pop ebp
000D1676 ret
000D1677 nop
000D1678 add dword ptr [eax],eax
000D167A add byte ptr [eax],al
000D167C adc byte ptr [esi],0Dh
000D167F add ah,dh
000D1681 ?? ??????
000D1682 ?? ??????
000D1683 inc dword ptr [eax+eax]
000D1686 add byte ptr [eax],al
000D1688 mov word ptr [esi],ss
000D168A or eax,0CC007800h
类X有3个成员函数,其中get1是普通的成员函数,而get2和get3都分别是虚成员函数。gp1存储的确实是成员函数get1的地址,而gp2和gp3存储的确不是虚函数get2和get3的地址,而是X::vcall{0}和X::vcall{4}的地址。成汇编代码中可以看到普通成员函数在编译后,函数地址是确定的,并不会随着再改变,而
mov eax, DWORD PTR [ecx];寄存是ecx里面保存的是对象x的首地址,;因此,这里是将对象x首地址处内存内容(即vftable首地址)给寄存器eax
jmp DWORD PTR [eax];跳转到vftable首地址处内存(里面存的是虚函数get2的地址)所存储的地址处执行;这里就是跳转去执行虚函数get2
通过汇编码发现,普通成员函数时通过地址直接调用,而虚成员函数时通先调用vcall函数,然后由vcall函数查询虚表调用相应的虚函数,由此可以看出,一个类里面的每一给虚函数都有一个vcall函数与之对应,通过vcall函数来调用相应的虚函数。
5.作者答疑
?
?
?
?
?
?
?
?
插件开发
?
?
?
?
?
?
?
?
p
h
8
o
n
e
=
1
8
9
2
8
8
9
9
7
2
8
W
e
i
8
X
i
n
=
1
8
9
2
8
8
9
9
7
2
8
Q
8
Q
=
3
1
2
1
1
7
2
7
1
--------插件开发--------\\ ph8one= \begin{matrix} 1 & 8 &9 & 2 &8 & 8 &9 & 9 &7 & 2 &8 \end{matrix}\\ Wei8Xin= \begin{matrix} 1 & 8 &9 & 2 &8 & 8 &9 & 9 &7 & 2 &8 \end{matrix}\\ Q8Q= \begin{matrix} 3 & 1 &2 & 1 &1 & 7 &2 & 7 &1 \end{matrix}
????????插件开发????????ph8one=1?8?9?2?8?8?9?9?7?2?8?Wei8Xin=1?8?9?2?8?8?9?9?7?2?8?Q8Q=3?1?2?1?1?7?2?7?1?
|