本文转载自:http://blog.51cto.com/9291927/2148695
总结:
一、成员函数的内存为什么不会反映在sizeof运算符上?
成员函数可以被看作是类作用域的全局函数, 不在对象分配的空间里, 只有虚函数才会在类对象里有一个指针, 存放虚函数的地址等相关信息(即虚函数表)。
成员函数的地址,编译期就已确定,并静态绑定或动态的绑定在对应的对象上。对象调用成员函数时,编译器可以确定这些函数的地址,并通过传入this指针和其
他参数,完成函数的调用,所以类中就没有必要存储成员函数的信息。
this指针是编译器默认传给类中非静态函数的隐含形参,其作用域在非静态成员函数的函数体内。在类作用域中,不同类的非静态成员函数中,this指针变
量的链接属性是内部的,但其所指对象是外部的,即this变量是不同的实体,但指向对象是同一个。
this指针是由编译器生成,当类的非静态成员函数的参数个数一定时,this指针存储在ECX寄存器中;若该函数参数个数未定(可变参数函数),则存放在栈中。
this指针并不是对象的一部分,this指针所占的内存大小是不会反映在sizeof操作符上的。this指针的类型取决于使用this指针的成员函数类型以及对象类型。类的成员
函数默认第一个参数为其对象的this指针。this在成员函数的开始执行前构造,在成员函数执行结束后清除。
类中的成员函数存在于代码段。调用成员函数时,类对象的地址作为参数隐式传递给成员函数,成员函数通过对象地址隐式访问成员变量,C++语法隐藏了对象
地址的传递过程。由于类成员函数内部有一个this指针,类成员函数的this指针会被调用的类对象地址赋值。因此,如果类成员函数中没有使用this指针访问成员,则类
指针为NULL时仍然可以成功对该成员函数进行调用。static成员函数作为一种特殊的成员函数,函数内部不存在this指针,因此类指针为NULL时同样可以成功对静态成
员函数进行调用。
二、虚成员函数调用机制分析(虚表指针以4字节保存在类内,指示了虚表的地址,而虚表本身并不在类内!)
虚成员函数的调用涉及运行时多态。当一个对象调用虚函数时,首先通过运行时对象获取指向虚函数表指针的值得到虚函数表的地址,然后将虚函数表的地址加上虚函数
离表头的偏移量即为虚函数的地址。 基类对象内部的虚函数表指针指向基类的虚函数表,派生类对象的虚函数表指针指向派生类的虚函数表,确保运行时对象调用正确的
虚函数。
那么如何定位虚表呢?编译器另外还为每个对象提供了一个虚表指针(即vptr),这个指针指向了对象所属类的虚表,在程序运行时,根据对象的类型去初始化vptr,从而让
vptr正确的指向了所属类的虚表,从而在调用虚函数的时候,能够找到正确的函数,对于第二段代码程序,由于pFather实际指向的对象类型是Son,因此vptr指向的Son类的虚函
数表,当调用pFather->Son()时,根据虚表中的函数地址找到的就是Son类的Say()函数.(此处参考文章:https://www.cnblogs.com/FengZeng666/p/9341861.html)
正是由于每个对象调用的虚函数都是通过虚表指针来索引的,也就决定了虚表指针的正确初始化是非常重要的,换句话说,在虚表指针没有正确初始化之前,我们不能够去
调用虚函数,那么虚表指针是在什么时候,或者什么地方初始化呢?
答案是在构造函数中进行虚表的创建和虚表指针的初始化,在构造子类对象时,要先调用父类的构造函数,此时编译器只“看到了”父类,并不知道后面是否还有继承者,它初
始化父类对象的虚表指针,该虚表指针指向父类的虚表,当执行子类的构造函数时,子类对象的虚表指针被初始化,指向自身的虚表。
以下是详细解释:
一、C++成员函数
1、C++成员函数的编译
C++中的函数在编译时会根据命名空间、类、参数签名等信息进行重新命名,形成新的函数名。函数重命名的过程通过一个特殊的Name Mangling(名字编码)算法来实现。Name Mangling算法是一种可逆的算法,既可以通过现有函数名计算出新函数名,也可以通过新函数名逆向推导出原有函数名。
Name Mangling算法可以确保新函数名的唯一性,只要命名空间、所属的类、参数签名等有一个不同,那么产生的新函数名也不同。不同的编译器有不同的 Name Mangling 算法,产生的函数名也不一样。2、this指针
this指针属性如下:
A、名称属性:标识符this表示。B、类型属性:classname constC、值属性:表示当前调用该函数对象的首地址。D、作用域:this指针是编译器默认传给类中非静态函数的隐含形参,其作用域在非静态成员函数的函数体内。E、链接属性:在类作用域中,不同类的非静态成员函数中,this指针变量的链接属性是内部的,但其所指对象是外部的,即this变量是不同的实体,但指向对象是同一个。F、存储类型:this指针是由编译器生成,当类的非静态成员函数的参数个数一定时,this指针存储在ECX寄存器中;若该函数参数个数未定(可变参数函数),则存放在栈中。this指针并不是对象的一部分,this指针所占的内存大小是不会反映在sizeof操作符上的。this指针的类型取决于使用this指针的成员函数类型以及对象类型。类的成员函数默认第一个参数为T const register this。this在成员函数的开始执行前构造,在成员函数执行结束后清除。二、C++成员函数指针
1、C++成员函数指针简介
C++语言规定,成员函数指针具有contravariance特性,即基类的成员函数指针可以赋值给派生类的成员函数指针,C++语言提供了默认的转换方式,但反过来不行。
C++编译器在代码编译阶段会对类对象调用的成员函数进行静态绑定(虚函数进行动态绑定),类成员函数的地址在代码编译时就确定,类成员函数地址可以使用成员函数指针进行保存。成员函数指针定义语法如下:ReturnType (ClassName::* pointerName) (ArgumentLList);ReturnType:成员函数返回类型ClassName: 成员函数所属类的名称Argument_List: 成员函数参数列表pointerName:指针名称
class Test{public: void print() { cout << "Test::print" << endl; } };
成员函数指针语法极其严格:
A、不能使用括号:例如&(Test::print)不对。B、 必须有限定符:例如&print不对,即使在类ClassName作用域内也不行。C、必须使用取地址符号:直接写Test::print不行,必须写:&Test::print。Test类的成员函数print的函数指针声明如下:void (Test::*pFun)();
初始化如下:pFunc = &Test::print;
Test类的成员函数print的函数指针声明及初始化如下:void (Test::* pFunc)() = &Test::print;
通常,为了简化代码,使用typedef关键字。 typedef void (Test::*pFunc)(); pFunc p = &Test::print;
可以通过函数指针调用成员函数,示例代码如下:
#includeusing 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::print Test* pTest = &test; //通过指针调用成员函数 (pTest->*pFunc)();//Test::print //pFunc();//error //error: must use '.*' or '->*' to call pointer-to-member //function in 'pFunc (...)', e.g. '(... ->* pFunc) (...)' return 0; }
上述代码中,.*pFunc
将pFunc绑定到对象test,->*pFunc
绑定pFunc到pTest指针所指向的对象。
2、C++成员函数地址
C++成员函数使用thiscall函数调用约定。C++静态成员函数、普通成员函数的函数地址在代码区,虚成员函数地址是一个相对地址。
#includeusing namespace std; class Parent { public: Parent(int i, int j) { m_i = i; m_j = j; cout << "Parent(int i, int j): " << this << endl; } virtual void print() { cout << "Parent::" << __func__<< endl; cout << "m_i = "<< m_i << endl; cout << "m_j = "<< m_j << endl; } virtual void sayHello() { cout << "Parent::sayHello()" << endl; } virtual void func() { cout << "Parent::func()" << endl; } virtual ~Parent() { cout << "~Parent(): " << this << endl; } static void display() { cout << "Parent::display()" << endl; } int add(int v) { return m_i + m_j + v; } protected: int m_i; int m_j; }; int main(int argc, char *argv[]) { cout <<&Parent::display<
上述代码中,打印出的所有的成员函数的地址为1。原因在于输出操作符<<没有对C++成员函数指针类型进行重载,C++编译器将C++成员函数指针类型转换为bool类型进行了输出,所以所有的输出为1。因此,C++成员函数地址进行打印时不能使用cout,可以用printf输出,因为printf可以接收任意类型的参数,包括__thiscall
类型。
#includeusing namespace std; class Parent { public: Parent(int i, int j) { m_i = i; m_j = j; cout << "Parent(int i, int j): " << this << endl; } virtual void print() { cout << "Parent::" << __func__<< endl; cout << "m_i = "<< m_i << endl; cout << "m_j = "<< m_j << endl; } virtual void sayHello() { cout << "Parent::sayHello()" << endl; } virtual void func() { cout << "Parent::func()" << endl; } virtual ~Parent() { cout << "~Parent(): " << this << endl; } static void display() { cout << "Parent::display()" << endl; } int add(int v) { return m_i + m_j + v; } protected: int m_i; int m_j; }; int main(int argc, char *argv[]) { //静态成员函数 cout << "static member function addree:" << endl; printf("0x%p\n", &Parent::display); printf("0x%p\n", Parent::display); //普通成员函数 cout << "normal member function addree:" << endl; printf("0x%p\n", &Parent::add); cout << "virtual member function addree:" << endl; //虚成员函数 printf("%d\n", &Parent::print);//1 printf("%d\n", &Parent::sayHello);//5 printf("%d\n", &Parent::func);//9 return 0; }
3、C++编译器成员函数指针的实现
C++编译器要实现成员函数指针,必须解决下列问题:
A、成员函数是不是虚函数。B、成员 函数运行时,需不需要调整this指针,如何调整。不需要调整this指针的情况如下:A、继承树最顶层的类。B、单继承,若所有类都不含有虚函数,那么继承树上所有类都不需要调整this指针。C、单继承,若最顶层的类含有虚函数,那么继承树上所有类都不需要调整this指针。可能需要进行this指针调整的情况如下:A、多继承B、单继承,最顶的base class不含virtual function,但继承类含虚函数,继承类可能需要进行this指针调整。Microsoft VC对C++成员函数指针的实现采用的是Microsoft一贯使用的Thunk技术。Microsoft将成员函数指针分为两种:struct pmf_type1{ void* vcall_addr;};struct pmf_type2{ void* vcall_addr; int delta; //调整this指针用 };
vcall_addr是Microsoft 的Thunk技术核心所在。vcall_addr是一个指针,隐藏了它所指的函数是虚拟函数还是普通函数的区别。如果所指的成员函数是一个普通成员函数,vcall_addr是成员函数的函数地址。如果所指的成员函数是虚成员函数,那么vcall_addr指向一小段代码,这段代码会根据this指针和虚函数索引值寻找出真正的函数地址,然后跳转到真实的函数地址处执行。
Microsoft根据情况选用函数指针结构表示成员函数指针,使用Thunk技术(vcall_addr)实现虚拟函数/非虚拟函数的自适应,在必要的时候进行this指针调整(使用delta)。GCC对于成员函数指针统一使用下面的结构进行表示:struct { void* __pfn; //函数地址,或者是虚拟函数的index long __delta; // offset, 用来进行this指针调整 };
不管是普通成员函数,还是虚成员函数,信息都记录在__pfn
。一般来说因为对齐的关系,函数地址都至少是4字节对齐的。即函数地址的最低位两个bit总是0。 GCC充分利用了这两个bit。如果是普通的函数,__pfn
记录函数的真实地址,最低位两个bit就是全0,如果是虚成员函数,最后两个bit不是0,剩下的30bit就是虚成员函数在函数表中的索引值。
__delta
变量,如果不需要调整,__delta=0
。GCC的实现比Microsoft简单,在所有场合其实现方式都是一样的。 4、C++成员函数指针的限制
C++语言的规定,基类的成员函数指针可以赋值给派生类的成员函数指针,不允许继承类的成员函数指针赋值给基类成员函数指针。
C++规定编译器必须提供一个从基类成员函数指针到继承类成员函数指针的默认转换。C++编译器提供的默认转换最关键的就是this指针调整。因此,一般情况下不要将继承类的成员函数指针赋值给基类成员函数指针。不同C++编译器可能有不同的表现。解决方案:A、不要使用static_cast将继承类的成员函数指针赋值给基类成员函数指针,如果一定要使用,首先确定没有问题。B、如果一定要使用static_cast,注意不要使用多继承。C、如果一定要使用多继承的话,不要把一个基类的成员函数指针赋值给另一个基类的函数指针。D、单继承要么全部不使用虚函数,要么全部使用虚函数。不要使用非虚基类,却让子类包含虚函数。#include#include using namespace std; class Parent { public: Parent(int i, int j) { m_i = i; m_j = j; } void print() { cout << "Parent::" << __func__<< endl; cout << "m_i = "<< m_i << endl; cout << "m_j = "<< m_j << endl; } double sum() { cout << "Parent::" << __func__<< endl; double ret = m_i + m_j; cout < << endl; return ret; } void display() { cout << "Parent::display()" << endl; } int add(int value) { return m_i + m_j + value; } protected: int m_i; int m_j; }; class ChildA : public Parent { public: ChildA(int i, int j, double d):Parent(i, j) { m_d = d; } void print() { cout << "ChildA::" << __func__<< endl; cout << "m_i = "<< m_i << endl; cout << "m_j = "<< m_j << endl; cout << "m_d = "<< m_d << endl; } double sum() { cout << "ChildA::" << __func__<< endl; double ret = m_i + m_j + m_d; cout << ret << endl; return ret; } private: void display() { cout << "ChildA::display()" << endl; } private: double m_d; }; int main(int argc, char *argv[]) { Parent parent(100,200); ChildA childA(1,2,3.14); Parent* pTestA = &childA; typedef void (Parent::*pPrintFunc)(); pPrintFunc pPrint = &Parent::print; typedef double (Parent::*pSumFunc)(); pSumFunc pSum = &Parent::sum; typedef void (Parent::*pDisplayFunc)(); pDisplayFunc pDisplay = &Parent::display; printf("0x%X\n",pPrint); printf("0x%X\n",pSum); printf("0x%X\n",pDisplay); //不能将派生类的成员函数指针赋值给基类的函数指针 //pPrint = &ChildA::print;//error //可以将基类的成员函数指针赋值给派生类 void (ChildA::*pChildPrintFunc)() = pPrint; (childA.*pChildPrintFunc)();//Parent::print void (*p)() = reinterpret_cast (pPrint); p(); return 0; }
5、静态成员函数指针
对于静态成员函数,函数体内部没有this指针,与类的其它成员函数共享类的命名空间,但静态成员函数并不是类的一部分,静态成员函数与常规的全局函数一样,成员函数指针的语法针对静态成员函数并不成立。
静态成员函数的函数指针定义语法如下:ReturnType (* pointerName) (ArgumentLList);ReturnType:成员函数返回类型Argument_List: 成员函数参数列表pointerName:指针名称
静态成员函数的函数指针的使用与全局函数相同,但静态成员函数指针保存的仍旧是个相对地址。
#includeusing namespace std; class Test { public: static void print() { cout << "Test::print" << endl; } }; int main(int argc, char *argv[]) { void (* pFunc)() = &Test::print; cout << pFunc << endl;//1 //直接调用 pFunc();//Test::print (*pFunc)();//Test::print Test test; //(test.*pFunc)();//error Test* pTest = &test; //(pTest->*pFunc)();//error return 0; }
6、普通成员函数指针
非静态、非虚的普通成员函数指针不能直接调用,必须绑定一个类对象。
普通函数指针的值指向代码区中的函数地址。如果强制转换为普通函数指针后调用,成员函数内部this指针访问的成员变量将是垃圾值。#include#include using namespace std; class Parent { public: Parent(int i, int j) { m_i = i; m_j = j; } void print() { cout << "Parent::" << __func__<< endl; cout << "m_i = "<< m_i << endl; cout << "m_j = "<< m_j << endl; } double sum() { cout << "Parent::" << __func__<< endl; double ret = m_i + m_j; cout < << endl; return ret; } void display() { cout << "Parent::display()" << endl; } int add(int value) { return m_i + m_j + value; } protected: int m_i; int m_j; }; class ChildA : public Parent { public: ChildA(int i, int j, double d):Parent(i, j) { m_d = d; } void print() { cout << "ChildA::" << __func__<< endl; cout << "m_i = "<< m_i << endl; cout << "m_j = "<< m_j << endl; cout << "m_d = "<< m_d << endl; } double sum() { cout << "ChildA::" << __func__<< endl; double ret = m_i + m_j + m_d; cout << ret << endl; return ret; } private: void display() { cout << "ChildA::display()" << endl; } private: double m_d; }; int main(int argc, char *argv[]) { Parent parent(100,200); ChildA childA(1,2,3.14); Parent* pTestA = &childA; typedef void (Parent::*pPrintFunc)(); pPrintFunc pPrint = &Parent::print; typedef double (Parent::*pSumFunc)(); pSumFunc pSum = &Parent::sum; typedef void (Parent::*pDisplayFunc)(); pDisplayFunc pDisplay = &Parent::display; printf("0x%X\n",pPrint); printf("0x%X\n",pSum); printf("0x%X\n",pDisplay); //绑定类对象进行调用 (pTestA->*pPrint)(); (pTestA->*pSum)(); (pTestA->*pDisplay)(); //强制转换为普通函数指针 void (*p)() = reinterpret_cast (pPrint); p();//打印随机值 return 0; }
7、虚成员函数指针
C++通过虚函数提供了运行时多态特性,编译器通常使用虚函数表实现虚函数。
#include#include using namespace std; class Parent { public: Parent(int i, int j) { m_i = i; m_j = j; } virtual void print() { cout << "Parent::" << __func__<< endl; cout << "m_i = "<< m_i << endl; cout << "m_j = "<< m_j << endl; } virtual double sum() { cout << "Parent::" << __func__<< endl; double ret = m_i + m_j; cout < << endl; return ret; } virtual void display() { cout << "Parent::display()" << endl; } int add(int value) { return m_i + m_j + value; } protected: int m_i; int m_j; }; class ChildA : public Parent { public: ChildA(int i, int j, double d):Parent(i, j) { m_d = d; } virtual void print() { cout << "ChildA::" << __func__<< endl; cout << "m_i = "<< m_i << endl; cout << "m_j = "<< m_j << endl; cout << "m_d = "<< m_d << endl; } virtual double sum() { cout << "ChildA::" << __func__<< endl; double ret = m_i + m_j + m_d; cout << ret << endl; return ret; } private: void display() { cout << "ChildA::display()" << endl; } private: double m_d; }; class ChildB : public Parent { public: ChildB(int i, int j, double d):Parent(i, j) { m_d = d; } virtual void print() { cout << "ChildB::" << __func__<< endl; cout << "m_i = "<< m_i << endl; cout << "m_j = "<< m_j << endl; cout << "m_d = "<< m_d << endl; } virtual double sum() { cout << "ChildB::" << __func__<< endl; double ret = m_i + m_j + m_d; cout << ret << endl; return ret; } private: void display() { cout << "ChildB::display()" << endl; } private: double m_d; }; int main(int argc, char *argv[]) { Parent parent(100,200); ChildA childA(1,2,3.14); //childA.display();//error,编译时private不可访问 ChildB childB(100,200,3.14); Parent* pTestA = &childA; Parent* pTestB = &childB; typedef void (Parent::*pVPrintFunc)(); pVPrintFunc pPrint = &Parent::print; (parent.*pPrint)();//Parent::print (pTestA->*pPrint)();//ChildA::print,多态 (pTestB->*pPrint)();//ChildB::print,多态 typedef double (Parent::*pVSumFunc)(); pVSumFunc pSum = &Parent::sum; (parent.*pSum)();//Parent::sum (pTestA->*pSum)();//ChildA::sum,多态 (pTestB->*pSum)();//ChildB::sum,多态 typedef void (Parent::*pVDisplayFunc)(); pVDisplayFunc pDisplay = &Parent::display; (parent.*pDisplay)();//Parent::display (pTestA->*pDisplay)();//ChildA::display,多态 (pTestB->*pDisplay)();//ChildB::display,多态 printf("0x%X\n",pPrint); printf("0x%X\n",pSum); printf("0x%X\n",pDisplay); return 0; }
虚成员函数指针的值是一个相对地址,表示虚函数在虚函数表中,离表头的偏移量+1。
当一个对象调用虚函数时,首先通过获取指向虚函数表指针的值得到虚函数表的地址,然后将虚函数表的地址加上虚函数离表头的偏移量即为虚函数的地址。8、成员函数指针示例
成员函数指针的一个重要应用是根据输入来生成响应事件,使用不同的处理函数来处理不同的输入。
#include#include #include using namespace std; //虚拟打印机 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; } // Output: // Strings are concatenated!
三、C++类成员函数的调用分析
1、成员函数调用简介
类中的成员函数存在于代码段。调用成员函数时,类对象的地址作为参数隐式传递给成员函数,成员函数通过对象地址隐式访问成员变量,C++语法隐藏了对象地址的传递过程。由于类成员函数内部有一个this指针,类成员函数的this指针会被调用的类对象地址赋值。因此,如果类成员函数中没有使用this指针访问成员,则类指针为NULL时仍然可以成功对该成员函数进行调用。static成员函数作为一种特殊的成员函数,函数内部不存在this指针,因此类指针为NULL时同样可以成功对静态成员函数进行调用。
#include#include using namespace std; namespace Core { class Test { public: Test(int i) { this->i = i; } void print() { cout << "i = " << i << endl; } void sayHello() { cout << "Hello,Test." << endl; } static void printHello() { cout << "Hello,Test." << endl; } private: int i; }; } int main() { using namespace Core; Core::Test* ptest = NULL; ptest->sayHello();//Hello,Test. ptest->printHello(); //定义函数指针类型 typedef void (Test::*pFunc)(); //获取类的成员函数地址 pFunc p = &Test::print; Test test(100); //调用成员函数 (test.*p)();//i = 100 return 0; }
2、普通成员函数调用机制分析
普通成员函数通过函数地址直接调用。
#includeusing namespace std; class Test { public: void print() { cout << "Test::print" << endl; } }; int main() { Test test; Test* p = &test; p->print(); }
对于非虚、非静态成员函数的调用,如p->print(),C++编译器会生成如下代码:
Test* const this = p;void Test::print(Test* const this) { cout << "Test::print" << endl; }
不管指针p是任何值,包括NULL,函数Test::print()都可以被调用,p被作为this指针并当作参数传递给print函数。因此,当传入print函数体内的p指针为NULL时,只要不对p指针进行解引用,函数就能正常调用而不发生异常退出。
#includeusing namespace std; class Test { public: void print() { cout << "Test::print" << endl; sayHello(); } void sayHello() { cout << "Test::sayHello" << endl; } }; int main() { Test* p = NULL; p->print(); } // output: // Test::print // Test::sayHello
3、静态成员函数调用机制分析
静态成员函数通过函数地址进行调用,其调用方式同全局函数。
4、虚成员函数调用机制分析
虚成员函数的调用涉及运行时多态。
当一个对象调用虚函数时,首先通过运行时对象获取指向虚函数表指针的值得到虚函数表的地址,然后将虚函数表的地址加上虚函数离表头的偏移量即为虚函数的地址。 基类对象内部的虚函数表指针指向基类的虚函数表,派生类对象的虚函数表指针指向派生类的虚函数表,确保运行时对象调用正确的虚函数。