
大家好啊,我是云泽Q,一名热爱计算机技术的在校大学生。近几年人工智能技术飞速发展,为了帮助大家更好地抓住这波浪潮,在开始正文之前,我想先为大家推荐一个非常优质的人工智能学习网站)。它提供了从基础到前沿的系列课程和实战项目,非常适合想要系统入门和提升AI技术的朋友,相信能对你的学习之路有所帮助。
类可以按增强版的结构体来理解,它是一个复合类型,C++把语言原生代的一些类型叫做基本类型,例如int,double,char,指针。用类定义的叫做自定义类型

C语言结构体的一大缺陷之一就是

名称并不代表类型,加上struct才代表类型,写起来很麻烦,太长了,所以加了typedef来解决这样的问题 而C++设计的类就改进了,类名(Stack)就是类型,不需要加class关键字
第一张图访问不了成员变量和成员函数的原因是C++在这引入了一个封装的概念 C++自己而言,提的是面向对象,C语言是面向过程 面向对象有三大特性:封装,继承,多态 C语言在封装这里典型的特点就是,数据和方法是分离的,C++封装的第一个特点反之,C++封装的第二个特点就是访问限定符,见1.2。接下来推荐1.1,1.2结合来看

再补充一个特殊的点,在数据结构中定义链表节点的时候必须要像下图这样写


如果不加这些额外的东西,有时候就会有这些场景

这时候形参和成员变量就不太分的清楚了 这里介绍两种大的规范,公司都会尊崇一种
//驼峰法(Windows) StackInit 类型 函数,单词首字母大写+单词首字母大写
// initCapacity 变量 单词首字母小写+单词首字母大写
//第二种规范(Linux) stack_init
// init_capacity访问限定符有3个,公有,私有和保护


如图红框的区域就都是公有,从private:到 } 都是私有,定义多个公有,私有,保护也是没有问题的
类是支持声明和定义分离的



这样写编译是不会通过的,编译器会认为这是个全局函数(搜索变量的时候只会在局部搜索和全局搜索,不会去类中去找)
类之所以做声明和定义分离是为了方便维护和管理代码,有时候定义的类在项目中代码量很大,全部定义在.h里面是很不方便的
补充一下,在类里面是不存在两个同名的变量函数冲突的,他们的作用域是独立的,这里以栈和队列举例
class Stack
{
void Init();
void Push(int x);
};
class Queue
{
void Init();
void Push(int x);
};

类实例化出的每个对象,都有独立的数据空间,所以对象中肯定包含成员变量,那么成员函数是否包含呢?首先函数被编译后是一段指令,对象中没办法存储,这些指令储存储在一个单独公共的区域(代码段),那么对象中非要存储的话,只能是成员函数的指针。再分析一下,对象中是否有存储指针的必要呢,s1的top和s2的top不是同一个top,因为s1,s2对象的成员变量是独立的,各自的成员变量存各自的数据,但是s1的Init和s2的Init调用的是同一个函数(地址是一样的),成员变量不同要各自开空间,成员函数都是一样,如果在每个对象都存一份就是空间的浪费,可以把成员函数的指针存到对象之中,但这就很浪费了。再额外说一下,另外函数指针不需要存储的一个原因,函数指针是一个地址,调用函数被编译成汇编指令[call 地址],其实编译器在编译链接时,就要找到函数的地址,即成员函数的地址是在编译链接时确定的(如果有定义,在一个文件,就是编译时确定,如果只有声明,没有定义,就链接的时候确定),不是在运行时找,只有动态多态是在运行时找,就需要存储函数地址


在 C++ 中,sizeof运算符既能接收对象也能接收类型作为操作数,核心源于其编译时计算的特性,具体可整合如下: sizeof是在编译阶段就确定结果的运算符,其计算不依赖程序运行时的状态。当作用于类型(如sizeof(Stack))时,编译器直接根据该类型的定义(如类Stack的成员变量布局),遵循内存对齐规则计算出该类型对象的内存占用大小 —— 此过程仅关注成员变量(成员函数存储在代码段,不占用对象内存)。 而当sizeof作用于对象(如sizeof(s1))时,编译器本质上仍是通过该对象的类型(如s1所属的Stack类型)来确定大小。它并非在运行时 “测量” 对象实际占用的内存,而是在编译时就依据其类型定义和内存对齐规则完成计算,结果与sizeof(Stack)完全一致。 这种特性使得sizeof的使用极为灵活:既可以直接针对类型计算(无需提前创建对象),也可以针对已存在的对象计算,且两种方式在本质上都是基于类型定义的编译时推导,与 C 语言结构体大小的计算逻辑(依赖成员布局和内存对齐)相通。
内存对齐:

第一个成员变量array,在X86,32位下是4个字节,第二个成员capacity;是4个字节,相比VS下默认对齐数小,所以第二个成员还是4个字节,依次类推,最后对象整体还要对齐,最大对齐数(4)的整数倍,12是4的整数倍,没有问题


这里A的大小很好看

B和C一样大,C是空的,B中成员函数是不存在对象之中的,这里开一个字节是为了占位,不存储实际数据,表示对象存在过
B b1;//如果不开空间,无法判断这里B对象到底存在不存在
cout<<&b1<<endl;
//不开空间无法取地址 
也就是在调用前之所以知道调用哪个对象的成员,是编译器将调用对象的地址传给了一个隐含的指针this,然后通过this来访问成员。达到了一个谁调用,就访问谁的成员变量的效果

这道题看似有空指针的问题,然而却可以正确运行,指针越界不一定可以检查出来,但是空指针一定可以检查出来,所以该题就不是空指针的问题
这里看似对空指针进行解引用,这是表象。语法没有问题,本质要转换成指令,有没有解引用要看实际转换成指令的语法表达有没有需求 Print()这里要转换成call 函数地址,但是函数地址没有存到p指向的对象之中,函数的地址不是运行时去找的,是编译时就确定的,编译时编译函数拿到一段指令,有定义编译时就确定地址,只有声明的话链接时拿到地址,这里p的功能是传参,用对象调用,是取对象的地址传给this指针

p本来就是对象的地址,将p的值传给一个叫ecx的寄存器,再通过寄存器传递,也就是说这里的this指针是空的

所以这个程序可以正常运行。空指针并不犯错,解引用空指针才犯错,接下来把p->Print();换为下面的代码,依旧可以正常运行
(*p).Print();p->Print() 的意思是:通过指针 p 调用它所指向的 A 类对象的 Print() 成员函数。 这相当于先对指针进行解引用(* p 得到指针所指向的对象),然后再访问成员。((* p).Print()),道理依旧一样,看似对空指针直接解引用了,但这只是语法的表象,编译器看到的这里的核心动作是调用Print();函数,但是函数的地址并没有存到(* p)的对象之中,没有存到对象之中就不去解引用了,单纯的调用函数是不需要对指针去解引用的,编译器不会先去解引用再取地址,可以理解编译器比你所在的维度更高

补充一下:编译器在遇到解引用空指针和野指针只能是运行错误,编译器无法直接检查出错,编译错误是检查语法错误,解引用空指针野指针不是语法错误,是运行逻辑错误

这题显然就是错的了,成员变量是存在对象之中的,所以有解引用
补充一下,this指针存在内存中的栈区(或寄存器),因为其是形参,形参和局部变量是类似的,形参的空间是在栈帧之中的。 因为this指针可能在函数中需要高频的访问,寄存器是比内存更快的,this指针也可能把对象的地址放到寄存器,不同的编译器实际情况不同。
虽然在C++入门阶段实现的Stack看起来变了很多,但是实质上变化不大,后面STL的文章中用适配器实现的Stack,方可感受C++的魅力