C++ Gotchas 条款60:没能区分单体内存分配与数组内存分配

翻译文档 专栏收录该内容
74 篇文章 0 订阅

Gotcha #60: Failure to Distinguish Scalar and Array Allocation

Gotcha条款60:没能区分单体内存分配与数组内存分配

 

单一个Widget与一个Widget数组是等同的吗?当然不是。那为何这么多C++程序员在发现“数组(arrays)与单个量(scalars)采用不同的运算符进行空间的分配和释放”时会感到吃惊?

 

我们都知道如何对单一个Widget进行空间分配和释放。我们使用new operatordelete operator

 

Widget *w = new Widget( arg );

// . . .

delete w;

 

new operatorC++中大部分其它运算符不同的是,其行为无法经由重载而被改变。new operator总是调用一个名为operator new的函数来(可想而知的)获取一些存储空间,并可能对该空间进行初始化。对于上述代码中Widget的情形,使用new operator将首先导致一个对operator new函数的调用,该函数接收一个size_t型别作为参数,然后唤起Widget构造函数施行于由operator new函数返回的未初始化存储空间,从而产生一个Widget对象。

 

代码中的delete operator 针对Widget唤起一个析构函数,然后调用一个名为operator delete的函数来(可想而知的)去配“现在已经消失的Widget对象原来所占用的那片存储空间”。

 

内存分配与去配的行为变化,是经由重载、替换或者隐藏operator new函数与operator delete函数来实现的,而不是通过修改new operatordelete operator的行为使然。

 

我们同样也知道如何对Widgets数组进行空间分配与释放。但我们不使用new operatordelete operator

 

w = new Widget[n];

// . . .

delete [] w;

 

不同之处在于,这里我们使用了new [] operatordelete [] operator(发音分别为“array new”和“array delete”)。与newdelete相同,array new operatorarray delete operator的行为是不可改变的。Array new首先唤起一个名为operator new[]的函数以获取一些存储空间,然后(如果必要的话)从数组的第一个元素直至最后一个元素,对每一个已分配空间的数组元素施行缺省的初始化动作。Array delete首先按照与初始化动作相反的次序销毁数组中的每一个元素,然后唤起一个名为operator delete[]的函数以归还(reclaim)存储空间。

 

顺带一提:应该注意,使用标准程序库提供的vector而非array,通常是更好的设计决策。vector 的效能几乎与array一样,并且还更加安全、灵活。一般说来,vector可以被看作是“灵巧的”array,与array具有相近的语义。唯有不同的是,vector在被销毁时,其元素是从头到尾被销毁的;而在array中,元素是按照相反的次序被销毁的。

 

内存管理函数必须妥当的配对使用。如果使用了new来获取存储空间,那么就应该使用delete来释放。如果使用了malloc来获取存储空间,那么就应该使用free来释放。将newfree一起,或者将mallocdelete一起使用于某个特定平台上的某些型别,在有些时候是可以“工作”的,但不存在任何保证使得这样的代码会继续正常工作下去。

 

int *ip = new int(12);

// . . .

free( ip ); // 错的!

ip = static_cast<int *>(malloc( sizeof(int) ));

*ip = 12;

// . . .

delete ip; // 错的!

 

数组的分配和删除遵循同样的配对规则。有一个常见的错误就是,使用array new分配一个数组之后又用scalar delete(用于单个量的delete)来进行释放。这种代码正如newfree的错配使用一样,或许在某个特定的情形之下可以侥幸工作,但诚然是不正确的做法,将来很可能会导致错误发生。

 

double *dp = new double[1];

// . . .

delete dp; // 错的!

 

应该注意的是,对于使用scalar delete(用于单个量的delete)删除数组的错误做法,编译器不会给出警告,因为其无法区分“指向数组的指针”与“指向单个元素的指针”。一般来说,array new会在为数组分配的内存之邻接位置插入一些额外信息,用以标明存储区块(block)的大小以及所分配数组中包含的元素个数。Array delete正是通过检视这些信息并由此来删除数组的。

 

(经由array new得到的)这种信息之格式也许不同于经由scalar new得到的信息之格式。如果唤起scalar delete来释放array new分配的存储空间,那么相应的关于空间大小和元素个数的信息就可能被scalar delete错误的解析,从而导致未定义结果——而其本来应该是由array delete进行解析的。Scalar形式的分配与array形式的分配也有可能使用了不同的内存池(memory pools)。倘若如此,那么使用scalar deletescalar pool归还一片分配于array pool的存储空间,就会导致灾难性的结果。

 

delete [] dp; // 对了!

 

这种在array分配与scalar分配两个概念上的闪失大意,也经常出现在用于内存管理的成员函数之设计中:

 

class Widget {

public:

void *operator new( size_t );

void operator delete( void *, size_t );

// . . .

};

 

Widget class的作者决心自己定制Widgets内存管理,但是却没有考虑到:array operator new函数和array operator delete函数与其对应的scalar形式的函数(scalar operator newscalar operator delete),实际上具有不同的名称(译注:scalar形式分别为operator new()operator delete();而array形式分别为operator new[]()operator delete[](),详见下文所述)。

 

Widget *w = new Widget( arg ); // OK

// . . .

delete w; // OK

w = new Widget[n]; // oops!

// . . .

delete [] w; // oops!

 

由于Widget class没有声明operator new[]operator delete[]函数,因此Widgets数组的内存管理将使用这些函数的global版本。这样可能就造成不正确的行为,而Widget class的作者则应该提供operator new[]operator delete[]的成员函数版本。

 

如果事实恰恰相反,上述恰好是意欲之中的正确行为,那么该类别的作者就应该向以后的维护者清楚的说明这个情况;否则的话,维护者就很可能会提供那些“遗失”的函数,试图“修正”这个“问题”。将这种设计决策文档化的最佳方法不是加注释,而是直接通过代码体现:

 

class Widget {

public:

void *operator new( size_t );

void operator delete( void *, size_t );

void *operator new[]( size_t n )

{ return ::operator new[](n); }

void operator delete[]( void *p, size_t )

{ ::operator delete[](p); }

// . . .

};

 

Array newarray deleteinline member function版本在运行期不会导致任何额外消耗;同时,即便是最粗心大意的维护者也能不二的确信作者的意图乃是“为Widgets唤起array newarray delete函数的global版本”,而不至于反复猜疑。

  • 0
    点赞
  • 0
    评论
  • 0
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

相关推荐
程序员的必经之路! 【限时优惠】 现在下单,还享四重好礼 1、教学课件免费下载 2、课程案例代码免费下载 3、专属VIP学员群免费答疑 4、下单还送800元编程大礼包 【超实用课程内容】  根据《2019-2020年中国开发者调查报告》显示,超83%的开发者都在使用MySQL数据库。使用量大同时,掌握MySQL早已是运维、DBA的必备技,甚至部分IT开发岗位也要求对数据库使用和原理有深入的了解和掌握。 学习编程,你可会犹豫选择 C++ 还是 Java;入门数据科学,你可会纠结于选择 Python 还是 R;但无论如何, MySQL 都是 IT 从业人员不可或缺的技!   套餐中一共包含2门MySQL数据库必学的核心课程(共98课时)   课程1《MySQL数据库从入门到实战应用》   课程2《高性MySQL实战课》   【哪些人适合学习这门课程?】  1)平时只接触了语言基础,并未学习任何数据库知识的人;  2)对MySQL掌握程度薄弱的人,课程可以让你更好发挥MySQL最佳性; 3)想修炼更好的MySQL内功,工作中遇到高并发场景可以游刃有余; 4)被面试官打破沙锅问到底的问题问到怀疑人生的应聘者。 【课程主要讲哪些内容?】 课程一《MySQL数据库从入门到实战应用》 主要从基础篇,SQL语言篇、MySQL进阶篇三个角度展开讲解,帮助大家更加高效的管理MySQL数据库。 课程二《高性MySQL实战课》主要从高可用篇、MySQL8.0新特性篇,性优化篇,面试篇四个角度展开讲解,帮助大家发挥MySQL的最佳性的优化方法,掌握如何处理海量业务数据和高并发请求 【你收获到什么?】  1.基础再提高,针对MySQL核心知识点学透,用对; 2.力再提高,日常工作中的代码换新貌,不怕问题; 3.面试再加分,巴不得面试官打破沙锅问到底,竞争力MAX。 【课程如何观看?】  1、登录CSDN学院 APP 在我的课程中进行学习; 2、移动端CSDN 学院APP(注意不是CSDN APP哦)  本课程为录播课,课程永久有效观看时长 【资料开放】 课件、课程案例代码完全开放给你,你可以根据所学知识,自行修改、优化。  下载方式电脑登录课程观看页面,点击右侧课件,可进行课程资料的打包下载。
本套餐将包括两个重磅性的课程一个赠送学习的课程,分别为SpringBoot实战视频教程RabbitMQ实战教程跟SSM整合开发之poi导入导出Excel。目的是为了让各位小伙伴可以从零基础一步一个脚印学习微服务项目的开发,特别是SpringBoot项目的开发,之后会进入第二个课程RabbitMQ的实战,即消息中间件在实际项目或者系统中各种业务模块的实战并解决一些常见的典型的问题!核心的知识点分别包括 一、SpringBoot实战历程课程 (1)SpringBoot实战应用场景的介绍代码实战 (2)发送邮件服务、上传下载文件服务、Poi导入导出Excel (3)单模块多模块项目构建、项目部署打包、日志、多环境配置、lombok、validator以及mybatis整合实战跟多数据源实战 (4)Redis缓存中间件的实战缓存雪崩跟缓存穿透等问题的解决实战 (5)RabbitMQ消息中间件在业务模块异步解耦、通信、消息确认机制以及并发量配置等的实战 二、RabbitMQ实战教程课程 (1)RabbitMQ的官网权威技术手册拜读,认识并理解各大专有名词,如队列,交换机,路由,死信队列,消息确认机制等等 (2)RabbitMQ在业务服务模块之间的异步解耦通信实战,如异步记录日志发送邮件等 (3)商城系统抢单高并发情况下RabbitMQ的限流作用代码实战 (4)消息确认机制并发量配置并用来实战商城用户下单 (5)死信队列深入讲解DLX,DLK,TTL等概念的讲解并用来实战 “支付系统用户下单后支付超时而失效其下单记录”实战 详情,各位小伙伴可以查看两个课程的目录。相信学完该套餐相关课程后,你的实战力将大大提升!涨薪将不再遥遥无期! 最后,赠送的SSM整合开发之POI导入导出Excel目的是为了让各位小伙伴也可以学习Spring+SpringMVC+Mybatis整合开发的项目,让大家一对比SpringBootSPring的项目开发流程以及复杂程度!!! 相信学完之后,你会对SpringBoot爱不释手!!
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值