Skip to content

面向对象

约 3077 个字 90 行代码 预计阅读时间 11 分钟

类(封装)

定义

类是用户定义的数据类型,它由数据成员(变量)和成员函数(方法)组成。在C++中class和struct都可以用于定义类,区别是class默认是私有的,而struct默认是公开的。

每个类定义了唯一类类型,类是按名称等价,而不是按内容。C++的类实现了封装,但是封装并不是完美的。

对象的创建

类定义完成后,可以创建类的对象。对象是类的实例,通过对象可以访问类中的数据成员和成员函数。

构造函数和析构函数

(Constructor & Deconstructor)

构造函数是用于初始化对象的特殊函数,构造函数的名称必须与类的名称相同,并且它没有返回类型(甚至没有void),类是保证了一定被初始化的;析构函数则用于清理对象在销毁前的资源,在类名前加一个取反运算符~,析构函数永远不会有参数,但是可以输出内容。

构造函数可以有很多个(重载即可),在C++11 标准中,如果我们在定义了构造函数的情况下需要默认行为,那么可以通过在参数列表后写上 = default 来要求编译器生成默认构造函数。C++11 还允许了初始化列表,允许用先初始化的变量来给后面的变量初始化,如果在函数内赋值则要求成员变量有默认构造函数,并且局部变量会覆盖全局变量。

但其实在调用构造函数以前,内存就分配好了,C++不允许只分配空间而不去调用,例子就比如

注意带参数的默认构造函数在所有成员变量有默认参数的情况下是可以当无参版调用

class x;
if(t<10)
    //goto jump1;
x x1
//jump1:

类的访问控制

C++ 提供了三种访问控制修饰符:public、private 和 protected。 * public: 公共成员可以在类的外部访问。 * private: 私有成员只能在类的内部访问。 * protected: 受保护成员可以在类的内部以及派生类中访问。

类的this指针

任何对类成员的直接访问都被看做作this的隐式引用,this是一个总是指向“这个对象”的常量指针,任何自定义名为this的参数或者变量都是非法的。注意默认状态下this是指向类类型非常量版本的常量指针,因此不能在一个常量对象上调用普通成员函数

友元

在C++中友元是一种特殊的机制,允许一个类或函数访问另一个类的私有和保护成员。通常,类的私有和保护成员只能被该类的成员函数或从该类派生的子类访问。然而,通过使用友元,你可以指定其他类或函数可以直接访问这些成员。友元关系不存在传递性,重载函数需要单独声明友元

拷贝构造

拷贝构造函数是用于初始化一个新对象为现有对象的拷贝。其函数签名为:

ClassName(const ClassName &other);
其中,ClassName是类名,other是需要拷贝的对象的引用。拷贝构造有如下几种用途: - 创建对象时通过另一个对象进行初始化。 - 通常在以下情况下被调用: - 使用拷贝初始化(ClassName obj2 = obj1;) - 通过对象传递到函数(按值传递) - 从函数返回对象(按值返回)

拷贝赋值操作符(Copy Assignment Operator)用于将一个对象的值赋给另一个已经存在的对象。其函数签名为:

ClassName& operator=(const ClassName &other);
其中,ClassName是类名,other是需要拷贝的对象的引用。它有以下几种用途: - 赋值已有对象(obj1 = obj2;) - 确保自我赋值时正确处理资源释放

在拷贝赋值操作符中,通常会检查自我赋值(if (this == &other)),这是为了避免释放自身资源后再分配新资源,导致程序出错。确保在执行资源分配之前,旧的资源已经被适当释放。

移动构造

移动构造函数用于将一个对象的资源(如动态分配的内存)从一个对象转移到另一个对象。它通常用于在对象初始化时避免不必要的复制操作,从而提高效率。其函数签名如下:

ClassName(ClassName&& other) noexcept;
移动赋值运算符(Move Assignment Operator)用于将一个对象的资源转移到当前对象中。与移动构造函数类似,它也用于避免不必要的复制操作,并释放当前对象持有的资源。其函数签名如下
ClassName& operator=(ClassName&& other) noexcept;
// noexcept 表示不会抛出异常
为什么需要移动赋值?

  1. 性能优化: 移动构造和移动赋值避免了不必要的深拷贝操作,减少了资源的分配和释放次数,通常比复制操作更高效。
  2. 资源管理: 移动操作使得资源管理更加明确,减少了可能出现的内存泄漏和重复释放问题。

注意事项: - 资源管理: 移动操作应该确保资源的正确转移,并处理自赋值的情况。 - 异常安全: 移动构造函数和移动赋值运算符应尽可能地提供强异常保证(例如使用 noexcept),以保证在发生异常时资源的正确管理。

继承

基本概念

在C++中,继承是一种面向对象编程中的重要特性,它允许一个类(称为子类或派生类)从另一个类(称为基类或父类)获取属性和方法。继承使代码的复用性和扩展性得到了提升,同时也支持了多态性和动态绑定等特性。

  • 基类(Base Class): 提供成员变量和方法的类,其他类可以继承它。
  • 派生类(Derived Class): 从基类继承的类,它继承了基类的成员变量和方法,但也可以有自己的独特成员。

继承的语法

在C++中,继承是通过使用一个冒号(:)和访问控制符(public, protected, private)来实现的。

class Base {
public:
    int baseVar;
    void baseMethod() {
        // 基类的方法
    }
};

class Derived : public Base {  // Derived从Base公有继承
public:
    int derivedVar;
    void derivedMethod() {
        // 派生类的方法
    }
};

继承类型

  • 公有继承(Public Inheritance): 基类的public成员在派生类中仍然是public的,protected成员在派生类中仍然为protectedprivate成员则无法被直接访问。

  • 保护继承(Protected Inheritance): 基类的publicprotected成员在派生类中都变为protectedprivate成员仍然无法被直接访问。

  • 私有继承(Private Inheritance): 基类的publicprotected成员在派生类中都变为privateprivate成员无法被直接访问。

访问控制

访问控制决定了派生类对象是否可以访问基类中的成员。根据继承类型,基类的成员在派生类中的访问级别会有所不同:

继承类型 基类的public成员 基类的protected成员 基类的private成员
public public protected 不可访问
protected protected protected 不可访问
private private private 不可访问

多继承

C++支持多继承,即一个类可以从多个基类继承:

class Derived : public Base1, public Base2 {
    // Derived类继承了Base1和Base2的成员
};

但多继承可能引发菱形继承问题,这是因为多个基类可能会有同名的成员,导致在派生类中产生二义性。为解决这一问题,C++提供了虚函数。

构造函数与析构函数

派生类的构造函数会首先调用基类的构造函数,而析构函数的调用顺序则与构造函数相反。

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

多态

定义

多态是面向对象编程的一个重要特性,它允许程序在运行时决定调用哪个函数或方法。多态的实现依赖于类的继承和虚函数,主要包括以下几种形式:

  1. 静态多态(编译时多态)
  2. 函数重载:同一个作用域内可以有多个同名但参数不同的函数。编译器根据函数的参数类型和数量来选择调用哪个函数。
  3. 运算符重载:可以重新定义运算符的行为,使其适应自定义的类。

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

  5. 虚函数:通过基类指针或引用调用虚函数时,程序会在运行时确定实际调用的函数。为了实现动态多态,基类中的函数需要声明为virtual,但实际当中不一定要显示地写出来。
  6. 纯虚函数和抽象类:当一个类中包含一个或多个纯虚函数(= 0)时,该类称为抽象类,不能被实例化,只能作为基类使用。

重载

C++中的运算符重载允许你为自定义类型定义或修改运算符的行为。通过运算符重载,你可以让自定义类型(例如类)使用标准运算符(如 +, -, *, /)进行操作,使得这些操作对于对象是直观和自然的。

重载的运算符是具有特殊名字的函数:它们的名字由关键字operator和其后要定义的运算符号公共组成,重载运算符应尽量与内置类型保持一致。运算符重载是通过定义一个特殊的成员函数或全局函数来实现的。这里是一些常见运算符重载的示例:

1. 成员函数重载

对于大多数运算符,重载的实现可以作为类的成员函数。例如,对于加法运算符 +,你可以这样定义:

#include <iostream>

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

    // 运算符重载的成员函数
    Complex operator + (const Complex& other) const {
        return Complex(real + other.real, imag + other.imag);
    }

    void print() const {
        std::cout << real << " + " << imag << "i" << std::endl;
    }
};

int main() {
    Complex c1(1.5, 2.5);
    Complex c2(3.0, 4.0);
    Complex c3 = c1 + c2;  // 使用重载的 + 运算符
    c3.print();            // 输出: 4.5 + 6.5i
    return 0;
}

2. 全局函数重载

有些运算符需要定义为全局函数,例如 <<>> 运算符:

#include <iostream>

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

    // 友元函数声明
    friend std::ostream& operator << (std::ostream& out, const Complex& c);
};

// 友元函数定义
std::ostream& operator << (std::ostream& out, const Complex& c) {
    out << c.real << " + " << c.imag << "i";
    return out;
}

int main() {
    Complex c1(1.5, 2.5);
    std::cout << c1 << std::endl;  // 使用重载的 << 运算符
    return 0;
}

注意事项

  1. 对称性: 有些运算符(如 ==, <, >)通常是对称的。
  2. 效率: 运算符重载应该高效,不应引入不必要的开销。
  3. 清晰性: 运算符的行为应该与其直观的意义相符,以避免困惑。

Warning

一个例外是,++ 和 -- 既可以作为前缀,也可以作为后缀;这如何区分呢?由于其他的单目运算符都是前缀,因此 C++ 规定 Foo::operator++() 和 operator++(Foo) 用来处理前缀的 ++,而后缀的 x++ 会调用 x.operator++(0) 或者 operator++(x, 0),即作为后缀时,编译器通过让一个额外的参数 0 参与重载解析。

虚函数

在C++中,虚函数是面向对象编程中实现多态的一种机制。虚函数允许在派生类中重写(覆盖)基类中的函数行为。使用虚函数时,基于对象的实际类型,而不是对象的类型声明,来决定调用哪个函数,这种机制称为动态绑定或晚绑定。

  • 虚函数: 通过在基类中声明函数前加virtual关键字来定义。派生类可以重写这些函数来实现特定的功能。
  • 多态: 通过基类的指针或引用来调用虚函数,运行时根据对象的实际类型来决定调用哪个版本的函数,实现多态。

在基类中,将函数声明为虚函数: 一个虚函数在派生类里也是隐式的虚函数,无需反复声明。C++11 提供了override关键字来说明派生类中的虚函数:如果使用override标记了某个函数,但是该函数没有覆盖已存在的虚函数则会报错。C++11新标准还提供了一种防止继承发生的方法,即在类名后面跟一个关键字final。只有通过基类的指针或引用调用虚函数时,才会发生动态绑定。如果直接通过对象调用,就不会使用动态绑定。

Base* b = new Derived();
b->show();  // 调用Derived类的show函数
Base b2;
b2.show();  // 调用Base类的show函数
如果一个类被设计为基类并且具有虚函数,那么它的析构函数也应该是虚的。这确保了通过基类指针删除派生类对象时,可以调用正确的析构函数,从而避免资源泄漏。在C++中,纯虚函数是一种特殊的虚函数,它在基类中没有实现,必须在派生类中实现。声明纯虚函数的类称为抽象类,不能实例化这样的类。