C++ 类和对象入门:类的定义、封装、对象大小与 this 指针 C 类和对象入门类的定义、封装、对象大小与 this 指针C 从 C 语言走向面向对象编程第一道真正的门槛就是“类和对象”。很多人刚开始会把class理解成“带函数的结构体”这个说法有一点直观但并不完整。类不仅能把数据和操作放在一起还能通过访问限定符控制哪些内容对外开放哪些细节隐藏在内部。这篇文章从最基础的类定义开始逐步讲清楚类成员、访问限定符、类域、对象实例化、对象大小、内存对齐、this指针以及 C 写法和 C 写法在组织代码上的差异。一、类是什么类可以理解为一种自定义类型用来描述一类对象应该拥有哪些数据以及能执行哪些操作。在类中变量称为成员变量也可以叫属性函数称为成员函数也可以叫方法class是定义类的关键字类定义结束后的分号不能省略。一个简单的栈可以写成这样#includecassert#includecstdio#includecstdlib#includeiostreamclassStack{public:voidInit(intn4){_arraystatic_castint*(std::malloc(sizeof(int)*n));if(_arraynullptr){std::perror(malloc failed);return;}_capacityn;_top0;}voidPush(intx){assert(_array!nullptr);if(_top_capacity){intnew_capacity_capacity0?4:_capacity*2;int*tmpstatic_castint*(std::realloc(_array,sizeof(int)*new_capacity));if(tmpnullptr){std::perror(realloc failed);return;}_arraytmp;_capacitynew_capacity;}_array[_top]x;}intTop(){assert(_top0);return_array[_top-1];}voidPop(){assert(_top0);--_top;}boolEmpty(){return_top0;}voidDestroy(){std::free(_array);_arraynullptr;_top0;_capacity0;}private:int*_array;int_capacity;int_top;};intmain(){Stack st;st.Init();st.Push(1);st.Push(2);std::coutst.Top()std::endl;st.Destroy();return0;}编译运行g stack_demo.cpp-stdc11-ostack_demo ./stack_demo预期输出2这里的_array、_capacity、_top是成员变量Init、Push、Top、Pop、Destroy是成员函数。成员变量前面加_不是语法要求只是一种常见命名习惯用来区分成员变量和普通局部变量。注意这个示例为了贴近入门阶段仍然使用malloc/realloc/free管理空间。真正进入 C 对象管理后会继续学习构造函数、析构函数、new/delete、RAII 等更自然的写法。二、class 和 struct 有什么区别C 兼容 C 中struct的用法同时也增强了struct在 C 中struct里面也可以定义成员函数。structListNode{voidInit(intx){valx;nextnullptr;}intval;ListNode*next;};class和struct都可以定义类最直接的区别是默认访问权限不同写法默认访问权限classprivatestructpublic实际开发中一个简单的数据聚合类型常用struct更强调封装和行为的类型常用class。这不是硬性规定但能让代码意图更清楚。三、访问限定符封装从这里开始C 提供三个访问限定符限定符类外能否直接访问说明public可以通常放对外接口private不可以通常放实现细节和成员变量protected不可以主要在继承中体现价值访问限定符的作用范围从它出现的位置开始一直到下一个访问限定符出现或者类定义结束。classDate{public:voidInit(intyear,intmonth,intday){_yearyear;_monthmonth;_dayday;}voidPrint(){std::cout_year/_month/_daystd::endl;}private:int_year;int_month;int_day;};这里_year、_month、_day被放在private区域类外不能直接修改只能通过Init、Print这样的公开成员函数使用。很多初学者会问既然成员变量最终还是要被函数访问为什么不直接设成public原因在于封装。成员变量直接暴露后外部代码可以随意改对象状态很容易绕过必要检查。成员函数作为接口可以控制修改规则让对象始终保持合理状态。四、类域类也有自己的作用域类定义了一个新的作用域。成员函数如果在类外定义需要使用类名::函数名指明它属于哪个类。classStack{public:voidInit(intn4);private:int*_array;int_capacity;int_top;};voidStack::Init(intn){_arraystatic_castint*(std::malloc(sizeof(int)*n));if(_arraynullptr){std::perror(malloc failed);return;}_capacityn;_top0;}如果写成下面这样voidInit(intn){_capacityn;}编译器会把它当成普通全局函数。全局函数里找不到_capacity这个成员变量自然会报错。所以类外定义成员函数时Stack::不是装饰而是在告诉编译器这个函数属于Stack的类域。五、实例化类只是设计图对象才占空间类本身只是对对象的抽象描述它规定对象有哪些成员变量和成员函数。真正占用内存的是通过类创建出来的对象这个过程叫实例化。Date d1;Date d2;d1.Init(2024,3,31);d2.Init(2024,7,5);可以把类理解成设计图把对象理解成按照设计图建出来的房子。设计图规定房间如何布局但设计图本身不能住人对象才真正拥有自己的数据空间。d1和d2都是Date类型对象它们有相同的成员函数但成员变量各自独立Date 类d1 对象d2 对象_year/_month/_day 一份数据_year/_month/_day 另一份数据Init/Print 代码只有一份这里有一个关键点对象中只存储成员变量不会为每个对象都存一份成员函数代码。成员函数编译后是指令存放在代码区域。不同对象调用同一个成员函数时函数代码是同一份只是传入的对象不同。六、对象大小成员变量、内存对齐和空类一个类对象大小主要由成员变量决定同时要满足内存对齐规则。#includeiostreamclassA{public:voidPrint(){std::cout_chstd::endl;}private:char_ch;int_i;};classB{public:voidPrint(){}};classC{};intmain(){std::coutsizeof(A)std::endl;std::coutsizeof(B)std::endl;std::coutsizeof(C)std::endl;return0;}A的大小不是简单的1 4 5因为要考虑内存对齐。常见平台上结果可能是 8。内存对齐可以简单理解为第一个成员从偏移量 0 开始后续成员要放在满足对齐要求的位置对象整体大小通常要对齐到最大对齐数的整数倍具体对齐数和编译器、平台设置有关。B和C没有成员变量但对象大小通常是 1。原因是对象必须有地址如果大小为 0就无法区分两个不同对象是否真实存在。这个 1 字节主要用于占位。七、this 指针成员函数如何知道操作哪个对象看下面这个类classDate{public:voidInit(intyear,intmonth,intday){_yearyear;_monthmonth;_dayday;}voidPrint(){std::cout_year/_month/_daystd::endl;}private:int_year;int_month;int_day;};如果有两个对象Date d1;Date d2;d1.Init(2024,3,31);d2.Init(2024,7,5);Init函数代码只有一份那它怎么知道当前要修改的是d1还是d2答案就是this指针。成员函数在调用时会隐式接收一个指向当前对象的指针。可以粗略理解成编译器把成员函数处理成类似下面的形式// 为了理解而写的示意不是实际源码写法voidInit(Date*constthis,intyear,intmonth,intday){this-_yearyear;this-_monthmonth;this-_dayday;}在成员函数内部_yearyear;本质上等价于this-_yearyear;this有几个特点this只能在成员函数内部使用调用成员函数时编译器自动传递当前对象地址不能在实参和形参位置显式写thisthis是类型* const指针本身不能被重新赋值this不存放在对象内部它是成员函数调用时的隐含参数。关于this在内存中的具体位置不要死记成某个固定区域。它作为隐含参数参与函数调用可能通过寄存器传递也可能按调用约定放到栈上取决于平台、编译器和优化设置。更重要的是理解this不是对象的一部分。八、空指针调用成员函数为什么危险有时会看到这样的代码classA{public:voidPrint(){std::coutA::Print()std::endl;}};intmain(){A*pnullptr;p-Print();return0;}某些环境下这段代码看起来可能能正常输出因为Print没有访问成员变量函数体没有真正解引用对象数据。但下面这个版本就危险得多classA{public:voidPrint(){std::coutA::Print()std::endl;std::cout_astd::endl;}private:int_a;};intmain(){A*pnullptr;p-Print();return0;}访问_a本质是通过this-_a访问成员变量而this此时是空指针程序很可能崩溃。更稳妥的结论是不要通过空指针调用成员函数。即使某些场景下“看起来能跑”也不是可靠写法更不能作为工程代码依赖。九、C 写法和 C 写法对比封装到底改变了什么用 C 写栈时常见形式是结构体加一组操作函数typedefintSTDataType;typedefstructStack{STDataType*a;inttop;intcapacity;}ST;voidSTInit(ST*ps);voidSTPush(ST*ps,STDataType x);voidSTPop(ST*ps);STDataTypeSTTop(ST*ps);intSTEmpty(ST*ps);voidSTDestroy(ST*ps);调用时每个函数都要显式传对象地址ST s;STInit(s);STPush(s,1);STPush(s,2);STDestroy(s);C 写法把数据和操作放进同一个类中classStack{public:voidInit(intn4);voidPush(intx);voidPop();intTop();boolEmpty();voidDestroy();private:int*_array;int_capacity;int_top;};调用时更像是“对象自己执行操作”Stack s;s.Init();s.Push(1);s.Push(2);s.Destroy();从底层逻辑看这两种写法做的事情差别并不大。C 成员函数隐式传递this相当于把 C 写法里反复传入的ST* ps隐藏起来了。真正重要的变化在于封装成员变量可以放到private中外部不能随便改对外只暴露public接口使用者不需要关心内部细节成员函数和成员变量聚合在一起代码组织更清晰默认参数、类域、this指针让调用方式更自然。这也是面向对象的第一层价值把数据和操作绑定起来并通过访问权限管理对象状态。十、常见问题与易错点1. 类定义后面的分号为什么不能省类定义本质上是在定义一种类型语法要求类体结束后必须有分号。少了分号后面的代码会被编译器错误解析。2. 成员函数定义在类里面有什么特点定义在类体内的成员函数默认具有内联属性。注意这只是给编译器的建议不代表一定展开。3.class中不写访问限定符会怎样class默认是privatestruct默认是public。如果类里直接写成员变量但忘了public类外访问时可能报访问权限错误。4. 对象里为什么不存成员函数成员函数编译后是代码指令不需要每个对象都存一份。对象中主要存储成员变量。不同对象调用同一成员函数时通过隐含的this指针区分当前对象。5. 空类为什么大小是 1空类没有成员变量但对象必须能被取地址并区分不同对象。给 1 字节是为了占位表示对象存在。6.this可以被修改吗不能。可以把this理解成类型* const this指针本身不能被重新赋值。但可以通过this修改对象成员前提是成员函数不是const成员函数。十一、总结类和对象是 C 从过程式编程走向面向对象编程的起点。入门阶段需要抓住几条主线类把成员变量和成员函数组织在一起public提供接口private隐藏实现细节类只是类型描述对象实例化后才真正占用内存对象中主要存成员变量成员函数代码只保留一份对象大小受成员变量和内存对齐影响空类大小通常为 1成员函数通过隐含的this指针区分当前对象C 的封装让数据修改更受控代码组织也更清晰。把这些基础理解透再继续学习构造函数、析构函数、拷贝构造、运算符重载和继承时就不会只是在背语法而能看懂 C 为什么要这样设计。