4. 类与对象(上):类的定义、访问权限、构造函数与析构函数、this指针
好,咱们今天正式进入C++的核心地带——类与对象。
说实话,从C语言的结构体跳到C++的类,很多人一开始会觉得别扭。我当年刚转C++时,也觉得“不就是结构体加了个函数嘛”,结果被坑了几次才明白——类带来的不仅仅是语法糖,而是一整套工程化的思维方式。
4.1 类的定义:从struct到class的进化
在C语言里,我们用struct组织数据。比如描述一个学生:
// C语言风格
struct Student {
char name[32];
int age;
float score;
};
到了C++,我们不仅存数据,还要管行为。于是有了类:
// C++风格
class Student {
public:
void setName(const char* name) {
strcpy(this->name, name);
}
void showInfo() {
printf("Name: %s, Age: %d, Score: %.1f\n", name, age, score);
}
private:
char name[32];
int age;
float score;
};
你看,数据和操作数据的函数,被绑在了一起。这就是封装。
核心区别:struct默认所有成员是public,class默认是private。除此之外,两者在C++里几乎一样。但我个人习惯——纯数据用struct,有行为用class。
4.2 访问权限:public、protected、private
访问权限说白了就是“谁能用谁不能用”。C语言里没有这个机制,所有成员随便改,项目大了就容易出乱子。
| 访问级别 | 类内 | 派生类 | 外部 |
|---|---|---|---|
| public | ✔ | ✔ | ✔ |
| protected | ✔ | ✔ | ✘ |
| private | ✔ | ✘ | ✘ |
我在项目中遇到过一个问题:有个同事把成员变量全写成public,结果到处都能改,最后查bug查了三天,发现是某个地方不小心把id改成了0。嗯,从那以后我要求团队:成员变量一律private,通过getter/setter访问。
我的习惯:数据成员放private,工具函数放private或protected,对外接口放public。这样改内部实现时,外部代码不用动。
4.3 构造函数:对象出生的那一刻
构造函数在对象创建时自动调用。你想想看,如果没有构造函数,每次创建对象都得手动初始化,多麻烦,还容易忘。
class Student {
public:
// 默认构造函数
Student() {
name[0] = '\0';
age = 0;
score = 0.0f;
}
// 带参构造函数
Student(const char* n, int a, float s) {
strcpy(name, n);
age = a;
score = s;
}
private:
char name[32];
int age;
float score;
};
// 使用
Student s1; // 调用默认构造
Student s2("张三", 20, 88.5f); // 调用带参构造
这里有个坑——一旦你定义了任何构造函数,编译器就不会再生成默认构造函数。我曾经因为这个,new了一个对象数组,结果编译报错,找了半天才发现是忘了给默认构造。
注意:如果类中有指针成员,一定要考虑深拷贝和浅拷贝的问题。默认的拷贝构造函数只是浅拷贝,两个对象会指向同一块内存,析构时double free就崩了。
4.4 析构函数:对象消亡前的清理
析构函数在对象生命周期结束时自动调用。主要用来释放资源——堆内存、文件句柄、网络连接等等。
class Buffer {
public:
Buffer(int size) {
data = new char[size];
this->size = size;
}
~Buffer() {
delete[] data;
data = nullptr;
}
private:
char* data;
int size;
};
析构函数不能重载,只能有一个,而且没有参数。为什么?因为对象销毁时不需要你传参数,编译器自动调用就行。
我记得有一次,团队里有人写了个类,析构函数里忘了delete,结果内存泄漏跑了一周才发现。从那以后,我要求所有new出来的资源,必须在构造函数里分配,在析构函数里释放——这叫RAII,资源获取即初始化。
4.5 this指针:指向自己的那个“我”
this是一个隐含的指针,指向当前对象本身。它在每个非静态成员函数里都能用。
class Student {
public:
void setAge(int age) {
// 这里如果不加this,age = age就是自己赋值给自己,没效果
this->age = age;
}
Student& growUp() {
this->age++;
return *this; // 返回当前对象的引用
}
private:
int age;
};
this最常见的用途有两个:
- 区分参数和成员变量——当参数名和成员名冲突时,用this->指明成员
- 链式调用——返回*this,可以实现类似a.setX(1).setY(2)的写法
你可能会问:this指针存在哪里?答案是——它是个右值,不占对象的内存空间,编译器通过寄存器或栈传递。嗯,这个细节面试常考。
一句话总结:this就是对象自己的地址。在成员函数里,你看到的每个成员变量,其实前面都隐式地加了个this->。
4.6 知识体系总览
下面这张图,把本章的核心知识点串了起来。你可以把它当作一个思维导图来看:
4.7 避坑指南
最后,分享几个我踩过的坑:
- 忘记写拷贝构造函数和赋值运算符——类里有指针成员时,默认的浅拷贝会让你double free。记住“三/五法则”:要么禁止拷贝,要么自己实现深拷贝。
- 构造函数里调用虚函数——不会触发多态,因为此时派生类还没构造完。我见过有人在这上面栽跟头,调试了半天才发现。
- 析构函数不是虚函数——如果基类析构不是虚函数,delete基类指针时,派生类的析构不会被调用。资源泄漏就这么来的。
一个小建议:写类的时候,先把构造、析构、拷贝构造、赋值运算符这“四大金刚”想清楚。哪怕暂时用默认的,也要明确知道它们的行为。这样能避免很多运行时问题。
好了,这一章的内容就到这里。类与对象是C++的基石,理解透彻了,后面的继承、多态、模板才能学得扎实。代码多写几遍,自然就熟了。
可可精品资料库