C++ 类和对象入门:类的定义、封装、对象大小与 this 指针
C++ 从 C 语言走向面向对象编程,第一道真正的门槛就是“类和对象”。很多人刚开始会把class理解成“带函数的结构体”,这个说法有一点直观,但并不完整。类不仅能把数据和操作放在一起,还能通过访问限定符控制哪些内容对外开放,哪些细节隐藏在内部。
这篇文章从最基础的类定义开始,逐步讲清楚类成员、访问限定符、类域、对象实例化、对象大小、内存对齐、this指针,以及 C 写法和 C++ 写法在组织代码上的差异。
一、类是什么
类可以理解为一种自定义类型,用来描述一类对象应该拥有哪些数据,以及能执行哪些操作。
在类中:
- 变量称为成员变量,也可以叫属性;
- 函数称为成员函数,也可以叫方法;
class是定义类的关键字;- 类定义结束后的分号不能省略。
一个简单的栈可以写成这样:
#include<cassert>#include<cstdio>#include<cstdlib>#include<iostream>classStack{public:voidInit(intn=4){_array=static_cast<int*>(std::malloc(sizeof(int)*n));if(_array==nullptr){std::perror("malloc failed");return;}_capacity=n;_top=0;}voidPush(intx){assert(_array!=nullptr);if(_top==_capacity){intnew_capacity=_capacity==0?4:_capacity*2;int*tmp=static_cast<int*>(std::realloc(_array,sizeof(int)*new_capacity));if(tmp==nullptr){std::perror("realloc failed");return;}_array=tmp;_capacity=new_capacity;}_array[_top++]=x;}intTop(){assert(_top>0);return_array[_top-1];}voidPop(){assert(_top>0);--_top;}boolEmpty(){return_top==0;}voidDestroy(){std::free(_array);_array=nullptr;_top=0;_capacity=0;}private:int*_array;int_capacity;int_top;};intmain(){Stack st;st.Init();st.Push(1);st.Push(2);std::cout<<st.Top()<<std::endl;st.Destroy();return0;}编译运行:
g++ stack_demo.cpp-std=c++11-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){val=x;next=nullptr;}intval;ListNode*next;};class和struct都可以定义类,最直接的区别是默认访问权限不同:
| 写法 | 默认访问权限 |
|---|---|
class | private |
struct | public |
实际开发中,一个简单的数据聚合类型常用struct,更强调封装和行为的类型常用class。这不是硬性规定,但能让代码意图更清楚。
三、访问限定符:封装从这里开始
C++ 提供三个访问限定符:
| 限定符 | 类外能否直接访问 | 说明 |
|---|---|---|
public | 可以 | 通常放对外接口 |
private | 不可以 | 通常放实现细节和成员变量 |
protected | 不可以 | 主要在继承中体现价值 |
访问限定符的作用范围从它出现的位置开始,一直到下一个访问限定符出现,或者类定义结束。
classDate{public:voidInit(intyear,intmonth,intday){_year=year;_month=month;_day=day;}voidPrint(){std::cout<<_year<<"/"<<_month<<"/"<<_day<<std::endl;}private:int_year;int_month;int_day;};这里_year、_month、_day被放在private区域,类外不能直接修改,只能通过Init、Print这样的公开成员函数使用。
很多初学者会问:既然成员变量最终还是要被函数访问,为什么不直接设成public?
原因在于封装。成员变量直接暴露后,外部代码可以随意改对象状态,很容易绕过必要检查。成员函数作为接口,可以控制修改规则,让对象始终保持合理状态。
四、类域:类也有自己的作用域
类定义了一个新的作用域。成员函数如果在类外定义,需要使用类名::函数名指明它属于哪个类。
classStack{public:voidInit(intn=4);private:int*_array;int_capacity;int_top;};voidStack::Init(intn){_array=static_cast<int*>(std::malloc(sizeof(int)*n));if(_array==nullptr){std::perror("malloc failed");return;}_capacity=n;_top=0;}如果写成下面这样:
voidInit(intn){_capacity=n;}编译器会把它当成普通全局函数。全局函数里找不到_capacity这个成员变量,自然会报错。
所以,类外定义成员函数时,Stack::不是装饰,而是在告诉编译器:这个函数属于Stack的类域。
五、实例化:类只是设计图,对象才占空间
类本身只是对对象的抽象描述,它规定对象有哪些成员变量和成员函数。真正占用内存的是通过类创建出来的对象,这个过程叫实例化。
Date d1;Date d2;d1.Init(2024,3,31);d2.Init(2024,7,5);可以把类理解成设计图,把对象理解成按照设计图建出来的房子。设计图规定房间如何布局,但设计图本身不能住人;对象才真正拥有自己的数据空间。
d1和d2都是Date类型对象,它们有相同的成员函数,但成员变量各自独立:
这里有一个关键点:对象中只存储成员变量,不会为每个对象都存一份成员函数代码。
成员函数编译后是指令,存放在代码区域。不同对象调用同一个成员函数时,函数代码是同一份,只是传入的对象不同。
六、对象大小:成员变量、内存对齐和空类
一个类对象大小主要由成员变量决定,同时要满足内存对齐规则。
#include<iostream>classA{public:voidPrint(){std::cout<<_ch<<std::endl;}private:char_ch;int_i;};classB{public:voidPrint(){}};classC{};intmain(){std::cout<<sizeof(A)<<std::endl;std::cout<<sizeof(B)<<std::endl;std::cout<<sizeof(C)<<std::endl;return0;}A的大小不是简单的1 + 4 = 5,因为要考虑内存对齐。常见平台上结果可能是 8。
内存对齐可以简单理解为:
- 第一个成员从偏移量 0 开始;
- 后续成员要放在满足对齐要求的位置;
- 对象整体大小通常要对齐到最大对齐数的整数倍;
- 具体对齐数和编译器、平台设置有关。
B和C没有成员变量,但对象大小通常是 1。原因是对象必须有地址,如果大小为 0,就无法区分两个不同对象是否真实存在。这个 1 字节主要用于占位。
七、this 指针:成员函数如何知道操作哪个对象
看下面这个类:
classDate{public:voidInit(intyear,intmonth,intday){_year=year;_month=month;_day=day;}voidPrint(){std::cout<<_year<<"/"<<_month<<"/"<<_day<<std::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->_year=year;this->_month=month;this->_day=day;}在成员函数内部:
_year=year;本质上等价于:
this->_year=year;this有几个特点:
this只能在成员函数内部使用;- 调用成员函数时,编译器自动传递当前对象地址;
- 不能在实参和形参位置显式写
this; this是类型* const,指针本身不能被重新赋值;this不存放在对象内部,它是成员函数调用时的隐含参数。
关于this在内存中的具体位置,不要死记成某个固定区域。它作为隐含参数参与函数调用,可能通过寄存器传递,也可能按调用约定放到栈上,取决于平台、编译器和优化设置。更重要的是理解:this不是对象的一部分。
八、空指针调用成员函数为什么危险
有时会看到这样的代码:
classA{public:voidPrint(){std::cout<<"A::Print()"<<std::endl;}};intmain(){A*p=nullptr;p->Print();return0;}某些环境下,这段代码看起来可能能正常输出,因为Print没有访问成员变量,函数体没有真正解引用对象数据。
但下面这个版本就危险得多:
classA{public:voidPrint(){std::cout<<"A::Print()"<<std::endl;std::cout<<_a<<std::endl;}private:int_a;};intmain(){A*p=nullptr;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(intn=4);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默认是private,struct默认是public。如果类里直接写成员变量但忘了public,类外访问时可能报访问权限错误。
4. 对象里为什么不存成员函数
成员函数编译后是代码指令,不需要每个对象都存一份。对象中主要存储成员变量。不同对象调用同一成员函数时,通过隐含的this指针区分当前对象。
5. 空类为什么大小是 1
空类没有成员变量,但对象必须能被取地址并区分不同对象。给 1 字节是为了占位,表示对象存在。
6.this可以被修改吗
不能。可以把this理解成类型* const this,指针本身不能被重新赋值。但可以通过this修改对象成员,前提是成员函数不是const成员函数。
十一、总结
类和对象是 C++ 从过程式编程走向面向对象编程的起点。入门阶段需要抓住几条主线:
- 类把成员变量和成员函数组织在一起;
public提供接口,private隐藏实现细节;- 类只是类型描述,对象实例化后才真正占用内存;
- 对象中主要存成员变量,成员函数代码只保留一份;
- 对象大小受成员变量和内存对齐影响,空类大小通常为 1;
- 成员函数通过隐含的
this指针区分当前对象; - C++ 的封装让数据修改更受控,代码组织也更清晰。
把这些基础理解透,再继续学习构造函数、析构函数、拷贝构造、运算符重载和继承时,就不会只是在背语法,而能看懂 C++ 为什么要这样设计。