当前位置: 首页 > news >正文

C++面向对象与类和对象之旅(上)----C++重要基础入门知识

C++面向对象与类和对象之旅(上)----C++重要基础入门知识

第一个重要问题:面向对象和面向过程的区别

类和对象的知识是我们学习C++编程的基础,它在我们的知识体系中扮演了一个至关重要的角色。

但在开始学习类、对象、继承这些具体概念之前,我们首先要解决一个最根本的问题:到底什么是“面向对象”?它和我们可能更熟悉的“面向过程”编程有什么不同?

一、面向过程编程:关注步骤的“流水线”

核心思想: “怎么做?”——关注的是解决问题需要的一系列步骤或函数。

在面向过程的世界里,程序员像一个流水线工程师,他的任务是精确地设计出每一个操作步骤。对于“把大象装进冰箱”这个问题,他的思路会是这样的:

  1. 打开冰箱门。

  2. 把大象塞进去。

  3. 关上冰箱门。

面向过程的特点:

  • 核心是函数: 程序由一个个函数组成,数据(如 door(门的开关状态), content(冰箱中的内容))和操作数据的函数是分离的。

  • 线性思维: 代码按照预定的步骤顺序执行。

  • 优点: 流程直观,在解决小型、任务明确的问题时非常高效。

缺点: 当系统变得庞大复杂时,数据和函数的关系会变得混乱。如果想增加一个“给冰箱通电”的功能,相关的数据和函数可能会散落在程序的各个角落,难以维护和扩展。

二、面向对象编程:关注对象之间的相互作用

核心思想: “谁来做?”——关注的是解决问题中涉及到的各类对象,以及它们之间的交互。

在面向对象的世界里,程序员像一个社会架构师。他首先会定义出这个问题的解决方案里有哪几种角色(),每种角色有什么属性(数据成员)和能力(成员函数)。对于同样的问题,他的思路会是:

  1. 这个问题里涉及几个对象?—— 冰箱大象

  2. 每个对象各自有什么属性和能力?

    • 冰箱:有门(状态)、内部容量、可以执行开门、装东西、关门等动作。

    • 大象:有名字、体积等属性。

    面向对象的特点:

    • 核心是类和对象: 程序是由多个对象组成的,对象是数据(属性)和操作(方法)的封装体。

    • 三大特性:

      • 封装: 把数据和处理数据的方法捆绑在一起,并可以隐藏内部细节(利用public,protected,private等访问限定符,后面会讲)。比如,main 函数不需要知道 Refrigerator(初始化冰箱状态) 内部如何记录门的状态,只需要调用它的 openDoor() 方法。

      • 继承: 允许我们基于已有的类创建新类,实现代码的复用和扩展。(后续章节详解)

      • 多态: 允许不同的对象对同一消息做出不同的响应。(后续章节详解)

    • 优点: 代码结构清晰,更接近现实世界,易于维护、扩展和复用。当需求变化时,通常只需要修改或扩展某个类,而不会影响整个程序,方便工程管理。

  3. 举个例子:我们在竞技游戏中,某个角色的技能出现了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;}
};

升级的核心点:

  1. 数据封装:可以包含成员变量(数据)和成员函数(行为)。

  2. 访问控制:引入了 publicprivateprotected 关键字来控制成员的访问权限。

  3. 构造函数/析构函数:提供了对象自动初始化和清理的能力。

  4. 继承与多态:成为面向对象继承体系的基础。

可以说我们的C++中的struct已经无限接近于class,但是他们还有一些细微的区别.

二、C++中的 structclass:孪生兄弟的细微差别

现在来到问题的关键:C++中的 structclass 有什么区别?

答案是:除了默认的成员访问权限和默认的继承方式,它们没有任何功能上的区别。 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++中,structclass几乎完全相同的概念

  • 它们唯一的核心区别在于默认的访问权限和继承方式。

  • 在实际编程中,根据语义和惯例来选择使用哪一个,并始终显式地写出访问控制符,这能让你的代码更清晰、更专业。

理解了这一点,你就可以自信地在C++中使用这两种关键字了。

第三个重要问题:类的定义与编码规范

理解了面向对象的思想和classstruct的关系后,现在让我们正式学习如何在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;
};

特点:

  • 优点:写法简单,适合教学和小型项目

  • 注意:在类内部定义的成员函数,编译器可能会将其当作内联函数处理

    这里我们可以简单了解一下内联函数(不是重点,可以跳过):

    传统函数调用过程:

    1. 跳转到函数地址

    2. 保存现场(压栈)

    3. 执行函数体

    4. 恢复现场(出栈)

    5. 跳转回调用处

    内联函数的处理方式: 编译器会将内联函数的代码体直接展开到调用处,避免函数调用的开销(省略了压栈出栈一系列步骤)。

    重要提醒:即使是显式使用 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(公有)

  • 权限:在任何地方都可以访问

  • 用途:提供对外的接口,定义类与外界交互的方式

重要规则:
  1. 作用域规则:从访问限定符出现的位置开始,到下一个访问限定符或类结束为止

    cpp

    class Example {
    public:int a;      // publicint b;      // public
    ​
    private:int c;      // privateint d;      // private
    ​
    public:int e;      // public
    };              // 类结束
  2. 默认访问权限

    • class:默认是 private

    • struct:默认是 public(为了兼容C语言)

  3. 编译时检查:访问权限只在编译阶段检查,运行时没有任何区别

二、封装:面向对象的"黑箱哲学"
什么是封装?

封装 = 数据 + 操作数据的方法 + 访问控制(对应成员变量,成员函数,访问限定符:封装就是我们来造一个完整的类)

用一句话概括:"隐藏实现细节,只暴露必要的接口"

现实世界的封装例子:

电脑的封装:

  • 隐藏的: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. 对象只存储成员变量,成员函数存放在公共的代码段

  2. 空类大小为1字节:为了确保每个对象在内存中有唯一的地址

  3. 计算类大小 = 所有成员变量大小之和(考虑内存对齐)

三、结构体内存对齐规则

为什么sizeof(A1)是4而不是其他值?这就涉及到内存对齐。

为什么要内存对齐?
  • 性能优化:CPU读取对齐的数据更快

  • 硬件要求:某些架构要求数据必须对齐访问

对齐规则(重要!):
  1. 第一个成员在结构体偏移量0处

  2. 其他成员要对齐到min(成员大小, 编译器默认对齐数)的整数倍地址

    • VS默认对齐数 = 8

    • Linux通常没有默认对齐数(按成员自身大小对齐)

  3. 结构体总大小 = 最大对齐数的整数倍

  4. 嵌套结构体:对齐到嵌套结构体自身最大对齐数的整数倍

实战分析:

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转换

  • 文件格式:某些文件格式指定了字节序

  • 硬件交互:与特定硬件设备通信时

  • 跨平台数据交换:不同架构的系统间传输数据

  1. 底层对齐层面:内存对齐提升性能,大小端影响本讲数据存储

最后一个重要问题: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,而thisnullptr,解引用空指针导致崩溃。

重要结论:

  • this指针可以为空

  • 但只要不通过空this指针访问成员变量/虚函数,程序就不会崩溃

  • 这是一种未定义行为,要避免!

http://www.dtcms.com/a/549526.html

相关文章:

  • MR30系列分布式I/O在造型机产线的应用
  • 网站建设优化网站排名河北百度seo点击软件
  • 杭州做网站模板网络搭建基础教程
  • 虚拟机的未来:云计算与边缘计算的核心引擎(一)
  • ​​比亚迪秦新能源汽车动力系统拆装与检测实训MR软件介绍​
  • 仓颉编程(21)扩展
  • 网站建设方案书php做旅游网站
  • 强化网站建设和管理东莞企业建站程序
  • [人工智能-大模型-112]:用通俗易懂的语言,阐述代价函数Cost Function(误差函数、偏差函数、距离函数)
  • 跨平台矩阵如何高效排期?
  • 吴中区网站建设wordpress页面点赞
  • 网站建设需求文案案例html情人节给女朋友做网站
  • MATLAB频散曲线绘制与相速度/群速度分析
  • LeetCode:204. 计数质数
  • MySQL 更新(UPDATE)语句的执行流程,包括 存储引擎内部的文件写入 和 主从复制的同步过程
  • HarmonyOS 系统分享功能概述
  • [crackme]033-dccrackme1
  • PNP机器人将要亮相2025 ROS中国区大会|发表演讲、共探具身智能新未来
  • 寻找大连网站建设企业建站公司是干嘛的
  • Slicer模块系统:核心继承架构解析
  • Mahony姿态解算算法解读
  • Nginx前端配置与服务器部署详解
  • 上海设计网站青岛航拍公司
  • ASR+MT+LLM+TTS 一体化实时翻译字幕系统
  • h5游戏免费下载:视觉差贪吃蛇
  • 【车载开发系列】如何用Parasoft实现跨平台编译环境的配置
  • 跨境网站开发公司青海做网站好的公司
  • Rust UI 框架GPUI 与 Electron 的对比
  • Go 的 IO 多路复用
  • 【论文精读】MicroCinema:基于分治策略的文本到视频生成新框架