当前位置: 首页 > news >正文

More Effective C++:改善编程与设计(上)

More Effective C++

目录

More Effective C++:

条款1:仔细区别pointers和 references

条款2:最好使用C++转型操作符

条款3:绝对不要以多态方式处理数组

条款4:非必要不要提供default constructor

条款5:对定制的“类型转换函数”保持警觉

条款6:区别increment/decrement操作符的前置和后置形式

条款7:千万不要重载&&,||和,操作符

条款8:了解各种不同意义的new和delete

条款9:利用destrucotors避免泄漏资源

条款10:在constructors内阻止资源泄漏

条款11:禁止exception流出destructors之外

条款12:了解“抛出一个exception”与“传递一个参数”或“调用一个虚函数”之间的差异

条款13:以by reference方式捕捉exceptions

条款14:明智运用exception specifications

条款15:了解异常处理的成本

条款16:谨记80-20法则

条款17:考虑使用lazy evaluation

条款18:分期摊还预期的计算成本


条款1:仔细区别pointers和 references

        引用不存在空引用,而指针可以存在空指针,并且指针的值是可以修改的,但是引用总是会代表某个对象。
        结论:当你知道需要指向某个东西,而且绝不会改变指向其他东西,或者当你实现一个操作符而其语法需求无法用指针达成,比如:operator */operator [],你应该选择引用,其他时候请采用指针。

条款2:最好使用C++转型操作符

        C语言旧式转型允许你将任何类型转换为任何其他类型,并且难以辨识,为此C++引入四个转型操作符:
static_cast,
reinterpret_cast,
const_cast,
dynamic_cast
        过去的(type)expression 应该改为 static_cast<type>(expression) ;
        四个转型操作符已经在前面讲过,现在来总结一下应用场景:
  • static_cast:可以进行类型具有一定相关性的转换,以及不涉及继承机制的类型执行转型动作;
  • const_cast:用来改变表达式中的常量性或变易性;
  • dynamic_cast:用来执行继承体系中“安全的向下转型或跨系转型动作”,如果转型成功则是正常指针,失败会以一个空指针或者异常表现出来,但是只能用于继承体系中,无法应用在缺乏续虚函数的类型上,也不能改变常量性
  • reinterpret_cast:通常用于函数指针转型动作,并不具备移植性,慎用!

条款3:绝对不要以多态方式处理数组

        假如你有一个class BST以及一个继承自BST的class BalancedBST,如果你将一个BalancedBST对象所组成的数组交给一个BST类型的变量进行打印,编译能够通过,但是当你试图用opearator []去访问数组时会出现问题,指针会假设访问的对象每一个都是BST的大小,但实际上每一个元素大小是BalancedBST的大小,由于派生类比基类占用的空间更大,因此访问出错。
总结:多态和指针算术不能够混用,数组对象几乎总是会涉及指针的算术运算,所以数组
和多态不要混用。

条款4:非必要不要提供default constructor

        在类中到底需不需要提供默认构造函数呢?这取决于具体情况,一方面如果要求类可以合理地从无到有生成对象,并且要依赖于模版,亦或是要实现一个虚基类,那么就需要提供默认构造函数;但如果必须有某些外来信息才能生成对象的类,则不需要提供默认构造函数,因为构造出来对象包含没有任何意义的值,无意义的默认构造函数也会影响代码运行效率,视情况而定。

条款5:对定制的“类型转换函数”保持警觉

        C++允许编译器在不同类型之间支持隐式类型转换,可以通过两种函数实现:
单参数构造函数以及隐式类型转换操作符,
        后者更加容易引发问题:形如operator 类型名称函数,它的出现可能导致错误的函数调用:
Rational r(1,2); count<<r;
你希望打印出r的信息,但是如果没有提供operator <<却提供了operator double,那么编
译仍会通过,但打印出的却不是期望信息。
总结:只要不声明隐式类型转换操作符即可解决,对于单参数构造函数建议使用explicit关键字来避免隐式类型转换,同时支持显式转换。

条款6:区别increment/decrement操作符的前置和后置形式

        C++扩充了++和—操作符的两种形式:
UPInt& operator++(); 前置式;
const UPInt operator++(int); 后置式;
UPInt& operator—(); 前置式;
const UPInt operator—(int); 后置式;
        后置式的int参数的唯一目的是为了区别前置式和后置式,并且返回const UPInt,这是为了
防止++++的情况发生,使用时应该尽可能使用前置式。

条款7:千万不要重载&&,||和,操作符

        原因如下:C++对于真假值表达式采用“骤死式”评估方式,在&&和||操作符左边的表达
式的真假值一旦确定即使右边的表达式尚未检验,整个语句结束,但是一旦你重载了操作符函
数,两边的表达式都需要完成评估,不仅如此,还无法确定评估顺序。至于,同样如此,你绝
对无法保证左侧的表达式一定比右侧的表达式更早被评估。
总结:如果你将它们重载,就没有办法提供程序员预期的某种行为模式,所以请不要重载
这三个操作符函数。

条款8:了解各种不同意义的new和delete

        在你new一个类型的时候,这种操作被称为new operator,会进行两件事情:
        1.调用operator new来分配足够的内存;
        2.调用构造函数,为分配的内存中的那个对象呢设定初始值。
        介绍一下operator new: void* operator new(size_t size);你可以将其重载。
        当你需要在已经分配好的原始内存上构建对象,那么可以使用定位new(Placement new):
        假设A为一个类,如果要在已经有地址为buffer的内存上构建size个对象,可以这样:
A* constructor(void* buffer,size_t size)
{return new(buffer)A(size);
}
        你可以理解为定位new的作用就是将获得的指针再返回,来作为放置的地址,定位new会去
调用operator new,看起来像这样:
void* operator new(size_t,void* location)
{return location;
}
        这里的size_t参数没有用到但一定要有,
小总结:如果你希望将对象产生于堆,使用new operator,如果你只是打算分配内存,使用operator new,如果你打算自己决定内存分配方式,可以重载一个operator new,如果你打算在已分配的内存中构造对象,使用placement new,它会去调用构造函数。
        在你delete一个类型的时候,这种操作被称为new delete,会进行两件事情:
        1.调用析构函数;
        2.调用operator delete释放内存,
        如果你使用placement new,你应该避免使用delete operator,因为它会调用operator delete来释放内存,但是起初内存不是由operator new得来的,你应该直接调用该对象的析构函数。
补充:如果是new一个数组,那么会调用operator new[],再依次调用构造函数,delete []
时会依次调用析构函数,最后调用operator delete[]。

条款9:利用destrucotors避免泄漏资源

        当你在一个函数中实例化一个类后,你想要调用类中的函数并delete掉它,但如果函数调用
时抛出了exception,那么delete语句会被跳过,造成内存泄露, 因此你通常可以通过捕获执行
函数的异常来在throw语句之前执行delete语句,还可以使用智能指针 :采用RAII资源获取即初 始化的思想,将资源封装在对象内,通常可以在exceptions出现时避免泄漏资源。

条款10:在constructors内阻止资源泄漏

        如果你设计的类中constructors调用了new,在构造时申请空间失败,抛出了一个
exception会怎么样呢?C++只会析构已经构造完成的对象,现在constructor没有执行完毕,
不会调用类中的析构函数,会发生内存泄露,解决办法可以如下: 将所有exception捕捉起
来,执行某种清理工作,然后重新抛出exception,而执行清理工作不必刻意进行,可以使用
智能指针来管理资源。

条款11:禁止exception流出destructors之外

        在destructors中抛出异常,会导致资源释放不完全,导致内存泄露,并且如果没有try…
catch语句捕捉异常,会导致terminate函数在exception传播过程中的栈展开机制中被调用,
程序立即终止。

条款12:了解“抛出一个exception”与“传递一个参数”或“调用一个虚函数”之间的差异

        “传递对象到函数去,或是以对象调用虚函数”和“将对象抛出成为一个exception”之间,有
三个主要的差异。
  • 第一,exception object总是会被复制,不论是自定义类型,内置类型或是静态类型,catch端捕捉的永远是throw端的副本,发生修改也只会影响到副本,如果以by value的方式捕捉,甚至会被复制两次。但是传递给函数参数的对象则不一定得复制。
  • 第二,“被抛出成为exception的对象,其被允许的类型转换动作,比“被传递到函数去”的对象少。
  • 第三,catch子句以其“出现于源代码的顺序”被编译器检验比对,其中第一个匹配成功者便执行;而当我们以某对象调用一个虚函数,被选中执行的是那个“与对象类型最佳吻合”的函数,不论它是不是源代码所列的第一个。

条款13:以by reference方式捕捉exceptions

        exception objects有三种方式传递到子句:by pointer,by value,by reference
        如果是by pointer,类似这样: static exception ex; throw &ex; 但是往往掉了static会使得指针指向不复存在的对象,即便是正确接收,那么是否应该调用delete?对于分配于heap的指针没有问题,其他则会招致未受定义的程序行为,所以无论如何必须以by value或by reference的方式捕捉它们。  
Catch-by-value中每当exception object被抛出,就会被复制两次,此外也会引起切割问题,将throw端的派生类切割赋值给catch端时,catch端将失去原派生类的派生成分,并且无法触发多态,重写的虚函数不能发挥作用。 Catch-by-reference,不会发生对象删除问题,没有切割问题,而且exception object也只会被复制一次, 也可以正常出发多态调用,所以强烈建议使用Catch exceptions by reference!

条款14:明智运用exception specifications

        异常声明确实对于“函数希望抛出什么样的exceptions”提供了卓越的说明,但是编译器允
许你调用“可能违反当前函数本身的异常声明”的函数,并且由于如此的调用行为可能导致程序
被迫中止,为了unexpected函数被调用有三个做法:
  • 不应该将templates和exception specifications混合使用;
  • 函数内调用函数无异常声明,那么函数本身也不应该有异常声明;
  • 处理系统可能抛出的exceptions,异常声明是一把双面刃,请谨慎使用!

条款15:了解异常处理的成本

        exception的处理机制首先需要付出 一些空间 来放置某些数据结构(记录哪些对象已被完全
构造妥当)和 一些时间 ,随时保持那些数据结构的正确性;其次try语句块和异常声明需要的成
本相似,对于这块,你只需要了解异常的处理需要消耗部分性能即可。

条款16:谨记80-20法则

        法则内容: 一个程序80%的资源用于20%的代码上, 重点在于:软件的整体性能聚会总是
由其构成要素的一小部分决定。为了提升程序的性能,你可以使用程序分析器或者尽可能使用
最多的数据来分析软件,专注于特别耗时的地方来加以改善。

条款17:考虑使用lazy evaluation

        缓式评估可以在多种场合派上用场:
  • Reference Counting(引用计数):举例string的写时拷贝,如果任何拷贝赋值都采用深拷贝,这就是急式评估,会消耗很多空间,但如果采用数据共享,使用引用计数的方式管理内存,直到字符串需要被修改发生深拷贝就可以节省大量空间。
  • 区分读和写:以string为例,如果只是读取就不需要进行深拷贝,但如果要写入则需要
  • Lazy Fetching(缓式取出):如果想要引入大型对象,只需产生该对象的外壳,不从磁盘中读取任何字段数据,当对象内的某个字段被需要了,程序才从数据库中取回对应的数据
  • Lazy Expression Evaluation(表达式缓评估):如果用m3来表示大型对象m1和m2的某种运算结果,可以用m3来存储一个数据结构,包含两个指针和一个enum,前者指向m1和m2,后者用来指示运算动作是什么,如果在m3被使用之前,被赋予了新的值,或者接受另一个复杂运算结果,那么就节省了运算m1和m2的成本。
注意:缓式评估并不会影响到必需的对象复制,如果计算是必要的,缓式评估并不会为你的程序节省任何工作,只有当计算可以被避免时,缓式评估才会发挥真正的用处。

条款18:分期摊还预期的计算成本

        这里用到的思想是:over-eager evaluation,如果你预期程序常常会用到某个计算,你可
以降低每次计算的平均成本,办法就是设计一份数据结构以便能够极有效率的处理需求。
        其中一个简单做法就是“将已经计算好而可能再次被需要”的数值保留下来,方便下一次拿取来降低平均成本,称之为Caching;还有将数组扩容时,将空间调整到所需大小更大一些的做法,被称为Prefetching 前者会消耗较多内存,但可以降低那些已被计算出的结果重新计算的时间,后者需要一些空间来放置被预先取出的东西,但可降低访问它们所需的时间。

相关文章:

  • java连数据库
  • AI-02a5a5.神经网络-与学习相关的技巧-权重初始值
  • 汽车免拆诊断案例 | 2015款路虎极光车组合仪表提示“充电系统故障”
  • 团结引擎开源车模 Sample 发布:光照渲染优化 动态交互全面体验升级
  • YOLO v1:目标检测领域的革命性突破
  • 【优选算法 | 字符串】字符串模拟题精选:思维+实现解析
  • 人工智能驱动的临床路径体系化解决方案与实施路径
  • KUKA库卡焊接机器人智能气阀
  • 大模型微调实战:基于GpuGeek平台的低成本高效训练方案
  • MySQL 服务器配置和管理(上)
  • react+html2canvas+jspdf将页面导出pdf
  • AI规则引擎:解锁SQL数据分析新姿势
  • RBTree的模拟实现
  • 《P2345 [USACO04OPEN] MooFest G》
  • PNG转ico图标(支持圆角矩形/方形+透明背景)Python脚本 - 随笔
  • STM32F103C8T6板子使用说明
  • Android架构 之 自定义native进程
  • loki grafana 页面查看 loki 日志偶发 too many outstanding requests
  • C语言之旅5---分支与循环【2】
  • 数睿通2.0数据中台,已购买源代码
  • 30平米的无障碍酒吧里,我们将偏见折叠又摊开
  • 泽连斯基:正在等待俄方确认参加会谈的代表团组成
  • 王征、解宁元、牛恺任西安市副市长
  • 日月谭天丨这轮中美关税会谈让台湾社会看清了什么?
  • 习近平同巴西总统卢拉会谈
  • 首映|奥斯卡最佳国际影片《我仍在此》即将公映