开发实战进阶之类class【c++】二

请先阅读:

开发实战进阶之类class【c++】一

接上篇

c++类进阶之构造/析构函数

在 C++ 中,构造函数是一种特殊的成员函数,用于初始化对象的状态。构造函数的名称与类名相同,不返回任何类型,甚至不返回 void。构造函数可以有多种形式,具备不同的特性,以满足不同的初始化需求。

1. 默认构造函数

默认构造函数是在不接受任何参数的情况下调用的构造函数。如果一个类没有定义任何构造函数,编译器会自动生成一个默认构造函数(只要没有其他构造函数)。如果类中定义了其他构造函数,但没有显示定义默认构造函数,且其他构造函数都需要参数,则需要显式定义一个无参数的构造函数,如果你需要无参数的对象创建。

class Example {
public:
    Example() {
        // 初始化代码
    }
};

2. 参数化构造函数

参数化构造函数接受一个或多个参数,用于提供创建对象时更多的灵活性。通过参数化构造函数,可以在创建对象的同时初始化对象的数据成员。

注:自己写了带参构造函数以后,系统就不会给你分配一个默认构造函数了,如果有需要无参构造函数,需要自行添加

class Example {
public:
    Example(Example&& other) noexcept {
        // 移动初始化代码
    }
};
class Example {
public:
    int x;
    Example(int val) : x(val) {}
};

3. 拷贝构造函数

拷贝构造函数用于初始化一个对象,使其成为另一个同类型对象的副本。如果类中没有显式定义拷贝构造函数,编译器会自动生成一个。

class Example {
public:
    Example(const Example& other) {
        // 拷贝初始化代码
    }
};

4. 移动构造函数(C++11)

移动构造函数用于通过移动而非拷贝的方式初始化一个对象,它接受一个右值引用参数。移动构造函数通常用于优化性能,避免在对象传递过程中创建不必要的临时对象和深拷贝。

class Example {
public:
    Example(Example&& other) noexcept {
        // 移动初始化代码
    }
};

5. 委托构造函数(C++11)

委托构造函数允许一个构造函数在同一个类中调用另一个构造函数,以避免代码重复。

5. 委托构造函数(C++11)
委托构造函数允许一个构造函数在同一个类中调用另一个构造函数,以避免代码重复。

6. 显式和删除的构造函数

  • 显式构造函数:通过 explicit 关键字标记的构造函数,防止编译器自动使用该构造函数进行隐式类型转换。
  • 删除的构造函数:通过 delete 关键字明确禁止某些构造函数形式,用于控制类的使用方式,如禁止拷贝或移动。
class Example {
public:
    explicit Example(int x) {}
    Example(const Example&) = delete; // 禁止拷贝构造
};

构造函数的正确使用可以确保对象的正确初始化,同时利用现代 C++ 的特性(如移动语义和委托构造)可以编写更高效、更简洁的代码。

在 C++ 中,析构函数是一个特殊的成员函数,其作用是在对象生命周期结束时执行清理任务,如释放资源、关闭文件、断开网络连接等。析构函数在对象销毁时自动调用,它的名字由波浪符 ~ 后跟类名构成,不能带参数,也不返回值。

析构函数的主要特点:

  1. 自动调用:析构函数在对象的生命周期结束时(如对象离开其作用域或被 delete 删除)被自动调用。
  2. 无参数无返回:析构函数不接受任何参数,也不返回任何值,甚至不返回 void
  3. 非重载:每个类只能有一个析构函数,因此析构函数不能重载。

析构函数的用途:

  • 资源释放:最常见的用途是释放对象在生命周期内申请的资源。这符合 RAII(资源获取即初始化)原则,确保程序的健壁性和异常安全。
  • 清理工作:执行任何必要的最终清理工作,如关闭文件句柄或网络连接,解锁互斥锁等。

示例代码:

class Demo {
public:
    int* array;

    Demo(int size) { // 构造函数
        array = new int[size]; // 申请资源
    }

    ~Demo() { // 析构函数
        delete[] array; // 释放资源
    }
};

void function() {
    Demo demo(100); // 在函数作用域中创建对象
} // demo 离开作用域,自动调用析构函数释放资源

特殊情况:

  • 虚析构函数:如果一个类预计会被继承,并且可能通过基类指针删除派生类对象,则应将析构函数声明为虚函数。这确保了通过基类指针删除派生类对象时,能够调用正确的析构函数,从而避免资源泄漏。
class Base {
public:
    virtual ~Base() {
        // 基类的析构函数
    }
};

class Derived : public Base {
public:
    ~Derived() {
        // 派生类的析构函数
    }
};

删除的析构函数:你可以将析构函数定义为删除的,来阻止对象的删除操作。这在设计某些特殊控制对象生命周期的类时非常有用。

class NonDeletable {
public:
    ~NonDeletable() = delete; // 阻止删除操作
};

注意事项:

  • 确保通过相同的方式分配和释放资源。例如,如果你使用 new[] 分配了内存,应该使用 delete[] 来释放它。
  • 虚析构函数可以增加一些运行时成本,因为它们需要虚函数机制支持。然而,当设计需要多态行为的类层次结构时,这是必须的。
  • 通常,只有当类用于管理资源时,才需要显式定义析构函数。如果类不拥有资源,可以依赖编译器自动生成的析构函数。

理解和正确使用析构函数对于保证 C++ 程序的资源正确管理和程序的稳定性至关重要。

c++类进阶之多态的实现

C++中的多态性是面向对象编程的核心特性之一,它允许使用统一的接口来操作不同的数据类型。多态主要分为两种类型:编译时多态(静态多态)和运行时多态(动态多态)。它们的实现方式和原理有所不同。

1. 编译时多态(静态多态)

编译时多态主要是通过函数重载和运算符重载实现的。这种多态在编译期间解析,所以称为静态多态。

函数重载

函数重载允许在同一作用域内存在多个同名函数,只要它们的参数列表不同(参数类型或参数数量)即可。

class Print {
public:
    void show(int i) {
        std::cout << "Integer: " << i << std::endl;
    }
    void show(double f) {
        std::cout << "Float: " << f << std::endl;
    }
};
运算符重载

运算符重载允许给已有的运算符赋予更多的功能,使它们能在用户定义的数据类型上进行操作。

class Complex {
public:
    int real, imag;
    Complex(int r = 0, int i = 0) : real(r), imag(i) {}

    // 运算符重载
    Complex operator + (const Complex& obj) {
        return Complex(real + obj.real, imag + obj.imag);
    }
};

2. 运行时多态(动态多态)

运行时多态是通过基类指针或引用访问派生类对象实现的,这种多态性依赖虚函数。

虚函数和虚拟继承

运行时多态的关键是虚函数。当一个函数在基类中被声明为虚函数后,派生类可以重写这个函数来提供特定的功能。C++ 使用虚函数表(vtable)来支持运行时多态。

class Animal {
public:
    virtual void sound() {
        std::cout << "Some sound" << std::endl;
    }
};

class Dog : public Animal {
public:
    void sound() override {
        std::cout << "Bark" << std::endl;
    }
};

void makeSound(Animal& a) {
    a.sound();  // 在运行时解析
}
虚函数表 (vtable)
  • 虚函数表:如果类中有虚函数,编译器会为这个类创建一个虚函数表。这个表包含指向虚函数地址的指针。
  • vptr:每个对象会有一个指针(vptr),指向它的虚函数表。当调用一个虚函数时,实际上是通过 vptr 来间接访问这个函数。

当通过基类的指针或引用调用虚函数时,程序会查看虚函数表以确定应该调用哪个函数,这个查找过程是在运行时进行的,因此称为动态多态。

特点和考虑

  • 动态多态的成本:使用动态多态会有性能损失,因为需要间接调用函数。但在需要使用多态性的场景下,这种成本是合理的。
  • 设计灵活性:多态性提高了代码的可维护性和扩展性,使得新的类类型可以很容易地整合进现有的系统中。
  • 接口一致性:多态允许不同类的对象对同一消息作出不同的响应,从而可以编写出更通用的代码来处理不同类型的对象。

多态性是 C++ 强大功能的体现,正确利用多态可以让我们的代码更加灵活和动态。

c++类进阶之运算符重载

在 C++ 中,运算符重载是一种形式的多态,允许开发者为用户定义的类型指定运算符的操作。这使得用户定义的类型可以像内建类型那样进行操作,增强了代码的可读性和便利性。下面是一些常见的运算符重载类型和它们的注意事项。

常见的运算符重载类型

  1. 赋值运算符(=

    • 用于定义对象之间的赋值行为。
class MyClass {
public:
    int *data;
    MyClass& operator=(const MyClass& other) {
        if (this != &other) {  // 检查自赋值
            delete data;
            data = new int(*(other.data));
        }
        return *this;
    }
};

   2.算术运算符(+, -, *, /, % 等)

  • 用于定义加减乘除等运算。
class Complex {
public:
    double real, imag;
    Complex operator+(const Complex& other) const {
        return Complex(real + other.real, imag + other.imag);
    }
};

3.比较运算符(==, !=, <, >, <=, >=

  • 用于定义对象之间的比较。
    
    class MyClass {
    public:
        int value;
        bool operator==(const MyClass& other) const {
            return value == other.value;
        }
    };
    
  • 4.逻辑运算符(&&, ||, !

    • 不常重载,因为它们涉及到短路逻辑,不能通过重载直接实现.
  • 5.下标运算符([]

  • 用于提供类似数组的访问方式。
class IntArray {
public:
    int* array;
    int& operator[](int index) {
        return array[index];
    }
};

6.函数调用运算符(()

  • 使得对象可以像函数那样被调用。
class Adder {
public:
    int operator()(int x, int y) const {
        return x + y;
    }
};

7.输入输出运算符(<<, >>

  • 常用于定义对象的输入输出方式,尤其是与流(如 std::ostreamstd::istream)相关的操作。
std::ostream& operator<<(std::ostream& os, const MyClass& obj) {
    os << obj.value;
    return os;
}

注意事项

  1. 自赋值安全:赋值运算符应该处理自赋值的情况,以防止错误和资源泄露。

  2. 返回引用:对于赋值运算符和流插入/提取运算符,通常返回引用以支持链式调用。

  3. 操作符的对称性:例如,如果你重载了 +,通常也会重载 +=。确保操作符的行为在逻辑上是一致的。

  4. 非成员函数:有些运算符,如 <<>>,通常应该作为非成员函数实现,以允许第一个操作数是非类类型。

  5. 不要过度重载:只在逻辑上合理的情况下重载运算符,避免造成混淆和误用。不要重载运算符以实现与预期完全不相关的功能。

  6. 保持简洁:运算符重载应简洁明了,不要在重载的运算符中包含复杂的逻辑。

  7. 避免重载某些运算符:特别是对于内置类型的运算符,如 &&|| 和逗号运算符,因为这些运算符涉及到特殊的求值策略(如短路求值),不能通过重载直接模拟。

通过恰当地使用运算符重载,可以让用户自定义的类型更加直观和易于使用,但也需要小心设计以避免引入意外的行为和复杂性。

c++类进阶之纯虚函数

在 C++ 中,纯虚函数是一种特殊的虚函数,其主要用途是在基类中声明一个接口,以强制派生类提供该函数的具体实现。纯虚函数没有实现体(即没有函数定义),而是在函数声明的末尾使用 = 0 来指定。

纯虚函数的作用

  1. 定义接口:纯虚函数允许基类定义一个接口,该接口必须由任何非抽象派生类实现。这保证了派生类遵循特定的接口规范。

  2. 抽象类:包含至少一个纯虚函数的类被称为抽象类。抽象类不能被实例化,它主要用于作为其他派生类的基类。

  3. 多态支持:通过纯虚函数实现的接口可以在运行时多态中使用。这允许通过基类的指针或引用调用派生类的实现,从而实现动态绑定。

用法示例

下面是一个使用纯虚函数的例子,其中定义了一个抽象基类和几个实现了这个基类的派生类:

#include <iostream>

class Shape {
public:
    // 纯虚函数
    virtual double area() const = 0;

    virtual ~Shape() {}
};

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, height;

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

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

void printArea(const Shape& shape) {
    std::cout << "Area: " << shape.area() << std::endl;
}

int main() {
    Circle circle(5);
    Rectangle rectangle(10, 5);

    printArea(circle);
    printArea(rectangle);

    return 0;
}

在这个例子中:

  • Shape 类有一个纯虚函数 area(),使得 Shape 成为一个抽象类。
  • CircleRectangleShape 的派生类,它们都提供了 area() 函数的具体实现。
  • printArea 函数展示了如何通过基类引用来调用具体派生类的 area() 方法,体现了多态的使用。

注意事项

  • 抽象类不能实例化:不能创建一个抽象类的对象,但可以创建指向派生类对象的基类指针或引用。
  • 析构函数应为虚:如果一个类是多态的基类,它的析构函数应该是虚的。这确保了通过基类指针删除派生类对象时,能够正确地调用派生类的析构函数。
  • 实现抽象类的派生类必须覆盖所有纯虚函数:任何从抽象类派生的类,如果想要被实例化,必须为基类中的所有纯虚函数提供实现。

纯虚函数是实现抽象和多态性的强大工具,适当使用可以大大增强程序的可维护性和灵活性。

c++类进阶之接口实现

在 C++ 中,虽然没有直接的关键字或构造来定义接口(如 Java 中的 interface 关键字),但可以通过抽象类来模拟接口的行为。一个纯接口类是由纯虚函数组成的抽象类,这意味着它没有成员变量和已实现的成员函数。这样的类在 C++ 中充当接口的角色。

定义 C++ 接口

一个典型的 C++ 接口类包含以下特点:

  1. 只有纯虚函数:这确保了派生类必须实现这些函数。
  2. 没有数据成员:接口只定义行为,不定义状态。
  3. 可能包含虚析构函数:虽然接口通常不需要实现析构函数,但提供一个虚析构函数是好的做法,以保证派生类的析构函数被正确调用。

示例:定义一个 C++ 接口

以下是一个模拟 C++ 接口的示例:

class IPrintable {
public:
    virtual void print() const = 0;  // 纯虚函数
    virtual ~IPrintable() {}         // 虚析构函数,确保派生类的析构器被调用
};

class Document : public IPrintable {
public:
    void print() const override {
        std::cout << "Document printing..." << std::endl;
    }
};

class Image : public IPrintable {
public:
    void print() const override {
        std::cout << "Image printing..." << std::endl;
    }
};

void performPrint(const IPrintable& obj) {
    obj.print();
}

int main() {
    Document doc;
    Image img;
    
    performPrint(doc);
    performPrint(img);

    return 0;
}

在这个示例中,IPrintable 是一个接口,它定义了一个 print 方法。DocumentImage 是两个具体的类,它们实现了 IPrintable 接口。performPrint 函数接受一个 IPrintable 类型的引用,展示了如何使用接口来调用具体实现。

使用 C++ 接口的优点

  1. 提高了代码的模块化:接口作为一个合约,使得不同的编程团队可以独立工作在接口的不同实现上。
  2. 增加了代码的灵活性:通过接口,可以在不修改现有代码的情况下引入新的行为和实现。
  3. 便于代码测试和维护:接口使得依赖注入和模拟变得更加容易,有利于单元测试。

注意事项

  • 在设计接口时,应仔细考虑所需的功能,避免接口变得过于庞大。一个庞大的接口可能需要被拆分成多个更小的接口,每个接口负责一部分明确的功能。
  • 由于 C++ 缺乏内置的接口支持,开发者需要自行确保遵守接口编程的最佳实践。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值