news 2026/4/20 17:41:40

【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/18 18:48:36

10、SQL 解析器与 Flex 规范详解

SQL 解析器与 Flex 规范详解 1. SQL 解析器代码与 Makefile 首先,我们来看 SQL 解析器的主函数代码: main(int ac, char **av) {extern FILE *yyin;if(ac > 1 && !strcmp(av[1], "-d")) {yydebug = 1; ac--; av++;}if(ac > 1 && (yyin =…

作者头像 李华
网站建设 2026/4/17 22:21:20

一文带你看懂 AI Agent 智能体

摘要 人工智能领域正经历着一场从“生成式AI”向“代理式AI”&#xff08;Agentic AI&#xff09;的历史性范式转移。如果说2022年至2023年是大语言模型&#xff08;LLM&#xff09;展现其惊人知识储备与推理能力的“静态展示期”&#xff0c;那么2024年及其后则标志着智能体&…

作者头像 李华
网站建设 2026/4/17 22:52:43

Kotaemon开源了!一键部署生产级智能问答服务

Kotaemon开源了&#xff01;一键部署生产级智能问答服务 在企业AI落地的浪潮中&#xff0c;一个令人兴奋的消息传来&#xff1a;Kotaemon 正式开源。这不仅是一个新的RAG框架发布&#xff0c;更标志着智能问答系统从“能用”迈向“可靠可用”的关键转折。 过去几年&#xff0…

作者头像 李华
网站建设 2026/4/16 19:16:31

EditPlus v6.1 Build 780 烈火汉化版

软件简介 EditPlus是一个Windows下的文本编辑器&#xff0c;它的功能比较强大&#xff0c;可以用于编写源代码、HTML、PHP、JavaScript等等。 采用多标签式界面&#xff0c;可以同时编辑多个文件。 它还有一些其他的功能&#xff0c;比如文件压缩、FTP功能、搜索和替换功能等…

作者头像 李华
网站建设 2026/4/17 12:47:27

Kotaemon支持动态知识更新,告别静态问答局限

Kotaemon支持动态知识更新&#xff0c;告别静态问答局限 在企业智能服务的演进过程中&#xff0c;一个长期存在的痛点逐渐浮出水面&#xff1a;AI系统明明“学富五车”&#xff0c;却总在关键时刻给出过时甚至错误的答案。比如某员工询问最新的年假政策&#xff0c;AI回答的却是…

作者头像 李华
网站建设 2026/4/18 8:10:32

从Demo到上线:一个Kotaemon项目的生命周期全记录

从Demo到上线&#xff1a;一个Kotaemon项目的生命周期全记录 在企业智能化转型的浪潮中&#xff0c;越来越多团队尝试用大语言模型&#xff08;LLM&#xff09;构建智能客服、知识助手或内部提效工具。但现实往往很骨感&#xff1a;原型阶段表现惊艳的 Demo&#xff0c;一旦接入…

作者头像 李华