《深入浅出MFC》– C++的重要性质、虚函数表

1.对象的属性有两大成员,一是成员变量、二是成员函数。把数据声明为private,不允许外界随意存取,只能通过特定的接口来操作,这就是面向对象的封装特性。

 

2.隐藏的this指针。

image

编译器实际上为你做出来的代码是:

CRect::setcolor(2, (CRect*)&rect1);

CRect::setcolor(3, (CRect*)&rect2);

 

3.虚函数

①:如果你以一个“基类之指针”指向“派生类对象”,那么经由该指针你只能够调用基类所定义的函数。

image

②:如果你以一个“派生类之指针”指向一个“基类之对象”,你必须先做明显的转型操作(explicit cast)。这种做法很危险,不符合真实生活经验,正在程序设计上也会带给程序猿困惑。

image

③:如果基类和派生类都定义了“相同名称之成员函数”,那么通过对象指针调用成员函数时,到底调用哪个函数,必须视该指针原始类型而定,而不是视指针实际所指的对象的类型而定,这与②意义相通。

image

虚函数正式为了对“如果你以一个基类之指针指向一个派生类之对象,那么通过该指针你就只能够调用基类所定义之成员函数”这条规则反其道而行的设计。

在函数前面加上virtual保留字,即可使它成为虚函数。

 

纯虚函数virtual void display() = 0;

纯虚函数不需定义其实际操作,它的存在只是为了在派生类中被重新定义,只是为了提供一个多态接口。只要是拥有纯虚函数的类,就是一种抽象类,它是不能够被实例化的,也就是说,你不能根据它产生一个对象。如果一个类并没有改写它继续的类的纯虚函数,那么它本身也就成为一个拥有纯虚函数的类,于是它也是一个抽象类。

对虚函数的总结:

①:如果你期望派生类重新定义一个成员函数,那么你应该在基类中把此函数设为virtual。

②:以单一指令调用不同函数,这种性质称为Polymorphism,意思是“the ability to assume many forms”,也就是多态。

③:虚拟函数是C++语言的Polymorphism性质以及动态绑定的关键。

④:既然抽象类中的虚函数不打算被调用,我们就不应该定义它,应该把它设为纯虚函数(在函数声明之后加上“=0”即可)

⑤:我们可以说,拥有纯虚函数者为抽象类(abstract Class),以别于所谓的具体类(concrete class).

⑥:抽象类不能产生出对象实例,但是我们可以拥有指向抽象类的指针,以便于操作抽象类的各个派生类。

⑦:虚函数派生下去仍为虚函数,而且可以省略virtual 关键词。

 

4.类与对象

image

每个由此类派生出来的对象,都有这么一个vptr。当我们通过这个对象调用虚函数时,实际上是通过vptr找到虚函数表,再找出虚函数的真正地址。

image

当我们在派生类中改写虚函数时,虚函数表就受了影响:表中元素所指的函数地址将不再是基类的函数地址,而是派生类的函数地址。

我们来看一个实际程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <iostream.h>
#include <stdio.h>

class ClassA
{
public:
    int m_data1;
    int m_data2;
    void func1(){}
    void func2(){}
    virtual void vfunc1(){}
    virtual void vfunc2(){}
};

class ClassB : public ClassA
{
public:
    int m_data;
    void func2(){}
    virtual void vfunc1(){}
};

class ClassC : public ClassB
{
public:
    int m_data1;
    int m_data4;
    void func2(){}
    virtual void vfunc1(){}
};

由ClassA、ClassB、ClassC生成的对象内容如下:

image

Sizeof(ClassA) = 12      2个int加上一个vptr

Sizeof(ClassB) = 16      继承自ClassA,再加上1个int

Sizeof(ClassC) = 24      继承自ClassB,再加上2个int

 

5.向上强制类型转换(Object slicing)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
#include <iostream>
using namespace std;
class CObject
{
public:
    virtual void Serialize()
    {
        cout<<"Cobject::Serialize() \n \n";
    };
};

class CDocument:public CObject
{
public:
    int m_data1;
    void func()
    {
        cout<<"CDocument::func()"<<endl;
        Serialize();

    };
    virtual void Serialize()
    {
        cout<<"CDocument::Serialize() \n \n";
   
    }
};

class CMyDoc : public CDocument
{
public:
    int m_data2;
    virtual void Serialize()
    {
        cout<<"CMyDoc::Serialize() \n \n";
    }
};

void main()
{
    CMyDoc mydoc;
    CMyDoc* pmydoc = new CMyDoc;

    cout<<"#1 testing"<<endl;
    mydoc.func();

    cout<<"#2 testing"<<endl;
    ((CDocument *)(&mydoc))->func();

    cout<<"#3 testing"<<endl;
    pmydoc->func();

    cout<<"#4 testing"<<endl;
    ((CDocument)mydoc).func();
}

//输出
#1 testing
CDocument::func()
CMyDoc::Serialize()

#2 testing
CDocument::func()
CMyDoc::Serialize()

#3 testing
CDocument::func()
CMyDoc::Serialize()

#4 testing
CDocument::func()
CDocument::Serialize()

image

因为派生类mydoc不但继承其基类的成员,又有自己的成员。那么所谓的upcasting(向上强制转型):(CDocument)mydoc,将会造成对象的内容被切割(object slicing)。

#4 tesing ((CDocument)mydoc) 调用了CDocument的复制构造函数,构造了一个临时变量。这个临时变量是CDocument类型的,所以调用的是CDocument的虚函数。加入即可看出:   CDocument(CDocument& theDoc){cout<<“复制构造函数”<<endl;}; 

printf(“%x\n”,&mydoc);
printf(“%x\n”,&(CDocument)mydoc);
可以看出不是同一个值

 

6.静态成员

static成员变量不属于对象的一部分,而是类的一部分,所以程序可以在还没有诞生任何对象的时候就处理此种成员变量。

不要把static成员变量的初始化操作安排在类的构造函数中,因为构造函数可能一再被调用,而变量的初始却只应该设定一次。也不要把初始化操作安排在头文件中,因为它可能被载入许多地方,因此也可能被执行多次。应该放在应用程序文件中,类以外的任何位置设定初值。

由于static成员函数不需要借助任何对象,就可以被调用执行,所以编译器不会为它暗加一个this指针。也因为如此,static成员函数无法处理类中的non-static成员变量。

 

7.构造函数与析构函数

先来看一个例子:

image

可得出结论:

①:对于全局对象(如GlobalObject),程序一开始,其构造函数就先被执行(比程序进入点更早);程序即将结束前其析构函数被执行,MFC程序就有这样一个全局对象,通常以application object称呼之。(global)

②:对于局部对象,当对象诞生时,其构造函数被执行;当程序流程将离开该对象的存活范围(以至于对象将毁灭)时,其析构函数被执行。(in stack)

③:对于静态(static)对象,当对象诞生时其构造函数被执行;当程序将结束时(此对象因而将毁灭)其析构函数才被执行,但比全局对象的析构函数早一步执行。(local static)

④:对于以new方式产生出来的局部对象,当对象诞生时其构造函数被执行,析构函数则在对象被delete时执行。(in heap)

 

转载请标注来源:http://www.alonemonkey.com

By:AloneMonkey

本文链接:http://www.alonemonkey.com/mfc-two.html