文章目录
- [C++ 类和对象(上)](#C++ 类和对象(上))
-
- 一、本章导读
- 二、类的定义
-
- [1. 类定义的基本格式](#1. 类定义的基本格式)
- [2. 成员变量命名习惯(不是 C++ 语法强制要求)](#2. 成员变量命名习惯(不是 C++ 语法强制要求))
- [3. `class` 和 `struct` 的区别](#3.
class和struct的区别)
- 三、访问限定符⭐
-
- [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)
- [结论一:不能在形参和实参位置显式写 `this`](#结论一:不能在形参和实参位置显式写
- [4. `this` 指针存在哪里?](#4.
this指针存在哪里?)
- [1. 为什么需要 `this` 指针?](#1. 为什么需要
- 八、典型选择题结论⭐️⭐️⭐️
-
- [题 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++ 面向对象入门中的三个基础问题展开:
- 类怎么定义?
- 类的成员变量、成员函数、访问限定符、类域。
- 类和对象是什么关系?
- 类是抽象描述,对象是实际占内存的实体。
- 成员函数如何知道自己操作的是哪个对象?
- 通过隐藏的
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. class 和 struct 的区别
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;
};
这里:
Init、Print是public。_year、_month、_day是private。
3. 封装的初步体现
C++ 通过访问限定符实现封装:
(一般情况)
- 成员变量一般设为
private或protected。 - 成员函数中需要给外部使用的接口设为
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)
表示:
Init是Stack类域中的成员函数。
五、实例化⭐
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);
d1 和 d2 共享同一套成员函数代码,但拥有各自独立的成员变量。
重点:
对象中存储的是成员变量,不存储普通成员函数代码 。(注释1)
注释1:对象只保存状态(成员变量),成员函数代码属于类本身,统一存放在代码段,由 this 指针决定操作哪个对象。形象的讲就是

- d1、d2 的成员变量存在栈上(如果是局部对象);
- new Date 的成员变量存在堆上;
- Print()、Init() 的代码永远只存在一份,在代码区。
六、对象大小⭐⭐
1. 对象中到底存什么?
类对象中主要存储:
cpp
成员变量
普通成员函数不会存储在每个对象中。
原因:
- 成员函数编译后是指令,放在代码段。
- 多个对象调用的是同一份函数代码。
- 如果每个对象都存函数代码或函数指针,会造成浪费。
例如:
cpp
Date d1;
Date d2;
d1 和 d2 各自有独立数据,但 Init、Print 函数代码只有一份。
2. 对象大小遵循内存对齐⭐️⭐️
类对象大小遵循结构体内存对齐规则。
内存对齐核心规则:
- 第一个成员放在偏移量为
0的位置。 - 后续成员要放到自己对齐数整数倍的位置。
- 对齐数 =
编译器默认对齐数和成员自身大小的较小值。 - VS 中默认对齐数通常为
8。 - 结构体或类的总大小是最大对齐数的整数倍。
- 如果有嵌套结构体,嵌套结构体按自己的最大对齐数对齐。
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)
{
}
十一、问题自测清单
复习时可以用下面的问题自测:
class定义结束后为什么要写分号?- 成员变量和成员函数分别是什么?
class和struct默认访问权限有什么区别?public和private的作用分别是什么?- 为什么成员变量一般要设为
private? - 什么是类域?类外定义成员函数为什么要写
Stack::Init? - 类和对象有什么区别?
- 类实例化对象时,哪些内容会真正占对象空间?
- 普通成员函数是否存储在对象中?为什么?
- 空类大小为什么是 1?
this指针是什么?由谁传递?this指针能不能显式写在形参列表里?this指针能不能在函数体内使用?this指针存在哪里?是否在对象中?- 空指针调用成员函数什么时候可能正常,什么时候会崩溃?
- C++ 实现 Stack 相比 C 语言实现,最重要的变化是什么?
十二、要点总结
- 类 = 类型/设计图;对象 = 实例/实体。
- 类定义不分配对象数据空间,对象实例化才分配。
- 对象中主要存成员变量,不存普通成员函数。
- 空类大小是 1,用于占位表示对象存在。
- 成员函数访问成员变量,本质是通过
this指针访问。 this是隐含形参,通常在栈帧中,不在对象里。class默认private,struct默认public。- 成员变量一般私有,成员函数作为公共接口,这是封装。
- 类外定义成员函数必须加类域:
类名::函数名。 - 空指针调用成员函数是否崩溃,关键看函数体是否访问成员变量。
下期预告
类和对象(中)
结语
** 本文到此结束,感谢大家的阅读!如果觉得本文对你有所帮助,欢迎点赞、收藏、关注,也欢迎在评论区一起交流讨论。**
** 欢迎订阅我的深入理解 C++系列:专栏,我会结合大量示例,从「是什么、为什么、怎么实现」三个角度,带你系统学习 C++,真正理解每一个知识点背后的设计思想与底层原理。**
** 如果你希望快速复盘知识体系、梳理重点难点,也欢迎关注我的快速复习系列:专栏,帮助你高效回顾、查漏补缺。**
** 如果你想系统学习算法,从基础题型到常见算法模板,再到算法思想与竞赛技巧,也欢迎订阅我的 算法系列:专栏。**
** 如果本文对你有所帮助,欢迎三连支持一下哦!**
** 愿每一次敲下键盘,都比昨天更进一步;愿每一份坚持,都终将有所收获!**