类与对象-上【由浅入深-C++】

文章目录

  • 前言
  • 第一章:编程思想的演变------从面向过程到面向对象
  • [第二章:类的引入 ------ 从 Struct 讲起](#第二章:类的引入 —— 从 Struct 讲起)
        • [1. 回顾 C 语言中的 Struct(传统的"数据包")](#1. 回顾 C 语言中的 Struct(传统的“数据包”))
        • [2. C++ 对 Struct 的"升级"](#2. C++ 对 Struct 的“升级”)
        • [3. 既然 Struct 这么强了,为什么还需要 Class?](#3. 既然 Struct 这么强了,为什么还需要 Class?)
        • [4. 对比总结(本章的核心知识点)](#4. 对比总结(本章的核心知识点))
  • 第三章:类的定义与核心机制
      • [3.1 预备知识:类的三大常识](#3.1 预备知识:类的三大常识)
        • [1. "蓝图"与"实体"的关系(实例化)](#1. “蓝图”与“实体”的关系(实例化))
        • [2. 成员的划分:属性与行为](#2. 成员的划分:属性与行为)
        • [3. 命名规范(约定俗成)](#3. 命名规范(约定俗成))
      • [3.2 类的标准定义语法](#3.2 类的标准定义语法)
      • [3.3 成员函数的两种实现方式](#3.3 成员函数的两种实现方式)
      • [3.4 完整代码演示](#3.4 完整代码演示)
      • [3.5 成员函数的参数](#3.5 成员函数的参数)
        • [1. 不需要传参:自给自足型](#1. 不需要传参:自给自足型)
        • [2. 需要传参:外部输入型](#2. 需要传参:外部输入型)
        • [3. 最核心的区别:C 语言 vs C++](#3. 最核心的区别:C 语言 vs C++)
        • 总结图表
      • [3.6 本章小结](#3.6 本章小结)
  • [第四章:OOP 的灵魂------封装与访问限定符](#第四章:OOP 的灵魂——封装与访问限定符)
    • [4.1 什么是封装 (Encapsulation)?](#4.1 什么是封装 (Encapsulation)?)
    • [4.2 C++ 的三大访问限定符](#4.2 C++ 的三大访问限定符)
      • [1. `public` (公共权限) ------ "客厅"](#1. public (公共权限) —— “客厅”)
      • [2. `private` (私有权限) ------ "卧室/保险箱"](#2. private (私有权限) —— “卧室/保险箱”)
      • [3. `protected` (保护权限) ------ "传家宝"](#3. protected (保护权限) —— “传家宝”)
    • [4.3 为什么要这么麻烦?(封装的好处)](#4.3 为什么要这么麻烦?(封装的好处))
      • [❌ 反面教材:没有任何封装](#❌ 反面教材:没有任何封装)
      • [✅ 正面教材:标准的封装](#✅ 正面教材:标准的封装)
      • 总结封装的意义:
    • [4.4 两个重要的补充知识点](#4.4 两个重要的补充知识点)
      • [1. `class` vs `struct` 的默认权限](#1. class vs struct 的默认权限)
      • [2. 成员变量命名规范](#2. 成员变量命名规范)
    • [4.5 C++的struct和class区别](#4.5 C++的struct和class区别)
      • [1. 核心区别:唯一的不同是"门没锁"](#1. 核心区别:唯一的不同是“门没锁”)
      • [2. 能力演示:Struct 也能"起舞"](#2. 能力演示:Struct 也能“起舞”)
      • [3. C 语言 struct vs C++ struct](#3. C 语言 struct vs C++ struct)
      • [4. 行业潜规则:什么时候用 struct,什么时候用 class?](#4. 行业潜规则:什么时候用 struct,什么时候用 class?)
        • [场景 A:使用 `struct` ------ "数据包"](#场景 A:使用 struct —— "数据包")
        • [场景 B:使用 `class` ------ "管理者/对象"](#场景 B:使用 class —— "管理者/对象")
  • 第五章:类的作用域
      • [5.1 什么是类作用域?](#5.1 什么是类作用域?)
      • [5.2 作用域解析运算符 `::`](#5.2 作用域解析运算符 ::)
  • 第六章:类的实例化
      • [6.1 再次强调:声明 vs 实例化](#6.1 再次强调:声明 vs 实例化)
      • [6.2 实例化的本质](#6.2 实例化的本质)
  • 第七章:类的对象大小的计算
      • [7.1 惊人的事实:成员函数不占对象空间](#7.1 惊人的事实:成员函数不占对象空间)
      • [7.2 案例验证](#7.2 案例验证)
      • [7.3 两个特殊规则](#7.3 两个特殊规则)
  • [第八章:类成员函数的 `this` 指针](#第八章:类成员函数的 this 指针)
      • [8.1 编译器背后的"手脚"](#8.1 编译器背后的“手脚”)
      • [8.2 `this` 指针的用途](#8.2 this 指针的用途)
        • [场景 1:解决名称冲突](#场景 1:解决名称冲突)
        • [场景 2:支持链式编程 (Returning `*this`)](#场景 2:支持链式编程 (Returning *this))
      • [8.3 容易忽略的细节](#8.3 容易忽略的细节)
      • [8.4 this指针存在哪里的](#8.4 this指针存在哪里的)
        • [1. 最经典的情况:寄存器(ECX)](#1. 最经典的情况:寄存器(ECX))
        • [2. 通用的情况:栈(Stack)](#2. 通用的情况:栈(Stack))
        • [3. x64 现代架构(你现在电脑大概率是这个)](#3. x64 现代架构(你现在电脑大概率是这个))
        • [4. 这里的【特大误区】](#4. 这里的【特大误区】)
      • [8.5 `this` 指针的特性](#8.5 this 指针的特性)
        • [1. 类型特性:它是"指针常量"](#1. 类型特性:它是“指针常量”)
        • [2. 作用域特性:只能在"非静态成员函数"中使用](#2. 作用域特性:只能在“非静态成员函数”中使用)
        • [3. 常函数特性:`const` 修饰后的变化](#3. 常函数特性:const 修饰后的变化)
        • [4. 空指针特性:`this` 可以是 `NULL` 吗?(高频面试题)](#4. 空指针特性:this 可以是 NULL 吗?(高频面试题))
        • [5. 自杀特性:`delete this`](#5. 自杀特性:delete this)
        • 总结表

前言

由于c++的类与对象较为繁琐复杂,本文介绍类与对象部分的相关内容。

(【由浅入深】是一个系列文章,它记录了我个人作为一个小白,在学习c++技术开发方向计相关知识过程中的笔记,欢迎各位彭于晏刘亦菲从中指出我的错误并且与我共同学习进步,作为该系列的第一部曲-c语言,大部分知识会根据本人所学和我的助手------通义,gimini等以及合并网络上所找到的相关资料进行核实誊抄,每一篇文章都可能会因为一些错误在后续时间增删改查,因为该系列按照我的网络课程学习笔记形式编写,我会使用绝大多数人使用的讲解顺序编写,所以基础框架和大部分内容案例会与他人一样,基础知识不会过于详细讲述)


第一章:编程思想的演变------从面向过程到面向对象

在深入学习 C++ 的语法(怎么写类、怎么写对象)之前,我们需要先调整大脑的思维方式。C++ 之所以强大,是因为它引入了面向对象编程 (OOP) 的思想,这是它与 C 语言最大的分水岭。

1.1 什么是编程范式?

简单来说,面向过程面向对象 是两种不同的 编程范式

  • 它们不是具体的语法,而是看待和解决问题的逻辑方式
  • C 语言是典型的面向过程语言。
  • C++ 是多范式语言,但其核心优势在于面向对象。

1.2 面向过程

核心思想:关注"怎么做" (How)

面向过程像是一个独裁的指挥官。它分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用。

  • 思维逻辑:步骤化、线性化。
  • 基本单位函数 (Function)
  • 数据处理:数据(变量)和操作数据的方法(函数)是分离的。

生活案例:手洗衣服

如果你用面向过程的思维去写一个"洗衣服"的程序,你的代码逻辑是这样的:

  1. 执行 拿桶() 函数
  2. 执行 放水() 函数
  3. 执行 放衣服() 函数
  4. 执行 放洗衣液() 函数
  5. 执行 手搓() 函数
  6. 执行 拧干() 函数

优缺点

  • 优点:流程清晰,效率高(贴近机器执行逻辑),适合编写底层代码(如驱动程序、嵌入式)。
  • 缺点:当程序变大时,代码会变得像"面条"一样乱。如果你要修改"洗衣服"变成"洗鞋子",可能需要修改大部分步骤,复用性差,难以维护。

1.3 面向对象

核心思想:关注"谁来做" (Who)

面向对象像是一个管理者 。它把问题分解成各个独立的实体(对象),每个对象都有自己的职责。解决问题就是让这些对象之间进行协作。

  • 思维逻辑:模块化、拟人化。
  • 基本单位类 (Class) 和 对象 (Object)
  • 数据处理 :数据和操作数据的方法被捆绑在一起(封装)。

生活案例:机洗衣服

如果你用面向对象的思维去解决"洗衣服"的问题,你的逻辑是这样的:

第一步:定义对象

我们需要两个主要对象:洗衣机

  • 洗衣机 这个对象内部包含了:转速、水量(数据/属性)以及 洗涤、甩干(功能/方法)。

第二步:对象交互

程序的主流程变成了对象之间的通信:

  1. 打开 洗衣机
  2. 把衣服放入 洗衣机
  3. 按下 洗衣机 的"启动"按钮。
  4. (此时,洗衣机 内部自己完成加水、洗涤、甩干, 不需要关心具体步骤)。

优缺点

  • 优点
    • 易维护:对象坏了换对象,不用改整个流水线。
    • 易复用:设计好的"洗衣机类"可以直接拿去别的房子(程序)用。
    • 易扩展:如果需要"洗鞋子",只需要给洗衣机增加一个"洗鞋模式",或者继承造一个"洗鞋机"。
  • 缺点:前期设计比较复杂,代码量可能比面向过程多一点,运行效率在某些极端情况下略低于面向过程(但在现代硬件上通常忽略不计)。

1.4 总结与对比

为了方便记忆,我们可以通过这张表来对比两者的区别:

维度 面向过程 (C 语言风格) 面向对象 (C++ 风格)
侧重点 过程、步骤、算法 对象、数据、交互
核心单位 函数 (Function) 类 (Class)
程序结构 自顶向下,逐步细化 自底向上,抽象出类,再组合
数据安全 数据往往暴露在全局,容易被误改 数据被封装在对象内部,更安全
比喻 蛋炒饭 (饭菜混在一起,很难把鸡蛋挑出来) 盖浇饭 (饭是饭,菜是菜,可以随意组合)

1.5 为什么我们要学 C++ 的 OOP?

在很多大型软件开发(如游戏引擎、操作系统UI、大型服务器)中,逻辑极其复杂。如果用面向过程的步骤式写法,代码行数一旦超过几万行,维护起来就是一场灾难。

面向对象的三大特性(后续章节重点):

  1. 封装:保护数据,不仅让代码安全,而且让代码用起来更简单。
  2. 继承:代码复用的神器,老爸有的功能,儿子天生就有。
  3. 多态:一个接口,多种实现,让程序极具灵活性。

第二章:类的引入 ------ 从 Struct 讲起

1. 回顾 C 语言中的 Struct(传统的"数据包")

在 C 语言中,结构体(struct)仅仅是数据的集合。它把一堆变量捆绑在一起,但它不管"动作"(函数)。

  • 痛点:数据和操作是分离的。

  • 代码示例

    c 复制代码
    // C语言写法
    struct Student {
        char name[20];
        int age;
    };
    
    // 操作函数必须写在外面,而且必须传指针进去
    void InitStudent(struct Student* s, const char* n, int a) {
        strcpy(s->name, n);
        s->age = a;
    }
    
    int main() {
        struct Student s1; // 必须带 struct 关键字
        InitStudent(&s1, "张三", 18); // 数据和操作是分开的
    }
2. C++ 对 Struct 的"升级"

C++ 为了兼容 C 语言,保留了 struct,但赋予了它全新的能力。在 C++ 中,struct 不仅仅可以放变量,还可以放函数!

这是一个巨大的飞跃,标志着 "面向过程"向"面向对象"的转变

"C++ 对 struct 进行了史诗级增强。它不再只是 C 语言里那个只能装数据的包裹,它现在拥有了和 class 一样的能力------可以包含函数,并且函数可以直接访问内部数据,无需传参。唯一的区别只是默认访问权限不同。"

  • 变化 1 :定义变量时不再需要写 struct 关键字(直接 Student s1;)。

  • 变化 2:函数可以定义在结构体内部(成员函数)。

  • 代码示例

    cpp 复制代码
    // C++中的 Struct
    struct Student {
        // 1. 成员变量
        char name[20];
        int age;
    
        // 2. 成员函数(直接写在里面!)
        void Init(const char* n, int a) {
            strcpy(name, n);
            age = a;
        }
        
        void Print() {
            cout << name << " " << age << endl;
        }
    };
    
    int main() {
        Student s1; // 不需要写 struct Student s1
        s1.Init("李四", 20); // 这种写法叫"调用对象的方法"
        s1.Print();
    }
3. 既然 Struct 这么强了,为什么还需要 Class?

这时候就可以抛出本章的核心概念:封装与安全

虽然 C++ 的 struct 可以包含函数,但在 C++ 的设计哲学中,struct 主要还是为了兼容 C 语言的数据结构,它有一个默认特性

  • Struct 的默认访问权限是 public(公有的)。
    这意味着谁都可以随意修改里面的数据,很不安全。

为了强调 "封装" (Encapsulation) ------ 即"把数据藏起来,把接口露出来",C++ 引入了 class 关键字。

  • Class 的默认访问权限是 private(私有的)。
    这强迫程序员显式地划分权限,这才是面向对象编程(OOP)的正统思想。
4. 对比总结(本章的核心知识点)
特性 C 语言 struct C++ struct C++ class
能否包含函数 ❌ 不能 ✅ 能 ✅ 能
定义变量 struct Tag v; Tag v; Tag v;
默认访问权限 (无此概念) Public (公有) Private (私有)
主要用途 纯数据聚合 数据结构兼容/轻量级对象 完整的面向对象封装

=

第三章:类的定义与核心机制

在上一章中,我们通过 struct 窥探了"将数据和函数打包"的雏形。到了这一章,我们要正式进入 C++ 面向对象的核心领地------类 (Class)

在动手写代码之前,我们需要先统一几个关键的 "行话" (术语)和常识,这将决定你对 C++ 的理解深度。

3.1 预备知识:类的三大常识

很多初学者会混淆"写了一个类"和"用了一个类",我们需要从三个维度来厘清:

1. "蓝图"与"实体"的关系(实例化)

这是面向对象最底层的逻辑:

  • 类 (Class) :它是图纸 (蓝图)。
    • 比如:"手机设计图"。
    • 它规定了手机有屏幕、电池(属性),能打电话、拍照(行为)。
    • 重要常识类本身不占内存空间 (因为它只是一个类型定义,就像 int 这个词不占内存一样)。
  • 对象 (Object) :它是盖好的房子 (实体)。
    • 比如:"你手里拿的那台 iPhone 16"。
    • 它是根据图纸制造出来的实物。
    • 重要常识对象是实实在在占用内存的

术语:实例化 (Instantiation)

由"类"创建"对象"的过程,叫做实例化。
Class -> Instantiate -> Object

2. 成员的划分:属性与行为

在 C++ 类中,万物皆成员。但为了逻辑清晰,我们把它们分为两类:

  • 成员变量 :描述属性
    • 对应 struct 中的数据。例如:身高、体重、学号。
  • 成员函数 :描述行为
    • 对应 struct 中的函数。例如:吃饭、睡觉、打印成绩。

成员变量:在类中是布局的描述。只有实例化对象(造房子)时,才会真正分配内存。每个对象各有一份。

成员函数:在类中是行为的定义。编译后放在公共代码区。无论你创建 1 个还是 100 个对象,函数代码只有一份,不占对象的空间。

3. 命名规范(约定俗成)

虽然 C++ 语法不强求,但专业的 C++ 程序员通常遵守以下规范,以便一眼看出"这是个类"还是"这是个变量":

  • 类名 :通常使用大驼峰命名法 (PascalCase) ,首字母大写。
    • 例如:Student, Car, TcpSocket
  • 成员变量 :为了防止和函数参数混淆,通常加前缀 m_ 或前缀 _
    • 例如:m_Age (m 代表 member) 或 _age

3.2 类的标准定义语法

有了上面的常识铺垫,我们现在来看 C++ 中定义类的标准"骨架"。

cpp 复制代码
class 类名 {
public:
    // 【公共区域】
    // 通常放置:成员函数(对外的接口)
    // 允许类外访问

private:
    // 【私有区域】
    // 通常放置:成员变量(核心数据)
    // 只允许类内访问,类外看不见

protected:
    // 【保护区域】
    // (留待继承章节讲解,目前暂时将其视为 private)

}; // <--- ⚠️ 这里的封号千万不能漏!

与 Struct 的核心区别:

我们在第二章讲过,Struct 默认是 public 的,而 Class 默认是 private 的 。如果不写 public:,那么类里的东西外界全都不许碰。

3.3 成员函数的两种实现方式

定义类时,关于"行为"(函数)怎么写,C++ 提供了极大的灵活性,这也是新手容易晕的地方。

方式一:类内实现(声明与实现合一)

直接在 class 的大括号里把代码写完。

  • 适用场景:代码极少、逻辑简单的函数(通常是 Get/Set 方法)。
  • 底层细节 :编译器通常会将其视为 inline(内联)函数处理。
cpp 复制代码
class Student {
public:
    void sleep() {
        cout << "学生在睡觉..." << endl;
    }
};
方式二:类外实现(声明与实现分离) ------ 更推荐

在类里面只写函数名字(声明),把具体的代码写在外面。

  • 适用场景 :逻辑复杂、代码较长的函数。这是大型项目开发的标准规范(通常分 .h.cpp 文件)。
  • 关键语法作用域解析运算符 ::
cpp 复制代码
class Student {
public:
    void study(); // 只写声明,加分号
};

// 在外面写实现
// 含义:我定义的这个 study 不是普通的全局函数,而是属于 Student 类的!
void Student::study() {
    cout << "学生在努力学习 C++" << endl;
}

3.4 完整代码演示

让我们用一个标准的 Circle(圆)类,把本章所有概念串起来:

cpp 复制代码
#include <iostream>
using namespace std;

const double PI = 3.14;

// 1. 定义类 (蓝图)
class Circle {
public:
    // --- 行为 (Public) ---
    
    // 给半径赋值
    void setR(double r) {
        if (r < 0) {
            cout << "错误:半径不能为负数" << endl;
            return;
        }
        m_R = r; // 成员函数可以直接访问私有变量
    }

    // 获取半径
    double getR() {
        return m_R;
    }

    // 计算周长 (类内只声明)
    double calculatePerimeter();

private:
    // --- 属性 (Private) ---
    double m_R; // 成员变量习惯加 m_ 前缀
};

// 2. 类外实现复杂函数
// 必须加上 Circle:: 告诉编译器这是谁的成员函数
double Circle::calculatePerimeter() {
    return 2 * PI * m_R;
}

int main() {
    // 3. 实例化对象 (盖房子)
    // 此时才真正分配内存
    Circle c1; 

    // c1.m_R = 10; // ❌ 报错!私有成员不可访问

    // ✅ 通过公共接口操作对象
    c1.setR(10); 
    
    cout << "圆的半径: " << c1.getR() << endl;
    cout << "圆的周长: " << c1.calculatePerimeter() << endl;

    return 0;
}

3.5 成员函数的参数

是否需要参数,完全取决于这个函数"要完成的任务"所需的数据来源。

我们可以把成员函数看作是这个对象的"动作"。判断需不需要传参,只需要问自己一个问题:
"完成这个动作,我自己肚子里的数据(成员变量)够不够用?"

  • 够用 → \rightarrow → 不需要参数。
  • 不够用 (需要外界告诉我) → \rightarrow → 需要参数。

下面我们分三种情况详细说明:

1. 不需要传参:自给自足型

如果这个函数只需要读取操作 类内部已经存在的变量,那就不需要别人给它传参。它直接用 this 指针(隐式)就能找到自己的数据。

  • 场景:获取当前状态、重置状态、基于内部数据计算。
  • 例子
    • getName():名字已经在对象里存着了,直接拿就行。
    • clear():把内部计数器清零,不需要外界告诉怎么清。
    • print():打印自己的信息。
cpp 复制代码
class Hero {
    int hp = 100; // 内部数据:血量

public:
    // 不需要参数
    // 因为通过成员变量 hp,我已经知道自己有多少血了
    int getHp() {
        return hp; 
    }

    // 不需要参数
    // 因为"回满血"这个动作隐含的意思就是变成 100,不需要外界告诉
    void recoverFull() {
        hp = 100;
    }
};
2. 需要传参:外部输入型

如果这个函数需要的数据,不在 类内部,或者需要外界指定一个新值,那就必须传参。

  • 场景:设置新值、与外部数据交互、计算依赖外部变量。
  • 例子
    • setName(string newName):你得告诉它新名字叫什么。
    • attack(Monster m):你得告诉它打哪一只怪。
    • add(int x):你得告诉它加上多少。
cpp 复制代码
class Hero {
    int hp = 100;

public:
    // 【需要参数】
    // 因为"受伤"这个动作,必须知道受了"多少"伤
    void takeDamage(int damage) {
        hp -= damage;
    }
    
    // 【需要参数】
    // 因为要改名,必须知道"新名字"是什么
    void rename(string newName) {
        // ... 修改名字的代码
    }
};
3. 最核心的区别:C 语言 vs C++

你可能会觉得:"在 C 语言里,函数不是都要传结构体指针进去吗?为什么 C++ 不需要?"

这是 C++ 也就是 面向对象 的魔法所在:隐式传参

  • C 语言的做法(所有数据都要显式传):

    c 复制代码
    // 吃苹果,必须把"人"传进去,否则不知道谁在吃
    void eat_apple(Person* p, int count) {
        p->stomach += count;
    }
  • C++ 的做法(只有外部数据要传):

    cpp 复制代码
    class Person {
        int stomach;
    public:
        // "人"不需要传,因为函数就在"人"里面
        // 只有"吃了几个"需要外界告诉
        void eatApple(int count) {
            stomach += count; // 自动识别成 this->stomach
        }
    };
总结图表

当你写一个成员函数时,按这个逻辑判断:

函数目的 数据来源 是否传参 例子
只是为了输出/查看 数据全在类内部 (this->xxx) ❌ 不需要 print(), getAge()
固定逻辑的操作 逻辑固定,不依赖外部数值 ❌ 不需要 reset(), turnOn()
修改数据 需要外界提供新数值 ✅ 需要 setPrice(int p), resize(int w, int h)
复杂交互 需要用到另一个对象的数据 ✅ 需要 compare(OtherObj b)

一句话口诀:
用自己的不用传,用别人的就要传。

3.6 本章小结

到这里,我们已经完成了从"思想"到"语法"的落地:

  1. 概念:类是图纸(不占地),对象是房子(占地)。
  2. 封装 :用 private 保护数据,用 public 开放接口。
  3. 语法 :类外写函数要加 类名::

第四章:OOP 的灵魂------封装与访问限定符

在学会了怎么定义类之后,新手最容易犯的错误就是:把所有东西都设为 public

这样做虽然代码能跑,但完全违背了面向对象编程(OOP)的初衷。这一章我们将深入探讨为什么要封装 ,以及如何使用访问限定符来保护你的对象。

4.1 什么是封装 (Encapsulation)?

封装 不仅仅是"把变量和函数扔进一个花括号里"。

它的核心含义是:隐藏细节,暴露接口。

  • 隐藏 (Hiding):把核心数据和复杂的内部逻辑藏起来,不让外界随意触碰。
  • 暴露 (Exposing):只提供一套安全、简单的操作入口(函数)给外界使用。

生活中的例子:

原子弹就是封装的极致。

  • Private (私有):内部的核反应原料、复杂的引爆电路。你绝对不希望普通人能随便打开盖子去摸里面的线路(不安全)。
  • Public (公有):只有一个红色的按钮。
  • 封装的结果:使用者不需要懂核物理,只需要按按钮(调用接口),就能实现功能。

4.2 C++ 的三大访问限定符

C++ 提供了三个关键字来划分"地盘"。这些限定符的作用域从冒号 : 开始,直到下一个限定符出现 或者类的大括号结束

1. public (公共权限) ------ "客厅"

  • 谁能访问
    • 类自己(成员函数)。
    • 子类(派生类)。
    • 外部(main 函数、其他类)。
  • 通常放什么
    • 对外提供的接口函数(如 setAge(), printInfo())。
    • 构造函数、析构函数。

2. private (私有权限) ------ "卧室/保险箱"

  • 谁能访问
    • 只有类自己(成员函数)。
    • ❌ 子类不可访问。
    • ❌ 外部不可访问。
  • 通常放什么
    • 成员变量(属性)。
    • 内部辅助函数(只给类自己用的逻辑,不想对外暴露)。

3. protected (保护权限) ------ "传家宝"

  • 谁能访问
    • 类自己
    • 子类(这是它和 private 的唯一区别)。
    • ❌ 外部不可访问。
  • 通常放什么
    • 希望留给子类继承使用,但不想公开给外界的数据。
    • (注:在没有继承的情况下,它的表现和 private 完全一样)

4.3 为什么要这么麻烦?(封装的好处)

为什么不直接把所有变量都 public,想怎么改就怎么改?

我们来看一个反面教材正面教材的对比。

❌ 反面教材:没有任何封装

cpp 复制代码
class Person {
public: // 全部公开
    string name;
    int age;
};

int main() {
    Person p;
    // 危险操作 1:输入非法数据
    p.age = -1000; // 人的年龄怎么可能是负数?逻辑崩坏!
    
    // 危险操作 2:数据被意外篡改
    p.name = "不知名"; // 谁都可以随时改名,无法追踪
    
    return 0;
}

✅ 正面教材:标准的封装

cpp 复制代码
class Person {
private: // 数据私有化
    string m_Name;
    int m_Age;

public: // 接口公开化
    // 1. 写权限 (Setter):可以加逻辑控制!
    void setAge(int age) {
        if (age < 0 || age > 150) {
            cout << "错误:年龄不合法!" << endl;
            // 可以选择报错、抛出异常或设为默认值
            m_Age = 0; 
            return;
        }
        m_Age = age; // 只有合法时才赋值
    }

    // 2. 读权限 (Getter)
    int getAge() {
        return m_Age;
    }
    
    // 3. 只读属性 (只给 Getter 不给 Setter)
    // 比如:名字一旦设定就不许改
    void setName(string name) {
        m_Name = name;
    }
    string getName() {
        return m_Name;
    }
};

int main() {
    Person p;
    // p.m_Age = -1000; // ❌ 编译报错!无法访问私有成员
    
    p.setAge(-1000); // ✅ 运行被拦截,输出"错误:年龄不合法"
    p.setAge(18);    // ✅ 正常赋值
    
    cout << p.getAge() << endl;
    return 0;
}

总结封装的意义:

  1. 安全性 :防止外部赋予不合法的值(如 age = -100)。
  2. 可维护性 :如果未来内部逻辑变了(比如 age 不存 int 了,改存 string),只要 setAge 的接口不变,外部调用的代码就不需要修改。这叫实现细节对用户透明
  3. 控制权:你可以轻松实现"只读"属性(只写 get 不写 set)或"只写"属性。

4.4 两个重要的补充知识点

1. class vs struct 的默认权限

这是面试必考题,再次强调:

  • class :如果不写 public/private,默认是 private
  • struct :如果不写 public/private,默认是 public
cpp 复制代码
class A {
    int x; // 默认 private
};

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

2. 成员变量命名规范

为了在代码中一眼区分"参数"和"成员变量",C++ 社区有几种常见的命名习惯,建议选一种坚持使用:

  • 前缀 m_ (Google 风格/Qt 风格):m_Age
  • 后缀 或者前缀_ (Google 风格/LLVM 风格):age_/_age
  • 小驼峰age (容易和参数 int age 混淆,不推荐新手使用)

4.5 C++的struct和class区别

1. 核心区别:唯一的不同是"门没锁"

在 C++ 面试中,如果你能答出下面这点,基本就及格了:

structclass 在 C++ 中唯一的实质区别是:默认的访问权限 (Default Access Specifier) 不同。

  • class :默认是 Private (私有)。如果你不写 public:,谁也别想碰里面的数据。
  • struct :默认是 Public (公有)。如果你不写 private:,谁都可以随意访问。
代码对比
cpp 复制代码
class A {
    int x; // 默认为 private
};

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

int main() {
    A a;
    // a.x = 10; // ❌ 报错!不可访问

    B b;
    b.x = 10; // ✅ 成功!可以直接访问
    return 0;
}

补充知识(进阶) :在继承时,默认权限也不同。

  • class Derived : Base 默认是 private 继承。
  • struct Derived : Base 默认是 public 继承。

2. 能力演示:Struct 也能"起舞"

很多从 C 语言转过来的同学以为 struct 只能放变量。错!

在 C++ 中,struct 拥有 class所有功能。它可以有:

  • 构造函数
  • 成员函数
  • 继承
  • 多态(虚函数)

看下面这个代码,完全合法的 C++ struct

cpp 复制代码
#include <iostream>
using namespace std;

struct Point {
    // 成员变量
    int x;
    int y;

    // 1. 它可以有构造函数!
    Point(int a, int b) {
        x = a;
        y = b;
    }

    // 2. 它可以有成员函数!
    void move(int dx, int dy) {
        x += dx;
        y += dy;
    }
    
    // 3. 它甚至可以有 private 区域!
private:
    int id; // 这个变量外面看不见
};

int main() {
    Point p(1, 2); // 调用构造函数
    p.move(10, 10); // 调用成员函数
    cout << p.x << ", " << p.y << endl; // 输出 11, 12
    return 0;
}

3. C 语言 struct vs C++ struct

为了避免混淆,我们需要理清历史包袱:

特性 C 语言的 struct C++ 的 struct
内部内容 只能包含数据变量,不能有函数。 可以包含数据函数。
定义变量 必须写 struct Point p; (除非用了 typedef)。 直接写 Point p; 即可。
内存模型 纯粹的数据块(POD)。 可能是复杂的对象(含虚函数表等)。
初始化 只能用 {} 初始化。 可以用构造函数初始化。

4. 行业潜规则:什么时候用 struct,什么时候用 class?

既然两者功能几乎一样,为什么我们还要保留两个关键字?为什么不把 struct 删了?

这是为了代码的可读性语义表达。在 C++ 工程师之间,有一套约定俗成的"潜规则":

场景 A:使用 struct ------ "数据包"

当我们定义一个主要用来存数据 ,不需要复杂的封装,也不需要太多逻辑控制的类型时,用 struct。这通常被称为 POD (Plain Old Data) 类型。

  • 例子:坐标点、颜色值、配置参数包、网络通信的数据包头。
  • 心态"你看吧,数据都在这,随便拿,没什么秘密。"
cpp 复制代码
struct Color {
    unsigned char r, g, b; // 简单的数据集合
};
场景 B:使用 class ------ "管理者/对象"

当我们定义一个需要维护内部状态 ,有复杂的行为逻辑 ,且数据不允许被随意修改 时,用 class

  • 例子:数据库连接池、学生管理系统、窗口控制器、游戏角色。
  • 心态"我是个复杂的对象,请通过我的函数接口来办事,不要直接动我的数据。"
cpp 复制代码
class BankAccount {
private:
    double balance; // 余额绝对不能随便改!
public:
    void deposit(double amount); // 必须通过存款函数来改
};

第五章:类的作用域

我们之前学过全局作用域、局部作用域(函数内)。类定义了一个新的作用域,就像一个围墙,把成员围了起来。

5.1 什么是类作用域?

在类 (class) 的大括号 {} 也就是类作用域

在这个范围内:

  1. 类的成员变量和成员函数可以直接互相访问,不需要加任何前缀。
  2. 但在类外面,这些名字是"看不见"的。

5.2 作用域解析运算符 ::

如果你想在类外面访问里面的东西(或者在类外定义成员函数),你需要告诉编译器:"我要找的是属于 Student 家里的那个 study 函数"。

这就是 :: 的作用。

cpp 复制代码
class Person {
public:
    void func(); // 声明
};

// ❌ 错误:编译器以为这是个全局函数
void func() { 
    ... 
} 

// ✅ 正确:使用 :: 指明作用域
void Person::func() { 
    ... 
}

补充 :如果在类成员函数中,参数名和成员变量名冲突了,也可以用 类名::变量名 来强制指定(不过更推荐下一章讲的 this 指针)。

第六章:类的实例化

这一章我们从内存的角度重新审视"创建对象"。

6.1 再次强调:声明 vs 实例化

  • 声明class Person { ... };
    • 这只是告诉编译器 Person 长什么样。
    • 不分配内存。就像画了一张图纸,还没有买砖头。
  • 实例化 (Instantiation)Person p1;
    • 这是根据图纸造房子。
    • 分配内存 。系统会在栈(Stack)或堆(Heap)上划出一块空间给 p1

6.2 实例化的本质

当你写下 Person p1; 时,操作系统做了两件事:

  1. 分配空间 :根据类中成员变量的大小,分配内存块。
  2. 初始化:调用构造函数填充这块内存。

第七章:类的对象大小的计算

这是一个经典的面试题,也是理解 C++ 内存模型的关键。

核心问题 :一个对象 Person p 到底占多少字节?
答案公式对象大小 ≈ 所有非静态成员变量大小之和 (+ 内存对齐 padding)

7.1 惊人的事实:成员函数不占对象空间

初学者常以为:对象里面装着变量和函数代码。
错!

  • 成员变量每个对象独有一份 。你叫张三,我叫李四,我们的 m_Name 数据不同,必须各自存一份。
  • 成员函数所有对象共享一份eat() 吃饭的动作(代码逻辑)是一样的,没必要每个对象都拷贝一份代码。函数代码存放在公共的代码区(Code Segment)。

7.2 案例验证

cpp 复制代码
class A {
    // 空类
};

class B {
    int m_a; // 4字节
};

class C {
    int m_a; // 4字节
    void func() {} // 函数不占空间
};

int main() {
    cout << "sizeof(A) = " << sizeof(A) << endl; // 结果:1
    cout << "sizeof(B) = " << sizeof(B) << endl; // 结果:4
    cout << "sizeof(C) = " << sizeof(C) << endl; // 结果:4 (不是8!)
}

7.3 两个特殊规则

  1. 空类的大小是 1 字节
    • 为什么 sizeof(A) 不是 0?
    • 如果是 0,那么 A obj1; A obj2; 这两个对象就没有区别了(地址可能重叠)。为了保证每个对象在内存中都有独一无二的地址,编译器会给空对象分配 1 个字节的"占位符"。
  2. 内存对齐 (Memory Alignment)
    • 类的内存布局遵循结构体的对齐规则(通常以最大成员的大小为单位对齐),这是为了 CPU 读取效率。
    • 例如:char (1) + int (4) 可能会变成 8 字节(中间补 3 字节 padding)。

1.sizeof(类名):你是在问编译器,"如果我要根据这张图纸造房子,需要多大的地皮?"编译器根据类定义里的成员变量算了一下,回答你:"16平米"。

2.sizeof(对象):你是在问编译器,"这个已经造好的房子占了多大地皮?"编译器看了一眼这个对象的类型(是 Student),回答你:"哦,这是按 Student 图纸造的,所以是 16平米"。

3.本质上:sizeof 是一个编译时运算符(Compile-time Operator)。 也就是说,在程序还没运行的时候,编译器就已经把 sizeof(...) 替换成具体的数字了。它看的是类型,而不是里面的值。

第八章:类成员函数的 this 指针

接上文,既然成员函数只有一份,存在公共代码区,那么问题来了:

p1 调用 eat()p2 调用 eat() 时,函数怎么知道是 p1 在吃,还是 p2 在吃? 怎么保证修改的是 p1.stomach 而不是 p2.stomach

答案就是:隐藏的 this 指针

8.1 编译器背后的"手脚"

当我们编写如下代码时:

cpp 复制代码
// 程序员写的
p1.setAge(18);

编译器在编译阶段,会将其翻译成类似 C 语言的全局函数调用,并多传一个参数:

cpp 复制代码
// 编译器实际执行的(伪代码)
Student_setAge(&p1, 18); // 把 p1 的地址传进去了!

而在类的内部,函数定义也被"篡改"了:

cpp 复制代码
// 程序员写的
void setAge(int age) {
    m_age = age;
}

// 编译器看到的
void setAge(Student * const this, int age) {
    this->m_age = age; // 这里的 this 就是刚才传进来的 &p1
}

结论this 指针是 C++ 编译器给每个非静态成员函数增加的一个隐藏的指针参数 。它指向当前调用该函数的对象

(this 就是调用该函数的"那个对象"的内存地址。)

注意:

1.实参和形参不能显示写this指针,由编译器自己加

2.在类里面可以显示用,this->age ✅ 代表"当前对象"的地址

8.2 this 指针的用途

虽然 this 是隐式的,但我们在写代码时也可以显式使用它,主要有两个场景:

场景 1:解决名称冲突

当参数名和成员变量名一样时:

cpp 复制代码
class Student {
public:
    int age;
    
    void setAge(int age) {
        // age = age; // ❌ 此时两个都是参数 age,赋值无效
        
        this->age = age; // ✅ 明确:左边是属性,右边是参数
    }
};
场景 2:支持链式编程 (Returning *this)

如果你想实现像 cout << a << b 或者 obj.add().sub().mul() 这样连续调用的效果,函数需要返回对象本身。

cpp 复制代码
class Calculator {
    int m_Num;
public:
    Calculator(int n) { m_Num = n; }

    // 返回引用,代表返回对象本身,而不是拷贝
    Calculator& add(int num) {
        m_Num += num;
        return *this; // <--- 返回当前对象的本体
    }

    void print() { cout << m_Num << endl; }
};

int main() {
    Calculator calc(10);
    // 链式调用:先加5,再加5,最后加5
    calc.add(5).add(5).add(5); 
    calc.print(); // 输出 25
}

8.3 容易忽略的细节

  1. this 指针的类型 :对于 Student 类,this 的类型是 Student * const。这意味着你不能修改 this 指针的指向(不能让它指向别的对象),但可以修改它指向的内容。
  2. this 只能在成员函数内部使用 :全局函数、静态成员函数(后面会讲)里没有 this

8.4 this指针存在哪里的

结论:this 指针主要存在"寄存器"或"函数栈帧(栈)"中。

它就像一个普通的函数参数 ,生命周期仅限于函数执行期间。它绝对不 存在于对象的内存空间里(即不占 sizeof 的空间)。

具体存在哪里,取决于操作系统、编译器和架构(x86 vs x64):

1. 最经典的情况:寄存器(ECX)

场景 :Windows 下 Visual Studio 编译器,x86(32位)程序。
约定__thiscall 调用约定。

这是 C++ 针对类成员函数特有的优化。编译器认为 this 指针太重要、用得太频繁了,所以不把它放在慢速的内存(栈)里,而是直接放在 CPU 的通用寄存器 ECX 中传递。

汇编视角:

asm 复制代码
; C++ 代码: p.move(10);
; 假设 p 的地址是 0x00400000

lea ecx, [p]    ; 1. 把对象 p 的地址加载到 ECX 寄存器
push 10         ; 2. 把参数 10 压入栈
call Student::move ; 3. 调用函数

move 函数内部,CPU 只要想找"当前对象",直接读 ECX 寄存器就行了,速度飞快。

2. 通用的情况:栈(Stack)

场景:GCC 编译器、某些复杂的调用约定,或者参数过多的情况。

在这种情况下,编译器会把 this 当作函数的第一个隐式参数 ,通过 push 指令压入栈中。

汇编视角:

asm 复制代码
; C++ 代码: p.move(10);

push 10         ; 1. 压入参数 10
lea eax, [p]    ; 2. 获取对象地址
push eax        ; 3. 【关键】把对象地址(this)压入栈,作为第一个参数
call Student::move 

在函数内部,程序通过读取栈上的数据(比如 [ebp+8])来获取 this 指针。

3. x64 现代架构(你现在电脑大概率是这个)

场景:64位程序(无论是 Windows 还是 Linux)。

64位系统通用寄存器很多,参数基本都是靠寄存器传的。

  • Windows (x64) : this 通常放在 RCX 寄存器。
  • Linux (x64) : this 通常放在 RDI 寄存器。
4. 这里的【特大误区】

很多初学者会误以为 this 指针存放在对象里,这是完全错误的。

请记住:

  • 对象(Object) :是房子 。里面装着 int age, double score 等家具。
  • this 指针 :是手里拿着的一张写着房子地址的纸条

这张纸条(this)是在你进入 房子(调用函数)的那一刻,由系统临时发给你的。当你离开房子(函数结束),纸条就被扔掉了。纸条绝对不贴在墙上,也不占房子的面积。

这也是为什么:

cpp 复制代码
class A { void func() {} };
sizeof(A) == 1; // 空类是1,完全没有 4字节/8字节 的 this 指针空间

8.5 this 指针的特性

this 指针的特性是 C++ 面向对象机制的灵魂。理解这些特性,能帮你避开很多底层的坑,也能让你写出更高级的代码(比如链式调用)。

我把它的特性总结为 5 个核心点,按重要程度排序:

1. 类型特性:它是"指针常量"

这是 this 指针最本质的属性。

  • 本质类型 :对于 Student 类,this 的类型是 Student * const
  • 含义
    • 指向不可变 :你不能 修改 this 指针本身的指向(不能让它指向别的对象)。
    • 内容可变 :但是你可以通过 this 修改它所指向的对象里的数据。
cpp 复制代码
void Student::func() {
    this->age = 18; // ✅ 可以:修改成员变量
    
    Student s2;
    // this = &s2;  // ❌ 报错:this 是 const 的,一旦绑定当前对象,就不能改嫁!
}
2. 作用域特性:只能在"非静态成员函数"中使用

这是最容易混淆的一点。

  • 能用 :普通的成员函数(如 setAge, print)。
  • 不能用
    1. 全局函数:显然没有对象。
    2. 静态成员函数 (static)这一点非常重要!

为什么静态函数没有 this

因为静态成员函数是属于 的,不属于某个对象 。调用它的时候(Student::func()),可能根本就没有实例化任何对象,所以系统无法传递 this 指针。

cpp 复制代码
class A {
public:
    int x;
    
    // 普通函数:有 this
    void func1() { 
        this->x = 100; // ✅
    }
    
    // 静态函数:无 this
    static void func2() {
        // this->x = 100; // ❌ 报错:静态函数里没有 this 指针
    }
};
3. 常函数特性:const 修饰后的变化

当你定义一个常成员函数时(在函数括号后面加 const),this 指针的权限会被降级。

  • 普通函数Student * const this (只读指针,可写数据)
  • 常函数const Student * const this双重只读:指针不可变,数据也不可变)
cpp 复制代码
class A {
    int x;
public:
    // 这是一个 const 函数
    void show() const {
        // this->x = 100; // ❌ 报错:const 函数里,this 指向的数据变成只读了
        cout << x << endl; // ✅ 只能读
    }
};
4. 空指针特性:this 可以是 NULL 吗?(高频面试题)

答案:可以,但是很危险。

在 C++ 中,如果一个指针是空指针 (nullptr),你依然可以用它调用成员函数!只要这个函数不访问任何成员变量,代码就能正常运行。

原理

因为成员函数存在公共代码区,调用函数本身不需要解引用。只有当你尝试访问 this->age 时,才会发生解引用,从而导致崩溃。

cpp 复制代码
class A {
public:
    void sayHello() {
        cout << "Hello" << endl; 
    }
    
    void showAge() {
        cout << m_Age << endl; // 等价于 this->m_Age
    }
    
    int m_Age;
};

int main() {
    A* p = nullptr; // 创建一个空指针
    
    p->sayHello(); // ✅ 正常运行!输出 Hello。
因为没有用到 this 指针里的内容,只是单纯跳转到代码区执行。
普通成员函数的地址是编译期确定的,存在代码区,不在对象里。
调用函数这个动作本身,只是跳转指令,不需要读取对象的内存。
只有当函数内部真正用到对象里的数据(成员变量)时,才会去"摸"那个空指针,这时候才会崩。
    
    p->showAge();  // ❌ 崩溃!(Segmentation Fault)
                   // 因为试图访问 this->m_Age,也就是 0x0->m_Age,读取非法内存。
}
5. 自杀特性:delete this

这是一个非常极端但合法的操作。在成员函数内部,你可以执行 delete this;

  • 后果:对象把自己销毁了,释放了内存。
  • 前提
    1. 该对象必须是 new 出来的(在堆上)。
    2. 执行完 delete this 后,绝对不能再访问任何成员变量或调用虚函数,函数必须立即返回,否则未定义行为。
  • 用途:通常用于引用计数系统(如 COM 组件)中,当引用计数归零时对象自动销毁。
cpp 复制代码
void Object::destroy() {
    delete this; // ✅ 自杀
}
总结表
特性 描述
类型 类名 * const (指针本身不可变)
存储位置 寄存器 (ECX) 或 栈 (Stack)
存在范围 仅限非静态成员函数内部
Const 修饰 const 函数中变为 const 类名 * const (数据也变只读)
空指针调用 指针为 nullptr 时也可调用函数,但访问数据会崩

理解了这几点,你对 C++ 类机制的掌控力就超过大部分初学者了。特别是空指针调用静态函数无 this,是实际开发和面试中遇到最多的坑。

相关推荐
csuzhucong39 分钟前
斜转魔方、斜转扭曲魔方
前端·c++·算法
郝学胜-神的一滴44 分钟前
Horse3D游戏引擎研发笔记(十):在QtOpenGL环境下,视图矩阵与投影矩阵(摄像机)带你正式进入三维世界
c++·3d·unity·游戏引擎·godot·图形渲染·unreal engine
-森屿安年-2 小时前
二叉平衡树的实现
开发语言·数据结构·c++
Q741_1472 小时前
C++ 高精度计算的讲解 模拟 力扣67.二进制求和 题解 每日一题
c++·算法·leetcode·高精度·模拟
水木姚姚2 小时前
C++ begin
开发语言·c++·算法
老王熬夜敲代码3 小时前
泛型编程的差异抽象思想
开发语言·c++·笔记
hetao17338373 小时前
2025-12-02~03 hetao1733837的刷题记录
c++·算法
“愿你如星辰如月”3 小时前
C++11核心特性全解析
开发语言·c++
广都--编程每日问3 小时前
c++右键菜单统一转化文件为utf8编码
c++·windows·python