C++类与对象(上)

1.类的定义

1.1类定义格式

• class为定义类的关键字,Stack为类的名字(跟结构体名类似),{}中为类的主体,注意类定义结束时后面分号不能省略。类体中内容称为类的成员:类中的变量称为类的属性或成员变量, 类中的函数称为类的方法或者成员函数。

我们来用类封装一个简单的基于动态数组的栈(Stack)数据结构:

#include<iostream>
#include<assert.h>
using namespace std;class Stack
{
public:// 成员函数void Init(int n = 4)//缺省参数{array = (int*)malloc(sizeof(int) * n);if (nullptr == array){perror("malloc申请空间失败");return;}capacity = n;top = 0;}void Push(int x){// ...扩容array[top++] = x;}int Top(){assert(top > 0);return array[top - 1];}void Destroy(){free(array);array = nullptr;top = capacity = 0;}private:// 成员变量int* array;size_t capacity;size_t top;
}; // 分号不能省略int main()
{Stack st;//创建了一个 Stack 类的对象 stst.Init();st.Push(1);st.Push(2);cout << st.Top() << endl;st.Destroy();return 0;
}

这段代码是用 C++ 的 类(class) 实现的栈(Stack),相比 C 语言的结构体(struct)+ 函数的方式,它更加面向对象,封装性更好,代码更清晰。

• 为了区分成员变量,⼀般习惯上成员变量会加⼀个特殊标识,如成员变量前面或者后面加_ 或者 m开头,注意C++中这个并不是强制的,只是⼀些惯例。

例如:

class Data
{
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;int _month;int _day;
};int main()
{Data da;da.Init(2023, 1, 1);da.print();return 0;
}

• C++中struct也可以定义类,C++兼容C中struct的用法,同时struct升级成了类,明显的变化是struct中可以定义函数,但是⼀般情况下我们还是推荐用class定义类。

// C语言写法
typedef struct ListNodeC
{struct ListNodeC* next;int val;
}LTNode;// C++写法
// C++升级struct升级成了类
// 1、类⾥⾯可以定义函数
// 2、struct名称就可以代表类型
struct ListNodeCPP
{
public:void Init(int x){next = nullptr;val = x;}private:ListNodeCPP* next;int val;
};

•在 C++ 中,定义在类内部的成员函数(即在类声明中直接实现的函数)默认会被编译器视为 inline 的候选,但最终是否真正内联展开由编译器决定。inline只是建议,并不代表绝对。

1.2访问限定符

在上面代码中,我们可以看到publicprivate出现,它们代表着什么意思呢?

C++ 中,封装是一种核心的面向对象编程特性,它通过 类(class) 将数据(属性)和操作数据的方法(成员函数)捆绑在一起,并通过 访问控制权限 选择性暴露接口,隐藏实现细节。

public,privateprotected就是用于实现封装的关键访问控制修饰符,它们决定了类成员的可见性和可访问性。

public修饰的成员在类外可以直接被访问;protectedprivate修饰的成员在类外不能直接被访问,protected和private是⼀样的,要在学习继承知识时才能看出它们的区别,这里不过多描述。

• 访问权限作用域从该访问限定符出现的位置开始直到下⼀个访问限定符出现时为止,如果后面没有访问限定符,作用域就到 }即类结束。

• class定义成员没有被访问限定符修饰时默认为private,struct默认为public。

• ⼀般成员变量都会被限制为private/protected,需要给别⼈使用的成员函数会放为public。

1.3类域

在 C++ 中,类定义了一个独立的作用域(class scope),所有成员(变量和函数)都属于这个作用域。当在类外定义成员函数时,必须通过 :: 作用域解析运算符 显式指明该函数属于哪个类,否则编译器会将其视为全局函数,导致编译错误。

核心概念
类作用域
类的成员(如 array、Push())的作用域仅限于类内部。
在类外直接访问这些成员时,编译器无法识别(除非通过对象或 :: 指定类域)。

:: 的作用 显式声明成员函数的归属类,指导编译器在正确的类作用域中查找成员变量和其他依赖。

class Stack
{
public:void Init(int n = 4);private:int* arr;int top;int capacity;
};// 声明和定义分离,需要指定类域
// 如果没有指定类域,编译器会认为 Init 是全局函数,找不到 arr(因为 arr 属于 Stack 类作用域)
void Stack::Init(int n)
{int* arr = (int*)malloc(sizeof(int) * n);if (arr == nullptr){perror("malloc fail");return;}top = 0;capacity = n;
}int main()
{Stack st;st.Init();return 0;
}

类作用域的实际影响

  1. 名称查找规则
    在类外定义成员函数时,编译器按以下顺序查找符号:
    当前函数局部作用域 → 2. 类作用域(通过 :: 指定)→ 3. 全局作用域。
    若未指定类域,编译器直接跳到全局作用域查找,导致成员变量无法找到。

  2. 与全局函数的区分

void Init();        // 全局函数
void Stack::Init(); // 类的成员函数

即使同名,因作用域不同,二者不会冲突。

2.类的实例化

2.1 什么是实例化

在C++中,类的实例化是将抽象的类定义转化为具体对象的过程。类本身只是对对象的一种抽象描述,它声明了成员变量和方法,但并不会实际分配内存空间,就像一个建筑设计图规定了房间的数量和功能,但图纸本身并不能住人。只有当通过类创建对象(如Person p;)时,系统才会为对象的成员变量分配物理内存空间,此时对象才能存储和操作真实数据。一个类可以实例化出多个独立的对象,每个对象都拥有自己的内存空间来存储数据,就像根据同一张设计图可以建造出多栋实际可居住的房子。类的成员函数(行为)则被所有对象共享,存储在代码段中,不会因实例化而重复占用内存。

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;int _month;int _day;
};int main()
{//// Date类实例化出对象d1和d2Date d1;Date d2;d1.Init(2020, 1, 1);d1.print();d2.Init(2020, 1, 2);d2.print();return 0;
}

2.2 计算类的实例化对象大小

在C++中,类实例化的对象仅包含成员变量,不存储成员函数。成员函数编译后位于代码段,所有对象共享同一份函数代码。调用成员函数时,编译器通过隐式传递this指针(如d1.Print()编译为Print(&d1))确定操作对象,无需在每个对象中存储函数指针。静态绑定(普通成员函数)的地址在编译期确定,直接硬编码到调用指令中;只有虚函数(动态多态)才需要运行时查表(通过虚函数表指针vptr),此时对象需额外存储vptr指向虚函数表,虚函数在以后我们会提及到。因此,非虚成员函数既不需要存储代码也不需存储指针,仅虚函数机制会引入额外指针开销(每个对象一个vptr),避免了重复存储造成的空间浪费。

this指针
• Date类中有 Init 与 Print 两个成员函数,函数体中没有关于不同对象的区分,那当d1调⽤Init和Print函数时,该函数是如何知道应该访问的是d1对象还是d2对象呢?那么这⾥就要看到C++给了⼀个隐含的this指针解决这⾥的问题
编译器编译后,类的成员函数默认都会在形参第⼀个位置,增加⼀个当前类类型的指针,叫做this指针。⽐如Date类的Init的真实原型为, void Init(Date* const this, int year,int month, int day)
类的成员函数中访问成员变量,本质都是通过this指针访问的,如Init函数中给_year赋值, this->_year = year;
C++规定不能在实参和形参的位置显示的写this指针(编译时编译器会处理),但是可以在函数体内显示使用this指针。

public:// void Init(Date* const this, int year, int month, int day)void Init(int year, int month, int day)//二者等价,但C++规定不能在形参显示this{//this = nullptr;编译报错:error C2106: “=”: 左操作数必须为左值// this->_year = year;_year = year;//二者等价,并且任意一种写法都符合语法this->_month = month;this->_day = day;}

言归正传,上⾯我们分析了对象中只存储成员变量,C++规定类实例化的对象也要符合内存对⻬的规则。

内存对齐规则其实我们以前学习C语言结构体大小时学习过,这里我们再复习一下。

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

看两个例子:
在这里插入图片描述
class A 的成员依次是 char _ch、int _i,内存对齐会按照以下步骤排布:
放置第一个成员 char _ch:
从地址 0 开始,char 占 1 字节,此时占用地址 0。
对齐填充:
下一个成员是 int _i,它的对齐值是 4 字节。当前偏移是 1,不是 4 的倍数,所以需要填充 3 个字节(填充到地址 3 之后,让下一个成员的起始地址满足自身对齐要求 ),填充的字节没有实际意义,是为了对齐。
放置第二个成员 int _i:
从地址 4 开始,int 占 4 字节,占用地址 4 到 7。
整体对齐:
类的整体大小需要是其最大对齐值(这里 int 的对齐值 4 是最大的)的整数倍。当前已用空间是 8 字节(1 + 3 + 4 = 8 ),8 是 4 的倍数,无需额外填充。

在这里插入图片描述
大家第一眼看会不会觉得结果是0,其实并不是,为什么呢?
因为如果⼀个字节都不给,怎么表示该对象存在过呢!所以这里给1字节,纯粹是为了占位标识对象存在。

更多的内存对齐计算例子可以看结构体博客。

以下是几道围绕 C++ 类 / 结构体布局设计的题目:
1.下⾯程序编译运⾏结果是()
A、编译报错 B、运⾏崩溃 C、正常运⾏

#include<iostream>
using namespace std;
class A
{
public:void Print(){cout << "A::Print()" << endl;}
private:int _a;
};int main()
{A* p = nullptr;p->Print();return 0;
}

这段代码的编译运行结果是 C、正常运行。

  1. 成员函数的调用机制

    p->Print() 会被编译器转换为 A::Print(),隐式传递 this 指针(此处 p 是 nullptr)。

    但 Print() 函数内部并未访问任何成员变量(如 _a),因此不会解引用 this 指针

  2. 未触发解引用

    只有通过 this 访问成员变量(如 this->_a)时才会引发解引用空指针崩溃。

    本例中 Print() 仅输出字符串,不依赖对象内存,故无崩溃风险。

2.下⾯程序编译运⾏结果是()
A、编译报错 B、运⾏崩溃 C、正常运⾏

#include<iostream>
using namespace std;
class A
{
public:void Print(){cout << "A::Print()" << endl;cout << _a << endl;}
private:int _a;
};
int main()
{A* p = nullptr;p->Print();return 0;
}

这段代码的编译运行结果是 B、运行崩溃。

  1. 空指针调用成员函数

    虽然 p->Print() 的语法可以编译通过(因为 Print() 是普通成员函数,非虚函数),但实际执行时会传递 this = nullptr 给函数。

  2. 访问成员变量导致解引用空指针

    在 Print() 函数中,第 10 行 cout << _a << endl; 实际上等价于 cout << this->_a << endl;。

    由于 this 是 nullptr,尝试访问 _a 会触发 解引用空指针,导致运行时崩溃(如段错误)。

  3. 与安全调用的区别

    如果 Print() 函数 不访问任何成员变量(如原题第 1 问),则不会解引用 this,可以正常运行。

    但本题中 _a 的访问直接依赖 this 指针,因此必然崩溃。

3.this指针存在内存哪个区域的 ()
A. 栈 B.堆 C.静态区 D.常量区 E.对象里面

在 C++ 中,this 指针的存储位置取决于调用成员函数的上下文环境,但最准确的答案是:A. 栈。当通过对象调用成员函数(如 obj.func())时,this 指针作为函数的隐式参数,会被编译器放入栈中。

我们来回忆一下这几个区域的存储内容

内存区域存储内容管理方式生命周期
局部变量、函数参数自动函数执行期间
动态分配的对象手动直到显式释放
静态区全局/静态变量自动整个程序运行期间
常量区字符串字面量、constexpr自动整个程序运行期间
代码区函数体、指令自动整个程序运行期间

this指针:作为函数调用的隐式参数,通常存储在栈或寄存器中。
对象成员:非静态成员变量存储在对象所属的内存区域(栈或堆),静态成员变量存储在静态区。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/589.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Cesium加载3DTiles模型并且重新设置3DTiles模型的高度

代码&#xff1a; 使用的时候&#xff0c;直接调用 load3DTiles() 方法既可。 // 加载3Dtiles const load3DTiles async () > {let tiles_url "/3DTiles2/Production_1.json";let tileset await Cesium.Cesium3DTileset.fromUrl(tiles_url, {enableCollision: …

JavaSE-多态

多态的概念在完成某个行为时&#xff0c;不同的对象在完成时会呈现出不同的状态。比如&#xff1a;动物都会吃饭&#xff0c;而猫和狗都是动物&#xff0c;猫在完成吃饭行为时吃猫粮&#xff0c;狗在完成吃饭行为时吃狗粮&#xff0c;猫和狗都会叫&#xff0c;狗在完成这个行为…

MySQL SQL语句精要:DDL、DML与DCL的深度探究

在数据库技术的浩瀚星空中&#xff0c;MySQL犹如一颗璀璨的星辰&#xff0c;以其卓越的性能、灵活的架构以及广泛的适用性&#xff0c;深受全球众多开发者的青睐。而 SQL&#xff08;Structured Query Language&#xff0c;结构化查询语言&#xff09;作为与数据库交互的核心语…

MYSQL笔记2

创建表&#xff1a; 格式&#xff1a; create table 表名(表选项) 表定义选项格式为&#xff1a; 列名1 列类型1 约束, 列名2 列类型2 约束,…… 默认的情况是&#xff0c;表被创建到当前的数据库中。若表已存在、没有当前数据库或者数据库不存在&#xff0c;则会出现错误 使…

Linux修炼:开发工具

Hello大家好&#xff01;很高兴我们又见面啦&#xff01;给生活添点passion&#xff0c;开始今天的编程之路&#xff01; 我的博客&#xff1a;<但凡. 我的专栏&#xff1a;《编程之路》、《数据结构与算法之美》、《题海拾贝》、《C修炼之路》、《Linux修炼&#xff1a;终端…

基于 CentOS 7 的 LVS+DR+Web+NFS 旅游攻略分享平台部署

1 项目概述 1.1 旅游攻略平台项目背景 随着互联网旅游行业的快速发展&#xff0c;用户对旅游攻略分享平台的高可用性和稳定性提出了更高要求。传统单服务器架构在面对高并发访问时容易出现性能瓶颈&#xff0c;导致响应延迟甚至服务中断。本项目基于 LVSDRWebNFS 架构&#x…

python-enumrate函数

文章目录基本语法基本用法基本遍历指定起始索引实际应用场景需要索引的循环创建字典映射处理文件行号与range(len())对比注意事项enumerate()是Python内置函数&#xff0c;用于在遍历序列&#xff08;如列表、元组或字符串&#xff09;时同时获取索引和值。基本语法 enumerate…

linux_线程概念

线程特征&#xff1a;是进程内的执行分支&#xff0c;线程的执行粒度&#xff0c;要比进程要细。1.理解Linux线程线程<执行流<进程 线程实际上是复用进程的数据结构和管理算法&#xff0c;进程的task struct&#xff0c;实际上是模拟线程&#xff0c;部分书中说Linux没有…

校园幸运抽(抽奖系统)测试报告

校园幸运抽&#xff08;抽奖系统&#xff09; 项目介绍测试用例设计部分测试示例自动化测试编写自动化脚本&#xff08;1&#xff09;引入相关的脚本依赖&#xff08;2&#xff09; 创建对应的类和文件夹自动化部分功能实战结果&#xff08;视频&#xff09;部分源码展示&#…

【跟着PMP学习项目管理】敏捷专题 - 敏捷概述

目录 1、可确定的工作与高度不确定的工作 2、《敏捷宣言》及思维模式 3、 精益与看板方法 4、 不确定性、风险和生命周期选择 1、可确定的工作与高度不确定的工作 项目工作包括可确定的工作与高度不确定的工作。可确定的工作项目具有明确的流程,它们在以往类似的…

os.machine()详解

核心功能返回硬件架构 返回字符串表示系统的硬件架构&#xff0c;常见值包括&#xff1a; x86_64&#xff1a;64 位 x86 架构&#xff08;Intel/AMD&#xff09;armv7l&#xff1a;32 位 ARM 架构&#xff08;如树莓派 3B&#xff09;aarch64&#xff1a;64 位 ARM 架构&#x…