普通成员函数的调用
C++的设计准则之一就是:普通成员函数的调用至少和全局函数有相同的效率。而事实上,C++编译器的实现是把普通成员函数转化为对全局函数的调用,请看下面的代码:
#include<iostream>
using namespace std;
class A{
public:
const char *name = "class A";
int val = 100;
void print(){
cout << "name = " << name << " val = " << val << endl;
}
};
void printA(A *a){
cout << "name = " << a->name << " val = " << a->val << endl;
}
int main(){
A a;
a.print();
printA(&a);
return 0;
}
这种转化,编译器需要做三件事:
- 改变函数参数,把this指针当做第一个参数传入函数。
- 对成员的访问都通过this指针。
- 把成员函数重新写成一个外部函数。
要验证这一点,可以查看汇编代码:
a.print();
00007FF7C69C461D lea rcx,[a]
00007FF7C69C4622 call A::print (07FF7C69C123Ah)
printA(&a);
00007FF7C69C4627 lea rcx,[a]
00007FF7C69C462C call printA (07FF7C69C1221h)
可以看出,明明print函数没有参数,但是还是传入了一个this指针,和printA函数的调用方式一样。
静态成员函数
静态成员函数是C++后面才引入的,在没有引入之前,也有类似的函数调用方法。下面是一个例子:
#include<iostream>
using namespace std;
class A{
public:
int val;
static void print(){
cout << "hello world" << endl;
}
};
int main(){
((A *)0)->print();
return 0;
}
可以想象,这样调用时没有任何问题的,因为C++编译器把成员函数改为了全局函数,这样也会传入this指针,并且没有访问成员的值,所以这样调用是没有问题的。这也引出了静态全局函数和普通成员函数的最大差别,没有this指针。总的来说,静态成员函数有以下的特点:
- 静态成员函数没有this指针,也就是说,无法访问类中的非静态成员函数。
- 静态成员函数不能使用const(没有this指针)修饰,也不能使用virtual(无法访问虚函数表指针)修饰。
- 可以使用类对象调用,但是无法访问类对象中的成员,也可以使用类名调用。
- 静态成员函数等同于非成员函数,有的需要提供回调函数的场合,可以将静态成员函数作为回调函数。
虚函数的调用
虚函数其实和普通成员函数的调用差不多,不同的是,虚函数的调用需要通过虚函数表,请看下面一个例子:
#include<iostream>
using namespace std;
class A{
public:
void * vptr;
const char *name = "class A";
int val = 100;
virtual void print_name(){
cout << "name = " << name << endl;
}
virtual void print_val(){
cout << "val = " << val << endl;
}
};
void a_name(A *a){
cout << "name = " << a->name << endl;
}
void a_val(A *a){
cout << "val = " << a->val << endl;
}
typedef void(*func)(A*);
int main(){
A *a = new A();
void * vt[2] = {(void *)a_name,(void *)a_val};
a->vptr = vt;
a->print_name();
a->print_val();
(*((func *)(a->vptr)))(a);
(((func *)a->vptr)[1])(a);
(*((func *)a->vptr)[1])(a);
(*((func *)(a->vptr) + 1))(a);
return 0;
}
这里有一些值得注意的东西,第一,就是数组名相当于对数组取了地址,也就是说,char *name[],使用name就相当于使用char **。第二个就是[i]操作符相当于*(name + i)。第三个就是对于函数指针,可以不适用*解引用,也可以不使用;大多数时候都是不使用的。
搞清楚了上面的问题,代码就比较好理解了,上面的代码手动模拟了编译器的处理,生成虚函数表和虚函数指针,然后使用虚函数指针取调用虚函数,这样就可以实现多态。如果要验证,还是要看看汇编代码,调用print_val的汇编代码如下:
a->print_val();
00007FF6D83F5256 mov rax,qword ptr [a] //rax = a
00007FF6D83F525B mov rax,qword ptr [rax] //rax = *a = vptr
00007FF6D83F525E mov rcx,qword ptr [a] //rcx = a
00007FF6D83F5263 call qword ptr [rax+8] //vptr指向第二个函数
可以看出,对于虚函数,还是需要传入this指针,并且还需要传入虚函数在虚函数表中的索引。
对于继承体系来说,还有可能需要调整this指针,这种情况比较复杂,就不展开讨论了,就简单说一下在多继承体系下不适用虚析构函数会出现问题。
#include<iostream>
using namespace std;
class A{
public:
int a_val;
};
class B{
public:
int b_val;
};
class C:public A,public B{
public:
int c_val;
};
int main(){
B *b = new C();
delete b;
return 0;
}
其实这就是this指针调整出现的问题,在C语言中我们就知道,free函数必须是malloc分配的指针,并且这个指针不能改变;而这里出现的错误,就是这个this指针并没有指向开始分配的内存(new在底层会调用malloc),导致free(delete在底层调用了free)的调用失败。这里,如果想要程序运行正常的话,只需要把父类的析构函数声明会虚函数。这个例子说明,在继承体系中,父类的析构函数最好声明为虚函数。
指向成员函数的指针
和指向类成员的指针一样,C++也有指向成员函数的指针,这个个人感觉可以了解,平时用得很少。
#include<iostream>
using namespace std;
class A{
public:
int val;
void f1(){
cout << "A::f1()" << endl;
}
virtual void f2(){
cout << "A::f2()" << endl;
}
};
class B :public A{
public:
virtual void f2() override{
cout << "B::f2()" << endl;
}
};
int main(){
void(A::*p1)() = &A::f1;
void(A::*p2)() = &A::f2;
printf("%p %p\n");
A *a = new B();
(a->*p1)();
(a->*p2)();
return 0;
}
这种调用之所以比较奇怪,是因为它必须要绑定一个对象才能调用,在语法上会有点奇怪。