目录
前言
一、重载
1.函数重载
2.运算符重载
二、构造函数
1.什么是构造函数
2.带参数的构造函数
3.使用初始化列表
4.this关键字
5.new关键字
三、析构函数
1.什么是析构函数
四、静态成员变量
1.静态成员的定义
2.静态成员变量的作用
五、继承
1.继承基本概念
2.权限对继承的影响
3.基类构造函数
4.虚函数
5.多重继承
6.虚继承
六、多态
1.多态的基本概念(polymorphic)
2.如何实现多态
3.抽象类
抽象类的基本概念
抽象类的特点
前言
本博客为C++学习第二篇,第一篇见:C++基础入门(一)。
本教程是针对QT学习打下基础,可能有些知识点讲的不是很深刻,但是对于QT的学习够用了,等见到未知的知识后会在QT相关文章进行补充,将要学习QT的同学可以关注我,后续会更新QT项目,从项目上手学习QT。
一、重载
1.函数重载
在同一个作用域内,可以声明几个功能类似的同名函数,这些同名函数的形式参数(指参数的个数、类型或者顺序)必须不同。您不能仅通过返回类型的不同来重载函数。下面的实例中,同名函数 print() 被用于输出不同的数据类型:
#include <iostream>
using namespace std;class printData
{
public:void print(int i) {cout << "整数为: " << i << endl;}void print(double f) {cout << "浮点数为: " << f << endl;}void print(string str) {cout << "字符串为: " << str << endl;}
};int main(void)
{printData pd;// 输出整数pd.print(5);// 输出浮点数pd.print(500.263);// 输出字符串string str = "Hello C++";pd.print(str);return 0;
}
这里要注意,不能只通过返回值来重载函数,必须通过参数的不同来重载。函数重载在QT中也是非常常见的。
2.运算符重载
(用的不多)在C++中,运算符重载是一个允许程序员自定义各种运算符,如(+,-,==,!=等)在自定义类型(类或结构体)上的行为的特性。这意味着你可以定义类似于内置类型的运算符行为,使你的自定义类型更加直观和易于使用。
基本原则
① 不可以创建新的运算符:只能重载已经存在的运算符。
② 至少有一个操作数是用户定义的类型:不能重载两个基本类型的运算符。
③ 不能更改运算符的优先级:重载的运算符保持其原有的优先级和结合性。
示例:假设我们有一个 Person 类,我们可以重载 == 运算符来实现两个 Person 是否相等的判断。
#include <iostream>
using namespace std;class Person {
public:string name;int inNumberTail;// 重载 == 运算符bool operator==(Person pTmp);
};// 可以把 operator== 理解成一个函数名,之前在类里面是一个函数的声明,这里实现函数的功能
bool Person::operator==(Person pTmp) {// 这里的 name 是调用对象 p1 的成员变量return pTmp.name == name && pTmp.inNumberTail == inNumberTail;
}int main() {// 假设我们认定名字和身份证尾号6位一样的两个对象是同一个人!Person p1;p1.name = "张三";p1.inNumberTail = 412508;Person p2;p2.name = "张三";p2.inNumberTail = 412508;bool ret = p1 == p2; // p1 是调用对象,p2 是传递给 operator== 的参数cout << ret << endl; // 输出 1(true)return 0;
}
解释:
● 调用 p1 == p2:
p1 是调用对象, this 指针(后面讲到构造函数时会讲)指向 p1。
p2 是传递给 operator== 函数的参数 pTmp。
在 operator== 函数内部,name 和 inNumberTail 指的是 p1 的成员变量。
pTmp.name 和 pTmp.inNumberTail 指的是 p2 的成员变量。
主要让人疑惑的点就在重载函数的具体实现部分,在我们不是到传入参数是p1还是p2时,并不会影响我们对代码的阅读,但是怎么和另一个对象比较?我们之前讲成员函数,知道了成员函数可以直接访问类里面的数据,这里重载运算符也相当于是成员函数,后面我们知道了p1是调用对象,也就是说重载函数是调用的p1的成员函数,所以p1可以直接访问name,入参就是p2。
示例2:假设我们有一个简单的 Point 类,我们可以重载 + 运算符来实现两个点的加法。
#include <iostream>using namespace std;class Point {
public:int x, y;// 重载 + 运算符Point operator+(Point other) const {Point ret;ret.x = x + other.x;ret.y = y + other.y;return ret;}
};int main() {Point p1;p1.x = 1;p1.y = 2;Point p2;p2.x = 2;p2.y = 3;Point p3 = p1 + p2; // 使用重载的 + 运算符std::cout << "p3.x: " << p3.x << ", p3.y: " << p3.y << std::endl; // 输出 p3.x: 3, p3.y: 5return 0;
}
在这个例子中, operator+ 被重载为一个成员函数,接受一个 Point 类型的常量引用作为参数,并返回两个点相加的结果。
二、构造函数
1.什么是构造函数
类的构造函数是类的一种特殊的成员函数,它会在每次创建类的新对象时执行。
构造,那构造的是什么呢?
构造成员变量的初始化值,内存空间等。构造函数的名称与类的名称是完全相同的,并且不会返回任何类型,也不会返回 void。构造函数可用于为某些成员变量设置初始值。
下面的实例有助于更好地理解构造函数的概念:
#include <iostream>
#include <string>
using namespace std; // 使用std命名空间class Car {
public:string brand; // 不需要使用std::stringint year;// 无参构造函数Car() {brand = "未知";year = 0;cout << "无参构造函数被调用" << endl; // 不需要使用std::cout和std::endl}void display() {cout << "Brand: " << brand << ", Year: " << year << endl;}
};int main() {Car myCar; // 创建Car对象myCar.display(); // 显示车辆信息return 0;
}
可以看到先打印了无参构造函数里的信息,再打印了品牌等信息。由此可见,实例化一个类的对象后,构造函数最先被调用,如果不写构造函数,也会有个默认的构造函数,只不过什么事也不做。
2.带参数的构造函数
默认的构造函数没有任何参数,但如果需要,构造函数也可以带有参数。这样在创建对象时就会给对象赋初始值,如下面的例子所示:
#include <iostream>
#include <string>
using namespace std;class Car {
public:string brand;int year;// 带参数的构造函数,使用常规的赋值方式Car(string b, int y) {brand = b;year = y;}void display() {cout << "Brand: " << brand << ", Year: " << year << endl;}
};int main() {Car myCar("Toyota", 2020); // 使用带参数的构造函数创建Car对象myCar.display(); // 显示车辆信息return 0;
}
3.使用初始化列表
在C++中,使用初始化列表来初始化类的字段是一种高效的初始化方式,尤其在构造函数中。初始化列表直接在对象的构造过程中初始化成员变量,而不是先创建成员变量后再赋值。这对于提高性能尤其重要,特别是在涉及到复杂对象或引用和常量成员的情况下。
初始化列表紧跟在构造函数参数列表后面,以冒号(:)开始,后跟一个或多个初始化表达式,每个表达式通常用逗号分隔。下面是使用初始化列表初始化字段的例子:
class MyClass {
private:int a;double b;std::string c;public:// 使用初始化列表来初始化字段MyClass(int x, double y, const std::string& z) : a(x), b(y), c(z) {// 构造函数体}
};
在这个例子中, MyClass 有三个成员变量:a( int 类型)、b( double 类型)和 c( s td::string 类型)。当创建MyClass 的一个实例时,我们通过构造函数传递三个参数,这些参数被用于通过初始化列表直接初始化成员变量。初始化列表 : a(x), b(y), c(z) 的意思是用 x 初始化 a ,用 y 初始化 b,用 z 初始化 c。
初始化列表的优点包括:
① 效率:对于非基本类型的对象,使用初始化列表比在构造函数体内赋值更高效,因为它避免了先默认构造然后再赋值的额外开销。
② 必要性:对于引用类型和常量类型的成员变量,必须使用初始化列表,因为这些类型的成员变量在构造函数体内不能被赋值。
③ 顺序:成员变量的初始化顺序是按照它们在类中声明的顺序,而不是初始化列表中的顺序。
使用初始化列表是C++中推荐的初始化类成员变量的方式,因为它提供了更好的性能和灵活性。
4.this关键字
在 C++ 中, this 关键字是一个指向调用对象的指针。它在成员函数内部使用,用于引用调用该函数的对象。使用 this 可以明确指出成员函数正在操作的是哪个对象的数据成员。下面是一个使用 Car 类来 展示 this 关键字用法的示例:(看这段文字的描述可能有些绕,还记得上一节的运算符重载吗, 我们重载 == 运算符用于判断两个类是否相等,重载函数只有一个传入的参数,那么我们怎么和另外一个类的对象比较?我们理所当然的认为p1是调用对象,所以重载函数里的name是p1的name,传入的参数是p2的name,我们现在引入this关键字,我的理解是,我们调用的是p1的重载函数,那么this这时候存储的就是p1的地址,this指向p1,所以我们之前的代码也可以写成this->name表示p1的name和p2进行比较)
#include <iostream>
#include <string>
using namespace std;class Car {
public:Car(string brand, int year) {cout << "构造函数中:" << endl;cout << this << endl;}
};int main()
{Car car("宝马",2024);cout << "main函数中:" << endl;cout << &car << endl;return 0;
}
可以看到this指向被调用对象,当我们实例化一个Car类的对象时,构造函数被调用,这时候this指向car,我们再在main函数中打印car的地址,发现是一样的。
利用引用和this关键字,我们可以做到链式调用,稍微有点复杂,示例如下:
#include <iostream>
#include <string>
using namespace std;class Car {
private:string brand;int year;
public:Car(string brand, int year) {this->brand = brand;this->year = year;}void display() const {cout << "Brand: " << this->brand << ", Year: " << this->year << endl;// 也可以不使用 this->,直接写 brand 和 year}//重点看这里Car& setYear(int year) {this->year = year; // 更新年份return *this; // 返回调用对象的引用}
};int main()
{Car car("宝马",2024);car.display();//链式调用car.setYear(2023).display();return 0;
}
可以看到我们调用完修改年份的函数后又调用了打印函数。重点就在于 setYear 成员函数,返回值是一个Car类的引用,关于引用的知识看我上篇博客C++基础入门(一),引用是直达地址的,我们 return *this ,this是一个指针嘛,我们之前使用this都是用->操作符,this 存放的是实例化后的car的地址,引用的初始化不是赋值地址,而是直接变量名赋值,所以这里return *this(相当于 Car& = car;),返回值就是对car的引用。
在我之前的博客就讲了,引用作为返回值时,可以把函数放在赋值语句的左边,我们调用 car.setYear(2023) 后,返回值还是引用,引用是变量的别名(这里可以把 car.setYear(2023) 简单理解成 car ),那么我们理所当然调用 car.display(); ,这就是链式调用。可能我说的不太好,同学们看看引用的含义和用法,this指针很好理解,自己用自己的理解解释一遍,可以在评论区说出自己的理解,让大伙们更加深刻。
5.new关键字
在C++中, new 关键字用于动态分配内存。它是C++中处理动态内存分配的主要工具之一,允许在程序运行时根据需要分配内存。
基本用法
分配单个对象:使用 new 可以在堆上动态分配一个对象。例如, new int 会分配一个 int 类型的空间,并返回一个指向该空间的指针。
int* ptr = new int; //C语言中,int *p = (int *)malloc(sizeof(int));
分配对象数组: new 也可以用来分配一个对象数组。例如, new int[10] 会分配一个包含10个整数的数组。
int* arr = new int[10]; //C语言中,int *arr = (int *)malloc(sizeof(int)*10);
初始化:可以在 new 表达式中使用初始化。对于单个对象,可以使用构造函数的参数:
MyClass* obj = new MyClass(arg1, arg2);
与 delete 配对使用
使用 new 分配的内存必须显式地通过 delete (对于单个对象)或 delete[] (对于数组)来释放,以避免内存泄露:
释放单个对象:
delete ptr; // 释放 ptr 指向的对象
释放数组:
delete[] arr; // 释放 arr 指向的数组
三、析构函数
1.什么是析构函数
析构函数是C++中的一个特殊的成员函数,它在对象生命周期结束时被自动调用,用于执行对象销毁前的清理工作。析构函数特别重要,尤其是在涉及动态分配的资源(如内存、文件句柄、网络连接等)的情况下。
基本特性
1. 名称:析构函数的名称由波浪号(~)后跟类名构成,如 ~MyClass() 。
2. 无返回值和参数:析构函数不接受任何参数,也不返回任何值。
3. 自动调用:当对象的生命周期结束时(例如,一个局部对象的作用域结束,或者使用 delete 删除一个动态分配的对象),析构函数会被自动调用。
4. 不可重载:每个类只能有一个析构函数。
5. 继承和多态:如果一个类是多态基类,其析构函数应该是虚的。
示例
假设我们有一个类 MyClass ,它包含了动态分配的内存或其他资源:
class MyClass {
private:int* datas; // 指向动态分配的整数数组
public:MyClass(int size) { // 构造函数datas = new int[size]; // 动态分配一个大小为 size 的整数数组}~MyClass() { // 析构函数cout << "析构函数被调用" << endl;delete[] datas; // 释放动态分配的数组}
};int main() {MyClass m1(5); // 在栈上创建一个 MyClass 对象 m1,大小为 5MyClass *m2 = new MyClass(10); // 在堆上创建一个 MyClass 对象 m2,大小为 10delete m2; // 释放堆上的对象 m2return 0;
}
详细解释
栈上对象: m1 是在栈上创建的,当 main 函数结束时,栈上的对象会自动析构。因此,m1 的析构函数会在 main 函数结束时被调用。
堆上对象: m2 是在堆上创建的,需要显式调用 delete 来释放。delete m2; 会调用 m2 的析构函数,释放动态分配的数组,并输出 "析构函数被调用"。
四、静态成员变量
1.静态成员的定义
静态成员在C++类中是一个重要的概念,它包括静态成员变量和静态成员函数。静态成员的特点和存在的意义如下:
静态成员变量
1. 定义:静态成员变量是类的所有对象共享的变量。与普通成员变量相比,无论创建了多少个类的实例,静态成员变量只有一份拷贝。
2. 初始化:静态成员变量需要在类外进行初始化,通常在类的实现文件中。
3. 访问:静态成员变量可以通过类名直接访问,不需要创建类的对象。也可以通过类的对象访问。 4. 用途:常用于存储类级别的信息(例如,计数类的实例数量)或全局数据需要被类的所有实例共享。
静态成员函数
1. 定义:静态成员函数是可以不依赖于类的实例而被调用的函数。它不能访问类的非静态成员变量和非静态成员函数。
2. 访问:类似于静态成员变量,静态成员函数可以通过类名直接调用,也可以通过类的实例调用。 3. 用途:常用于实现与具体对象无关的功能,或访问静态成员变量。
示例代码
#include <iostream>class MyClass {
public:static int staticValue; // 静态成员变量MyClass() {// 每创建一个对象,静态变量增加1staticValue++;}static int getStaticValue() {// 静态成员函数return staticValue;}
};
// 类外初始化静态成员变量
int MyClass::staticValue = 0;
int main() {MyClass obj1, obj2;std::cout << MyClass::getStaticValue(); // 输出2
}
存在的意义
共享数据:允许对象之间共享数据,而不需要每个对象都有一份拷贝。
节省内存:对于频繁使用的类,使用静态成员可以节省内存。
独立于对象的功能:静态成员函数提供了一种在不创建对象的情况下执行操作的方法,这对于实现工具函数或管理类级别状态很有用。
2.静态成员变量的作用
静态成员变量在C++中的一个典型应用是用于跟踪类的实例数量。这个案例体现了静态成员变量的特性:它们在类的所有实例之间共享,因此适合于存储所有实例共有的信息。
下面是一个示例,展示了如何使用静态成员变量来计数一个类的实例数量:
#include <iostream>
using namespace std;
class Myclass{
private:static int staticNumofInstance;
public:Myclass(){staticNumofInstance++;}~Myclass(){staticNumofInstance--;}static int getNunofInstance(){return staticNumofInstance;}
};int Myclass::staticNumofInstance = 0;int main()
{Myclass m1;cout << Myclass::getNunofInstance() << endl;Myclass m2;cout << m2.getNunofInstance() << endl;{Myclass m3;cout << Myclass::getNunofInstance() << endl;Myclass m4;cout << Myclass::getNunofInstance() << endl;}cout << Myclass::getNunofInstance() << endl;Myclass *m5 = new Myclass;cout << Myclass::getNunofInstance() << endl;delete m5;cout << Myclass::getNunofInstance() << endl;return 0;
}
可以看到我们将m3和m4放在一个{ }里面,当大括号结束时,m3和m4的栈被释放,这里是QT的一个新的用法。
在这个例子中:
● Myclass 类有一个静态成员变量 staticNumofInstance ,用来跟踪该类的实例数量。
● 每当创建 Myclass 的新实例时,构造函数会增加 staticNumofInstance 。
● 每当一个 Myclass 实例被销毁时,析构函数会减少 staticNumofInstance 。
● 通过静态成员函数 getNunofInstance 可以随时获取当前的实例数量。
● 静态成员变量 staticNumofInstance 在类外初始化为0。
这个案例展示了静态成员变量如何在类的所有实例之间共享,并为所有实例提供了一个共同的状态(在这个例子中是实例的数量)。这种技术在需要跟踪对象数量或实现某种形式的资源管理时特别有用。
五、继承
1.继承基本概念
继承是面向对象编程(OOP)中的一个核心概念,特别是在C++中。它允许一个类(称为派生类或子类)继承另一个类(称为基类或父类)的属性和方法。继承的主要目的是实现代码重用,以及建立一种类型之间的层次关系。
特点
1. 代码重用:子类继承了父类的属性和方法,减少了代码的重复编写。
2. 扩展性:子类可以扩展父类的功能,添加新的属性和方法,或者重写(覆盖)现有的方法。
3. 多态性:通过继承和虚函数,C++支持多态,允许在运行时决定调用哪个函数。
基本用法
在C++中,继承可以是公有(public)、保护(protected)或私有(private)的,这决定了基类成员在派生类中的访问权限。
#include <iostream>
using namespace std;
//基类,父类
class Vehicle{ //交通工具,车,抽象的概念
public:string type;string contry;string color;double price;int numOfWheel;void run(){cout << "车跑起来了" << endl;}void stop();
};
//派生类,子类
class Bickle : public Vehicle{
};
//派生类,子类
class Roadster : public Vehicle{ //跑车,也是抽象,比父类感觉上范围缩小了点
public:int stateOfTop;void openTopped();void pdrifting();
};int main()
{Roadster ftype;ftype.type = "捷豹Ftype";ftype.run();Bickle bike;bike.type = "死飞";bike.run();return 0;
}
在这个例子中, Vehicle 的子类公有地继承自 Vehicle 类,这意味着所有 Vehicle 类的公有成员在 Vehicle 的子类中也是公有的。
2.权限对继承的影响
在C++中,访问控制符对继承的影响可以通过下表来清晰地展示。这个表格展示了不同类型的继承如何影响基类的不同类型成员在派生类中的访问级别。
这个表格提供了一个快速参考,帮助理解在不同类型的继承中基类成员的访问级别是如何变化的。记住,无论继承类型如何,基类的 private 成员始终不可直接在派生类中访问!!!
访问权限回顾
代码验证:
#include <iostream>
using namespace std;
//基类,父类
class Vehicle{ //交通工具,车,抽象的概念
public:string type;string contry;string color;double price;int numOfWheel;
protected:int protectedData;
private:int privateData;
public:void run(){cout << "车跑起来了" << endl;}void stop();
};//私有继承测试
class TestClass : private Vehicle{
public:void tsetFunc(){price = 10; //基类的公有数据被私有继承后,在派生类中权限编程私有,只限在类内部使用}
};
//公有继承测试
class Truck : protected Vehicle{
public:void testFunc(){type = "数据测试"; //编程了公有权限protectedData = 10; //保持公有权限privateData = 10; //报错了,基类的私有成员,不管哪种方式的继承都是不可访问的。}
};
//公有继承,基类的公有权限和保护权限不变,私有成员不能访问
class Bickle : public Vehicle{
public:void testFunc(){protectedData = 10;}
};
//派生类,子类
class Roadster : public Vehicle{ //跑车,也是抽象,比父类感觉上范围缩小了点
public:int stateOfTop;void openTopped();void pdrifting();
};
int main()
{TestClass test;test.price = 3.3; //报错了,基类的公有成员被私有继承后,降为私有权限Truck t;t.type = "测试"; //报错了,基类的公有成员被保护继承后,降为保护权限t.protectedData = 10; //从报错信息看出,保护继承造成基类的保护成员还是保持保护权限Roadster ftype;ftype.type = "捷豹Ftype";ftype.run();Bickle bike;bike.type = "死飞";bike.run();return 0;
}
3.基类构造函数
在C++中,派生类可以通过其构造函数的初始化列表来调用基类的构造函数。这是在构造派生类对象时初始化基类部分的标准做法。
当创建派生类的对象时,基类的构造函数总是在派生类的构造函数之前被调用。如果没有明确指定,将调用基类的默认构造函数。如果基类没有默认构造函数,或者你需要调用一个特定的基类构造函数,就需要在派生类构造函数的初始化列表中明确指定。
示例
#include <iostream>
class Base {
public:int data;Base(int x) {std::cout << "Base constructor with x = " << x << std::endl;}
};
class Derived : public Base {
public:double ydata;Derived(int x, double y) : Base(x) { // 调用 Base 类的构造函数std::cout << "Derived constructor with y = " << y << std::endl;}
};int main() {Derived obj(10, 3.14); // 首先调用 Base(10),然后调用 Derived 的构造函数return 0;
}
在这个例子中:
● Base 类有一个接受一个整数参数的构造函数。
● Derived 类继承自 Base ,它的构造函数接受一个整数和一个双精度浮点数。在其初始化列表中,它调用 Base 类的构造函数,并传递整数参数。
● 当 Derived 类的对象被创建时,首先调用 Base 类的构造函数,然后调用 Derived 类的构造函数。
通过这种方式,派生类能够确保其基类部分被正确初始化。在继承层次结构中,这是非常重要的,特别是当基类需要一些特定的初始化操作时。
4.虚函数
在C++中, virtual 和 override 关键字用于支持多态,尤其是在涉及类继承和方法重写的情况下。正确地理解和使用这两个关键字对于编写可维护和易于理解的面向对象代码至关重要。
(如果学过32的话,大概知道虚函数是什么一回事,我们的中断回调函数前通常会有__weak 标志,表示我们可以重写这个回调函数,实现我们自己的功能)
virtual 关键字
1. 使用场景:在基类中声明虚函数。
2. 目的:允许派生类重写该函数,实现多态。
3. 行为:当通过基类的指针或引用调用一个虚函数时,调用的是对象实际类型的函数版本。
4. 示例:
class Base {
public:virtual void func() {std::cout << "Function in Base" << std::endl;}
};
override 关键字
1. 使用场景:在派生类中重写虚函数。
2. 目的:明确指示函数意图重写基类的虚函数。
3. 行为:确保派生类的函数确实重写了基类中的一个虚函数。如果没有匹配的虚函数,编译器会报错。
4. 示例:
class Derived : public Base {
public:void func() override {std::cout << "Function in Derived" << std::endl;}
};
注意点
● 只在派生类中使用 override: override 应仅用于派生类中重写基类的虚函数。
● 虚析构函数:如果类中有虚函数,通常应该将析构函数也声明为虚的。
● 默认情况下,成员函数不是虚的:在C++中,成员函数默认不是虚函数。只有显式地使用 virtual 关键字才会成为虚函数。
● 继承中的虚函数:一旦在基类中声明为虚函数,该函数在所有派生类中自动成为虚函数,无论是否使用 virtual 关键字。
正确使用 virtual 和 override 关键字有助于清晰地表达程序员的意图,并利用编译器检查来避免常 见的错误,如签名不匹配导致的非预期的函数重写。
5.多重继承
在C++中,多重继承是一种允许一个类同时继承多个基类的特性。这意味着派生类可以继承多个基类的属性和方法。多重继承增加了语言的灵活性,但同时也引入了额外的复杂性,特别是当多个基类具有相同的成员时。
基本概念
在多重继承中,派生类继承了所有基类的特性。这包括成员变量和成员函数。如果不同的基类有相同名称的成员,则必须明确指出所引用的是哪个基类的成员。
示例
假设有两个基类 ClassA 和 ClassB ,以及一个同时从这两个类继承的派生类 Derived:
#include <iostream>
class ClassA {
public:void displayA() {std::cout << "Displaying ClassA" << std::endl;}
};
class ClassB {
public:void displayB() {std::cout << "Displaying ClassB" << std::endl;}
};//同时继承A和B
class Derived : public ClassA, public ClassB {
public:void display() {displayA(); // 调用 ClassA 的 displayAdisplayB(); // 调用 ClassB 的 displayB}
};int main() {Derived obj;obj.displayA(); // 调用 ClassA 的 displayAobj.displayB(); // 调用 ClassB 的 displayBobj.display();// 调用 Derived 的 displayreturn 0;
}
注意事项
● 菱形继承问题:如果两个基类继承自同一个更高层的基类,这可能导致派生类中存在两份基类的副本,称为菱形继承(或钻石继承)问题。这可以通过虚继承来解决。
● 复杂性:多重继承可能会使类的结构变得复杂,尤其是当继承层次较深或类中有多个基类时。 ● ● 设计考虑:虽然多重继承提供了很大的灵活性,但过度使用可能导致代码难以理解和维护。在一些情况下,使用组合或接口(纯虚类)可能是更好的设计选择。
多重继承是C++的一个强大特性,但应谨慎使用。合理地应用多重继承可以使代码更加灵活和强大,但不当的使用可能导致设计上的问题和维护困难。
6.虚继承
虚继承是C++中一种特殊的继承方式,主要用来解决多重继承中的菱形继承问题。在菱形继承结构中,一个类继承自两个具有共同基类的类时,会导致共同基类的成员在派生类中存在两份拷贝,这不仅会导致资源浪费,还可能引起数据不一致的问题。虚继承通过确保共同基类的单一实例存在于继承层次中,来解决这一问题。
菱形继承问题示例
FinalDerived 类如果想引用 Base 类里的成员,那么是使用 Derived1 里的还是 2?
要解决这个问题,应使用虚继承:
class Base {
public:int data;
};class Derived1 : virtual public Base {// 虚继承 Base
};
class Derived2 : virtual public Base {// 虚继承 Base
};class FinalDerived : public Derived1, public Derived2 {// 继承自 Derived1 和 Derived2
};
通过将 Derived1 和 Derived2 对 Base 的继承声明为虚继承( virtual public Base),FinalDerived 类中只会有一份 Base 类的成员。无论通过 Derived1 还是 Derived2 的路径,访问的都是同一个 base 类的成员。
特点和注意事项
● 初始化虚基类:在使用虚继承时,虚基类(如上例中的 Base 类)只能由最派生的类(如 FinalDerived )初始化。
● 内存布局:虚继承可能会改变类的内存布局,通常会增加额外的开销,比如虚基类指针。
● 设计考虑:虚继承应谨慎使用,因为它增加了复杂性。在实际应用中,如果可以通过其他设计(如组合或接口)避免菱形继承,那通常是更好的选择。
虚继承是C++语言中处理复杂继承关系的一种重要机制,但它也带来了一定的复杂性和性能考虑。正确地使用虚继承可以帮助你建立清晰、有效的类层次结构。
六、多态
1.多态的基本概念(polymorphic)
想象一下,你有一个遥控器(这就像是一个基类的指针),这个遥控器可以控制不同的电子设备(这些设备就像是派生类)。无论是电视、音响还是灯光,遥控器上的“开/关”按钮(这个按钮就像是一个虚函数)都能控制它们,但具体的操作(打开电视、播放音乐、开灯)则取决于你指向的设备。
2.如何实现多态
1. 使用虚函数(Virtual Function):
● 我们在基类中定义一个虚函数,这个函数可以在任何派生类中被“重写”或者说“定制”。
● 使用关键字 virtual 来声明。
2. 创建派生类并重写虚函数:
● 在派生类中,我们提供该虚函数的具体实现。这就像是告诉遥控器,“当你控制我的这个设备时,这个按钮应该这样工作”。
3. 通过基类的引用或指针调用虚函数:
● 当我们使用基类类型的指针或引用来调用虚函数时,实际调用的是对象的实际类型(派生类)中的函数版本。
代码示例:
#include <iostream>
using namespace std;// 基类:遥控器接口
class RemoteCon {
public:// 虚函数,用于打开设备virtual void openUtils() {cout << "遥控器的开被按下" << endl;}
};// 派生类:电视遥控器
class TvRemoteCon : public RemoteCon {
public:// 重写虚函数,实现具体功能void openUtils() override {cout << "电视遥控器的开被按下" << endl;}// 测试函数,仅用于演示void testFunc() {}
};// 派生类:音响遥控器
class RoundspeakerCon : public RemoteCon {
public:// 重写虚函数,实现具体功能void openUtils() override {cout << "音响遥控器的开被按下" << endl;}
};// 派生类:灯光遥控器
class LightCon : public RemoteCon {
public:// 重写虚函数,实现具体功能void openUtils() override {cout << "灯光遥控器的开被按下" << endl;}
};// 测试函数,接受基类引用作为参数
void test(RemoteCon& r) {r.openUtils(); // 调用虚函数,具体调用哪个函数取决于传入对象的实际类型
}int main() {// 创建一个电视遥控器对象,通过基类指针管理RemoteCon *remoteCon = new TvRemoteCon; // 多态:基类指针指向派生类对象remoteCon->openUtils(); // 调用重写的虚函数,输出 "电视遥控器的开被按下"// 创建一个音响遥控器对象,通过基类指针管理RemoteCon *remoteCon2 = new RoundspeakerCon; // 多态:基类指针指向派生类对象remoteCon2->openUtils(); // 调用重写的虚函数,输出 "音响遥控器的开被按下"// 创建一个灯光遥控器对象,通过基类指针管理RemoteCon *remoteCon3 = new LightCon; // 多态:基类指针指向派生类对象remoteCon3->openUtils(); // 调用重写的虚函数,输出 "灯光遥控器的开被按下"// 创建一个电视遥控器对象,通过对象本身管理TvRemoteCon tvRemote;test(tvRemote); // 传入对象引用,调用重写的虚函数,输出 "电视遥控器的开被按下"// 释放动态分配的内存delete remoteCon;delete remoteCon2;delete remoteCon3;return 0;
}
在这个例子中,不同的对象以它们自己的方式“开”,尽管调用的是相同的函数。这就是多态的魅力——相同的接口,不同的行为。
为什么使用多态
灵活性:允许我们编写可以处理不确定类型的对象的代码。
可扩展性:我们可以添加新的派生类而不必修改使用基类引用或指针的代码。
接口与实现分离:我们可以设计一个稳定的接口,而将具体的实现留给派生类去处理。
3.抽象类
抽象类的基本概念
想象一下,你有一个“交通工具”的概念。这个概念告诉你所有交通工具都应该能做什么,比如移动(move),但它并不具体说明怎么移动。对于不同的交通工具,比如汽车和自行车,它们的移动方式是不同的。在这个意义上,“交通工具”是一个抽象的概念,因为它本身并不能直接被使用。你需要一个具体的交通工具,比如“汽车”或“自行车”,它们根据“交通工具”的概念具体实现了移动的功能。
在 C++ 中,抽象类就像是这样的一个抽象概念。它定义了一组方法(比如移动),但这些方法可能没有具体的实现。这意味着,抽象类定义了派生类应该具有的功能,但不完全实现这些功能。
抽象类的特点
1. 包含至少一个纯虚函数:
抽象类至少有一个纯虚函数。这是一种特殊的虚函数,在抽象类中没有具体实现,而是留给派生类去实现。
纯虚函数的声明方式是在函数声明的末尾加上 = 0。
2. 不能直接实例化:
由于抽象类不完整,所以不能直接创建它的对象。就像你不能直接使用“交通工具”的概念去任何地方,你需要一个具体的交通工具。
3. 用于提供基础结构:
抽象类的主要目的是为派生类提供一个共同的基础结构,确保所有派生类都有一致的接口和行为。
示例代码:
#include <iostream>
using namespace std;// 抽象基类:教师
class Teacher {
public:string name; // 教师姓名string school; // 所在学校string major; // 专业领域// 纯虚函数,定义教师进入教室的行为virtual void goInClass() = 0;// 纯虚函数,定义教师开始教学的行为virtual void startTeaching() = 0;// 纯虚函数,定义教师教学结束后的行为virtual void afterTeaching() = 0;
};// 派生类:英语老师
class EnglishTeacher : public Teacher {
public:// 重写虚函数,实现英语老师进入教室的具体行为void goInClass() override {cout << "英语老师开始进入教室" << endl;}// 重写虚函数,实现英语老师开始教学的具体行为void startTeaching() override {cout << "英语老师开始教学" << endl;}// 重写虚函数,实现英语老师教学结束后的具体行为void afterTeaching() override {// 英语老师教学结束后没有特定行为,可以留空}
};// 派生类:编程老师
class ProTeacher : public Teacher {
public:// 重写虚函数,实现编程老师进入教室的具体行为void goInClass() override {cout << "编程老师开始进入教室" << endl;}// 重写虚函数,实现编程老师开始教学的具体行为void startTeaching() override {cout << "编程老师开始撸代码了,拒绝读PPT" << endl;}// 重写虚函数,实现编程老师教学结束后的具体行为void afterTeaching() override {cout << "编程老师下课后手把手教学生写代码" << endl;}
};int main() {// Teacher t; // 抽象类,不支持被实例化// 创建一个英语老师对象EnglishTeacher e;e.goInClass(); // 调用英语老师进入教室的行为// 创建一个编程老师对象ProTeacher t;t.startTeaching(); // 调用编程老师开始教学的行为t.afterTeaching(); // 调用编程老师教学结束后的行为// 抽象类,多态Teacher *teacher = new ProTeacher; // 基类指针指向派生类对象teacher->startTeaching(); // 调用编程老师开始教学的行为// 释放动态分配的内存delete teacher;return 0;
}
到此C++阶段的学习就结束了,这些对于QT的学习已经够用了,等后续要用到再在QT文章进行讲解,下一篇博客就是QT的记事本项目,直接从项目入手。希望大家持续关注一手,一起学习。