C++ 知识

C++ 虚函数

C++ 虚函数是定义在基类中的函数,子类必须对其进行覆盖。

虚函数的作用

  • 定义子类对象,并调用对象中未被子类重写的基类函数 A。同时在该函数A 中,又调用了已被子类重写的基类函数B。那此时将会调用基类中的函数B,可我们本应该调用的是子类中的函数B。虚函数即能解决这个问题。
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

#include<iostream>

using namespace std;

// 基类 Father
class Father {
public:
void display() {
cout<<"Father::display()\n";
}
// 在函数中调用了,子类重写基类的函数 display()
void fatherShowDisplay() {
display();
}};

// 子类 Son
class Son:public Father {
public:
// 重写基类中的 display() 函数
void display() {
cout<<"Son::display()\n";
}};

int main() {
Son son; // 子类对象
son.fatherShowDisplay(); // 通过基类中未被重写的函数,想调用子类中重写的 display 函数
}

Father::display()

用虚函数:

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

#include<iostream>

using namespace std;

// 基类 Father
class Father {
public:
virtual void display() {
cout<<"Father::display()\n";
}

// 在函数中调用了,子类重写基类的函数 display()
void fatherShowDisplay() {
display();
}};

// 子类 Son
class Son:public Father {
public:
// 重写基类中的 display() 函数
void display() {
cout<<"Son::display()\n";
}};

int main() {
Son son; // 子类对象
son.fatherShowDisplay(); // 通过基类中未被覆盖的函数,想调用子类中重写的 display 函数
}

Son::display()

  • 在使用 指向子类对象的基类指针,并调用子类中的重写函数 时,如果该函数不是虚函数,那么将调用基类中的该函数;如果该函数是虚函数,则会调用子类中的该函数。
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

#include<iostream>

using namespace std;

// 基类 Father
class Father {
public:
void display() {
cout<<"Father::display()\n";
}};

// 子类 Son
class Son:public Father {
public:
// 重写基类中的 display 函数
void display() {
cout<<"Son::display()\n";
}};

int main() {
Father *fp; // 定义基类指针
Son son; // 子类对象
fp = &son; // 使基类指针指向子类对象
fp->display(); // 通过基类指针想调用子类中重写的 display 函数
}

Father::display()

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

#include<iostream>

using namespace std;

// 基类 Father
class Father {
public:
// 定义了虚函数
void virtual display() {
cout<<"Father::display()\n";
}};

// 子类 Son
class Son:public Father {
public:
// 覆盖基类中的 display 函数
void display() {
cout<<"Son::display()\n";
}};

int main() {
Father *fp; // 定义基类指针
Son son; // 子类对象
fp = &son; // 使基类指针指向子类对象
fp->display(); // 通过基类指针想调用子类中覆盖的 display 函数
}

Son::display()

虚函数的原理

虚函数的本质是一个简单的 虚函数表 。当一个类存在虚函数时,通过该类创建的对象实例,会在内存空间的前 4 个字节保存一个指向虚函数表的指针__vfptr
__vfptr 指向的虚函数表是类独有的,而且该类的所有对象共享。虚函数表的实质是一个虚函数地址的数组,它包含了类中每个虚函数的地址,既有当前类定义的虚函数,也有重写父类的虚函数,也有继承而来的虚函数。
当子类重写了父类的虚函数时,子类虚函数表将包含子类虚函数的地址,而不会有父类虚函数的地址。同时,当用基类指针指向子类对象时,基类指针指向的内存空间中的 __vfptr 依旧指向了子类的虚函数表。所以,基类指针依旧会调用子类的虚函数。

定义一个有虚函数的类

1
2
3
4
5
6
7
8
class Base1 {
public:
int base1_1;
int base1_2;

virtual void base1_func1() {}
virtual void base1_func2() {}
};

定义两个对象:

1
2
Base1 b1;
Base1 b2;

两个对象的内存空间分配如下:

定义一个子类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

class Base1 {public:
int base1_1;
int base1_2;

virtual void base1_fun1() {}
virtual void base1_fun2() {}

};
class Derive1 : public Base1 {public:
int derive1_1;
int derive1_2;

// 覆盖基类函数
virtual void base1_fun1() {}
};

定义一个子类对象:

1
Derive1 d1;

其内存空间如下:

c++ 中的纯虚函数

纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加=0

1
2
3
4
5
class A
{
public:
virtual void foo() = 0;
};

在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。而针对每种动物的方法又有所不同,此时需要使用多态特性,也就需要在基类中定义虚函数。

纯虚函数是在基类中声明的虚函数,它要求任何派生类都要定义自己的实现方法,以实现多态性。实现了纯虚函数的子类,该纯虚函数在子类中就变成了虚函数。

定义纯虚函数是为了实现一个接口,用来规范派生类的行为,也即规范继承这个类的程序员必须实现这个函数。派生类仅仅只是继承函数的接口。纯虚函数的意义在于,让所有的类对象(主要是派生类对象)都可以执行纯虚函数的动作,但基类无法为纯虚函数提供一个合理的缺省实现。所以类纯虚函数的声明就是在告诉子类的设计者,“你必须提供一个纯虚函数的实现,但我不知道你会怎样实现它”。

含有纯虚函数的类称之为抽象类,它不能生成对象(创建实例),只能创建它的派生类的实例。抽象类是一种特殊的类,它是为了抽象和设计的目的为建立的,它处于继承层次结构的较上层。抽象类的主要作用是将有关的操作作为结果接口组织在一个继承层次结构中,由它来为派生类提供一个公共的根,派生类将具体实现在其基类中作为接口的操作。

抽象类只能作为基类来使用,其纯虚函数的实现由派生类给出。如果派生类中没有重新定义纯虚函数,而只是继承基类的纯虚函数,则这个派生类仍然还是一个抽象类。如果派生类中给出了基类纯虚函数的实现,则该派生类就不再是抽象类了,它是一个可以建立对象的具体的类。

虚析构函数

主要功能就是确保继承体系中的对象正确释放。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

class Base
{
public:
virtual ~Base(){}
};
class Derived :public Base
{
public:
Derived() {
pointer = new int[10];
}
~Derived() {
delete []pointer;
}
int *pointer;};

void main(){
Derived *d = new Derived();
Base *b = d;
delete b;
}

如果没有虚析构函数,则只会调用基类的析构函数,那么派生类中分配的内存就没办法释放,造成泄露。所以,如果发生继承,一定要把基类析构函数定义为虚函数。

C++ 中函数重载、重写和重定义

  • 重载(overload):函数名相同,参数列表不同,重载只存在类的内部

  • 重写(override):子类重新定义父类中有相同名称和参数的虚函数,存在于继承关系之间

    • 被重写的函数不能是 static 的,必须是 virtual
    • 重写函数必须有相同的返回值类型、名称和参数列表
    • 重写函数的访问修饰符可以不同
  • 重定义(redefining):子类重新定义父类有相同名称的非虚函数

编译器如何解决命名冲突

编译后,重载的函数名各不相同。变名的机制:作用域 + 返回类型 + 函数名 + 参数列表,来解决命名冲突的问题

智能指针 shared_ptrweak_ptrunique_ptr

shared_ptr

只有指向动态分配的对象的指针才能交给 shared_ptr 对象托管。

不能用下面的方式使得两个 shared_ptr 对象托管同一个指针:

1
2
A* p = new A(10);
shared_ptr<A> sp1(p), sp2(p);

sp1sp2 并不会共享同一个 p 的托管技术,而是各自将对 p 的的托管计数都记为 1。这样,当 sp1 消亡时要析构 psp2 消亡时要再次析构p,这会导致程序崩溃。

weak_ptr

weak_ptr是一种用于解决 shared_ptr 相互引用时产生死锁问题的智能指针。weak_ptr时对对象的一种弱引用,它不会增加对象的引用计数,weak_ptrshared_ptr 可以相互转化,shared_ptr可以直接赋值给 weak_ptrweak_ptr 也可以通过 lock 函数来获得shared_ptr

  • weak_ptr指针通常不单独使用,只能和 shared_ptr 类型指针搭配使用。将一个 weak_ptr 绑定到一个 shared_ptr 不会改变 shared_ptr 的引用计数。一旦最后一个指向对象的 shared_ptr 被销毁,对象就会被释放。即使有 weak_ptr 指向对象,对象也还是会被释放。
  • weak_ptr并没有重载 operator ->operator *操作符,因此不可直接通过 weak_ptr 使用对象,典型的用法时调用其 lock 函数来获得 shared_ptr 实例,进而访问原始对象。

unique_ptr

unique_ptr是一个独享所有权的智能指针,unique_ptr对象包装一个原始指针,并负责其生命周期。当该对象被销毁时,会在其析构函数中删除关联的原始指针。unique_ptr重载了 ->*运算符,因此可以像其他普通指针一样使用。

引用计数是在对上还是栈上

多个 shared_ptrweak_ptr可以指向同一个被引用计数的对象。而其中一个或几个离开作用域被析构,强引用计数未归 0,指向的对象不能释放。因此不能放到栈上。


C++ 知识
https://silhouettesforyou.github.io/2021/11/08/ea34a7c058d8/
Author
Xiaoming
Posted on
November 8, 2021
Licensed under