《深入探索C++对象模型》笔记 Chapter2 构造函数
《深入探索C++对象模型》笔记 Chapter3 成员变量
《深入探索C++对象模型》笔记 Chapter4 成员函数
第4章 成员函数
4.1 总述
非静态成员函数
C++的设计准则之一就是,非静态成员函数至少必须要和普通函数有相同的效率。
编译器会将成员函数转换为了对等的普通函数。转换步骤如下:
- 改写函数原型(signature),传入参数添加一个this指针
- 将对成员变量(非静态)的操作改为通过this指针来存取
- 对成员函数的函数名称做命名重整(mangling)
上述所说的 mangling 过程,通常函数名会被加上class名称的前缀,以及参数类型的后缀,从而确保每个函数名都是独一无二的。
虚函数
//虚函数调用
ptr->func();
//转为为
(*ptr->vptr[1])(ptr);
以上的转换,vptr表示指向虚函数表的指针;1是虚函数表中该函数的索引值,和func()函数关联;函数参数中的ptr表示this指针。
静态成员函数
首先来看看为什么需要静态成员函数。
在引入静态成员函数之前,C++规定所有成员函数都必须由对象来调用,然而有些成员函数不对成员变量操作,也就没必要传入this指针,而C++并不能辨识这种情况。
于是,如果将静态成员变量声明为private,就必须提供成员函数来调用它,也就必须要实例化一个对象才能调用,以至出现了以下奇葩的写法:
((ClassName*) 0 ) -> get_staticmember_func();
将0强转成class指针,然后调用成员函数返回一个静态成员变量。
而有了静态成员函数,才解决了上述所说的问题。静态成员函数的主要特性是没有this指针,以此延伸出了其他特性:
- 不能存取非静态成员
- 不能被声明为 const volatile virtual
- 不需要经由对象才能被调用
由于静态成员函数没有this指针,所以它在内存上的存储类似于普通函数。
4.2 虚函数
在C++中,多态表示以一个 public base class 的指针(或 reference),寻址出一个 derived class object 的意思。
单继承
如上图, Point3d 继承 Point2d , Point2d 继承 Point 。
virtual table 在编译期就已经确定,virtual table 的每一个函数地址称之为一个 slot ,派生类定义虚函数会 overriding 基类相应函数的 slot 。这样 ptr->z()
,不管 ptr 指向的对象是基类还是派生类,它调用 z() 时函数地址一定是放在第四个 slot ,于是编译器转换代码为 (*ptr->vptr[4])(ptr)
。
对于纯虚函数,slot 里放置的是 pure_virtual_called()
函数,这个函数如果被意外调用,通常会结束掉这个程序。可以利用这个特性做运行期异常处理。
多继承
虚继承
4.3 效率
经过测试,普通函数、非静态成员函数、静态成员函数的效率很相近。inline成员函数效率惊人。
4.4 指向成员函数的指针
A::*
意为指向类A成员的指针,我们可以用 void (A::*pmf)() = &A::func
表示指向A中 func() 成员函数的指针。对于非虚函数,这个指针指向一个函数地址,对于虚函数,由于地址在编译时期是未知的,所以存放的是该函数在虚函数表中的索引值。(个人以为,此时再说 pmf 是个指针就比较牵强了,它只是个记录函数相对位置的int值)
于是,(ptr->*pmf)()
就会被编译器翻译为 (*ptr->vptr[(int)pmf])(ptr)
。可以写个demo做个验证:
#include <iostream>
#include <stdio.h>
using namespace std;
class A {
public:
void common(){}
virtual void foo() { printf("A::foo(): this = 0x%p\n", this); }
};
class B :public A{
public:
virtual void foo() { printf("B::foo(): this = 0x%p\n", this); }
virtual void bar() { printf("B::bar(): this = 0x%p\n", this); }
virtual void foo(int i) { printf("B::bar(): this = 0x%p\n", this); }
};
void (A::*pafoo)() = &A::foo;
void (B::*pbfoo)() = &B::foo;
void (B::*pbbar)() = &B::bar;
void (A::*pcommon)() = &A::common;
int main(){
A* a = new A;
B* b = new B;
printf("A::commont: %x\n",pcommon);
printf("A::foo: %x\n",pafoo);
printf("B::foo: %x\n",pbfoo);
printf("B::bar: %x\n",pbbar);
}
输出如下:
A::commont: 31425c9e
A::foo: 1
B::foo: 1
B::bar: 9
至于为什么索引值从1开始,书上没有说,不过想来和 3.6指向成员函数的指针 所说的类似,为了区分指向成员函数的空指针。
而索引值按8递增,那是因为我的云主机是64位,一个指针8字节。
单继承的情况,一个索引值就足够调用不同虚函数了,但是考虑到多继承呢?一个派生类继承自A和B,调用它的虚函数时,this指针作为传入参数可能是A,也可能是B,所以多继承还需要考虑this指针的偏移。我本来想仿照此篇博客 C/C++杂记:深入理解数据成员指针、函数成员指针 打印出this指针的偏移,但是事与愿违,偏移值始终为0,也只能归因于不同编译器的实现方式不同了。
4.5 内联函数
inline关键字并不能强迫将任何函数都变成 inline ,而是给编译器一个建议。至于编译器是否采纳这个建议,需要一系列复杂的测试,包括计算赋值、调用函数、调用虚函数的次数,每种操作都会有一个权值,这些操作与权值的乘积之和就是函数的复杂度。
对于inline函数,如果传入的参数是变量或者常量,那么直接对函数体内代码执行参数替换就可以了,但是如果传入的参数是个表达式呢?难道函数体内的代码每次碰到这个表达式都要算一遍?显示编译器不会这么傻。针对这种情况,编译器会产生一个临时对象,以避免重复求值。
再考虑一种情况,如果inline函数中有局部变量会怎么样?如果直接把代码扩展到调用inline函数的函数,万一后者有同名的变量呢?所以需要把局部变量放在封闭区段中,让它们有自己的作用域。这个过程依旧会产生局部变量。
可以看到,局部变量和表达式参数都会使inline函数产生临时对象,所以使用inline不当会产生大量临时对象反而降低效率。