13、类与对象(上):类的定义、访问修饰符、构造函数与析构函数、this指针、成员初始化列表
好,咱们今天聊聊C++里最核心的概念——类与对象。说实话,我当年刚学C++时,觉得类这东西不就是结构体加函数嘛,有啥了不起的?直到我在一个嵌入式项目里,用结构体硬生生写了三千行代码,被同事骂得狗血淋头……嗯,从那以后我才真正理解了类的价值。
类,说白了就是把数据和操作数据的方法打包在一起。你想想看,现实世界里的任何东西,都有属性和行为。比如一辆车,有颜色、速度(属性),能加速、刹车(行为)。类就是用来描述这种东西的。
13.1 类的定义——从struct到class的进化
在C语言里,我们用struct来打包数据。到了C++,struct升级了,可以包含函数。但更常用的是class关键字。两者唯一的区别是:struct默认成员是public,class默认是private。我个人习惯用class,因为更符合面向对象的封装思想。
// 一个简单的类定义
class Car {
// 默认这里是 private
std::string brand;
int speed;
public:
// 公有接口
void setBrand(const std::string& b) { brand = b; }
void accelerate(int delta) { speed += delta; }
int getSpeed() const { return speed; }
};
注意最后那个分号,我见过太多新手漏掉它,然后编译器报一堆莫名其妙的错误。嗯,这里要注意。
13.2 访问修饰符——谁可以碰我的数据?
访问修饰符有三种:public、private、protected。它们决定了谁能访问类里的成员。
| 修饰符 | 本类 | 派生类 | 外部 |
|---|---|---|---|
| public | ✅ | ✅ | ✅ |
| protected | ✅ | ✅ | ❌ |
| private | ✅ | ❌ | ❌ |
我在项目中遇到过一个问题:有个同事把所有成员都设为public,结果整个项目到处都在直接修改内部数据,最后调试时根本不知道是谁改坏了状态。所以我的建议是:数据成员一律private,对外只暴露必要的接口。这叫封装,是面向对象的第一原则。
核心原则:把数据藏起来,通过方法去操作。这样你改内部实现时,外部代码不用动。
13.3 构造函数——对象是怎么出生的?
构造函数在对象创建时自动调用。名字和类名相同,没有返回值。你可以重载多个构造函数,就像函数重载一样。
class Student {
std::string name;
int age;
float score;
public:
// 默认构造函数
Student() : name("未知"), age(0), score(0.0f) {}
// 带参数的构造函数
Student(const std::string& n, int a, float s)
: name(n), age(a), score(s) {}
// 拷贝构造函数
Student(const Student& other)
: name(other.name), age(other.age), score(other.score) {
std::cout << "拷贝构造被调用了" << std::endl;
}
};
你可能会问:如果我不写构造函数会怎样?编译器会帮你生成一个默认的。但一旦你写了任何构造函数,编译器就不再生成默认的了。我曾经踩过这个坑:写了一个带参数的构造,忘了写默认构造,结果在创建对象数组时报错,排查了半天……
13.4 析构函数——对象怎么消失的?
析构函数在对象销毁时自动调用。名字是~类名,没有参数,没有返回值,且不能重载,一个类只有一个析构函数。
class FileHandler {
FILE* filePtr;
public:
FileHandler(const char* filename) {
filePtr = fopen(filename, "r");
if (!filePtr) {
throw std::runtime_error("文件打开失败");
}
}
~FileHandler() {
if (filePtr) {
fclose(filePtr);
std::cout << "文件已关闭" << std::endl;
}
}
};
这个例子很实用。你在构造函数里申请资源(打开文件、分配内存),在析构函数里释放资源。这就是RAII(资源获取即初始化)思想。我写服务端程序时,所有数据库连接、网络套接字都用这种方式管理,从来没漏过资源。
警告:如果类里有指针成员,并且你手动管理内存,一定要实现拷贝构造函数、拷贝赋值运算符和析构函数(三法则)。否则浅拷贝会导致double free。
13.5 this指针——谁在调用我?
this是一个隐含的指针,指向当前调用成员函数的对象。它在每个非静态成员函数里都能用。
class Person {
std::string name;
public:
Person(const std::string& name) {
// 这里必须用this,因为参数名和成员名相同
this->name = name;
}
Person& setName(const std::string& name) {
this->name = name;
return *this; // 返回当前对象的引用,支持链式调用
}
};
// 链式调用
Person p("张三");
p.setName("李四").setName("王五");
this最常见的用途有两个:一是解决参数名和成员名冲突,二是返回*this实现链式调用。我个人觉得链式调用在配置类里特别好用,代码读起来像在写自然语言。
13.6 成员初始化列表——效率与正确性的关键
成员初始化列表是在构造函数参数列表后面,用冒号开头的一串初始化语句。很多人觉得它可有可无,其实不然。
class Example {
const int constVal; // 常量成员,必须在初始化列表初始化
int& refVal; // 引用成员,必须在初始化列表初始化
std::string name; // 普通成员
public:
// 错误写法:在函数体内赋值
// Example(int c, int& r, const std::string& n) {
// constVal = c; // 编译错误!const成员不能赋值
// refVal = r; // 编译错误!引用必须初始化
// name = n; // 这是赋值,不是初始化,效率低
// }
// 正确写法:使用初始化列表
Example(int c, int& r, const std::string& n)
: constVal(c), refVal(r), name(n) {
// 函数体可以为空
}
};
为什么推荐用初始化列表?三个原因:
- 必须用:const成员和引用成员只能在初始化列表初始化
- 效率高:在函数体内赋值,会先调用默认构造再赋值;初始化列表直接构造,省了一步
- 顺序问题:初始化顺序按成员声明顺序,不是按列表顺序。我见过有人在这里栽跟头
小技巧:养成习惯,所有构造函数都用初始化列表。即使对普通成员,也建议用列表初始化。这样代码更统一,也避免遗漏。
13.7 知识体系总览
下面这张图把本章的核心知识点串起来了。你可以看到,类定义是基础,访问修饰符控制可见性,构造和析构管理生命周期,this指针提供自引用,初始化列表优化构造过程。
说实话,类与对象这部分内容,我当年学了三遍才真正理解。第一遍是看书,第二遍是写代码,第三遍是在实际项目中踩坑。所以如果你现在觉得有点懵,别着急,多写几行代码自然就通了。
最后送你一句话:类是你对现实世界的建模,好的类设计让代码像乐高积木一样优雅。下一章我们会继续深入,看看对象到底怎么用,以及那些藏在细节里的魔鬼。
公众号:蓝海资料掘金营,微信deep3321