12. 面向对象基础:类与对象、访问修饰符、成员函数、构造函数与析构函数

面向对象编程,说白了就是把现实世界的东西映射到代码里。你想想看,现实中有「人」这个概念,有「车」这个概念。每个具体的人有名字、年龄,会走路、会说话。C++ 的类,就是用来干这个的。

我个人习惯把类理解成「蓝图」,对象就是根据蓝图造出来的「实物」。这个比喻虽然老套,但确实管用。咱们今天就把这块彻底讲透。

12.1 类与对象:从蓝图到实物

先看一个最简单的例子。假设我们要描述一个学生:

class Student {
    // 这里先空着,后面慢慢填
};  // 注意这个分号,我见过太多人漏掉了

这个 Student 就是一个类。它只是一个类型,不占内存。真正占内存的是对象:

Student stu1;          // 栈上创建对象
Student* pStu = new Student();  // 堆上创建对象,记得 delete

嗯,这里要注意:stu1pStu 的区别。前者生命周期由作用域控制,后者你得手动管理。我在项目中遇到过好几次内存泄漏,都是因为 new 了忘记 delete。

核心概念:类是抽象的概念,对象是具体的实例。类不占内存,对象占内存。

12.2 访问修饰符:public、private、protected

这三个关键字,决定了谁能碰你的数据。我刚开始学的时候总觉得这东西多余,后来被坑过一次才明白——封装是面向对象的基石。

修饰符 类内部 派生类 外部
public 可访问 可访问 可访问
protected 可访问 可访问 不可访问
private 可访问 不可访问 不可访问

举个例子你就明白了:

class BankAccount {
private:
    double balance;  // 余额,外部不能直接改

public:
    void deposit(double amount) {
        if (amount > 0) {
            balance += amount;
        }
    }

    double getBalance() {
        return balance;
    }
};

int main() {
    BankAccount myAccount;
    // myAccount.balance = 1000;  // 编译错误!private 成员不能直接访问
    myAccount.deposit(500);       // 正确,通过 public 接口操作
    cout << myAccount.getBalance();  // 正确
    return 0;
}

为什么要把 balance 设为 private?你想想看,如果外部可以直接改余额,那岂不是可以随便改成负数?通过 deposit 函数,我们可以加校验逻辑。这就是封装的意义。

我的习惯:数据成员一律 private,通过 public 的 getter/setter 访问。protected 留给继承场景用,平时很少碰。

12.3 成员函数:类里面的函数

成员函数就是属于类的函数。它可以访问类的所有成员,包括 private 的。

class Rectangle {
private:
    double width;
    double height;

public:
    void setDimensions(double w, double h) {
        width = w;
        height = h;
    }

    double area() {
        return width * height;
    }

    double perimeter() {
        return 2 * (width + height);
    }
};

调用的时候很简单:

Rectangle rect;
rect.setDimensions(3.0, 4.0);
cout << "面积: " << rect.area();  // 输出 12

这里有个细节:成员函数内部可以直接访问成员变量,不需要加任何前缀。编译器知道 widthheight 是属于当前对象的。

我曾经踩过的坑:成员函数参数名和成员变量名重名。比如 void setWidth(double width) { width = width; },这样赋值的是参数自己。正确做法是用 this->width = width; 或者给参数换个名字。

12.4 构造函数:对象出生时自动调用

构造函数在对象创建时自动执行。它的名字和类名相同,没有返回值。

class Student {
private:
    string name;
    int age;

public:
    // 默认构造函数
    Student() {
        name = "未知";
        age = 0;
        cout << "Student 对象创建了" << endl;
    }

    // 带参数的构造函数
    Student(string n, int a) {
        name = n;
        age = a;
    }

    void display() {
        cout << name << ", " << age << "岁" << endl;
    }
};

使用方式:

Student s1;                    // 调用默认构造函数
Student s2("张三", 20);        // 调用带参构造函数
s1.display();                  // 输出: 未知, 0岁
s2.display();                  // 输出: 张三, 20岁

我个人建议:如果类里有指针成员,一定要写构造函数来初始化。否则指针是野指针,后果很严重。

12.5 析构函数:对象死亡时自动调用

析构函数在对象销毁时自动执行。名字是 ~类名,没有参数,没有返回值。

class FileHandler {
private:
    FILE* file;

public:
    FileHandler(const char* filename) {
        file = fopen(filename, "r");
        if (file == nullptr) {
            cout << "文件打开失败" << endl;
        }
    }

    ~FileHandler() {
        if (file != nullptr) {
            fclose(file);
            cout << "文件已关闭" << endl;
        }
    }
};

你看这个例子:构造函数打开文件,析构函数关闭文件。这样就不怕忘记释放资源了。我在项目中处理数据库连接、网络 socket 时,都是这个套路。

重要原则:谁申请,谁释放。构造函数里 new 了,析构函数里就要 delete。这叫 RAII(资源获取即初始化),是 C++ 里非常核心的思想。

12.6 知识体系总览

下面这张图,把本章的核心知识点串起来了:

类 (Class) 实例化 对象 (Object) 访问修饰符 • public:谁都能访问 • private:只有类内部能访问 • protected:类内部+派生类 封装的核心机制 成员函数 • 属于类的函数 • 可访问所有成员 • 通过对象调用 构造与析构 • 构造函数:对象创建时 • 析构函数:对象销毁时 • 自动调用,不可重载 RAII 的基础 面向对象三大特性:封装、继承、多态 本章重点:封装 —— 通过访问修饰符实现

12.7 综合示例:一个完整的类

最后,咱们把今天学的所有东西揉在一起,写一个完整的例子:

#include <iostream>
#include <string>
using namespace std;

class Book {
private:
    string title;
    string author;
    int pages;

public:
    // 构造函数
    Book() {
        title = "未命名";
        author = "未知作者";
        pages = 0;
        cout << "默认构造函数被调用" << endl;
    }

    Book(string t, string a, int p) {
        title = t;
        author = a;
        pages = p;
        cout << "带参构造函数被调用" << endl;
    }

    // 析构函数
    ~Book() {
        cout << "析构函数被调用: " << title << endl;
    }

    // 成员函数
    void display() {
        cout << "书名: " << title << endl;
        cout << "作者: " << author << endl;
        cout << "页数: " << pages << endl;
    }

    // getter/setter
    void setTitle(string t) { title = t; }
    string getTitle() { return title; }
};

int main() {
    Book book1;                    // 调用默认构造函数
    Book book2("C++入门", "张三", 300);  // 调用带参构造函数

    book1.setTitle("算法导论");
    book1.display();
    cout << "---" << endl;
    book2.display();

    return 0;
    // 程序结束时,book1 和 book2 的析构函数被自动调用
}

运行结果:

默认构造函数被调用
带参构造函数被调用
书名: 算法导论
作者: 未知作者
页数: 0
---
书名: C++入门
作者: 张三
页数: 300
析构函数被调用: 算法导论
析构函数被调用: C++入门

你看,构造和析构的顺序是相反的——后创建的对象先销毁。这个细节在管理资源时很重要。

一点建议:刚开始学的时候,可以在构造和析构函数里加打印语句,这样能清楚地看到对象的生命周期。等熟练了再删掉。

好了,面向对象的基础就这些。说白了就是三件事:把数据藏起来(封装)、在合适的时机初始化(构造)、在合适的时机清理(析构)。把这三点吃透了,后面的继承和多态就好办了。


公众号:蓝海资料掘金营,微信deep3321