尧图精选

C++ 基础入门篇(四)类和对象-第一篇:定义,实例化,this指针,封装

🕒 发布时间:2026/9/26 10:38:30 📁 来源:尧图网络
文章目录1. 类的定义1.1 类的定义和成员1.2 class与struct1.2.1 C的兼容性1.2.2 class和strcut的区别1.3 类访问限定符1.3.1 介绍1.3.2 访问权限作用域1.4 类定义的习惯1.4.1 定义的位置1.4.2 变量的命名1.5 类域1.5.1 类域的基本规则1.5.2 成员函数内的名字查找顺序1.5.3 类外访问类域成员2. 类的实例化2.1 类的实例化的定义2.2 对象大小的计算2.2.1 对象中有哪些需要计算2.2.2 内存对齐2.2.3 为什么存在内存对齐?3. this指针3.1 引入3.2 this指针的定义3.3 this指针的注意事项3.4 this指针的特点3.5 this指针的存储位置3.6 常考易错面试题4. C与封装规范管理4.1 C和C实现栈的区别1. 类的定义类class是面向对象编程中的一种用户自定义数据类型它把数据和对这些数据的操作封装在一起是创建对象的模板。对象是类的实例。1.1 类的定义和成员类的定义和和C语言中的结构体非常相似但是C中的类除了定义变量外还可以定义函数。class为定义类的关键字Stack为类的名字{}中为类的主体注意类定义结束时后面分号不能省略。类体中内容称为类的成员类中的变量称为类的属性或成员变量。类中的函数称为类的方法或者成员函数。例子定义“学生”这个类classStudent{// 定义一个类类名是 Studentprivate:// 私有成员类外不能直接访问intid;// 数据成员学号intscore;// 数据成员成绩public:// 公有成员类外可以通过对象访问voidsetId(inti){// 成员函数设置学号idi;}intgetId()const{// 成员函数获取学号const 表示不修改对象returnid;}voidsetScore(ints){// 成员函数设置成绩scores;}intgetScore()const{// 成员函数获取成绩returnscore;}};1.2 class与struct1.2.1 C的兼容性typedefstructListNode_C{intval;structListNode_C*next;}LTNode;structListNode_CPP{intval;ListNode_CPP*next;};如上两种结构体 写法在C都适用。第一种保留了C语言的传统使struct结构体名的方式命名指针使用typedef重命名结构体名称以便后续代码编写方便。第二种C在C语言的基础上做出改进命名结构体指针不再需要struct关键字。自然省略了typedef的过程。1.2.2 class和strcut的区别C 语言的 struct 只是“数据的集合”而 C 的 class 是“数据 操作数据的函数”的封装体支持面向对象特性。C 语言structStudent{intid;intscore;};voidsetScore(structStudent*s,intscore){s-scorescore;}CclassStudent{private:intid;intscore;public:voidsetScore(ints){// 函数直接放在类里scores;}};补充在 C 中struct 和 class 几乎一样唯一区别是class 默认访问权限是 private(私有)struct 默认访问权限是 public(公开)。所以 C 的 struct 也能有成员函数、继承等只是默认公开。而 C 语言的 struct 是 C struct 的一个子集。C中struct也可以定义类C兼容C中struct的用法同时struct升级成了类明显的变化是struct中可以定义函数一般情况下我们还是推荐用class定义类。1.3 类访问限定符1.3.1 介绍C 一种实现封装的方式用类将对象的属性与方法结合在一起让对象更加完善通过访问权限选择性地将其接口提供给外部的用户使用。三种类访问限定符1. public公有任何地方都能访问。2. private私有只有本类成员和友元能访问禁止类外部访问3. protected保护本类、派生类、友元能访问类外不能访问。private和protected都是禁止类外部访问现阶段没有任何区别推荐目前写类的时候都使用private 在后面的继承中会详细介绍他们的区别。class定义成员没有被访问限定符修饰时默认为privatestruct默认为public。1.3.2 访问权限作用域访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止如果后面没有其他访问限定符作用域就到}即类结束。classDemo{inta;// 默认 private因为 class 默认私有public:intb;// publicvoidf();// publicprivate:intc;// privateprotected:intd;// protectedpublic:inte;// public};// 类结束实际设计中通常把成员变量设为 private 或 protected只把需要对外使用的成员函数设为 public通过这种封装隐藏实现细节、保证数据合法性与一致性并便于后期修改维护1.4 类定义的习惯1.4.1 定义的位置各种公司中习惯将类的方法定义在上面把类的属性定义在下面。1.4.2 变量的命名错误示范classDate{public:voidInit(intyear,intmonth,intday){yearyear;monthmonth;dayday;}private:intyear;intmonth;intday;};形参名和成员变量名完全一样导致成员变量被“遮蔽”赋值语句变成了自己给自己赋值成员变量根本没被初始化。所以我们需要将参数设计为voidInit(inty,intm,intd)但是这样同时也降低了代码的可读性。其他人看你的代码不能够一目了然C并没有规定具体的命名规则于是在各个公司中就有了各种各样的约定俗称的命名习惯成员变量加 m_ 前缀或 _ 后缀如 m_year、year_从源头避免同名冲突。1.5 类域类定义了一个新的作用域类的所有成员都在类的作用域中在类体外定义成员时需要使用 :: 作用域操作符指明成员属于哪个类域。1.5.1 类域的基本规则classDate{public:voidInit(inty,intm,intd);private:intyear,month,day;};Date 定义了一个类域。year、month、day、Init 都是这个类域中的名字。在类外定义成员函数时要用 Date:: 进入类域voidDate::Init(inty,intm,intd){yeary;// year 在类域中查找monthm;dayd;}Date::Init 表示 Init 是 Date 类域中的成员。1.5.2 成员函数内的名字查找顺序在成员函数内部名字查找顺序大致是1. 局部作用域包括形参2. 类作用域3. 外围作用域全局、命名空间等所以之前那个错误示范voidInit(intyear,intmonth,intday){yearyear;// 左边的 year 是形参右边的也是形参}形参 year 在局部作用域遮蔽了类域中的成员 year导致成员变量没被赋值。正确写法voidInit(intyear,intmonth,intday){this-yearyear;// this-year 明确指定类域中的成员(后面会讲)}1.5.3 类外访问类域成员类外访问要用::Date d;d.Init(2025,1,1);// 通过对象访问 public 成员静态成员classA{public:staticintcount;};intA::count0;// 类外定义A:: 进入类域嵌套类classOuter{public:classInner{intx;};};Outer::Inner obj;// 访问 Outer 类域中的 Inner2. 类的实例化2.1 类的实例化的定义用类类型在物理内存中创建对象的过程称为类实例化出对象类是对象进行的一种抽象描述是一个模型一样的东西限定了类有哪些成员变量这些成员变量只是声明没有分配空间用类实例化出对象时才会分配空间。 一个类可以实例化出多个对象实例化出的对象占用实际的物理空间存储类成员变量举例如下代码其中d1就是实例化的Date类的对象。#includeiostreamusingnamespacestd;classDate{public:voidInit(intyear,intmonth,intday){_yearyear;_monthmonth;_dayday;}voidPrint(){cout_year/_month/_dayendl;}private:// 这⾥只是声明没有开空间int_year;int_month;int_day;};intmain(){// Date类实例化出对象d1和d2Date d1;Date d2;d1.Init(2024,3,31);// 使用对象d1.Print();d2.Init(2024,7,5);d2.Print();return0;}Date 是类是模板。d1d2是对象是 Date 类的实例。Data::_year2024;如上代码必然报错_year只是声明没有定义开辟空间不能这样访问。打个比方类实例化出对象就像现实中使用建筑设计图建造出房子类就像是设计图设计图规划了多少个房间、房间大小功能等但是并没有实体的建筑存在也不能住人用设计图修建出房子房子才能住人。同样类就像设计图一样不能存储数据实例化出的对象才会分配物理内存存储数据。classHouse{public:introoms;// 每个房子自己的房间数staticinttotal;// 所有房子共享的总数};intHouse::total0;// 静态成员类外定义intmain(){House h1,h2;// 实例化两栋房子h1.rooms3;// h1 有 3 个房间h2.rooms5;// h2 有 5 个房间House::total2;// 共享数据}2.2 对象大小的计算2.2.1 对象中有哪些需要计算分析一下类对象中哪些成员呢类实例化出的每个对象都有独立的数据空间所以对象中肯定包含成员变量那么成员函数是否包含呢首先函数被编译后是一段指令对象中没办法存储这些指令存储在一个单独的区域(代码段)那么对象中非要存储的话只能是成员函数的指针。再分析一下对象中是否有存储指针的必要呢Date实例化d1和d2两个对象d1和d2都有各自独立的成员变量_year/_month/_day存储各自的数据但是d1和d2的成员函数Init/Print指针却是一样的存储在对象中就浪费了。如果用Date实例化100个对象那么成员函数指针就重复存储100次太浪费了。这里需要再额外说一下其实函数指针是不需要存储的函数指针是一个地址调用函数被编译成汇编指令[call 地址]其实编译器在编译链接时就要找到函数的地址不是在运行时找只有动态多态是在运行时找就需要存储函数地址这个我们以后会讲解所以我们可以得出一个对象进行计算的时候只需要计算成员变量的大小并且成员变量要遵循内存对齐的规则2.2.2 内存对齐C中的内存对齐原理与C语言中的结构体内存对齐完全一致。这里我们简单回顾下更具体地讲解可以看我的这篇博客C语言自定义类型:结构体详解内存对齐规则1. 第一个成员在与结构体偏移量为0的地址处。2. 其他成员变量要对齐到某个数字对齐数的整数倍的地址处。3. 注意对齐数编译器默认的一个对齐数与该成员大小的较小值。4. VS中默认的对齐数为85. 结构体总大小为最大对齐数所有变量类型最大者与默认对齐参数取最小的整数倍。6. 如果嵌套了结构体的情况嵌套的结构体对齐到自己的最大对齐数的整数倍处结构体的整体大小就是所有最大对齐数含嵌套结构体的对齐数的整数倍。例子#includeiostreamusingnamespacestd;// 计算⼀下A/B/C实例化的对象是多⼤classA{public:voidPrint(){cout_chendl;}private:char_ch;int_i;};classB{public:voidPrint(){//...}};classC{};intmain(){A a;B b;C c;coutsizeof(a)endl;coutsizeof(b)endl;coutsizeof(c)endl;return0;}1. A的大小classA{public:voidPrint(){cout_chendl;}// 成员函数不占对象空间private:char_ch;// 1 字节int_i;// 4 字节};内存布局_ch 在偏移 0占 1 字节。_i 要对齐到 4 的整数倍所以偏移 4占 4 字节。偏移 1~3 是填充字节。总大小 4 4 8最大对齐数是 48 是 4 的整数倍。所以 sizeof(A) 8。2. B的大小classB{public:voidPrint(){/* ... */}};B 没有任何非静态数据成员只有一个普通成员函数。成员函数不占对象空间所以 B 仍然是一个空类。空类对象大小至少为 1通常就是 1。所以 sizeof(B) 1。3. C的大小classC{};C 是空类大小同样至少为 1。所以 sizeof( c ) 1。我们看到没有成员变量的B和C类对象的大小是1为什么没有成员变量还要给1个字节呢因为如果一个字节都不给怎么表示对象存在过呢所以这里给1字节纯粹是为了占位标识对象存在。2.2.3 为什么存在内存对齐?1. 平台原因(移植原因)不是所有的硬件平台都能访问任意地址上的任意数据的某些硬件平台只能在某些地址处取某些特定类型的数据否则抛出硬件异常。2. 性能原因数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于为了访问未对齐的内存处理器需要作两次内存访问而对齐的内存访问仅需要一次访问。假设一个处理器总是从内存中取8个字节则地址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对齐成8的倍数那么就可以用一个内存操作来读或者写值了。否则我们可能需要执行两次内存访问因为对象可能被分放在两个8字节内存块中。总体来说结构体的内存对齐是拿空间来换取时间的做法。3. this指针3.1 引入#includeiostreamusingnamespacestd;classDate{public:voidInit(intyear,intmonth,intday){_yearyear;_monthmonth;_dayday;}voidPrint(){cout_year/_month/_dayendl;}private:int_year;int_month;int_day;};intmain(){Date d1;d1.Init(2026,9,25);d1.Print();Date d2;d2.Init(2026,10,1);d2.Print();return0;}上面的代码中Date类中有 Init 与 Print 两个成员函数函数体中没有关于不同对象的区分那当d1调用Init和Print函数时该函数是如何知道应该访问的是d1对象还是d2对象呢那么这里就要看到C给了一个隐含的this指针解决这里的问题。3.2 this指针的定义编译器编译后类的成员函数默认都会在形参第一个位置增加一个当前类类型的指针叫做this指针。比如Date类的Init的真实原型为voidInit(Date*constthis,intyear,intmonth,intday);编译器背后的处理#includeiostreamusingnamespacestd;classDate{public:/* 编译器实际上把成员函数看成这样 void Init(Date* const this, int year, int month, int day) { this-year year; this-month month; this-day day; }*/voidInit(intyear,intmonth,intday){_yearyear;_monthmonth;_dayday;}/* 编译器处理 void Print(Data* this) { cout _year / _month / _day endl; }*/voidPrint(){cout_year/_month/_dayendl;}private:int_year;int_month;int_day;};intmain(){Date d1;d1.Init(2026,9,25);//d1.Init(d1,2026, 9, 25);编译器处理d1.Print();//d1.Print(d1)编译器处理Date d2;d2.Init(2026,10,1);//d2.Init(d2,2026, 10, 1);编译器处理d2.Print();//d2.Print(d2)编译器处理return0;}类的成员函数中访问成员变量本质都是通过this指针访问的如Init函数中给_year赋值this-_yearyear;所以你的函数的第一个参数不是你那个year也不是有的函数没有参数他们都有一个隐含的参数this,注意这个const修饰的是this指针本身。所有的非静态的成员函数都会增加这么一个隐含的this指针。静态的成员函数以后会学习。d1和d2都调用的是Print但是因d1.Print(d1)把d1传给了thisd2.Print(d2)把d2传给了this,这样就实现了访问内容的不同从而打印出不同的内容。3.3 this指针的注意事项C规定不能在实参和形参的位置显示的写this指针(编译时编译器会处理)但是可以在函数体内显示使用this指针。但是目前不会这么写在后期的学习中你会发现在少数情况下需要。例子classA{public:voidf(intx){// 不能写成 void f(A* this, int x)// ...}};A a;a.f(1);// 编译器自动处理成类似 f(a, 1)函数体内可以显式使用 thisvoidInit(intyear,intmonth,intday){this-_yearyear;this-_monthmonth;this-_dayday;}3.4 this指针的特点非 const 成员函数中this的值是不可以修改的this的指向的内容是可以被修改。1. this 的值不能改this 本身是编译器隐式提供的指针不能重新赋值。classA{public:voidf(){// this nullptr; // 错误不能修改 this 的值}private:intx;};普通成员函数中this 的类型相当于A*constthis;// 指针本身是常量所以 this 不能指向别的对象2. this 指向的内容可以改在非 const 成员函数中可以通过 this 修改对象的成员voidf(){this-x10;// 可以修改指向对象的成员(*this).x20;// 同上}因为此时 this 指向的对象不是 const 的。3. 在 const 成员函数中连内容也不能const 成员函数中this 的类型相当于constA*constthis;既不能改 this 的值也不能通过 this 修改对象成员。voidg()const{// this-x 10; // const 成员函数不能修改成员}3.5 this指针的存储位置this 指针不是存储在对象里面的对象里没有它的位置。它本质上是非静态成员函数的一个隐藏参数在每次调用成员函数时由编译器自动传递。所以问“this 指针存储在哪里”准确答案是它通常作为函数调用的一个隐藏参数存放在寄存器或栈帧中具体取决于编译器和调用约定。它不是对象的成员不占对象空间。3.6 常考易错面试题1下面程序编译运行结果是A、编译报错 B、运行崩溃 C、正常运行#includeiostreamusingnamespacestd;classA{public:voidPrint(){coutA::Print()endl;}private:int_a;};intmain(){A*pnullptr;p-Print();return0;}分析A*pnullptr;p-Print();p 是空指针但调用的是成员函数 Print()。Print() 内部只输出字符串没有访问任何成员变量比如 _a。成员函数调用时编译器会隐式传入 this 指针此时 this nullptr。但函数体内没有通过 this 去访问对象成员所以没有解引用空指针不会触发崩溃。因此程序可以正常输出A::Print()本质上p-Print()大致被编译器处理为A::Print(p);// 传入的 this 是 nullptr由于 Print 不依赖 this所以能正常执行。但是这其实是未定义行为UB。标准并不保证空指针调用成员函数一定安全只是这个特定例子中函数没有使用 this大多数编译器会正常运行。如果 Print 里访问了 _a比如voidPrint(){cout_aendl;// 访问成员解引用 this}那么就会因为 this 是空指针而崩溃。2下面程序编译运行结果是A、编译报错 B、运行崩溃 C、正常运行#includeiostreamusingnamespacestd;classA{public:voidPrint(){coutA::Print()endl;cout_aendl;}private:int_a;};intmain(){A*pnullptr;p-Print();return0;}分析A*pnullptr;p-Print();p 是空指针调用 Print() 时编译器隐式传入的 this 就是 nullptr。进入 Print() 后coutA::Print()endl;// 这一句正常输出cout_aendl;// 这里崩溃_a 是成员变量实际访问时会被编译器处理成this-_a也就是(*this)._a此时 this nullptr所以相当于nullptr-_a即解引用空指针试图访问地址 0 附近的内存。所以编译正常运行崩溃。第一行 A::Print() 可能输出然后访问 _a 时因空指针解引用而崩溃。4. C与封装规范管理面向对象三大特性封装、继承、多态下面的对比我们可以初步了解一下封装。4.1 C和C实现栈的区别通过下面两份代码对比我们发现C实现Stack形态上还是发生了挺多的变化底层和逻辑上没啥变化。c语言的栈C的栈底层的数据结构动态数组和核心逻辑判满、扩容、压栈、出栈完全没变变的是代码的组织形态从面向过程转向面向对象。C中数据和函数都放到了类里面通过访问限定符进行了限制不能再随意通过对象直接修改数据这是C封装的一种体现这个是最重要的变化。这里的封装的本质是一种更严格规范的管理避免出现乱访问修改的问题。当然封装不仅仅是这样的我们后面还需要不断的去学习。C中有⼀些相对方便的语法比如Init给的缺省参数会方便很多成员函数每次不需要传对象地址因为this指针隐含的传递了方便了很多使用类型不再需要typedef用类名就很方便在我们这个C入门阶段实现的Stack看起来变了很多但是实质上变化不大。等着我们后面看STL中的用适配器实现的Stack大家再感受C的魅力。今天的学习我们完成了一次从 C 语言“自由荒野” 到 C “规范王国” 的跨越。表面上我们只是在 C 语言的基础上给数据和函数套上了一层 class 的壳用 private 加了一把锁用 this 指针省去了传参。但实质上这是我们编程思维的一次重大蜕变——从“面向过程”走向“面向对象”的第一课。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →