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

C++ 类与对象深入解析

2024-05-164.2k 阅读

C++ 类与对象基础概念

类的定义与声明

在 C++ 中,类(Class)是一种用户自定义的数据类型,它将数据成员(变量)和成员函数(函数)封装在一起。类的声明语法如下:

class ClassName {
    // 访问修饰符
    private:
        // 私有数据成员和成员函数
        int privateData;
        void privateFunction();
    public:
        // 公有数据成员和成员函数
        int publicData;
        void publicFunction();
    protected:
        // 受保护数据成员和成员函数
        int protectedData;
        void protectedFunction();
};

这里,ClassName 是类的名称。类体中使用访问修饰符(privatepublicprotected)来控制对成员的访问权限。private 成员只能在类的内部被访问,public 成员可以在类的外部被访问,protected 成员在类的内部以及派生类中可以被访问。

对象的创建与初始化

对象是类的实例。一旦定义了类,就可以创建该类的对象。例如:

ClassName obj;

对象在创建时可以进行初始化。对于简单的类,可以在定义对象时直接初始化公有数据成员:

class Point {
public:
    int x;
    int y;
};

Point p = {10, 20};

对于更复杂的类,特别是包含私有数据成员的类,通常使用构造函数来进行初始化。

构造函数与析构函数

构造函数

构造函数是一种特殊的成员函数,用于在创建对象时初始化对象的成员变量。构造函数的名称与类名相同,没有返回类型(包括 void)。例如:

class Rectangle {
private:
    int width;
    int height;
public:
    // 构造函数
    Rectangle(int w, int h) {
        width = w;
        height = h;
    }
    int getArea() {
        return width * height;
    }
};

可以这样创建 Rectangle 对象:

Rectangle rect(5, 10);
int area = rect.getArea();

构造函数可以有默认参数,这使得在创建对象时可以有更多的灵活性:

class Circle {
private:
    double radius;
public:
    // 带有默认参数的构造函数
    Circle(double r = 1.0) {
        radius = r;
    }
    double getArea() {
        return 3.14159 * radius * radius;
    }
};

这样就可以创建带有默认半径或者指定半径的 Circle 对象:

Circle c1; // 使用默认半径 1.0
Circle c2(5.0);

析构函数

析构函数与构造函数相反,用于在对象销毁时释放资源。析构函数的名称是在类名前加上波浪线(~),同样没有返回类型。例如:

class DynamicArray {
private:
    int *arr;
    int size;
public:
    DynamicArray(int s) {
        size = s;
        arr = new int[size];
    }
    ~DynamicArray() {
        delete[] arr;
    }
};

在这个例子中,构造函数分配了动态内存,而析构函数释放了这些内存,以防止内存泄漏。

类的访问修饰符

私有成员(private)

私有成员是类中最严格的访问级别。只有类的成员函数可以访问私有成员。例如:

class BankAccount {
private:
    double balance;
public:
    BankAccount(double initialBalance) {
        balance = initialBalance;
    }
    void deposit(double amount) {
        balance += amount;
    }
    bool withdraw(double amount) {
        if (balance >= amount) {
            balance -= amount;
            return true;
        }
        return false;
    }
    double getBalance() {
        return balance;
    }
};

BankAccount 类中,balance 是私有成员。外部代码不能直接访问 balance,只能通过公有成员函数 depositwithdrawgetBalance 来间接操作 balance。这样可以保证数据的安全性和一致性。

公有成员(public)

公有成员可以在类的外部被访问。通常,公有成员函数用于提供对私有数据成员的访问接口,以及执行对象的主要操作。例如,在 BankAccount 类中,depositwithdrawgetBalance 函数是公有成员,外部代码可以调用这些函数来与 BankAccount 对象进行交互:

BankAccount account(1000.0);
account.deposit(500.0);
bool success = account.withdraw(200.0);
double currentBalance = account.getBalance();

受保护成员(protected)

受保护成员在类的内部以及派生类中可以被访问,但在类的外部不能被访问。受保护成员主要用于继承机制,当一个类派生自另一个类时,派生类可以访问基类的受保护成员。例如:

class Shape {
protected:
    int x;
    int y;
public:
    Shape(int a, int b) : x(a), y(b) {}
};

class Rectangle : public Shape {
private:
    int width;
    int height;
public:
    Rectangle(int a, int b, int w, int h) : Shape(a, b), width(w), height(h) {}
    int getArea() {
        return width * height;
    }
};

在这个例子中,Shape 类的 xy 是受保护成员。Rectangle 类派生自 Shape 类,Rectangle 的成员函数可以访问 Shape 的受保护成员 xy

类的继承

继承的概念与语法

继承是面向对象编程的重要特性之一,它允许一个类(派生类)从另一个类(基类)获取属性和行为。通过继承,派生类可以复用基类的代码,并且可以添加新的成员或者重写基类的成员。继承的语法如下:

class BaseClass {
    // 基类成员
};

class DerivedClass : accessModifier BaseClass {
    // 派生类成员
};

这里,accessModifier 可以是 publicprivateprotected,它决定了基类成员在派生类中的访问权限。例如,使用 public 继承:

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

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

在这个例子中,Dog 类继承自 Animal 类。Dog 类不仅拥有自己的 bark 函数,还继承了 Animal 类的 eat 函数。

Dog myDog;
myDog.eat(); // 调用基类的 eat 函数
myDog.bark(); // 调用派生类的 bark 函数

继承中的访问权限

当使用 public 继承时,基类的 public 成员在派生类中仍然是 public,基类的 protected 成员在派生类中仍然是 protected,基类的 private 成员在派生类中不可访问。

使用 private 继承时,基类的 publicprotected 成员在派生类中都变成 private,基类的 private 成员在派生类中仍然不可访问。

使用 protected 继承时,基类的 publicprotected 成员在派生类中都变成 protected,基类的 private 成员在派生类中仍然不可访问。

例如,对于 private 继承:

class Base {
public:
    int publicData;
protected:
    int protectedData;
private:
    int privateData;
};

class Derived : private Base {
public:
    void accessMembers() {
        publicData = 10; // 可以访问,因为在派生类中变成 private
        protectedData = 20; // 可以访问,因为在派生类中变成 private
        // privateData = 30; // 错误,不能访问基类的 private 成员
    }
};

多态性

多态性是面向对象编程的另一个重要特性,它允许通过基类的指针或引用调用派生类的函数。C++ 中实现多态性主要通过虚函数和函数重写。

虚函数

虚函数是在基类中声明为 virtual 的成员函数。当派生类重写了虚函数时,通过基类指针或引用调用该函数,会根据对象的实际类型来决定调用哪个版本的函数。例如:

class Shape {
public:
    virtual double getArea() {
        return 0.0;
    }
};

class Circle : public Shape {
private:
    double radius;
public:
    Circle(double r) : radius(r) {}
    double getArea() override {
        return 3.14159 * radius * radius;
    }
};

class Rectangle : public Shape {
private:
    int width;
    int height;
public:
    Rectangle(int w, int h) : width(w), height(h) {}
    double getArea() override {
        return width * height;
    }
};

这里,Shape 类的 getArea 函数是虚函数。CircleRectangle 类重写了 getArea 函数。可以通过以下方式实现多态调用:

Shape *shapes[2];
shapes[0] = new Circle(5.0);
shapes[1] = new Rectangle(4, 6);

for (int i = 0; i < 2; ++i) {
    std::cout << "Area: " << shapes[i]->getArea() << std::endl;
}

for (int i = 0; i < 2; ++i) {
    delete shapes[i];
}

在这个例子中,shapes 数组包含了 Shape 指针,但是实际指向的是 CircleRectangle 对象。当调用 getArea 函数时,会根据对象的实际类型调用相应的函数版本。

纯虚函数与抽象类

纯虚函数是在声明时赋值为 0 的虚函数。包含纯虚函数的类称为抽象类,抽象类不能直接创建对象。例如:

class AbstractShape {
public:
    virtual double getArea() = 0;
};

class Triangle : public AbstractShape {
private:
    double base;
    double height;
public:
    Triangle(double b, double h) : base(b), height(h) {}
    double getArea() override {
        return 0.5 * base * height;
    }
};

在这个例子中,AbstractShape 类是抽象类,因为它包含纯虚函数 getAreaTriangle 类继承自 AbstractShape 并实现了 getArea 函数,因此可以创建 Triangle 对象。

// AbstractShape absShape; // 错误,不能创建抽象类对象
Triangle tri(4, 5);
double area = tri.getArea();

运算符重载

运算符重载的概念与语法

运算符重载允许为用户定义的类定义运算符的行为。通过运算符重载,可以使类对象像内置数据类型一样使用运算符。运算符重载是通过定义特殊的成员函数来实现的,这些函数的名称以 operator 关键字开头,后面跟着要重载的运算符。例如,为 Complex 类重载 + 运算符:

class Complex {
private:
    double real;
    double imag;
public:
    Complex(double r = 0.0, double i = 0.0) : real(r), imag(i) {}
    Complex operator+(const Complex& other) {
        return Complex(real + other.real, imag + other.imag);
    }
    void print() {
        std::cout << real << " + " << imag << "i" << std::endl;
    }
};

这样就可以使用 + 运算符来相加两个 Complex 对象:

Complex c1(1, 2);
Complex c2(3, 4);
Complex result = c1 + c2;
result.print();

一元运算符重载

一元运算符只对一个操作数进行操作。例如,重载 ++ 运算符(前置和后置):

class Counter {
private:
    int value;
public:
    Counter(int v = 0) : value(v) {}
    // 前置 ++
    Counter& operator++() {
        value++;
        return *this;
    }
    // 后置 ++
    Counter operator++(int) {
        Counter temp = *this;
        value++;
        return temp;
    }
    int getValue() {
        return value;
    }
};

可以这样使用重载的 ++ 运算符:

Counter c(5);
Counter preIncremented = ++c;
Counter postIncremented = c++;
std::cout << "Pre - incremented: " << preIncremented.getValue() << std::endl;
std::cout << "Post - incremented: " << postIncremented.getValue() << std::endl;

二元运算符重载

二元运算符对两个操作数进行操作。除了前面提到的 + 运算符,还可以重载其他二元运算符,如 -*/ 等。例如,为 Complex 类重载 - 运算符:

class Complex {
private:
    double real;
    double imag;
public:
    Complex(double r = 0.0, double i = 0.0) : real(r), imag(i) {}
    Complex operator-(const Complex& other) {
        return Complex(real - other.real, imag - other.imag);
    }
    void print() {
        std::cout << real << " + " << imag << "i" << std::endl;
    }
};

使用 - 运算符:

Complex c1(5, 3);
Complex c2(2, 1);
Complex result = c1 - c2;
result.print();

友元函数与运算符重载

有时候,需要重载一些运算符,使得运算符的左侧操作数不是类对象。例如,重载 << 运算符用于输出自定义类对象到 std::cout。这时可以使用友元函数。友元函数不是类的成员函数,但可以访问类的私有成员。例如:

class Point {
private:
    int x;
    int y;
public:
    Point(int a, int b) : x(a), y(b) {}
    friend std::ostream& operator<<(std::ostream& os, const Point& p) {
        os << "(" << p.x << ", " << p.y << ")";
        return os;
    }
};

使用 << 运算符输出 Point 对象:

Point p(10, 20);
std::cout << p << std::endl;

类模板

模板的基本概念

模板是 C++ 中一种强大的代码复用机制,它允许编写通用的代码,而不必为不同的数据类型编写重复的代码。类模板是用于创建类的模板。通过类模板,可以定义一个通用的类,在实例化时指定具体的数据类型。例如,定义一个简单的 Stack 类模板:

template <typename T>
class Stack {
private:
    T *data;
    int top;
    int capacity;
public:
    Stack(int cap = 10) : capacity(cap), top(-1) {
        data = new T[capacity];
    }
    ~Stack() {
        delete[] data;
    }
    void push(T value) {
        if (top == capacity - 1) {
            // 处理栈满的情况
        }
        data[++top] = value;
    }
    T pop() {
        if (top == -1) {
            // 处理栈空的情况
        }
        return data[top--];
    }
    bool isEmpty() {
        return top == -1;
    }
};

这里,typename T 表示一个类型参数,在实例化 Stack 类时,需要指定具体的数据类型来替换 T

类模板的实例化

可以通过以下方式实例化 Stack 类模板:

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

Stack<double> doubleStack;
doubleStack.push(3.14);
double dValue = doubleStack.pop();

在这两个例子中,分别实例化了 Stack<int>Stack<double>,创建了针对 intdouble 类型的栈。

模板特化

模板特化允许为特定的数据类型提供专门的实现。例如,对于 Stack 类模板,可以为 bool 类型提供一个特化版本,因为 bool 类型的存储和操作可能与其他类型不同:

template <>
class Stack<bool> {
private:
    bool *data;
    int top;
    int capacity;
public:
    Stack(int cap = 10) : capacity(cap), top(-1) {
        data = new bool[capacity];
    }
    ~Stack() {
        delete[] data;
    }
    void push(bool value) {
        if (top == capacity - 1) {
            // 处理栈满的情况
        }
        data[++top] = value;
    }
    bool pop() {
        if (top == -1) {
            // 处理栈空的情况
        }
        return data[top--];
    }
    bool isEmpty() {
        return top == -1;
    }
};

这样,当实例化 Stack<bool> 时,就会使用这个特化版本的 Stack 类。

通过深入理解 C++ 的类与对象相关知识,包括类的定义、继承、多态、运算符重载和类模板等,开发者可以编写出更加灵活、高效和可维护的面向对象程序。这些特性使得 C++ 成为一种功能强大的编程语言,适用于各种规模和领域的软件开发项目。无论是系统级编程、游戏开发还是大型企业级应用,C++ 的类与对象机制都能提供坚实的基础。