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(intn=4){_a=(int*)malloc(sizeof(int)*n);if(nullptr==_a){perror("malloc申请空间失败");return;}_capacity=n;_top=0;}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的成员默认是public,class的成员默认是private。
// C语言的struct用法——需要typedeftypedefstructListNodeC{structListNodeC*next;intval;}LTNode;// C++的struct用法——不用typedef,可以直接当类型用structListNodeCPP{voidInit(intx){next=nullptr;val=x;}ListNodeCPP*next;intval;};虽然struct也能用,但在实际开发中,推荐用class定义类,语义更清晰。
二、访问限定符:封装的第一道防线
C++提供了三种访问限定符来实现封装:
| 限定符 | 访问范围 | 使用场景 |
|---|---|---|
| public | 类内外都能访问 | 对外接口(成员函数) |
| private | 仅类内部可访问 | 成员变量、内部辅助函数 |
| protected | 类内部和子类可访问 | 继承场景中使用 |
关键规则:
- 访问权限从当前限定符开始,到下一个限定符或类结束为止。
- class定义类时,默认访问权限是private;struct默认是public。
- 通常成员变量设为private或protected,对外暴露的接口函数设为public。
classDate{public:voidInit(intyear,intmonth,intday){_year=year;_month=month;_day=day;}private:int_year;int_month;int_day;};这样做的好处是:外部代码无法直接修改对象的内部数据,只能通过我们提供的public接口来操作,数据的安全性得到了保障。
三、类域:不要把成员函数写成全局函数
类定义了一个新的作用域,所有成员都在这个作用域内。如果要在类体外定义成员函数,需要用 “::” 作用域操作符指明它属于哪个类:
classStack{public:voidInit(intn=4);private:int*_a;size_t _capacity;size_t _top;};// 声明和定义分离,需要指定类域voidStack::Init(intn){_a=(int*)malloc(sizeof(int)*n);if(nullptr==_a){perror("malloc申请空间失败");return;}_capacity=n;_top=0;}如果不写Stack::,编译器会把Init当作全局函数,找不到_a等成员变量就会报错。
四、实例化:把设计图变成房子
4.1 什么是实例化?
类是抽象的“设计图”,限定了一个类有哪些成员,但成员变量此时只是声明,并没有分配内存空间。用类在物理内存中创建对象的过程,就叫做实例化。实例化出来的对象,才真正占有内存空间,存储具体的成员变量数据。
classDate{public:voidInit(intyear,intmonth,intday){_year=year;_month=month;_day=day;}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){_year=year;}经过编译器处理后,实际是这样:
voidInit(Date*constthis,intyear,intmonth,intday){this->_year=year;}5.2 this指针的特性
- this指针是隐藏的,你不能在实参和形参的位置显式写出来,但可以在函数体里使用它。
- this的类型是类名* const,即this的指向不能改变。
- 成员函数中访问成员变量,本质都是通过this指针访问。
- 在const成员函数中,this的类型是const 类名* const。
classDate{public:voidInit(intyear,intmonth,intday){// this = nullptr; // 错误!this不能被赋值this->_year=year;// 可以显式使用_month=month;// 编译器自动翻译为 this->_month}private:int_year;int_month;int_day;};5.3 经典面试题
题目1:
classA{public:voidPrint(){cout<<"A::Print()"<<endl;}private:int_a;};intmain(){A*p=nullptr;p->Print();return0;}结果:正常运行。 因为Print函数没有访问任何成员变量,编译器生成调用指令时,函数地址已经确定,不需要解引用p来访问数据,所以不会崩溃。
题目2:
classA{public:voidPrint(){cout<<"A::Print()"<<endl;cout<<_a<<endl;// 访问成员变量}private:int_a;};intmain(){A*p=nullptr;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(intn=4);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()){cout<<s.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指针让对象知道“自己是谁”。理解这些,你就迈入了面向对象编程的大门。
