从C开始的CPP学习生活04-多态与虚函数

多态是一种泛型编程思想,虚函数是实现这个思想的语法基础,即父类的指针调用子类的函数

1、虚函数

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
class CFather
{
public:
virtual void show()
{
std::cout << "class Father" << std::endl;
}
};

class CSon : public CFather
{
public:
int aa;
void show()
{
std::cout << "class CSon" << std::endl;
}
};


int main()
{
CFather* fa = new CSon;
fa->show();
return 0;
}

这段代码运行结果是打印class CSon

这就是虚函数的作用,可以让父类指针调用子类的函数,原因就是父类函数声明是virtual,而子类对这一函数实现了重写

  • 形式: virtual void fun()
  • 子类的函数要和父类函数名称相同
  • 多个子类,换子类就调用相应子类的函数 CFather* fa = new CSon;如果把CSon更换为另一个子类,另一个子类也重写了show函数,则会调用另一个子类的show函数
  • 多态指针对指针对象
  • 重写针对虚函数,覆盖针对普通函数

2、虚函数的特点

  • 子类重写的函数,默认是虚函数,可以显示的加virtual,也可以不加
  • 返回值和函数体的内容必须完全相同才能构成重载
  • 但是存在一种特殊情况,即子类的函数是父类函数的协变
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class CFather
{
public:
virtual CFather& show()
{
std::cout << "class Father" << std::endl;
return (*this);
}
};

class CSon : public CFather
{
public:
int aa;
CSon& show()
{
std::cout << "class CSon" << std::endl;
return (*this);
}
};
  • 虚函数不能是内联函数,但并不是编译出错,而是编译过后内联无效
    • 虚函数可以是内联函数,内联是可以修饰虚函数的,但是当虚函数表现多态性的时候不能内联
    • 内联是在编译期建议编译器内联,而虚函数的多态性在运行期,编译器无法知道运行期调用哪个代码,因此虚函数表现为多态性时不可以内联
    • 只有在编译器知道所调用的对象时哪个类的时候才能进行内联
  • 构造函数不能是虚函数,内联是构造函数唯一的合法存储类

3、虚表

多态与虚函数的实现原理

每个包含了虚函数的类都包含一个虚表

当一个类A继承另一个类B时,A同样会继承类B的虚表,但不是使用类B的虚表,而是复制了一份;

若A未重写B的虚函数,则A类的对象调用该函数时访问的是继承自B的虚表,所以调用的是B的函数,而当A重写了B的虚函数之后,A的虚表内容被替换成A所重写的函数地址,所以A类对象调用该函数的时候自然使用的是A类的函数;

3.1、虚表指针

虚表是一个指针数组,其元素是虚函数的函数指针,也就是存储着虚函数的地址;

普通的函数不是虚函数,调用的时候不需要经过虚表;

虚表在程序编译阶段就可以构造出来;

当然,虚表不是只能有一个,一个类如果继承了多个存在虚函数的类,则会存在多个虚表,每个虚表对应一个父类;

为了指定虚表的对象,编译器会给类添加一个*__vptr指针,用来指向虚表;

3.2、虚表存放位置

c/c++程序所占用内存一共分为五种

栈区、堆区、程序代码区、全局数据区、文字常量区

虚表存储在全局数据区

3.3、upcasting

1
CFather* fa = new CSon;

CSon类继承自CFather,上面这段代码地址空间是CSon类型的,我们使用父类指针指向这段地址,则父类指针会用父类指针的解释方式看待这段内存。但同时,父类指针获得的v__ptr是指向子类CSon的虚表的,所以我们在调用虚函数的时候,会调用子类的虚函数

这里面涉及upcasting的问题,即把子类对象当作父类来看待

3.4、找到虚表的地址

虚表指针在对象内存的首4个字节,虚表里面每4个字节存储一个函数指针,最后4字节存储0x0

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
class CFather
{
public:
virtual void fun() {
std::cout << "fun" << std::endl;
}
virtual void show()
{
std::cout << "class Father" << std::endl;
}
};

class CSon : public CFather
{
public:
int aa;
void show()
{
std::cout << "class CSon" << std::endl;
}
};


int main()
{
CFather* fa = new CSon;
typedef void(*p)();
((p)(*((int*)(*(int*)fa) + 0)))();
((p)(*((int*)(*(int*)fa) + 1)))();
std::cout << *((int*)(*(int*)fa) + 2) << std::endl;
delete fa;
return 0;
}

上述代码分别打印 fun class CSon 以及 0

4、虚析构

正常情况下,使用父类的指针指向子类的对象,在进行析构时调用的是父类的析构函数

如果父类的析构函数的虚析构,则子类的函数默认也是虚析构

在进行析构时会先调用子类的析构函数,然后调用父类的析构函数

delete指向哪种类型的指针,就调用哪种类型的析构函数。

5、纯虚函数

形式:

1
virtual void fun() = 0;

特点:

  • 没有函数实现
  • 无法进行对象声明,即有纯虚函数的类不能实例化对象
  • 相实现对象需要使用子类继承该类,并且子类要实现纯虚函数

抽象类:有纯虚函数的类

接口类:只有纯虚函数的类,可以有成员,可以有构造函数

1
2
3
4
5
6
7
8
9
class CFather
{
int a;
public:
virtual void fun() = 0;
virtual void gnu() = 0;
virtual ~CFather() = 0;
CFather();
};

6、虚继承

有这样一个问题 类B继承类A,类C继承类A,然后类D继承类A和类B,这会发生什么问题?

1
2
3
4
5
6
7
8
9
10
class A{};
class B : public A{};
class C : public A{};
class D : public B, public C{};
int main()
{
D d;
d.a;
return 0;
}

此时编译会出现问题:"D::a" is ambiguous ambiguous access of 'a'

注意,使用命名空间的访问方法时,只能使用d.B::a以及d.C::a,无法使用d.A::a

解决此类问题的方法是使用虚继承

1
2
3
4
5
6
7
8
9
10
11
class A{};
class B : virtual public A{};
class C : virtual public A{};
class D : public B, public C{};

int main()
{
D d;
d.a;
return 0;
}

解决了多继承中访问不明确的问题

不建议用,结构复杂,内存开销比较大

7、联编

将模块或者函数合并在一起生成可执行代码的处理过程

按照联编所进行的阶段不同,可分为两种不同的联编方法:静态联编和动态联编

对于非虚的方法,编译器使用静态联编

对于含有虚函数的类,由于需要到运行阶段才能知道具体要调用哪个函数,所以编译器要使用动态联编

8、单例模式

思想:一个类只能创造一个对象

  • 构造函数:private/protected
  • 通过静态成员函数申请对象空间,并返回地址
  • 定义一个静态标记,记录对象个数,并控制
  • 析构函数,将标记清空,以达到重复申请对象的目的
  • 该类可以被继承,但是继承后的类无法实例化对象(因为实例化对象需要调用父类的构造函数)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class CFather
{
private:
CFather()
{

}
public:
static int flag;
static CFather* CreateOJ()
{
if (flag == 0) {
flag = 1;
return (new CFather);
}
else {
return NULL;
}
}

~CFather() {
flag = 0;
}
};

从C开始的CPP学习生活04-多态与虚函数
http://example.com/2022/10/26/cppnote04/
作者
Anhongzhan
发布于
2022年10月26日
许可协议