02--C++ 类和对象上篇

类与对象(上)

【本节目标】

  1. 面向过程和面向对象初步认识
  2. 类的引入
  3. 类的定义
  4. 类的访问限定符及封装
  5. 类的作用域
  6. 类的实例化
  7. 类的对象大小的计算
  8. 类成员函数的 this 指针

1. 面向过程和面向对象初步认识

  • C 语言(面向过程):关注「过程」,拆解解决问题的步骤,通过函数调用逐步执行。

    示例:洗衣服的过程

    拿个盆子 → 放水 → 放衣服 → 放洗衣粉 → 手搓 → 换水 → 手搓 → 拧干 → 晾衣服

  • C++(基于面向对象):关注「对象」,拆分出核心对象,靠对象间交互完成任务。

    示例:洗衣服的对象交互

    • 核心对象:衣服、洗衣机、洗衣粉
    • 核心逻辑:无需关注洗衣机内部原理,仅通过对象交互完成洗衣任务

2. 类的引入

C 语言结构体仅能定义变量,C++ 结构体支持同时定义变量 + 函数,示例(用 struct 实现栈):

cpp 复制代码
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);
    s.Push(3);
    cout << s.Top() << endl;
    s.Destroy();
    return 0;
}

!note\] 提示 C++ 中更推荐用`class`关键字替代`struct`定义类(核心逻辑一致,仅默认权限不同)。

3. 类的定义

3.1 基本格式

cpp 复制代码
class className
{
    // 类体:成员函数(方法) + 成员变量(属性)
}; // 注意:类定义结束必须加分号
  • class:定义类的关键字
  • className:类名
  • 类体成员:
    • 成员变量:类的属性(描述对象特征)
    • 成员函数:类的方法(操作对象的行为)

3.2 类的两种定义方式

方式 1:声明 + 定义全放在类体中
  • 特点:成员函数在类内定义时,编译器可能视为内联函数
cpp 复制代码
class Person
{
public:
    // 显示基本信息(声明+定义)
    void showInfo()
    {
        cout << _name << "." << _sex << "." << _age << endl;
    }

public:
    char* _name; // 姓名
    char* _sex;  // 性别
    int _age;    // 年龄
};
方式 2:声明与定义分离(推荐)
  • 声明:放在.h头文件(仅声明函数,不写实现)
  • 定义:放在.cpp文件(函数名前加类名::作用域限定符)

头文件 person.h(声明):--仅声明

cpp 复制代码
class Person
{
public:
    // 显示基本信息(仅声明)
    void showInfo();

public:
    char* _name; // 姓名
    char* _sex;  // 性别
    int _age;    // 年龄
};

实现文件 person.cpp(定义)

cpp 复制代码
#include "person.h"
// 显示基本信息(实现)
void Person::showInfo()
{
    cout << _name << " " << _sex << " " << _age << endl;
}

3.3 成员变量命名规则建议

为避免与函数形参同名,建议给成员变量加前缀 / 后缀标识:

cpp 复制代码
// 推荐方式1:下划线前缀(最常用)
class Date
{
public:
    void Init(int year)
    {
        _year = year; // 清晰区分成员变量 vs 形参
    }
private:
    int _year;
};

// 推荐方式2:m前缀(部分公司规范)
class Date
{
public:
    void Init(int year)
    {
        mYear = year;
    }
private:
    int mYear;
};

!tip\] 说明 具体规则可按公司规范调整,核心是**区分成员变量与普通变量**。

4. 类的访问限定符及封装

4.1 访问限定符

C++ 通过 3 种限定符控制成员的外部访问权限,实现「封装」:

访问限定符 核心作用
public 类外可直接访问(对外接口)
protected 类外不可访问(继承场景用)
private 类外不可访问(隐藏实现)
关键说明
  1. 权限作用域:从当前限定符开始,到下一个限定符 / 类结束(})为止;
  2. 默认权限:
    • class:默认private(隐藏细节)
    • struct:默认public(兼容 C 语言);
  3. 本质:仅编译期有效,编译后内存中无权限差异。

4.2 封装(面向对象三大特性之一)

!definition\] 封装定义 将数据(成员变量)和操作数据的方法(成员函数)有机结合,隐藏对象的属性和实现细节,仅对外公开接口用于交互。 \[!idea\] 封装本质 一种「管理方式」:隐藏复杂细节,只暴露简单接口,降低使用复杂度。 示例:电脑封装 → 隐藏 CPU / 显卡原理,仅暴露开关机键、键盘等接口; C++ 类封装 → 用`private`隐藏成员变量,用`public`提供操作接口。

面试题汇总

!question\] 面试题 1:C++ 中 struct 和 class 的区别? 1. 兼容性:struct 兼容 C 语言结构体,class 不兼容; 2. 默认权限:struct 默认 public,class 默认 private; 3. 其他差异:继承、模板参数列表中表现不同(后续章节讲解)。 \[!question\] 面试题 2:面向对象的三大特性? 封装、继承、多态(本节重点讲解「封装」)。

5. 类的作用域

类定义了独立的作用域,所有成员均属于该类域;类外定义成员时,需用::指定作用域:

cpp 复制代码
class Person
{
public:
    void PrintPersonInfo(); // 声明在Person类域中
private:
    char _name[20];
    char _gender[3];
    int _age;
};

// 类外定义:必须加Person::
void Person::PrintPersonInfo()
{
    cout << _name << " " << _gender << " " << _age << endl;
}

6. 类的实例化

!definition\] 实例化定义 用「类类型」创建「对象」的过程,称为类的实例化。

核心要点

  1. 类是「模型 / 图纸」:仅描述对象结构,定义类时不分配内存

    → 类比:类 = 建筑设计图,对象 = 按图纸盖的房子。

  2. 对象是「具体实例」:实例化后占用物理内存,存储成员变量(成员函数存于公共代码段);

  3. 一个类可实例化多个对象:共享成员函数,独立存储成员变量。

错误示例(直接操作类的成员)
cpp 复制代码
int main()
{
    Person._age = 100; // 编译失败:类无内存,不能直接访问成员
    return 0;
}
正确示例(实例化对象后操作)
cpp 复制代码
void Test()
{
    Person man; // 实例化对象man(分配内存)
    man._name = "jack";
    man._sex = "男";
    man._age = 10;
    man.showInfo(); // 调用成员函数
}

7. 类对象模型(对象大小计算)

7.1 核心结论

对象中仅存储成员变量(成员函数存于公共代码段),因此:

对象大小 = 成员变量之和(需遵循内存对齐规则)

7.2 验证:不同类的大小计算

cpp 复制代码
// 类1:有成员变量+成员函数
class A1 {
public:
    void f1(){}
private:
    int _a;
};

// 类2:仅成员函数
class A2 {
public:
    void f2() {}
};

// 类3:空类(无任何成员)
class A3 {};

// 计算结果(32/64位环境一致)
sizeof(A1) = 4;  // 仅int _a的大小
sizeof(A2) = 1;  // 空类占位字节(编译器分配)
sizeof(A3) = 1;  // 空类占位字节(唯一标识对象)

7.3 结构体内存对齐规则(计算基础)

  1. 第一个成员:偏移量为 0 的地址处;

  2. 其他成员:对齐到「对齐数」的整数倍地址;

    → 对齐数 = min (编译器默认对齐数,成员大小)(VS 默认 8,Linux 默认 4);

  3. 总大小:最大对齐数的整数倍;

  4. 嵌套结构体:嵌套部分对齐到自身最大对齐数的整数倍,整体大小为所有最大对齐数的整数倍。

面试题汇总

!question\] 面试题 1:结构体怎么对齐?为什么要内存对齐? * 对齐方式:按上述 4 条规则; * 对齐原因:提高 CPU 访问效率(CPU 按固定字节数读取内存,不对齐会触发多次读取)。 \[!question\] 面试题 2:如何指定结构体对齐参数?能否任意字节对齐? * 指定对齐:用`#pragma pack(n)`(n 为对齐参数); * 限制:n 必须是 2 的幂次(1/2/4/8),不能是 3/5 等任意值。 \[!question\] 面试题 3:什么是大小端?如何测试? * 大小端定义: * 大端:高位字节存低地址,低位字节存高地址; * 小端:低位字节存低地址,高位字节存高地址(x86 架构默认小端); * 测试方法: cpp 运行 ```cpp // 方法1:union(共用体) union Test { int a; char b; } t; t.a = 1; if (t.b == 1) cout << "小端" << endl; else cout << "大端" << endl; // 方法2:指针强制转换 int a = 1; char* p = (char*)&a; if (*p == 1) cout << "小端" << endl; else cout << "大端" << endl; ```

8. 类成员函数的 this 指针

8.1 this 指针的引出

问题:同一个类的多个对象共享成员函数,函数如何区分操作哪个对象的成员?

cpp 复制代码
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); // 函数如何知道操作d1?
    d2.Init(2022, 1, 12); // 函数如何知道操作d2?
    d1.Print();
    d2.Print();
    return 0;
}

!answer\] 解答 C++ 编译器为每个**非静态成员函数** 添加隐藏的`this`指针参数,该指针指向当前调用函数的对象;函数体内所有成员变量的操作,均通过`this`指针完成(用户无需手动传递,编译器自动处理)。

8.2 this 指针的特性

  1. 类型:类类型* const(不能给 this 赋值,指针指向不可改);
  2. 作用域:仅在成员函数内部使用;
  3. 本质:成员函数的隐含形参,对象调用函数时,自动传递对象地址给 this;
  4. 存储:不占用对象内存,通常由编译器通过 ecx 寄存器传递。
编译器的隐式处理(示例)
cpp 复制代码
// 用户写的代码
void Date::Print()
{
    cout << _year << "-" << _month << "-" << _day << endl;
}

// 编译器处理后的代码(隐含this指针)
void Date::Print(Date* const this)
{
    cout << this->_year << "-" << this->_month << "-" << this->_day << endl;
}

// 调用时的隐式转换
d1.Print(); → Date::Print(&d1); // 自动传递d1的地址给this

8.3 面试题汇总

!question\] 面试题 1:this 指针存在哪里? 答案:通常存储在寄存器(如 x86 的 ecx),而非对象 / 栈中(核心:不占用对象内存)。 \[!question\] 面试题 2:this 指针可以为空吗? 答案:可以,但需避免通过空 this 访问成员变量(否则触发空指针崩溃)。

示例 1(正常运行)
cpp 复制代码
class A
{
public:
    void Print()
    {
        cout << "Print()" << endl; // 未访问成员变量,this为空不影响
    }
private:
    int _a;
};

int main()
{
    A* p = nullptr;
    p->Print(); // 结果:正常输出Print()
    return 0;
}
示例 2(运行崩溃)
cpp 复制代码
class A
{
public:
    void PrintA()
    {
        cout << _a << endl; // 等价于this->_a,this为空触发崩溃
    }
private:
    int _a;
};

int main()
{
    A* p = nullptr;
    p->PrintA(); // 结果:运行崩溃(空指针访问)
    return 0;
}

!answer\] 解答 核心原因: 在 PrintA0) 函数内部,虽然没有显式写出 this-\>a,但是当前的 this 指针是nu∥ptr。当代码执行到 cout 的相关实现(或者是某些编译器对成员函数调用的检查机制)时,发生了对 this 指针的解引用或访问(即试图读取地址 0 处的内存)访问内存地址0是非法的,操作系统会立即拦截并抛出异常,导致程序运行崩溃(通常Access Violation或Segmentation Fault)

示例 3(正常运行)
cpp 复制代码
class A
{
public:
    void PrintA()
    {
	    cout << this << endl;
        cout << "PrintA()" << endl;
    }
private:
    int _a;
};

int main()
{
    A* p = nullptr;
    (*p).PrintA();
    return 0;
}

!answer\] 解答 1.场景一:没有 cout\<\< this 在 Visual Studio 的 Debug 模式下,偷插入一段代码来检查 this指针。 编译器非常"尽职尽责"。它知道通过空指针调用函数是危险的,所以它会在函数刚进入的时候,偷**偷偷插入** 一段代码来检查 `this` 指针。 编译器眼中的代码是这样的:

cpp 复制代码
void PrintA()
{
    // <--- 编译器自动插入的"安检代码"
    #ifdef _DEBUG
        if (this == nullptr) {
            // 发现是空指针!立即报错,中断程序
            _CrtIsValidHeapPointer(this); // 导致崩溃
        }
    #endif
    cout << "PrintA()" << endl;
}

!answer\] 续答 **结果:** 程序刚进入 `PrintA` 函数,第一行就执行了检查代码。`this` 是 0,检查不通过,**触发异常,程序崩溃** 。它甚至没机会去执行后面的 `cout`。 2.场景二:加上了 cout\<\

cpp 复制代码
void PrintA()
{
    // <--- Release 模式下,检查代码被删掉了,干干净净
    
    // 下面是你真正的代码
    cout << this;        // 只是打印指针变量的值 0,不访问内存
    cout << "PrintA()";
}

!answer\] 结果: 没有检査代码,也没有访问非法内存。程序就像开着一辆没有刹车的车在空旷的高速上跑,虽然危险(指针是空的),但只要不撞墙 (不访问成员变量),它就能一直开下去。

9. C 语言 vs C++ 实现 Stack 对比

9.1 C 语言实现(数据与操作分离)

cpp 复制代码
typedef int DataType;
typedef struct Stack
{
    DataType* array;
    int capacity;
    int size;
}Stack;

// 初始化栈
void StackInit(Stack* ps)
{
    assert(ps); // 必须检测ps非空
    ps->array = (DataType*)malloc(sizeof(DataType) * 3);
    if (NULL == ps->array)
    {
        assert(0);
        return;
    }
    ps->capacity = 3;
    ps->size = 0;
}

// 入栈
void StackPush(Stack* ps, DataType data)
{
    assert(ps);
    CheckCapacity(ps); // 扩容函数(省略实现)
    ps->array[ps->size] = data;
    ps->size++;
}

// 其他操作:StackDestroy、StackPop、StackTop等(均需传递Stack*)

int main()
{
    Stack s;
    StackInit(&s);
    StackPush(&s, 1);
    StackPush(&s, 2);
    printf("%d\n", StackTop(&s));
    StackDestroy(&s);
    return 0;
}
C 语言实现的缺点
  1. 所有函数需传递Stack*参数,且必须检测非空;
  2. 数据(结构体)与操作(函数)分离,逻辑分散;
  3. 指针操作复杂,易出错。

9.2 C++ 实现(封装:数据与操作结合)

cpp 复制代码
typedef int DataType;
class Stack
{
public:
    void Init()
    {
        _array = (DataType*)malloc(sizeof(DataType) * 3);
        if (NULL == _array)
        {
            perror("malloc申请空间失败!!!");
            return;
        }
        _capacity = 3;
        _size = 0;
    }

    void Push(DataType data)
    {
        CheckCapacity(); // 内部调用,无需传参
        _array[_size] = data;
        _size++;
    }

    void Pop()
    {
        if (Empty())
            return;
        _size--;
    }

    DataType Top() { return _array[_size - 1]; }
    int Empty() { return 0 == _size; }
    int Size() { return _size; }

    void Destroy()
    {
        if (_array)
        {
            free(_array);
            _array = NULL;
            _capacity = 0;
            _size = 0;
        }
    }

private:
    // 扩容函数(私有:外部不可访问)
    void CheckCapacity()
    {
        if (_size == _capacity)
        {
            int newcapacity = _capacity * 2;
            DataType* temp = (DataType*)realloc(_array, newcapacity * sizeof(DataType));
            if (temp == NULL)
            {
                perror("realloc申请空间失败!!!");
                return;
            }
            _array = temp;
            _capacity = newcapacity;
        }
    }

private:
    DataType* _array;   // 数据私有:外部不可直接操作
    int _capacity;
    int _size;
};

int main()
{
    Stack s;
    s.Init();
    s.Push(1);
    s.Push(2);
    s.Push(3);
    s.Push(4);
    printf("%d\n", s.Top()); // 直接调用,无需传参
    printf("%d\n", s.Size());
    s.Pop();
    s.Pop();
    printf("%d\n", s.Top());
    printf("%d\n", s.Size());
    s.Destroy();
    return 0;
}
相关推荐
廋到被风吹走14 小时前
【Java】【JVM】垃圾回收深度解析:G1/ZGC/Shenandoah原理、日志分析与STW优化
java·开发语言·jvm
xrkhy14 小时前
Java全栈面试题及答案汇总(3)
java·开发语言·面试
橘颂TA14 小时前
【Linux】从 “抢资源” 到 “优雅控场”:Linux 互斥锁的原理与 C++ RAII 封装实战(Ⅰ)
linux·运维·服务器·c++·算法
菩提祖师_14 小时前
量子机器学习在时间序列预测中的应用
开发语言·javascript·爬虫·flutter
刘975314 小时前
【第22天】22c#今日小结
开发语言·c#
明天好,会的14 小时前
分形生成实验(三):Rust强类型驱动的后端分步实现与编译时契约
开发语言·人工智能·后端·rust
YanDDDeat14 小时前
【JVM】类初始化和加载
java·开发语言·jvm·后端
枫叶丹414 小时前
【Qt开发】Qt系统(三)->事件过滤器
java·c语言·开发语言·数据库·c++·qt
wjs202414 小时前
CSS Position(定位)
开发语言