[导读] C++语言有时候也拿来写写应用代码,可是居然发现连构造、析构都还没弄明白,把这糟心的概念整理分享一下。
在谈类的构造前,先聊聊面向对象编程与面向过程的个人体会。
要谈这个问题,先来对比一下传统面向过程的编程策略:
比较典型的–C, Pascal, Basic, Fortran语言,传统的做法是整了很多函数,整合时主要是整合各函数的调用,main函数协调对过程的调用,并将适当的数据作为参数在调用间进行传递流转而实现业务需求。信息流转主要靠:
如上图,传统面向过程编程语言,比如C语言其编程的主要元为函数,这种语言有这些弊端:
当然如C语言开发,现在的编程策略已经大量引入了面向对象编程策略了,但是要实现这些对象编程策略,则开发人员本身需要去实现这些面向对象本身的策略,需要自己手撕实现这些基础的对象思想。所以这里仅仅就语言本身特征做为说明,不必纠结。而这些策略如果由编程语言本身去实现,则显然是一个更优异的解决方案。但是比如嵌入式领域为嘛不直接用C++呢,而往往更多采用C的方式采用对象策略来撸代码呢? 嵌入式领域更多需要与底层硬件打交道,而C语言本身抽象层级相对更适合这种场景,结合对象策略编程则可以兼顾重用封装等考量。
回到技术发展历史来看,1970年代初期,美国国防部(DoD)成立了一个工作队,调查其IT预算为何总是失控的原因,其调查结果是:
而面向对象编程语言则很好的解决了这些弊端:
而现代面向对象编程语言(OOP: Object-Oriented Programming) ,从语言本身角度,其编程的场景则变成下面一种截然不一样的画风:
程序的运行态:是不同的实例化对象一起协作干活的场景
应用程序通过对象间消息传递,对象执行自身的行为进而实现业务需求。编写代码,设计类,撰写类的代码,然而应用程序运行时,却是以相应的类实例化的对象协作完成逻辑,这就是所谓面向对象编程的含义。那么对于对象而言,具有哪些属性呢?
从上面的描述,应用程序本质是很多对象相互协作一起在干活,就好比一个车间,有机器、工人、工具等一起相互在一起产生相互作用,从而完成某项产品的制造。那么,这些对象从哪里来呢?
对象来自于类的实例化,谁负责实例化对象呢?这就是类中构造函数干的活,那么析构函数就是销毁对象的。所以构造函数管生,析构函数管埋。
构造函数按照类的样式生成对象,也称为实例化对象,那么C++中有哪几种构造函数呢?
构造函数的相同点:
那为嘛又要整这么几个不同的构造函数呢?举个生活中你或许遇到过的栗子:
那么,到底不同的构造函数有些什么不同呢?为嘛C++语言设计这么多种不同的构造函数呢?
//函数名为类名,参数为原对象const引用
ClassName(const ClassName &old_object);
析构函数通常用于释放内存,并在销毁对象时对类对象及其类成员进行其他清理操作。当类对象超出生命周期范围或被显式删除时,将为该类对象调用析构函数。
那么析构函数具有哪些特点呢?
既然析构函数是构造函数的反向操作,对于对象管"埋",那么什么时候“埋”呢?
前面说如果程序猿没有显式定义析构函数,编译器会自动生成一个默认的析构函数。言下之意是有的时候需要显式定义析构函数,那么什么时候需要呢?
当类中动态分配了内存时,或当一个类包含指向在该类中分配的内存的指针时,应该编写一个析构函数以释放该类实例之前的内存。否则会造成内存泄漏。
前面说构造管“生”,析构管“埋”,那么到底怎么“生”的呢?怎么“埋”呢?,看看栗子:
#include <iostream>
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矩形埋掉了!
对于拷贝构造函数,还有一个所谓深拷贝、浅拷贝的要点没有涉及,下次学习总结分享一下,敬请关注期待~,如发现文中有错误,敬请留言指正,不胜感激~
—END—