深入理解C++系列(02)——类和对象(上)

文章目录

  • [C++ 类和对象(上)](#C++ 类和对象(上))
    • 一、本章导读
    • 二、类的定义
      • [1. 类定义的基本格式](#1. 类定义的基本格式)
      • [2. 成员变量命名习惯(不是 C++ 语法强制要求)](#2. 成员变量命名习惯(不是 C++ 语法强制要求))
      • [3. `class` 和 `struct` 的区别](#3. classstruct 的区别)
    • 三、访问限定符⭐
      • [1. 三种访问限定符](#1. 三种访问限定符)
      • [2. 访问限定符的作用范围](#2. 访问限定符的作用范围)
      • [3. 封装的初步体现](#3. 封装的初步体现)
    • 四、类域
      • [1. 类会形成新的作用域](#1. 类会形成新的作用域)
      • [2. 为什么类外定义要写 `Stack::`?](#2. 为什么类外定义要写 Stack::?)
    • 五、实例化⭐
      • [1. 类和对象的关系](#1. 类和对象的关系)
      • [2. 成员变量什么时候开空间?](#2. 成员变量什么时候开空间?)
      • [3. 一个类可以实例化多个对象](#3. 一个类可以实例化多个对象)
    • 六、对象大小⭐⭐
      • [1. 对象中到底存什么?](#1. 对象中到底存什么?)
      • [2. 对象大小遵循内存对齐⭐️⭐️](#2. 对象大小遵循内存对齐⭐️⭐️)
      • [3. 典型对象大小问题](#3. 典型对象大小问题)
      • [4. 空类大小为什么是 1?](#4. 空类大小为什么是 1?)
    • [七、`this` 指针⭐⭐](#七、this 指针⭐⭐)
      • [1. 为什么需要 `this` 指针?](#1. 为什么需要 this 指针?)
      • [2. `this` 指针的本质](#2. this 指针的本质)
      • [3. `this` 指针的几个关键结论](#3. this 指针的几个关键结论)
        • [结论一:不能在形参和实参位置显式写 `this`](#结论一:不能在形参和实参位置显式写 this)
        • [结论二:可以在函数体内部显式使用 `this`](#结论二:可以在函数体内部显式使用 this)
        • [结论三:`this` 的类型可以理解为 `类类型* const`](#结论三:this 的类型可以理解为 类类型* const)
      • [4. `this` 指针存在哪里?](#4. this 指针存在哪里?)
    • 八、典型选择题结论⭐️⭐️⭐️
      • [题 1:空指针调用不访问成员变量的成员函数](#题 1:空指针调用不访问成员变量的成员函数)
      • [题 2:空指针调用访问成员变量的成员函数](#题 2:空指针调用访问成员变量的成员函数)
    • [九、C 和 C++ 实现 Stack 的对比](#九、C 和 C++ 实现 Stack 的对比)
      • [1. C 语言写法特点](#1. C 语言写法特点)
      • [2. C++ 写法特点](#2. C++ 写法特点)
      • [3. 二者核心区别](#3. 二者核心区别)
      • [4. 最重要的变化:封装](#4. 最重要的变化:封装)
    • 十、易错点汇总
      • [易错点 1:类定义最后忘记分号](#易错点 1:类定义最后忘记分号)
      • [易错点 2:误以为类定义时成员变量已经开空间](#易错点 2:误以为类定义时成员变量已经开空间)
      • [易错点 3:误以为对象中存成员函数](#易错点 3:误以为对象中存成员函数)
      • [易错点 4:空类大小不是 0](#易错点 4:空类大小不是 0)
      • [易错点 5:`this` 指针不是对象成员](#易错点 5:this 指针不是对象成员)
      • [易错点 6:空指针调用成员函数不一定立刻崩溃](#易错点 6:空指针调用成员函数不一定立刻崩溃)
      • [易错点 7:类外定义成员函数忘记加类域](#易错点 7:类外定义成员函数忘记加类域)
    • 十一、问题自测清单
    • 十二、要点总结
    • 下期预告
    • 结语

◆ 博主: @此生决int
分享编程知识!深挖底层原理!持续原创更新!

热门专栏:

深入理解 C++系列:从语法入门到底层原理,系统掌握现代 C++

算法系列:从入门到精通,蓝桥杯、ACM、LeetCode 与面试算法全路线

快速复习系列:知识梳理、查漏补缺,考前冲刺必备

Java 速通系列:已学 C 语言,快速上手 Java,轻松备战期末考试

📌 本系列知识点前后关联较强,建议按照专栏顺序阅读哦

🎯 本系列主要面向的阅读人群

1,学完 C 语言,准备系统学习 C++ 的同学

2,已经学习过 C++,希望重新梳理知识体系的同学

3,想深入理解 C++ 设计思想与底层原理,而不仅仅停留在语法层面的同学

不止教你会写代码,更带你理解代码为什么这样设计!

上期回顾

上一篇我们主要学习了 《初识C++》 ,重点掌握了 C++对比C语言新增了命名空间、函数重载 、引用、inline 内联函数以及nullptr。如果还有印象模糊的地方,建议先回顾上一篇内容,再继续阅读本文。


C++ 类和对象(上)


一、本章导读

一、精简目录

一、本章导读

二、类的定义

三、访问限定符⭐

四、类域

五、实例化⭐

六、对象大小 ⭐⭐

七、this 指针⭐⭐

八、典型选择题结论 ⭐⭐⭐

九、C 和 C++ 实现 Stack 的对比

十、易错点汇总

十一、问题自测清单

十二、要点总结

二、本节内容总览

本节主要围绕 C++ 面向对象入门中的三个基础问题展开:

  1. 类怎么定义?
    • 类的成员变量、成员函数、访问限定符、类域。
  2. 类和对象是什么关系?
    • 类是抽象描述,对象是实际占内存的实体。
  3. 成员函数如何知道自己操作的是哪个对象?
    • 通过隐藏的 this 指针。

可以用一句话概括:

类把数据和操作数据的函数封装 到一起;对象负责存储具体数据;成员函数通过 this 指针找到当前对象的数据。**


二、类的定义

1. 类定义的基本格式

cpp 复制代码
class Stack
{
public:
    void Init(int n = 4)
    {
        // 成员函数
    }

private:
    int* _array;
    size_t _capacity;
    size_t _top;
};

注意点:

  • class 是定义类的关键字。
  • {} 内部是类体。
  • 类体中的变量叫:
    • 成员变量
    • 属性
  • 类体中的函数叫:
    • 成员函数
    • 方法
  • 类定义结束后的分号不能省略。

2. 成员变量命名习惯(不是 C++ 语法强制要求)

为了区分普通变量和成员变量,常见写法有:

cpp 复制代码
int _year;
int year_;
int m_year;

本人常用 _xxx 风格,例如:

cpp 复制代码
int _year;
int _month;
int _day;

注意:

这只是编码习惯,不是 C++ 语法强制要求。


3. classstruct 的区别

C++ 中 struct 也可以定义类,并且可以在结构体中写函数。

cpp 复制代码
struct ListNodeCPP
{
    void Init(int x)
    {
        next = nullptr;
        val = x;
    }

    ListNodeCPP* next;
    int val;
};

重点区别:

对比项 class struct
默认访问权限 private public
是否能定义成员函数 可以 可以
C++ 中是否能表示类型 可以 可以
推荐用途 更常用于封装类 更常用于简单数据结构

最重要的区别:默认访问权限的不同

cpp 复制代码
class A
{
    int x; // 默认 private
};

struct B
{
    int x; // 默认 public
};

三、访问限定符⭐

1. 三种访问限定符

cpp 复制代码
public:
protected:
private:

本节重点掌握:

访问限定符 类外能否直接访问 当前阶段理解
public 可以 对外提供的接口
private 不可以 内部实现细节
protected 不可以 暂时和 private 类似,继承中再区分

2. 访问限定符的作用范围

访问限定符从出现的位置开始生效,一直到:

  • 下一个访问限定符出现;或者
  • 类结束 }

例如:

cpp 复制代码
class Date
{
public:
    void Init(int year, int month, int day);
    void Print();

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

这里:

  • InitPrintpublic
  • _year_month_dayprivate

3. 封装的初步体现

C++ 通过访问限定符实现封装:

(一般情况)

  • 成员变量一般设为 privateprotected
  • 成员函数中需要给外部使用的接口设为 public

这样做的好处:

防止外部随意修改对象内部数据,让对象的状态由成员函数统一管理

例如 Stack 中:

cpp 复制代码
private:
    STDataType* _a;
    size_t _capacity;
    size_t _top;

外部不能直接改 _top_capacity,只能通过:

cpp 复制代码
s.Push(1);
s.Pop();
s.Top();
s.Empty();

这就是封装的基础价值。


四、类域

1. 类会形成新的作用域

类定义了一个新的作用域,类中所有成员都属于这个类域。

如果成员函数在类外定义,需要使用作用域限定符 ::

cpp 复制代码
class Stack
{
public:
    void Init(int n = 4);

private:
    int* _array;
    size_t _capacity;
    size_t _top;
};

void Stack::Init(int n)
{
    _array = (int*)malloc(sizeof(int) * n);
    _capacity = n;
    _top = 0;
}

2. 为什么类外定义要写 Stack::

如果写成:

cpp 复制代码
void Init(int n)
{
    _array = ...;
}

编译器会认为这是一个全局函数 ,而不是 Stack 的成员函数。

这样函数内部访问 _array_capacity_top 时,编译器在当前作用域找不到这些成员,就会报错。

所以必须写:

cpp 复制代码
void Stack::Init(int n)

表示:

InitStack 类域中的成员函数。


五、实例化⭐

1. 类和对象的关系

核心区别:

概念 含义 是否占用对象数据空间
抽象描述、设计图、类型 不占具体对象空间
对象 根据类创建出来的实体 占实际物理空间

经典类比:

类像建筑设计图,对象像根据设计图建出来的房子。设计图不能住人,房子才能住人。


2. 成员变量什么时候开空间?

在类中写成员变量时:

cpp 复制代码
class Date
{
private:
    int _year;
    int _month;
    int _day;
};

这里的 _year_month_day 只是声明对象将来有哪些数据成员。

真正开空间是在实例化对象时:

cpp 复制代码
Date d1;
Date d2;

此时:

  • d1 有自己独立的 _year/_month/_day
  • d2 也有自己独立的 _year/_month/_day

3. 一个类可以实例化多个对象

cpp 复制代码
Date d1;
Date d2;

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

d1d2 共享同一套成员函数代码,但拥有各自独立的成员变量。

重点:

对象中存储的是成员变量,不存储普通成员函数代码 。(注释1


注释1:对象只保存状态(成员变量),成员函数代码属于类本身,统一存放在代码段,由 this 指针决定操作哪个对象。形象的讲就是

  • d1、d2 的成员变量存在栈上(如果是局部对象);
  • new Date 的成员变量存在堆上;
  • Print()、Init() 的代码永远只存在一份,在代码区。

六、对象大小⭐⭐

1. 对象中到底存什么?

类对象中主要存储:

cpp 复制代码
成员变量

普通成员函数不会存储在每个对象中。

原因:

  • 成员函数编译后是指令,放在代码段。
  • 多个对象调用的是同一份函数代码。
  • 如果每个对象都存函数代码或函数指针,会造成浪费。

例如:

cpp 复制代码
Date d1;
Date d2;

d1d2 各自有独立数据,但 InitPrint 函数代码只有一份。


2. 对象大小遵循内存对齐⭐️⭐️

类对象大小遵循结构体内存对齐规则

内存对齐核心规则:

  1. 第一个成员放在偏移量为 0 的位置。
  2. 后续成员要放到自己对齐数整数倍的位置。
  3. 对齐数 = 编译器默认对齐数成员自身大小 的较小值。
  4. VS 中默认对齐数通常为 8
  5. 结构体或类的总大小是最大对齐数的整数倍。
  6. 如果有嵌套结构体,嵌套结构体按自己的最大对齐数对齐。

3. 典型对象大小问题

cpp 复制代码
class A
{
private:
    char _ch;
    int _i;
};

分析:

  • _ch 占 1 字节。
  • 为了让 _i 对齐到 4 的整数倍,中间会填充 3 字节。
  • _i 占 4 字节。
  • 总大小为 8 字节。

所以:

cpp 复制代码
sizeof(A) == 8

4. 空类大小为什么是 1?

cpp 复制代码
class B
{
public:
    void Print()
    {
    }
};

class C
{
};

B 没有成员变量,只有成员函数。

C 是完全空类。

但是:

cpp 复制代码
sizeof(B) == 1
sizeof(C) == 1

原因:

即使没有成员变量,也要给对象 1 个字节,用来标识这个对象真实存在。

注意:

  • 这个 1 字节不是用来存成员函数的。
  • 它只是占位标识。

七、this 指针⭐⭐

1. 为什么需要 this 指针?

成员函数代码只有一份,但可以被多个对象调用:

cpp 复制代码
Date d1;
Date d2;

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

问题是:

同一个 Init 函数,怎么知道当前应该修改 d1 还是 d2

答案:

编译器会给非静态成员函数隐含传递一个 this 指针。


2. this 指针的本质

你写的成员函数:

cpp 复制代码
void Init(int year, int month, int day)
{
    _year = year;
    _month = month;
    _day = day;
}

编译器大致会理解成:

cpp 复制代码
void Init(Date* const this, int year, int month, int day)
{
    this->_year = year;
    this->_month = month;
    this->_day = day;
}

重点:

  • this 是当前对象的地址。
  • 成员函数中访问成员变量,本质都是通过 this 指针访问。
  • this 指针由编译器自动传递。

3. this 指针的几个关键结论

结论一:不能在形参和实参位置显式写 this

不能这样写:

cpp 复制代码
void Init(Date* const this, int year, int month, int day); // 错误理解方式,不是用户写法

调用时也不能这样写:

cpp 复制代码
d1.Init(&d1, 2024, 3, 31); // 错误

应该写:

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

结论二:可以在函数体内部显式使用 this
cpp 复制代码
void Init(int year, int month, int day)
{
    this->_year = year;
    this->_month = month;
    this->_day = day;
}

结论三:this 的类型可以理解为 类类型* const

例如在 Date 类中:

cpp 复制代码
Date* const this

含义:

  • this 是指向当前对象的指针。
  • this 本身不能被修改!

所以不能写:

cpp 复制代码
this = nullptr; // 错误

但可以通过 this 修改对象成员:

cpp 复制代码
this->_year = 2024;

4. this 指针存在哪里?

选择题结论:

this 指针**本质是成员函数的一个隐含形参,**所以通常存在栈帧中。

注意:

this 指针不在对象里面。

对象里面存的是成员变量。


八、典型选择题结论⭐️⭐️⭐️

题 1:空指针调用不访问成员变量的成员函数

cpp 复制代码
class A
{
public:
    void Print()
    {
        cout << "A::Print()" << endl;
    }
private:
    int _a;
};

int main()
{
    A* p = nullptr;
    p->Print();
    return 0;
}

结论:

text 复制代码
正常运行

原因:

  • p->Print() 会把 p 作为 this 指针传给成员函数。
  • 函数体内没有访问成员变量,也就没有解引用空指针访问对象数据。
  • 因此通常可以正常打印。

题 2:空指针调用访问成员变量的成员函数

cpp 复制代码
class A
{
public:
    void Print()
    {
        cout << "A::Print()" << endl;
        cout << _a << endl;
    }
private:
    int _a;
};

int main()
{
    A* p = nullptr;
    p->Print();
    return 0;
}

结论:

text 复制代码
运行崩溃

原因:

  • _a 本质是 this->_a
  • 此时 this == nullptr
  • 访问 this->_a 等价于通过空指针访问对象成员,会崩溃。

九、C 和 C++ 实现 Stack 的对比

1. C 语言写法特点

C 语言中,数据和函数是分离的:

cpp 复制代码
typedef struct Stack
{
    STDataType* a;
    int top;
    int capacity;
} ST;

void STInit(ST* ps);
void STPush(ST* ps, STDataType x);
void STPop(ST* ps);

使用时要手动传地址:

cpp 复制代码
ST s;
STInit(&s);
STPush(&s, 1);
STPop(&s);

2. C++ 写法特点

C++ 中,数据和函数放进类里:

cpp 复制代码
class Stack
{
public:
    void Init(int n = 4);
    void Push(STDataType x);
    void Pop();
    bool Empty();
    int Top();
    void Destroy();

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

使用时:

cpp 复制代码
Stack s;
s.Init();
s.Push(1);
s.Pop();

3. 二者核心区别

对比项 C 语言实现 C++ 实现
数据和函数 分离 封装到类中
是否手动传对象地址 需要 &s 不需要,隐藏传 this
成员变量保护 外部可以随意访问结构体成员 可以用 private 限制
类型命名 常需要 typedef 类名本身就是类型名
默认参数 不支持 支持,如 Init(int n = 4)

4. 最重要的变化:封装

本节中 C++ Stack 的底层逻辑和 C 语言差别不大,仍然是:

  • 动态数组;
  • top 表示栈顶位置;
  • 容量不足时扩容;
  • 入栈、出栈、取栈顶。

真正重要的是形态变化:

C++ 把数据和操作数据的函数绑定到一起,并通过访问限定符控制外部访问,这是封装的体现。


十、易错点汇总

易错点 1:类定义最后忘记分号

cpp 复制代码
class Date
{
}; // 不能少

易错点 2:误以为类定义时成员变量已经开空间

正确理解:

类只是类型描述,实例化对象时才为成员变量分配空间。


易错点 3:误以为对象中存成员函数

正确结论:

对象中只存成员变量,普通成员函数代码在公共代码区,不属于某一个对象。


易错点 4:空类大小不是 0

cpp 复制代码
class C {};
sizeof(C) == 1

原因:

至少给 1 字节用于标识对象存在。


易错点 5:this 指针不是对象成员

正确结论:

this 是成员函数的隐含形参,不存储在对象中。


易错点 6:空指针调用成员函数不一定立刻崩溃

分情况:

情况 结果
成员函数不访问成员变量 可能正常运行
成员函数访问成员变量 通常运行崩溃

本质判断标准:

是否通过 this 解引用访问对象数据。


易错点 7:类外定义成员函数忘记加类域

错误:

cpp 复制代码
void Init(int n)
{
}

正确:

cpp 复制代码
void Stack::Init(int n)
{
}

十一、问题自测清单

复习时可以用下面的问题自测:

  1. class 定义结束后为什么要写分号?
  2. 成员变量和成员函数分别是什么?
  3. classstruct 默认访问权限有什么区别?
  4. publicprivate 的作用分别是什么?
  5. 为什么成员变量一般要设为 private
  6. 什么是类域?类外定义成员函数为什么要写 Stack::Init
  7. 类和对象有什么区别?
  8. 类实例化对象时,哪些内容会真正占对象空间?
  9. 普通成员函数是否存储在对象中?为什么?
  10. 空类大小为什么是 1?
  11. this 指针是什么?由谁传递?
  12. this 指针能不能显式写在形参列表里?
  13. this 指针能不能在函数体内使用?
  14. this 指针存在哪里?是否在对象中?
  15. 空指针调用成员函数什么时候可能正常,什么时候会崩溃?
  16. C++ 实现 Stack 相比 C 语言实现,最重要的变化是什么?

十二、要点总结

  • 类 = 类型/设计图;对象 = 实例/实体。
  • 类定义不分配对象数据空间,对象实例化才分配。
  • 对象中主要存成员变量,不存普通成员函数。
  • 空类大小是 1,用于占位表示对象存在。
  • 成员函数访问成员变量,本质是通过 this 指针访问。
  • this 是隐含形参,通常在栈帧中,不在对象里。
  • class 默认 privatestruct 默认 public
  • 成员变量一般私有,成员函数作为公共接口,这是封装。
  • 类外定义成员函数必须加类域:类名::函数名
  • 空指针调用成员函数是否崩溃,关键看函数体是否访问成员变量。

下期预告

类和对象(中)


结语

**  本文到此结束,感谢大家的阅读!如果觉得本文对你有所帮助,欢迎点赞、收藏、关注,也欢迎在评论区一起交流讨论。**

**  欢迎订阅我的深入理解 C++系列:专栏,我会结合大量示例,从「是什么、为什么、怎么实现」三个角度,带你系统学习 C++,真正理解每一个知识点背后的设计思想与底层原理。**

**  如果你希望快速复盘知识体系、梳理重点难点,也欢迎关注我的快速复习系列:专栏,帮助你高效回顾、查漏补缺。**

**  如果你想系统学习算法,从基础题型到常见算法模板,再到算法思想与竞赛技巧,也欢迎订阅我的 算法系列:专栏。**

**  如果本文对你有所帮助,欢迎三连支持一下哦!**


**  愿每一次敲下键盘,都比昨天更进一步;愿每一份坚持,都终将有所收获!**

相关推荐
zjun10012 小时前
C++:单例模式
c++·单例模式
-银雾鸢尾-6 小时前
C#中的拓展方法
开发语言·c#
guodingdingh8 小时前
软件开发工作问题总结0718
java·开发语言·数据库
优橙教育10 小时前
5G网优培训 vs Java开发:转行选哪个?
java·开发语言·5g
同勉共进11 小时前
记一例 vibe coding + gcc bug 导致的线程池死锁问题
c++·线程池·gcc·死锁·vibe coding
我叫洋洋11 小时前
C ++ [ hello world ]
c语言·c++·算法
SeaTunnel12 小时前
从 Python Script 地狱到标准化数据集成框架
大数据·开发语言·python·程序员·代码·seatunnel
碎光拾影12 小时前
ARM交叉工具链各工具作用及IMX6ULL平台LED+蜂鸣器裸机程序实现
java·开发语言·数据库
Marst Code13 小时前
(python)2026Plotly 库评估:交互式可视化到底值不值得引入?
开发语言·python