C++ 2:C++ 类和对象(上)
- 1. 类和对象
- 1.1 类定义格式
- 1.1 补充:灵魂拷问 —— 既然能在 main 里面直接写 int year,为什么非要搞个类?
- 1.2 访问限定符(public / private / protected)
- 1.3 类域
- 2. 实例化
- 2.1 实例化的概念(图纸与盖房子)
- 2.2 对象大小的计算(高频面试题)
- 3. this 指针 (类的幕后英雄)
- 3.4 灵魂测试题:this 指针避坑
- 4. C++和C语言实现Stack(栈)对比
- 4.1 C语言实现:数据与方法分离
- 4.2 C++实现:数据与方法绑定
1. 类和对象
在 C 语言中,我们经常使用struct(结构体)把多个不同类型的变量打包在一起。到了 C++,为了实现真正的“面向对象”,我们不仅能打包变量,还能把操作这些变量的函数一起打包进去。
在 C++ 中,最常用来定义这种结构的关键字就是class(类)。
1.1 类定义格式
基本语法格式:
classClassName{// 类体:由成员函数(方法)和成员变量(属性)组成};// ⚠️ 极其致命的新手坑:大括号后面的分号绝不能漏掉!class是定义类的关键字,ClassName是你给类起的名字。- 类体中包含的内容称为类的成员:
- 变量称为类的属性或成员变量。
- 函数称为类的方法或成员函数。
代码实战:定义一个最简单的日期类
classDate{// 成员函数(类的方法)voidInit(intyear,intmonth,intday){// 将外界传入的形参值(比如 year = 2026),赋值给类自己的成员变量 _year_year=year;_month=month;_day=day;}// 成员变量(类的属性)/* 💡 避坑指南:为什么要加下划线 `_`? 如果不加 `_`,成员变量也叫 year,那么在 Init 函数里写成 `year = year;` 时, 编译器会就近匹配,认为这是“形参 year 赋值给形参 year”。 结果就是类的成员变量根本没被修改,还是个随机值! 加上 `_` 就能清晰区分:_year(成员变量) = year(局部形参)。 */int_year;int_month;int_day;};1.1 补充:灵魂拷问 —— 既然能在 main 里面直接写 int year,为什么非要搞个类?
很多刚学完 C 语言的朋友都会有这个疑惑:写单个孤立的数字,直接在main函数里写个int year = 2026;不香吗?为什么非要搞个复杂的类把它包起来?
引入“类”主要解决在开发中大型项目时的两个致命痛点:
痛点 1:现实世界的数据往往是“捆绑”在一起的
一个日期从来不只是一个孤立的年份,它是“年、月、日”三个数据的组合。
如果在main里面存 10 个人的生日,你需要写 30 个零散的变量(year1, month1, day1...),代码瞬间就会乱成一锅粥,甚至容易张冠李戴(把张三的年配上了李四的月)。
而用了类,你只需要:
Date d1;// 张三的生日,年月日全打包在里面了Date d2;// 李四的生日痛点 2:极其关键的“数据安全与拦截校验”
如果直接在main里写普通变量,它是完全不设防的。你写出month = 13; day = 32;,编译器根本不会管你,但现实中哪有 13 月 32 号?
把变量放进“类”里面,配合函数,我们就可以在入口处设卡拦截:
classDate{voidInit(intyear,intmonth,intday){// 在类的内部进行合法性检查,把错误数据直接拦在门外!if(month<1||month>12||day<1||day>31){cout<<"非法日期,设置失败!"<<endl;return;}_year=year;_month=month;_day=day;}int_year;int_month;int_day;};一句话总结:类就像是一个带有保安(成员函数)的保险箱(成员变量),既把相关的东西整齐地收纳在一起,又防止外界乱改数据。
1.2 访问限定符(public / private / protected)
前面我们提到,为了保护数据安全,类内部可以设卡拦截。C++ 主要是通过三个**“访问限定符”**来实现这种权限管理的:
public(公有):修饰的成员在类的外部可以直接被访问。通常我们会把成员函数设为 public,作为类与外界交互的“公开服务窗口”。private(私有):修饰的成员在类的外部绝对不能直接访问,只能在类的内部(也就是成员函数里)被使用。通常我们会把**成员变量(数据)**设为 private,藏起来保护好。protected(保护):在目前的初学阶段,你可以把它当成和private一样。它真正的作用要等到以后学习“继承”时才会体现。
访问限定符的作用范围:
从该访问限定符出现的位置开始,直到下一个访问限定符出现为止。如果后面没有其他限定符了,就一直管到类结束的}。
外界不能直接碰private变量,但可以通过调用public成员函数去间接操作它!
classDate{public:// 服务窗口:外界可以调用它来修改数据voidInit(intyear,intmonth,intday){_year=year;// 类内部毫无限制,随便改_month=month;_day=day;}private:// 核心数据int_year;int_month;int_day;};intmain(){Date d;d.Init(2026,9,20);// ✅ 正确:通过 public 服务窗口操作数据// d._year = 2026; // ❌ 错误:外部想直接修改 private 数据,被编译器拒绝!return0;}🏦 秒懂比喻:
private就是银行金库,你不能自己冲进去改余额。public就是柜台柜员,你想存取款,必须按规矩通过柜员来办。关于
protected(保护):现阶段完全把它当成private看待即可。它的作用要等学了“继承”才会用到(外人不能碰,但自家人/子类可以碰)。
💡 面试 / 期末考试高频考点:class和struct的默认权限区别?
如果你在定义类的时候,什么限定符都不写:
- 在
class中,默认的访问权限是private(私有)。它天生比较保守。- 在
struct中,为了兼容以前的 C 语言,默认的访问权限是public(公有)。
1.3 类域
类不仅打包了数据和方法,它本身还定义了一个全新的作用域(就像我们之前学的namespace一样)。类的所有成员都属于这个“类域”。
最核心的应用场景:声明与定义分离
在实际工程中,我们通常把类的声明写在头文件里,把类的函数具体实现写在外部。
当你在类外面写函数实现时,必须使用作用域解析符::,指明这个函数是属于哪个类的。否则编译器会把它当成一个普通的全局函数。
代码实战:声明、实现与使用
#include<iostream>usingnamespacestd;classDate{public:// 1. 在类内部只写函数的声明voidInit(intyear,intmonth,intday);private:int_year;int_month;int_day;};// 2. 在类外部写具体的实现,必须加上 Date:: 告诉编译器这是 Date 类的专属函数voidDate::Init(intyear,intmonth,intday){_year=year;_month=month;_day=day;}// 3. main 函数中的调用方式intmain(){Date d;// 哪怕实现写在了外面,调用的语法依然完全不变d.Init(2026,9,20);return0;}💡 避坑点:
如果在外面实现时忘了写Date::,编译器就会认为你在写一个名叫Init的普通全局函数。紧接着它就会报错:“我不认识_year、_month这些变量”,因为没有Date::领路,它根本进不去类的地盘。
2. 实例化
2.1 实例化的概念(图纸与盖房子)
用类类型创建对象的过程,称为类的实例化。
很多初学者容易混淆“定义类”和“定义对象”,其实我们可以用盖房子来完美类比:
- 类是抽象的图纸:
class Date { ... };只是设计规范,告诉计算机这个类长什么样,但它不占用物理内存空间。 - 对象是真实的实体:在
main函数中写下Date d;,编译器才会真正按照图纸在内存中为它开辟空间,这就叫实例化(相当于拿着图纸把房子盖出来了)。
intmain(){Date d1;// 实例化出第 1 个对象(盖了第 1 栋房子)Date d2;// 实例化出第 2 个对象(盖了第 2 栋房子)// d1 和 d2 各自拥有独立的变量空间,互不影响d1.Init(2026,9,20);d2.Init(2027,1,1);return0;}2.2 对象大小的计算(高频面试题)
既然实例化出来的对象会在内存中占空间,那一个对象到底有多大?我们来看看下面这个类:
classDate{public:voidInit(intyear,intmonth,intday);voidPrint();private:int_year;int_month;int_day;};这里面有 3 个int变量和 2 个函数。我们知道 3 个int是 12 字节,那函数占空间吗?
👉 类对象的大小,只计算“成员变量”,不计算“成员函数”。
(注:成员变量的计算需要遵循内存对齐规则,和 C 语言的 struct 完全一样)
🤔 为什么不把函数算进去?
想象一下,开发商照着同一套图纸在小区里盖了 100 栋别墅(实例化了 100 个对象)。
每栋别墅里都必须有自己独立的卧室、客厅(成员变量,大家的数据各不相同)。但是,小区只需要建一个公共的篮球场(成员函数),让所有业主共用就可以了,完全没必要在每一栋别墅里都塞一个一模一样的篮球场!
在 C++ 底层,成员函数被统一放在了公共代码段,所有对象共享调用这同一份代码,以此来极大地节省内存。
⚠️ 终极陷阱:空类的大小是多少?
classA{// 空空如也,连成员变量都没有};如果用sizeof(A)去计算,结果是 0 吗?
答案是:1 个字节。
这 1 个字节不存储任何有效数据,仅仅是为了占位,向编译器证明“这个对象确实在内存中存在过”,让它能拥有一个独一无二的内存地址。
3. this 指针 (类的幕后英雄)
既然多个对象共享同一份成员函数,那执行d1.Init(...)时,函数是怎么精准修改d1的变量,而不会错改d2的呢?
核心重点:编译器的“暗箱操作”
C++ 编译器在底层给每一个“非静态成员函数”都偷偷增加了一个隐藏的指针参数,名叫this。当你调用函数时,编译器会自动把当前对象的地址传给这个this指针。
代码实战:揭秘底层逻辑
classDate{public:// 【你写的代码】voidInit(intyear,intmonth,intday){_year=year;_month=month;_day=day;}/* 【编译器真实转换的伪代码】 // 偷偷在第一个位置加了 Date* const this 这个参数 void Init(Date* const this, int year, int month, int day) { this->_year = year; // 靠 this 指针精准定位当前对象 this->_month = month; this->_day = day; } */private:int_year;int_month;int_day;};intmain(){Date d1;// 【你写的代码】d1.Init(2026,9,20);/* 【编译器真实调用的伪代码】 d1.Init(&d1, 2026, 9, 20); // 偷偷把 d1 的地址当做 this 传了进去 */return0;}3.4 灵魂测试题:this 指针避坑
下面通过选择题测试一下前面的知识学得如何?
1. 下面程序编译运行结果是( )
A、编译报错 B、运行崩溃 C、正常运行
#include<iostream>usingnamespacestd;classA{public:voidPrint(){cout<<"A::Print()"<<endl;}private:int_a;};intmain(){A*p=nullptr;p->Print();return0;}答案:C、正常运行
很多初学者看到p = nullptr;然后p->Print();,第一反应肯定是“空指针解引用,肯定运行崩溃!”(选B)。但这其实是个非常经典的陷阱。
结合我们前面讲的底层逻辑:成员函数是不存在对象里面的。
执行p->Print();时,编译器并没有去p指向的内存里找函数,而是去公共代码段调用函数,并仅仅把指针p的值(nullptr)作为实参,传递给了隐藏的this指针。
也就是说,进入Print()函数后,this确实等于nullptr。
但是,重点来了:在Print()函数内部,只执行了一句普通的cout打印,并没有访问任何类的成员变量。既然没有通过this指针去访问或修改任何数据(没有解引用行为),自然就不会引发空指针崩溃。程序会正常打印出A::Print()并结束。
2. 下面程序编译运行结果是( )
A、编译报错 B、运行崩溃 C、正常运行
#include<iostream>usingnamespacestd;classA{public:voidPrint(){cout<<"A::Print()"<<endl;cout<<_a<<endl;}private:int_a;};intmain(){A*p=nullptr;p->Print();return0;}答案:B、运行崩溃
这题是上一题的“致命进阶版”。
和上一题一样,p->Print();调用本身不会引起崩溃,this指针默默接收了传入的nullptr。
但是,当程序执行到Print()函数内部的cout << _a << endl;时出事了。编译器底层会把这句话翻译为:cout << this->_a << endl;。
此时this是空指针,试图去访问(解引用)空指针指向的成员变量_a,直接引发空指针解引用,导致程序运行崩溃。
💡 核心总结:
指针为空时调用成员函数,并不一定会崩溃。崩溃的真正原因在于:函数内部是否通过this指针访问了成员变量。
3. this指针存在内存哪个区域的 ()
A. 栈 B. 堆 C. 静态区 D. 常量区 E. 对象里面
答案:A、栈
对象的sizeof并没有把this指针算进去
this指针的本质,其实就是成员函数的一个隐藏形参。既然是函数的参数,它就和普通的局部变量一样,在函数被调用时存放在内存的**栈区(Stack)**中。当函数执行结束,栈帧销毁,this指针也就随之销毁了。
4. C++和C语言实现Stack(栈)对比
通过对比 C 语言和 C++ 实现栈的代码,可以直观感受到面向对象三大特性之一——**“封装”**的核心优势。
4.1 C语言实现:数据与方法分离
在 C 语言中,数据(结构体)和操作数据的方法(函数)是完全分开的。
// C语言结构体定义typedefstructStack{int*a;inttop;intcapacity;}ST;// C语言函数声明voidSTInit(ST*ps);voidSTPush(ST*ps,intx);intmain(){ST st;STInit(&st);// 必须手动传入对象的地址STPush(&st,10);st.top=-1;// ❌ 致命缺陷:外部可以毫无阻碍地随意篡改核心数据return0;}C语言实现的痛点:
- 调用繁琐:每次调用函数,都必须手动传入结构体的指针(
&st)。 - 数据极不安全:数据完全暴露,任何人都可以绕过函数直接修改
top等关键控制变量,极易引发程序崩溃。
4.2 C++实现:数据与方法绑定
C++ 通过class将数据和函数打包在一起,并通过public/private实现了严格的权限控制。
classStack{public:// 公开的接口服务voidInit(intn=4){_a=(int*)malloc(sizeof(int)*n);_capacity=n;_top=0;}voidPush(intx){_a[_top++]=x;}private:// 核心数据被严格保护int*_a;int_top;int_capacity;};intmain(){Stack st;st.Init();// 自动传递 this 指针,调用极简st.Push(10);// st._top = -1; // ❌ 编译报错:private 成员禁止外部直接访问,从根源斩断误操作!return0;}C++带来的核心改变:
- 调用极简(this 指针的功劳):不需要再手动传地址,直接
st.Push(10)即可,编译器在底层会通过this指针自动绑定当前对象。 - 安全隔离(封装的本质):把不想让别人碰的核心数据设为
private,把允许外部使用的标准操作设为public。强制使用者必须按照既定规则操作,大大降低了出错的概率。