MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C++面向对象程序设计思想核心剖析

2022-10-056.3k 阅读

C++面向对象程序设计思想核心剖析

面向对象编程基础概念

  1. 类与对象 在C++中,类(class)是一种用户自定义的数据类型,它将数据(成员变量)和操作这些数据的函数(成员函数)封装在一起。对象(object)则是类的实例,通过创建对象,我们可以使用类中定义的成员变量和成员函数。

例如,定义一个简单的Circle类来表示圆:

class Circle {
private:
    double radius;
public:
    // 构造函数
    Circle(double r) : radius(r) {}

    // 计算面积的成员函数
    double calculateArea() {
        return 3.14159 * radius * radius;
    }
};

在上述代码中,Circle类有一个私有成员变量radius用于存储圆的半径,还有一个公有构造函数用于初始化半径,以及一个公有成员函数calculateArea用于计算圆的面积。

我们可以通过以下方式创建Circle类的对象并使用其成员函数:

int main() {
    Circle myCircle(5.0);
    double area = myCircle.calculateArea();
    return 0;
}
  1. 封装 封装是面向对象编程的重要特性之一,它将数据和操作数据的方法包装在一起,对外部隐藏对象的内部实现细节。在Circle类中,我们将radius设置为私有成员变量,这意味着外部代码无法直接访问它,只能通过类提供的公有成员函数(如calculateArea)来间接操作它。这样可以保护数据的完整性,避免外部代码对数据进行不合理的修改。

访问控制

  1. 访问修饰符 C++提供了三种访问修饰符:public(公有)、private(私有)和protected(保护)。
    • public:公有成员可以被类的对象和类的成员函数访问。在Circle类中,calculateArea函数是公有的,因此可以在类外部通过对象来调用。
    • private:私有成员只能被类的成员函数访问。如Circle类中的radius变量,外部代码无法直接访问。
    • protected:保护成员与私有成员类似,区别在于保护成员可以被派生类(子类)的成员函数访问。这在继承关系中非常有用,我们将在后续详细讨论。

例如,我们可以进一步扩展Circle类,添加一个私有成员函数validateRadius用于验证半径的合法性:

class Circle {
private:
    double radius;
    // 私有成员函数
    bool validateRadius(double r) {
        return r > 0;
    }
public:
    // 构造函数
    Circle(double r) {
        if (validateRadius(r)) {
            radius = r;
        } else {
            radius = 1.0;
        }
    }

    // 计算面积的成员函数
    double calculateArea() {
        return 3.14159 * radius * radius;
    }
};

在上述代码中,validateRadius函数是私有的,只能在类内部被调用,外部代码无法直接调用它。

构造函数与析构函数

  1. 构造函数 构造函数是一种特殊的成员函数,它在创建对象时自动调用,用于初始化对象的成员变量。构造函数的名称与类名相同,并且没有返回类型。

Circle类中,我们已经定义了一个构造函数Circle(double r),它接受一个参数r并将其赋值给radius成员变量。构造函数可以有多个重载版本,以适应不同的初始化需求。

例如,我们可以为Circle类添加一个默认构造函数:

class Circle {
private:
    double radius;
public:
    // 默认构造函数
    Circle() : radius(1.0) {}

    // 带参数的构造函数
    Circle(double r) : radius(r) {}

    // 计算面积的成员函数
    double calculateArea() {
        return 3.14159 * radius * radius;
    }
};

现在我们可以通过以下两种方式创建Circle对象:

int main() {
    Circle circle1; // 使用默认构造函数
    Circle circle2(5.0); // 使用带参数的构造函数
    return 0;
}
  1. 析构函数 析构函数也是一种特殊的成员函数,它在对象被销毁时自动调用,用于释放对象占用的资源。析构函数的名称是在类名前加上波浪号(~),同样没有返回类型。

例如,假设Circle类需要动态分配内存(虽然在这个简单例子中不需要,但为了演示析构函数的作用):

class Circle {
private:
    double* radiusPtr;
public:
    Circle(double r) {
        radiusPtr = new double(r);
    }

    ~Circle() {
        delete radiusPtr;
    }

    double calculateArea() {
        return 3.14159 * (*radiusPtr) * (*radiusPtr);
    }
};

在上述代码中,构造函数使用new运算符为radiusPtr分配内存,析构函数使用delete运算符释放该内存,以避免内存泄漏。

继承

  1. 继承的概念 继承是一种机制,通过它一个类(子类或派生类)可以从另一个类(父类或基类)继承成员变量和成员函数。子类可以扩展或修改从父类继承的行为,同时也可以添加自己特有的成员。

例如,我们定义一个Shape类作为基类,然后定义Circle类和Rectangle类作为Shape类的子类:

class Shape {
protected:
    std::string color;
public:
    Shape(const std::string& c) : color(c) {}

    virtual std::string getColor() {
        return color;
    }
};

class Circle : public Shape {
private:
    double radius;
public:
    Circle(const std::string& c, double r) : Shape(c), radius(r) {}

    double calculateArea() {
        return 3.14159 * radius * radius;
    }
};

class Rectangle : public Shape {
private:
    double width;
    double height;
public:
    Rectangle(const std::string& c, double w, double h) : Shape(c), width(w), height(h) {}

    double calculateArea() {
        return width * height;
    }
};

在上述代码中,Circle类和Rectangle类都继承自Shape类,它们继承了color成员变量和getColor成员函数。同时,它们各自实现了自己的calculateArea函数来计算不同形状的面积。

  1. 访问控制与继承 在继承关系中,访问修饰符会影响子类对父类成员的访问权限。如果子类以public方式继承父类,那么父类的public成员在子类中仍然是publicprotected成员在子类中仍然是protectedprivate成员在子类中不可访问。如果以protected方式继承,父类的publicprotected成员在子类中变为protected。如果以private方式继承,父类的publicprotected成员在子类中变为private

多态

  1. 静态多态(函数重载与运算符重载)
    • 函数重载:在同一个类中,可以定义多个同名但参数列表不同的函数,这就是函数重载。编译器会根据调用函数时提供的参数类型和数量来选择合适的函数版本。

例如,在Circle类中,我们可以重载构造函数:

class Circle {
private:
    double radius;
public:
    // 默认构造函数
    Circle() : radius(1.0) {}

    // 带参数的构造函数
    Circle(double r) : radius(r) {}

    // 另一个带参数的构造函数,接受两个参数(这里只是为了演示重载)
    Circle(double r, int someFlag) : radius(r) {
        // 根据someFlag做一些额外操作
    }
};
- **运算符重载**:C++允许我们为自定义类型重载运算符,使其能够像内置类型一样使用运算符。例如,我们可以为`Circle`类重载`+`运算符,用于计算两个圆的半径之和:
class Circle {
private:
    double radius;
public:
    Circle(double r) : radius(r) {}

    Circle operator+(const Circle& other) {
        return Circle(radius + other.radius);
    }

    double getRadius() {
        return radius;
    }
};

使用重载的+运算符:

int main() {
    Circle circle1(5.0);
    Circle circle2(3.0);
    Circle result = circle1 + circle2;
    double sumRadius = result.getRadius();
    return 0;
}
  1. 动态多态(虚函数与函数重写) 动态多态是通过虚函数和函数重写实现的。在基类中定义虚函数,子类可以重写这些虚函数以提供不同的实现。通过基类指针或引用调用虚函数时,实际调用的是子类中重写的函数版本,这取决于指针或引用所指向的对象的实际类型。

在前面的Shape类、Circle类和Rectangle类的例子中,Shape类中的getColor函数被声明为虚函数。这样,当我们通过Shape类的指针或引用调用getColor函数时,会根据实际指向的对象类型(CircleRectangle)来调用相应类中的getColor函数。

例如:

int main() {
    Shape* shape1 = new Circle("Red", 5.0);
    Shape* shape2 = new Rectangle("Blue", 4.0, 3.0);

    std::cout << "Shape 1 color: " << shape1->getColor() << std::endl;
    std::cout << "Shape 2 color: " << shape2->getColor() << std::endl;

    delete shape1;
    delete shape2;
    return 0;
}

在上述代码中,shape1实际指向Circle对象,shape2实际指向Rectangle对象,通过Shape指针调用getColor函数时,会调用各自子类中的实现,从而实现动态多态。

抽象类与接口

  1. 抽象类 抽象类是一种不能被实例化的类,它至少包含一个纯虚函数。纯虚函数是在声明时赋值为0的虚函数。抽象类的主要作用是为子类提供一个通用的接口或框架,子类必须重写抽象类中的纯虚函数才能被实例化。

例如,我们可以将Shape类修改为抽象类:

class Shape {
protected:
    std::string color;
public:
    Shape(const std::string& c) : color(c) {}

    virtual std::string getColor() {
        return color;
    }

    // 纯虚函数
    virtual double calculateArea() = 0;
};

class Circle : public Shape {
private:
    double radius;
public:
    Circle(const std::string& c, double r) : Shape(c), radius(r) {}

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

class Rectangle : public Shape {
private:
    double width;
    double height;
public:
    Rectangle(const std::string& c, double w, double h) : Shape(c), width(w), height(h) {}

    double calculateArea() override {
        return width * height;
    }
};

在上述代码中,Shape类由于包含纯虚函数calculateArea,所以成为抽象类。Circle类和Rectangle类必须重写calculateArea函数才能被实例化。

  1. 接口 在C++中,接口通常通过抽象类来实现。接口定义了一组函数的签名,但不包含函数的实现。实现接口的类必须提供这些函数的具体实现。通过接口,我们可以实现一种松散耦合的编程方式,使得不同的类可以通过相同的接口进行交互。

例如,我们可以定义一个Drawable接口:

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

class Circle : public Shape, public Drawable {
private:
    double radius;
public:
    Circle(const std::string& c, double r) : Shape(c), radius(r) {}

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

    void draw() override {
        std::cout << "Drawing a circle with radius " << radius << std::endl;
    }
};

在上述代码中,Circle类实现了Drawable接口的draw函数,这样Circle对象就可以通过Drawable接口进行绘制操作。

模板

  1. 函数模板 函数模板允许我们定义一个通用的函数,该函数可以处理不同类型的数据,而无需为每种类型都编写一个单独的函数。

例如,定义一个通用的交换函数模板:

template <typename T>
void swap(T& a, T& b) {
    T temp = a;
    a = b;
    b = temp;
}

我们可以使用这个函数模板来交换不同类型的变量:

int main() {
    int num1 = 5, num2 = 10;
    swap(num1, num2);

    double double1 = 3.14, double2 = 2.71;
    swap(double1, double2);
    return 0;
}

在上述代码中,swap函数模板可以根据传递的参数类型自动实例化相应的函数版本。

  1. 类模板 类模板允许我们定义一个通用的类,该类可以处理不同类型的数据。

例如,定义一个简单的栈类模板:

template <typename T, int size>
class Stack {
private:
    T data[size];
    int top;
public:
    Stack() : top(-1) {}

    void push(T value) {
        if (top < size - 1) {
            data[++top] = value;
        }
    }

    T pop() {
        if (top >= 0) {
            return data[top--];
        }
        return T();
    }
};

使用栈类模板:

int main() {
    Stack<int, 10> intStack;
    intStack.push(5);
    int value = intStack.pop();

    Stack<double, 5> doubleStack;
    doubleStack.push(3.14);
    double doubleValue = doubleStack.pop();
    return 0;
}

在上述代码中,Stack类模板可以根据指定的类型T和大小size实例化不同的栈类。

内存管理

  1. 栈内存与堆内存 在C++中,变量可以存储在栈内存或堆内存中。局部变量(在函数内部定义的变量)存储在栈内存中,其生命周期与函数的执行周期相同。当函数结束时,栈上的变量会自动销毁。

动态分配的变量(使用new运算符创建的变量)存储在堆内存中,需要手动使用delete运算符来释放内存,否则会导致内存泄漏。

例如:

int main() {
    int stackVar = 10; // 栈变量
    int* heapVar = new int(20); // 堆变量

    delete heapVar;
    return 0;
}
  1. 智能指针 为了简化内存管理并避免内存泄漏,C++11引入了智能指针。智能指针是一种模板类,它可以自动管理动态分配的内存。C++11提供了三种智能指针:std::unique_ptrstd::shared_ptrstd::weak_ptr

    • std::unique_ptrstd::unique_ptr拥有对动态分配对象的唯一所有权。当std::unique_ptr被销毁时,它会自动释放所指向的对象。
int main() {
    std::unique_ptr<int> uniquePtr(new int(10));
    return 0;
}
- **`std::shared_ptr`**:`std::shared_ptr`允许多个指针共享对动态分配对象的所有权。对象的引用计数会随着新的`std::shared_ptr`指向该对象而增加,当引用计数为0时,对象会自动被释放。
int main() {
    std::shared_ptr<int> sharedPtr1(new int(10));
    std::shared_ptr<int> sharedPtr2 = sharedPtr1;
    return 0;
}
- **`std::weak_ptr`**:`std::weak_ptr`是一种弱引用,它不增加对象的引用计数。`std::weak_ptr`通常与`std::shared_ptr`一起使用,用于解决循环引用的问题。
class B;
class A {
public:
    std::shared_ptr<B> ptrToB;
};

class B {
public:
    std::weak_ptr<A> ptrToA;
};

在上述代码中,A类包含一个指向B类对象的std::shared_ptrB类包含一个指向A类对象的std::weak_ptr,这样可以避免循环引用导致的内存泄漏。

异常处理

  1. 异常的抛出与捕获 C++提供了异常处理机制,用于处理程序运行时出现的错误。通过throw关键字可以抛出异常,通过try - catch块可以捕获并处理异常。

例如:

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

int main() {
    try {
        double result = divide(10.0, 0.0);
    } catch (const std::runtime_error& e) {
        std::cerr << "Exception caught: " << e.what() << std::endl;
    }
    return 0;
}

在上述代码中,divide函数在除数为0时抛出一个std::runtime_error异常,main函数中的try - catch块捕获并处理该异常。

  1. 异常安全 在编写代码时,需要考虑异常安全。异常安全的代码在发生异常时能够保持对象的一致性,并且不会泄漏资源。例如,在使用动态内存分配时,如果在分配内存后但在初始化对象之前发生异常,需要确保已分配的内存被正确释放。智能指针在这方面非常有用,因为它们会自动处理内存释放,从而提高代码的异常安全性。

总结

C++面向对象程序设计思想涵盖了类与对象、封装、继承、多态等核心概念,同时还包括模板、内存管理、异常处理等重要特性。通过深入理解和运用这些概念和特性,开发者可以编写出高效、可维护、可扩展的C++程序。在实际编程中,需要根据具体的需求和场景,合理地选择和组合这些特性,以实现最佳的编程效果。