成员函数
成员函数是由编译器解释的,编译器只需要保证类中的成员函数只能够被类对象使用,同时将对象的指针作为成员函数的第一个参数传递即可。成员函数在编译过程中会根据对象的类型确定下来。
成员函数在实际意义上仅仅是一个语法限制,它会被编译器转化为类似非成员函数类型,不存在额外的访问负载。
成员函数又可以分为【静态成员函数】与【非静态成员函数】。
对于非静态成员函数而言,this 指针指向每一个对象的本地数据,对对象内成员的存取通过 this 指针来完成。
一个具体的示例如下:
#include <iostream>
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 <test::get_a()>:
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 <test::set_a(char)>:
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 <test::set_a(char)+0x1d>
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 <main>:
......
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 <test::set_a(char)>
1215: 48 8d 45 e0 lea -0x20(%rbp),%rax
1219: 48 89 c7 mov %rax,%rdi
121c: e8 75 ff ff ff callq 1196 <test::get_a()>
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 <test::set_a(char)>
1232: be 63 00 00 00 mov $0x63,%esi
1237: 48 8d 3d 3a 2f 00 00 lea 0x2f3a(%rip),%rdi # 4178 <g_obj>
123e: e8 65 ff ff ff callq 11a8 <test::set_a(char)>
上述汇编中使用 lea 指令先加载不同的对象的 this 指针到 rax 寄存器中,然后使用 mov 指令将 rax 寄存器的内容复制到 rdi 寄存器中,在成员函数中通过访问 rdi 寄存器就能获取到 this 指针的值,通过 this 指针 就能够完成对类对象本地成员的访问。
实际开发中也存在不需要 this 指针的情况,在这种情况下我们不必要通过一个类对象来调用一个成员函数,这也是静态成员函数的主要特性。
既然静态成员函数没有 this 指针,这样它就不能直接访问类对象中的非静态成员。实际上,静态成员函数会被放到类的声明之外。由于它没有 this 指针,因此与非成员函数差不多等同。
我对上面的代码进行修改,增加一个静态成员函数。修改后的代码如下:
#include <iostream>
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 <test::static_member_function()>
可以看到的是上面的汇编代码里并没有传递 this 指针的语句,这与静态成员函数的特点一致。也可以通过下面的方式调用静态成员函数:
((test*)0)->static_member_function();
这样的过程是合法的,上面的调用与普通非成员函数的调用类似,不会产生异常。我们也可以以类似的方法调用类的非静态成员函数,一个具体的示例如下:
((test*)0)->get_a();
这句语句能够编译通过,在运行的时候因为 get_a 函数中要访问类对象的本地数据成员,需要对 this 指针解引用,而这时 this 指针为 NULL,对一个 NULL 地址解引用会触发段错误。
虚函数
上文中讲到成员函数是在编译过程中确定的,而虚函数却是在运行时动态确定的。虚函数对应的成员函数是根据对象的类型确定的,而非指针或引用指向对象的类型确定。
编译器会为具有虚函数的类生成一个虚函数表,同时在每一个类对象中添加指向虚函数表的指针,每一个虚函数有唯一的索引值。虚函数会被转化为对虚函数表中不同表项对应函数的调用形式,调用一个虚函数需要额外的指针解引用负载。
继续修改上面的代码,增加一个虚函数。修改后的代码如下:
#include <iostream>
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 <vtable for test+16>, a = 0 '\000', b = 0}
(gdb) n
40 a = value;
(gdb) n
42 }
(gdb) print *this
$2 = {_vptr.test = 0x555555557da0 <vtable for test+16>, 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 <vtable for test+16>, a = 0 '\000', b = 0}
(gdb) n
40 a = value;
(gdb)
42 }
(gdb) print *this
$4 = {_vptr.test = 0x555555557da0 <vtable for test+16>, 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 <vtable for test+16>, a = 97 'a', b = 0}
(gdb) n
30 }
(gdb) print *this
$6 = {_vptr.test = 0x555555557da0 <vtable for test+16>, 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 <vtable for test+16>, a = 98 'b', b = 0}
(gdb) n
30 }
(gdb) print *this
$8 = {_vptr.test = 0x555555557da0 <vtable for test+16>, 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++ 对象模型》