问题 : 通过基类指针,释放派生类的堆空间时,并不会调用派生类的析构函数,导致派生类中构造分配的资源未被释放。
当没有成功调用子类析构函数时,通常是由于基类的析构函数不是虚析构函数。这会导致在通过基类指针删除派生类对象时,只会调用基类的析构函数,而不会调用派生类的析构函数。下面是一个示例代码,展示了没有成功调用子类析构函数的情况:
- #include
- using namespace std;
-
- class Base {
- public:
- Base() {
- cout << "Base Constructor" << endl;
- }
-
- ~Base() {
- cout << "Base Destructor" << endl;
- }
- };
-
- class Derived : public Base {
- public:
- Derived() {
- cout << "Derived Constructor" << endl;
- }
-
- ~Derived() {
- cout << "Derived Destructor" << endl;
- }
- };
-
- int main() {
- Base* ptr = new Derived();
- delete ptr;
-
- return 0;
- }
在上述示例中,Base
是基类,Derived
是派生类。然而,基类的析构函数 ~Base()
没有被声明为虚析构函数。
在 main()
函数中,我们使用基类指针 ptr
指向派生类对象,并使用 new
运算符创建对象。接下来,我们使用 delete
运算符删除对象。
由于基类 Base
的析构函数不是虚析构函数,所以只会调用基类的析构函数,而没有调用派生类 Derived
的析构函数。
输出结果为:
- Base Constructor
- Derived Constructor
- Base Destructor
可以看到,只有基类 Base
的构造函数和析构函数被调用,派生类 Derived
的析构函数没有被调用。这表示在删除对象时没有成功调用子类的析构函数,可能导致内存泄漏和资源泄漏问题。为了正确调用子类析构函数,需要将基类的析构函数声明为虚析构函数,并确保在删除对象时使用正确的方式。
虚析构函数是一个在 C++ 中使用的特殊类型的析构函数,用于实现多态性。通过将基类的析构函数声明为虚析构函数,可以确保在删除派生类对象时,会正确调用派生类的析构函数。
虚析构函数的主要作用是允许通过基类指针或引用来删除派生类的对象,并在运行时正确调用派生类的析构函数。这对于涉及到继承关系的代码非常重要,因为如果基类的析构函数不是虚析构函数,则可能导致内存泄漏和未定义的行为。
virtual
关键字来标识虚析构函数。delete
运算符而不是 delete[]
(删除数组对象时使用)。虚析构函数常常用于以下情况:
下面是一个示例来说明虚析构函数的应用:
- #include
- using namespace std;
-
- class Base {
- public:
- Base() {
- cout << "Base Constructor" << endl;
- }
-
- virtual ~Base() {
- cout << "Base Destructor" << endl;
- }
- };
-
- class Derived : public Base {
- public:
- Derived() {
- cout << "Derived Constructor" << endl;
- }
-
- ~Derived() override {
- cout << "Derived Destructor" << endl;
- }
- };
-
- int main() {
- Base* ptr = new Derived();
- /*
- 先使用new操作符在堆上分配了一个存储xDerived对象的内存块,构造
- Derived对象,调用Derived类的构造函数来初始化刚分配的内存块
- */
- delete ptr;// 释放堆空间 通过基类指针删除对象,自动调用派生类的析构函数
-
- return 0;
- }
在上述示例中,Base
是一个基类,Derived
是一个派生类,并且 ~Base()
和 ~Derived()
都被声明为虚析构函数。
在 main()
函数中,我们通过基类指针指向派生类对象,并使用 new
运算符进行创建。然后,通过 delete
运算符删除对象。由于基类的析构函数是虚析构函数,它会自动调用派生类的析构函数。
输出结果为:
- Base Constructor
- Derived Constructor
- Derived Destructor
- Base Destructor
可以看到,在删除对象时,首先调用派生类的析构函数,然后调用基类的析构函数。这是通过使用虚析构函数来确保正确的析构顺序和资源的释放。
普通的虚函数,其定义形式如下:
- class A
- {
- virtual void someFunc();
- };
而所谓的纯虚函数,其定义形式如下:
- class A
- {
- virtual void someFunc() = 0; // 注意此处,有个 "=0"
- };
形如上述代码所示,在声明尾处加了=0
的虚函数被称为纯虚函数。包含了纯虚函数的类被称为抽象基类(Abstract Base Class,简称ABC)
,关于纯虚函数和抽象基类的语法规则如下:
比如上述例子,是无法定义对象的:
- // pureVirtualMethod.cpp
- int main()
- {
- A a;
- }
编译报错:
- gec@ubuntu:$ g++ pureVirtualMethod.cpp
- pureVirtualMethod.cpp: In function ‘int main(int, const char**)’:
- pureVirtualMethod.cpp:23:7: error: cannot declare variable ‘a’ to be of abstract type ‘A’
- 23 | A a;
- | ^
- gec@ubuntu:$
只有当其派生出子类,并且子类复写了父类所有的纯虚函数后,才能基于子类定义对象。比如:
- class B : public A
- {
- void someFunc(){} // 复写父类的纯虚函数(此处为空函数)
- };
-
- int main()
- {
- B b; // 正确
- }
抽象类的作用:预留接口提供派生类去覆盖,方便实现动态多态。
1.提高代码重用性:实现动态多态,就是提高复用性。 2.类型安全性:抽象无法创建对象,这样对于一些不复合常理的类,就不能创建。 3.代码可读性和可维护性:抽象类提供了一个基类,子类可以继承这个基类,从而简化代码的编写过程。
纯虚函数实际上是父类用来规范子类的一种手段,当父类出现纯虚函数时,就意味着给后代子类规范了一个必须实现的接口(类方法),这在现实中是非常常见的情形,比如:
由于所有的飞行器都必然会飞行,且各种不同的飞行器的飞行方式各不相同,那么飞行器作为基类就应该定义一个类似 virtual void fly() = 0;
这样的纯虚函数,强迫所有的客机、战斗机、热气球、直升机等等必须提供一个叫 fly()
的类方法,否则就无法构建对象。至于该接口如何实现?那就是各个飞行器类根据自己的实际情况要去做的事情了。
由于所有的按钮都必然需要被绘制才能显现,且各种不同按钮的绘制行为各不相同,那么按钮类(QAbstrat Button)作为基类就应该定义一个类似 virturl void paintEvent() = 0; 这样的纯虚函数,强迫所有的单选按钮、复选按钮、写实按钮等等必须提供一个叫 paintEnven() 的类方法,否则就无法显现按钮图形。至于该接口如何实现?那就是各种按钮根据自己的实际情况要去做的事情了。
显而易见,如果把普通的虚函数理解为基类给派生类建议性的接口实现建议的话,那么纯虚函数就是基类给派生类强迫性的接口设计规范。
略显吊诡的是,C++居然允许纯虚函数可以有一个基类的版本,基类的纯虚函数的代码实现既不会改变基类无法定义对象的事实,也无法成为子类默认可用的版本。因此抽象基类的纯虚函数的实现版本在实际应用中并不多见,可以理解为 C++ 语言为无法预料的现实世界的一种妥协,给不可预知的场景留下后路的做法。
构造函数不能是虚函数,逻辑上说不通,语法上也不允许。
先说结论:
- class Base
- {
- public:
- // 虚析构函数: 保证在针对父类执行delete操作时,能正确释放子类的资源
- virtual ~Base(){}
- };
-
- class Derived : public Base
- {
- int *p;
-
- public:
- Derived(){p = new int[100];}
- ~Derived(){delete [] p;}
- };
-
- int main()
- {
- Base *b = new Derived;
- Deived p1;
- // 释放父类指针或引用
- // 由于 b 的父类拥有虚析构函数,因此能正常调用 b 的析构函数
- delete b;
-
- return 0;
- }
虚·析构函数 与 虚·普通函数本质上都是一样的,唯一的区别是:由于每个类的析构函数有且仅有一个,因此虚析构函数无需同名,也无法同名。
一、虚函数能重载吗?如何确保派生类正确复写了基类的虚函数?
能。
但要注意,虚函数的重载要看类域的情况。
- class Base
- {
- // 以下两个虚函数,构成重载
- virtual void f(int);
- virtual void f(float);
-
- void f1();
- virtual void f2(int);
- };
-
- class Derived : public Base
- {
- // 以下两个虚函数,构成重载
- virtual void f(string);
- virtual void f(double);
-
- void f1(); // 此同名函数不会复写基类的版本
- virtual void f2(int); // 此同名函数会复写基类的版本
-
- // override确保: 基类中必定存在一个完全一致的虚函数,否则报错。
- virtual void f(int) override;
- };
-
- int main()
- {
- Base *p = new Derived;
- delete p; // 调用~Base() ? 还是调用 ~Derived()
-
- p->f1(); // 调用的是基类的版本
- p->f2(); // 调用的居然是派生类的版本
- }
二、简述构造函数、析构函数和虚函数的关系。
三、为什么多态基类需要声明 virtual 析构函数?
基类的指针或引用才能正确调用派生类的析构函数,从而释放派生类的资源。
四、为什么不能在构造和析构的过程中调用 virtual 函数?
当我们想从类外访问类内部数据的时候,除非我们是类成员,或即将要访问的数据是公有数据,否则是无法访问的。这也恰恰是类成员权限限定符public 、protected、private
的设计初衷,它们可以更好地保护类内数据,更好地体现封装性。
但是,在某些场合,我们却需要在类外对类内部的非公有数据进行访问,比如一台遥控器对象和一台电视对象,遥控器绕过电视机的公有控制面板,直接改变电视机的音量、频道、启停等状态,很明显遥控器对象必然要访问电视机对象的内部非公有数据,但它们属于不同的两个类,并且也没有继承关系。
像上述例子,就需要让遥控器类或遥控器中的某些类方法成为电视机类的友元,友元就是朋友,既然遥控器是电视机的朋友,那么遥控器访问电视机的内部数据的时候,电视机就会视遥控器为家人,可以让其任意访问所有权限的内部数据。
假设有如下类 A,我们希望普通函数 show() 可以访问其内部私有数据,那么可以在类中将该函数声明为类的友元,语法如下:
- class A
- {
- int x;
-
- // 声明一个友元函数
- friend void show(const A &r);
- };
语法要点:
而友元函数的定义,则与普通的函数定义无异:
- void show(const A &r)
- {
- cout << r.x << endl;
- }
语法要点:
friend
。特点:
作用:
应用:
"+"
和"<<"
等运算符,使得类的对象可以通过运算符进行直接操作。友元函数 访问权限 可以继承吗?
不可以!爸爸的朋友并不是儿子的朋友。
一个类可以有多个友元函数吗?
肯定可以,每个人都有很多不同的朋友。
一个友元函数可以声明在多个类中吗 ?
可以的,声明了一个类就对该类的所有成员具有访问权限。
当一个类是另外一个类的友元类时,当前类的所有成员函数就可以访问对方的一切数据成员和成员函数。
friend class 类名;
- // 电视机类
- class TV
- {
- int channel;
-
- // 在电视机类中,将遥控器类声明为友元类
- friend class Remoter;
- };
- // 遥控器类
- class Remoter
- {
- public:
- void setChannel(TV &r, int c)
- {
- // 遥控器类中可以直接访问电视机类任意内部数据
- r.channel = c;
- }
- };
- 这样,就可以使用遥控器对象去控制电视机了,例如:
-
- int main(void)
- {
- TV xiaomi;
- Remoter r;
-
- // 使用遥控器将电视机调整到5号频道
- r.setChannel(xiaomi, 5);
- }
练习: 利用友元类的技术点,让两个类互为友元,并访问对方的私有成员。 遇到语法错误,思考如何解决?
- 友元类的练习.cpp: In member function ‘void ClassA::displayPrivateDataB(ClassB&)’:
- 友元类的练习.cpp:17:69: error: invalid use of incomplete type ‘class ClassB’
- 17 | cout << "ClassA: Accessing private data B from ClassB: " << b.privateDataB << endl;
- | ^
- 友元类的练习.cpp:4:7: note: forward declaration of ‘class ClassB’
- 4 | class ClassB; // 前置声明ClassB
- | ^~~~~~
解决方法: 在所有类定义完毕后,再去类外定义这些访问友元类的成员函数
-
- #include
- using namespace std;
-
- class ClassB; // 前置声明ClassB
-
- // 定义ClassA类
- class ClassA
- {
- private:
- int privateDataA;
-
- public:
- ClassA() : privateDataA(20) {}
-
- void displayPrivateDataB(ClassB &b);
- // {
- // cout << "ClassA: Accessing private data B from ClassB: " << b.privateDataB << endl;
- // }
- // 声明ClassB为友元类,可以访问其私有成员
- friend class ClassB;
- };
-
- // 定义ClassB类
- class ClassB
- {
- private:
- int privateDataB;
-
- public:
- ClassB() : privateDataB(12) {}
-
- void displayPrivateDataA(ClassA &a);
- // {
- // cout << "ClassB: Accessing private data A from ClassA: " << a.privateDataA << endl;
- // }
- // 声明ClassA为友元类,可以访问其私有成员
- friend class ClassA;
- };
-
- void ClassA::displayPrivateDataB(ClassB &b)
- {
- cout << "ClassA: Accessing private data B from ClassB: " << b.privateDataB << endl;
- }
-
- void ClassB::displayPrivateDataA(ClassA &a)
- {
- cout << "ClassB: Accessing private data A from ClassA: " << a.privateDataA << endl;
- }
-
- int main()
- {
- ClassA objA;
- ClassB objB;
-
- objB.displayPrivateDataA(objA); // ClassB访问ClassA的私有成员值
- objA.displayPrivateDataB(objB); // ClassB访问ClassA的私有成员值
-
- return 0;
- }
友元成员函数是指一个类的成员函数可以被声明为另一个类的友元函数。当一个类将另一个类的成员函数声明为友元函数时,该友元函数可以访问该类的所有成员,包括私有成员、保护成员和公有成员。友元成员函数的声明在类中进行,但定义和实现通常在类外部进行。
- #include
- using namespace std;
- // 前向声明 value类
- class value;
-
- class base
- {
- public:
- void set_value(value &v);
-
- void show_value(value &v);
- };
-
- class value
- {
- private:
- int v;
- // friend class base; // 声明base是当前类友元,base类的有函数都可以访问 value的私有成员
- friend void base::set_value(value &v); // 只声明了 set_value
- };
-
- // 所有类定义完毕后编写函数 定义函数
- void base::set_value(value &v)
- {
- v.v = 10;
- }
-
- void base::show_value(value &v)
- {
- cout << v.v << endl; // 未声明为友元,不能访问私有
- }
-
- int main()
- {
- value v;
-
- base b;
- b.set_value(v);
- b.show_value(v);
- }