类与对象(上):面向过程到面向对象的跨越,类的定义、封装与 this 指针等核心概念深度剖析
🔥 脏脏a的技术站 🔥
「在代码的世界里,脏脏的技术探索从不设限~」
🚀 个人主页:脏脏a-CSDN博客📌 技术聚焦:类的定义、封装、this指针等
📊 文章专栏:C++🔗 上篇回顾:【下篇】C++入门
目录
一、 面向对象VS面向过程
1.1 面向过程:“按步骤做事”
1.2 面向对象:“拆对象,靠交互”
二、类的引入
三、类的定义
3.1 声明 + 定义全在类里
3.2 声明在.h,定义在.cpp(推荐)
3.3 类的作用域
小技巧:成员变量命名规则
四、访问限定符与封装
1. 三个访问限定符
2. 封装的本质
【面试题】:C++中struct和class的区别
五、类的实例化
六、类对象大小计算
1. 为什么不算成员函数?
2. 三个例子看懂大小计算
3. 内存对齐规则(和结构体一样)
【面试题】
问题 1:结构体怎么对齐?为什么要进行内存对齐?
问题 2:如何让结构体按照指定的对齐参数进行对齐?能否按照 3、4、5 即任意字节对齐?
问题 3:什么是大小端?如何测试某台机器是大端还是小端,有没有遇到过要考虑大小端的场景?
七、this 指针:“隐藏的对象地址”
1. this 指针的引出
2. this 指针的 3 个关键特性
【面试题】:
问题 1:this 指针存在哪里?
问题 2:this 指针可以为空吗?
八、C 和 C++ 实现栈的对比
一、 面向对象VS面向过程
C 语言是面向过程的,C++ 是基于面向对象的 —— 这不是一句空话,而是解决问题的思路完全不同。
1.1 面向过程:“按步骤做事”
比如 “洗衣服” 这个需求,C 语言的思路是拆成 “线性步骤”,用函数一步步实现:
// 伪代码:面向过程的洗衣服
void 拿盆子() { ... }
void 放水() { ... }
void 放衣服() { ... }
void 手搓() { ... }
void 拧干() { ... }
void 晾衣服() { ... }// 主逻辑:按顺序调用函数
int main() {拿盆子();放水();放衣服();手搓();拧干();晾衣服();return 0;
}
核心是 “关注过程”,函数是操作的载体,数据(比如衣服、水)和函数是分离的。
1.2 面向对象:“拆对象,靠交互”
同样是 “洗衣服”,C++ 会先拆分出对象(衣服、洗衣机、洗衣粉),再定义每个对象的 “属性” 和 “行为”,最后靠对象交互完成需求:
- 衣服对象:属性(脏污程度、材质),行为(被清洗、被晾干)
- 洗衣机对象:属性(容量、转速),行为(注水、洗衣、甩干)
- 洗衣粉对象:属性(类型、用量),行为(溶解、去污)
核心是 “关注对象”,把数据和操作数据的方法 “打包” 到对象里,逻辑更贴近现实世界,也更适合复杂系统。
二、类的引入
C 语言的结构体只能放变量,但 C++ 的结构体(甚至更常用的class)能同时放变量和函数—— 这是 C++ 实现 “数据 + 方法封装” 的基础。
【示例】:
typedef int DataType;
struct Stack {// 成员函数:操作栈的方法void Init(size_t capacity) {_array = (DataType*)malloc(sizeof(DataType) * capacity);if (nullptr == _array) { perror("malloc失败"); return; }_capacity = capacity;_size = 0;}void Push(const DataType& data) {// 扩容逻辑(简化)_array[_size] = data;++_size;}DataType Top() { return _array[_size - 1]; }void Destroy() {if (_array) { free(_array); _array = nullptr; _capacity = 0; _size = 0; }}// 成员变量:栈的数据DataType* _array;size_t _capacity;size_t _size;
};// 使用时直接调用对象的方法,不用传结构体指针!
int main()
{Stack s;s.Init(10); // 直接调用成员函数s.Push(1);s.Push(2);cout << s.Top() << endl; // 输出2s.Destroy();return 0;
}
不过在 C++ 里,更推荐用class代替struct定义类 —— 两者的核心区别是默认访问权限(后面会讲)。
三、类的定义
class className
{// 类体:由成员函数和成员变量组成}; //分号不能丢
定义类有两种写法,关键看 “成员函数的定义位置”,实际开发中要根据场景选。
3.1 声明 + 定义全在类里
成员函数直接在类内实现,编译器可能会把它当成内联函数(适合简单函数,比如 Get/Set):
class Person {
public:// 成员函数:类内定义(可能被视为内联)void ShowInfo() {cout << _name << " " << _sex << " " << _age << endl;}// 成员变量char* _name; // 姓名char* _sex; // 性别int _age; // 年龄
};
3.2 声明在.h,定义在.cpp(推荐)
类的声明放在头文件(如person.h),成员函数的实现放在源文件(如person.cpp)—— 这是大型项目的标准写法,避免重复包含和编译错误。
【person.h(声明)】
#ifndef PERSON_H
#define PERSON_H // 防止头文件重复包含
#include <iostream>
using namespace std;class Person
{
public:// 成员函数声明void ShowInfo();// 成员变量char* _name;char* _sex;int _age;
};#endif
【person.cpp(定义)】
#include "person.h" // 包含头文件// 成员函数定义:必须加“类名::”指明作用域
void Person::ShowInfo()
{cout << _name << " " << _sex << " " << _age << endl;
}
3.3 类的作用域
类定义了新的作用域,名为类域。同命名空间类般能解决命名冲突。类的所有成员都在类的作用域中。在类体外定义成员时,需要使用 ::作用域操作符指明成员属于哪个类域进行优先查找。
//这里PrintPersonInfo函数属于Person类域
void Person:: PrintPersonInfo()
{cout<<_name<<_sex<<_age<<endl;
}
小技巧:成员变量命名规则
一定要区分 “成员变量” 和 “函数形参”,避免像下面这样写 “无效代码”:
class Date {
public:void Init(int year) {year = year; // 到底是给成员变量赋值,还是形参自己赋值?}
private:int year; // 成员变量
};
推荐加前缀(如_)或后缀区分,比如:
class Date {
public:void Init(int year) {_year = year; // 清晰:_year是成员变量,year是形参}
private:int _year; // 加前缀_// 或 int mYear; // 加前缀m(微软常用)
};
四、访问限定符与封装
封装是面向对象的三大特性之一(另外两个是继承、多态),而 C++ 靠 “访问限定符” 实现封装 —— 核心是 “隐藏内部细节,只暴露有用接口”。
1. 三个访问限定符
| 限定符 | 作用域内访问 | 类外访问 | 说明 |
|---|---|---|---|
| public | 可以 | 可以 | 对外暴露的接口(如成员函数) |
| protected | 可以 | 不可以 | 目前和private一样,到继承那会有区别 |
| private | 可以 | 不可以 | 隐藏的内部细节(如成员变量) |
注意两点:
- 访问权限的作用域 “从当前限定符开始,到下一个限定符结束”;
class默认访问权限是private,struct默认是public(为了兼容 C)。
2. 封装的本质
比如 “电脑” 这个对象:
- 内部细节(CPU、内存、显卡):隐藏(
private),用户不用关心; - 对外接口(开关机键、键盘、USB 口):暴露(
public),用户通过接口交互。
对应到 C++ 类里,就是把 “成员变量” 设为private,“操作变量的方法” 设为public:
class Computer
{
private:// 内部细节:隐藏,用户不能直接改int _cpuFreq; // CPU频率int _memorySize; // 内存大小public:// 对外接口:暴露,用户通过方法操作void PowerOn() { cout << "开机,CPU频率:" << _cpuFreq << endl; }void PowerOff() { cout << "关机" << endl; }void SetMemory(int size) { _memorySize = size; } // 间接修改成员变量
};int main()
{Computer c;c.PowerOn(); // 可以:public接口c.SetMemory(16); // 可以:间接修改private变量// c._cpuFreq = 3000; // 报错:private成员,类外不能访问return 0;
}
【面试题】:C++中struct和class的区别
1、C++需要兼容C语言,所以C++中struct可以当成结构体使用。另外C++中struct还可以用来定义类。和class定义类是一样的,区别是struct定义的类默认访问权限是public,class定义的类默认访问权限是private。
2.继承:
对于struct,如果不写public、protected或private继承,默认就是公有继承(public继承)。
对于class,如果不写继承方式,默认就是私有继承(private继承)。
3.模版参数:

五、类的实例化
类只是 “对象的设计图”,比如 “学生信息表” 是类,“张三的信息” 才是对象 ——实例化就是用类创建对象的过程。
【关键区别】:类没有内存,对象才有
class Person
{
public:char* _name;int _age;
};int main()
{// Person._age = 18; // 报错:类没有内存,不能直接访问成员Person zhangsan; // 实例化对象:分配内存zhangsan._age = 18; // 可以:对象有内存,能访问成员return 0;
}
类比:类是 “建筑设计图”,对象是 “按图盖的房子”—— 设计图没有实体,房子才有实际空间。

六、类对象大小计算
类里既有成员变量又有成员函数,但计算对象大小时,只算成员变量的大小(还要考虑内存对齐),成员函数存在公共代码区。
1. 为什么不算成员函数?
如果每个对象都存一份成员函数,100 个对象就有 100 份相同的代码,会浪费内存 —— 所以成员函数统一存在 “公共代码区”,所有对象共享。
2. 三个例子看懂大小计算
sizeof(类名)实际计算的是实例化后对象的大小
// 例1:有成员变量
class A1 {
public:void f1() {}
private:int _a; // 4字节
};
cout << sizeof(A1) << endl; // 输出4(只算_a,不算f1)// 例2:只有成员函数
class A2 {
public:void f2() {}
};
cout << sizeof(A2) << endl; // 输出1(空类的占位符,不是函数大小)// 例3:空类(没有成员)
class A3 {};
cout << sizeof(A3) << endl; // 输出1(编译器给1字节,标识对象存在)
3. 内存对齐规则(和结构体一样)
计算成员变量大小时,要遵守 “内存对齐”(为了 CPU 高效访问),规则如下:
- 第一个成员在偏移量 0 的位置;
- 其他成员对齐到 “对齐数” 的整数倍(对齐数 = 编译器默认对齐数(VS 默认是 8)和成员大小的较小值);
- 整个对象的大小是 “最大对齐数” 的整数倍。
- 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
【比如】:
class A {
private:char _a; // 1字节,对齐数1int _b; // 4字节,对齐数4(min(8,4))double _c;// 8字节,对齐数8(min(8,8))
};
// 计算:1(_a) + 3(填充) + 4(_b) + 8(_c) = 16字节(最大对齐数8,16是8的整数倍)
cout << sizeof(A) << endl; // 输出16

【面试题】
问题 1:结构体怎么对齐?为什么要进行内存对齐?
- 对齐规则: 上面有
- 对齐原因:
- 硬件层面:多数 CPU 访问内存时按固定字节(如 4、8 字节)对齐,非对齐访问会降低效率甚至无法访问;
- 性能层面:对齐后 CPU 可单次读取成员,提升数据访问速度。
问题 2:如何让结构体按照指定的对齐参数进行对齐?能否按照 3、4、5 即任意字节对齐?
- 指定对齐方式:
- 编译器指令:如
#pragma pack(n)(n为对齐字节数,如#pragma pack(4)指定 4 字节对齐); - C++11 属性:
[[gnu::aligned(n)]](GCC 扩展)或alignas(n)(标准语法)。
- 编译器指令:如
- 能否任意对齐:不能。对齐参数
n需是 2 的幂(如 1、2、4、8 等),因为硬件访问机制基于 2 的幂对齐,非 2 幂(如 3、5)的对齐参数编译器可能不支持或引发性能 / 兼容性问题。
问题 3:什么是大小端?如何测试某台机器是大端还是小端,有没有遇到过要考虑大小端的场景?
- 大小端定义:
- 大端(Big Endian):数据高位字节存低地址,如
0x1234存为0x12(低地址)、0x34(高地址); - 小端(Little Endian):数据低位字节存低地址,如
0x1234存为0x34(低地址)、0x12(高地址)。
- 大端(Big Endian):数据高位字节存低地址,如
- 测试方法:
int a = 1; // 二进制0x00000001 if (*(char*)&a == 1) printf("小端\n"); else printf("大端\n"); - 应用场景:网络通信(TCP/IP 协议用大端,需转换字节序)、文件读写(跨平台存储二进制数据时需统一字节序)、硬件交互(如嵌入式设备与 CPU 字节序不一致时需处理)。
七、this 指针:“隐藏的对象地址”
为什么d1.Print()和d2.Print()能分别打印自己的日期?秘密就在this指针里。
1. this 指针的引出
日期类例子:
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, _month, _day;
};int main()
{Date d1, d2;d1.Init(2022, 1, 11);d2.Init(2022, 1, 12);d1.Print(); // 输出2022-1-11d2.Print(); // 输出2022-1-12return 0;
}
Print()函数里没有区分d1和d2,怎么知道该打印哪个对象的_year?
答案是:编译器会给每个 “非静态成员函数” 加一个隐藏的 this 指针参数,把调用对象的地址传进去 —— 上面的代码其实被编译器处理成这样:
// 编译器的隐藏操作(用户不用写)
void Init(Date* this, int year, int month, int day)
{this->_year = year; // 通过this指针访问当前对象的成员this->_month = month;this->_day = day;
}void Print(Date* this)
{cout << this->_year << "-" << this->_month << "-" << this->_day << endl;
}// 调用时也自动传地址
d1.Init(&d1, 2022, 1, 11);
d1.Print(&d1);
2. this 指针的 3 个关键特性
- 类型:
类类型* const,即指向当前类对象的常量指针,不能被赋值以指向其他对象; - 存储与传递:通过寄存器(而非栈)隐式传递,是成员函数的隐式形参,不存储在对象内部;
- 空指针场景:是否崩溃取决于是否解引用
this。若成员函数不访问成员变量(未解引用this),this为空时可正常运行;若访问成员变量(解引用this),this为空则会触发未定义行为(通常崩溃)。
下面程序的编译结果是:
// 例1:不访问成员变量,this为空也运行
class A
{
public:void Print() { cout << "Print()" << endl; }
private:int _a;
};int main()
{A* p = nullptr;p->Print(); // 正常运行:Print()不访问_a,不用解引用thisreturn 0;
}// 例2:访问成员变量,this为空崩溃
class A
{
public:void PrintA() { cout << _a << endl; } // 要访问_a,需解引用this
private:int _a;
};
int main()
{A* p = nullptr;p->PrintA(); // 崩溃:this是nullptr,解引用报错return 0;
}
【面试题】:
问题 1:this 指针存在哪里?
在 C++ 中,this指针通常存储在寄存器中(如 x86 架构下的ecx寄存器),由编译器在编译阶段隐式处理,不会在栈、堆或全局数据区单独分配内存。它是成员函数调用时的隐式参数,用于指向当前对象的地址。
问题 2:this 指针可以为空吗?
this指针可以为空,但需注意场景:
- 当通过空指针调用成员函数时(如
NULL->func()),若函数内部未访问成员变量或调用其他依赖对象的操作,程序可能不会崩溃; - 但如果函数中访问了成员变量(如
this->_a),则会因解引用空指针导致未定义行为(程序崩溃)。
因此,从语法上this可为空,但从逻辑和安全性上应避免通过空指针调用成员函数并访问对象资源。
八、C 和 C++ 实现栈的对比
总结 C++ 类的优势:
| 对比维度 | C 语言实现 | C++ 类实现 |
|---|---|---|
| 数据与方法 | 分离:结构体存数据,函数单独写 | 封装:数据和方法放一起 |
| 函数参数 | 必须传结构体指针(如Stack* ps) | 编译器自动传 this,不用手动传指针 |
| 安全性 | 结构体成员可随意修改(无访问控制) | 成员变量设为 private,只能通过方法改 |
| 代码简洁度 | 调用函数要传指针(如StackPush(&s, 1)) | 直接调用对象方法(如s.Push(1)) |

