C++类与对象(上):从struct到class,一文吃透核心概念
文章目录引言一、类的定义从struct到class的升级1.1 类定义格式1.2 struct也能定义类二、访问限定符封装的第一道防线三、类域不要把成员函数写成全局函数四、实例化把设计图变成房子4.1 什么是实例化4.2 对象大小成员函数去哪了4.3 内存对齐规则五、this指针让对象知道“我是谁”5.1 this指针从哪来5.2 this指针的特性5.3 经典面试题六、C语言 vs C实现Stack的对比简介掌握C类与对象的精髓本文面向具备C语言基础、正在向C进阶的开发者系统讲解C类与对象这一核心概念。我们将从C语言的结构体出发对比引入C类所带来的根本性变革——不仅仅是语法升级更是编程范式的跃迁。你将深入理解封装与访问限定符如何构建数据安全的第一道防线类域如何优雅地组织代码告别全局函数的混乱实例化过程与对象内存布局的奥秘this指针的底层机制及其在面向对象设计中的关键作用。通过清晰的代码对比、内存布局分析和经典面试题剖析本文旨在帮助你不仅“会用”类与对象更能“懂其所以然”为后续学习更复杂的面向对象特性打下坚实基础。引言在C编程的世界里类与对象是面向对象编程OOP的基石也是从C语言过程式思维向C面向对象思维转变的关键一步。本文旨在为有一定C语言基础、正在学习C的开发者提供一个清晰、系统的学习路径通过对比C语言的结构体与C的类深入剖析访问限定符、类域、实例化、this指针等核心概念。你将不仅理解C类与对象的语法形式更能掌握其背后的设计哲学封装、数据隐藏和代码组织。文章通过大量代码示例和内存布局分析帮助你从知道怎么写进阶到明白为什么这么写最终能够运用面向对象思想解决实际问题。无论你是准备面试还是希望提升代码设计能力本文都将为你提供实用的知识和思考框架。一、类的定义从struct到class的升级如果你写过C语言的结构体那类对你来说就没什么神秘的。1.1 类定义格式在C中用class关键字定义一个类classStack{public:// 成员函数voidInit(intn4){_a(int*)malloc(sizeof(int)*n);if(nullptr_a){perror(malloc申请空间失败);return;}_capacityn;_top0;}voidPush(intx){// 扩容逻辑..._a[_top]x;}private:// 成员变量int*_a;size_t _capacity;size_t _top;};// 注意这个分号不能省略这里有几个细节需要注意class是定义类的关键字Stack是类名大括号里是类体末尾的分号一定不能少。类体中的变量叫成员变量属性类体中的函数叫成员函数方法。为了区分成员变量通常会在成员变量名前加_或m比如_a、_capacity。这不是语法强制要求只是行业惯例。定义在类里面的成员函数默认是inline的。1.2 struct也能定义类C兼容C语言的所有语法同时把struct也升级成了类。升级后的struct和class最大的区别就一个| struct的成员默认是publicclass的成员默认是private。// C语言的struct用法——需要typedeftypedefstructListNodeC{structListNodeC*next;intval;}LTNode;// C的struct用法——不用typedef可以直接当类型用structListNodeCPP{voidInit(intx){nextnullptr;valx;}ListNodeCPP*next;intval;};虽然struct也能用但在实际开发中推荐用class定义类语义更清晰。二、访问限定符封装的第一道防线C提供了三种访问限定符来实现封装限定符访问范围使用场景public类内外都能访问对外接口成员函数private仅类内部可访问成员变量、内部辅助函数protected类内部和子类可访问继承场景中使用关键规则访问权限从当前限定符开始到下一个限定符或类结束为止。class定义类时默认访问权限是privatestruct默认是public。通常成员变量设为private或protected对外暴露的接口函数设为public。classDate{public:voidInit(intyear,intmonth,intday){_yearyear;_monthmonth;_dayday;}private:int_year;int_month;int_day;};这样做的好处是外部代码无法直接修改对象的内部数据只能通过我们提供的public接口来操作数据的安全性得到了保障。三、类域不要把成员函数写成全局函数类定义了一个新的作用域所有成员都在这个作用域内。如果要在类体外定义成员函数需要用 “::” 作用域操作符指明它属于哪个类classStack{public:voidInit(intn4);private:int*_a;size_t _capacity;size_t _top;};// 声明和定义分离需要指定类域voidStack::Init(intn){_a(int*)malloc(sizeof(int)*n);if(nullptr_a){perror(malloc申请空间失败);return;}_capacityn;_top0;}如果不写Stack::编译器会把Init当作全局函数找不到_a等成员变量就会报错。四、实例化把设计图变成房子4.1 什么是实例化类是抽象的“设计图”限定了一个类有哪些成员但成员变量此时只是声明并没有分配内存空间。用类在物理内存中创建对象的过程就叫做实例化。实例化出来的对象才真正占有内存空间存储具体的成员变量数据。classDate{public:voidInit(intyear,intmonth,intday){_yearyear;_monthmonth;_dayday;}private:int_year;// 声明没有开空间int_month;int_day;};intmain(){Date d1;// 实例化对象分配内存Date d2;// 可以实例化多个对象d1.Init(2024,3,31);d2.Init(2024,7,5);return0;}一个类可以实例化出多个对象每个对象的成员变量各自独立互不干扰4.2 对象大小成员函数去哪了关键问题对象内存里存什么成员函数编译后是一段指令存在代码段不在对象里。如果每个对象都存一份函数指针100个对象就要存100次同样的地址太浪费了。所以C的设计是对象中只存储成员变量成员函数单独放在代码段。空类呢如果一个类没有任何成员变量C会给它分配1字节的内存空间。这不是为了存数据而是为了在内存中标记这个对象“存在过”。classA{int_i;char_ch;};// 8字节考虑内存对齐classB{};// 1字节classC{};// 1字节| 空类的大小是1是为了让不同的对象在内存中有不同的地址避免零大小带来的问 题。4.3 内存对齐规则对象的大小计算遵循内存对齐规则和C语言结构体一样第一个成员在偏移量为0的地址处。其他成员要对齐到对齐数的整数倍地址。对齐数 min(成员大小, 编译器默认对齐数)。VS中默认对齐数为8。结构体总大小为最大对齐数的整数倍。嵌套结构体时需要对齐到自身最大对齐数的整数倍。classA{char_ch;// 1字节int_i;// 4字节从偏移量4开始补3字节};// 总大小4 4 8字节五、this指针让对象知道“我是谁”5.1 this指针从哪来有这样一个问题Date类实例化出d1和d2两个对象它们调用同一个Init函数时函数怎么知道自己处理的是哪个对象的数据答案:是this指针。编译器在编译时会默默给每个成员函数添加一个隐藏参数——当前对象的地址这就是this指针。比如这个成员函数voidInit(intyear,intmonth,intday){_yearyear;}经过编译器处理后实际是这样voidInit(Date*constthis,intyear,intmonth,intday){this-_yearyear;}5.2 this指针的特性this指针是隐藏的你不能在实参和形参的位置显式写出来但可以在函数体里使用它。this的类型是类名* const即this的指向不能改变。成员函数中访问成员变量本质都是通过this指针访问。在const成员函数中this的类型是const 类名* const。classDate{public:voidInit(intyear,intmonth,intday){// this nullptr; // 错误this不能被赋值this-_yearyear;// 可以显式使用_monthmonth;// 编译器自动翻译为 this-_month}private:int_year;int_month;int_day;};5.3 经典面试题题目1classA{public:voidPrint(){coutA::Print()endl;}private:int_a;};intmain(){A*pnullptr;p-Print();return0;}结果正常运行。 因为Print函数没有访问任何成员变量编译器生成调用指令时函数地址已经确定不需要解引用p来访问数据所以不会崩溃。题目2classA{public:voidPrint(){coutA::Print()endl;cout_aendl;// 访问成员变量}private:int_a;};intmain(){A*pnullptr;p-Print();return0;}结果运行崩溃。 因为Print内部访问了_a这会被翻译为this-_a而this是nullptr解引用非法。题目3 this指针存在哪个区域?栈区。 this是一个隐含的形参在调用成员函数时由编译器压栈传递所以存在栈中部分编译器的优化可能会把它放在寄存器中。六、C语言 vs C实现Stack的对比我们把C语言和C实现的Stack放在一起直观感受一下封装带来的变化。C语言实现面向过程typedefstructStack{STDataType*a;inttop;intcapacity;}ST;voidSTInit(ST*ps);voidSTPush(ST*ps,STDataType x);intSTTop(ST*ps);voidSTPop(ST*ps);intSTEmpty(ST*ps);痛点数据和操作函数是分离的维护成本高。用户可以直接修改top等成员变量绕过函数破坏栈的状态。每个函数都必须手动传递结构体指针。C实现面向对象classStack{public:voidInit(intn4);voidPush(STDataType x);voidPop();intTop();boolEmpty();voidDestroy();private:STDataType*_a;size_t _capacity;size_t _top;};intmain(){Stack s;s.Init();s.Push(1);s.Push(2);while(!s.Empty()){couts.Top()endl;s.Pop();}s.Destroy();return0;}C带来的变化数据和操作数据的函数封装在一起逻辑更清晰。成员变量设为private外部无法直接访问必须通过公开接口操作数据安全性大大提高。成员函数不需要手动传对象地址this指针隐式传递代码更简洁。缺省参数让代码更灵活。类名直接作为类型名使用不用再写typedef。虽然C版本在底层逻辑上和C版本没有本质区别但封装带来的代码组织方式的变化让代码更安全、更易于维护和扩展。这就是面向对象的核心优势之一。总结:这篇文章我们梳理了C类和对象的基础知识类的定义用class定义类成员变量和成员函数组织在一起。struct也可以用但默认访问权限是public。访问限定符public对外公开private内部私有protected留给继承。类域成员函数可以在类外定义需要加类名::指明归属。实例化类是设计图对象是房子。对象中只存储成员变量成员函数存在代码段。空类占1字节。内存对齐对象的大小遵循内存对齐规则不是成员变量的简单相加。this指针每个成员函数隐藏的第一个参数指向当前调用的对象。nullptr调用成员函数时如果访问成员变量就会崩溃。封装的优势通过C和C的Stac k对比直观感受了封装带来的安全性和可维护性提升。下一篇文章会讲构造函数、析构函数、拷贝构造函数这些默认成员函数敬请期待。本文总结类把数据和行为绑定在一起访问限定符控制外部访问权限this指针让对象知道“自己是谁”。理解这些你就迈入了面向对象编程的大门。