C++ 封装、继承、多态和抽象

一.封装

封装(Encapsulation)是面向对象编程(OOP)的核心原则之一,旨在通过隐藏对象的内部状态,保护其不受外界的直接访问和修改。C++ 中的封装通常通过访问控制符(private, protected, public)实现,结合类和对象,限制外部对类内部数据的访问。

封装的概念

  1. 定义
    封装是将数据(属性)和行为(方法)组合在一起,使得外部只能通过公开的方法访问类的内部数据,而无法直接访问或修改这些数据。

  2. 目的

    • 安全性:防止外部代码对类内部数据的误操作。
    • 简化接口:隐藏实现细节,只暴露必要的接口,使得类更易于使用。
    • 数据保护:允许开发者通过定义的接口控制数据的访问方式。

C++ 中的封装实现

封装通过类(class)和结构体(struct)的访问控制来实现。C++ 提供了三种访问控制修饰符:

  • private:私有成员只能在类的内部(即类的成员函数和友元函数)访问,外部代码无法访问。
  • protected:受保护的成员可以在类的内部访问,也可以在派生类中访问,但外部代码无法访问。
  • public:公共成员可以被任何代码访问。
访问控制的示例:

#include <iostream>
using namespace std;

class Person {
private:
    string name;   // 私有成员变量,外部无法直接访问
    int age;

public:
    // 构造函数
    Person(string n, int a) : name(n), age(a) {}

    // 公有成员函数,用于访问和修改私有变量
    void setName(string n) {
        name = n;
    }

    string getName() {
        return name;
    }

    void setAge(int a) {
        age = a;
    }

    int getAge() {
        return age;
    }

    // 公有成员函数,用于显示信息
    void displayInfo() {
        cout << "Name: " << name << ", Age: " << age << endl;
    }
};

int main() {
    // 创建对象
    Person person("Alice", 25);

    // 调用公有函数来访问私有数据
    person.displayInfo();

    // 修改私有数据
    person.setAge(26);
    person.displayInfo();

    return 0;
}
输出:

Name: Alice, Age: 25
Name: Alice, Age: 26

解释:
  • Person 中的成员变量 nameage 被声明为 private,因此无法从类外部直接访问或修改它们。
  • 通过公有的 setName, getName, setAge, getAge 方法,外部可以间接地访问和修改这些私有变量。
封装的好处
  1. 信息隐藏:将实现细节隐藏在类内部,防止外部代码直接依赖实现细节,使得类的内部实现可以随时修改而不影响使用者。
  2. 易于维护:由于类的使用者只依赖于公共接口,开发者可以自由地修改类的实现,而不需要担心破坏代码的其他部分。
  3. 提高代码安全性:通过封装,可以严格控制外部对类内部数据的访问,减少潜在的错误和数据不一致的风险。
封装的最佳实践
  1. 尽量将成员变量声明为 private,通过公有方法(getter/setter)来进行访问。
  2. 接口稳定:设计类时,确保公开的接口(public 方法)是稳定的,并尽量避免对外暴露类的内部实现。
  3. 避免暴露实现细节:不要通过接口直接暴露复杂的内部数据结构,尽量返回值对象或常量引用,防止外部修改内部状态。

进阶封装:只读和只写属性

在某些情况下,类可能需要只读或只写的属性。通过只定义 gettersetter,可以实现这种访问控制。

示例:只读属性

class ReadOnlyExample {
private:
    int value;

public:
    ReadOnlyExample(int v) : value(v) {}

    // 只读:没有提供 setter 方法
    int getValue() const {
        return value;
    }
};

封装的常见问题

  1. 过度封装:封装应该适度,过度封装会导致接口复杂,降低代码的可读性和可维护性。
  2. 直接暴露内部实现:一些开发者可能通过返回指针或引用直接暴露内部数据,导致封装失效。应避免这种做法。
错误示例:返回指针暴露内部数据

class BadExample {
private:
    int* data;

public:
    int* getData() {
        return data;  // 直接暴露了内部数据的指针
    }
};

在这种情况下,外部代码可以通过返回的指针直接修改类的私有数据,违反了封装原则。更好的做法是返回数据的副本或使用常量引用。

封装与继承、组合的关系

封装并不意味着不允许继承。在面向对象编程中,子类可以继承父类的封装机制,并通过 protected 访问控制实现部分封装。例如,子类可以访问父类的 protected 成员,但无法访问 private 成员。

此外,封装和组合也紧密相关。组合是一种将其他对象作为成员变量的设计模式,通过组合可以实现更强的封装,因为对象的内部状态完全由其所包含的对象管理。

示例:封装与组合

class Engine {
private:
    int horsepower;

public:
    Engine(int hp) : horsepower(hp) {}

    int getHorsepower() const {
        return horsepower;
    }
};

class Car {
private:
    Engine engine;

public:
    Car(int hp) : engine(hp) {}

    int getCarHorsepower() const {
        return engine.getHorsepower();
    }
};

总结

C++ 的封装通过类和访问控制符实现,确保类的内部状态不会被外部直接修改,从而提高了代码的安全性和可维护性。在设计类时,应该遵循封装的原则,隐藏实现细节,并通过公有接口与外界交互。

封装的核心要点:
  1. 信息隐藏:使用 privateprotected 限制外部访问。
  2. 数据安全性:通过公有方法(getter/setter)控制对数据的访问。
  3. 易于维护:隐藏实现细节,减少外部依赖,提高代码的可维护性。

二.继承

继承的基本概念

继承是通过定义一个子类(派生类)父类(基类)继承其成员(数据和方法)。子类可以:

  • 直接使用父类的成员
  • 增加新的成员
  • 重写父类的方法

#include <iostream>
using namespace std;

class Animal {
public:
    void eat() {
        cout << "Eating..." << endl;
    }
};

class Dog : public Animal {
public:
    void bark() {
        cout << "Barking..." << endl;
    }
};

int main() {
    Dog dog;
    dog.eat();   // 继承自父类的函数
    dog.bark();  // 子类自己的函数
    return 0;
}
输出:

Eating...
Barking...

1. 继承的访问控制

除了之前提到的 publicprotectedprivate 继承方式,这里再详细阐述不同继承方式对父类成员的影响:

1.1 public 继承
  • 父类的 public 成员在子类中保持 public
  • 父类的 protected 成员在子类中保持 protected
  • 父类的 private 成员无法在子类中直接访问(除非通过父类的 protectedpublic 成员进行间接访问)。
1.2 protected 继承
  • 父类的 public 成员在子类中变为 protected
  • 父类的 protected 成员在子类中保持 protected
  • 父类的 private 成员无法在子类中直接访问。
1.3 private 继承
  • 父类的 publicprotected 成员在子类中都变为 private
  • 子类的派生类无法访问任何父类的成员,除非通过子类对父类成员的封装方法。
访问控制示例:

class Base {
public:
    int public_member;
protected:
    int protected_member;
private:
    int private_member;
};

class DerivedPublic : public Base {
    // public_member 是 public
    // protected_member 是 protected
    // private_member 无法访问
};

class DerivedProtected : protected Base {
    // public_member 是 protected
    // protected_member 是 protected
    // private_member 无法访问
};

class DerivedPrivate : private Base {
    // public_member 是 private
    // protected_member 是 private
    // private_member 无法访问
};

2. 构造函数与析构函数的继承

2.1 构造函数的执行顺序

构造函数的执行顺序是从基类到派生类,这意味着:

  • 基类的构造函数先执行,完成基类部分的初始化。
  • 然后执行派生类的构造函数,初始化派生类部分。
2.2 析构函数的执行顺序

析构函数的执行顺序正好相反:

  • 派生类的析构函数先执行,清理派生类部分的资源。
  • 然后执行基类的析构函数,清理基类部分的资源。
2.3 显式调用基类构造函数

在派生类的构造函数中可以显式调用基类的构造函数(包括带参数的构造函数):

class Base {
public:
    Base(int x) {
        cout << "Base constructor with x: " << x << endl;
    }
};

class Derived : public Base {
public:
    Derived(int x, int y) : Base(x) {  // 显式调用 Base 的构造函数
        cout << "Derived constructor with y: " << y << endl;
    }
};
 

2.4 基类构造函数的继承(C++11)

C++11 允许派生类继承基类的构造函数,简化了派生类对基类构造函数的调用:

class Base {
public:
    Base(int x) { cout << "Base constructor" << endl; }
};

class Derived : public Base {
    using Base::Base;  // 继承构造函数
};

int main() {
    Derived d(5);  // 相当于调用 Base 的构造函数
    return 0;
}

2.5 基类析构函数的虚拟化

如果你希望通过基类指针删除派生类对象,那么基类的析构函数必须是虚函数。否则,派生类的析构函数不会被调用,可能导致内存泄漏。

class Base {
public:
    virtual ~Base() { cout << "Base destructor" << endl; }  // 虚析构函数
};

class Derived : public Base {
public:
    ~Derived() { cout << "Derived destructor" << endl; }
};

int main() {
    Base* basePtr = new Derived();
    delete basePtr;  // 调用 Derived 和 Base 的析构函数
    return 0;
}

3. 虚继承的深度理解

虚继承解决的是菱形继承中父类成员重复的问题。在 C++ 中,虚继承的机制保证最顶层的基类只有一个副本。对于多个派生类继承同一个基类,并且子类再次继承这些派生类的情况下,虚继承保证基类的成员不会重复。

3.1 虚继承的语法:

class A {
public:
    int x;
};

class B : virtual public A { };
class C : virtual public A { };

class D : public B, public C {  // 虚继承,D 只有一个 A 的副本
};

3.2 虚继承中的构造函数顺序

在虚继承中,最顶层的基类必须由最派生类(如上例的 D)负责调用其构造函数,而不是中间层的派生类(如 BC)。这使得基类只初始化一次。

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

class B : virtual public A {
public:
    B(int x) : A(x) { }
};

class C : virtual public A {
public:
    C(int x) : A(x) { }
};

class D : public B, public C {
public:
    D(int x, int y) : A(x), B(x), C(x) { }  // 由 D 来初始化 A
};

4. 虚函数、纯虚函数与抽象类

4.1 虚函数

虚函数允许派生类重写基类的函数,并通过基类指针调用派生类的重写版本。多态性的关键依赖于虚函数。

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

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

int main() {
    Base* ptr = new Derived();
    ptr->display();  // 输出:Derived display
    delete ptr;
}

4.2 纯虚函数

纯虚函数是没有实现的虚函数,需要在派生类中实现。定义了纯虚函数的类称为抽象类,抽象类无法被实例化。

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

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

5. 多态与继承的高级技巧

5.1 使用多态的优点
  • 灵活性:通过基类接口操作对象,无需关心对象的具体类型。
  • 代码重用:只需编写一个函数,便可以处理基类和所有派生类的对象。
5.2 RTTI(运行时类型识别)

C++ 提供了 dynamic_cast 关键字,可以在运行时进行类型安全的转换。它通常用于指针或引用的向下转换,即从基类指针转换为派生类指针。

class Base { virtual void func() {} };
class Derived : public Base { };

int main() {
    Base* basePtr = new Derived();
    Derived* derivedPtr = dynamic_cast<Derived*>(basePtr);
    if (derivedPtr) {
        cout << "Successful cast to Derived" << endl;
    } else {
        cout << "Failed cast" << endl;
    }
    delete basePtr;
}

5.3 对象切片(Object Slicing)

当用基类的对象而非指针或引用存储派生类对象时,会发生对象切片,即派生类对象的成员变量或成员函数会被切掉,仅保留基类部分。

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

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

int main() {
    Derived d;
    Base b = d;  // 对象切片,Derived 的 show 被切掉
    b.show();    // 输出:Base show
}

6. C++11 新特性在继承中的应用

6.1 overridefinal

C++11 引入了 overridefinal 关键字,以增强虚函数的管理:

  • override 确保派生类正确地覆盖基类的虚函数。
  • final 阻止派生类进一步覆盖虚函数。

class Base {
public:
    virtual void foo() const;
    virtual void bar() final;
};

class Derived : public Base {
public:
    void foo() const override;  // 正确覆盖
    void bar() override;        // 错误,bar 是 final
};

6.2 defaultdelete
  • = default 可以显式声明编译器生成默认的构造函数、析构函数等。
  • = delete 可以禁止某些函数的使用。
  • class Base {
    public:
        Base() = default;  // 默认构造函数
        Base(const Base&) = delete;  // 禁止拷贝构造
    };
     

三.多态

1. 多态的基本概念

1.1 编译时多态(静态多态)

编译时多态指的是函数重载、运算符重载和模板,函数的调用在编译时被决定。它可以通过以下几种方式实现:

  • 函数重载(Function Overloading):同一个函数名可以有不同的参数签名。
  • 运算符重载(Operator Overloading):为自定义类型定义新的运算符行为。
  • 模板(Templates):模板是泛型编程的一种方式,允许在编译时生成代码。

示例

#include <iostream>
using namespace std;

class Calculator {
public:
    int add(int a, int b) {
        return a + b;
    }
    
    double add(double a, double b) {
        return a + b;
    }
};

int main() {
    Calculator calc;
    cout << calc.add(3, 4) << endl;       // 输出:7
    cout << calc.add(3.5, 4.5) << endl;   // 输出:8
    return 0;
}

1.2 运行时多态(动态多态)

运行时多态是 C++ 的核心特性,通过继承和虚函数来实现。在编译时,基类指针或引用可以指向派生类的对象,但在运行时会根据对象的实际类型调用适当的函数。这种能力使得程序可以在运行时表现出不同的行为。

2. 运行时多态的实现

2.1 虚函数(Virtual Function)

虚函数是实现动态多态的关键。使用 virtual 关键字修饰基类函数,派生类可以通过 override 关键字重写该函数。通过基类指针或引用调用虚函数时,实际会调用派生类的实现。

示例

#include <iostream>
using namespace std;

class Animal {
public:
    virtual void speak() {
        cout << "Animal speaking" << endl;
    }
};

class Dog : public Animal {
public:
    void speak() override {
        cout << "Woof!" << endl;
    }
};

int main() {
    Animal* animal = new Dog();
    animal->speak();  // 输出:Woof!
    delete animal;
    return 0;
}

2.2 override 关键字

C++11 引入了 override 关键字,明确表明派生类的函数是在重写基类的虚函数。如果函数签名与基类不匹配,编译器会报错,防止由于签名不匹配导致的错误。

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

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

2.3 纯虚函数与抽象类

如果希望基类只是提供一个接口,而不具体实现某些函数,可以将函数声明为纯虚函数(pure virtual function)。包含纯虚函数的类称为抽象类,无法直接实例化。

纯虚函数语法

class Shape {
public:
    virtual void draw() const = 0;  // 纯虚函数
};

派生类必须实现所有的纯虚函数才能实例化对象。

示例

class Shape {
public:
    virtual void draw() const = 0;  // 纯虚函数
};

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

int main() {
    Shape* shape = new Circle();
    shape->draw();  // 输出:Drawing a circle
    delete shape;
    return 0;
}

3. 析构函数与多态

当通过基类指针删除派生类对象时,如果基类的析构函数不是虚函数,派生类的析构函数不会被调用,可能导致资源泄露。为了避免这种情况,基类的析构函数应当始终声明为虚函数

class Base {
public:
    virtual ~Base() {
        cout << "Base destructor" << endl;
    }
};

class Derived : public Base {
public:
    ~Derived() {
        cout << "Derived destructor" << endl;
    }
};

int main() {
    Base* base = new Derived();
    delete base;  // 调用 Derived 和 Base 的析构函数
    return 0;
}
 

4. 虚函数表(vtable)

C++ 通过虚函数表(virtual table, vtable)实现动态多态。每个类都有一个虚函数表,存储着虚函数的指针。在运行时,虚函数调用通过虚表进行动态绑定。

  • 当对象被创建时,编译器为它分配一个指向虚函数表的指针(vptr)。
  • 当调用虚函数时,编译器使用 vptr 指向虚表中的相应函数地址。

5. 多态的实际应用场景

5.1 框架设计中的接口与实现分离

多态允许设计灵活的接口,派生类可以提供不同的具体实现。比如,图形绘制框架可以有一个通用的 Shape 接口,不同的形状如 CircleSquare 可以提供各自的实现。

示例

class Shape {
public:
    virtual void draw() const = 0;
};

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

class Square : public Shape {
public:
    void draw() const override {
        cout << "Drawing a square" << endl;
    }
};

void renderShape(const Shape& shape) {
    shape.draw();  // 动态绑定到具体的派生类
}

int main() {
    Circle circle;
    Square square;

    renderShape(circle);  // 输出:Drawing a circle
    renderShape(square);  // 输出:Drawing a square

    return 0;
}

5.2 动物行为模拟

通过多态模拟不同的动物行为。例如,不同的动物都有 makeSound() 方法,但发出的声音不同。

示例

#include <iostream>
using namespace std;

class Animal {
public:
    virtual void makeSound() const = 0;
};

class Dog : public Animal {
public:
    void makeSound() const override {
        cout << "Woof!" << endl;
    }
};

class Cat : public Animal {
public:
    void makeSound() const override {
        cout << "Meow!" << endl;
    }
};

int main() {
    Animal* dog = new Dog();
    Animal* cat = new Cat();

    dog->makeSound();  // 输出:Woof!
    cat->makeSound();  // 输出:Meow!

    delete dog;
    delete cat;
    return 0;
}

6. 多态的注意事项

6.1 对象切片

当派生类对象被赋值给基类对象时,派生类的特有部分会被“切掉”,只保留基类的部分。为了避免对象切片,应该通过基类的指针或引用来操作派生类对象。

#include <iostream>
using namespace std;

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

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

int main() {
    Derived d;
    Base b = d;  // 对象切片,b 只保留了 Base 部分
    b.show();    // 输出:Base show
    return 0;
}

6.2 不能多态的函数
  • 构造函数:构造函数不能是虚函数,因为在对象创建时,虚表还未初始化。
  • 静态成员函数:静态成员函数属于类而非对象,因此它不能是虚函数。
总结

C++ 中的多态是通过虚函数和继承机制实现的,它使得程序能够在运行时动态地选择合适的函数调用。这种特性使得代码更加灵活和可扩展,但也带来了一些额外的开销,如虚函数表的管理和运行时的动态绑定。同时,开发者在使用多态时要注意对象切片、析构函数的正确使用等问题。

四.抽象

1. 抽象的基本概念

抽象是一种通过隐藏实现细节来简化代码的方式。在 C++ 中,抽象通常通过定义抽象类来实现,抽象类包含至少一个纯虚函数,这意味着该类不能被实例化,只能作为其他类的基类。

1.1 抽象类(Abstract Class)

抽象类是包含至少一个纯虚函数的类。纯虚函数是没有实现的虚函数,使用 = 0 来表示。抽象类可以包含数据成员、普通成员函数和虚函数,但不能直接创建该类的对象。

示例

class AbstractAnimal {
public:
    virtual void makeSound() = 0;  // 纯虚函数
    virtual void eat() = 0;        // 另一个纯虚函数
};

2. 纯虚函数(Pure Virtual Function)

纯虚函数是没有实现的虚函数,强制派生类必须实现这些函数。通过将函数声明为纯虚函数,可以确保派生类提供具体的实现。

2.1 纯虚函数的声明与实现

纯虚函数在类中声明时,后面需要加 = 0。在派生类中必须重写这些纯虚函数,才能创建该派生类的对象。

示例

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

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

class Square : public Shape {
public:
    void draw() override {
        cout << "Drawing a square" << endl;
    }
};
 

2.2 纯虚函数的使用

在抽象类中,纯虚函数定义了接口,派生类必须实现这些接口。这种方法强制开发者遵循一致的接口规范。

示例

void renderShape(Shape& shape) {
    shape.draw();  // 动态绑定,调用具体实现
}

int main() {
    Circle circle;
    Square square;
    
    renderShape(circle);  // 输出:Drawing a circle
    renderShape(square);  // 输出:Drawing a square
    
    return 0;
}
 

3. 抽象类的特点

  • 不能实例化:抽象类不能创建对象,只能作为基类。
  • 可以包含实现:抽象类可以包含已实现的成员函数和数据成员,这为派生类提供了公共的实现。
  • 用于提供接口:抽象类提供了接口的蓝图,确保所有派生类实现必要的功能。

4. 抽象类与接口的区别

在 C++ 中,抽象类与接口的概念有些许不同:

  • 抽象类:可以包含已实现的成员函数和数据成员,可以有构造函数和析构函数。
  • 接口:通常被认为是一个只包含纯虚函数的抽象类,不能包含任何实现。

5. 抽象类的实际应用场景

5.1 框架与库设计

在设计框架和库时,使用抽象类可以提供清晰的接口,允许用户实现特定功能。这样可以提高可扩展性和灵活性。

5.2 插件系统

在插件系统中,抽象类可以定义插件的接口,具体的插件可以实现这些接口,动态地扩展应用程序的功能。

5.3 数据模型

在数据模型中,可以定义一个抽象类来表示通用的数据操作,具体的实现可以在派生类中完成,比如数据库操作、文件操作等。


6. 抽象类与多态的结合

抽象类通常与多态结合使用,派生类可以通过实现抽象类的接口,展现多态的特性。通过基类指针或引用,可以操作不同类型的派生类对象,达到灵活性。

示例

void processAnimal(AbstractAnimal& animal) {
    animal.makeSound();  // 多态调用
}

class Dog : public AbstractAnimal {
public:
    void makeSound() override {
        cout << "Bark!" << endl;
    }
};

class Cat : public AbstractAnimal {
public:
    void makeSound() override {
        cout << "Meow!" << endl;
    }
};

int main() {
    Dog dog;
    Cat cat;
    
    processAnimal(dog);  // 输出:Bark!
    processAnimal(cat);  // 输出:Meow!
    
    return 0;
}

总结

抽象是面向对象编程的重要概念,通过抽象类和纯虚函数,C++ 提供了一种强大且灵活的方式来设计和实现接口。这种方法不仅简化了复杂性,还提高了代码的可维护性和可扩展性。在编写可重用和灵活的代码时,抽象类和多态结合使用是非常有效的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值