wengqidaifeng

个人主页:

wengqidaifeng

永远在路上,永远向前走

个人专栏:

数据结构
C语言
嵌入式小白启动!
重要OJ算法题详解
蓝桥杯备战
C++从菜鸟到强手
python启航

前言

C++ 与 C 语言最大的区别之一,就是 C++ 是一门面向对象的编程语言。而面向对象的核心,就是类(Class)和对象(Object)

在 C 语言中,我们用 struct 来组织数据,相关的函数只能定义在结构体外部,数据和操作是分离的。C++ 的 class 则把数据和操作封装在了一起,让代码更加内聚、更加符合现实世界的思维模型。

本系列分为上、中、下三篇,由浅入深地介绍 C++ 类和对象。本篇(上)聚焦最基础的概念。


一、类的定义

1.1 从 struct 到 class

// C 风格:数据与操作分离
struct Date_C {
    int _year;
    int _month;
    int _day;
};

void Init(Date_C* d, int year, int month, int day) {
    d->_year = year;
    d->_month = month;
    d->_day = day;
}

// C++ 风格:数据与操作封装在一起
class Date {
public:
    void Init(int year, int month, int day) {
        _year = year;
        _month = month;
        _day = day;
    }

    void Print() {
        cout << _year << "/" << _month << "/" << _day << endl;
    }

private:
    int _year;
    int _month;
    int _day;
};

C++ 中 struct 也可以定义成员函数,与 class 的唯一区别是:struct 的默认访问权限是 public,class 的默认访问权限是 private

一张图看懂 C struct 与 C++ class 的区别:

升级

C++:数据与操作封装在一起

class Date {
int y,m,d;
void Init(...){ }
void Print(){ }
};

C 语言:数据与操作分离

struct Date { int y,m,d; };

void Init(Date* d, ...) { }

void Print(Date* d) { }

1.2 类的基本语法

class ClassName {
    // 成员函数(方法)
    // 成员变量(属性)
};

注意:类定义末尾必须加分号 ;,这和函数不一样。


二、访问限定符

限定符 含义
public 类的外部可以访问
private 只能在类的成员函数内部访问
protected 与 private 类似,但在继承中子类可以访问

Date 类

可以访问

✅ 允许

❌ 禁止 d._year

private 区域——仅类内可访问

_year / _month / _day 等成员变量

public 区域——外部可访问

Init() / Print() 等成员函数

外部代码: d.Init() ✅

class Date {
public:
    void Init(int year, int month, int day) {
        _year = year;   // 类内可以访问 private 成员
        _month = month;
        _day = day;
    }
private:
    int _year;   // 类的"外面"无法直接访问
    int _month;
    int _day;
};

int main() {
    Date d;
    d.Init(2024, 7, 10);  // OK,public 成员函数可访问
    // d._year = 2024;     // 错误!private 成员不能从外部直接访问
}

设计原则:成员变量一般设为 private,对外提供 public 的成员函数作为接口。这是"封装"的思想。


三、类的实例化 —— 类和对象的关系

是描述某一类事物的"模板"(蓝图),对象是根据这个模板创建出来的具体"实例"。

class Date {
    // ...
private:
    int _year;   // 这里只是声明,还没有分配空间
    int _month;
    int _day;
};

int main() {
    Date d1;          // d1 是一个具体的对象,此时才真正占用内存
    Date d2;          // d2 是另一个对象,有自己独立的内存空间

    cout << sizeof(d1) << endl;  // 输出:12(3个int)
    cout << sizeof(Date) << endl; // 输出:12(同上,类本身也有大小)
}

重要理解:类本身不占用存储空间(只是一个模板),只有实例化出来的对象才占用内存。用 sizeof(类名) 得到的是该类对象的大小。

空类的大小

class A {};        // 空类,没有成员变量
class B {
    void Print() {} // 只有成员函数,没有成员变量
};

cout << sizeof(A) << endl;  // 输出:1
cout << sizeof(B) << endl;  // 输出:1

空类的大小为 1 字节,这是 C++ 标准规定的——为了让每个对象在内存中有唯一的地址。成员函数不占用对象的内存空间,它们存放在代码段中。

class Date —— 模板/蓝图(不占运行时内存)

实例化

实例化

成员变量声明
int _year;
int _month;
int _day;

成员函数定义
void Init();
void Print();

对象 d1
栈内存
_year=2024
_month=7
_day=10
sizeof=12

对象 d2
栈内存
_year=2024
_month=7
_day=5
sizeof=12

代码段(所有对象共享)
Init() 的二进制代码
Print() 的二进制代码


四、this 指针

4.1 什么是 this 指针

当你写下:

d1.Init(2024, 3, 31);
d2.Init(2024, 7, 5);

d1d2 调用的是同一个 Init 函数。编译器怎么知道要初始化的是 d1 还是 d2 的成员变量呢?

答案是 this 指针。编译器在编译成员函数时,会隐式地增加一个参数:

// 你写的:
void Init(int year, int month, int day) {
    _year = year;
    _month = month;
    _day = day;
}

// 编译器处理后的等价形式:
void Init(Date* const this, int year, int month, int day) {
    this->_year = year;
    this->_month = month;
    this->_day = day;
}

调用时也做了转换:

d1.Init(2024, 3, 31);
// 编译器转换为:
d1.Init(&d1, 2024, 3, 31);

this 指针工作机制一图胜千言:

运行时 编译器 源代码 运行时 编译器 源代码 d1.Init(2024, 3, 31) 隐式转换: d1.Init(&d1, 2024, 3, 31) 生成汇编,this 通过 ecx 寄存器传递 void Init(Date* const this, ...) this->>_year = 2024 → 写入 d1 的内存 this->>_month = 3 this->>_day = 31

4.2 this 指针的特性

  • 类型Date* const this —— 指针本身不可修改(不能 this = ...),但指向的对象可以修改
  • 位置:this 通过 ecx 寄存器(VS 下)传递,而不是压栈
  • 作用域:只能在非静态成员函数中使用
  • 可为空:在以下情况下 this 可以为 nullptr,只要不访问成员变量就不会崩溃
class A {
public:
    void Print() {
        cout << "A::Print()" << endl;  // 没有访问成员变量
    }
    int _a;
};

int main() {
    A* p = nullptr;
    p->Print();      // OK!没有解引用空指针,只是传递了 nullptr 给 this
    // p->_a = 1;    // 崩溃!访问空指针的成员变量
}

理解:p->Print() 底层是调用 Print(p),函数内部没有使用 this 访问任何成员变量,所以不会崩溃。

4.3 this 的显式使用

正常情况下你不需要写 this->,编译器会自动补全。但以下场景需要显式写出:

class Date {
public:
    void Init(int year, int month, int day) {
        // 如果参数名和成员名冲突,必须用 this 区分
        // 此时不带 this 的 year 指的是参数
        // (建议成员变量加 _ 前缀来避免冲突)
    }
};

五、C++ 类的默认成员函数

如果一个类是"空"的,编译器会自动生成 6 个默认成员函数

6个默认成员函数

初始化和清理

构造函数

对象创建时自动调用

可重载

析构函数

对象销毁时自动调用

不可重载

拷贝复制

拷贝构造函数

用已有对象初始化新对象

浅拷贝/深拷贝

赋值运算符重载

两个已存在对象间赋值

需考虑自我赋值

地址相关

取地址运算符重载

普通对象取地址

const取地址运算符重载

const对象取地址

  1. 构造函数 —— 用于对象初始化
  2. 析构函数 —— 用于对象销毁时的清理
  3. 拷贝构造函数 —— 用于创建一个已有对象的副本
  4. 赋值操作符重载 —— 用于对象间的赋值
  5. 取地址操作符重载 —— 返回对象的地址
  6. const 取地址操作符重载 —— const 对象的地址

下面我们详细讲解最重要的两个:构造函数和析构函数。


六、构造函数

6.1 为什么需要构造函数

构造函数是对象创建时自动调用的特殊成员函数,用于初始化。

class Date {
public:
    void Init(int year, int month, int day) {
        _year = year;
        _month = month;
        _day = day;
    }
private:
    int _year;
    int _month;
    int _day;
};

int main() {
    Date d1;
    d1.Init(2024, 7, 10);  // 必须先创建对象,再调用 Init——两步操作
    // 如果忘记调用 Init,成员变量就是随机值(未初始化)
}

有没有办法在创建对象的同时就完成初始化呢?构造函数就是干这个的。

6.2 构造函数的语法

  • 函数名与类名相同
  • 没有返回值(不是 void,是真的没有返回类型)
  • 对象创建时自动调用
  • 可以重载(多个构造函数,参数列表不同)
class Date {
public:
    // 1. 无参构造函数
    Date() {
        _year = 1;
        _month = 1;
        _day = 1;
    }

    // 2. 带参构造函数
    Date(int year, int month, int day) {
        _year = year;
        _month = month;
        _day = day;
    }

    // 3. 全缺省构造函数(推荐写法——一个顶多个)
    Date(int year = 1, int month = 1, int day = 1) {
        _year = year;
        _month = month;
        _day = day;
    }

    void Print() {
        cout << _year << "/" << _month << "/" << _day << endl;
    }

private:
    int _year;
    int _month;
    int _day;
};

int main() {
    Date d1;               // 调用无参/全缺省构造函数
    Date d2(2024, 7, 10);  // 调用带参构造函数
    Date d3(2024);         // 使用缺省值,月日=1
    d1.Print();            // 输出:1/1/1
    d2.Print();            // 输出:2024/7/10
    d3.Print();            // 输出:2024/1/1
}

推荐写法:使用全缺省构造函数,一个函数覆盖多种调用方式。

构造函数生命周期概览:

无(编译器生成)

定义对象
Date d1;

分配内存
(栈/堆)

有构造函数?

调用构造函数
初始化成员变量

默认构造函数
内置类型=随机值
类类型=调默认构造

对象可用

...使用对象...

作用域结束

调用析构函数

释放内存

6.3 默认构造函数

默认构造函数指不需要传参就能调用的构造函数,它可以是以下三种之一:

  1. 编译器自动生成的(如果你一个构造函数都没写)
  2. 你自己写的无参构造函数 Date()
  3. 你自己写的全缺省构造函数 Date(int year = 1, int month = 1, int day = 1)

重要规则:如果你写了任何一个构造函数,编译器就不再自动生成默认构造函数。此时如果你还想用 Date d1; 的方式创建对象,你必须自己提供默认构造函数。

class Date {
public:
    Date(int year, int month, int day) { /* ... */ }
private:
    int _year, _month, _day;
};

int main() {
    // Date d1;             // 错误!没有默认构造函数
    Date d2(2024, 7, 10);   // OK,使用带参构造函数
}

6.4 一个常见陷阱

Date d1;       // OK,调用默认构造函数
Date d2();     // 这不是创建对象!这是声明一个返回 Date 的函数!

// d2.Print(); // 错误!d2 是函数声明,不是对象

Date d2(); 被编译器解析为函数声明而不是对象定义,这是 C++ 的"最令人迷惑的解析"(Most Vexing Parse)。要调用默认构造函数,直接 Date d2;Date d2{};(C++11)。

6.5 构造函数与资源管理

构造函数最常见的用途之一:申请资源。

typedef int STDataType;
class Stack {
public:
    Stack(int n = 4) {
        _a = (STDataType*)malloc(sizeof(STDataType) * n);
        if (nullptr == _a) {
            perror("malloc fail");
            return;
        }
        _capacity = n;
        _top = 0;
    }
    // ...
private:
    STDataType* _a;
    size_t _capacity;
    size_t _top;
};

int main() {
    Stack st1;       // 默认容量 4,malloc 在构造函数中自动调用
    Stack st2(100);  // 容量 100
}

对比 C 语言的方式:

// C 语言:必须显式调用 Init,两步操作
ST st;
STInit(&st);       // 容易忘记调用

// C++:构造函数自动完成,一步到位
Stack st;          // 构造函数自动调用,不会忘记

七、析构函数

7.1 为什么需要析构函数

有创建就有销毁。构造函数负责"申请资源",析构函数负责"释放资源"。

7.2 析构函数的语法

  • 函数名:~类名
  • 无参数、无返回值
  • 对象生命周期结束时自动调用(不能重载,一个类只有一个析构函数)
  • 执行顺序:先创建的后销毁,后创建的先销毁(栈的行为)
class Stack {
public:
    Stack(int n = 4) {
        _a = (STDataType*)malloc(sizeof(STDataType) * n);
        _capacity = n;
        _top = 0;
    }

    ~Stack() {
        free(_a);
        _a = nullptr;
        _top = _capacity = 0;
    }

private:
    STDataType* _a;
    size_t _capacity;
    size_t _top;
};

7.3 用 C++ 版 Stack 简化代码

看一个实际对比——括号匹配问题,C 版本 vs C++ 版本:

// C 版本:需要手动调用 Init 和 Destroy
bool isValid_C(const char* s) {
    ST st;
    STInit(&st);
    while (*s) {
        if (*s == '(' || *s == '[' || *s == '{') {
            STPush(&st, *s);
        } else {
            if (STEmpty(&st)) {
                STDestroy(&st);  // 别忘了清理!
                return false;
            }
            char top = STTop(&st);
            STPop(&st);
            if ((top == '(' && *s != ')')
                || (top == '[' && *s != ']')
                || (top == '{' && *s != '}')) {
                STDestroy(&st);  // 别忘了清理!
                return false;
            }
        }
        ++s;
    }
    bool ret = STEmpty(&st);
    STDestroy(&st);  // 别忘了清理!
    return ret;
}

// C++ 版本:构造函数和析构函数自动管理资源
bool isValid_Cpp(const char* s) {
    Stack st;  // 构造自动调用
    while (*s) {
        if (*s == '(' || *s == '[' || *s == '{') {
            st.Push(*s);
        } else {
            if (st.Empty()) {
                return false;  // 析构自动调用,无需手动清理!
            }
            char top = st.Top();
            st.Pop();
            if ((top == '(' && *s != ')')
                || (top == '[' && *s != ']')
                || (top == '{' && *s != '}')) {
                return false;  // 析构自动调用!
            }
        }
        ++s;
    }
    return st.Empty();  // 析构自动调用!
}

C++ 版本代码更简洁、更安全——无论函数从哪个 return 退出,析构函数都会自动执行,不会发生资源泄漏。这就是 RAII(Resource Acquisition Is Initialization) 的核心思想。

C++ RAII 方式

构造函数自动 Init

使用中...

析构函数自动 Destroy

✅ 任何 return → 自动析构 → 绝不泄漏

C 语言方式

STInit(&st) —— 手动初始化

使用中...

STDestroy(&st) —— 手动清理

❌ 提前 return → 忘记 Destroy → 内存泄漏

7.4 编译器自动生成的析构函数

如果类中没有动态分配的资源(如 malloc/new 出来的指针),编译器生成的析构函数就够了。如 Date 类只有 int 成员,编译器生成的析构函数什么都不用做。


八、运算符重载入门

C++ 允许我们给自定义类型定义运算符的行为,让代码更加直观。

class Date {
public:
    Date(int year = 1, int month = 1, int day = 1) {
        _year = year;
        _month = month;
        _day = day;
    }

    // 成员函数形式重载 ==
    bool operator==(Date d2) {
        return _year == d2._year
            && _month == d2._month
            && _day == d2._day;
    }

private:
    int _year, _month, _day;
};

int main() {
    Date x1(2024, 7, 10);
    Date x2(2024, 7, 11);

    // 以下两种写法等价:
    x1.operator==(x2);   // 显式调用
    x1 == x2;            // 编译器转换为 x1.operator==(x2)
}

运算符重载的本质是一个名为 operatorX 的特殊函数:

你写 编译器转换
x1 == x2 x1.operator==(x2)
x1 + x2 x1.operator+(x2)
x1 = x2 x1.operator=(x2)

运算符重载不会改变运算符的优先级和结合性。它是语法糖,目的是让自定义类型的使用方式和内置类型一样自然。

篇幅所限,运算符重载的详细讲解留到中篇和下篇。


总结

上篇知识体系总览:

C++类和对象 上篇

类的定义

class vs struct

默认访问权限不同

成员函数 + 成员变量

封装思想

访问限定符

public —— 外部可访问

private —— 仅类内可访问

protected —— 继承中用

实例化

类 = 模板,不占内存

对象 = 实例,占用内存

空类大小为 1 字节

this指针

编译器隐式传递

类型: Date* const

通过 ecx 寄存器

构造函数

对象创建时自动调用

可重载

推荐全缺省形式

析构函数

对象销毁时自动调用

RAII 思想

后创建先销毁

运算符重载入门

本质是 operatorX 函数

语法糖

不改变优先级

本篇我们学习了 C++ 类和对象的基础:

  1. 类的定义class 关键字,成员变量 + 成员函数,分号结尾
  2. 访问限定符publicprivateprotected,class 默认 private
  3. 实例化:类只是模板,对象才是真正占用内存的实体
  4. this 指针:编译器隐式传递给每个非静态成员函数,指向调用该函数的对象
  5. 构造函数:对象创建时自动调用,用于初始化,可以重载,推荐全缺省形式
  6. 析构函数:对象销毁时自动调用,用于清理资源,体现 RAII 思想
  7. 运算符重载入门:让自定义类型支持运算符,本质是 operatorX 函数

下一篇(中篇),我们将深入讲解拷贝构造函数、深拷贝与浅拷贝、赋值运算符重载、比较运算符重载、const 成员函数等重要内容。

更多推荐