C++面向对象与类和对象之旅(上)----C++重要基础入门知识
C++面向对象与类和对象之旅(上)----C++重要基础入门知识
第一个重要问题:面向对象和面向过程的区别
类和对象的知识是我们学习C++编程的基础,它在我们的知识体系中扮演了一个至关重要的角色。
但在开始学习类、对象、继承这些具体概念之前,我们首先要解决一个最根本的问题:到底什么是“面向对象”?它和我们可能更熟悉的“面向过程”编程有什么不同?
一、面向过程编程:关注步骤的“流水线”
核心思想: “怎么做?”——关注的是解决问题需要的一系列步骤或函数。
在面向过程的世界里,程序员像一个流水线工程师,他的任务是精确地设计出每一个操作步骤。对于“把大象装进冰箱”这个问题,他的思路会是这样的:
- 打开冰箱门。 
- 把大象塞进去。 
- 关上冰箱门。 
面向过程的特点:
- 核心是函数: 程序由一个个函数组成,数据(如 - door(门的开关状态),- content(冰箱中的内容))和操作数据的函数是分离的。
- 线性思维: 代码按照预定的步骤顺序执行。 
- 优点: 流程直观,在解决小型、任务明确的问题时非常高效。 
缺点: 当系统变得庞大复杂时,数据和函数的关系会变得混乱。如果想增加一个“给冰箱通电”的功能,相关的数据和函数可能会散落在程序的各个角落,难以维护和扩展。
二、面向对象编程:关注对象之间的相互作用
核心思想: “谁来做?”——关注的是解决问题中涉及到的各类对象,以及它们之间的交互。
在面向对象的世界里,程序员像一个社会架构师。他首先会定义出这个问题的解决方案里有哪几种角色(类),每种角色有什么属性(数据成员)和能力(成员函数)。对于同样的问题,他的思路会是:
- 这个问题里涉及几个对象?—— 冰箱和大象。 
- 每个对象各自有什么属性和能力? - 冰箱:有门(状态)、内部容量、可以执行开门、装东西、关门等动作。 
- 大象:有名字、体积等属性。 
 - 面向对象的特点: - 核心是类和对象: 程序是由多个对象组成的,对象是数据(属性)和操作(方法)的封装体。 
- 三大特性: - 封装: 把数据和处理数据的方法捆绑在一起,并可以隐藏内部细节(利用public,protected,private等访问限定符,后面会讲)。比如, - main函数不需要知道- Refrigerator(初始化冰箱状态) 内部如何记录门的状态,只需要调用它的- openDoor()方法。
- 继承: 允许我们基于已有的类创建新类,实现代码的复用和扩展。(后续章节详解) 
- 多态: 允许不同的对象对同一消息做出不同的响应。(后续章节详解) 
 
- 优点: 代码结构清晰,更接近现实世界,易于维护、扩展和复用。当需求变化时,通常只需要修改或扩展某个类,而不会影响整个程序,方便工程管理。 
 
- 举个例子:我们在竞技游戏中,某个角色的技能出现了bug,例如王者荣耀的露娜大招不刷新的bug。我们会说策划怎么还不修复露娜大招bug。而这个说法说的正是对象!我们的露娜是一个对象。如果我们是面向过程,我们则会说哪一步出了问题,而不是谁谁谁出了问题。 - 可以看出,面向对象似乎比面向过程更贴近我们人的思维方式 
第二个重要问题:C++中结构体升级成了类
在C语言中,我们学习的struct已经具有了很强大的功能,它能够在内部定义变量,我们用它来实现了很多很多数据结构。
而现在,在C++中,我们不在拘泥于定义变量,我们的对象,需要有更多的功能与方法,来进行更加复杂却便利的交互。
所以我们的struct迎来了升级:
一、从C的 struct 到C++的 class:一次伟大的升级
在C语言中,struct 只是一个数据集合,它允许你将不同的数据类型组合在一起,形成一个新的复合类型。但它不能包含函数(方法)。
我们的struct中现在不仅可以定义变量,我们还可以定义包含方法,而我们所说的这些struct的功能已经无限接近class的功能。
C语言的 struct(数据包):
c
// C语言代码
struct Person_C {char name[20];int age;
}; 
// 只能定义数据,不能定义函数
// 操作这个结构体的函数必须与结构体分离
void printPerson(struct Person_C p) {printf("Name: %s, Age: %d\n", p.name, p.age);
}到了C++,class 的概念被引入,它极大地扩展了 struct 的能力。C++中的 class 是一个数据与行为的封装体。
C++的 class(智能对象):
cpp
// C++代码
class Person_CPP {
private:std::string name; int age;
public:// 可以在类内部定义函数(方法)void setName(const std::string& newName) {name = newName;}void setAge(int newAge) {if(newAge > 0) { // 可以加入逻辑验证age = newAge;}}void print() const { // 成员函数std::cout << "Name: " << name << ", Age: " << age << std::endl;}
};升级的核心点:
- 数据封装:可以包含成员变量(数据)和成员函数(行为)。 
- 访问控制:引入了 - public、- private、- protected关键字来控制成员的访问权限。
- 构造函数/析构函数:提供了对象自动初始化和清理的能力。 
- 继承与多态:成为面向对象继承体系的基础。 
可以说我们的C++中的struct已经无限接近于class,但是他们还有一些细微的区别.
二、C++中的 struct 与 class:孪生兄弟的细微差别
现在来到问题的关键:C++中的 struct 和 class 有什么区别?
答案是:除了默认的成员访问权限和默认的继承方式,它们没有任何功能上的区别。 C++对原有的C风格 struct 进行了增强,让它拥有了 class 的所有能力。
核心区别:默认访问权限
- class:成员和继承默认是- private的。
- struct:成员和继承默认是- public的。
让我们用代码来展示这个区别:
示例1:默认成员访问权限
cpp
// 使用 class 关键字
class MyClass {int data; // 默认为 private,外部无法直接访问
public:void setData(int d) { data = d; }
};
// 使用 struct 关键字
struct MyStruct {int data; // 默认为 public,外部可以直接访问void setData(int d) { data = d; }
};
int main() {MyClass obj_c;// obj_c.data = 10; // 错误!data 是 private 成员obj_c.setData(10); // 必须通过公共接口
MyStruct obj_s;obj_s.data = 20; // 正确!data 是 public 成员obj_s.setData(20); // 当然也可以这样做,也就是说通过公共接口或是直接访问都可以
return 0;
}示例2:默认继承方式
cpp
class Base {
public:int x;
};
// class 默认是 private 继承
class DerivedClass : Base { // 等价于 : private Base// 在DerivedClass内部,Base的public成员x变成了private
};
// struct 默认是 public 继承
struct DerivedStruct : Base { // 等价于 : public Base// 在DerivedStruct内部,Base的public成员x仍然是public
};
int main() {DerivedClass d_c;// d_c.x = 10; // 错误!由于是private继承,x在派生类中不可见
DerivedStruct d_s;d_s.x = 10; // 正确!由于是public继承,x在派生类中保持public
return 0;
}重要提示:尽管默认行为不同,但我们强烈建议在代码中显式地写出访问控制符和继承方式,以避免混淆。
cpp
// 好的实践:显式声明
class MyClass {
private:int data;
public:MyClass() = default;
};
struct MyStruct {
public: // 虽然默认就是public,但写上更清晰int data;MyStruct() = default;
};
class Derived : public Base { // 显式声明public继承// ...
};诶现在我们有了一个新的疑问?那我到底什么时候用Class, 什么时候用Struct?
三、如何选择:struct vs class?
既然功能几乎一样,我们该如何选择?C++社区形成了一些约定俗成的惯例:
- 使用 - struct:- 当你主要需要一个纯粹的数据结构时。 
- 当所有成员都希望是公共的时(例如:坐标点 - Point {x, y},配置参数- Config {width, height})。
- 用于与C代码交互的兼容性结构体。 
- 在模板元编程中,有时用 - struct来作为“编译期函数”或特性标签,因为它默认的public更便捷。
 
- 使用 - class:- 当你需要定义一个具有复杂行为和严格封装的抽象数据类型(Abstract Data Type)时。 
- 当你需要用到私有成员、保护成员,并需要通过公共接口来与对象交互时。 
- 当你打算使用继承和多态等面向对象特性时(尽管 - struct也可以,但- class的语义更贴切)。
 
简单来说:struct 感觉更像一个“数据包”,而 class 感觉更像一个“功能完整的对象”。
总结
- C++的 - class是C语言- struct的超级增强版,引入了成员函数和访问控制。
- 在C++中, - struct和- class是几乎完全相同的概念。
- 它们唯一的核心区别在于默认的访问权限和继承方式。 
- 在实际编程中,根据语义和惯例来选择使用哪一个,并始终显式地写出访问控制符,这能让你的代码更清晰、更专业。 
理解了这一点,你就可以自信地在C++中使用这两种关键字了。
第三个重要问题:类的定义与编码规范
理解了面向对象的思想和class与struct的关系后,现在让我们正式学习如何在C++中定义类。这就像学习一门新语言的语法规则一样,是打好基础的关键一步。
一、类的基本语法结构
cpp
class ClassName 
{// 类体:由成员函数和成员变量组成
};  // 一定要注意这个分号!- class:定义类的关键字
- ClassName:类的名字(遵循大驼峰命名法,如- MyClass)
- {}:类的主体,包含类的所有成员(成员变量啊,成员函数啊等等)
- ;:类定义结束的分号绝对不能省略!这是很多初学者容易犯错的地方。
类的成员分为两类:
- 成员变量(属性):描述对象的特征,如人的年龄、姓名 
- 成员函数(方法):描述对象的行为,如人会走路、说话 - 比如我们定义一个学生类,那么他的成员变量就是学号,姓名,年龄 - 他的成员函数就是写作业,考试,上课 - 就好像语文中的名词与动词的区别一样。 
二、类的两种定义方式
方式1:声明和定义全部放在类体中
cpp
class Date {
public:// 成员函数直接在类内部定义void Init(int year, int month, int day) {_year = year;_month = month;_day = day;}void Print() {std::cout << _year << "-" << _month << "-" << _day << std::endl;}
private:int _year;int _month;int _day;
};特点:
- 优点:写法简单,适合教学和小型项目 
- 注意:在类内部定义的成员函数,编译器可能会将其当作内联函数处理 - 这里我们可以简单了解一下内联函数(不是重点,可以跳过): - 传统函数调用过程: - 跳转到函数地址 
- 保存现场(压栈) 
- 执行函数体 
- 恢复现场(出栈) 
- 跳转回调用处 
 - 内联函数的处理方式: 编译器会将内联函数的代码体直接展开到调用处,避免函数调用的开销(省略了压栈出栈一系列步骤)。 - 重要提醒:即使是显式使用 - inline关键字,也只是给编译器的建议,不是命令!编译器最终还是会根据自己的判断来决定是否真正内联。- 内联的优缺点- 优点: - 消除函数调用开销,提高性能 
- 避免跳转指令,有利于CPU指令流水线 
 - 缺点: - 代码膨胀:每个调用处都展开一份函数体 
- 可能增加编译后文件大小 
- 调试困难(函数调用栈信息可能丢失) 
 - 可能被当成内联函数这一点是需要我们避免的,在这里编译器有完全的自主权,到底有没有被当成内联函数我们不得而知。这是一种不确定性,而且内联可能导致编译完之后代码膨胀(因为展开) 
(方式2:声明和定义分离(推荐!)
Date.h(头文件):
cpp
class Date {
public:// 只在类中声明函数void Init(int year, int month, int day);void Print();
private:int _year;int _month;int _day;
};Date.cpp(源文件):
cpp
#include "Date.h"
#include <iostream>
// 在源文件中定义函数,需要加上类名和作用域解析符 ::
void Date::Init(int year, int month, int day) {_year = year;_month = month;_day = day;
}
void Date::Print() {std::cout << _year << "-" << _month << "-" << _day << std::endl;
}为什么推荐方式2?
- 分离编译:头文件只放声明,源文件放实现,符合软件工程原则 
- 减少依赖:修改函数实现时,只需要重新编译对应的.cpp文件 
- 代码清晰:接口和实现分离,更易于阅读和维护 
- 避免内联膨胀:不会意外产生大量的内联函数(与上文相呼应) 
三、成员变量命名规范(重要!)
这是一个看似简单但极其重要的实践问题。先看一个反面教材:
cpp
class Date {
public:void Init(int year) {// 灾难!这里的year到底是成员变量还是函数参数?year = year; // 自己赋值给自己,毫无意义!}private:int year; // 成员变量
};上面的代码中,year = year 实际上只是把参数赋值给自己,成员变量完全没有被修改!
这样既导致了严重的南辕北辙的逻辑错误,也使阅读代码变成非常困难的一件事。比我们在C语言中的普通的变量命名规范重要的多,后者只是规范而前者是你必须好好命名否则会有严重后果
解决方案:使用命名区分
方案1:使用下划线前缀(常见于Linux/Unix风格)
cpp
class Date {
public:void Init(int year) {_year = year; // 清晰明了!}private:int _year;    // 成员变量加下划线int _month;int _day;
};方案2:使用'm'前缀(常见于Windows/MFC风格)
cpp
class Date {
public:void Init(int year) {mYear = year; // 同样清晰!}private:int mYear;    // 成员变量加m前缀int mMonth;int mDay;
};核心原则:
- 一致性:选择一个风格并在整个项目中坚持使用 
- 可读性:让成员变量在代码中一眼就能被认出来 
- 避免歧义:确保不会与函数参数或局部变量混淆 
在实际工作中,请遵循你所在团队的编码规范。没有绝对的对错,只有一致性最重要!
第四个重要问题:访问限定符与封装——面向对象的大门守卫
通过前几个问题的讲解,我们已经知道如何定义类了。但面向对象不仅仅是把数据和函数打包在一起,更重要的是控制外界如何与我们的对象交互。这就是我们今天要讲的访问限定符和封装。
一、访问限定符
想象一下,类的成员就像一栋建筑里的不同房间,而访问限定符就是这些房间的门禁系统,控制着谁可以进入哪个房间。
而房间我们有三个分级权限按秘密性更高分别递增public(公共的)<protected(受保护的)<private(私有的)
C++提供了三种访问限定符:
cpp
class BankAccount {
private:// 金库区 - 只有银行内部人员可以进入double balance;          // 余额string password;         // 密码protected:// 办公区 - 银行员工和经理可以进入double calculateInterest(); // 计算利息public:// 营业大厅 - 所有客户都可以使用void deposit(double amount);  // 存款void withdraw(double amount); // 取款double getBalance();          // 查询余额
};访问限定符详解:
1. private(私有)
- 权限:只能在类的内部访问 
- 用途:隐藏实现细节,保护敏感数据 
- 示例: - balance,- password这些数据不应该被外部直接修改- 不过我们依然可以提供特定的get方法来让外界适当的访问数据,后面说。 
2. protected(保护)
- 权限:在类内部和派生类中可访问 
- 用途:为继承体系设计,我们讲到继承时会详细说明 - 在继承里常见。 
3. public(公有)
- 权限:在任何地方都可以访问 
- 用途:提供对外的接口,定义类与外界交互的方式 
重要规则:
- 作用域规则:从访问限定符出现的位置开始,到下一个访问限定符或类结束为止 - cpp - class Example { public:int a; // publicint b; // public  private:int c; // privateint d; // private  public:int e; // public }; // 类结束
- 默认访问权限: - class:默认是- private
- struct:默认是- public(为了兼容C语言)
 
- 编译时检查:访问权限只在编译阶段检查,运行时没有任何区别 
二、封装:面向对象的"黑箱哲学"
什么是封装?
封装 = 数据 + 操作数据的方法 + 访问控制(对应成员变量,成员函数,访问限定符:封装就是我们来造一个完整的类)
用一句话概括:"隐藏实现细节,只暴露必要的接口"
现实世界的封装例子:
电脑的封装:
- 隐藏的:CPU如何运算、内存如何管理、硬盘如何存储 
- 暴露的:开机按钮、USB接口、显示器、键盘鼠标 
汽车的封装:
- 隐藏的:发动机工作原理、变速箱机制、燃油喷射系统 
- 暴露的:方向盘、油门、刹车、仪表盘 - 用户管他那么多,只管用就完了(使用,但不考虑底层) 
代码示例:没有封装 vs 有封装
没有封装的糟糕设计:
cpp
// 糟糕!所有数据都是public的
class Person {
public:string name;int age;double salary;
};
// 使用时可以直接修改,可能出现不合理的数据
Person p;
p.age = -5;        // 年龄可以是负数?不合理!
p.salary = 100000; // 随便设置薪水?不安全!甚至在一个项目中的同事,都有可能无意间改造了你的代码数据!这将导致你遭到无比严峻的惩罚
良好的封装设计:
cpp
class Person {
private:string name;int age;double salary;
public:// 通过公共方法来访问和修改数据void setAge(int newAge) {if (newAge >= 0 && newAge <= 150) { // 数据验证age = newAge;} else {cout << "无效的年龄!" << endl;}}void setSalary(double newSalary, bool isAdmin) {if (isAdmin) { // 权限检查salary = newSalary;} else {cout << "无权修改薪水!" << endl;}}int getAge() const { return age; }string getName() const { return name; }// 注意:没有提供getSalary(),薪水信息对外隐藏
};
// 使用封装后的类
Person p;
p.setAge(25);           // 正确
p.setAge(-5);           // 会被拒绝并提示错误
p.setSalary(50000, false); // 无权限,操作被拒绝通过public方法,来限制和管理用户访问数据的方式。
如果你是游戏玩家,你大概也会像我一样梦想着通过修改游戏货币,来获得自己想要的角色和皮肤。可惜我们的游戏货币大概是private变量,而游戏公司没给我们public的方法.
三、封装带来的巨大好处
1. 数据保护
cpp
class BankAccount {
private:double balance;
public:void withdraw(double amount) {if (amount <= balance) {  // 防止透支balance -= amount;}}
};2. 实现灵活性
cpp
class DataStorage {
private:// 内部实现可以随时改变,不影响外部使用者vector<int> data;  // 今天用vector// 明天可以改成 map<int, int> 或其他结构public:void addData(int value) {data.push_back(value);}int getData(int index) {return data[index];}
};3. 易于维护 当需要修改验证逻辑时,只需要在一个地方修改:
cpp
void setAge(int newAge) {// 只需要在这里修改验证规则if (newAge >= 0 && newAge <= 120) { // 从150改为120age = newAge;}
}4. 降低复杂度 使用者不需要了解内部实现,只需要知道接口怎么用:
cpp
// 使用者只需要知道: calculator.add(5, 3); // 结果是8 // 不需要知道: // - 是用CPU加法器实现的? // - 还是用位运算实现的? // - 有没有缓存机制?
节省了使用者的精力,我们的软件因此更好用。
四、封装的最佳实践
1. 数据成员通常设为 private
cpp
class GoodDesign {
private:int importantData;  // 数据隐藏
public:// 通过方法访问
};2. 提供完整的Get/Set方法
cpp
class Rectangle {
private:double width;double height;
public:double getWidth() const { return width; }void setWidth(double w) { if (w > 0) width = w; }double getHeight() const { return height; }void setHeight(double h) { if (h > 0) height = h; }// 提供有意义的业务方法,而不仅仅是Get/Setdouble getArea() const { return width * height; }
};这一点我们在所有面向对象的语言中都很常见,例如java也通过类似的方式访问
3. 构造函数初始化
cpp
class Student {
private:string name;int id;public:// 通过构造函数确保对象始终处于有效状态Student(const string& n, int i) : name(n), id(i) {// 可以在构造函数中进行验证}
};第五个重要问题:类的实例化与对象模型——从蓝图到实体
通过前面的学习,我们已经知道如何设计"蓝图"(类),现在让我们看看如何把蓝图变成"实体"(对象),以及这些实体在内存中是如何存储的。
一、类的实例化:从蓝图到建筑
什么是实例化?
类的实例化就是用类类型创建对象的过程。
生动的比喻:实例化,就是我们按照类的定义(这个建筑图纸),来创建一个实际的类对象(一栋实际的房子)
关键理解:类本身不占空间
cpp
class Person {
public:void introduce() {cout << "我叫" << name << ",今年" << age << "岁" << endl;}private:string name;int age;
};
int main() {// 错误!类本身没有空间,不能直接访问成员// Person::age = 100;  // 编译错误// 正确!必须先实例化对象Person p1;  // 创建第一个Person对象Person p2;  // 创建第二个Person对象return 0;
}重要结论:
- 类 = 设计图(不占内存空间) 
- 对象 = 实际建筑(占用物理内存) 
- 必须先实例化才能使用 
二、类对象模型:对象在内存中的秘密
当我们创建一个对象时,它到底包含什么?成员变量和成员函数是如何存储的?
三种可能的存储方式:
方式1:对象包含所有成员(变量+函数)
text
对象A: [变量_a] [函数PrintA代码] 对象B: [变量_b] [函数PrintA代码] ← 重复存储,浪费空间!
方式2:对象包含变量和函数指针
text
对象A: [变量_a] [指向PrintA的指针] 对象B: [变量_b] [指向PrintA的指针] ← 还是有多余的指针
方式3:对象只包含变量,函数在代码区
text
对象A: [变量_a] 对象B: [变量_b] 代码区: [PrintA函数代码] ← 只有一份,所有对象共享
真相验证:通过sizeof来探究
让我们用代码来验证计算机到底采用哪种方式:
cpp
#include <iostream>
using namespace std;
// 类中既有成员变量,又有成员函数
class A1 {
public:void f1() {}
private:int _a;
};
// 类中仅有成员函数  
class A2 {
public:void f2() {}
};
// 空类
class A3 {};
int main() {cout << "sizeof(A1): " << sizeof(A1) << endl;  // 输出多少?cout << "sizeof(A2): " << sizeof(A2) << endl;  // 输出多少?  cout << "sizeof(A3): " << sizeof(A3) << endl;  // 输出多少?return 0;
}运行结果:
text
sizeof(A1): 4 // 只有一个int成员变量 sizeof(A2): 1 // 没有成员变量,但空类不能为0 sizeof(A3): 1 // 空类
重要结论:
- 对象只存储成员变量,成员函数存放在公共的代码段 
- 空类大小为1字节:为了确保每个对象在内存中有唯一的地址 
- 计算类大小 = 所有成员变量大小之和(考虑内存对齐) 
三、结构体内存对齐规则
为什么sizeof(A1)是4而不是其他值?这就涉及到内存对齐。
为什么要内存对齐?
- 性能优化:CPU读取对齐的数据更快 
- 硬件要求:某些架构要求数据必须对齐访问 
对齐规则(重要!):
- 第一个成员在结构体偏移量0处 
- 其他成员要对齐到 - min(成员大小, 编译器默认对齐数)的整数倍地址- VS默认对齐数 = 8 
- Linux通常没有默认对齐数(按成员自身大小对齐) 
 
- 结构体总大小 = 最大对齐数的整数倍 
- 嵌套结构体:对齐到嵌套结构体自身最大对齐数的整数倍 
实战分析:
cpp
struct Example1 {char a;     // 1字节,偏移0int b;      // 4字节,对齐到4的倍数(偏移4)double c;   // 8字节,对齐到8的倍数(偏移8)// 总大小:8(c) + 8 = 16,是8的倍数 ✓
};
// sizeof(Example1) = 16
struct Example2 {int a;      // 4字节,偏移0  char b;     // 1字节,偏移4short c;    // 2字节,对齐到2的倍数// 总大小:4+1+2=7,但要是4的倍数 → 8
};
// sizeof(Example2) = 8
struct Example3 {char a;         // 1字节,偏移0// 填充3字节int b;          // 4字节,偏移4char c;         // 1字节,偏移8// 填充7字节(因为嵌套结构体最大对齐数是8)struct Inner {double d;   // 8字节} inner;
};
// sizeof(Example3) = 24关于这一知识点的:重要问题
1. 结构体怎么对齐?为什么要进行内存对齐?
对齐方法:
- 按成员声明顺序依次放置 
- 每个成员对齐到特定边界 
- 最后补齐到最大对齐数的整数倍 
对齐原因:
- 性能:对齐后CPU可以用更少的周期读取数据 
- 移植性:避免在不同平台出现兼容性问题 
- 硬件限制:某些CPU无法访问未对齐的内存 
2. 如何指定对齐参数?
cpp
// 指定按4字节对齐
#pragma pack(4)
struct AlignedStruct {char a;     // 1字节int b;      // 4字节,现在按4对齐(而不是8)double c;   // 8字节,但受pack(4)影响,按4对齐
};
#pragma pack()  // 恢复默认对齐
// C++11方式
struct alignas(8) MyStruct {int a;char b;
};注意:不能任意对齐(如3、5字节),必须是2的幂次方。
3. 大小端问题
什么是大小端?
- 大端模式:高位字节存储在低地址(人类阅读顺序) 
- 小端模式:低位字节存储在低地址(Intel/ARM常用) 
测试方法:
cpp
#include <iostream>
using namespace std;
void checkEndian() {int num = 0x12345678;//从左往右字节越来越低char* ptr = (char*)&num//强制按字节访问我们的整形if (*ptr == 0x78) {cout << "小端模式" << endl;  // 78在低地址} else {cout << "大端模式" << endl;  // 12在低地址}
}
需要考虑大小端的场景:
- 网络编程:网络字节序是大端,需要htonl/ntohl转换 
- 文件格式:某些文件格式指定了字节序 
- 硬件交互:与特定硬件设备通信时 
- 跨平台数据交换:不同架构的系统间传输数据 
- 底层对齐层面:内存对齐提升性能,大小端影响本讲数据存储 
最后一个重要问题:this指针——对象的自我认知
通过前面的学习,我们已经知道如何定义类、创建对象,但有一个关键问题:当多个对象调用同一个成员函数时,函数如何知道自己在操作哪个对象?
这就是 this 指针要解决的核心问题!
一、this指针的引出:对象身份的困惑
问题场景
cpp
class Date {
public:void Init(int year, int month, int day) {_year = year;    // 问题:这个_year属于哪个对象?_month = month;  // d1的month?还是d2的month?_day = day;}void Print() {cout << _year << "-" << _month << "-" << _day << endl;}
private:int _year;    int _month;   int _day;     
};
int main() {Date d1, d2;d1.Init(2022, 1, 11);  // 设置d1的日期d2.Init(2022, 1, 12);  // 设置d2的日期d1.Print();  // 打印d1d2.Print();  // 打印d2return 0;
}关键问题: 同一个Init函数,如何知道现在是要初始化d1还是d2?
二、this指针的真相:编译器的额外处理
编译器在背后做了什么?
实际上,编译器把我们的成员函数"翻译"成了这样:
cpp
// 我们写的:
void Init(int year, int month, int day) {_year = year;_month = month;_day = day;
}
// 编译器处理的:
void Init(Date* this, int year, int month, int day) {this->_year = year;this->_month = month;this->_day = day;
}
// 调用时:
d1.Init(2022, 1, 11);
// 被编译器转换为:
Init(&d1, 2022, 1, 11);  // 传递d1的地址作为第一个参数!显式使用this指针
虽然编译器自动处理,但我们也可以显式使用:
cpp
class Date {
public:void Init(int year, int month, int day) {this->_year = year;    // 显式使用thisthis->_month = month;this->_day = day;}// 更常见的用途:解决命名冲突void SetDate(int _year, int _month, int _day) {this->_year = _year;    // this->_year是成员变量this->_month = _month;  // _month是参数this->_day = _day;}// 返回对象自身的引用,支持链式调用Date& addYear(int years) {this->_year += years;return *this;  // 返回当前对象}Date& addMonth(int months) {this->_month += months;return *this;}
private:int _year, _month, _day;
};
// 链式调用
Date d;
d.addYear(1).addMonth(2);  // 连续操作三、this指针的特性
1. 类型和不可变性
cpp
class MyClass {
public:void example() {// this的类型是:MyClass* const// 意味着:this本身是常量指针,不能修改this指向别的地址// this = nullptr;  // 错误!不能修改this指针本身}
};2. 作用域限制
cpp
class MyClass {
public:void memberFunction() {cout << this << endl;  // 正确:在成员函数内使用}private:// int* ptr = this;  // 错误!不能在类定义中直接使用
};
void externalFunction() {// cout << this << endl;  // 错误!不能在非成员函数中使用
}3. 存储位置和传递方式
cpp
// this指针是函数的形参,不是对象的一部分
class Empty {// 空类,没有成员变量
};
void demo() {Empty e;// e对象中不包含this指针!// this指针在调用成员函数时通过寄存器(如ecx)传递
}四、关键问题解析
问题1:this指针存在哪里?
答案: this指针是成员函数的形参,存在于函数调用的栈帧中,或者通过寄存器传递。
cpp
class Test {
public:void func() { /* 这个函数实际是:void func(Test* this) */ }
};
// 调用时:
Test obj;
obj.func();  // 实际传递:func(&obj)可以理解为this指针并不存在对象中,只有在调用时,他才会作为形参出现存在栈帧中的特定位置,调用结束又随着函数栈帧销毁消除了
问题2:this指针可以为空吗?
案例1:正常运行
cpp
class A {
public:void Print() {cout << "Print()" << endl;  // 没有访问成员变量}
private:int _a;
};
int main() {A* p = nullptr;p->Print();  //  正常运行!return 0;
}分析: 没有访问成员变量,不涉及this解引用,所以安全。
案例2:运行崩溃
cpp
class A {
public:void PrintA() {cout << _a << endl;  // 访问成员变量 ⇐ 这里会崩溃!}
private:int _a;
};
int main() {A* p = nullptr;p->PrintA();  //  运行崩溃!return 0;
}分析: 访问_a相当于this->_a,而this是nullptr,解引用空指针导致崩溃。
重要结论:
- this指针可以为空 
- 但只要不通过空this指针访问成员变量/虚函数,程序就不会崩溃 
- 这是一种未定义行为,要避免! 
