C++对象模型:Function 语意学

Member 的各种调用方式

Nonstatic Member Function

使用C++时,成员函数和非成员函数在性能上应该是等价的。当设计类时,我们不应该因为担心效率问题而避免使用成员函数。

实现:编译器会将成员函数转换为一个带有额外this指针参数的非成员函数,这使得成员函数可以直接访问对象的数据成员。

成员函数到非成员函数的转换

签名修改:首先,成员函数的签名会被修改,在最前面添加一个隐式的this指针作为参数。如果成员函数是const的,那么this指针也会是const。

非const成员函数:

float Point3d::magnitude() { ... }

转换后:

float Point3d::magnitude(Point3d *const this) { ... }

const成员函数:

float Point3d::magnitude() const { ... }

转换后:

float Point3d::magnitude(const Point3d *const this) { ... }

通过this指针存取数据成员:接下来,所有对非静态数据成员的直接访问都会被替换为通过this指针的间接访问。 例如:

return sqrt(_x*_x +_y*_y + _z*_z);

会被转换为:

return sqrt(this->_x * this->_x + this->_y * this->_y + this->_z * this->_z);

名称修饰:最后,成员函数的名称会被进行“名称修饰”(name mangling),以确保它在整个程序中的唯一性。这样可以支持重载和其他语言特性。 假设原始的成员函数是:

float Point3d::magnitude() const;

编译器可能会生成如下形式的外部函数:

extern "C" float __Z9magnitudeRK7Point3d(const Point3d *const this);

调用转换:对于每个成员函数调用,编译器会生成相应的代码来传递当前对象的地址作为this指针。

对象直接调用:

obj.magnitude();

变为

__Z9magnitudeRK7Point3d(&obj);

指针调用:

ptr->magnitude();

变为:

__Z9magnitudeRK7Point3d(ptr);

优化例子

考虑一个归一化向量的成员函数:

Point3d Point3d::normalize() const {
    float mag = magnitude();
    return Point3d(_x / mag, _y / mag, _z / mag);
}

这个函数可能被转换成类似下面的形式(假设已经进行了NRV优化)

void normalize__7Point3dFv(register const Point3d *const this, Point3d &result) {
    register float mag = this->magnitude(); // 使用了转换后的magnitude函数
    new (&result) Point3d(this->_x / mag, this->_y / mag, this->_z / mag); // 直接构造
}

这里,&result代表了返回值的位置,new (&result)是放置新对象的原地构造(placement new)。这样做避免了默认构造函数的开销,并且直接创建了归一化后的Point3d对象。

名称的特殊处理(Name Mangling)

名称修饰(name mangling)将函数和成员变量的名称转换为唯一的内部表示形式。这样做为了支持重载、类成员访问以及跨模块链接时的类型安全。

解决同名问题:当一个基类和派生类中存在同名成员时,编译器需要一种方式来区分它们。

支持函数重载:即使两个函数具有相同的名字,只要它们的参数列表不同,编译器也需要能够生成不同的内部名称。

确保类型安全链接:通过将函数签名编码进名称中,可以防止链接时由于类型不匹配导致的错误。

考虑以下类定义:

class Bar {
public:
    int ival;
};
class Foo:public Bar {
public:
    int ival; // 与Bar::ival同名
};

Foo对象包含了一个Bar的实例和一个自己的ival。

为了区分这两个ival,编译器可能会对它们进行名称修饰如:

Bar::ival 可能被修饰为 ival__3Bar,Foo::ival 可能被修饰为 ival__3Foo。这使每个成员都有一个唯一的名字,避免了命名冲突。

对于成员函数,尤其是重载的成员函数,名称修饰更加复杂。因为除了类名外,还需要包括函数的参数类型信息。

class Point {
public:
    void x(float newX);
    float x();
};

编译器可能将这些函数修饰为:

void x__5PointFf(float newX); (5是Point的长度,Ff表示有一个float参数)
float x__5PointFv(); (Fv表示没有参数)

这确保即使函数名字相同,只要参数列表不同,就会有不同的内部名称。

类型安全链接

名称修饰有助于在链接阶段进行有限的形式类型检查。如果有一个print函数定义如下:

void print(const Point3d& p) { ... }

而用户意外地声明并调用它为:

// 错误的声明
void print(const Point3d* p);

由于名称修饰的不同,链接器会发现无法解析这个函数调用,从而报错。这称为“类型安全链接”(type-safe linkage)。这种机制只能检测函数签名(即名称、参数个数和类型)的错误,而不能检测返回类型的错误。

Virtual Member Functions(虚拟成员函数)

C++中虚拟成员函数是实现多态的关键机制。当一个成员函数被声明为virtual时,它可以在派生类中被重写,并且通过基类指针或引用调用时,会根据实际对象的类型来决定调用哪个版本的函数。

假设normalize()是一个虚拟成员函数,那么以下的调用:

ptr->normalize();

会被内部转换为:

(*ptr->vptr[1])(ptr);

vptr 是编译器生成的一个指向虚函数表的指针。每个包含或继承了至少一个虚拟函数的对象都会有一个这样的指针。1 是虚函数表中的索引值,对应于normalize()函数的位置。

第二个 ptr 表示 this 指针,即当前对象的地址。

显式调用虚函数以避免虚函数机制开销

如果在同一个类的方法中调用另一个虚函数,并且已经知道具体的类型,可以直接调用该函数,而不是通过虚函数机制。如在 Point3d::normalize() 中调用 magnitude() 时,直接调用 Point3d::magnitude() 会更高效,因为它避免了查找vtable的过程。

register float mag = Point3d::magnitude();

内联虚函数

如果虚函数被声明为内联,编译器可以直接展开函数体,从而进一步提高性能。如果 magnitude() 是内联的,那么在 normalize() 中调用它时,编译器可以直接将 magnitude() 的代码嵌入到 normalize() 中,避免了虚函数机制的开销。

对象直接调用虚函数

对于直接通过对象调用虚函数的情况,编译器可以进行优化,直接调用具体类型的函数,而不是通过虚函数表。如对于 obj.normalize();,编译器可以直接调用 Point3d::normalize(),而不需要通过 vptr 查找vtable。

normalize__7Point3dFv(dobj);

假设我们有以下类定义:

class Point3d {
public:
    float _x, _y, _z;
    virtual float magnitude() const {
        return sqrt(_x * _x + _y * _y + _z * _z);
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值