第七章 面向对象基础:类与对象、访问修饰符、构造函数与析构函数、this指针、友元函数/类
好,咱们今天来聊聊C++面向对象编程的核心。说实话,我当年从C语言转到C++时,最兴奋的就是这个“类”的概念。它让代码不再是冷冰冰的函数堆砌,而是有了“生命”和“关系”。
面向对象编程,说白了就是把现实世界的东西抽象成代码里的“对象”。你想想看,一辆车有颜色、有速度,能跑能停——这不就是数据加行为吗?C++里的类,就是用来干这个的。
核心思想:类 = 数据成员 + 成员函数。对象 = 类的具体实例。
7.1 类与对象:从结构体到类的进化
如果你学过C语言,一定用过struct。C++的class,其实就是struct的“超级升级版”。区别在哪?struct默认所有成员是公开的,class默认是私有的。但这不是重点。
重点在于:类把数据和操作数据的方法打包在了一起。这叫“封装”。
// 一个简单的类定义
class Car {
// 数据成员
string brand;
int speed;
public:
// 成员函数
void setBrand(const string& b) { brand = b; }
void accelerate(int delta) { speed += delta; }
int getSpeed() const { return speed; }
};
// 使用对象
Car myCar;
myCar.setBrand("Tesla");
myCar.accelerate(30);
cout << myCar.getSpeed() << endl; // 输出 30
我个人习惯把类的声明放在头文件里,实现放在cpp文件里。这样结构清晰,编译也快。你在项目里最好也养成这个习惯。
7.2 访问修饰符:谁可以碰我的数据?
访问修饰符有三个:public、private、protected。我简单说说我的理解:
- public:谁都能访问。就像你家大门,快递员都能敲。
- private:只有类内部能访问。就像你的保险柜,只有你自己知道密码。
- protected:自己和子类能访问。就像家里的工具箱,你和孩子能用,外人不行。
我的建议:数据成员一律设为private,通过public的getter/setter来访问。这不是矫情,是为了以后好维护。我在项目中见过太多直接暴露成员变量导致后期改得想哭的代码。
class BankAccount {
private:
double balance; // 私有,外部不能直接改
public:
void deposit(double amount) {
if (amount > 0) balance += amount;
}
double getBalance() const {
return balance;
}
};
7.3 构造函数与析构函数:对象的生与死
构造函数在对象创建时自动调用,用来初始化数据。析构函数在对象销毁时自动调用,用来释放资源。
嗯,这里要注意:如果你没写构造函数,编译器会给你一个默认的。但一旦你写了任何构造函数,默认的那个就没了。
class FileHandler {
private:
FILE* file;
public:
// 构造函数:打开文件
FileHandler(const char* filename) {
file = fopen(filename, "r");
if (!file) {
cerr << "文件打开失败!" << endl;
}
}
// 析构函数:关闭文件
~FileHandler() {
if (file) {
fclose(file);
cout << "文件已关闭" << endl;
}
}
void readData() {
// 读取文件内容...
}
};
// 使用
{
FileHandler fh("data.txt");
fh.readData();
} // 这里fh离开作用域,析构函数自动调用
避坑指南:我曾经在项目中忘记给包含动态内存的类写析构函数,结果内存泄漏了整整一周才查出来。记住:如果你的类里有new出来的资源,一定要写析构函数来delete。这就是“RAII”原则——资源获取即初始化,析构时自动释放。
7.4 this指针:我就是我
this是一个指向当前对象的指针。它在每个成员函数里都隐式可用。什么时候显式用它?最常见的情况是参数名和成员变量名冲突时。
class Person {
private:
string name;
int age;
public:
// 参数名和成员名一样,用this区分
void setName(const string& name) {
this->name = name; // 左边的name是成员,右边的是参数
}
// 链式调用:返回当前对象的引用
Person& setAge(int age) {
this->age = age;
return *this; // 返回对象本身
}
void print() const {
cout << "Name: " << this->name << ", Age: " << this->age << endl;
}
};
// 链式调用示例
Person p;
p.setName("Alice").setAge(25).print(); // 输出:Name: Alice, Age: 25
你想想看,如果没有this指针,成员函数怎么知道自己在操作哪个对象?其实每个成员函数调用时,编译器都会偷偷把对象的地址传进去——就是this。
7.5 友元函数与友元类:打破规则的特权
友元,说白了就是“我信任你,你可以看我的私有数据”。友元函数不是类的成员函数,但它可以访问类的私有成员。友元类里的所有成员函数都可以访问当前类的私有成员。
重要提醒:友元破坏了封装性,能不用就别用。我只有在运算符重载或者两个类需要紧密协作时才会考虑它。
class Matrix; // 前向声明
class Vector {
private:
double x, y, z;
public:
Vector(double x, double y, double z) : x(x), y(y), z(z) {}
// 声明友元函数
friend double dotProduct(const Vector& v, const Vector& w);
// 声明友元类
friend class Matrix;
};
// 友元函数实现:可以访问Vector的私有成员
double dotProduct(const Vector& v, const Vector& w) {
return v.x * w.x + v.y * w.y + v.z * w.z;
}
class Matrix {
private:
double data[3][3];
public:
// 友元类中的函数可以访问Vector的私有成员
Vector transform(const Vector& v) {
return Vector(
data[0][0]*v.x + data[0][1]*v.y + data[0][2]*v.z,
data[1][0]*v.x + data[1][1]*v.y + data[1][2]*v.z,
data[2][0]*v.x + data[2][1]*v.y + data[2][2]*v.z
);
}
};
我的经验:友元类通常用在迭代器模式中。比如一个Container类,它的Iterator需要访问Container的内部数据。这时候把Iterator声明为友元类,既保持了封装,又提供了必要的访问权限。
7.6 综合示例:一个简单的Student类
咱们把今天学的知识点串起来,写一个完整的例子:
#include <iostream>
#include <string>
#include <vector>
using namespace std;
class Student {
private:
string name;
int id;
vector<double> scores;
public:
// 构造函数
Student(const string& name, int id)
: name(name), id(id) {
cout << "学生 " << name << " 已创建" << endl;
}
// 析构函数
~Student() {
cout << "学生 " << name << " 已销毁" << endl;
}
// setter/getter
void setName(const string& name) { this->name = name; }
string getName() const { return name; }
void addScore(double score) {
if (score >= 0 && score <= 100) {
scores.push_back(score);
}
}
double getAverage() const {
if (scores.empty()) return 0.0;
double sum = 0;
for (double s : scores) {
sum += s;
}
return sum / scores.size();
}
// 友元函数:比较两个学生的平均分
friend bool compareAverage(const Student& a, const Student& b);
};
// 友元函数实现
bool compareAverage(const Student& a, const Student& b) {
return a.getAverage() > b.getAverage();
}
int main() {
Student s1("张三", 1001);
Student s2("李四", 1002);
s1.addScore(85);
s1.addScore(92);
s1.addScore(78);
s2.addScore(88);
s2.addScore(76);
s2.addScore(95);
cout << s1.getName() << " 平均分: " << s1.getAverage() << endl;
cout << s2.getName() << " 平均分: " << s2.getAverage() << endl;
if (compareAverage(s1, s2)) {
cout << s1.getName() << " 成绩更好" << endl;
} else {
cout << s2.getName() << " 成绩更好" << endl;
}
return 0;
}
这个例子涵盖了类定义、构造函数、析构函数、this指针、友元函数。你可以在自己的编译器上跑跑看,感受一下对象的生命周期。
最后提醒一句:面向对象不是银弹。我见过有人为了用面向对象而用面向对象,把简单的逻辑搞得无比复杂。记住:封装、继承、多态是工具,不是目的。该用struct就用struct,该用class就用class,别为了炫技而过度设计。
公众号:蓝海资料掘金营,微信deep3321