C++快速回顾(四)

前言

在Android音视频开发中,网上知识点过于零碎,自学起来难度非常大,不过音视频大牛Jhuster提出了《Android 音视频从入门到提高 - 任务列表》,结合我自己的工作学习经历,我准备写一个音视频系列blog。C/C++是音视频必备编程语言,我准备用几篇文章来快速回顾C++。本文是音视频系列blog的其中一个, 对应的要学习的内容是:快速回顾C++的面向对象,模板与泛型编程,相关补充内容。


音视频系列blog

音视频系列blog: 点击此处跳转查看


目录

在这里插入图片描述


1 面向对象

1.1 面向对象简介

在C++中,可以创建类(class),就像是定义了一个对象的模板。这个类可以包含属性(成员变量)和功能(成员函数)。例如,如果要描述一个"狗",可以创建一个"Dog"类,里面有属性如"颜色"、“品种”,还有功能如"叫"、“跑”。

一旦创建了类,就可以通过它来创建具体的对象(实例),就像用模板制造了一个具体的玩具。这些对象可以互相交互,通过调用彼此的功能来完成任务。比如,可以创建两个"Dog"对象,让它们互相比赛跑步,或者让其中一个叫,另一个听到后回应。

这种方式有助于将复杂的问题分解成更小的部分,每个部分都有特定的功能和属性。这使得代码更易于维护和扩展,就像可以随时添加新的房子而不必从头开始一样。

总之,C++面向对象编程就是一种让编程更有组织、更易懂的方法,通过创建类和对象,把现实世界的事物抽象成计算机中的概念,从而更方便地处理复杂的任务和数据。


1.2 定义基类和派生类

1.2.1 定义基类

在C++中,可以使用类来定义基类(Base Class)。基类是一个通用的模板,可以被其他类继承,从而共享其属性和功能。下面是一个简单的例子来演示如何定义一个基类:

#include <iostream>

// 定义一个基类
class Shape {
public:
    // 成员函数来获取形状的名称
    virtual std::string getName() {
        return "Shape";
    }

    // 成员函数来计算面积,基类中暂时没有实现
    virtual double getArea() {
        return 0.0;
    }
};

int main() {
    // 创建一个基类对象
    Shape shape;

    // 输出基类对象的名称
    std::cout << "Shape Name: " << shape.getName() << std::endl;

    // 输出基类对象的面积(注意这里面积是0,因为基类中的计算函数未实现)
    std::cout << "Shape Area: " << shape.getArea() << std::endl;

    return 0;
}

在这个例子中,定义了一个名为Shape的基类。它有两个成员函数,getName()用来获取形状的名称,getArea()用来计算形状的面积(注意,这里面积的计算未在基类中实现,因为不同的形状会有不同的计算方法)。

基类中的成员函数前面使用了virtual关键字,这是为了支持多态性(Polymorphism),这是面向对象编程的重要概念之一。通过在基类中将函数标记为virtual,可以在派生类中进行函数的覆盖(override),从而实现不同派生类的特定行为。

在主函数中,创建了一个Shape类的对象shape,并调用了基类的成员函数来获取名称和计算面积。需要注意的是,在这个例子中,基类的面积计算函数返回了0,因为基类并没有提供具体的计算方法。

这只是一个简单的基类定义示例,实际中基类可以包含更多的属性和功能,供派生类继承和使用。同时,可以创建更多的派生类来继承基类,并在派生类中实现特定的功能和属性。


1.2.2 定义派生类

在C++中,可以通过继承来创建派生类(Derived Class)。派生类是基于一个或多个现有的类(基类)创建的,它可以继承基类的属性和功能,并且可以添加自己特定的属性和功能。下面是一个简单的例子来演示如何定义一个派生类:

#include <iostream>

// 定义一个基类
class Shape {
public:
    virtual std::string getName() {
        return "Shape";
    }

    virtual double getArea() {
        return 0.0;
    }
};

// 定义一个派生类 Circle(圆形),继承自 Shape
class Circle : public Shape {
private:
    double radius;

public:
    Circle(double r) : radius(r) {}

    std::string getName() override {
        return "Circle";
    }

    double getArea() override {
        return 3.14159 * radius * radius;
    }
};

int main() {
    // 创建一个派生类对象 Circle
    Circle circle(5.0);

    // 输出派生类对象的名称
    std::cout << "Circle Name: " << circle.getName() << std::endl;

    // 输出派生类对象的面积
    std::cout << "Circle Area: " << circle.getArea() << std::endl;

    return 0;
}

在这个例子中,首先定义了一个基类Shape,与之前的示例相同。然后,定义了一个派生类Circle,它继承自基类Shape。在派生类中,添加了一个私有成员变量radius(半径),并在构造函数中初始化它。

派生类中重写(override)了基类的getName()getArea()函数,分别提供了特定于圆形的名称和面积计算方法。

在主函数中,创建了一个Circle派生类的对象circle,并调用了派生类的成员函数来获取名称和计算面积。会看到,派生类对象可以使用基类的成员函数,同时也可以调用派生类自己的成员函数。

通过继承和派生,可以构建出更复杂的类层次结构,其中不同的派生类可以共享基类的通用属性和功能,并且还可以添加自己独特的特性。这种结构使得代码的组织和管理变得更加灵活和可扩展。


1.2.3 类型转换与继承

在C++中,类型转换(Type Conversion)和继承(Inheritance)是两个重要的概念,它们在面向对象编程中起到了不同的作用。

类型转换:

类型转换是将一个数据类型转换为另一个数据类型的过程。在C++中,有多种类型转换的方式,包括隐式类型转换和显式类型转换。

  1. 隐式类型转换(Implicit Type Conversion): 有时编译器会自动进行类型转换,以便在表达式中进行计算。这种转换也称为自动类型提升。例如,当将一个int类型和一个double类型相加时,int会隐式地转换为double
  2. 显式类型转换(Explicit Type Conversion): 也称为强制类型转换或类型转换操作符。这是一种由程序员显式地指示编译器进行的类型转换。C++中有以下几种显式类型转换的方式:
    • static_cast: 用于基本类型的转换,以及类之间的向上转换(派生类向基类转换)和部分向下转换(基类向派生类转换)。
    • dynamic_cast: 用于类层次结构中的安全向下转换,只适用于具有虚函数的类。
    • reinterpret_cast: 用于低级别的位级转换,通常用于不同类型的指针或引用之间的转换。
    • const_cast: 用于添加或删除变量的const属性或volatile属性。

继承:

继承是面向对象编程的一个重要概念,它允许创建一个新类(派生类或子类),从一个现有的类(基类或父类)继承属性和方法。派生类可以重用基类的代码,同时也可以添加自己的新属性和方法。

继承关系中,基类和派生类之间有不同的访问权限:

  • public 继承:派生类可以继承基类的publicprotected成员,但无法继承private成员。派生类的对象可以访问基类的public成员。
  • protected 继承:派生类可以继承基类的publicprotected成员,但无法继承private成员。派生类的对象无法直接访问基类的public成员。
  • private 继承:派生类可以继承基类的private成员,但这些成员在派生类中变成了private成员,无法从外部访问。

继承允许创建更具体、更特定的类,而不必从头开始编写代码。可以在派生类中添加新的功能,同时也可以覆盖基类的成员函数,从而实现多态性。多态性允许在编译时不必确定要调用的函数,而是在运行时根据实际对象的类型来决定调用哪个函数。

综上所述,类型转换和继承是C++面向对象编程中的两个重要概念,它们共同构建了面向对象的程序设计模型。类型转换用于在不同数据类型之间进行转换,而继承用于创建类层次结构,从现有类中派生出新的类,共享和扩展功能。

当讨论C++中的类型转换和继承时,可以通过一个具体的示例来更好地理解这两个概念是如何相互作用的。

类型转换和继承的示例:

假设要建立一个几何形状(Shape)的类层次结构,其中有一个基类Shape,以及派生类Circle(圆形)和Rectangle(矩形)。

#include <iostream>

class Shape {
public:
    virtual double area() const {
        return 0.0;
    }
};

class Circle : public Shape {
private:
    double radius;

public:
    Circle(double r) : radius(r) {}

    double area() const override {
        return 3.14159 * radius * radius;
    }
};

class Rectangle : public Shape {
private:
    double width;
    double height;

public:
    Rectangle(double w, double h) : width(w), height(h) {}

    double area() const override {
        return width * height;
    }
};

int main() {
    // 创建不同类型的对象
    Circle circle(5.0);
    Rectangle rectangle(4.0, 6.0);

    // 使用基类指针来指向派生类对象
    Shape* shapePtr1 = &circle;
    Shape* shapePtr2 = &rectangle;

    // 使用动态类型转换进行运行时类型检查和转换
    if (Circle* circlePtr = dynamic_cast<Circle*>(shapePtr1)) {
        std::cout << "Circle Area: " << circlePtr->area() << std::endl;
    }

    if (Rectangle* rectanglePtr = dynamic_cast<Rectangle*>(shapePtr2)) {
        std::cout << "Rectangle Area: " << rectanglePtr->area() << std::endl;
    }

    return 0;
}

在这个示例中,定义了一个基类Shape,以及两个派生类CircleRectangle。每个类都有一个area()函数,用于计算不同形状的面积。

在主函数中,创建了一个Circle对象和一个Rectangle对象,并使用基类指针来指向这些对象。这允许使用多态性,即在运行时根据实际对象的类型来调用相应的成员函数。

使用了动态类型转换操作符dynamic_cast来在运行时检查基类指针所指对象的实际类型,并进行转换。如果转换成功,就可以调用派生类的特定成员函数。

在输出中,计算并打印了圆形和矩形的面积,这证明了多态性和继承的概念如何协同工作,让能够处理不同类型的对象,而无需知道具体的对象类型。

这个示例演示了类型转换和继承如何一起工作,使得可以创建灵活的类层次结构,并在运行时处理不同类型的对象。


1.3 虚函数

在C++中,虚函数(Virtual Function)是面向对象编程的一个重要概念,它用于实现多态性(Polymorphism),允许派生类覆盖基类的函数,并在运行时动态地调用适当的函数实现。

虚函数的主要作用是允许在基类中声明一个函数,然后在派生类中对该函数进行重新定义,从而实现不同派生类的特定行为。这允许通过基类指针或引用来调用派生类中的函数,从而在运行时根据实际对象的类型来调用正确的函数。

使用虚函数的步骤如下:

  1. 在基类中声明虚函数,使用关键字 virtual
  2. 在派生类中重新定义(override)基类中的虚函数,使用相同的函数签名(函数名和参数列表)。
  3. 使用基类指针或引用来调用虚函数,运行时会根据实际对象的类型来调用适当的函数实现。

以下是一个简单的示例,演示了虚函数的用法:

#include <iostream>

class Shape {
public:
    virtual void display() const {
        std::cout << "This is a Shape." << std::endl;
    }
};

class Circle : public Shape {
public:
    void display() const override {
        std::cout << "This is a Circle." << std::endl;
    }
};

class Rectangle : public Shape {
public:
    void display() const override {
        std::cout << "This is a Rectangle." << std::endl;
    }
};

int main() {
    Circle circle;
    Rectangle rectangle;

    // 使用基类指针来调用虚函数,实现多态性
    Shape* shapePtr1 = &circle;
    Shape* shapePtr2 = &rectangle;

    shapePtr1->display(); // 调用 Circle 类的 display 函数
    shapePtr2->display(); // 调用 Rectangle 类的 display 函数

    return 0;
}

在这个示例中,定义了一个基类 Shape,并在其内部声明了一个虚函数 display()。然后,分别创建了 CircleRectangle 派生类,对 display() 函数进行了重定义。

main() 函数中,使用基类指针来指向 CircleRectangle 对象,然后通过调用虚函数 display() 来实现多态性。根据实际对象的类型,会调用相应的派生类中的 display() 函数。

这个例子展示了虚函数如何使得可以在基类和派生类之间实现多态性,允许在运行时根据对象的实际类型来动态调用正确的函数实现。


1.4 抽象基类

在C++中,抽象基类(Abstract Base Class)是一个含有至少一个纯虚函数的类,它不能被实例化(不能创建对象),而只能被其他派生类继承并实现其纯虚函数。抽象基类通常用于定义一个通用的接口,而具体的实现由派生类完成。

以下是创建抽象基类的步骤和示例:

  1. 在基类中至少声明一个纯虚函数(使用 virtual 关键字和 = 0),表示该函数没有实现,必须在派生类中进行重定义。
  2. 抽象基类无法被实例化,只能作为其他类的基类,供派生类继承。
  3. 派生类必须实现基类的所有纯虚函数,以便成为一个具体的类。

下面是一个抽象基类的示例,以及一个派生类实现的示例:

#include <iostream>

// 抽象基类 Shape
class Shape {
public:
    // 纯虚函数,需要在派生类中实现
    virtual double area() const = 0;
    virtual void display() const = 0;
};

// 派生类 Circle
class Circle : public Shape {
private:
    double radius;

public:
    Circle(double r) : radius(r) {}

    double area() const override {
        return 3.14159 * radius * radius;
    }

    void display() const override {
        std::cout << "This is a Circle." << std::endl;
    }
};

int main() {
    // 无法创建抽象基类的对象
    // Shape shape;  // 错误!

    // 创建派生类的对象
    Circle circle(5.0);

    // 使用抽象基类指针来调用纯虚函数
    Shape* shapePtr = &circle;

    std::cout << "Circle Area: " << shapePtr->area() << std::endl;
    shapePtr->display();

    return 0;
}

在这个示例中,定义了一个抽象基类 Shape,其中包含两个纯虚函数 area()display()。然后,创建了一个派生类 Circle,它继承了 Shape,并实现了基类的纯虚函数。

main() 函数中,创建了一个 Circle 对象,并使用抽象基类指针来调用纯虚函数。注意,无法直接创建抽象基类的对象,但可以使用抽象基类指针或引用来操作派生类的对象。

抽象基类的主要目的是定义一个通用的接口,以及一些基本的行为,然后由具体的派生类来实现特定的功能。这种设计模式在面向对象编程中很常见,可以帮助实现多态性和代码的组织。


1.5 访问控制与继承

C++中的访问控制和继承是面向对象编程中的两个重要概念,它们共同定义了类的成员的可见性和访问权限,以及派生类如何继承和使用基类的成员。

访问控制:

C++中有三种访问控制修饰符,用于控制类的成员对外部的可见性:

  1. public:成员在类内外均可访问。
  2. protected:成员在类内可访问,在派生类中也可以访问。
  3. private:成员仅在类内部可访问,派生类中不可直接访问。

默认情况下,类的成员是private的。可以使用上述访问控制修饰符来明确指定成员的可见性。

继承:

继承是一种创建新类的机制,通过继承,派生类可以获取基类的成员,并在此基础上添加新的成员或修改继承的成员。

在C++中,有三种继承方式:

  1. public 继承:基类的publicprotected成员在派生类中保持相同的访问权限,private成员在派生类中不可直接访问。
  2. protected 继承:基类的publicprotected成员在派生类中变为protectedprivate成员在派生类中不可直接访问。
  3. private 继承:基类的publicprotectedprivate成员在派生类中都变为private,不可直接访问。

继承时,可以通过使用访问控制修饰符来显式指定继承方式。

示例:

#include <iostream>

class Base {
public:
    int publicVar;
    Base() : publicVar(1), protectedVar(2), privateVar(3) {}

protected:
    int protectedVar;

private:
    int privateVar;
};

class Derived : public Base {
public:
    void accessBaseMembers() {
        std::cout << "Public in Base: " << publicVar << std::endl;
        std::cout << "Protected in Base: " << protectedVar << std::endl;
        // std::cout << "Private in Base: " << privateVar << std::endl;  // 错误!无法访问
    }
};

int main() {
    Derived derivedObj;
    derivedObj.accessBaseMembers();

    return 0;
}

在这个示例中,定义了一个基类Base,其中包含不同访问权限的成员变量。然后,创建了一个派生类Derived,使用public继承方式。在派生类中,可以直接访问基类的publicprotected成员,但无法直接访问private成员。

主函数中,创建了一个Derived对象,并通过成员函数accessBaseMembers()来访问基类的不同成员。从输出中可以看出,能够访问publicprotected成员,但无法访问private成员。

总之,C++中的访问控制和继承共同定义了类的成员的可见性和派生类对基类成员的继承关系。使用不同的访问控制修饰符和继承方式,可以实现类的成员的灵活组织和访问。


1.6 继承中的类作用域

在C++中,继承关系中的类作用域是指派生类如何访问和使用基类的成员(变量、函数等)。派生类可以在自己的作用域中访问从基类继承而来的成员,但不同的成员访问权限可能会有所不同。

继承中的类作用域受到访问控制修饰符的影响,这些修饰符决定了基类成员在派生类中的可见性。有三种主要的访问控制修饰符:publicprotectedprivate

以下是继承中类作用域的一些重要概念和示例:

  1. public 继承: 基类的public成员在派生类中保持为publicprotected成员保持为protectedprivate成员不可直接访问。
class Base {
public:
    int publicVar;
    void publicFunction() {}
    
protected:
    int protectedVar;
    void protectedFunction() {}

private:
    int privateVar;
    void privateFunction() {}
};

class Derived : public Base {
    // 在 Derived 中,publicVar 和 publicFunction 可以直接访问
    // protectedVar 和 protectedFunction 也可以直接访问
    // privateVar 和 privateFunction 不可直接访问
};
  1. protected 继承: 基类的publicprotected成员在派生类中变为protectedprivate成员不可直接访问。
class Derived : protected Base {
    // 在 Derived 中,publicVar、protectedVar、publicFunction 和 protectedFunction 可以直接访问
    // privateVar 和 privateFunction 不可直接访问
};
  1. private 继承: 基类的publicprotectedprivate成员在派生类中都变为private,不可直接访问。
class Derived : private Base {
    // 在 Derived 中,publicVar、protectedVar、publicFunction 和 protectedFunction 都不可直接访问
    // privateVar 和 privateFunction 不可直接访问
};

需要注意的是,即使在派生类的作用域内,基类的成员仍然遵循原始的访问控制规则。例如,派生类内部的成员函数可以直接访问基类的protected成员,但不能访问private成员。

继承中的类作用域影响了派生类如何使用基类的成员,这在设计类层次结构时非常重要。通过合理地选择继承方式和访问控制修饰符,可以实现类的封装和组织,以及实现继承的目标。


1.7 构造函数与拷贝控制

1.7.1 虚析构函数

在C++中,构造函数、拷贝控制和虚析构函数是面向对象编程中的重要概念,它们涉及对象的创建、复制和销毁。

构造函数:

构造函数用于在创建对象时初始化对象的成员变量,它是一个特殊的成员函数。每个类可以有多个构造函数,根据不同的参数列表可以实现不同的初始化方式。构造函数没有返回类型,其名称与类名相同。

拷贝控制:

拷贝控制涉及到对象的复制、移动和销毁。C++11引入了特殊的成员函数,包括拷贝构造函数、移动构造函数、拷贝赋值运算符和移动赋值运算符。这些函数允许在对象之间进行复制和移动,以及定义对象的销毁方式。如果不显式提供这些函数,编译器会自动生成默认的版本。

虚析构函数:

虚析构函数是一个在基类中声明为虚函数的析构函数。当在基类中使用虚析构函数时,它允许在派生类对象上正确地销毁对象,并释放相应的资源。这在使用多态性时特别重要。如果不使用虚析构函数,可能会导致在派生类对象上出现内存泄漏。

以下是一个示例,演示了构造函数、拷贝控制和虚析构函数的用法:

#include <iostream>

class Base {
public:
    Base() {
        std::cout << "Base Constructor" << std::endl;
    }

    Base(const Base& other) {
        std::cout << "Base Copy Constructor" << std::endl;
    }

    Base& operator=(const Base& other) {
        std::cout << "Base Copy Assignment Operator" << std::endl;
        return *this;
    }

    virtual ~Base() {
        std::cout << "Base Destructor" << std::endl;
    }
};

class Derived : public Base {
public:
    Derived() {
        std::cout << "Derived Constructor" << std::endl;
    }

    Derived(const Derived& other) {
        std::cout << "Derived Copy Constructor" << std::endl;
    }

    Derived& operator=(const Derived& other) {
        std::cout << "Derived Copy Assignment Operator" << std::endl;
        return *this;
    }

    ~Derived() override {
        std::cout << "Derived Destructor" << std::endl;
    }
};

int main() {
    Base base;
    Base baseCopy = base;

    Derived derived;
    Derived derivedCopy = derived;

    Base* basePtr = new Derived;
    delete basePtr;

    return 0;
}

在这个示例中,定义了一个基类 Base 和一个派生类 Derived。实现了构造函数、拷贝构造函数、拷贝赋值运算符和虚析构函数,并在主函数中创建了对象和指针,以演示它们的行为。

注意输出中不同函数的调用顺序,这有助于理解构造、拷贝和析构的过程。特别地,虚析构函数允许在派生类对象上正确地销毁对象,确保适当的析构函数被调用。


1.7.2 合成拷贝控制与继承

C++中的构造函数和拷贝控制(拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符)对于管理对象的创建、复制和销毁非常重要。合成拷贝控制是指当没有显式提供拷贝构造函数和拷贝赋值运算符时,编译器会自动生成默认的版本。

继承在这一过程中也扮演着重要的角色,因为派生类的拷贝控制函数可以影响基类的拷贝控制,从而影响派生类对象的复制行为。

合成拷贝控制:

当没有显式提供拷贝构造函数和拷贝赋值运算符时,编译器会自动生成默认的版本。这些默认的合成拷贝控制函数会按照逐个成员逐个成员的方式进行复制。如果类的成员是内置类型或类类型,它们的合成拷贝控制会递归地调用。

class MyClass {
    // 编译器生成的合成拷贝构造函数和拷贝赋值运算符
};

继承和合成拷贝控制:

当一个派生类没有显式提供拷贝构造函数和拷贝赋值运算符,但基类有这些函数时,编译器会自动生成派生类的合成拷贝构造函数和拷贝赋值运算符。派生类的合成拷贝控制会调用基类的对应函数来复制基类部分。

class Base {
    // 基类的合成拷贝构造函数和拷贝赋值运算符
};

class Derived : public Base {
    // 派生类的合成拷贝构造函数和拷贝赋值运算符
};

需要注意的是,合成拷贝控制函数只会复制对象的成员,而不会对对象内的指针等进行深层复制。这可能会导致浅层复制的问题,当多个对象共享同一资源时,可能会造成意外的行为。

如果需要进行深层复制或有特定的拷贝控制需求,可能需要自己显式提供拷贝构造函数和拷贝赋值运算符,并在其中实现适当的复制逻辑。

综上所述,合成拷贝控制是C++中一种自动生成的机制,用于管理对象的复制行为。在继承中,派生类的合成拷贝控制会调用基类的相应函数来复制基类部分。如果需要更复杂的拷贝控制逻辑,可以显式提供自定义的拷贝构造函数和拷贝赋值运算符。


1.7.3 派生类的拷贝控制成员

派生类的拷贝控制成员包括拷贝构造函数、拷贝赋值运算符以及析构函数。这些成员函数在派生类中的实现会影响基类部分和派生类自身的对象复制、赋值和销毁行为。

1. 拷贝构造函数:

派生类的拷贝构造函数负责创建一个新对象,并初始化它的成员,包括基类部分和派生类部分。派生类的拷贝构造函数可以显式调用基类的拷贝构造函数来初始化基类部分。

Derived(const Derived& other) : Base(other), /* 初始化派生类部分 */ {
    // 拷贝构造逻辑
}

2. 拷贝赋值运算符:

派生类的拷贝赋值运算符用于将一个对象的值复制给另一个对象。同样,派生类的拷贝赋值运算符应该考虑基类部分和派生类部分的赋值。

Derived& operator=(const Derived& other) {
    if (this != &other) {
        Base::operator=(other); // 赋值基类部分
        // 赋值派生类部分
    }
    return *this;
}

3. 析构函数:

派生类的析构函数用于销毁对象并释放资源。通常,派生类的析构函数会自动调用基类的析构函数,以确保正确释放基类部分的资源。

~Derived() {
    // 派生类析构逻辑
}

总之,派生类的拷贝控制成员需要考虑到基类部分和派生类部分的复制、赋值和销毁。基类部分的拷贝和销毁可以使用派生类中的拷贝构造函数、拷贝赋值运算符和析构函数来处理。


1.7.4 继承的构造函数

C++11引入了继承的构造函数(Inherited Constructors)的概念,使得派生类可以继承基类的构造函数。这样做可以减少代码重复,方便地使用基类的构造函数来初始化派生类的对象。

继承的构造函数允许派生类使用基类的构造函数,从而创建派生类对象时可以选择性地传递相同的参数。这在构造函数的重载和多态性方面具有很大的优势。

继承的构造函数的基本概念:

  • 派生类可以使用using关键字继承基类的构造函数,通过这种方式,基类的构造函数会变成派生类的构造函数,可以在派生类中直接使用。
  • 继承的构造函数也可以进行重载,从而提供不同的构造方式。
  • 继承的构造函数在编译器生成派生类的构造函数时,会逐个成员逐个成员地初始化,其中包括基类部分和派生类部分。

示例:

class Base {
public:
    Base(int value) {
        // 初始化基类部分
    }
};

class Derived : public Base {
public:
    using Base::Base; // 继承基类的构造函数

    // 可以添加派生类特有的构造函数
    Derived(double value) : Base(static_cast<int>(value)) {
        // 初始化派生类部分
    }
};

int main() {
    Derived d1(5);     // 使用基类构造函数初始化派生类对象
    Derived d2(3.14);  // 使用派生类构造函数初始化派生类对象

    return 0;
}

在这个示例中,Derived类继承了Base类的构造函数。通过使用using Base::Base;语句,Derived类可以直接使用Base类的构造函数。然后,在Derived类中添加了一个派生类特有的构造函数,该构造函数接受double类型的参数,然后通过强制类型转换将其传递给基类的构造函数。

通过继承基类的构造函数,在派生类中不仅可以重用基类的构造函数,还可以添加额外的构造函数,以满足派生类特有的需求。

总之,继承的构造函数是C++11引入的一个便捷的特性,允许派生类直接使用基类的构造函数,从而减少了代码重复和构造函数的维护成本。


1.8 容器与继承

C++标准库提供了多种容器(Containers)来存储和管理数据。容器是一种数据结构,用于在内存中存储多个元素,并提供了访问、插入、删除等操作的接口。在使用容器时,继承也可能发挥一定的作用。

1. 容器的种类:

C++标准库提供了多种容器,包括但不限于:

  • std::vector:动态数组,可以自动扩展大小。
  • std::list:双向链表。
  • std::mapstd::unordered_map:关联容器,用于键值对的存储。
  • std::setstd::unordered_set:存储唯一元素的容器。
  • std::deque:双端队列。
  • 等等…

2. 继承和容器:

在使用容器时,通常情况下不直接涉及继承关系。容器用于存储数据,而继承关系用于设计类层次结构。然而,在某些情况下,继承和容器可能会有一些相互影响。

3. 容器元素的继承:

容器可以存储各种类型的元素,包括基本类型、自定义类对象等。如果存储的元素是派生类对象,那么容器会在内部创建这些对象的副本。这意味着容器中的对象与原始对象之间是独立的,修改容器中的对象不会影响原始对象。

4. 容器和多态性:

如果容器存储的是指向基类的指针或智能指针,而基类中有虚函数,那么在遍历容器元素时可以实现多态性。这允许在运行时根据对象的实际类型来调用正确的函数实现。

#include <iostream>
#include <vector>

class Shape {
public:
    virtual void draw() const {
        std::cout << "Drawing a shape." << std::endl;
    }
};

class Circle : public Shape {
public:
    void draw() const override {
        std::cout << "Drawing a circle." << std::endl;
    }
};

int main() {
    std::vector<Shape*> shapes;
    shapes.push_back(new Circle);

    for (const auto& shape : shapes) {
        shape->draw(); // 多态性调用
    }

    // 清理内存
    for (const auto& shape : shapes) {
        delete shape;
    }

    return 0;
}

在这个示例中,使用了一个存储基类指针的std::vector容器,其中存储了一个派生类Circle的对象指针。在遍历容器元素时,通过基类指针的多态性调用了正确的draw()函数。

总之,C++容器和继承是两个独立的概念,但在某些情况下它们可能会交叉使用。容器用于存储数据,而继承用于设计类层次结构。在存储派生类对象时,容器会进行适当的复制或存储指针,而在使用容器进行遍历时,可以利用继承实现多态性。


2 模板与泛型编程

2.1 定义模板

C++中的模板是一种通用的编程工具,用于创建可以适用于多种数据类型的函数、类和数据结构。模板允许编写通用的代码,以便在需要时根据特定的数据类型进行实例化。

1. 函数模板:

函数模板是一种定义通用函数的方式,可以根据参数的类型自动推导返回值类型和参数类型。函数模板以关键字 template 开始,后跟模板参数列表和函数定义。

template <typename T>
T Max(T a, T b) {
    return (a > b) ? a : b;
}

2. 类模板:

类模板是一种定义通用类的方式,可以根据特定的数据类型创建不同类型的类。类模板以关键字 template 开始,后跟模板参数列表和类定义。

template <typename T>
class Stack {
private:
    T elements[100];
    int top;

public:
    // 类成员和方法定义
};

3. 模板参数:

模板参数指定了在模板中可以用于通用编程的数据类型或值。模板参数可以是类型参数(如 typename T)或非类型参数(如 int N)。

4. 成员模板:

成员模板是在类或结构体内部定义的模板函数,用于为类模板中的成员函数提供通用的实现。

template <typename T>
class MyContainer {
public:
    template <typename U>
    void Add(U value) {
        // 实现
    }
};

5. 控制实例化:

模板代码不会被编译器直接翻译成机器代码,而是在使用时进行实例化。控制实例化是指选择何时将模板代码实例化成特定的类型。

在模板的使用过程中,编译器会根据需要自动对模板进行实例化。也可以显式地要求编译器进行实例化。

template class MyContainer<int>;  // 显式实例化

综上所述,C++中的模板允许编写通用的函数和类,用于处理不同类型的数据。函数模板和类模板提供了通用的框架,可以根据不同的数据类型进行实例化。模板参数、成员模板和控制实例化是在使用模板时的一些重要概念和技术。


2.2 模板实参推断

C++模板实参推断是指在使用模板函数或类时,编译器自动确定模板参数的类型。模板实参推断是模板的一个重要特性,它使得使用模板更加方便和灵活。

1. 类型转换与模板类型参数:

当使用模板函数或类时,编译器会尝试将实参类型转换为模板类型参数,以便进行匹配。这涉及到隐式类型转换和派生类到基类的转换。

2. 函数模板显式实参:

在使用函数模板时,有时可以显式地提供模板实参,以明确指定模板参数的类型。

template <typename T>
T Add(T a, T b) {
    return a + b;
}

int result = Add<int>(2, 3); // 显式指定模板参数类型

3. 尾置返回类型与类型转换:

C++11引入了尾置返回类型(trailing return type),可以在函数声明的尾部指定返回类型,这在模板函数中特别有用。

template <typename T, typename U>
auto Multiply(T a, U b) -> decltype(a * b) {
    return a * b;
}

4. 函数指针和实参推断:

使用函数指针作为函数模板的参数时,编译器会根据函数指针的类型进行实参推断,这允许在函数模板中使用不同类型的函数。

template <typename Func>
void PerformOperation(Func operation, int a, int b) {
    int result = operation(a, b);
    // ...
}

int Add(int x, int y) {
    return x + y;
}

int Multiply(int x, int y) {
    return x * y;
}

int main() {
    PerformOperation(Add, 2, 3);
    PerformOperation(Multiply, 4, 5);

    return 0;
}

5. 模板实参推断和引用:

在模板函数中,实参推断适用于引用类型。编译器会根据传递的实参类型来推断模板参数的引用类型。

template <typename T>
void ModifyAndPrint(T& value) {
    ++value;
    std::cout << value << std::endl;
}

int main() {
    int num = 5;
    ModifyAndPrint(num);

    return 0;
}

6. std::move:

std::move 是C++标准库中的一个函数模板,用于将左值转换为右值引用,常用于支持移动语义。它允许在避免不必要的内存拷贝的情况下将资源转移到另一个对象。

#include <utility>

int main() {
    int x = 42;
    int y = std::move(x); // 将x的值移动给y,x的值变为不确定

    return 0;
}

总之,C++模板实参推断是使模板代码通用和灵活的重要特性。它允许编译器根据传递的实参类型自动确定模板参数的类型。在使用模板时,可以使用显式实参、尾置返回类型、函数指针等技术来更精确地控制模板参数的推断。


2.3 重载与模板

C++中的重载(Overloading)和模板(Templates)是两种不同的编程概念,它们都有助于实现代码的灵活性和复用性。

重载(Overloading):

函数重载是指在同一作用域内,可以有多个同名的函数,但它们的参数列表不同。重载函数根据传递的参数类型或数量来确定调用哪个函数。重载可以用于创建一组功能相似但参数不同的函数,使得函数命名更加直观。

int Add(int a, int b) {
    return a + b;
}

double Add(double a, double b) {
    return a + b;
}

模板(Templates):

模板是一种通用的编程机制,用于创建通用的函数、类或数据结构,以适应不同类型的数据。模板的参数可以是类型参数或非类型参数,它们在使用时被替换为实际的类型或值。模板允许在编译时生成多个函数或类的实例,以适应不同的类型。

template <typename T>
T Max(T a, T b) {
    return (a > b) ? a : b;
}

重载与模板的关系:

重载和模板可以结合使用,即可以对模板进行重载。这种情况下,根据参数的类型和数量,编译器会选择适合的函数或模板来执行。如果找不到匹配的重载函数,编译器会考虑模板进行实例化。

template <typename T>
T Add(T a, T b) {
    return a + b;
}

int Add(int a, int b) {
    return a + b + 10;
}

在这个示例中,当调用 Add(5, 6) 时,由于有一个匹配的重载函数,编译器会选择重载函数。如果调用 Add(3.14, 2.71),则编译器会实例化模板,因为没有精确匹配的重载函数。

总之,重载和模板是C++中的两种不同的代码重用机制。重载允许创建具有相同函数名但不同参数的函数,而模板允许创建通用的函数或类,以适应不同类型的数据。这两种机制可以结合使用,使能够更灵活地处理不同类型的需求。


2.4 可变参数模板

C++11引入了可变参数模板(Variadic Templates)的概念,它允许编写接受可变数量的参数的模板函数或类,从而实现更加通用和灵活的代码。

1. 编写可变参数函数模板:

可变参数函数模板使用递归的方式来处理参数包,直到参数包为空为止。以下是一个示例,展示如何编写可变参数模板来计算参数的和:

#include <iostream>

template <typename T>
T Sum(T value) {
    return value;
}

template <typename T, typename... Args>
T Sum(T first, Args... rest) {
    return first + Sum(rest...);
}

int main() {
    std::cout << Sum(1, 2, 3, 4, 5) << std::endl;
    std::cout << Sum(3.14, 2.71, 1.618) << std::endl;
    
    return 0;
}

2. 包扩展:

包扩展(Pack Expansion)允许在模板中展开参数包,以进行更复杂的操作。使用...来展开参数包。

template <typename... Args>
void PrintArgs(Args... args) {
    (std::cout << ... << args) << std::endl;
}

int main() {
    PrintArgs(1, 2, "hello", 3.14);
    
    return 0;
}

3. 转发参数包:

参数包的转发允许将参数包传递给另一个函数。使用...来将参数包转发给函数。

template <typename... Args>
void ForwardArgs(Args... args) {
    AnotherFunction(args...);
}

以上只是可变参数模板的基本概念和示例,实际使用中可以根据需求进行更复杂的操作和逻辑。可变参数模板在实现通用的、灵活的函数和类时非常有用,它使能够处理不同数量和类型的参数,从而减少代码的冗余性。


3 C++补充1

3.1 tuple 类型

在C++中,std::tuple 是一个标准库类模板,用于存储不同类型的值,类似于一个可以容纳多个元素的容器。std::tuple 可以用于返回多个值,传递多个参数,以及在需要多个不同类型的数据时提供灵活性。

1. 定义和初始化 std::tuple

使用 std::tuple 之前,需要包含 <tuple> 头文件。定义和初始化 std::tuple 可以通过以下方式:

#include <tuple>
#include <iostream>

int main() {
    std::tuple<int, double, std::string> myTuple(42, 3.14, "hello");

    // 或者使用 make_tuple
    auto anotherTuple = std::make_tuple(10, 2.71, "world");

    return 0;
}

2. 使用 std::tuple 返回多个值:

可以使用 std::tuple 来从函数中返回多个值。以下是一个返回多个值的函数示例:

#include <tuple>
#include <iostream>

std::tuple<int, double, std::string> GetData() {
    return std::make_tuple(42, 3.14, "data");
}

int main() {
    auto result = GetData();
    int intValue;
    double doubleValue;
    std::string stringValue;

    std::tie(intValue, doubleValue, stringValue) = result;

    std::cout << "int: " << intValue << ", double: " << doubleValue << ", string: " << stringValue << std::endl;

    return 0;
}

在上面的示例中,GetData() 函数返回一个 std::tuple,然后使用 std::tie 将返回的值解包到不同的变量中。

3. 使用结构化绑定:

C++17 引入了结构化绑定(Structured Bindings)特性,可以更方便地从 std::tuple 中解包值:

#include <tuple>
#include <iostream>

int main() {
    std::tuple<int, double, std::string> myTuple(42, 3.14, "hello");

    auto [intValue, doubleValue, stringValue] = myTuple;

    std::cout << "int: " << intValue << ", double: " << doubleValue << ", string: " << stringValue << std::endl;

    return 0;
}

结构化绑定允许通过自动的方式将 std::tuple 中的值解包到对应的变量中,更加简洁和直观。

总之,std::tuple 类型是C++标准库中用于存储不同类型的值的类模板,它可以用于返回多个值、传递多个参数,以及提供更灵活的数据结构。可以使用 std::make_tuple 创建和初始化 std::tuple,并通过 std::tie 或结构化绑定来从 std::tuple 中获取值。


3.2 bitset 类型

在C++中,std::bitset 是一个标准库类模板,用于表示固定大小的二进制位序列,可以进行位操作。std::bitset 是一种非常有效的方式来处理位操作,例如对位字段进行操作,或者用于编码布尔值。

1. 定义和初始化 std::bitset

使用 std::bitset 之前,需要包含 <bitset> 头文件。定义和初始化 std::bitset 可以通过以下方式:

#include <bitset>
#include <iostream>

int main() {
    std::bitset<8> bits1;  // 创建一个8位的二进制位序列,所有位初始化为0
    std::bitset<8> bits2(0b10101010);  // 使用二进制字面值初始化
    std::bitset<8> bits3("11001100"); // 使用字符串初始化

    return 0;
}

2. std::bitset 操作:

std::bitset 支持多种位操作,例如设置、清除、翻转和测试位。以下是一些常用的位操作示例:

#include <bitset>
#include <iostream>

int main() {
    std::bitset<8> bits(0b10101010);

    bits.set(2);      // 设置第2位为1
    bits.reset(5);    // 将第5位重置为0
    bits.flip(7);     // 翻转第7位的值

    bool isSet = bits.test(4); // 测试第4位是否为1

    std::cout << bits << std::endl; // 打印二进制位序列

    return 0;
}

3. std::bitset 高级操作:

std::bitset 还支持按位操作,例如按位与、按位或和按位异或。

#include <bitset>
#include <iostream>

int main() {
    std::bitset<8> bits1(0b10101010);
    std::bitset<8> bits2(0b11001100);

    std::bitset<8> result = bits1 & bits2; // 按位与操作
    std::bitset<8> result2 = bits1 | bits2; // 按位或操作
    std::bitset<8> result3 = bits1 ^ bits2; // 按位异或操作

    std::cout << result << std::endl;
    std::cout << result2 << std::endl;
    std::cout << result3 << std::endl;

    return 0;
}

总之,std::bitset 类型是C++标准库中用于表示固定大小的二进制位序列的类模板。可以使用 std::bitset 进行位操作,包括设置、清除、翻转、测试以及按位操作等。这使得可以方便地进行位操作,处理二进制数据和位字段。


3.3 正则表达式

C++标准库提供了对正则表达式的支持,可以通过 <regex> 头文件来使用正则表达式功能。正则表达式允许通过一种模式来匹配和操作字符串,用于文本处理、搜索和替换等操作。

1. 匹配字符串:

可以使用 std::regex_match 函数来检查一个字符串是否与正则表达式匹配。

#include <iostream>
#include <regex>

int main() {
    std::string input = "Hello, world!";
    std::regex pattern("Hello.*");

    if (std::regex_match(input, pattern)) {
        std::cout << "Match found." << std::endl;
    } else {
        std::cout << "No match found." << std::endl;
    }

    return 0;
}

2. 搜索和替换:

使用 std::regex_search 函数可以在字符串中搜索满足正则表达式的子串。使用 std::regex_replace 函数可以将满足正则表达式的子串替换为指定的内容。

#include <iostream>
#include <regex>

int main() {
    std::string input = "The quick brown fox jumps over the lazy dog.";
    std::regex pattern("fox");

    if (std::regex_search(input, pattern)) {
        std::cout << "Pattern found." << std::endl;
    } else {
        std::cout << "Pattern not found." << std::endl;
    }

    std::string replaced = std::regex_replace(input, pattern, "cat");
    std::cout << "Replaced: " << replaced << std::endl;

    return 0;
}

3. 正则表达式选项:

可以在正则表达式的模式字符串中添加选项来控制匹配的方式,例如大小写敏感或不敏感、多行匹配等。使用 std::regex 的第二个参数来传递这些选项。

#include <iostream>
#include <regex>

int main() {
    std::string input = "This is a Test String.";
    std::regex pattern("test", std::regex_constants::icase); // 忽略大小写

    if (std::regex_search(input, pattern)) {
        std::cout << "Pattern found." << std::endl;
    } else {
        std::cout << "Pattern not found." << std::endl;
    }

    return 0;
}

4. 正则表达式捕获组:

正则表达式允许使用捕获组来提取匹配的部分。可以使用 std::smatch 来获取捕获的结果。

#include <iostream>
#include <regex>

int main() {
    std::string input = "Name: John, Age: 30";
    std::regex pattern("Name: (\\w+), Age: (\\d+)");

    std::smatch matches;
    if (std::regex_search(input, matches, pattern)) {
        std::cout << "Name: " << matches[1] << ", Age: " << matches[2] << std::endl;
    } else {
        std::cout << "Pattern not found." << std::endl;
    }

    return 0;
}

总之,C++标准库的正则表达式功能可以帮助处理字符串的匹配、搜索和替换等操作。可以使用 std::regex 头文件中的函数和类来操作正则表达式,同时也可以使用正则表达式选项和捕获组来满足不同的需求。


3.4 随机数

在C++中,可以使用标准库中的 <random> 头文件来生成随机数。C++提供了一组随机数生成器和分布器,用于生成不同类型和范围的随机数。

1. 随机数生成器(Random Number Engines):

随机数生成器是一种用于生成伪随机数序列的设备。C++标准库提供了多个随机数生成器,其中最常用的是 std::mt19937(Mersenne Twister)。

#include <iostream>
#include <random>

int main() {
    std::random_device rd; // 获取一个真随机数种子
    std::mt19937 generator(rd()); // 使用种子初始化随机数生成器

    int randomValue = generator(); // 生成随机整数
    std::cout << "Random value: " << randomValue << std::endl;

    return 0;
}

2. 分布器(Random Number Distributions):

分布器将随机数生成器生成的整数映射到特定范围内的随机数。例如,可以使用 std::uniform_int_distribution 生成均匀分布的整数,或者使用 std::normal_distribution 生成正态分布的随机数。

#include <iostream>
#include <random>

int main() {
    std::random_device rd;
    std::mt19937 generator(rd());
    
    std::uniform_int_distribution<int> distribution(1, 6); // 生成1到6之间的均匀分布整数

    int randomValue = distribution(generator);
    std::cout << "Random value: " << randomValue << std::endl;

    return 0;
}

3. 随机数范围:

可以使用分布器来生成特定范围内的随机数。

#include <iostream>
#include <random>

int main() {
    std::random_device rd;
    std::mt19937 generator(rd());

    std::uniform_real_distribution<double> distribution(0.0, 1.0); // 生成0到1之间的均匀分布实数

    double randomValue = distribution(generator);
    std::cout << "Random value: " << randomValue << std::endl;

    return 0;
}

总之,C++标准库的 <random> 头文件提供了随机数生成器和分布器,用于生成不同类型和范围的随机数。通过使用随机数生成器和分布器,可以生成具有不同分布的随机数,以满足的随机数需求。


3.5 异常处理

C++异常处理机制允许在程序运行过程中处理错误情况,以提高程序的健壮性和容错性。

1. 抛出异常:

使用 throw 关键字可以抛出一个异常。异常通常是由函数在发生错误或无法正常执行时抛出的。

#include <iostream>

int Divide(int a, int b) {
    if (b == 0) {
        throw std::runtime_error("Divide by zero");
    }
    return a / b;
}

int main() {
    try {
        int result = Divide(10, 0);
    } catch (const std::exception& e) {
        std::cout << "Exception: " << e.what() << std::endl;
    }

    return 0;
}

2. 捕获异常:

使用 trycatch 关键字可以捕获并处理异常。在 catch 块中,可以处理不同类型的异常并采取相应的措施。

try {
    // 可能抛出异常的代码
} catch (const std::exception& e) {
    // 处理异常
}

3. 函数 try 语句块与构造函数:

函数 try 语句块允许在构造函数中捕获异常,以确保对象在构造过程中不会泄漏资源。

class MyObject {
public:
    MyObject() try {
        // 构造过程中可能抛出异常的代码
    } catch (...) {
        // 处理异常
    }
    // 析构函数和其他成员函数
};

4. noexcept 异常说明:

noexcept 是一个异常说明操作符,用于声明一个函数是否会抛出异常。当函数被声明为 noexcept,表示该函数不会抛出任何异常。

void MyFunction() noexcept {
    // 不会抛出异常的代码
}

5. 异常类层次:

可以自定义异常类,通过继承标准异常类或其他自定义异常类来建立异常类层次结构,以便更好地组织和处理异常。

class MyException : public std::exception {
public:
    MyException(const char* msg) : message(msg) {}
    virtual const char* what() const noexcept {
        return message.c_str();
    }

private:
    std::string message;
};

在处理异常时,可以根据异常类的类型来执行不同的操作。

try {
    if (some_condition) {
        throw MyException("Custom exception");
    }
} catch (const std::exception& e) {
    std::cout << "Exception: " << e.what() << std::endl;
} catch (const MyException& e) {
    std::cout << "Custom Exception: " << e.what() << std::endl;
}

总之,C++异常处理机制允许在代码中处理错误情况,提高程序的容错性。可以使用 throw 抛出异常,使用 trycatch 捕获异常,并使用异常类层次结构来更好地组织和处理不同类型的异常。同时,函数 try 语句块和 noexcept 异常说明也提供了更多的异常处理工具。


3.6 命名空间

C++中的命名空间(Namespace)是一种用来组织和管理程序中各种标识符(例如变量、函数、类等)的机制,以避免命名冲突和提供更好的代码组织。

1. 命名空间定义:

命名空间可以用来创建一个封闭的、独立的名称空间,其中可以包含各种标识符。

namespace MyNamespace {
    int x = 10;
    void Print() {
        std::cout << "Hello from MyNamespace!" << std::endl;
    }
}

2. 使用命名空间成员:

通过使用作用域解析运算符 ::,可以访问命名空间中的成员。

#include <iostream>

int main() {
    std::cout << MyNamespace::x << std::endl;
    MyNamespace::Print();

    return 0;
}

3. 类、命名空间与作用域:

命名空间可以用来组织类,这有助于更好地管理代码和避免冲突。

namespace Math {
    class Calculator {
    public:
        static int Add(int a, int b) {
            return a + b;
        }
    };
}
#include <iostream>

int main() {
    int result = Math::Calculator::Add(3, 5);
    std::cout << "Result: " << result << std::endl;

    return 0;
}

4. 重载与命名空间:

如果在不同的命名空间中有相同名称的函数,可以通过使用作用域解析运算符来调用特定命名空间中的函数。

namespace A {
    void Function() {
        std::cout << "Function in namespace A" << std::endl;
    }
}

namespace B {
    void Function() {
        std::cout << "Function in namespace B" << std::endl;
    }
}
#include <iostream>

int main() {
    A::Function();
    B::Function();

    return 0;
}

总之,C++命名空间是用于组织和管理代码标识符的一种机制,可以避免命名冲突、提供更好的代码结构,并且可以用于组织类和函数等。通过使用作用域解析运算符,可以访问命名空间中的成员,处理类和命名空间的组合,以及在不同命名空间中进行重载函数的调用。


3.7 多重继承与虚继承

C++中的多重继承和虚继承是面向对象编程中的重要概念,用于实现多个基类的组合以及解决由多重继承可能引发的问题。

1. 多重继承:

多重继承是指一个类可以从多个基类派生。派生类会继承多个基类的成员。

class Base1 {
public:
    void Function1() {}
};

class Base2 {
public:
    void Function2() {}
};

class Derived : public Base1, public Base2 {
public:
    void Function3() {}
};

2. 类型转换与多个基类:

当派生类从多个基类派生时,需要注意类型转换的问题。通过基类指针或引用,可以访问派生类的成员。

Derived obj;
Base1* ptr1 = &obj;
Base2* ptr2 = &obj;

ptr1->Function1();
ptr2->Function2();

3. 多重继承下的类作用域:

在多重继承中,可能会出现不同基类有相同成员名的情况,这时需要使用作用域解析运算符来指定访问哪个基类的成员。

class Derived : public Base1, public Base2 {
public:
    void Function() {
        Base1::Function1(); // 调用 Base1 的成员函数
        Base2::Function2(); // 调用 Base2 的成员函数
    }
};

4. 虚继承:

虚继承用于解决多重继承可能引发的菱形继承问题,避免同一个基类在继承链中出现多次。

class Base {
public:
    int value;
};

class Derived1 : virtual public Base {};
class Derived2 : virtual public Base {};

class MultipleDerived : public Derived1, public Derived2 {};

5. 构造函数与虚继承:

在虚继承中,构造函数的调用顺序变得复杂,因为派生类可能有多个虚基类,每个虚基类又可能有自己的基类。构造函数的调用按照虚基类的继承顺序从最基础的类开始,然后逐步构建出派生类的实例。

class A {
public:
    A(int x) { std::cout << "A: " << x << std::endl; }
};

class B : virtual public A {
public:
    B(int x) : A(x) { std::cout << "B: " << x << std::endl; }
};

class C : virtual public A {
public:
    C(int x) : A(x) { std::cout << "C: " << x << std::endl; }
};

class D : public B, public C {
public:
    D(int x) : A(x), B(x), C(x) { std::cout << "D: " << x << std::endl; }
};

在上述例子中,创建 D 类的对象时,会按照虚基类 A 的继承顺序调用构造函数。

总之,C++的多重继承允许一个类从多个基类派生,但需要注意类型转换、类作用域和构造函数的调用顺序。虚继承用于解决多重继承可能引发的问题,确保只有一个实例被共享。


4 C++补充2

4.1 控制内存分配

C++允许重载 newdelete 运算符,以控制内存的分配和释放过程。这使得可以实现自定义的内存管理策略,例如从特定的内存池中分配内存或进行资源跟踪。此外,C++还支持定位 new 表达式,允许在特定的内存位置上分配对象。

1. 重载 new 和 delete:

可以重载全局的 newdelete 运算符,也可以在类中重载类特定的 newdelete 运算符。

#include <iostream>

void* operator new(std::size_t size) {
    std::cout << "Custom new called, size: " << size << std::endl;
    return std::malloc(size);
}

void operator delete(void* ptr) noexcept {
    std::cout << "Custom delete called" << std::endl;
    std::free(ptr);
}

class MyClass {
public:
    void* operator new(std::size_t size) {
        std::cout << "Custom new in class called, size: " << size << std::endl;
        return std::malloc(size);
    }

    void operator delete(void* ptr) noexcept {
        std::cout << "Custom delete in class called" << std::endl;
        std::free(ptr);
    }
};

int main() {
    int* ptr1 = new int;
    delete ptr1;

    MyClass* obj = new MyClass;
    delete obj;

    return 0;
}

2. 定位 new 表达式:

定位 new 表达式允许在特定的内存位置上分配对象。这对于在预分配的内存块中构造对象非常有用,例如在内存池中。

#include <iostream>

class MyClass {
public:
    MyClass(int value) : data(value) {}
    int GetData() const { return data; }

private:
    int data;
};

int main() {
    void* memory = std::malloc(sizeof(MyClass));
    MyClass* obj = new(memory) MyClass(42);

    std::cout << "Data: " << obj->GetData() << std::endl;

    obj->~MyClass();
    std::free(memory);

    return 0;
}

在上述示例中,首先使用 std::malloc 分配了一块内存,然后使用定位 new 表达式在这块内存中构造了一个 MyClass 对象,最后使用析构函数和 std::free 释放了内存。

总之,通过重载 newdelete 运算符,可以实现自定义的内存分配和释放策略。定位 new 表达式允许在指定的内存位置上构造对象,从而提供更灵活的内存管理能力。但在实际使用时,需要小心管理内存和正确处理析构等操作,以避免内存泄漏和潜在的问题。


4.2 运行时类型识别

C++运行时类型识别(Run-Time Type Identification,RTTI)是一种在程序运行时判断对象的实际类型的机制。C++提供了一些工具来进行运行时类型识别,包括 dynamic_cast 运算符、typeid 运算符和 type_info 类。

1. dynamic_cast 运算符:

dynamic_cast 运算符用于在继承层次结构中进行安全的向下转型(downcasting)。它在运行时检查对象的类型信息,如果转型不合法,则返回空指针(对于指针类型)或抛出 std::bad_cast 异常(对于引用类型)。

#include <iostream>

class Base {
public:
    virtual void Print() { std::cout << "Base" << std::endl; }
};

class Derived : public Base {
public:
    void Print() override { std::cout << "Derived" << std::endl; }
};

int main() {
    Base* basePtr = new Derived;
    
    Derived* derivedPtr = dynamic_cast<Derived*>(basePtr);
    if (derivedPtr) {
        derivedPtr->Print(); // 输出 "Derived"
    }

    delete basePtr;
    
    return 0;
}

2. typeid 运算符:

typeid 运算符返回一个 type_info 对象,用于表示表达式的类型信息。可以使用它来比较两个类型是否相同。

#include <iostream>
#include <typeinfo>

class Base {};
class Derived : public Base {};

int main() {
    Base* basePtr = new Derived;
    
    if (typeid(*basePtr) == typeid(Derived)) {
        std::cout << "Type is Derived" << std::endl;
    }

    delete basePtr;
    
    return 0;
}

3. 使用 RTTI:

dynamic_casttypeid 运算符都依赖于 RTTI 功能。为了启用 RTTI,需要在编译时使用 -frtti 编译选项。

4. type_info 类:

type_info 类是一个表示类型信息的标准类,可以通过 typeid 运算符获取。它提供了一些成员函数,例如 name() 函数用于获取类型的名称。

#include <iostream>
#include <typeinfo>

class Base {};
class Derived : public Base {};

int main() {
    Base* basePtr = new Derived;
    
    const std::type_info& typeInfo = typeid(*basePtr);
    std::cout << "Type name: " << typeInfo.name() << std::endl;

    delete basePtr;
    
    return 0;
}

总之,C++的运行时类型识别(RTTI)机制通过 dynamic_cast 运算符、typeid 运算符和 type_info 类来支持在运行时获取对象的类型信息。这些工具可以在面对继承层次结构时帮助进行安全的类型转换和类型比较。


4.3 枚举类型

C++中的枚举(Enumeration)类型是一种用户定义的数据类型,用于为一组相关的常量赋予有意义的名字,以提高代码的可读性和维护性。枚举类型可以用来定义一组取值有限且固定的常量。

1. 枚举类型的定义:

可以使用 enum 关键字来定义一个枚举类型,然后列出其可能的取值。

enum Color {
    RED,
    GREEN,
    BLUE
};

2. 枚举变量的声明和赋值:

定义枚举类型后,可以声明枚举变量并赋予其一个取值。

Color myColor = GREEN;

3. 枚举常量的值:

枚举常量默认从0开始递增,但也可以为它们赋予特定的值。

enum Day {
    SUNDAY = 1,
    MONDAY,
    TUESDAY,
    WEDNESDAY,
    THURSDAY,
    FRIDAY,
    SATURDAY
};

4. 使用枚举类型:

枚举类型通常用于限制变量的取值范围,提高代码的可读性。

void PrintColor(Color color) {
    switch (color) {
        case RED:
            std::cout << "Red color" << std::endl;
            break;
        case GREEN:
            std::cout << "Green color" << std::endl;
            break;
        case BLUE:
            std::cout << "Blue color" << std::endl;
            break;
        default:
            std::cout << "Unknown color" << std::endl;
    }
}

5. 强类型枚举(C++11 及以后版本):

C++11 引入了强类型枚举,使用 enum class 来定义,可以避免枚举值之间的隐式转换,提高代码的类型安全性。

enum class Fruit {
    APPLE,
    BANANA,
    ORANGE
};

Fruit myFruit = Fruit::BANANA;

枚举类型在C++中是一种方便的工具,用于定义一组相关的常量并提高代码的可读性。强类型枚举更进一步提高了代码的类型安全性,减少了可能的错误。


4.4 类成员指针

C++中的类成员指针允许在运行时引用类的数据成员和成员函数。这些指针可以用于实现灵活的代码设计,例如回调函数、事件处理等。

1. 数据成员指针:

数据成员指针是指向类的数据成员的指针,可以用于访问和操作类的数据成员。

#include <iostream>

class MyClass {
public:
    int x;
    double y;
};

int main() {
    int MyClass::*ptrToInt = &MyClass::x;
    double MyClass::*ptrToDouble = &MyClass::y;

    MyClass obj;
    obj.*ptrToInt = 42;
    obj.*ptrToDouble = 3.14;

    std::cout << "x: " << obj.*ptrToInt << std::endl;
    std::cout << "y: " << obj.*ptrToDouble << std::endl;

    return 0;
}

2. 成员函数指针:

成员函数指针是指向类的成员函数的指针,可以用于调用类的成员函数。

#include <iostream>

class MyClass {
public:
    void PrintHello() {
        std::cout << "Hello from MyClass" << std::endl;
    }
};

int main() {
    void (MyClass::*ptrToFunction)() = &MyClass::PrintHello;

    MyClass obj;
    (obj.*ptrToFunction)();

    return 0;
}

3. 将成员函数用作可调用对象:

可以使用 std::function 类来将成员函数用作可调用对象,这样可以方便地将成员函数传递给其他函数,例如作为回调函数。

#include <iostream>
#include <functional>

class MyClass {
public:
    void Print(int value) {
        std::cout << "Value: " << value << std::endl;
    }
};

int main() {
    MyClass obj;
    std::function<void(MyClass*, int)> func = &MyClass::Print;

    func(&obj, 42);

    return 0;
}

类成员指针是一种强大的工具,可以在某些情况下提高代码的灵活性和可扩展性。通过使用数据成员指针和成员函数指针,可以访问和操作类的数据成员和成员函数。同时,将成员函数用作可调用对象可以在函数传递和回调等场景中非常有用。


4.5 嵌套类

C++中的嵌套类是指一个类定义在另一个类的内部,被称为外围类的成员。嵌套类可以访问外围类的私有成员,但它们与外围类的其他成员之间的关系相对独立。嵌套类也可以被外部代码实例化和使用,但需要通过外围类的作用域限定。

下面是一个简单的示例,演示了如何定义和使用嵌套类:

#include <iostream>

class OuterClass {
public:
    OuterClass(int x) : outerValue(x) {}

    class InnerClass {
    public:
        void Display(OuterClass& obj) {
            std::cout << "Inner value: " << obj.outerValue << std::endl;
        }
    };

private:
    int outerValue;
};

int main() {
    OuterClass outerObj(42);
    OuterClass::InnerClass innerObj;

    innerObj.Display(outerObj);

    return 0;
}

在上述示例中,InnerClass 是嵌套在 OuterClass 内部的类。通过创建 outerObjinnerObj 来实例化外围类和嵌套类,然后通过 innerObj 调用嵌套类的方法,该方法可以访问外围类的私有成员 outerValue

需要注意的是,嵌套类的生命周期独立于外围类的实例。嵌套类也可以拥有自己的成员变量和成员函数,与外围类的其他成员一样,只要它们的可见性在合适的范围内。嵌套类的定义可以在外围类的公有、私有或保护部分,具体取决于的设计需求。


4.6 union:一种节省空间的类

在C++中,union 是一种特殊的数据结构,它允许在相同的内存位置存储不同类型的数据,以节省空间。与结构体(struct)不同,union 只会使用其内部的最大成员的内存空间,而不会分配每个成员的空间。这使得 union 在某些情况下可以用来节省内存,但也要注意它的一些限制和潜在的问题。

下面是一个简单的示例,演示了如何定义和使用 union

#include <iostream>

union MyUnion {
    int intValue;
    double doubleValue;
    char charValue;
};

int main() {
    MyUnion u;
    u.intValue = 42;

    std::cout << "int value: " << u.intValue << std::endl;

    u.doubleValue = 3.14;
    std::cout << "double value: " << u.doubleValue << std::endl;

    u.charValue = 'A';
    std::cout << "char value: " << u.charValue << std::endl;

    // 注意:访问一个成员可能会影响其他成员的值,因为它们共享同一块内存

    return 0;
}

需要注意的是,union 中的不同成员共享相同的内存空间。这意味着在改变一个成员的值后,其他成员的值可能会被修改。因此,在使用 union 时要特别小心,确保了解每个成员的访问和修改规则,以避免出现不可预测的结果。


4.7 局部类

C++中的局部类(Local Class)是指定义在函数内部的类。局部类具有函数作用域,只能在其定义所在的函数内部使用。局部类可以访问外部函数的局部变量,但需要注意它们的生命周期以避免悬垂指针等问题。

下面是一个示例,演示了如何定义和使用局部类:

#include <iostream>

void OuterFunction() {
    int outerValue = 42;

    class LocalClass {
    public:
        void Display(int value) {
            std::cout << "Outer value: " << outerValue << std::endl; // 访问外部函数的局部变量
            std::cout << "Parameter value: " << value << std::endl;
        }
    };

    LocalClass localObj;
    localObj.Display(10);
}

int main() {
    OuterFunction();

    return 0;
}

在上述示例中,LocalClass 是定义在 OuterFunction 内部的局部类。它可以访问 OuterFunction 中的局部变量 outerValue,同时也可以接受参数并进行操作。注意,局部类的定义范围限制在 OuterFunction 内部,无法在其他函数中使用。

局部类在一些特定情况下非常有用,例如需要在函数内部定义一个辅助类来解决特定问题,或者需要在函数内部实现一些具体的功能。但要注意,局部类的生命周期只限于包含它的函数的执行期间,一旦函数执行结束,局部类的实例和定义都会被销毁。

总之,C++中的局部类是一种在函数内部定义的类,具有函数作用域。它们可以访问外部函数的局部变量,但需要注意生命周期限制。局部类可以在一些特定情况下提供更清晰和模块化的代码结构。

如果需要转载,请加上本文链接:https://blog.csdn.net/a13027629517/article/details/132484544?spm=1001.2014.3001.5501

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
电子书大小: 15.00MB 电子书语言: 简体中文 本书是程序设计大师Herbert Schildt多年开发、教学经验的总结,以实践证明行之有效的方法让您快速精通C++语言。 本书以最易于教学的编排和大量附有细致注解的典型程序示例,从基础知识到最新的高级特性,全面讲解了C++语言。通过本书,读者可以了解C++C++程序的一般形式,并逐步掌握C++语言的核心内容,包括控制语句、运算符、变量、类和对象、异常处理、模板、名称空间、运行时类型ID和标准模板库等,还能学习到用于.NET编程的扩展关键字。全书内容秉承Herbert一贯的写作风格:简洁、清晰、精准。 本书为C++编程入门读物,面向没有编程经验的程序设计及C++语言的初学者,适合作为高等院校计算机专业相关课程教材,也可为广大编程爱好者深入学习C++及其他面向对象语言打下坚实的基础。 第1章 C++的发展历程 1.1 C++的起源 1.2 C++的发展 1.3 什么是面向对象程序设计第2章 C++概览 2.1 第一个C++程序 2.2 处理语法错误 2.3 第二个C++程序 2.4 一个更实际的例子 2.5 一种新的数据类型 2.6 快速回顾 2.7 函数 2.8 函数的参数 2.9 输出选项 2.10 两个简单的命令 2.11 代码块 2.12 分号与定位 2.13 缩进编排 2.14 C++中的关键字 2.15 C++的标识符 2.16 标准C++库 第3章 基本数据类型 3.1 变量的声明 3.2 类型修饰符 3.3 字面量 3.4 变量的初始化 3.5 运算符 3.6 表达式第4章 程序控制语句 4.1 if语句 4.2 for循环 4.3 switch语句 4.4 while循环 4.5 do-while循环 4.6 使用continue 4.7 用break跳出循环 4.8 嵌套循环 4.9 使用goto语句 4.10 综合应用 第5章 数组和字符串 5.1 一维数组 5.2 字符串 5.3 字符串库函数 5.4 二维数组 5.5 多维数组 5.6 数组初始化 5.7 字符串数组 第6章 指针 6.1 什么是指针 6.2 指针运算符 6.3 指针表达式 6.4 指针和数组 6.5 指针与字符串字面量 6.6 指针比较的示例 6.7 指针数组 6.8 空指针约定 6.9 多重间接 6.10 指针带来的问题第7章 函数,第一部分;基础知识 7.1 函数的作用域准则 7.2 传递指针和数组 7.3 argc和argv:函数main()的参数 7.4 return语句 7.5 函数原型 7.6 头文件:进一步的学习 7.7 递归 第8章 函数,第二部分:引用,重载和默认参数 8.1 两种参数传递的方法 8.2 引用参数 8.3 函数的重载 8.4 函数的默认函数 8.5 函数重载与歧义性 第9章 更多的数据类型与运算符 第10章 结构与联合 第11章 类 第12章 类的深入学习 第13章 运算符的重载 第14章 继承 第15章 虚函数与多态 第16章 模板 第17章 异常处理 第18章 C++的I/O系统 第19章 运行时类型识别与强制转换运算符 第20章 名字空间和其他高级主题 第21章 标准模板库 第22章 C++预处理器 附录A 基于C的I/O 附录B 使用旧的C++编译器 附录C .NET对C++的受控扩展

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值