ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

C++类与对象完整学习路线(上)

C++类与对象完整学习路线(上) 文章导航前序一.类的定义1.类定义格式1.1 访问限定符默认规则1.2 类域二.实例化到底是什么1.实例化2. 对象的大小三.你了解this指针多少大家好我是小沐。最近持续深耕C基础语法今天也是刚好来到了“类与对象”这一板块我也刚好整理了一些知识点对新手友好适合刚入门的同学查缺补漏。类的定义、实例化和this指针一.类的定义1.类定义格式C中用class和struct两个关键字来定义类{}中为类的主体结束时加上分号即可和C语言中的结构体写法一模一样。那我们不经问类到底是什么类是一种自定义的数据类型把自变量和函数作为主体即封装称里面的自变量为成员变量或属性称函数为成员函数或方法。那小白肯定会有疑惑的刚从C语言过渡只见过内置的数据类型如int 、char、double和struct等甚至都不知道一直在用的struct也是一种数据类型那你一个类里面又装函数又装变量的既然也是一种数据类型这时候看到这里脑子里面就会懵懵的。是的类就是一种数据类型。class zhangsan { void fuct1() { cout 1 endl; } }; struct lisi { void fuct2() { cout 2 endl; } };两种类定义和使用方法一模一样也算沿用C语言结构体的方式了。我们继续看下面这种调用会有什么结果#include iostream using namespace std; class zhangsan { void fauct1() { cout 1 endl; } }; struct lisi { void fuct2() { cout 2 endl; } }; int main() { zhangsan zhang; lisi li; zhang.fuct1(); li.fuct2(); return 0; }答案就是编译报错。class和struct定义的类虽然调用方式一样但语法上存在差异那就是访问限定符默认规则不一样。1.1 访问限定符默认规则class与struct的访问限定符——》public 公开的访问限定符——》protect 保护性有限制的限定符——》private 私有性限定符总所周知有这三个限定符修饰的区域都会有相应的特性在里面那没有限定符修饰的区域呢从上面的例子我们知道并不是两个都可以直接访问默认区域的成员函数的。规则class的默认区域是属于private区域的类外不以直接访问。struct的默认区域是属于public区域的类外可以直接访问。在习一般情况下我们是不会把成员函数写到private中而是写到public中。那么定义在private中的成员变量外边就没有办法访问了吗——是的类外确实不能直接访问private中的成员变量但可以通过这种间接的访问获取成员变量。我们以时间类为例class Date { public: int GetYear() { return _year ; } int GetMonth() { return _month; } int GetDay() { return _day; } private: int _year; int _month; int _day; };1.2 类域上面我们说类是一种数据类型那么这种数据类型定义时就会产生一个域它的所有成员都在里面我们称这个域为 “类域”。类域和其它域一样都会影响编译器的查找规则。我们一起来看下面的例子class Stack { public: //成员函数 void Init(int n 4); private: //成员变量 int* array; size_t capacity; size_t top; }; void Init(int n) { array (int*)malloc(sizeof(int) * n); if (array nullptr) { perror(Init::malloc); return; } capacity n; top 0; } int main() { Stack st; st.Init(); return 0; }思考一下这段代码有没有问题答案有问题我们说类会定义出一个域而域要被类外访问是需要通过域访问限定符::访问的。也就是说void Stack::Init(int n) //这里需要加上Stack:: { array (int*)malloc(sizeof(int) * n); if (array nullptr) { perror(Init::malloc); return; } capacity n; top 0; }如果这里没有加入Stack::这个初始化函数就只是一个在全局变量中定义的函数缺少的各种变量编译器都会自动去全局域里面找没有找到自然就会报错了。二.实例化到底是什么1.实例化定义类定义出的具体对象叫做实例化说得这么高级其实就是用类定义一个具体对象如class Data { public: int month; int day; } int main() { Data d1; //d1就是具体对象 return 0; }这里的d1就是具体的对象。定义了具体对象后开辟了可以运用的空间这个过程我们就管它就实例化。形象比喻类定义时就像做建筑用的设计图这时建筑仅仅停留在图纸层面还不能住人。当定义了一个具体对象后就像建筑已经从设计图到完成建筑建设可以住人了。2. 对象的大小思考既然类是一种数据类型那么类的大小是多大呢是所有成员的简单相加吗在计算对象大小前我们先再弄明白一个事情类的成员函数是存储在类里面吗如果是那就是定义一个具体对象就会同时在栈帧上开辟一块空间存储函数。如果不是那成员函数会存储在哪多个对象调用时过程是怎样的为回答这个问题我们将从汇编的层面观察调用过程。以这段代码为例class Stack { public: //成员函数 void Init(int n 4) { array (int*)malloc(sizeof(int) * n); if (array nullptr) { perror(Init::malloc); return; } capacity n; top 0; } private: //成员变量 int* array; size_t capacity; size_t top; }; int main() { Stack ST1; Stack ST2; ST1.Init(); ST2.Init(); return 0; }现在我们分别在ST1和ST2调用出打断点进行调试为方便观察我们选择在x86环境进行调试。我们看到第一步push入栈第二步把ST1的地址传给寄存器ecx第三步把Stack里面的Init函数的地址计算出来然后跳转到这个函数的地址执行操作。我们画的两个红圈中明显标注着ST1和ST2调用时计算出的地址是一样的呀这说明它两调用的是同一个函数也就是说类的成员函数虽然是在类里面定义的但编译时并没有存储在类的栈区里面。既然成员函数没有存储在类的栈区里面那么类的对象的大小就不需要计算成员函数的大小。好我们回归对象大小的计算问题一起看例子求下面对象的大小:class ST1 { public: int a; char b; }; class ST2 { void fuct() { int a; } }; class ST3 { }; int main() { cout sizeof(ST1) endl; cout sizeof(ST2) endl; cout sizeof(ST3) endl; return 0; }请问这段代码输出的结果是多少A.50, 0 B.8 0, 0 C.5,1,1 D.8,1,1我们来公布答案由于编译器在运行时受到内存对齐规则的约束第一个成员放在结构体偏移量为0的位置其它成员对齐到对齐数vs的默认对齐数为8的整数倍的地址出。注对齐数 编译器默认的一个对齐数 与该成员大小的较小值。总大小 最大对齐数的整数倍那么ST1在内存中应该是这样的由于最大对齐数是整型的对齐数4故总大小应该为4的整数倍故最终结果应为8个字节。问为什么只定义了成员函数和没有成员的对象大小不为0因为如果一个字节都不给就没有办法表示它们确实存在所以这里给1字节纯粹是为了占位标识对象存在。那么问题又来了为什么要内存对齐这不是会造成内存浪费吗这和机器硬件层面有关硬件里面有一个叫做地址总线有32条或64条与机器有关每一条通过充放电表示1和0也就是2进制的表现形式。以32条地址总线的机器为例无论你数据在内存中如何存放我cpu每次读取都是按照32bit的大小的整数倍范围进行读取如果数据存放没有内存对齐有可能出现数据被截断的情况就需要在读取结束后进行拼接更加繁琐还导致性能下降。这个时候我们的选择是牺牲空间换取时间。三.你了解this指针多少上面我们有说过类的成员函数并存储在类的栈帧区域里面但一个类同时定义出多个具体对象时它们可以调用统一函数那么这个函数是如何识别是哪个具体对象在调用它呢答案就是这部分的主角this指针。class Date { public: void DATE(int year, int month, int day) //void DATE(DATE* const this, int year, int month, int day) { cout year / month / day endl; } }; int main() { Date d1; Date d2; d1.DATE(2026, 8, 3); //d1.DATE(d1,2026, 8, 3); d2.DATE(2023, 9, 1); //d2.DATE(d2,2023, 9, 1); return 0; }在类域里面定义的函数都隐藏了一个this指针,在调用函数时编译器会自动帮你把对象的地址也传过去在输出部分实际上是这样的void DATE(int year, int month, int day) //void DATE(DATE* const this, int year, int month, int day) { cout this-year / this-month / this-day endl; }只是编译器把this指针给隐藏了。C规定不能在实参和形参的位置显式的写this指针但是可以在函数体内部显式写this指针。既然是指针就有可能出现空指针解引用的情况现在我们结合前面的知识来判断下面两段代码运行结果是什么代码1class A { public: void Print() { cout Print(int a 4) endl; } private: int _a; }; class B { public: void Print() { cout Print(int a 4) endl; cout _a endl; } private: int _a; }; int main() { A* a nullptr; //B* b nullptr; a-Print(); //b-Print(); return 0; }代码2class A { public: void Print() { cout Print(int a 4) endl; } private: int _a; }; class B { public: void Print() { cout Print(int a 4) endl; cout _a endl; } private: int _a; }; int main() { //A* a nullptr; B* b nullptr; //a-Print(); b-Print(); return 0; }代码1编译报错 运行崩溃 正常运行代码2编译报错 运行崩溃 正常运行那么答案是代码1正常运行代码2运行崩溃。如果这个问题没有做对说明你对this指针还是不了解。知识点this指针传的是对象的地址不是对象指针的地址这两个定义因为a和b已经存放的是对象的地址了所以这里传值的时候默认是这样a-Print(a);//不是a-Print(a) b-Print(b);//不是b-Print(b)那我们又知道地址传过去后如果没有解引用操作编译器是不会报错的。而代码1仅仅是打印了一个字符串啊并没有解引用操作。 对于代码2就不一样了class B { public: void Print() { cout Print(int a 4) endl; cout _a endl; //这里本质就是this-_a } private: int _a; };这里面_a的打印实际就是this-_a但是我们的this指针传过去的是空指针啊对空指针解引用就导致程序崩溃。好了本篇到这里就结束了你在学习这部分的时候有没有踩过什么坑评论区聊聊我整理到下一篇里~
返回列表