12. 面向对象基础:类与对象、访问修饰符、成员函数、构造函数与析构函数
面向对象编程,说白了就是把现实世界的东西映射到代码里。你想想看,现实中有「人」这个概念,有「车」这个概念。每个具体的人有名字、年龄,会走路、会说话。C++ 的类,就是用来干这个的。
我个人习惯把类理解成「蓝图」,对象就是根据蓝图造出来的「实物」。这个比喻虽然老套,但确实管用。咱们今天就把这块彻底讲透。
12.1 类与对象:从蓝图到实物
先看一个最简单的例子。假设我们要描述一个学生:
class Student {
// 这里先空着,后面慢慢填
}; // 注意这个分号,我见过太多人漏掉了
这个 Student 就是一个类。它只是一个类型,不占内存。真正占内存的是对象:
Student stu1; // 栈上创建对象
Student* pStu = new Student(); // 堆上创建对象,记得 delete
嗯,这里要注意:stu1 和 pStu 的区别。前者生命周期由作用域控制,后者你得手动管理。我在项目中遇到过好几次内存泄漏,都是因为 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
这里有个细节:成员函数内部可以直接访问成员变量,不需要加任何前缀。编译器知道 width 和 height 是属于当前对象的。
我曾经踩过的坑:成员函数参数名和成员变量名重名。比如 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 知识体系总览
下面这张图,把本章的核心知识点串起来了:
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