☰
C++2:类和对象(上)
2026/10/8 8:16:58 网站建设 项目流程

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++ 主要是通过三个**“访问限定符”**来实现这种权限管理的:

  1. public(公有):修饰的成员在类的外部可以直接被访问。通常我们会把成员函数设为 public,作为类与外界交互的“公开服务窗口”。
  2. private(私有):修饰的成员在类的外部绝对不能直接访问,只能在类的内部(也就是成员函数里)被使用。通常我们会把**成员变量(数据)**设为 private,藏起来保护好。
  3. 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 实例化的概念(图纸与盖房子)

用类类型创建对象的过程,称为类的实例化。

很多初学者容易混淆“定义类”和“定义对象”,其实我们可以用盖房子来完美类比:

  1. 类是抽象的图纸:class Date { ... };只是设计规范,告诉计算机这个类长什么样,但它不占用物理内存空间。
  2. 对象是真实的实体:在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语言实现的痛点:

  1. 调用繁琐:每次调用函数,都必须手动传入结构体的指针(&st)。
  2. 数据极不安全:数据完全暴露,任何人都可以绕过函数直接修改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++带来的核心改变:

  1. 调用极简(this 指针的功劳):不需要再手动传地址,直接st.Push(10)即可,编译器在底层会通过this指针自动绑定当前对象。
  2. 安全隔离(封装的本质):把不想让别人碰的核心数据设为private,把允许外部使用的标准操作设为public。强制使用者必须按照既定规则操作,大大降低了出错的概率。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询