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 知识体系总览

下面这张图,把本章的核心知识点串了起来。你可以把它当作一个思维导图来看:

类与对象(上) 类的定义 struct vs class 成员变量 + 成员函数 访问权限 public / protected / private 封装性 构造函数 默认 / 带参 / 拷贝 初始化列表 析构函数 资源释放 RAII思想 this指针 区分参数与成员 链式调用 核心思想:封装 + RAII + 资源管理 用类组织代码,用构造/析构管理生命周期

4.7 避坑指南

最后,分享几个我踩过的坑:

  • 忘记写拷贝构造函数和赋值运算符——类里有指针成员时,默认的浅拷贝会让你double free。记住“三/五法则”:要么禁止拷贝,要么自己实现深拷贝。
  • 构造函数里调用虚函数——不会触发多态,因为此时派生类还没构造完。我见过有人在这上面栽跟头,调试了半天才发现。
  • 析构函数不是虚函数——如果基类析构不是虚函数,delete基类指针时,派生类的析构不会被调用。资源泄漏就这么来的。

一个小建议:写类的时候,先把构造、析构、拷贝构造、赋值运算符这“四大金刚”想清楚。哪怕暂时用默认的,也要明确知道它们的行为。这样能避免很多运行时问题。

好了,这一章的内容就到这里。类与对象是C++的基石,理解透彻了,后面的继承、多态、模板才能学得扎实。代码多写几遍,自然就熟了。

可可精品资料库