c++ 类中的成员函数、虚函数、纯虚函数

成员函数

成员函数是由编译器解释的,编译器只需要保证类中的成员函数只能够被类对象使用,同时将对象的指针作为成员函数的第一个参数传递即可。成员函数在编译过程中会根据对象的类型确定下来。

成员函数在实际意义上仅仅是一个语法限制,它会被编译器转化为类似非成员函数类型,不存在额外的访问负载。

成员函数又可以分为【静态成员函数】与【非静态成员函数】。

对于非静态成员函数而言,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++ 对象模型》

你可能感兴趣的:(c++,C++,类中的成员函数,C++,中的虚函数与虚函数表,类的转化)