11. 类与对象(上):类的定义;访问修饰符;构造函数与析构函数;this指针
好,咱们今天正式进入C++面向对象的大门。说实话,我当年从C转C++时,最兴奋的就是这个“类”的概念。它让代码不再是散落一地的函数和变量,而是有了骨架、有了生命。
这一章,咱们把类的基础打牢。我会从最朴素的写法开始,一步步带你理解:类到底怎么定义?访问权限怎么控制?对象怎么生、怎么死?还有那个神秘的this指针到底指向谁。
11.1 类的定义——从结构体到类
如果你学过C语言的结构体,那恭喜你,你已经懂了一半。C++的类,本质上就是结构体的“进化版”。
结构体只能装数据,而类既能装数据,也能装操作这些数据的函数。说白了,类把数据和逻辑绑在了一起。
看个最简单的例子:
class Student {
// 默认是private
char name[32];
int age;
float score;
public:
void setName(const char* n) {
strcpy(name, n);
}
void showInfo() {
printf("姓名: %s, 年龄: %d, 分数: %.1f\n", name, age, score);
}
};
这里定义了一个Student类。它有三个成员变量(也叫属性),两个成员函数(也叫方法)。
注意结尾的分号——我见过不少新手在这里漏掉,编译报错半天找不到原因。嗯,我自己也犯过这个错。
11.2 访问修饰符——public、private、protected
类里的东西,不是谁都能随便碰的。访问修饰符就是用来控制“谁能碰”的规则。
| 修饰符 | 类内部 | 派生类 | 外部 |
|---|---|---|---|
| public | 可访问 | 可访问 | 可访问 |
| protected | 可访问 | 可访问 | 不可访问 |
| private | 可访问 | 不可访问 | 不可访问 |
我个人的习惯是:成员变量一律private,对外暴露的接口用public。为什么?因为这样可以控制别人怎么修改你的数据。你想想看,如果age是public的,别人直接写stu.age = -5,你拦都拦不住。
核心原则:把数据藏起来,把接口露出来。这叫“封装”。
protected我们后面讲继承时会细说,现在你只需要知道:它介于public和private之间,对“儿子”类开放,对外人关闭。
11.3 构造函数与析构函数
对象不是凭空出现的。它需要初始化,也需要清理。构造函数和析构函数就是干这个的。
构造函数
构造函数在对象创建时自动调用。名字和类名相同,没有返回值。
class Student {
private:
char name[32];
int age;
float score;
public:
// 构造函数
Student() {
age = 0;
score = 0.0f;
name[0] = '\0';
printf("Student对象已创建\n");
}
// 带参数的构造函数
Student(const char* n, int a, float s) {
strcpy(name, n);
age = a;
score = s;
}
void showInfo() {
printf("姓名: %s, 年龄: %d, 分数: %.1f\n", name, age, score);
}
};
使用起来很简单:
Student s1; // 调用无参构造函数
Student s2("张三", 20, 88.5f); // 调用带参构造函数
s1.showInfo();
s2.showInfo();
这里有个坑:如果你自己写了带参构造函数,编译器就不会再自动生成无参构造函数了。我曾经在项目里遇到过,定义了一个带参构造,结果后面想用Student s1;时报错,找了半天才发现少写了无参构造。
注意:一旦你定义了任何构造函数,编译器就不再提供默认的无参构造函数。如果需要,请手动补上。
析构函数
析构函数在对象销毁时自动调用。名字是~类名,没有参数,没有返回值。
class Student {
// ... 成员变量 ...
public:
// 构造函数
Student() {
printf("Student对象已创建\n");
}
// 析构函数
~Student() {
printf("Student对象已销毁\n");
}
};
析构函数主要用来释放资源。比如你在构造函数里new了内存,那就在析构函数里delete掉。否则内存泄漏等着你。
小技巧:如果类里没有动态分配的资源,可以不写析构函数,编译器会帮你生成一个默认的。但如果有new/malloc,一定要写析构函数去释放。
11.4 this指针
每个成员函数里,都藏着一个隐式的指针——this。它指向调用这个函数的对象本身。
看个例子你就明白了:
class Student {
private:
char name[32];
public:
void setName(const char* name) {
// 这里的name是参数,不是成员变量
// 用this->name来访问成员变量
strcpy(this->name, name);
}
void show() {
printf("name = %s\n", this->name);
}
};
当参数名和成员变量名冲突时,this->就派上用场了。它明确告诉编译器:我要访问的是对象的成员,不是函数的参数。
其实,就算你不写this->,编译器也会自动加上。但显式写出来,代码更清晰,也避免了歧义。
还有一个经典用法——链式调用:
class Printer {
public:
Printer& print(const char* msg) {
printf("%s", msg);
return *this; // 返回对象本身
}
};
// 用法
Printer p;
p.print("Hello ").print("World!").print("\n");
每次调用print后返回*this,就可以继续调用下一个函数。这种写法在C++标准库里很常见,比如cout << "a" << "b"就是靠这个实现的。
11.5 知识体系总览
下面这张图,把本章的核心知识点串在了一起。你可以把它当作一个“思维导图”来看。
11.6 避坑指南
最后,分享几个我踩过的坑:
- 忘记写析构函数释放内存——程序跑着跑着内存暴涨,最后OOM崩溃。排查起来特别痛苦。
- 构造函数里用memset清零——如果类里有虚函数或std::string,memset会直接破坏内部结构。我见过有人这么干,结果程序随机崩溃。
- 在析构函数里抛异常——C++规定析构函数不能往外抛异常,否则程序会直接terminate。如果你需要清理资源,请在析构函数内部处理所有异常。
- this指针被误用——记住,this是一个右值,你不能取它的地址,也不能给它赋值。
好了,这一章的内容就到这里。类与对象是C++面向对象的基石,把这些概念吃透了,后面的继承、多态、模板学起来会轻松很多。