C++基础入门(二)
目录
前言
一、重载
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的记事本项目,直接从项目入手。希望大家持续关注一手,一起学习。
相关文章:
C++基础入门(二)
目录 前言 一、重载 1.函数重载 2.运算符重载 二、构造函数 1.什么是构造函数 2.带参数的构造函数 3.使用初始化列表 4.this关键字 5.new关键字 三、析构函数 1.什么是析构函数 四、静态成员变量 1.静态成员的定义 2.静态成员变量的作用 五、继承 1.继承基本概…...
互联网架构困境:网络与信息安全
当我们说 TCP/IP 没有内置安全属性时,这到底是什么意思?事实上仔细观察身边的世界,很少有内置安全属性的,这源自于石器时代的野人们没有粮仓需要保护。 “互联网前身 ARPAnet 最初来自于美国国防部对等通信需求”,即使…...
HIVE技术
本文章基于黑马免费资料编写。 hive介绍 简介 hive架构 hive需要启动的配置 执行元数据库初始化命令 使用hive必须启动的服务 ./schematool -initSchema -dbType mysql -verbos启动 Hive 创建一个 hive 的日志文件夹 mkdir /export/server/hive/logs启动元数据管理服务 n…...
RustDesk ID更新脚本
RustDesk ID更新脚本 此PowerShell脚本自动更新RustDesk ID和密码,并将信息安全地存储在Bitwarden中。 特点 使用以下选项更新RustDesk ID: 使用系统主机名生成一个随机的9位数输入自定义值 为RustDesk生成新的随机密码将RustDesk ID和密码安全地存储…...
卷积神经网络的底层是傅里叶变换
1 卷积神经网络与傅里叶变换、希尔伯特空间坐标变换的关系_卷积神经网络与傅里页变换之间的关系-CSDN博客 从卷积到图像卷积再到卷积神经网络,到底卷了什么? 一维信号卷积:当前时刻之前的每一个时刻是如何对当前时刻产生影响的 图像卷积&…...
Bootstrap 下拉菜单
Bootstrap 下拉菜单 Bootstrap 是一个流行的前端框架,它提供了许多预构建的组件,其中之一就是下拉菜单。下拉菜单是一个交互式元素,允许用户从一系列选项中选择一个。在本篇文章中,我们将详细介绍如何在 Bootstrap 中创建和使用下…...
计算机组成原理(计算机系统3)--实验一:WinMIPS64模拟器实验
一、实验目标: 了解WinMIPS64的基本功能和作用; 熟悉MIPS指令、初步建立指令流水执行的感性认识; 掌握该工具的基本命令和操作,为流水线实验做准备。 二、实验内容 按照下面的实验步骤及说明,完成相关操作记录实验…...
读书笔记~管理修炼-风险性决策:学会缩小风险阈值
假设你的团队为了提升业绩,提出了两个解决方案:A方案是通过营销提升老产品的利润;B方案是通过研发开拓新产品,你会怎么选? 我们先来分析下,其实无论是A方案还是B方案,都会遇到市场难题…...
VIVADO FIFO (同步和异步) IP 核详细使用配置步骤
VIVADO FIFO (同步和异步) IP 核详细使用配置步骤 目录 前言 一、同步FIFO的使用 1、配置 2、仿真 二、异步FIFO的使用 1、配置 2、仿真 前言 在系统设计中,利用FIFO(first in first out)进行数据处理是再普遍不过的应用了,…...
tcp粘包原理和解决
tcp粘包原理和解决 咱们先通过展示基于tcp 的cs端消息通信时的现象,带着问题再解释下面的tcp粘包问题。 一、原始代码 tcp 服务端代码 // socket_stick/server/main.gofunc process(conn net.Conn) {defer conn.Close()reader : bufio.NewReader(conn)var bu…...
C语言预处理艺术:编译前的魔法之旅
大家好,这里是小编的博客频道 小编的博客:就爱学编程 很高兴在CSDN这个大家庭与大家相识,希望能在这里与大家共同进步,共同收获更好的自己!!! 本文目录 引言正文一、预处理的作用与流程…...
C++算法第十六天
本篇文章我们继续学习动态规划 第一题 题目链接 978. 最长湍流子数组 - 力扣(LeetCode) 题目解析 从上图可见其实有三个状态 代码原理 注意:我们在分析题目的时候分析出来的是三个状态,分别是上升、下降、平坦,但是…...
计算机网络 (45)动态主机配置协议DHCP
前言 计算机网络中的动态主机配置协议(DHCP,Dynamic Host Configuration Protocol)是一种网络管理协议,主要用于自动分配IP地址和其他网络配置参数给连接到网络的设备。 一、基本概念 定义:DHCP是一种网络协议…...
归子莫的科技周刊#2:白天搬砖,夜里读诗
归子莫的科技周刊#2:白天搬砖,夜里读诗 本周刊开源,欢迎投稿。 刊期:2025.1.5 - 2025.1.11。原文地址。 封面图 下班在深圳看到的夕阳,能遇到是一种偶然的机会,能拍下更是一种幸运。 白天搬砖,…...
平滑算法 效果比较
目录 高斯平滑 效果对比 移动平均效果比较: 高斯平滑 效果对比 右边两个参数是1.5 2 代码: smooth_demo.py import numpy as np import cv2 from scipy.ndimage import gaussian_filter1ddef gaussian_smooth_array(arr, sigma):smoothed_arr = gaussian_filter1d(arr, s…...
Elasticsearch容器启动报错:AccessDeniedException[/usr/share/elasticsearch/data/nodes];
AccessDeniedException 表明 Elasticsearch 容器无法访问或写入数据目录 /usr/share/elasticsearch/data/nodes。这是一个权限问题。 问题原因: 1、宿主机目录权限不足:映射到容器的数据目录 /data/es/data 在宿主机上可能没有足够的权限供容器访问。 …...
【Linux系统编程】——深入理解 GCC/G++ 编译过程及常用选项详解
文章目录 1. GCC/G 编译过程预处理(Preprocessing)编译(Compilation)汇编(Assembly)连接(Linking) 静态链接与动态链接静态链接动态链接静态库和动态库 GCC 常用选项关于编译器的周边…...
Mac安装配置使用nginx的一系列问题
brew安装nginx https://juejin.cn/post/6986190222241464350 使用brew安装nginx,如下命令所示: brew install nginx 如下图所示: 2.查看nginx的配置信息,如下命令: brew info nginxFrom:xxx 这样的,是n…...
Vue3中使用组合式API通过路由传值详解
在Vue 3中,使用组合式API来传递路由参数是一种常见的需求。Vue Router 是 Vue.js 的官方路由管理工具,可以在不同的场景下通过多种方式传递和接收路由参数。下面将详细讲解几种常见的路由传值方式,并提供相应的代码示例。 1. 通过路由参数传…...
两分钟解决 :![rejected] master -> master (fetch first) , 无法正常push到远端库
目录 分析问题的原因解决 分析问题的原因 在git push的时候莫名遇到这种情况 若你在git上修改了如README.md的文件。由于本地是没有README.md文件的,所以导致 远端仓库git和本地不同步。 将远端、本地进行合并就可以很好的解决这个问题 注意:直接git pu…...
KubeSphere 容器平台高可用:环境搭建与可视化操作指南
Linux_k8s篇 欢迎来到Linux的世界,看笔记好好学多敲多打,每个人都是大神! 题目:KubeSphere 容器平台高可用:环境搭建与可视化操作指南 版本号: 1.0,0 作者: 老王要学习 日期: 2025.06.05 适用环境: Ubuntu22 文档说…...
相机Camera日志实例分析之二:相机Camx【专业模式开启直方图拍照】单帧流程日志详解
【关注我,后续持续新增专题博文,谢谢!!!】 上一篇我们讲了: 这一篇我们开始讲: 目录 一、场景操作步骤 二、日志基础关键字分级如下 三、场景日志如下: 一、场景操作步骤 操作步…...
前端倒计时误差!
提示:记录工作中遇到的需求及解决办法 文章目录 前言一、误差从何而来?二、五大解决方案1. 动态校准法(基础版)2. Web Worker 计时3. 服务器时间同步4. Performance API 高精度计时5. 页面可见性API优化三、生产环境最佳实践四、终极解决方案架构前言 前几天听说公司某个项…...
django filter 统计数量 按属性去重
在Django中,如果你想要根据某个属性对查询集进行去重并统计数量,你可以使用values()方法配合annotate()方法来实现。这里有两种常见的方法来完成这个需求: 方法1:使用annotate()和Count 假设你有一个模型Item,并且你想…...
相机Camera日志分析之三十一:高通Camx HAL十种流程基础分析关键字汇总(后续持续更新中)
【关注我,后续持续新增专题博文,谢谢!!!】 上一篇我们讲了:有对最普通的场景进行各个日志注释讲解,但相机场景太多,日志差异也巨大。后面将展示各种场景下的日志。 通过notepad++打开场景下的日志,通过下列分类关键字搜索,即可清晰的分析不同场景的相机运行流程差异…...
Redis数据倾斜问题解决
Redis 数据倾斜问题解析与解决方案 什么是 Redis 数据倾斜 Redis 数据倾斜指的是在 Redis 集群中,部分节点存储的数据量或访问量远高于其他节点,导致这些节点负载过高,影响整体性能。 数据倾斜的主要表现 部分节点内存使用率远高于其他节…...
CMake控制VS2022项目文件分组
我们可以通过 CMake 控制源文件的组织结构,使它们在 VS 解决方案资源管理器中以“组”(Filter)的形式进行分类展示。 🎯 目标 通过 CMake 脚本将 .cpp、.h 等源文件分组显示在 Visual Studio 2022 的解决方案资源管理器中。 ✅ 支持的方法汇总(共4种) 方法描述是否推荐…...
【碎碎念】宝可梦 Mesh GO : 基于MESH网络的口袋妖怪 宝可梦GO游戏自组网系统
目录 游戏说明《宝可梦 Mesh GO》 —— 局域宝可梦探索Pokmon GO 类游戏核心理念应用场景Mesh 特性 宝可梦玩法融合设计游戏构想要素1. 地图探索(基于物理空间 广播范围)2. 野生宝可梦生成与广播3. 对战系统4. 道具与通信5. 延伸玩法 安全性设计 技术选…...
Device Mapper 机制
Device Mapper 机制详解 Device Mapper(简称 DM)是 Linux 内核中的一套通用块设备映射框架,为 LVM、加密磁盘、RAID 等提供底层支持。本文将详细介绍 Device Mapper 的原理、实现、内核配置、常用工具、操作测试流程,并配以详细的…...
python执行测试用例,allure报乱码且未成功生成报告
allure执行测试用例时显示乱码:‘allure’ �����ڲ����ⲿ���Ҳ���ǿ�&am…...
