news 2026/4/15 11:53:28

【C++ 入门】类和对象(上)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【C++ 入门】类和对象(上)

大家好!今天咱们正式踏入 C++ 的核心 ——类和对象的世界。如果说 C 语言是 “面向过程” 的工具箱,那 C++ 的 “类和对象” 就是把工具打包成 “智能设备”,让代码更贴近现实逻辑。这篇文章先从最基础的 3 个问题入手:对象占多大内存?为什么函数能区分不同对象?对象的 “出生” 和 “死亡” 谁来管?全程带代码和图解,新手也能轻松看懂~

一、对象大小:只装 “数据”,不装 “功能”

刚学类的时候,我总疑惑:类里又有成员变量(比如日期的年 / 月 / 日),又有成员函数(比如打印日期),那实例化一个对象后,它占多大内存呢?难道把函数也一起装进去了?

1.1 关键结论:对象只存储成员变量

其实答案很简单:类对象的大小 = 所有成员变量的大小之和(遵循 C 语言结构体的内存对齐规则),成员函数压根不占对象的空间!

为什么?因为函数编译后是一段 “指令代码”,这些代码会统一存放在内存的代码段(所有对象共用同一份)。如果每个对象都存一份函数,100 个对象就会存 100 份相同的指令,纯属浪费内存!

举个例子就懂了:

cpp

#include <iostream> using namespace std; // 定义一个日期类 class Date { public: // 成员函数:初始化日期 void Init(int year, int month, int day) { _year = year; _month = month; _day = day; } // 成员函数:打印日期 void Print() { cout << _year << "/" << _month << "/" << _day << endl; } private: // 成员变量:年、月、日(只占对象空间) int _year; // 4字节 int _month; // 4字节 int _day; // 4字节 }; int main() { Date d1, d2; // 实例化两个对象 d1.Init(2024, 5, 20); d2.Init(2024, 5, 21); // 打印对象大小:4+4+4=12字节(无内存对齐时) cout << "Date对象大小:" << sizeof(d1) << endl; return 0; }

运行结果:Date对象大小:12(不同编译器对齐规则可能微调,但肯定不包含函数)。

1.2 内存分布图解

为了更直观,画一张内存分布图(建议保存):![图 1:对象与成员函数的内存分布](这里建议配一张示意图,包含以下元素:

  • 栈区:两个 Date 对象 d1、d2,每个对象内只有_year/_month/_day 三个成员变量;
  • 代码段:存放 Date 类的 Init ()、Print () 函数指令,标注 “所有对象共用”;
  • 箭头:d1 调用 Print () 时,指向代码段的 Print () 指令)

二、this 指针:对象的 “专属身份证”

接着上面的例子,d1 和 d2 都调用 Print () 函数,函数怎么知道该打印 d1 的日期,还是 d2 的日期?总不能 “脸盲” 吧?

这就需要 C++ 的隐藏机制 ——this 指针

2.1 this 指针是什么?

编译器会给每个成员函数 “偷偷加一个参数”:当前类类型的指针,名叫 this。它指向当前调用该函数的对象,函数里访问的所有成员变量,本质都是通过 this 指针访问的。

比如我们写的Init函数,编译器会偷偷改成这样(我们看不到,但实际运行是这样):

cpp

// 我们写的代码(无this) void Init(int year, int month, int day) { _year = year; _month = month; _day = day; } // 编译器实际处理的代码(加了this) void Init(Date* const this, int year, int month, int day) { this->_year = year; // 显式通过this访问成员 this->_month = month; this->_day = day; }

而我们调用d1.Init(2024,5,20)时,编译器也会偷偷传参:

cpp

// 我们写的调用代码 d1.Init(2024,5,20); // 编译器实际执行的代码(传d1的地址给this) Init(&d1, 2024, 5, 20);

2.2 this 指针的 3 个关键性质

  1. 不能显式写:不能在函数的形参或实参里写 this(编译器会自己处理),但可以在函数体内显式用(比如this->_year);
  2. 存储位置:通常存在栈区(作为函数参数压栈),部分编译器会优化到寄存器(比如 VS 用 ECX 寄存器),不在对象里,也不在堆 / 静态区
  3. 指向不能改:this 是const指针(比如Date* const this),只能指向当前对象,不能指向其他对象。

2.3 经典易错题:空指针调用成员函数会崩溃吗?

这是面试常考的坑,咱们用两个例子对比,瞬间明白:

例子 1:空指针调用不访问成员的函数

cpp

#include <iostream> using namespace std; class A { public: void Print() { // 只打印字符串,不访问成员变量 cout << "A::Print()" << endl; } private: int _a; // 成员变量 }; int main() { A* p = nullptr; // 空指针 p->Print(); // 调用Print() return 0; }

运行结果:正常打印A::Print(),不崩溃。原因:Print () 不访问成员变量,不需要解引用 this 指针(虽然 this 是 nullptr,但没用到),直接执行代码段的指令即可。

例子 2:空指针调用访问成员的函数

cpp

#include <iostream> using namespace std; class A { public: void Print() { // 访问成员变量_a,本质是this->_a cout << _a << endl; } private: int _a; }; int main() { A* p = nullptr; // 空指针 p->Print(); // 调用Print() return 0; }

运行结果:程序崩溃。原因:Print () 要访问_a,即this->_a,但 this 是 nullptr(空指针),解引用空指针会触发内存访问错误。

三、默认成员函数:对象的 “自动服务”

当我们定义一个类时,即使什么成员函数都不写,编译器也会自动生成 6 个 “默认成员函数”(这篇先讲最常用的 2 个:构造、析构)。它们就像对象的 “自动服务”,负责对象的 “出生初始化” 和 “死亡清理”。

3.1 构造函数:对象的 “出生向导”

为什么需要构造函数?

C 语言里,我们定义结构体后,要手动调用Init函数初始化(比如InitDate(&date, 2024,5,20)),万一忘了调用,成员变量就是随机值。

C++ 的构造函数解决了这个问题:对象实例化时,编译器会自动调用构造函数,完成成员变量的初始化,不用我们手动调。

构造函数的 5 个核心特点
  1. 函数名 = 类名:比如 Date 类的构造函数就叫 Date;
  2. 无返回值:不用写 void,也不用 return 任何值;
  3. 自动调用:创建对象时自动执行,不能手动调用(除非搞特殊操作,不推荐);
  4. 支持重载:可以写多个构造函数,满足不同初始化需求;
  5. 默认生成:如果我们没写构造函数,编译器会自动生成一个 “无参默认构造函数”;一旦我们写了,编译器就不生成了。
3 种 “默认构造函数”(重点!)

“默认构造函数” 指的是不用传实参就能调用的构造函数,有 3 种:

  1. 编译器自动生成的无参构造;
  2. 我们写的无参构造函数
  3. 我们写的全缺省构造函数(所有参数都有默认值)。

⚠️ 注意:这 3 种只能存在一个!否则调用时会有歧义(编译器不知道选哪个)。

代码示例:构造函数的用法

cpp

#include <iostream> using namespace std; class Date { public: // 1. 无参构造函数(默认构造之一) Date() { _year = 2000; _month = 1; _day = 1; } // 2. 全缺省构造函数(默认构造之一) // 注意:如果同时写无参和全缺省,编译报错(歧义) // Date(int year = 2000, int month = 1, int day = 1) { // _year = year; // _month = month; // _day = day; // } // 3. 带参构造函数(非默认,需要传参) Date(int year, int month, int day) { _year = year; _month = month; _day = day; } void Print() { cout << _year << "/" << _month << "/" << _day << endl; } private: int _year; int _month; int _day; }; int main() { Date d1; // 调用无参构造(默认构造) Date d2(2024,5,20);// 调用带参构造 // Date d3(); // 错误!编译器会认为这是函数声明,不是创建对象 d1.Print(); // 输出 2000/1/1 d2.Print(); // 输出 2024/5/20 return 0; }
编译器默认构造的 “小脾气”

如果我们没写构造函数,编译器自动生成的默认构造有个特点:

  • 内置类型(int、char、指针等):不初始化,成员变量是随机值;
  • 自定义类型(比如类、结构体):会调用该自定义类型的默认构造函数。

比如:

cpp

class Time { public: // Time的无参构造 Time() { _hour = 0; _minute = 0; _second = 0; cout << "Time默认构造调用" << endl; } private: int _hour; int _minute; int _second; }; class Date { private: // 内置类型:默认构造不初始化(随机值) int _year; // 自定义类型:默认构造会调用Time的无参构造 Time _t; }; int main() { Date d; // 创建Date对象,会打印"Time默认构造调用" return 0; }

3.2 析构函数:对象的 “资源清理工”

为什么需要析构函数?

如果对象里申请了资源(比如堆内存、文件句柄),对象销毁时不清理,就会造成 “内存泄漏”。

析构函数的作用就是:对象生命周期结束时,自动调用,清理对象申请的资源(不是销毁对象本身,对象本身在栈 / 堆里,由系统回收)。

析构函数的 5 个核心特点
  1. 函数名 = ~ 类名:比如 Date 类的析构函数叫~Date;
  2. 无参数、无返回值:不能重载(一个类只能有一个析构);
  3. 自动调用:对象出作用域(比如 main 函数结束)、delete 对象时,自动执行;
  4. 默认生成:没写析构时,编译器自动生成默认析构;
  5. 清理规则:和默认构造类似 —— 内置类型不处理,自定义类型调用其析构。
什么时候需要自己写析构?

只有当类申请了资源(比如 new、malloc 分配内存)时,才需要手动写析构函数释放资源。如果没有资源申请(比如 Date 类),用编译器默认的就够了。

代码示例:手动写析构函数(以栈为例)

cpp

#include <iostream> using namespace std; class Stack { public: // 构造函数:申请堆内存(资源) Stack(int capacity = 4) { _arr = new int[capacity]; // 申请堆内存 _top = 0; _capacity = capacity; cout << "Stack构造调用" << endl; } // 析构函数:释放堆内存(清理资源) ~Stack() { delete[] _arr; // 释放堆内存 _arr = nullptr; _top = _capacity = 0; cout << "Stack析构调用" << endl; } private: int* _arr; // 堆内存指针(需要清理) int _top; // 内置类型 int _capacity; // 内置类型 }; int main() { Stack s; // 创建Stack对象,调用构造 // main结束时,s出作用域,自动调用析构释放_arr return 0; }

运行结果

plaintext

Stack构造调用 Stack析构调用

四、总结

这篇我们搞懂了类和对象的 3 个核心基础:

  1. 对象大小:只存成员变量,成员函数在代码段共用;
  2. this 指针:隐藏的 “对象身份证”,区分不同对象的调用;
  3. 构造 / 析构:对象的 “自动初始化” 和 “自动清理”,有资源申请才手动写析构。

下一篇我们会深入讲剩下的默认成员函数(拷贝构造、赋值重载), 大家可以先动手敲一敲今天的代码,感受一下对象的 “自动服务” 有多方便~

如果有疑问,欢迎在评论区留言!

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/15 2:16:43

50、Windows Forms开发:多窗体显示与常用控件介绍

Windows Forms开发:多窗体显示与常用控件介绍 在Windows Forms应用程序开发中,我们常常需要创建额外的窗口或窗体来满足不同的需求,同时也会用到各种控件来展示和处理数据。本文将介绍如何显示其他窗体,以及一些常见的数据显示控件。 显示其他窗体 在Windows Forms应用中…

作者头像 李华
网站建设 2026/4/15 11:53:26

【C++ 入门】类和对象(中)

大家好&#xff01;上一篇我们学会了对象的 “出生”&#xff08;构造函数&#xff09;和 “死亡”&#xff08;析构函数&#xff09;&#xff0c;今天咱们聚焦对象的 “日常互动”—— 怎么用已有对象 “克隆” 新对象&#xff1f;怎么让自定义类型像int一样用、做运算&#x…

作者头像 李华
网站建设 2026/3/26 23:39:56

53、无处不在的数据集——DataSet使用全解析

无处不在的数据集——DataSet使用全解析 1. 认识DataSet 在Windows Forms数据绑定中,最常见的数据类型就是DataSet,或者是派生的类型化数据集类。在.NET 2.0中,将数据绑定到自定义对象和集合也非常容易,但DataSet是专门为.NET中的数据绑定而设计的。 DataSet本质上是一个…

作者头像 李华
网站建设 2026/4/13 11:45:58

20、Bison解析器相关技术及SQL语法规则详解

Bison解析器相关技术及SQL语法规则详解 1. 扫描器与错误处理 在扫描器的工作机制中,若未从扫描器返回,前一步骤仅在 yylex 返回并再次被调用时才会被触发。对于最后一条通用规则,它会打印错误信息。在原始的C版本扫描器中,会调用 yyerror ,但由于当前扫描器并非C++解…

作者头像 李华
网站建设 2026/4/15 9:41:37

Kotaemon本地部署教程:30分钟完成全链路配置

Kotaemon本地部署实战&#xff1a;30分钟构建企业级智能问答系统 在企业知识管理日益复杂的今天&#xff0c;员工每天要面对成百上千页的制度文档、操作手册和流程规范。一个常见的场景是&#xff1a;新员工入职第三天&#xff0c;终于鼓起勇气问HR&#xff1a;“我什么时候能…

作者头像 李华
网站建设 2026/4/11 3:45:43

基于Kotaemon的多语言问答系统构建方法

基于Kotaemon的多语言问答系统构建方法 在一家跨国企业的客服中心&#xff0c;每天要处理来自30多个国家的数万条用户咨询——有人用西班牙语问订单状态&#xff0c;有人用日语查退换货政策&#xff0c;还有人用阿拉伯语追问产品兼容性。传统客服机器人面对这种复杂场景往往束手…

作者头像 李华