C++类和对象进阶 —— 与数据结构的结合

微软技术分享 微软全球最有价值专家
全栈领域优质创作者
博客专家认证
2025-06-07 07:58:17

C++类和对象(上)

一、类的定义

1.1类定义格式

  • class 为定义类的关键字,以栈为例:Stack 为类的名字,{} 中为类的主体,注意类定义结束时后⾯分号不能省略。类体中内容称为类的成员:类中的变量称为类的属性或成员变量;类中的函数称为类的方法或者成员函数
  • 为了区分成员变量,⼀般习惯上成员变量会加⼀个特殊标识,如成员变量前⾯或者后⾯加 _ 或者 m 开头,注意 C++ 中这个并不是强制的,只是⼀些惯例,具体看公司的要求
  • 定义在类中的成员函数默认为 inline(内联)
#include<iostream>
using namespace std;
#include<cassert>

typedef int STDataType;
class Stack
{
public:
    //成员函数
    //1、对栈初始化
    void StackInit(int n = 4)
    {
        array = (STDataType*)malloc(sizeof(STDataType) * n);
        if (array == nullptr)
        {
            perror("malloc fail!");
            exit(1);
        }
        capacity = 4;
        top = 0;
    }

    //2、入栈操作
    void StackPush(STDataType x)
    {
        //增容操作
        if (top == capacity)
        {
            int newCapacity = capacity == 0 ? 4 : 2 * capacity;
            STDataRtpe* temp = (STDataType*)realloc(array, newCapacity * sizeof(STDataType));
        
            if (temp == nullptr)
            {
                perror("realloc fail!");
                exit(1);
            }
            array = temp;
            capacity = newCapacity;
        }

        array[top++] = x;
    }

    //3、检擦栈顶元素
    STDataRtpe StackTop()
    {
        assert(!StackEmpty());
        return array[top - 1];
    }

    //4、判空操作
    bool StackEmpty()
    {
        return top == 0;
    }

    //5、出栈操作
    void StackPop()
    {
        assert(!StackEmpty());
        /*
        断言:
            条件为ture,程序继续执行
            条件为false,程序停止运行
        */
        top--;
    }

    //6、销毁栈
    void StackDestroy()
    {
        if(array)
            free(array);

        array = nullptr;
        top = capacity = 0;
    }

private:

    //成员变量(多个变量的符合)
    STDataType* array;
    size_t capacity;
    size_t top;
};

int main()
{
    Stack st;
    st.StackInit();
    return 0;
}

数据结构 ** 中的栈数据写在 结构体中 ,栈操作实现方法在 结构体外,而在类中将数据(成员变量)与实现方法(成员函数)统一写在类内**


1.2兼容结构体

  • C++struct 也可以定义类,**C++** 兼容C中 struct 的⽤法,同时 struct 升级成了类,明显的变化是 struct 中可以定义函数,⼀般情况下我们还是推荐⽤ class 定义类
  • C++ 中,**struct** 的结构体成员默认持有公共权限 public,而 class 的成员对象默认持有私有权限 private

以链表为例:

/*
    C++ 将 struct 升级为了类
    1、结构体类中可以定义函数
    2、struct 名称就可以代表数据类型
*/

//C++兼容 C 中 struct 的用法
typedef int LTDataType;

typedef struct ListNodeC
{
    LTDataType val;
    struct ListNodeC* next;//不可以直接在结构体中 ListNodeC* next;

}LTNode;

//不需要 typedef 修饰,结构体名 ListNodeCPP 就是数据类型
struct ListNodeCPP
{
    //不仅可以定义变量,也可定义函数
    void LTInit(LTDataType x)
    {
        val = x;
        next = nullptr;
    }

private:

    LTDataType val;
    ListNodeCPP* next;
};

int main()
{
    LTNode* node1 = NULL;
    struct ListNodeC* node2 = NULL;

    ListNodeCPP* node = nullptr;
    node->LTInit(1);

    return 0;
}

本质上:

  • 结构体是数据的集合
  • 类是数据和方法的集合

二、实例化

2.1实例化概念

  • ⽤类类型在物理内存中创建对象的过程,称为类实例化出对象
  • 类是对象进行的⼀种抽象描述,是⼀个模型⼀样的东西,限定了类有哪些成员变量,这些成员变量只 是声明,没有分配空间,⽤类实例化出对象时,才会分配空间
  • ⼀个类可以实例化出多个对象,实例化出的对象 占⽤实际的物理空间,存储类成员变量。打个⽐ ⽅:类实例化出对象就像现实中使⽤建筑设计图建造出房⼦,类就像是设计图,设计图规划了有多 少个房间,房间⼤⼩功能等,但是并没有实体的建筑存在,也不能住⼈,⽤设计图修建出房⼦,房 ⼦才能住⼈。同样类就像设计图⼀样,不能存储数据,实例化出的对象分配物理内存存储数据

https://i-blog.csdnimg.cn/direct/50467aeb6a694bdfba9f31ebc0dd6b33.png


class Date
{
public:

    void Init(int year, int month, int day)
    {
        this->year = year;
        this->month = month;
        this->day = day;
    }

private:

    //成员变量是类中的声明(没有开辟任何空间)
    int year;
    int month;
    int day;
};
int main()
{
    //通过类所实例化出的对象为成员变量开辟空间
    Date d1, d2;

    return 0;
}

2.2对象大小

  • 实例对象中,只存储成员变量地址,不存储成员函数地址

​ 分析⼀下类对象中哪些成员呢?类实例化出的每个对象,都有独⽴的数据空间,所以对象中肯定包含 成员变量,那么成员函数是否包含呢?

​ ⾸先 函数被编译后是⼀段指令,在实例对象中没办法存储,这些指令 存储在⼀个单独的区域(代码段),那么实例对象中⾮要存储的话,只能通过 成员函数的指针

​ 再分析⼀下,实例对象中是否有存储指针的必要呢,Date 实例化 d1d2两 个对象,d1d2 都有各⾃独⽴的成员变量 year/_month/_day 存储各⾃的数据,但是 d1d2 的成员函数 Init/Print 指针却是⼀样的,存储在对象中就浪费了。

class Date
{
public:

    void Init(int year, int month, int day)
    {
        this->year = year;
        this->month = month;
        this->day = day;
    }

private:

    //成员变量是类中的声明(没有开辟任何空间)
    int year;
    int month;
    int day;
};
int main()
{
    //通过类所实例化出的对象为成员变量开辟空间
    Date d1, d2;

    cout << sizeof(Date) << endl;
    cout << sizeof(d1) << endl;
    return 0;
}


https://i-blog.csdnimg.cn/direct/7aa7829219a24e28bcad00fab2381563.png

汇编角度:函数调用,被定义结束后是一串指令,这串指令会存储到一个单独的区域(常量区,在操作系统的角度上:也称代码段)



https://i-blog.csdnimg.cn/direct/c012416bfe4d48ac924f5fe024645872.png


​ 成员变量需要独立的空间存放各种来自外界传入的值

C++
    //通过不同实例对象调用的 year 需要不同空间存储独立的值(本质:存储地址不同)
    d1.year++;
    d2.year++;

    //成员函数 Print,所有对象共享同一份函数代码,而非单独存储在某一个对象上
    d1.Print();
    d2.Print();

​ 如果⽤ Date 实例化100个对象,那么 成员函数指针 就重复存储100次,太浪费了。这⾥需要再额外哆嗦⼀下,其实 函数指针 是不需要存储的,函数指针是⼀个地址,调⽤函数被编译成汇编指 令[call 地址],其实编译器在编译链接时,就要找到函数的地址,不是在运⾏时找, 只有动态多态是在运⾏时找,就需要存储函数地址,这个我们以后会讲解

  • 函数在本文件中有定义 - 编译阶段确定函数地址
  • 函数定义在外文件 - 链接阶段确定函数

https://i-blog.csdnimg.cn/direct/e4a3a85c5fe64401aff34262473dea9c.png


上面我们分析了 实例对象只存储成员变量C++ 规定类实例化的对象也要符合 **内存对齐 **的规则


2.3内存对齐

设计思路:以空间换时间,每个对象从对齐数的位置开始存放

  • 第⼀个成员在与结构体偏移量为 0 的地址处
  • 其他成员变量要对⻬到某个数字(对⻬数)的整数倍的地址处
  • 注意:对⻬数=编译器默认的⼀个对⻬数与该成员⼤⼩的较⼩值
  • VS中默认的对⻬数为 8
  • 结构体总⼤⼩为:最⼤对⻬数(所有变量类型最⼤者与默认对⻬参数取最⼩)的整数倍
  • 如果嵌套了结构体的情况,嵌套的结构体对⻬到⾃⼰的最⼤对⻬数的整数倍处,结构体的整体⼤⼩ 就是所有最⼤对⻬数(含嵌套结构体的对⻬数)的整数倍

class A
{
public:
    void Print()
    {
        cout << _ch << endl;
    }
private:
    char _ch;
    int _i;
};
class B
{
public:
    void Print()
    {
        //...
    }
};
class C
{ };
int main()
{
    cout << sizeof(A) << endl;//8
    cout << sizeof(B) << endl;//1
    cout << sizeof(C) << endl;//1

    return 0;
}
  • 上⾯的程序运⾏后,我们看到没有成员变量的B和C类对象的⼤⼩是 1,为什么没有成员变量还要给1个 字节呢?因为如果⼀个字节都不给,怎么表⽰对象存在过呢!所以这⾥给1字节,纯粹是为了 占位标识 对象存在

面试经典问题:为什么需要内存对齐?

https://i-blog.csdnimg.cn/direct/1615c1318a074624ac7154f0c1b6523b.png


https://i-blog.csdnimg.cn/direct/8f8f2c653d114c3fbb663611391d9a46.png


https://i-blog.csdnimg.cn/direct/959c185ef2a54b95b6a5f4fb74aaec28.png


三、this指针

  • Date 类中有 InitPrint 两个成员函数,函数体中没有关于不同对象的区分,那当 d1 调⽤ InitPrint 函数时,该函数是如何知道应该访问的是 d1 对象还是 d2 对象呢?那么这⾥就要看到 C++ 给了 ⼀个隐含的 this 指针解决这⾥的问题
  • C++ 规定不能在实参和 形参 的位置显⽰的写 this指针 (编译时编译器会处理),但是可以在函数体内显 ⽰使⽤ this指针
class Date
{
public:

    void Init(int year, int month, int day)
    {
        this->year = year;
        this->month = month;
        this->day = day;
    }

    void Print()
    {
        cout << year << "/" << month << "/" << day << endl;
    }

private:

    int year;
    int month;
    int day;
};
int main()
{
    Date d1, d2;

    d1.Init(2025, 5, 1);
    d2.Init(2025, 5, 2);

    d1.Print();
    d2.Print();
    
    return 0;
}

思考:d1、d2 调用的是同一个 Print,执行的是同一个函数,参数相同,那为什么输出的值不同?

我们先前调用同一个函数,执行出的结果不同,是因为我们传入了不同的参数

编译阶段,编译器为我们处理隐藏过程。实际上,我们调用同一个函数 Print() 执行出了不同结果,还是因为我们传入了不同参数

https://i-blog.csdnimg.cn/direct/b62fabe9c9d94320bfeebfddb35fe3a2.png


同样地,

https://i-blog.csdnimg.cn/direct/4483de818c3a4a75a3a6f669caa2ced1.png

this 本身无法被修改:this = nullptr(false)


3.1常量指针与指针常量

  • 常量指针与指向常量的指针

https://i-blog.csdnimg.cn/direct/c3dd93caae64477886884af7f387022b.png


https://i-blog.csdnimg.cn/direct/cb6a84a7b4674131bcfa7bc853818572.png


https://i-blog.csdnimg.cn/direct/24263ed0b78841b3bfeb3be89d8af1dd.png


验证 this 指针指向 d1/d2对象地址:

https://i-blog.csdnimg.cn/direct/3b8c99f935954519962b709434408d65.png


3.2面试真题

https://i-blog.csdnimg.cn/direct/a766139e8fb34ddc92b925d1ab57fec9.png


答案:

正常运行

https://i-blog.csdnimg.cn/direct/b58b178ec847417390e99cdd746f34e4.png

Print() 编译时确定地址,不存在 对空指针进行解引用的行为


https://i-blog.csdnimg.cn/direct/ade9f5371b874d9399b4bb9d6d3d04cd.png


对象 p 的作用:

  • 搜查、核对成员函数的出处
  • 指针所指向的对象(或对象)传递 this指针

https://i-blog.csdnimg.cn/direct/bdb703d0e6244be380866b47aff5d0af.png


  • 编译错误:语法问题,因此对于空指针的解引用(运行错误)一定不会触及语法编译的问题
  • 链接错误:语法规则符合,但存在函数或变量只声明,未定义行为
  • 运行错误:行为/功能错误

https://i-blog.csdnimg.cn/direct/5ca0cfcdaba64eb5909c95d859eae57f.png


答案:

运行崩溃

https://i-blog.csdnimg.cn/direct/678c9f05ceb545a29cfd462e304da7e7.png

(*p).Print(); 同理


https://i-blog.csdnimg.cn/direct/f6f1d65e92ee470a8428140c0a8487a9.png


解决方法:使 指针p 指向一个有效对象

C++
    A aa;
    A* p = &aa;//或 A* p = new A();

https://i-blog.csdnimg.cn/direct/25cfce3e2754464db9e6bf53eb1ff4e1.png


​ 常量区(语言层面) - 代码段(或叫数据段 - 操作系统角度)(函数被编译成指令后,存储在常量区)

​ 代码段:将函数编译好的代码指令(因此代码指令存储在常量区/代码段)

this指针 所存放的区域中,this指针 存放在对象中这一选项或许会有些许疑虑,但很简单,如果其存放在对象中,那么 sizoef(指针) == 4;这就与我们前面提到的 sizeof(p)(或sizeof(Person))== 1; 背道而驰了,因此,可论证: this指针 并不存储在实例对象中


https://i-blog.csdnimg.cn/direct/2a4efd79e83c4dfabcbc5e4cc6cf8732.png


this指针 是形式参数,严格一点是存储在 栈和寄存器

3.3浅识寄存器 esp/ebp

https://i-blog.csdnimg.cn/direct/e4f83cef05dc4d208da9ca253880e994.png


四、C++与C语言实现Stack对比

⾯向对象三⼤特性:封装、继承、多态,下⾯的对⽐我们可以初步了解⼀下封装。 通过下⾯两份代码对⽐,我们发现 C++ 实现 Stack 形态上还是发⽣了挺多的变化,底层和逻辑上没啥变化

以栈为例,C++ 对栈的各项操作进行了严格封装(减少了 C语言 多样化,但易错误的代码形态),而 C语言 则更依赖于程序员应对形式多样化代码能力的素质

C
    STack st;
    //访问栈顶元素,方式一(直接调用函数 - 推荐):
    st.STop();
    
    //方式二:
    st.arr[st.top - 1];
    方式二:存在一定的不安全性,比如数组越界、0 - 1 == -1 等情况 

  • C++ 中数据和函数都放到了类⾥⾯,通过 访问限定符 进⾏了限制,不能再随意通过对象直接修改数 据,这是 C++封装 的⼀种体现,这个是最重要的变化。这⾥的 封装 的本质是⼀种更严格规范的管理,避免出现乱访问修改的问题 。当然 封装 不仅仅是这样的,我们后⾯还需要不断的去学习
  • C++ 中有⼀些相对⽅便的语法,⽐如 Init 给的缺省参数会⽅便很多,成员函数每次不需要传对象地址,因为 this指针 隐含的传递了,⽅便了很多,使⽤类型不再需要 typedef ⽤类名就很⽅便


文章来源: https://blog.csdn.net/2401_87692970/article/details/147749689
版权声明: 本文为博主原创文章,遵循CC 4.0 BY-SA 知识共享协议,转载请附上原文出处链接和本声明。


...全文
16 回复 打赏 收藏 转发到动态 举报
AI 作业
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复
本资源是压缩包形式的, 里面包含 本书,里面是pdf格式的, 带书签目录,本书是完整版的。 资源都是我自己用过的,不骗大家。 本书作者: 肖俊宇 吴为胜; 出版社: 电子工业出版社 内容简介: 《由浅入深学C++:基础、进阶与必做300题(含DVD光盘1张)》是C++语言的入门教程,较为系统地介绍了C++语言的基础内容。本书共分为3篇22章,详细介绍了C++语言的基础知识、面向对象、标准模块、底层开发和综合案例。本书循序渐进地讲述了C++的基础知识、C++程序的组成及其开发过程、C++程序中的数据、表达式和语句、控制程序流程、数组与字符串、指针与引用、使用函数、函数模板、错误和异常处理、宏和预编译、面向对象的开发、封装、继承、多态、类模板、文件流、标准模板库STL和编程实践等内容。 《由浅入深学C++:基础、进阶与必做300题(含DVD光盘1张)》涉及面广,从基本知识到高级内容和核心概念,再到综合案例,几乎涉及C++开发的所有重要知识。本书适合所有想全面学习C++开发技术的人员阅读,尤其适合没有编程基础的C++语言初学者作为入门教程,也可作为大、中院校师生和培训班的教材,对于C++语言开发爱好者,本书也有较大的参考价值。 章节目录: 第1篇 C++基础篇 第1章 C++概述 1 1.1 引言 1 1.1.1 C++的历史沿革 1 1.1.2 入门C++ 2 1.1.3 编程思想的转变 3 1.2 C++概述 4 1.2.1 C++的特征 5 1.2.2 C与C++的比较 5 1.2.3 C++的应用领域 6 1.3 C++源程序的组成 6 1.3.1 基本组成元素 7 1.3.2 标识符 8 1.3.3 保留字 8 1.3.4 符号 8 1.4 C++集成开发环境——DEV-C++ 9 1.4.1 选择C++编译器 9 1.4.2 安装DEV-C++ 10 1.4.3 DEV-C++ IDE简介 11 1.5 第一个C++程序——Hello World 11 1.5.1 创建源程序 11 1.5.2 编译运行 13 1.6 小结 14 1.7 习题 14 第2章 变量与数据类型 18 2.1 常量和变量 18 2.1.1 常量 18 2.1.2 变量 21 2.1.3 变量的定义及赋值 22 2.1.4 变量的应用示例 24 2.2 基本数据类型 25 2.2.1 基本数据类型概述 25 2.2.2 整型数据类型 26 2.2.3 浮点型数据类型 27 2.2.4 字符型数据类型 29 2.2.5 布尔型数据类型 30 2.3 变量的作用域 31 2.4 类型转换 32 2.4.1 隐式转换 32 2.4.2 显式转换 33 2.5 小结 34 2.6 习题 34 第3章 表达式与语句 39 3.1 运算符 39 3.1.1 运算符概述 39 3.1.2 算术运算符 40 3.1.3 自增和自减运算符 42 3.1.4 赋值运算符 43 3.1.5 关系运算符 44 3.1.6 逻辑运算符 45 3.1.7 条件运算符 46 3.1.8 逗号运算符 47 3.1.9 位运算符 48 3.1.10 sizeof运算符 49 3.2 运算符的优先级和结合性 50 3.3 表达式 51 3.4 语句 53 3.4.1 空格的作用 53 3.4.2 语句块 54 3.4.3 赋值语句 55 3.4.4 空语句 56 3.5 小结 57 3.6 习题 57 第4章 流程控制结构之顺序结构 63 4.1 程序流程图 63 4.2 表达式语句 64 4.3 格式化输入/输出 65 4.3.1 标准输入流cin 65 4.3.2 标准输出流cout 66 4.3.3 输出流cerr和clog 68 4.4 格式控制函数 69 4.5 格式控制符 71 4.5.1 控制不同进制的输出 72 4.5.2 控制输出宽度 72 4.5.3 控制输出精度 73 4.6 顺序结构综合应用 74 4.7 小结 75 4.8 习题 75
《Cocos2d-x实战:C++卷》[1] 系统论述了Cocos2d-x游戏开发理论与实践。全书内容涵盖了Cocos2d-x的核心类、瓦片地图、物理引擎、音乐音效、数据持久化、网络通信、数据交换格式、内存管理、性能优化、平台移植、程序代码管理、三大应用商店发布产品等。本书共29章,按内容结构可分为六篇: 第一篇开发基础,即第2章~第8章,内容包括Cocos2d-x简介、环境搭建、字符串、标签、菜单、精灵、场景、层、动作、特效、动画和Cocos2d-x用户事件。 第二篇开发进阶,即第9章~第12章,内容包括游戏音乐与音效、粒子系统、瓦片地图和物理引擎。 第三篇数据与网络,即第13章~第17章,内容包括Cocos2d-x中使用的数据容器类、数据持久化、数据交换格式、基于HTTP网络通信和基于Node.js的Socket.IO网络通信。 第四篇设计与优化,即第18章~第20章,内容包括Cocos2d-x中的常用设计模式、Cocos2d-x中的内存管理和性能优化。 第五篇平台移植,即第21章~第23章,内容包括从Win32到Android平台的移植、从Win32到WindowsPhone8平台的移植和从Win32到iOS平台的移植。 第六篇开发实战,即第24章~第29章,内容包括使用Git管理程序代码和多个项目实战——迷失航线手机游戏项目开发、为迷失航线游戏添加广告、发布放到Googleplay应用商店、发布放到WindowsPhone应用商店和发布放到苹果AppStore。

5,178

社区成员

发帖
与我相关
我的任务
社区描述
微软技术社区为中国的开发者们提供一个技术干货传播平台,传递微软全球的技术和产品最新动态,分享各大技术方向的学习资源,同时也涵盖针对不同行业和场景的实践案例,希望可以全方位地帮助你获取更多知识和技能。
windowsmicrosoft 企业社区
社区管理员
  • 山月照空舟
  • 郑子铭
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告

微软技术社区为中国的开发者们提供一个技术干货传播平台,传递微软全球的技术和产品最新动态,分享各大技术方向的学习资源,同时也涵盖针对不同行业和场景的实践案例,希望可以全方位地帮助你获取更多知识和技能。

予力众生,成就不凡!微软致力于用技术改变世界,助力企业实现数字化转型。

试试用AI创作助手写篇文章吧