C++从菜鸟到强手:2.类和对象(上)—— 从结构体到类的跨越

个人主页:
wengqidaifeng

✨ 永远在路上,永远向前走

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

文章目录

    • 前言
    • 一、类的定义
      • [1.1 从 struct 到 class](#1.1 从 struct 到 class)
      • [1.2 类的基本语法](#1.2 类的基本语法)
    • 二、访问限定符
    • [三、类的实例化 ------ 类和对象的关系](#三、类的实例化 —— 类和对象的关系)
    • [四、this 指针](#四、this 指针)
      • [4.1 什么是 this 指针](#4.1 什么是 this 指针)
      • [4.2 this 指针的特性](#4.2 this 指针的特性)
      • [4.3 this 的显式使用](#4.3 this 的显式使用)
    • [五、C++ 类的默认成员函数](#五、C++ 类的默认成员函数)
    • 六、构造函数
      • [6.1 为什么需要构造函数](#6.1 为什么需要构造函数)
      • [6.2 构造函数的语法](#6.2 构造函数的语法)
      • [6.3 默认构造函数](#6.3 默认构造函数)
      • [6.4 一个常见陷阱](#6.4 一个常见陷阱)
      • [6.5 构造函数与资源管理](#6.5 构造函数与资源管理)
    • 七、析构函数
      • [7.1 为什么需要析构函数](#7.1 为什么需要析构函数)
      • [7.2 析构函数的语法](#7.2 析构函数的语法)
      • [7.3 用 C++ 版 Stack 简化代码](#7.3 用 C++ 版 Stack 简化代码)
      • [7.4 编译器自动生成的析构函数](#7.4 编译器自动生成的析构函数)
    • 八、运算符重载入门
    • 总结

前言

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

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

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


一、类的定义

1.1 从 struct 到 class

cpp 复制代码
// 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 类的基本语法

cpp 复制代码
class ClassName {
    // 成员函数(方法)
    // 成员变量(属性)
};

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


二、访问限定符

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

Date 类
可以访问
✅ 允许
❌ 禁止 d._year
private 区域------仅类内可访问
_year / _month / _day 等成员变量
public 区域------外部可访问
Init() / Print() 等成员函数
外部代码: d.Init() ✅

cpp 复制代码
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 的成员函数作为接口。这是"封装"的思想。


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

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

cpp 复制代码
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(类名) 得到的是该类对象的大小。

空类的大小

cpp 复制代码
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 指针

当你写下:

cpp 复制代码
d1.Init(2024, 3, 31);
d2.Init(2024, 7, 5);

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

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

cpp 复制代码
// 你写的:
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;
}

调用时也做了转换:

cpp 复制代码
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,只要不访问成员变量就不会崩溃
cpp 复制代码
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->,编译器会自动补全。但以下场景需要显式写出:

cpp 复制代码
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 为什么需要构造函数

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

cpp 复制代码
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,是真的没有返回类型)
  • 对象创建时自动调用
  • 可以重载(多个构造函数,参数列表不同)
cpp 复制代码
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; 的方式创建对象,你必须自己提供默认构造函数。

cpp 复制代码
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 一个常见陷阱

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

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

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

6.5 构造函数与资源管理

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

cpp 复制代码
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 语言的方式:

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

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

七、析构函数

7.1 为什么需要析构函数

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

7.2 析构函数的语法

  • 函数名:~类名
  • 无参数、无返回值
  • 对象生命周期结束时自动调用(不能重载,一个类只有一个析构函数)
  • 执行顺序:先创建的后销毁,后创建的先销毁(栈的行为)
cpp 复制代码
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++ 版本:

cpp 复制代码
// 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++ 允许我们给自定义类型定义运算符的行为,让代码更加直观。

cpp 复制代码
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 成员函数等重要内容。

相关推荐
雪靡10 小时前
Visual Studio 2026 优雅的给Cmake设置大代理
c++·ide·cmake·visual studio
5008410 小时前
PagedAttention 源码解析:KV Cache 怎么管理
开发语言·python
自律懒人10 小时前
2026年AI编程工具横评:Trae、Cursor、Claude Code、Copilot X,同一需求谁更强?
java·copilot·ai编程
夕除10 小时前
spring boot 13
java·mysql·spring
追烽少年x10 小时前
STL中的设计模式(二)
c++·设计模式
marlondu10 小时前
ScopedValue:Java 21 引入的结构化作用域值
java
risc12345610 小时前
DocumentsWriterDeleteQueue
java·开发语言
日月云棠10 小时前
12 Dubbo 2.7 服务发布全流程源码解析
java·后端
沈阳信息学奥赛培训10 小时前
C++ 位运算练习题
开发语言·c++