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);