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(); // 输出:大家好,我是李四
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。
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时销毁
- 全局对象:程序结束时销毁
8.5 知识体系总览
下面这张图把本章的核心知识点串起来了:
8.6 避坑指南与最佳实践
最后分享几个我实际工作中积累的经验:
- 初始化列表优先于赋值:在构造函数中,能用初始化列表就用初始化列表,效率更高。尤其是const成员和引用成员,必须用初始化列表。
- 析构函数记得加virtual:如果类可能被继承,析构函数要声明为virtual。否则通过基类指针删除派生类对象时,可能不会调用派生类的析构函数,导致资源泄漏。这个坑我踩过不止一次。
- 遵循最小权限原则:能private就别public,能protected就别public。暴露越少,后续修改越安全。
- 构造函数不要做太复杂的事情:比如不要在里面打开网络连接、分配大块内存。如果失败了怎么办?构造函数没有返回值,处理错误很麻烦。我一般只在构造函数里做简单的初始化,复杂的初始化单独写一个init函数。
好了,这一章的内容就到这里。类与对象是面向对象的基石,访问修饰符帮你控制访问权限,构造和析构函数管理对象的生命周期。把这些基础打牢了,后面学继承、多态就会轻松很多。
公众号:蓝海资料掘金营,微信deep3321