8、面向对象基础(上):类与对象、访问修饰符(public/private/protected)、构造函数与析构函数

面向对象编程,说白了就是把现实世界的东西映射到代码里。你想想看,现实中有「人」这个概念,有具体的「张三」「李四」这些个体。在C++里,就是那个概念,对象就是具体的个体。这个思想一旦理解了,后面很多东西就顺了。

我个人习惯把类看作一张「设计图纸」,对象就是按照这张图纸造出来的「实物」。图纸上规定了有什么属性(比如身高、体重),有什么行为(比如走路、说话)。但图纸本身不能走路,只有造出来的实物才能走。这个比喻我一直觉得挺贴切的。

8.1 类与对象:从图纸到实物

先看一个最简单的类定义:

class Student {
    // 这里放属性和行为
};  // 注意这个分号,我见过不少新手漏掉它

类里面可以放两种东西:成员变量(也叫属性)和成员函数(也叫方法)。成员变量描述对象的状态,成员函数描述对象的行为。

举个例子:

class Student {
public:
    // 成员变量
    string name;
    int age;
    float score;

    // 成员函数
    void sayHello() {
        cout << "大家好,我是" << name << endl;
    }
};

有了类之后,怎么创建对象?很简单:

int main() {
    Student stu1;          // 创建对象
    stu1.name = "张三";    // 访问成员变量
    stu1.age = 20;
    stu1.score = 92.5;
    stu1.sayHello();       // 调用成员函数

    return 0;
}

这里 stu1 就是一个具体的对象。你可以创建多个对象,每个对象都有自己的数据副本:

Student stu2;
stu2.name = "李四";
stu2.age = 22;
stu2.score = 88.0;
stu2.sayHello();  // 输出:大家好,我是李四
小提示:对象在内存中是独立存储的。stu1的name和stu2的name是两个不同的内存区域,互不干扰。这一点我在调试多线程程序时深有体会——每个线程有自己的对象副本,就不会出现数据竞争。

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

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

修饰符 含义 类内部 类外部 子类
public 公开的 可访问 可访问 可访问
private 私有的 可访问 不可访问 不可访问
protected 受保护的 可访问 不可访问 可访问

为什么要搞这么复杂?直接全部public不就行了?嗯,这里要注意——封装是面向对象的四大特性之一。把内部实现细节隐藏起来,只暴露必要的接口,这样外部代码就不能随意破坏对象的状态。

我曾经在一个项目中接手过别人的代码,所有成员变量都是public。结果呢?到处都在直接修改数据,一个地方改了,另一个地方就出bug。后来我花了整整两天才把所有访问路径理清楚。从那以后,我写类的时候默认都用private,只有确实需要暴露的才用public。

看个例子:

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

public:
    // 通过公开的接口来操作
    void deposit(double amount) {
        if (amount > 0) {
            balance += amount;
        }
    }

    double getBalance() {
        return balance;
    }
};

int main() {
    BankAccount account;
    // account.balance = 1000;  // 错误!balance是private
    account.deposit(500);       // 正确,通过公开接口
    cout << account.getBalance(); // 输出500
    return 0;
}

protected的用法我们后面讲继承时会详细说。简单提一句:如果你希望子类能访问父类的某些成员,但又不希望外部代码访问,就用protected。

核心原则:成员变量尽量用private,成员函数根据需求选择public或private。对外提供接口用public,内部辅助函数用private。这个习惯能帮你写出更健壮的代码。

8.3 构造函数:对象的初始化

每次创建对象时,都需要初始化成员变量。如果忘了初始化,成员变量里就是随机值——这在我刚学C++时踩过坑。比如:

class Student {
public:
    string name;
    int age;
};

int main() {
    Student stu;
    cout << stu.age;  // 输出什么?不确定!可能是0,也可能是垃圾值
    return 0;
}

为了避免这种问题,C++提供了构造函数。构造函数是一种特殊的成员函数,在对象创建时自动调用,用于初始化对象。

构造函数的特点:

  • 函数名与类名相同
  • 没有返回值(连void都不写)
  • 可以重载(多个构造函数,参数不同)
  • 创建对象时自动调用

看个例子:

class Student {
private:
    string name;
    int age;
    float score;

public:
    // 构造函数
    Student(string n, int a, float s) {
        name = n;
        age = a;
        score = s;
        cout << "学生" << name << "创建成功" << endl;
    }

    void showInfo() {
        cout << name << " " << age << "岁 成绩:" << score << endl;
    }
};

int main() {
    Student stu1("张三", 20, 92.5);  // 自动调用构造函数
    stu1.showInfo();
    return 0;
}

构造函数也可以重载:

class Student {
private:
    string name;
    int age;
    float score;

public:
    // 无参构造函数
    Student() {
        name = "未知";
        age = 0;
        score = 0;
    }

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

int main() {
    Student stu1;              // 调用无参构造函数
    Student stu2("李四", 21, 88.5);  // 调用带参构造函数
    return 0;
}
注意:如果你没有定义任何构造函数,编译器会生成一个默认的无参构造函数(什么都不做)。但只要你定义了任何一个构造函数,编译器就不会再自动生成默认构造函数了。这时候如果你还想用无参方式创建对象,就必须自己写一个无参构造函数。

8.4 析构函数:对象的清理

有创建就有销毁。析构函数在对象销毁时自动调用,用于释放资源(比如动态分配的内存、打开的文件等)。

析构函数的特点:

  • 函数名是 ~类名
  • 没有返回值,没有参数
  • 不能重载(一个类只有一个析构函数)
  • 对象销毁时自动调用

看个例子:

class FileHandler {
private:
    FILE* file;

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

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

int main() {
    FileHandler fh("test.txt");
    // 做一些文件操作...
    // 函数结束时,fh对象销毁,自动调用析构函数关闭文件
    return 0;
}

析构函数的调用时机:

  • 栈上创建的对象:离开作用域时销毁
  • 堆上创建的对象:调用delete时销毁
  • 全局对象:程序结束时销毁
经验之谈:如果你在构造函数中new了内存,一定要在析构函数中delete。否则就会内存泄漏。我早期写代码时经常忘记这一点,后来养成了一个习惯:写构造函数的同时就把析构函数也写好,确保成对出现。

8.5 知识体系总览

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

面向对象基础(上) 类与对象 类 = 设计图纸 对象 = 具体实物 成员变量 + 成员函数 访问修饰符 public:公开访问 private:类内部访问 protected:子类可访问 构造与析构 构造函数:初始化 析构函数:清理资源 自动调用,不可重载 封装:隐藏内部细节,暴露必要接口 类 = 数据 + 操作数据的函数

8.6 避坑指南与最佳实践

最后分享几个我实际工作中积累的经验:

  1. 初始化列表优先于赋值:在构造函数中,能用初始化列表就用初始化列表,效率更高。尤其是const成员和引用成员,必须用初始化列表。
  2. 析构函数记得加virtual:如果类可能被继承,析构函数要声明为virtual。否则通过基类指针删除派生类对象时,可能不会调用派生类的析构函数,导致资源泄漏。这个坑我踩过不止一次。
  3. 遵循最小权限原则:能private就别public,能protected就别public。暴露越少,后续修改越安全。
  4. 构造函数不要做太复杂的事情:比如不要在里面打开网络连接、分配大块内存。如果失败了怎么办?构造函数没有返回值,处理错误很麻烦。我一般只在构造函数里做简单的初始化,复杂的初始化单独写一个init函数。

好了,这一章的内容就到这里。类与对象是面向对象的基石,访问修饰符帮你控制访问权限,构造和析构函数管理对象的生命周期。把这些基础打牢了,后面学继承、多态就会轻松很多。


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