C++面向对象编程(OOP)课程的知识整理(自用可参考)

C++ 面向对象编程

1. 面向对象编程概述

面向对象编程(OOP)是一种编程范式,它使用对象来设计应用程序和计算机程序。对象是类的实例,类是定义对象数据格式和可用方法的蓝图。OOP 提供了一种通过封装、继承和多态来结构化软件的方法。

2. 类和对象

2.1 类的定义

类是创建对象的模板。一个类定义了对象的属性(成员变量)和行为(成员函数或方法)。类的定义包含在 class 关键字之后,并以分号结束。类可以有访问控制符如 publicprotectedprivate,它们用于控制类成员的访问权限。

示例:

下面是一个简单的类定义,该类描述了一辆汽车(Car):

class Car {
public:
    // 属性
    string brand;
    string model;
    int year;

    // 方法
    void printDetails() {
        cout << "Brand: " << brand << ", Model: " << model << ", Year: " << year << endl;
    }
};

在这个例子中,类 Car 包含了三个属性(成员变量):brandmodelyear,以及一个方法(成员函数) printDetailsprintDetails 方法用于输出汽车的详细信息。

属性和方法
  • 属性(成员变量):它们是类的变量,用于描述对象的特性。例如,Car 类中的 brandmodelyear
  • 方法(成员函数):它们是类的函数,用于定义对象的行为。例如,Car 类中的 printDetails

2.2 对象的创建

对象是类的实例化。在创建对象时,分配内存并初始化其成员变量。使用类名创建对象,然后通过对象访问成员变量和成员函数。

示例:

下面展示了如何创建 Car 类的对象,并使用该对象访问和操作类的成员:

int main() {
    // 创建对象
    Car car1;

    // 初始化对象的属性
    car1.brand = "Toyota";
    car1.model = "Corolla";
    car1.year = 2020;

    // 调用对象的方法
    car1.printDetails();

    return 0;
}

在这个例子中,我们创建了一个 Car 类的对象 car1。然后,我们设置了 car1brandmodelyear 属性,并调用 car1printDetails 方法来输出汽车的详细信息。

构造函数和析构函数

为了更好地初始化对象和管理资源,C++ 提供了构造函数和析构函数。

  • 构造函数:在创建对象时自动调用,用于初始化对象。构造函数的名称与类名相同,不返回任何值。
  • 析构函数:在对象生命周期结束时自动调用,用于清理资源。析构函数的名称与类名相同,但前面带有波浪号(~)。
示例:
class Car {
public:
    string brand;
    string model;
    int year;

    // 构造函数
    Car(string b, string m, int y) {
        brand = b;
        model = m;
        year = y;
    }

    // 析构函数
    ~Car() {
        cout << "Car object is being deleted" << endl;
    }

    void printDetails() {
        cout << "Brand: " << brand << ", Model: " << model << ", Year: " << year << endl;
    }
};

int main() {
    // 使用构造函数创建对象并初始化
    Car car1("Toyota", "Corolla", 2020);
    car1.printDetails();

    return 0;
}

在这个例子中,Car 类定义了一个构造函数,用于初始化 brandmodelyear 属性。析构函数在对象生命周期结束时自动调用,用于执行清理操作。

2.3 类的成员访问控制

C++ 提供了三种访问控制符来控制类成员的访问权限:

  • public:公有成员可以被类外部访问。
  • protected:受保护成员只能被类及其子类访问。
  • private:私有成员只能被类本身访问。
示例:
class Car {
private:
    string brand;
    string model;
    int year;

public:
    // 构造函数
    Car(string b, string m, int y) {
        brand = b;
        model = m;
        year = y;
    }

    // 公共方法来访问私有属性
    void printDetails() {
        cout << "Brand: " << brand << ", Model: " << model << ", Year: " << year << endl;
    }
};

int main() {
    Car car1("Toyota", "Corolla", 2020);
    car1.printDetails();
    // car1.brand = "Honda"; // 错误:无法访问私有成员

    return 0;
}

在这个例子中,brandmodelyear 属性被声明为私有(private),因此不能直接在类外部访问。通过公共方法 printDetails 可以间接访问这些属性。

通过这些扩展,可以更全面地了解 C++ 中类和对象的定义、创建、使用以及其访问控制机制。

3. 封装

封装是面向对象编程的核心概念之一。它通过将数据(属性)和操作数据的方法(行为)结合在一起,并隐藏对象的实现细节,使得对象的内部状态只能通过定义好的接口进行访问和修改。封装提高了代码的安全性和可维护性。

3.1 封装的优势

  1. 数据隐藏:通过将成员变量设为私有,防止外部直接访问,保护数据完整性。
  2. 简化接口:通过定义清晰的公共方法(接口)来访问和修改数据,简化了与对象的交互。
  3. 代码复用:封装使得类的实现细节与使用细节分离,提高了代码复用性。
  4. 易维护性:封装使得类的内部实现可以独立于外部代码进行修改,提高了代码的可维护性。

3.2 类的访问控制

C++ 提供了三种访问控制符来控制类成员的可见性:

  • public:公有成员可以被类的外部访问。
  • protected:受保护成员只能被类和派生类访问。
  • private:私有成员只能被类内部访问,无法被类的外部或派生类访问。

3.3 封装的实现

以下是一个实现封装的示例,该示例定义了一个 EncapsulatedCar 类,将成员变量设为私有,并提供公共方法来访问和修改这些变量。

class EncapsulatedCar {
private:
    string brand;
    string model;
    int year;

public:
    // 构造函数
    EncapsulatedCar(string b, string m, int y) : brand(b), model(m), year(y) {}

    // 公共方法来访问私有属性
    void printDetails() {
        cout << "Brand: " << brand << ", Model: " << model << ", Year: " << year << endl;
    }

    // 获取品牌
    string getBrand() {
        return brand;
    }

    // 设置品牌
    void setBrand(string b) {
        brand = b;
    }

    // 获取型号
    string getModel() {
        return model;
    }

    // 设置型号
    void setModel(string m) {
        model = m;
    }

    // 获取年份
    int getYear() {
        return year;
    }

    // 设置年份
    void setYear(int y) {
        year = y;
    }
};

3.4 示例代码解释

在上述示例中,EncapsulatedCar 类包含了三个私有成员变量:brandmodelyear。这些成员变量只能通过类内部的方法访问和修改。类提供了以下公共方法:

  1. 构造函数:用于初始化对象的成员变量。
  2. printDetails 方法:用于输出汽车的详细信息。
  3. getBrandsetBrand 方法:用于获取和设置汽车的品牌。
  4. getModelsetModel 方法:用于获取和设置汽车的型号。
  5. getYearsetYear 方法:用于获取和设置汽车的年份。

3.5 使用封装的类

下面是一个使用 EncapsulatedCar 类的示例:

int main() {
    // 创建对象并初始化
    EncapsulatedCar car1("Toyota", "Corolla", 2020);
    
    // 输出对象的详细信息
    car1.printDetails();

    // 修改对象的品牌
    car1.setBrand("Honda");
    
    // 输出修改后的对象详细信息
    car1.printDetails();

    return 0;
}

在这个示例中,我们创建了一个 EncapsulatedCar 对象 car1,并初始化其品牌、型号和年份。然后,我们使用公共方法 printDetails 输出对象的详细信息。接着,我们通过 setBrand 方法修改对象的品牌,并再次输出修改后的详细信息。

通过以上示例,可以看出封装在保护数据完整性、简化接口和提高代码可维护性方面的重要作用。封装使得类的使用者无需关心类的内部实现细节,只需通过定义好的接口与类进行交互,从而提高了代码的安全性和可维护性。

4. 继承

继承是面向对象编程中用于创建新类的机制,新类(派生类或子类)从现有类(基类或父类)继承特性和行为。通过继承,子类可以重用父类的代码,同时可以扩展和修改父类的功能。继承提高了代码的重用性和组织性。

4.1 继承的基本语法

在 C++ 中,继承通过类定义中的冒号(:)和访问控制符来实现。访问控制符(public、protected、private)决定了基类成员在派生类中的访问权限。

class BaseClass {
    // 基类的定义
};

class DerivedClass : public BaseClass {
    // 派生类的定义
};

4.2 继承的示例

下面是一个简单的示例,演示了如何从基类 Vehicle 创建派生类 Car

class Vehicle {
public:
    string brand = "Ford";

    void honk() {
        cout << "Tuut, tuut!" << endl;
    }
};

class Car : public Vehicle {
public:
    string model = "Mustang";
};

int main() {
    Car myCar;
    myCar.honk();
    cout << myCar.brand + " " + myCar.model << endl;
    return 0;
}

4.3 示例代码解释

在上述示例中,Vehicle 类是基类,包含一个公共成员变量 brand 和一个公共成员函数 honkCar 类是从 Vehicle 派生出来的子类,除了继承自 Vehicle 的成员外,还增加了一个新的成员变量 model

main 函数中,我们创建了一个 Car 类的对象 myCar。通过这个对象,我们可以调用继承自 Vehiclehonk 方法,并访问 brandmodel 成员。

4.4 继承的类型

C++ 支持三种继承类型,决定了基类成员在派生类中的访问权限:

  1. 公有继承(public inheritance):基类的公共成员和受保护成员在派生类中保持原有的访问权限。
  2. 保护继承(protected inheritance):基类的公共成员和受保护成员在派生类中变为受保护成员。
  3. 私有继承(private inheritance):基类的公共成员和受保护成员在派生类中变为私有成员。
示例:
class Base {
public:
    int publicVar;
protected:
    int protectedVar;
private:
    int privateVar;
};

class PublicDerived : public Base {
    // publicVar 是 public
    // protectedVar 是 protected
    // privateVar 是不可访问的
};

class ProtectedDerived : protected Base {
    // publicVar 是 protected
    // protectedVar 是 protected
    // privateVar 是不可访问的
};

class PrivateDerived : private Base {
    // publicVar 是 private
    // protectedVar 是 private
    // privateVar 是不可访问的
};

4.5 重写和覆盖

派生类可以重写(override)基类的方法,从而提供特定实现。使用 virtual 关键字可以实现多态性。

示例:
class Vehicle {
public:
    virtual void honk() {
        cout << "Vehicle honk!" << endl;
    }
};

class Car : public Vehicle {
public:
    void honk() override {
        cout << "Car honk!" << endl;
    }
};

int main() {
    Vehicle* v = new Car();
    v->honk(); // 输出 "Car honk!"
    delete v;
    return 0;
}

4.6 继承的优点和缺点

优点

  1. 代码重用:通过继承,可以重用基类的代码,减少重复代码。
  2. 代码组织:继承有助于组织代码,使其更具结构性和逻辑性。
  3. 扩展性:继承使得在不修改基类代码的情况下扩展类的功能成为可能。

缺点

  1. 复杂性增加:继承层次过深可能导致代码复杂性增加,难以维护。
  2. 紧耦合:派生类和基类之间的紧耦合可能导致修改基类时需要修改所有派生类。
  3. 多继承问题:C++ 支持多继承,但多继承可能引发复杂的继承关系和二义性问题。

通过理解和应用继承,程序员可以创建更加灵活和可维护的代码结构,同时提高代码的重用性和扩展性。在设计类层次结构时,应当平衡继承的优点和潜在的复杂性。

5. 多态

多态允许在子类中定义与父类中同名的方法,但具体行为取决于调用该方法的对象。多态性是面向对象编程的重要特性,主要分为编译时多态(函数重载和运算符重载)和运行时多态(通过继承和虚函数实现)。

5.1 编译时多态(函数重载和运算符重载)

编译时多态是在编译期间决定的,包括函数重载和运算符重载。

5.1.1 函数重载

函数重载是指在同一个作用域内可以定义多个同名函数,但这些函数的参数列表必须不同。

class Print {
public:
    void show(int i) {
        cout << "Integer: " << i << endl;
    }

    void show(double d) {
        cout << "Double: " << d << endl;
    }

    void show(string s) {
        cout << "String: " << s << endl;
    }
};

在上述示例中,Print 类包含了三个重载的 show 方法,分别接受 intdoublestring 类型的参数。根据传递的参数类型,调用对应的重载函数。

5.1.2 运算符重载

运算符重载允许用户定义或重定义运算符的行为,使得自定义类型可以使用运算符。

class Complex {
private:
    float real;
    float imag;

public:
    Complex() : real(0), imag(0) {}
    Complex(float r, float i) : real(r), imag(i) {}

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

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

int main() {
    Complex c1(3.0, 4.0), c2(1.0, 2.0);
    Complex c3 = c1 + c2;
    c3.print(); // 输出 "4.0 + 6.0i"
    return 0;
}

在这个示例中,我们重载了 + 运算符,使其能够用于 Complex 类的对象。

5.2 运行时多态(虚函数和动态绑定)

运行时多态是通过继承和虚函数实现的。在运行时,根据对象的实际类型调用对应的方法。虚函数允许子类重写父类中的方法,并在运行时决定调用哪个类的方法。

5.2.1 虚函数

在基类中使用 virtual 关键字声明虚函数,在派生类中重写该函数。

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

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

int main() {
    Base* b;
    Derived d;
    b = &d;
    b->show(); // 输出 "Derived class"
    return 0;
}

在这个示例中,show 方法在 Base 类中被声明为虚函数,并在 Derived 类中重写。当基类指针 b 指向派生类对象 d 时,调用 show 方法将输出 “Derived class”,而不是 “Base class”。这是因为在运行时,调用的是派生类的实现。

5.3 纯虚函数和抽象类

纯虚函数是没有实现的虚函数,用于定义接口。包含纯虚函数的类称为抽象类,不能实例化。

class AbstractBase {
public:
    virtual void show() = 0; // 纯虚函数
};

class ConcreteDerived : public AbstractBase {
public:
    void show() override {
        cout << "ConcreteDerived class" << endl;
    }
};

int main() {
    ConcreteDerived obj;
    obj.show(); // 输出 "ConcreteDerived class"
    return 0;
}

在这个示例中,AbstractBase 类包含一个纯虚函数 show,因此它是一个抽象类。ConcreteDerived 类继承自 AbstractBase 并实现了 show 方法。尽管 AbstractBase 类不能实例化,但 ConcreteDerived 类可以。

5.4 多态的优点和应用

优点

  1. 代码复用:通过继承和虚函数机制,子类可以重用父类的代码,并提供自己的实现。
  2. 灵活性:多态性使得程序更灵活,可以在运行时选择适当的方法。
  3. 可扩展性:通过增加新的子类,可以扩展系统的功能,而无需修改现有代码。

应用

  1. 面向接口编程:通过定义抽象基类,具体实现由子类提供。
  2. 事件驱动编程:基于事件处理机制,可以动态绑定事件处理函数。
  3. 插件系统:通过多态机制,插件可以动态加载和执行。

多态是面向对象编程的重要特性,它通过函数重载、运算符重载、虚函数和动态绑定提供了灵活而强大的机制,使得代码更加可复用、可扩展和易维护。理解和掌握多态,可以有效提升编写高质量 C++ 程序的能力。

6. 构造函数和析构函数

6.1 构造函数

构造函数是与类同名的特殊成员函数,在对象创建时自动调用,用于初始化对象的成员变量。构造函数可以被重载,这意味着一个类可以有多个构造函数,它们的参数不同。构造函数没有返回值。

示例:
class MyClass {
public:
    int x;

    // 构造函数
    MyClass(int val) {
        x = val;
    }

    // 重载构造函数
    MyClass() {
        x = 0;
    }
};

在这个示例中,MyClass 类定义了两个构造函数。一个构造函数接受一个整数参数并将其赋值给成员变量 x,另一个构造函数没有参数,并将 x 初始化为 0。

构造函数的初始化列表

构造函数还可以使用初始化列表来初始化成员变量。这在需要初始化常量成员或引用成员时特别有用。

class MyClass {
public:
    const int x;
    int& y;

    // 构造函数使用初始化列表
    MyClass(int val, int& ref) : x(val), y(ref) {}
};

在这个示例中,使用初始化列表初始化 const 成员 x 和引用成员 y

6.2 析构函数

析构函数是类的特殊成员函数,当对象的生命周期结束时自动调用,用于清理资源。析构函数的名称前带有波浪号(~),且没有参数。一个类只能有一个析构函数。

示例:
class MyClass {
public:
    // 构造函数
    MyClass() {
        cout << "Constructor called" << endl;
    }

    // 析构函数
    ~MyClass() {
        cout << "Destructor called" << endl;
    }
};

在这个示例中,当创建 MyClass 对象时,将调用构造函数输出 “Constructor called”。当对象的生命周期结束时,将调用析构函数输出 “Destructor called”。

6.3 构造函数和析构函数的用途

构造函数的用途

  1. 初始化成员变量:构造函数用于初始化对象的成员变量。
  2. 分配资源:构造函数可以分配对象需要的资源,例如动态内存、文件句柄等。
  3. 执行任何启动代码:在对象开始使用之前,构造函数可以执行任何需要的启动代码。

析构函数的用途

  1. 释放资源:析构函数用于释放对象占用的资源,例如动态内存、文件句柄等。
  2. 执行清理代码:析构函数可以执行任何需要的清理代码,例如关闭文件、断开网络连接等。

6.4 默认构造函数和默认析构函数

如果没有定义构造函数,编译器将提供一个默认的无参数构造函数。如果没有定义析构函数,编译器将提供一个默认析构函数。这些默认函数执行默认的初始化和清理工作。

示例:
class MyClass {
public:
    int x;
    // 没有定义构造函数,编译器将提供一个默认的无参数构造函数
    // 没有定义析构函数,编译器将提供一个默认析构函数
};

int main() {
    MyClass obj; // 调用默认构造函数
    return 0; // 调用默认析构函数
}

6.5 拷贝构造函数

拷贝构造函数用于创建对象的副本。它的参数是对另一个同类对象的引用。拷贝构造函数在以下情况中调用:

  1. 对象以值传递的方式传入函数
  2. 对象以值传递的方式从函数返回
  3. 一个对象用于初始化另一个对象
示例:
class MyClass {
public:
    int x;

    // 构造函数
    MyClass(int val) : x(val) {}

    // 拷贝构造函数
    MyClass(const MyClass& obj) {
        x = obj.x;
        cout << "Copy constructor called" << endl;
    }
};

void function(MyClass obj) {
    // 传递对象将调用拷贝构造函数
}

int main() {
    MyClass obj1(10);
    MyClass obj2 = obj1; // 使用拷贝构造函数
    function(obj1); // 使用拷贝构造函数
    return 0;
}

在这个示例中,MyClass 类定义了一个拷贝构造函数,当创建 obj2 以及将 obj1 传递给函数 function 时,调用拷贝构造函数。

6.6 析构函数的细节

析构函数在以下情况中调用:

  1. 对象的生命周期结束时
  2. 对象从作用域中离开时
  3. 调用 delete 运算符时

析构函数的主要目的是执行清理工作,确保对象占用的资源被正确释放。

示例:
class MyClass {
public:
    int* ptr;

    // 构造函数
    MyClass(int val) {
        ptr = new int(val);
    }

    // 析构函数
    ~MyClass() {
        delete ptr;
        cout << "Destructor called, memory released" << endl;
    }
};

int main() {
    MyClass obj(10);
    return 0; // obj 的生命周期结束,调用析构函数
}

在这个示例中,MyClass 的析构函数释放了动态分配的内存,确保资源不会泄漏。

通过理解构造函数和析构函数,可以更好地管理对象的生命周期,确保资源的正确分配和释放。掌握这些概念是编写高效、健壮的 C++ 程序的基础。

7. 拷贝构造函数

拷贝构造函数用于创建对象的副本。它的参数是对另一个同类对象的引用。在 C++ 中,拷贝构造函数在以下情况中自动调用:

  1. 对象以值传递的方式传入函数
  2. 对象以值传递的方式从函数返回
  3. 一个对象用于初始化另一个对象

拷贝构造函数的主要作用是确保在复制对象时正确地复制对象的成员,尤其是在对象包含指针成员或需要深拷贝的情况下。

7.1 拷贝构造函数的定义

拷贝构造函数的定义形式如下:

class ClassName {
public:
    ClassName(const ClassName& obj); // 拷贝构造函数
};

7.2 示例代码

以下是一个包含指针成员的类 MyClass 的示例,展示了如何定义和使用拷贝构造函数:

class MyClass {
public:
    int* p;

    // 构造函数
    MyClass(int val) {
        p = new int(val);
    }

    // 拷贝构造函数
    MyClass(const MyClass& obj) {
        p = new int(*obj.p);
    }

    // 析构函数
    ~MyClass() {
        delete p;
    }
};

在这个示例中,MyClass 类包含一个指针成员 p。构造函数分配动态内存并初始化 p。拷贝构造函数创建一个新的动态内存区域,并复制原对象中的数据。析构函数释放动态内存,以防止内存泄漏。

7.3 示例代码解释

  1. 构造函数MyClass(int val) 用于初始化对象的成员 p,它分配了动态内存并将其初始化为 val
  2. 拷贝构造函数MyClass(const MyClass& obj) 用于创建对象的副本。它通过 new int(*obj.p) 分配新的内存,并将原对象的值复制到新对象中。
  3. 析构函数~MyClass() 释放动态分配的内存,防止内存泄漏。

7.4 使用拷贝构造函数

以下是一个使用拷贝构造函数的示例,展示了如何创建对象副本并确保正确的内存管理:

int main() {
    MyClass obj1(10);  // 调用构造函数
    MyClass obj2 = obj1;  // 调用拷贝构造函数

    cout << "obj1.p: " << *obj1.p << endl;  // 输出: 10
    cout << "obj2.p: " << *obj2.p << endl;  // 输出: 10

    return 0;  // 调用析构函数
}

在这个示例中,obj1 使用构造函数进行初始化,而 obj2 使用拷贝构造函数进行初始化。两者的指针成员 p 指向不同的内存地址,但其内容相同。

7.5 浅拷贝与深拷贝

浅拷贝:只复制对象的非指针成员,对于指针成员,只复制指针的地址。这会导致多个对象共享同一块内存,可能引发内存管理问题。

深拷贝:不仅复制对象的非指针成员,还复制指针所指向的内容。这需要为每个指针成员分配新的内存并复制原对象的内容。

浅拷贝示例:
class ShallowCopy {
public:
    int* p;

    // 构造函数
    ShallowCopy(int val) {
        p = new int(val);
    }

    // 拷贝构造函数 (浅拷贝)
    ShallowCopy(const ShallowCopy& obj) {
        p = obj.p;
    }

    // 析构函数
    ~ShallowCopy() {
        delete p;
    }
};

在这个浅拷贝示例中,拷贝构造函数只复制指针的地址,这会导致多个对象共享同一块内存。

深拷贝示例:
class DeepCopy {
public:
    int* p;

    // 构造函数
    DeepCopy(int val) {
        p = new int(val);
    }

    // 拷贝构造函数 (深拷贝)
    DeepCopy(const DeepCopy& obj) {
        p = new int(*obj.p);
    }

    // 析构函数
    ~DeepCopy() {
        delete p;
    }
};

在这个深拷贝示例中,拷贝构造函数分配新的内存并复制原对象的内容,确保每个对象拥有独立的内存空间。

7.6 拷贝赋值运算符

除了拷贝构造函数,还有一个相关的运算符——拷贝赋值运算符(copy assignment operator),它用于将一个对象的内容赋值给另一个已经存在的对象。

class MyClass {
public:
    int* p;

    // 构造函数
    MyClass(int val) {
        p = new int(val);
    }

    // 拷贝构造函数
    MyClass(const MyClass& obj) {
        p = new int(*obj.p);
    }

    // 拷贝赋值运算符
    MyClass& operator=(const MyClass& obj) {
        if (this == &obj) return *this; // 防止自我赋值
        delete p; // 释放旧内存
        p = new int(*obj.p); // 分配新内存并复制内容
        return *this;
    }

    // 析构函数
    ~MyClass() {
        delete p;
    }
};

在这个示例中,拷贝赋值运算符 operator= 先释放对象当前持有的内存,然后分配新内存并复制内容,确保不会发生内存泄漏。

通过理解和正确使用拷贝构造函数、浅拷贝与深拷贝以及拷贝赋值运算符,可以有效管理对象的复制过程,确保资源正确分配和释放,避免内存泄漏和其他潜在问题。

8. 操作符重载

操作符重载允许我们为用户自定义类型定义特定的操作行为。通过重载操作符,可以使用户自定义类型对象之间的操作更加直观和易于使用。

8.1 操作符重载的基本语法

操作符重载是在类内部定义的特殊成员函数或在类外部定义的友元函数。其语法如下:

class ClassName {
public:
    // 成员函数形式
    ReturnType operator op (const ClassName& obj);

    // 友元函数形式
    friend ReturnType operator op (const ClassName& lhs, const ClassName& rhs);
};

8.2 示例代码

以下是一个复数类 Complex 的示例,展示了如何重载加法操作符 + 以便于复数对象相加:

#include <iostream>
using namespace std;

class Complex {
private:
    float real;
    float imag;

public:
    // 默认构造函数
    Complex() : real(0), imag(0) {}

    // 参数化构造函数
    Complex(float r, float i) : real(r), imag(i) {}

    // 重载加法操作符
    Complex operator + (const Complex& obj) {
        return Complex(real + obj.real, imag + obj.imag);
    }

    // 打印复数
    void print() {
        cout << real << " + " << imag << "i" << endl;
    }
};

int main() {
    Complex c1(3.0, 4.0);
    Complex c2(1.0, 2.0);
    Complex c3 = c1 + c2;

    c1.print(); // 输出: 3.0 + 4.0i
    c2.print(); // 输出: 1.0 + 2.0i
    c3.print(); // 输出: 4.0 + 6.0i

    return 0;
}

8.3 示例代码解释

  1. 构造函数

    • Complex():默认构造函数,将 realimag 初始化为 0。
    • Complex(float r, float i):参数化构造函数,用于初始化 realimag
  2. 操作符重载

    • Complex operator + (const Complex& obj):重载 + 操作符,使得两个 Complex 对象可以相加。该函数返回一个新的 Complex 对象,其 realimag 成员分别是两个操作数的 realimag 成员之和。
  3. 打印函数

    • void print():用于打印复数的值。

8.4 其他操作符重载示例

除了加法操作符,可以重载许多其他操作符,如减法、乘法、除法、赋值、比较等。以下是一些常见的操作符重载示例:

8.4.1 减法操作符 -
Complex operator - (const Complex& obj) {
    return Complex(real - obj.real, imag - obj.imag);
}
8.4.2 乘法操作符 *
Complex operator * (const Complex& obj) {
    return Complex(real * obj.real - imag * obj.imag, real * obj.imag + imag * obj.real);
}
8.4.3 赋值操作符 =
Complex& operator = (const Complex& obj) {
    if (this != &obj) { // 防止自我赋值
        real = obj.real;
        imag = obj.imag;
    }
    return *this;
}
8.4.4 比较操作符 ==
bool operator == (const Complex& obj) const {
    return (real == obj.real && imag == obj.imag);
}

8.5 友元函数形式的操作符重载

有些操作符需要访问类的私有成员,但不能作为成员函数实现,例如二元运算符 +。此时可以使用友元函数形式的操作符重载:

class Complex {
private:
    float real;
    float imag;

public:
    Complex() : real(0), imag(0) {}
    Complex(float r, float i) : real(r), imag(i) {}

    // 声明友元函数
    friend Complex operator + (const Complex& lhs, const Complex& rhs);

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

// 友元函数形式的操作符重载
Complex operator + (const Complex& lhs, const Complex& rhs) {
    return Complex(lhs.real + rhs.real, lhs.imag + rhs.imag);
}

8.6 操作符重载的注意事项

  1. 不能改变操作符的优先级和结合性:操作符重载不能改变操作符的优先级和结合性。
  2. 保持操作符的自然语义:操作符重载应尽量保持操作符的自然语义,以便代码易于理解和维护。
  3. 避免滥用操作符重载:过度或不恰当的操作符重载可能导致代码混乱和难以维护,需谨慎使用。

通过理解和应用操作符重载,可以使自定义类型的对象操作更加直观和易用,提高代码的可读性和易维护性。掌握这些技巧是编写高效、优雅的 C++ 程序的重要技能。

9. 友元函数

友元函数是一种特殊的函数,即使它不是类的成员,也可以访问该类的私有和保护成员。通过使用关键字 friend,可以将一个函数声明为类的友元函数。友元函数可以是普通函数、成员函数或友元类中的成员函数。

9.1 友元函数的定义

友元函数在类的定义中使用 friend 关键字声明:

class ClassName {
private:
    // 私有成员
    int privateVar;

public:
    // 友元函数声明
    friend void friendFunction(ClassName& obj);
};

友元函数的实现不需要包含 friend 关键字:

void friendFunction(ClassName& obj) {
    // 可以访问 obj 的私有成员
    cout << obj.privateVar << endl;
}

9.2 示例代码

以下是一个示例,展示了如何定义和使用友元函数:

#include <iostream>
using namespace std;

class Box {
private:
    double width;

public:
    // 友元函数声明
    friend void printWidth(Box box);

    // 设置宽度的方法
    void setWidth(double w) {
        width = w;
    }
};

// 友元函数实现
void printWidth(Box box) {
    cout << "Width of box: " << box.width << endl;
}

int main() {
    Box box;
    box.setWidth(10.5);
    printWidth(box); // 输出: Width of box: 10.5

    return 0;
}

9.3 示例代码解释

  1. 类定义Box 类包含一个私有成员变量 width 和一个公共方法 setWidth 用于设置 width 的值。
  2. 友元函数声明:在 Box 类中使用 friend 关键字声明了友元函数 printWidth,使其可以访问 Box 类的私有成员 width
  3. 友元函数实现printWidth 函数定义在类的外部,但由于它是友元函数,可以访问 Box 对象的私有成员 width
  4. 使用友元函数:在 main 函数中,创建 Box 对象 box,设置其宽度为 10.5,然后调用友元函数 printWidth 输出宽度。

9.4 友元类

友元类是另一个类,可以访问指定类的私有和保护成员。在类的定义中使用 friend class 关键字声明友元类。

示例:
class Box {
private:
    double width;

public:
    // 声明友元类
    friend class BoxFriend;

    // 设置宽度的方法
    void setWidth(double w) {
        width = w;
    }
};

class BoxFriend {
public:
    void showWidth(Box& box) {
        // 访问 Box 的私有成员
        cout << "Width of box: " << box.width << endl;
    }
};

int main() {
    Box box;
    box.setWidth(10.5);
    BoxFriend friendObj;
    friendObj.showWidth(box); // 输出: Width of box: 10.5

    return 0;
}

在这个示例中,BoxFriend 类是 Box 类的友元类,因此可以访问 Box 的私有成员 width

9.5 友元函数和友元类的用途

友元函数和友元类在以下情况下特别有用:

  1. 操作符重载:某些操作符重载需要访问类的私有成员,而不适合作为类的成员函数实现。
  2. 复杂数据结构:某些数据结构(如链表、树等)需要访问彼此的私有成员,友元类可以简化这种实现。
  3. 调试和测试:友元函数可以用于调试和测试类的私有成员,提供更灵活的访问控制。

9.6 友元的注意事项

  1. 破坏封装:友元函数和友元类可以访问私有成员,破坏了类的封装性,因此应谨慎使用。
  2. 维护复杂性:过多的友元声明可能导致代码维护复杂性增加,因为类的内部实现细节暴露给了外部函数或类。
  3. 单向关系:友元关系是单向的,即 ClassA 的友元 ClassB 可以访问 ClassA 的私有成员,但 ClassA 无法访问 ClassB 的私有成员,除非 ClassB 也将 ClassA 声明为友元。

通过理解和正确使用友元函数和友元类,可以在需要时突破访问控制的限制,同时保持代码的清晰和结构性。然而,友元应当谨慎使用,以避免破坏类的封装性和增加代码的复杂性。

10. 抽象类和纯虚函数

抽象类是包含一个或多个纯虚函数的类。纯虚函数在基类中声明但不实现,要求派生类实现这些函数。抽象类不能实例化,但可以用于创建指向派生类对象的指针或引用。抽象类用于定义接口,强制派生类实现特定的功能。

10.1 抽象类

抽象类是不能实例化的类,它包含一个或多个纯虚函数。抽象类用于定义接口,并且在面向对象设计中起到了非常重要的作用。

示例:
class AbstractClass {
public:
    // 纯虚函数
    virtual void pureVirtualFunction() = 0;
};

在这个示例中,AbstractClass 是一个抽象类,因为它包含一个纯虚函数 pureVirtualFunction

10.2 纯虚函数

纯虚函数是在基类中声明但不实现的函数,表示该函数必须在派生类中实现。纯虚函数的语法是在函数声明后加上 = 0

示例:
class AbstractClass {
public:
    // 纯虚函数
    virtual void pureVirtualFunction() = 0;
};

10.3 派生类

派生类从抽象类继承,并实现所有纯虚函数。只有在实现了所有纯虚函数后,派生类才能实例化。

示例:
class DerivedClass : public AbstractClass {
public:
    // 实现纯虚函数
    void pureVirtualFunction() override {
        cout << "Implementation of pure virtual function" << endl;
    }
};

在这个示例中,DerivedClassAbstractClass 继承,并实现了纯虚函数 pureVirtualFunction

10.4 使用抽象类和纯虚函数

抽象类和纯虚函数用于定义接口和实现多态性。在面向对象设计中,通过抽象类可以确保所有派生类都实现特定的功能。

示例:
class AbstractClass {
public:
    // 纯虚函数
    virtual void pureVirtualFunction() = 0;
};

class DerivedClass : public AbstractClass {
public:
    // 实现纯虚函数
    void pureVirtualFunction() override {
        cout << "Implementation of pure virtual function" << endl;
    }
};

int main() {
    DerivedClass obj;
    obj.pureVirtualFunction(); // 输出: Implementation of pure virtual function

    AbstractClass* ptr = new DerivedClass();
    ptr->pureVirtualFunction(); // 输出: Implementation of pure virtual function
    delete ptr;

    return 0;
}

在这个示例中,我们定义了一个抽象类 AbstractClass 和一个派生类 DerivedClass。在 main 函数中,我们创建了 DerivedClass 对象,并调用了实现的纯虚函数。我们还演示了如何使用抽象类指针指向派生类对象,实现多态性。

10.5 抽象类和接口的区别

在 C++ 中,抽象类和接口的概念非常相似。事实上,C++ 中没有专门的接口概念,而是通过抽象类来实现接口的功能。一个纯抽象类(只包含纯虚函数且没有任何数据成员或非纯虚函数)可以看作是一个接口。

示例:
class Interface {
public:
    // 纯虚函数,相当于接口的方法
    virtual void method1() = 0;
    virtual void method2() = 0;
};

class Implementation : public Interface {
public:
    // 实现接口的方法
    void method1() override {
        cout << "Implementation of method1" << endl;
    }

    void method2() override {
        cout << "Implementation of method2" << endl;
    }
};

在这个示例中,Interface 类相当于一个接口,它定义了两个纯虚函数。Implementation 类实现了这些函数。

10.6 抽象类的优点

  1. 定义接口:抽象类用于定义接口,强制派生类实现特定的功能。
  2. 实现多态性:通过抽象类指针或引用,可以实现运行时多态性。
  3. 提高代码的可维护性:通过定义明确的接口,抽象类使得代码更易于理解和维护。

10.7 使用抽象类的注意事项

  1. 不能实例化:抽象类不能直接实例化,只能作为基类使用。
  2. 必须实现纯虚函数:派生类必须实现所有纯虚函数,否则派生类也将成为抽象类,无法实例化。
  3. 合理设计接口:在设计抽象类时,应合理定义接口,确保其具有通用性和扩展性。

通过理解和应用抽象类和纯虚函数,可以有效地设计和实现面向对象的程序,提高代码的可复用性、可维护性和扩展性。掌握这些概念是编写高质量 C++ 程序的重要技能。

11. 标准模板库(STL)

标准模板库(STL)提供了一套通用的类和函数模板,包括容器、算法和迭代器。STL 使得 C++ 编程更加简洁和高效,能够更方便地处理数据集合、执行常见的算法操作以及遍历数据。

11.1 容器

STL 容器用于存储和管理数据集合。常用的容器包括 vectorlistdequesetmap 等。

vector 示例

vector 是一种动态数组,能够自动调整大小。

#include <vector>
#include <iostream>

int main() {
    std::vector<int> vec;
    vec.push_back(1);
    vec.push_back(2);
    vec.push_back(3);

    for (int i : vec) {
        std::cout << i << std::endl;
    }
    return 0;
}
其他容器示例
list 示例

list 是一种双向链表,适合频繁的插入和删除操作。

#include <list>
#include <iostream>

int main() {
    std::list<int> lst = {1, 2, 3, 4, 5};

    for (int i : lst) {
        std::cout << i << std::endl;
    }
    return 0;
}
map 示例

map 是一种关联容器,存储键值对,使用红黑树实现。

#include <map>
#include <iostream>

int main() {
    std::map<int, std::string> map;
    map[1] = "one";
    map[2] = "two";
    map[3] = "three";

    for (const auto& pair : map) {
        std::cout << pair.first << ": " << pair.second << std::endl;
    }
    return 0;
}

11.2 算法

STL 算法是一组通用的算法,如排序、搜索、复制、替换等。它们通过函数模板实现,能够对不同类型的数据集合进行操作。

sort 示例

sort 算法用于对容器中的元素进行排序。

#include <algorithm>
#include <vector>
#include <iostream>

int main() {
    std::vector<int> vec = {3, 1, 4, 1, 5, 9};
    std::sort(vec.begin(), vec.end());

    for (int i : vec) {
        std::cout << i << std::endl;
    }
    return 0;
}
其他算法示例
find 示例

find 算法用于在容器中查找指定元素。

#include <algorithm>
#include <vector>
#include <iostream>

int main() {
    std::vector<int> vec = {3, 1, 4, 1, 5, 9};
    auto it = std::find(vec.begin(), vec.end(), 4);

    if (it != vec.end()) {
        std::cout << "Element found at position: " << std::distance(vec.begin(), it) << std::endl;
    } else {
        std::cout << "Element not found" << std::endl;
    }
    return 0;
}
for_each 示例

for_each 算法用于对容器中的每个元素执行指定操作。

#include <algorithm>
#include <vector>
#include <iostream>

void printElement(int i) {
    std::cout << i << " ";
}

int main() {
    std::vector<int> vec = {3, 1, 4, 1, 5, 9};
    std::for_each(vec.begin(), vec.end(), printElement);
    return 0;
}

11.3 迭代器

迭代器用于遍历容器中的元素。STL 提供了多种迭代器,如输入迭代器、输出迭代器、前向迭代器、双向迭代器和随机访问迭代器。

vector 迭代器示例

使用 vector 迭代器遍历元素。

#include <vector>
#include <iostream>

int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5};

    for (std::vector<int>::iterator it = vec.begin(); it != vec.end(); ++it) {
        std::cout << *it << std::endl;
    }
    return 0;
}
其他迭代器示例
list 迭代器示例

使用 list 迭代器遍历元素。

#include <list>
#include <iostream>

int main() {
    std::list<int> lst = {1, 2, 3, 4, 5};

    for (std::list<int>::iterator it = lst.begin(); it != lst.end(); ++it) {
        std::cout << *it << std::endl;
    }
    return 0;
}
map 迭代器示例

使用 map 迭代器遍历键值对。

#include <map>
#include <iostream>

int main() {
    std::map<int, std::string> map;
    map[1] = "one";
    map[2] = "two";
    map[3] = "three";

    for (std::map<int, std::string>::iterator it = map.begin(); it != map.end(); ++it) {
        std::cout << it->first << ": " << it->second << std::endl;
    }
    return 0;
}

11.4 总结

STL 提供了强大且灵活的工具集,使得 C++ 编程更高效、更简洁。通过理解和使用 STL 的容器、算法和迭代器,可以大大简化数据处理和算法实现的过程,提高代码的可读性和可维护性。掌握 STL 是编写高效 C++ 程序的重要技能。

结论

C++ 的面向对象编程通过类和对象的概念组织代码,并利用封装、继承和多态等特性提高代码的可重用性、可维护性和可扩展性。掌握这些概念和技巧对于编写高效、结构良好的 C++ 程序至关重要。

  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值