C++2:类和对象(上)

发布时间:2026/10/8 8:17:01
C++2:类和对象(上) C 2C 类和对象上1. 类和对象1.1 类定义格式1.1 补充灵魂拷问 —— 既然能在 main 里面直接写 int year为什么非要搞个类1.2 访问限定符public / private / protected1.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_yearyear;_monthmonth;_dayday;}// 成员变量类的属性/* 避坑指南为什么要加下划线 _ 如果不加 _成员变量也叫 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(month1||month12||day1||day31){cout非法日期设置失败endl;return;}_yearyear;_monthmonth;_dayday;}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){_yearyear;// 类内部毫无限制随便改_monthmonth;_dayday;}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一样。类的所有成员都属于这个“类域”。最核心的应用场景声明与定义分离在实际工程中我们通常把类的声明写在头文件里把类的函数具体实现写在外部。当你在类外面写函数实现时必须使用作用域解析符::指明这个函数是属于哪个类的。否则编译器会把它当成一个普通的全局函数。代码实战声明、实现与使用#includeiostreamusingnamespacestd;classDate{public:// 1. 在类内部只写函数的声明voidInit(intyear,intmonth,intday);private:int_year;int_month;int_day;};// 2. 在类外部写具体的实现必须加上 Date:: 告诉编译器这是 Date 类的专属函数voidDate::Init(intyear,intmonth,intday){_yearyear;_monthmonth;_dayday;}// 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){_yearyear;_monthmonth;_dayday;}/* 【编译器真实转换的伪代码】 // 偷偷在第一个位置加了 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、正常运行#includeiostreamusingnamespacestd;classA{public:voidPrint(){coutA::Print()endl;}private:int_a;};intmain(){A*pnullptr;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、正常运行#includeiostreamusingnamespacestd;classA{public:voidPrint(){coutA::Print()endl;cout_aendl;}private:int_a;};intmain(){A*pnullptr;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(intn4){_a(int*)malloc(sizeof(int)*n);_capacityn;_top0;}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。强制使用者必须按照既定规则操作大大降低了出错的概率。