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

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

类与对象(上) 类的定义 class 类名 { }; 成员变量 + 成员函数 访问修饰符 public / private / protected 封装的核心机制 构造与析构 构造函数:对象创建时调用 析构函数:对象销毁时调用 this 指针 指向当前对象的指针 解决命名冲突 支持链式调用 封装 → 数据隐藏 + 接口暴露

11.6 避坑指南

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

  • 忘记写析构函数释放内存——程序跑着跑着内存暴涨,最后OOM崩溃。排查起来特别痛苦。
  • 构造函数里用memset清零——如果类里有虚函数或std::string,memset会直接破坏内部结构。我见过有人这么干,结果程序随机崩溃。
  • 在析构函数里抛异常——C++规定析构函数不能往外抛异常,否则程序会直接terminate。如果你需要清理资源,请在析构函数内部处理所有异常。
  • this指针被误用——记住,this是一个右值,你不能取它的地址,也不能给它赋值。

好了,这一章的内容就到这里。类与对象是C++面向对象的基石,把这些概念吃透了,后面的继承、多态、模板学起来会轻松很多。


公众号:蓝海资料掘金营,微信deep3321