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:
\circ p1 是调用对象, this 指针(后面讲到构造函数时会讲)指向 p1。
\circ p2 是传递给 operator== 函数的参数 pTmp。
\circ 在 operator== 函数内部,name 和 inNumberTail 指的是 p1 的成员变量。
\circ 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: 5
    return 0;
}

        在这个例子中, operator+ 被重载为一个成员函数,接受一个 Point 类型的常量引用作为参数,并返回两个点相加的结果。


二、构造函数

1.什么是构造函数

        的构造函数是类的一种特殊的成员函数,它会在每次创建类的新对象时执行

构造,那构造的是什么呢?

        构造成员变量的初始化值,内存空间等。构造函数的名称与类的名称是完全相同的,并且不会返回任何类型,也不会返回 void。构造函数可用于为某些成员变量设置初始值。

        下面的实例有助于更好地理解构造函数的概念:

#include <iostream>
#include <string>
using namespace std;  // 使用std命名空间

class Car {
public:
    string brand;  // 不需要使用std::string
    int 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,大小为 5
    MyClass *m2 = new MyClass(10);  // 在堆上创建一个 MyClass 对象 m2,大小为 10
    delete m2;  // 释放堆上的对象 m2
    return 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() {
        // 每创建一个对象,静态变量增加1
        staticValue++;
    }
    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 的 displayA
        displayB(); // 调用 ClassB 的 displayB
    }
};

int main() {
    Derived obj;
    obj.displayA();  // 调用 ClassA 的 displayA
    obj.displayB();  // 调用 ClassB 的 displayB
    obj.display();
    // 调用 Derived 的 display
    return 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的记事本项目,直接从项目入手。希望大家持续关注一手,一起学习。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/954798.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Redis 中 TTL 的基本知识与禁用缓存键的实现策略(Java)

目录 前言1. 基本知识2. Java代码 前言 &#x1f91f; 找工作&#xff0c;来万码优才&#xff1a;&#x1f449; #小程序://万码优才/r6rqmzDaXpYkJZF 单纯学习Redis可以看我前言的Java基本知识路线&#xff01;&#xff01; 对于Java的基本知识推荐阅读&#xff1a; java框架…

基于unity的多人家装应用的设计与实现

摘要 本课题根据主流家装应用存在的问题和结合了Unity3D引擎所具有的优势&#xff0c;在主流家装应用的基础上弥补了常见的缺憾&#xff0c;实现了一种新型的交互更强的家装展示系统。 本系统主要通过将家具模型资源和材质等资源导入Unity3D平台中&#xff0c;通过C#代码开发&a…

Three.js+Vue3+Vite应用lil-GUI调试开发3D效果(三)

前期文章中我们完成了创建第一个场景、添加轨道控制器的功能&#xff0c;接下来我们继续阐述其他的功能&#xff0c;本篇文章中主要讲述如何应用lil-GUI调试开发3D效果&#xff0c;在开始具体流程和步骤之前&#xff0c;请先查看之前的内容&#xff0c;因为该功能必须在前期内容…

采用海豚调度器+Doris开发数仓保姆级教程(满满是踩坑干货细节,持续更新)

一、采用海豚调度器Doris开发平替CDH Hdfs Yarn Hive Oozie的理由。 海豚调度器Doris离线数仓方案与CDH Hive在多个方面存在显著差异&#xff0c;以下是对这两种方案的对比分析&#xff1a; 1. 架构复杂性 CDH Hive&#xff1a;基于Hadoop生态&#xff0c;组件众多&#…

50.【8】BUUCTF WEB HardSql

进入靶场 随便输输 上order by ????????&#xff0c;被过滤了,继续找其他也被过滤的关键字 #&#xff0c;-- -&#xff0c;-- 都不行&#xff0c;尝试其他特殊字符后发现and&#xff0c;union&#xff0c;select&#xff0c;空格&#xff0c;都被过滤了 如下 我就不知…

Redis 3.2.1在Win10系统上的安装教程

诸神缄默不语-个人CSDN博文目录 这个文件可以跟我要&#xff0c;也可以从官网下载&#xff1a;https://github.com/MicrosoftArchive/redis/releases 这个是微软以前维护的Windows版Redis安装包&#xff0c;如果想要比较新的版本可以从别人维护的项目里下&#xff08;https://…

mac配置 iTerm2 使用lrzsz与服务器传输文件

mac配置 1. 安装支持rz和sz命令的lrzsz brew install lrzsz2. 下载iterm2-send-zmodem.sh和iterm2-recv-zmodem.sh两个脚本 # 克隆仓库 git clone https://github.com/aikuyun/iterm2-zmodem ~/iterm2-zmodem# 进入到仓库目录 cd ~/iterm2-zmodem# 设置脚本文件可执行权限 c…

9.7 visual studio 搭建yolov10的onnx的预测(c++)

1.环境配置 在进行onnx预测前&#xff0c;需要搭建的环境如下: 1.opencv环境的配置&#xff0c;可参考博客:9.2 c搭建opencv环境-CSDN博客 2.libtorch环境的配置&#xff0c;可参考博客&#xff1a;9.4 visualStudio 2022 配置 cuda 和 torch (c)-CSDN博客 3.cuda环境的配置…

YOLOv8从菜鸟到精通(二):YOLOv8数据标注以及模型训练

数据标注 前期准备 先打开Anaconda Navigator&#xff0c;点击Environment&#xff0c;再点击new(new是我下载anaconda的文件夹名称)&#xff0c;然后点击创建 点击绿色按钮&#xff0c;并点击Open Terminal 输入labelimg便可打开它,labelimg是图像标注工具&#xff0c;在上篇…

STM32-keil安装时遇到的一些问题以及解决方案

前言&#xff1a; 本人项目需要使用到STM32,故需配置keil 5&#xff0c;在配置时遇到了以下问题&#xff0c;并找到相应的解决方案&#xff0c;希望能够为遇到相同问题的道友提供一些解决思路 1、提示缺少&#xff08;missing&#xff09;version 5编译器 step1&#xff1a;找…

C语言结构体漫谈:从平凡中见不平凡

大家好&#xff0c;这里是小编的博客频道 小编的博客&#xff1a;就爱学编程 很高兴在CSDN这个大家庭与大家相识&#xff0c;希望能在这里与大家共同进步&#xff0c;共同收获更好的自己&#xff01;&#xff01;&#xff01; 本文目录 引言正文《1》 结构体的两种声明一、结构…

LabVIEW与WPS文件格式的兼容性

LabVIEW 本身并不原生支持将文件直接保存为 WPS 格式&#xff08;如 WPS 文档或表格&#xff09;。然而&#xff0c;可以通过几种间接的方式实现这一目标&#xff0c;确保您能将 LabVIEW 中的数据或报告转换为 WPS 可兼容的格式。以下是几种常见的解决方案&#xff1a; ​ 导出…

LeetCode | 栈与队列:算法入门到进阶的全解析

栈和队列作为最基础的数据结构&#xff0c;不仅简单直观&#xff0c;还在算法世界中扮演着举足轻重的角色。无论是处理括号匹配问题、滑动窗口、还是实现先进先出的任务调度&#xff0c;栈与队列都是核心工具。 在本篇文章中&#xff0c;我们将以 LeetCode 中的经典题目为例&am…

得物App再迎开放日,全流程体验正品查验鉴别

近日&#xff0c;得物App超级品质保障中心再度迎来了开放日活动。近60位得物App的用户与粉丝齐聚超级品质保障中心&#xff0c;全流程体验正品查验鉴别。开放日当天&#xff0c;参与者有机会近距离观察得物App的商品质检区、鉴别区、收发流转区、实验室和正品库等关键功能区&am…

docker 部署 MantisBT

1. docker 安装MantisBT docker pull vimagick/mantisbt:latest 2.先运行实例&#xff0c;复制配置文件 docker run -p 8084:80 --name mantisbt -d vimagick/mantisbt:latest 3. 复制所需要配置文件到本地路径 docker cp mantisbt:/var/www/html/config/config_inc.php.…

【大语言模型】ACL2024论文-38 从信息瓶颈视角有效过滤检索增强生成中的噪声

【大语言模型】ACL2024论文-38 从信息瓶颈视角有效过滤检索增强生成中的噪声 目录 文章目录 【大语言模型】ACL2024论文-38 从信息瓶颈视角有效过滤检索增强生成中的噪声目录后记 《An Information Bottleneck Perspective for Effective Noise Filtering on Retrieval-Augment…

《火焰烟雾检测开源神经网络模型:智能防火的科技护盾》

一、火灾威胁与检测需求 火灾&#xff0c;始终是高悬在人类社会头顶的 “达摩克利斯之剑”&#xff0c;其带来的灾难后果触目惊心。根据国家消防救援局发布的数据&#xff0c;仅在 2024 年上半年&#xff0c;全国就接报火灾达 31.7 万起 &#xff0c;造成了 1173 人不幸遇难&am…

深入探究Linux树状目录结构

Linux 作为一款广泛使用的开源操作系统&#xff0c;其目录结构采用了树状设计&#xff0c;这种结构清晰、有条理&#xff0c;便于用户和系统进行文件管理与操作。 一、根目录&#xff08;/&#xff09; 根目录是整个 Linux 文件系统的起始点&#xff0c;就像一棵大树的根部&…

【C语言4】数组:一维数组、二维数组、变长数组及数组的练习题

文章目录 前言一、数组的概念二、一维数组2.1. 数组的创建和初始化2.2. 数组的类型2.3. 一维数组的下标2.4. 数组元素的打印和输入2.5. 一维数组在内存中的存储2.6. sizeof 计算数组元素个数 三、二维数组3.1. 二维数组的概念3.1. 二维数组的创建与初始化3.2. 二维数组的下标3.…

图论1-问题 C: 算法7-6:图的遍历——广度优先搜索

题目描述 广度优先搜索遍历类似于树的按层次遍历的过程。其过程为&#xff1a;假设从图中的某顶点v出发&#xff0c;在访问了v之后依次访问v的各个未曾被访问过的邻接点&#xff0c;然后分别从这些邻接点出发依次访问它们的邻接点&#xff0c;并使“先被访问的顶点的邻接点”先…