成员函数是由编译器解释的,编译器只需要保证类中的成员函数只能够被类对象使用,同时将对象的指针作为成员函数的第一个参数传递即可。成员函数在编译过程中会根据对象的类型确定下来。
成员函数在实际意义上仅仅是一个语法限制,它会被编译器转化为类似非成员函数类型,不存在额外的访问负载。
成员函数又可以分为【静态成员函数】与【非静态成员函数】。
对于非静态成员函数而言,this 指针指向每一个对象的本地数据,对对象内成员的存取通过 this 指针来完成。
一个具体的示例如下:
#include
using namespace std;
class test
{
private:
char a;
int b;
public:
char get_a(void);
void set_a(char value);
};
char test::get_a(void)
{
return a;
}
void test::set_a(char value)
{
if (value > 0) {
a = value;
}
}
int normal_function(void)
{
cout << "This is a normal function\n";
return 0;
}
int main(int argc, char *argv[])
{
test obj1;
test *obj2 = new(test);
obj1.set_a('a');
obj2->set_a('b');
cout << "a of obj1 is " << obj1.get_a() << endl;
cout << "a of obj2 is " << obj2->get_a() << endl;
delete obj2;
return 0;
}
在 amd64 平台上编译,反汇编可执行文件并指定 demangle 后生成的成员函数汇编代码如下:
0000000000001196 :
1196: 55 push %rbp
1197: 48 89 e5 mov %rsp,%rbp
119a: 48 89 7d f8 mov %rdi,-0x8(%rbp)
119e: 48 8b 45 f8 mov -0x8(%rbp),%rax
11a2: 0f b6 00 movzbl (%rax),%eax
11a5: 5d pop %rbp
11a6: c3 retq
11a7: 90 nop
00000000000011a8 :
11a8: 55 push %rbp
11a9: 48 89 e5 mov %rsp,%rbp
11ac: 48 89 7d f8 mov %rdi,-0x8(%rbp)
11b0: 89 f0 mov %esi,%eax
11b2: 88 45 f4 mov %al,-0xc(%rbp)
11b5: 80 7d f4 00 cmpb $0x0,-0xc(%rbp)
11b9: 7e 0a jle 11c5
11bb: 48 8b 45 f8 mov -0x8(%rbp),%rax
11bf: 0f b6 55 f4 movzbl -0xc(%rbp),%edx
11c3: 88 10 mov %dl,(%rax)
11c5: 90 nop
11c6: 5d pop %rbp
11c7: c3 retq
上面的汇编代码中,this 指针通过 rdi 寄存器来传递。rdi 寄存器的值在成员函数调用前由调用函数传递,在这个例子里对应的函数就是 main 函数,相关的汇编代码如下:
00000000000011e6 :
......
1204: 48 8d 45 e0 lea -0x20(%rbp),%rax
1208: be 61 00 00 00 mov $0x61,%esi
120d: 48 89 c7 mov %rax,%rdi
1210: e8 93 ff ff ff callq 11a8
1215: 48 8d 45 e0 lea -0x20(%rbp),%rax
1219: 48 89 c7 mov %rax,%rdi
121c: e8 75 ff ff ff callq 1196
1221: 48 8b 45 e8 mov -0x18(%rbp),%rax
1225: be 62 00 00 00 mov $0x62,%esi
122a: 48 89 c7 mov %rax,%rdi
122d: e8 76 ff ff ff callq 11a8
1232: be 63 00 00 00 mov $0x63,%esi
1237: 48 8d 3d 3a 2f 00 00 lea 0x2f3a(%rip),%rdi # 4178
123e: e8 65 ff ff ff callq 11a8
上述汇编中使用 lea 指令先加载不同的对象的 this 指针到 rax 寄存器中,然后使用 mov 指令将 rax 寄存器的内容复制到 rdi 寄存器中,在成员函数中通过访问 rdi 寄存器就能获取到 this 指针的值,通过 this 指针 就能够完成对类对象本地成员的访问。
实际开发中也存在不需要 this 指针的情况,在这种情况下我们不必要通过一个类对象来调用一个成员函数,这也是静态成员函数的主要特性。
既然静态成员函数没有 this 指针,这样它就不能直接访问类对象中的非静态成员。实际上,静态成员函数会被放到类的声明之外。由于它没有 this 指针,因此与非成员函数差不多等同。
我对上面的代码进行修改,增加一个静态成员函数。修改后的代码如下:
#include
using namespace std;
class test
{
private:
char a;
int b;
public:
char get_a(void);
void set_a(char value);
static void static_member_function(void);
};
void test::static_member_function(void)
{
cout << "calling a static member function" << endl;
}
char test::get_a(void)
{
return 'a';
}
void test::set_a(char value)
{
if (value > 0) {
a = value;
}
}
int main(int argc, char *argv[])
{
test obj1;
test *obj2 = new(test);
test::static_member_function();
obj1.set_a('a');
obj2->set_a('b');
cout << "a in obj1 is " << obj1.get_a() << endl;
cout << "a in obj2 is " << obj2->get_a() << endl;
delete obj2;
return 0;
}
对编译生成的可执行函数进行反汇编,调用静态成员函数的汇编代码如下:
1214: e8 7d ff ff ff callq 1196
可以看到的是上面的汇编代码里并没有传递 this 指针的语句,这与静态成员函数的特点一致。也可以通过下面的方式调用静态成员函数:
((test*)0)->static_member_function();
这样的过程是合法的,上面的调用与普通非成员函数的调用类似,不会产生异常。我们也可以以类似的方法调用类的非静态成员函数,一个具体的示例如下:
((test*)0)->get_a();
这句语句能够编译通过,在运行的时候因为 get_a 函数中要访问类对象的本地数据成员,需要对 this 指针解引用,而这时 this 指针为 NULL,对一个 NULL 地址解引用会触发段错误。
上文中讲到成员函数是在编译过程中确定的,而虚函数却是在运行时动态确定的。虚函数对应的成员函数是根据对象的类型确定的,而非指针或引用指向对象的类型确定。
编译器会为具有虚函数的类生成一个虚函数表,同时在每一个类对象中添加指向虚函数表的指针,每一个虚函数有唯一的索引值。虚函数会被转化为对虚函数表中不同表项对应函数的调用形式,调用一个虚函数需要额外的指针解引用负载。
继续修改上面的代码,增加一个虚函数。修改后的代码如下:
#include
using namespace std;
class test
{
private:
char a;
int b;
public:
char get_a(void);
void set_a(char value);
int get_b(void);
virtual void increment_b(int value);
static void static_member_function(void);
};
int test::get_b(void)
{
return b;
}
void test::static_member_function(void)
{
cout << "calling a static member function" << endl;
}
void test::increment_b(int value)
{
this->b += value;
}
char test::get_a(void)
{
return a;
}
void test::set_a(char value)
{
if (value > 0) {
a = value;
}
}
int main(int argc, char *argv[])
{
test obj1;
test *obj2 = new(test);
obj1.set_a('a');
obj2->set_a('b');
obj1.increment_b(5);
obj2->increment_b(6);
cout << "a in obj1 is " << obj1.get_a() << endl;
cout << "a in obj2 is " << obj2->get_a() << endl;
cout << "b in obj1 is " << obj1.get_b() << endl;
cout << "b in obj2 is " << obj2->get_b() << endl;
delete obj2;
return 0;
}
生成调试信息,使用 gdb 调试时的部分内容如下:
(gdb) b main
Breakpoint 1 at 0x124d: file virtual_member_function.cpp, line 46.
(gdb) start
Temporary breakpoint 2 at 0x124d: file virtual_member_function.cpp, line 46.
Starting program: /home/longyu/The_Programming_Language/C++/a.out
Breakpoint 1, main (argc=1, argv=0x7fffffffdc28) at virtual_member_function.cpp:46
46 test obj1;
(gdb) n
47 test *obj2 = new(test);
(gdb) n
49 obj1.set_a('a');
(gdb) s
test::set_a (this=0x7fffffffdb10, value=97 'a') at virtual_member_function.cpp:39
39 if (value > 0) {
(gdb) print *this
$1 = {_vptr.test = 0x555555557da0 , a = 0 '\000', b = 0}
(gdb) n
40 a = value;
(gdb) n
42 }
(gdb) print *this
$2 = {_vptr.test = 0x555555557da0 , a = 97 'a', b = 0}
(gdb) n
main (argc=1, argv=0x7fffffffdc28) at virtual_member_function.cpp:50
50 obj2->set_a('b');
(gdb) s
test::set_a (this=0x55555556ae70, value=98 'b') at virtual_member_function.cpp:39
39 if (value > 0) {
(gdb) print *this
$3 = {_vptr.test = 0x555555557da0 , a = 0 '\000', b = 0}
(gdb) n
40 a = value;
(gdb)
42 }
(gdb) print *this
$4 = {_vptr.test = 0x555555557da0 , a = 98 'b', b = 0}
(gdb) n
main (argc=1, argv=0x7fffffffdc28) at virtual_member_function.cpp:52
52 obj1.increment_b(5);
(gdb) s
test::increment_b (this=0x7fffffffdb10, value=5) at virtual_member_function.cpp:29
29 this->b += value;
(gdb) print *this
$5 = {_vptr.test = 0x555555557da0 , a = 97 'a', b = 0}
(gdb) n
30 }
(gdb) print *this
$6 = {_vptr.test = 0x555555557da0 , a = 97 'a', b = 5}
(gdb) n
main (argc=1, argv=0x7fffffffdc28) at virtual_member_function.cpp:53
53 obj2->increment_b(6);
(gdb) s
test::increment_b (this=0x55555556ae70, value=6) at virtual_member_function.cpp:29
29 this->b += value;
(gdb) print *this
$7 = {_vptr.test = 0x555555557da0 , a = 98 'b', b = 0}
(gdb) n
30 }
(gdb) print *this
$8 = {_vptr.test = 0x555555557da0 , a = 98 'b', b = 6}
(gdb)
从上面的调试信息中我们可以发现,每一个对象的 this 指针都是唯一的。每一个类对象中增加一个指向虚函数表的指针,且相同类的所有实例化对象都共享同一个虚函数表。
纯虚函数是在虚函数的基础上扩展的语法。纯虚函数只能被非抽象派生类实现,超类不能实现纯虚函数。
当类中存在纯虚函数时,这个类一般被成为抽象类,它不能实例化自身,只能使用派生类中实现的纯虚函数。
转化的方向:
向上转型——从派生类转化到基类,向下转型——从基类转化为派生类。
static_cast< Type* >(ptr)
在编译时进行。当类型相关时才能成功转化,否则编译器会报错。
dynamic_cast< Type* >(ptr)
在执行时进行转化。如果类型没有关联会转化失败,返回 NULL。
参考链接:
virtual pure virtual explained
dynamic-cast-and-static-cast-in-c
参考书籍:
《深度探索 C++ 对象模型》