当前位置:首页 > > C语言与CPP编程
[导读]C++语言有时候也拿来写写应用代码,可是居然发现连构造、析构都还没弄明白,把这糟心的概念整理分享一下。 在谈类的构造前,先聊聊面向对象编程与面向过程的个人体会。

[导读] C++语言有时候也拿来写写应用代码,可是居然发现连构造、析构都还没弄明白,把这糟心的概念整理分享一下。

在谈类的构造前,先聊聊面向对象编程与面向过程的个人体会。

面向过程策略

要谈这个问题,先来对比一下传统面向过程的编程策略:

比较典型的–C, Pascal, Basic, Fortran语言,传统的做法是整了很多函数,整合时主要是整合各函数的调用,main函数协调对过程的调用,并将适当的数据作为参数在调用间进行传递流转而实现业务需求。信息流转主要靠:

  • 函数传递参数
  • 函数返回值

如上图,传统面向过程编程语言,比如C语言其编程的主要元为函数,这种语言有这些弊端:

  • 程序主要由函数组成,重用性较差。比如直接将从一个程序源文件中复制一个函数并在另一个程序中重用是非常困难的,因为该函数可能需要包含头文件,引用其他全局变量或者其他函数。换句话说,函数没有很好地封装为独立的可重用单元。相互牵扯、相互耦合比较复杂。
  • 过程编程语言不适合用于解决现实生活中问题的高级抽象。例如,C程序使用诸如if-else,for循环,array,function,pointer之类的结构,这些结构是低级的并且很难抽象出诸如客户关系管理(CRM)系统或足球实况游戏之类的实际问题。(想象一下,使用汇编代码来实现足球实况游戏,酸爽否?显然C语言比汇编好,但还不是足够好)

当然如C语言开发,现在的编程策略已经大量引入了面向对象编程策略了,但是要实现这些对象编程策略,则开发人员本身需要去实现这些面向对象本身的策略,需要自己手撕实现这些基础的对象思想。所以这里仅仅就语言本身特征做为说明,不必纠结。而这些策略如果由编程语言本身去实现,则显然是一个更优异的解决方案。但是比如嵌入式领域为嘛不直接用C++呢,而往往更多采用C的方式采用对象策略来撸代码呢? 嵌入式领域更多需要与底层硬件打交道,而C语言本身抽象层级相对更适合这种场景,结合对象策略编程则可以兼顾重用封装等考量。

回到技术发展历史来看,1970年代初期,美国国防部(DoD)成立了一个工作队,调查其IT预算为何总是失控的原因,其调查结果是:

  • 预算的80%用于软件(其余20%用于硬件)。
  • 超过80%的软件预算用于维护(仅剩余的20%用于新软件开发)。
  • 硬件组件可以应用于各种产品,其完整性通常不会影响其他产品。(硬件可以共享和重用!硬件故障是隔离的!)
  • 软件过程通常是不可共享且不可重用的。软件故障可能会影响计算机中运行的其他程序。

而面向对象编程语言则很好的解决了这些弊端:

  • OOP的基本单元是一个类,该类将静态属性和动态行为封装在一个“黑盒”里,并开放使用这些黑盒的公共接口。由于类相对函数具有更好的封装,因此重用这些类更加容易。换句话说,OOP将同一黑盒中的软件实体的数据结构和算法组合在一起。 良好的类封装不需要去读实现代码,只要知道接口怎么用,实现真正意义上的造砖,哪里需要哪里搬!
  • OOP语言允许更高级别的抽象来解决现实生活中的问题。传统的过程语言例如C需要程序猿根据计算机的结构(例如内存位和字节,数组,决策,循环)进行思考,而不是根据您要解决的问题进行思考。OOP语言(例如Java,C ++,C#)使开发人员可以在问题空间中进行思考,并使用软件对象来表示和抽象问题空间的实体进行建模从而解决问题。 因此OOP会更聚焦于问题域!

面向对象策略

而现代面向对象编程语言(OOP: Object-Oriented Programming) ,从语言本身角度,其编程的场景则变成下面一种截然不一样的画风:

程序的运行态:是不同的实例化对象一起协作干活的场景

应用程序通过对象间消息传递,对象执行自身的行为进而实现业务需求。编写代码,设计类,撰写类的代码,然而应用程序运行时,却是以相应的类实例化的对象协作完成逻辑,这就是所谓面向对象编程的含义。那么对于对象而言,具有哪些属性呢?

  • 对象是一种抽象,所谓抽象就是类。比如MFC中的Window
    • 代表现实世界的实体
    • 类是定义共享公共属性或属性的数据类型
    • 对象是类的实例存在,类本身在程序的运行态并不存在,以对象存在。
  • 对象具有状态,或者称为属性,是运行时值。比如MFC窗体大小,title等
  • 对象具有行为,亦称为方法或者操作。比如常见MFC中窗体具有show方法
  • 对象具有消息,通过发送消息来请求对象执行其操作之一,消息是对象之间交换数据的方式。

从上面的描述,应用程序本质是很多对象相互协作一起在干活,就好比一个车间,有机器、工人、工具等一起相互在一起产生相互作用,从而完成某项产品的制造。那么,这些对象从哪里来呢?

对象来自于类的实例化,谁负责实例化对象呢?这就是类中构造函数干的活,那么析构函数就是销毁对象的。所以构造函数管生,析构函数管埋。

构造管 “生”

构造函数按照类的样式生成对象,也称为实例化对象,那么C++中有哪几种构造函数呢?

构造函数的相同点:

  • 函数名都与类的名字一样;
  • 构造函数都没有返回类型;
  • 创建对象时会自动调用构造函数;

那为嘛又要整这么几个不同的构造函数呢?举个生活中你或许遇到过的栗子:

  • Case 1: 打比方你去商店对售货员说买个灯泡,没有什么附加信息(比如品牌、功率、发光类型、尺寸等),那么售货员把常见的灯泡给你一个,这就相当于C++语言中创建对象时,按照默认方式创建,故调用默认构造函数。
  • Case 2: 拿到灯之后回家一装,擦,太大了装不上!这回你聪明了,量了下安装尺寸,跑去给售货员说你要XX大小的灯,此时就相当于C++利用参数化构造函数实例化对象。
  • Case 3: 擦,用了很久灯又挂了,这回你更聪明了,把坏灯卸下来带到商店照着买一个,这场景就有点像C++中的拷贝构造函数了~。

那么,到底不同的构造函数有些什么不同呢?为嘛C++语言设计这么多种不同的构造函数呢?

  • 默认构造函数:默认构造函数不带任何参数。 如果不指定构造函数,则C ++编译器会为我们生成一个默认构造函数(不带参数,并且具有空主体)。
  • 参数化构造函数:参数传递给构造函数,这些参数用于创建对象时初始化对象。要实现参数化构造函数,只需像向其他函数一样向其添加参数即可。定义构造函数的主体时,使用参数初始化对象的数据成员。
  • 拷贝构造函数:拷贝构造函数,顾名思义,就是按照现有对象一模一样克隆出一个新的对象。其参数一般为现有对象的引用,一般具有如下函数原型:
//函数名为类名,参数为原对象const引用
ClassName(const ClassName &old_object); 

析构管“埋”

析构函数通常用于释放内存,并在销毁对象时对类对象及其类成员进行其他清理操作。当类对象超出生命周期范围或被显式删除时,将为该类对象调用析构函数。

那么析构函数具有哪些特点呢?

  • 销毁对象时,将自动调用析构函数。
  • 不能将其声明为static或const。
  • 析构函数没有参数,也没有返回类型。
  • 具有析构函数的类的对象不能成为联合的成员。
  • 析构函数应在该类的public部中声明。
  • 程序员无法访问析构函数的地址。
  • 一个类有且仅有一个析构函数。
  • 如果没有显式定义析构函数,编译器会自动生成一个默认的析构函数。

既然析构函数是构造函数的反向操作,对于对象管"埋",那么什么时候“埋”呢?

  1. 函数返回退出
  2. 程序被关掉,比如一个应用程序被kill
  3. 包含局部对象的块结尾
  4. 主动调用删除符delete

前面说如果程序猿没有显式定义析构函数,编译器会自动生成一个默认的析构函数。言下之意是有的时候需要显式定义析构函数,那么什么时候需要呢?

当类中动态分配了内存时,或当一个类包含指向在该类中分配的内存的指针时,应该编写一个析构函数以释放该类实例之前的内存。否则会造成内存泄漏。

“生”与“埋”举例

前面说构造管“生”,析构管“埋”,那么到底怎么“生”的呢?怎么“埋”呢?,看看栗子:

#include 
using namespace std;
class Rectangle
{

public: 
 Rectangle(); 
 Rectangle(int w, int l);
  Rectangle(const Rectangle &rct) {width = rct.width; length = rct.length; }

 ~Rectangle();
public:
  int width, length;
};
Rectangle::Rectangle()
{
  cout << "默认矩形诞生了!" << endl;
}
Rectangle::Rectangle(int w, int l)
{
 width  = w;
  length = l;
 cout << "指定矩形诞生了!" << endl;
}
Rectangle::~Rectangle()
{
  cout << "矩形埋掉了!" << endl;
}
int main()
{ 
  Rectangle rct1;
  Rectangle *pRct = new Rectangle(2,3);
   Rectangle rct2  = rct1;
  
 return 0;
}

这个简单的代码,实际运行的输出结果:

默认矩形诞生了!
指定矩形诞生了!
矩形埋掉了!
矩形埋掉了!

技术人总是喜欢眼见为实:因为看见,所以相信!,看看其对应的汇编代码(VC++ 2010汇编结果,这里仅贴出main函数,仅为理解原理,对于汇编指令不做描述,其中#为对汇编注释):

    31: int main()
32: {
012C1660 55 push ebp
012C1661 8B EC mov ebp,esp
012C1663 6A FF push 0FFFFFFFFh
012C1665 68 76 53 2C 01 push offset __ehhandler$_main (12C5376h)
012C166A 64 A1 00 00 00 00 mov eax,dword ptr fs:[00000000h]
012C1670 50 push eax
012C1671 81 EC 14 01 00 00 sub esp,114h
012C1677 53 push ebx
012C1678 56 push esi
012C1679 57 push edi
012C167A 8D BD E0 FE FF FF lea edi,[ebp-120h]
012C1680 B9 45 00 00 00 mov ecx,45h
012C1685 B8 CC CC CC CC mov eax,0CCCCCCCCh
012C168A F3 AB rep stos dword ptr es:[edi]
012C168C A1 00 90 2C 01 mov eax,dword ptr [___security_cookie (12C9000h)]
012C1691 33 C5 xor eax,ebp
012C1693 50 push eax
012C1694 8D 45 F4 lea eax,[ebp-0Ch]
012C1697 64 A3 00 00 00 00 mov dword ptr fs:[00000000h],eax
33: Rectangle rct1;
012C169D 8D 4D E8 lea ecx,[ebp-18h]
#调用默认构造函数管“生”
012C16A0 E8 32 FA FF FF call Rectangle::Rectangle (12C10D7h)
012C16A5 C7 45 FC 00 00 00 00 mov dword ptr [ebp-4],0
34: Rectangle *pRct = new Rectangle(2,3);
012C16AC 6A 08 push 8
012C16AE E8 41 FB FF FF call operator new (12C11F4h)
012C16B3 83 C4 04 add esp,4
012C16B6 89 85 F4 FE FF FF mov dword ptr [ebp-10Ch],eax
012C16BC C6 45 FC 01 mov byte ptr [ebp-4],1
012C16C0 83 BD F4 FE FF FF 00 cmp dword ptr [ebp-10Ch],0
012C16C7 74 17 je main+80h (12C16E0h)
012C16C9 6A 03 push 3 #传参
012C16CB 6A 02 push 2 #传参
012C16CD 8B 8D F4 FE FF FF mov ecx,dword ptr [ebp-10Ch]
#调用参数化构造函数
012C16D3 E8 B8 FA FF FF call Rectangle::Rectangle (12C1190h)
012C16D8 89 85 E0 FE FF FF mov dword ptr [ebp-120h],eax
012C16DE EB 0A jmp main+8Ah (12C16EAh)
012C16E0 C7 85 E0 FE FF FF 00 00 00 00 mov dword ptr [ebp-120h],0
012C16EA 8B 85 E0 FE FF FF mov eax,dword ptr [ebp-120h]
012C16F0 89 85 E8 FE FF FF mov dword ptr [ebp-118h],eax
012C16F6 C6 45 FC 00 mov byte ptr [ebp-4],0
012C16FA 8B 8D E8 FE FF FF mov ecx,dword ptr [ebp-118h]
012C1700 89 4D DC mov dword ptr [ebp-24h],ecx
35: Rectangle rct2 = rct1;
012C1703 8D 45 E8 lea eax,[ebp-18h]
012C1706 50 push eax
012C1707 8D 4D CC lea ecx,[ebp-34h]
#调用拷贝构造函数
012C170A E8 3C F9 FF FF call Rectangle::Rectangle (12C104Bh)
36:
37: return 0;
012C170F C7 85 00 FF FF FF 00 00 00 00 mov dword ptr [ebp-100h],0
012C1719 8D 4D CC lea ecx,[ebp-34h]
#调用析构函数,销毁rct2
012C171C E8 15 FA FF FF call Rectangle::~Rectangle (12C1136h)
012C1721 C7 45 FC FF FF FF FF mov dword ptr [ebp-4],0FFFFFFFFh
012C1728 8D 4D E8 lea ecx,[ebp-18h]
#调用析构函数,销毁rct1
012C172B E8 06 FA FF FF call Rectangle::~Rectangle (12C1136h)
012C1730 8B 85 00 FF FF FF mov eax,dword ptr [ebp-100h]
38: }

这里引发几个问题:

问题1:为什么先析构rct2,后析构rct1呢?

这是由于这两个对象在栈上分配内存,所以基于栈的特性,显然rct2位于C++运行时栈的顶部,而rct1位于栈底。

你如不信,将上述代码修改一下,测测:

Rectangle::~Rectangle()
{
    cout <<"当前宽为:" << width << "矩形埋掉了!" << endl;
}
int main()
{ 
 Rectangle rct1;
  rct1.width = 1;
  Rectangle *pRct = new Rectangle(2,3);
   Rectangle rct2  = rct1;
   rct2.width = 2;
 return 0;
}

其输出结果为:

默认矩形诞生了!
指定矩形诞生了!
当前宽为:2矩形埋掉了!
当前宽为:1矩形埋掉了!

问题2:请问上述代码,构造函数被调用了几次?析构函数又被调用了几次?这是经常面试会考察的基础知识。显然前面的打印以及给出了答案。

问题3:该代码有啥隐患?

答:调用了new,而没有调用delete,为啥这是隐患,new属于动态申请内存,是在堆上为对象申请内存,这属于典型的管“生”不管“埋”,造成内存泄漏,如果整的多了,**必然尸体埋 “堆”!**造成程序引发不可预料的崩溃!

所以应该修正一下:

Rectangle::~Rectangle()
{
    cout <<"当前宽为:" << width << "矩形埋掉了!" << endl;
}
int main()
{ 
 Rectangle rct1;
  rct1.width = 1;
 Rectangle *pRct = new Rectangle(2,3);
   Rectangle rct2  = rct1;
   rct2.width = 3;
 delete pRct;
 cout << "手动埋掉!" << endl;
 return 0;
}

看看输出结果:

默认矩形诞生了!
指定矩形诞生了!
当前宽为:2矩形埋掉了!
手动埋掉!
当前宽为:3矩形埋掉了!
当前宽为:1矩形埋掉了!

总结一下

  • OOP的基本单元是类,该类将静态属性和动态行为封装在一个黑盒中,并指定使用这些框的公共接口。由于该类具有很好的封装(与功能相比),因此重用这些类会更加容易。换句话说,OOP将同一黑盒中软件实体的数据结构和算法较好结合在一起。
  • OOP语言允许更高级别的抽象来解决现实生活中的问题。传统的过程语言例如C迫使您根据计算机的结构(例如内存位和字节,数组,决策,循环)进行思考,而不是根据您要解决的问题进行思考。OOP语言(例如Java,C ++,C#)使您可以在问题空间中进行思考,并使用软件对象来表示和抽象问题空间的实体以解决问题。
  • 对于C++语言,构造函数与析构函数是基础中的基础,类在运行态并不存在,类以对象形式在运行态实现业务需求。对象如何按照类黑盒样式如何在运行态诞生,利用类的构造函数而诞生,对象生存期结束,析构函数管“埋”,销毁对象。
  • 对于局部对象,非new产生的对象,诞生地为栈,在栈中诞生,编译器会插入析构函数使得程序运行态在对象生命周期结束时自动管“埋”,而如果利用new动态创建的对象,则需要手动管“埋”,如手动管“生”(new),不手动管“埋”(delete),对象必成孤魂野鬼,严重时,对象尸体满“堆”。

对于拷贝构造函数,还有一个所谓深拷贝、浅拷贝的要点没有涉及,下次学习总结分享一下,敬请关注期待~,如发现文中有错误,敬请留言指正,不胜感激~

—END—

免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除( 邮箱:macysun@21ic.com )。
换一批
延伸阅读

逻辑分析、高效能运算核心、灵活自动化烧录、先进通讯协议

关键字: 嵌入式 自动化 RS-485 接口

覆盖 TI MCU、MPU 和无线连接解决方案的统一、生产就绪平台,消除迁移障碍,并缩短全球 Zephyr 开发者的上市时间 上海2026年10月14日 /美通社/ --...

关键字: RTOS 嵌入式 TI 开发者

在物联网终端、无线传感节点、便携采集设备与无人值守嵌入式控制系统中,电池续航是决定产品实用性与运维成本的核心指标。多数低功耗嵌入式设计过度依赖软件休眠、数字时钟裁剪等数字节能手段,却忽略了模拟电路长期静态漏电、有源器件偏...

关键字: 嵌入式 控制系统 低功耗

随着物联网设备向微型化、多频段、低成本、快迭代方向快速发展,嵌入式天线设计已成为硬件开发的核心瓶颈。传统板载天线、陶瓷天线、FPC天线方案,普遍存在设计周期长、PCB布局限制多、频段适配性差、量产一致性低等问题,极大拖慢...

关键字: 天线 物联网 嵌入式

随着汽车向电动化、智能化、网联化高速迭代,车载嵌入式系统的功能复杂度、控制精度与可靠性要求大幅提升。传统车用嵌入式系统多采用“纯数字MCU+外置模拟芯片”的分立架构,存在线路繁琐、信号损耗大、响应延迟高、故障率偏高等问题...

关键字: 嵌入式 汽车电子 模拟芯片

随着生活节奏不断加快,一次采购更多食材、提前规划家庭饮食,逐渐成为不少消费者的日常习惯。尤其对于追求高品质生活的家庭而言,冰箱不仅需要拥有充足的储存空间,更需要在长时间储存过程中尽可能保持食材原有的口感与状态。然而,传统...

关键字: 十字门冰箱 净化系统 嵌入式

触觉行业论坛(HIF)制定的规范为可编程触觉提供了通用框架;Vector Haptics API控制、实时音频转触觉及双通道驱动,则为产品团队提供了紧凑的集成路径

关键字: 可编程触觉 处理器 嵌入式

我一直对谷歌助手在我们说“好,谷歌”或“嘿,Siri”时立即开始录音的机制感到着迷。这种启发促使我探索如何将唤醒词检测技术应用于嵌入式设备。在这个项目中,我也意识到在树莓派等小型板载计算机(SBC)上运行时需要进行必要的...

关键字: 嵌入式 树莓派 计算机 INMP441

在万物互联的数字化浪潮中,物联网技术打破了传统设备的信息孤岛,而嵌入式视觉作为设备感知物理世界的核心载体,正依托物联网架构完成颠覆性变革。不同于传统机器视觉依赖云端处理、固定场景作业的局限,物联网驱动下的嵌入式视觉,实现...

关键字: 物联网 嵌入式 视觉
关闭