博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Effective C++学习笔记(1)
阅读量:4329 次
发布时间:2019-06-06

本文共 4950 字,大约阅读时间需要 16 分钟。

最近刚看完Effective C++,记录一下当前几个比较常用的方法。

1.以独立语句将newed对象置入智能指针

  • 智能指针是以对象管理资源,在构造函数中获得资源并在析构函数中释放资源​

  • 以下调用:​

processWidget(std::tr1::shared_ptr<Widget>(new Widget),priority());​

  创建该调用的代码,编译器要做以下三件事:​

   (1)调用priority​

   (2)执行“new Widget”​

   (3)调用tr1::shared_ptr构造函数​

  C++编译器以什么次序完成这些事呢?弹性很大。可以确定(2)一定在(3)之前,但(1)可以排在第一或第二或第三执行。如果编译器选择以第二顺位执行它,即(2)(1)(3)的顺序,万一对(1)的调用导致异常,此情况下“new Widget”返回的指针将会遗失,因为它尚未被置入tr1::shared_ptr内,而tr1::shared_ptr是我们用来防卫资源泄漏的。所以对processWidget的调用过程可能引发资源泄漏。

  • 解决办法:使用分离语句,即分别写出(1)创建Widget,(2)将它置入一个智能指针内,然后再把那个智能指针传给processWidget:​

   std::tr1::shared_ptr<Widget> pw(new Widget);​

   ProcessWidget(pw,priority()) ;​

  以上之所以行得通,是因为编译器对于“跨越语句的各项操作”没有重新排列的自由(自用在语句内它才有那个自由度

 

2.宁以pass-by-reference-to-const替换pass-by-value

考虑以下代码,其中调用validateStudent,后者需要一个Student实参(by value)并返回它是否有效:​

bool validateStudent(Student s);​

Student plato;​

Bool platoIsOK = validateStudent(plato);​

上述调用会发生以下:​

Student的copy构造函数会被调用,以plato为蓝本将s初始化。validateStudent返回后s会被销毁。因此对此函数而言,参数传递的成本是“一次Student copy构造函数调用,加上一次Student析构函数调用”.

  但那还不是全部。Student内有两个string对象,所以每次构造一个Student对象也就构造了两个string对象。此外,Student继承自Person,所以每次构造Student对象也必须构造出一个Person对象。一个Person对象又有两个string对象在其中,因此每一次Person构造动作又需承担两个string构造动作。​

  最终结果是,以by value方式传递一个Student对象会导致调用一次Studnt copy构造函数、两次Person copy构造函数、四次string copy构造函数。当函数内的Student复件被销毁,每个构造函数调用动作都需要一个对应的析构函数动作。因此,以by value方式传递一个Student对象,总体成本是“6次构造函数和6次析构函数”!

 

现在考虑pass by reference-to-const:​

  bool validateStudent(const Student& s);​

这种传递方式的效率高得多:没有任何构造函数或析构函数被调用,因为没有任何新对象被创建。const是很重要的,它保护了传进去的参数不会被修改。​

另外,以by reference方式传递参数也可以避免slicing(对象切割)问题。当一个子类对象以by value方式传递并被视为一个基类对象,基类的copy构造函数会被调用,而“造成此对象的行为像个子类对象”的那些特化性质全被切割掉了,仅仅留下一个基类对象。​

 

3.将成员变量声明为private

1.语法一致性​

如果成员变量不是public,客户唯一能够访问对象的方法就是通过成员函数。客户对class成员的访问将统一通过成员函数来实现。​

2.使用函数可以使你对成员变量的处理用更精确的控制。如果你令成员变量为pubilic,每个人都可读写它,但如果你以函数取得或设定其值,你就可以实现出“不准访问”、“只读访问”、“读写访问”,甚至“只写访问”。​

3.封装。如果你通过函数访问变量,日后可改以某个计算替换这个成员变量,而class用户一点也不会知道class的内部实现已经起了变化。​

封装的重要性:如果你对客户隐藏成员变量,你可以确保class的约束条件总是会获得维护,因为只有成员函数可以影响他们。而且,你保留了日后变更实现的权利。如果不隐藏他们,即使拥有class源码,改变任何public事物的能力还是极端受到束缚,因为那会破坏太多客户码。Public意味不封装,而几乎可以说,不封装意味着不可改变,特别是对被广泛采用的class而言。被广泛使用的class是最需要封装的一个族群,因为他们最能够从“改变用一个较佳实现版本”中获益。​

假设我们有一个public成员变量,而我们最终取消了它。所有使用它的客户码都会被破坏,而那是一个不可知的大量。​

protected并不更好。假设我们有一个protected成员变量,而我们最终取消了它。所有使用它的子类都会被破坏,而那往往也是是一个不可知的大量。​

从封装的角度来说,其实只有两种访问权限,private和其他。

 

4.尽可能延后变量定义式出现的时间

只要你定义了一个变量而其类型带有一个构造函数或析构函数,那么当程序的控制流到达这个变量定义式时,你便得承受构造成本;当这个变量离开其作用域时,你便得承受析构成本。即使这个变量最终并未被使用,仍需耗费这些成本,所以应当尽可能避免这种情形。

更,不只应该考虑延后变量的定义,直到非得使用该变量的前一刻为止,甚至应该尝试延后这份定义直到能够给它初值实参为止。这样不仅能够避免构造(和析构)非必要的对象,还可以避免无意义的default构造行为。

以上两种写法的成本如下:​

(1)方法A:1个构造函数+1个析构函数+n个赋值操作​

(2)方法B:n个构造函数+n个析构函数

如果class的赋值成本低于一组构造+析构成本,做法A大体而言比较高效。尤其当n值比价大时。否则B比较好。此外方法A造成名称w的作用域比方法B大,有时那对程序的可理解性和易维护性造成冲突。因此,除非(1)你知道赋值成本比“构造+析构”成本低,(2)你正在处理代码中效率高度敏感的部分,否则你应该使用方法B

 

5.尽量少做转型动作

C风格的转型:​

(T)expression   //将expression转型为T​

函数风格的转型:​

T(expression)  //将expression转型为T​

以上两种称为“旧式转型”,C++的四种新式转型:​

(1)const_cast<T>(expression)​

通常用来将对象的常量性转除。​

(2)dynamic_cast<T>(expression)​

主要用来执行“安全向下转型”,也就是用来决定某对象是否属于继承体系中的某个类型。将一个基类对象指针(或引用)cast到继承类指针,dynamic_cast会根据基类指针是否真正指向继承类指针来做相应处理。是唯一可能耗费重大运行成本的转型动作。​

(3)reinterpret_cast<T>(expression)​

T 必须是一个指针、引用、算术类型、函数指针或者成员指针。它可以把一个指针转换成一个整数,也可以把一个整数转换成一个指针​

(4)static_cast<T>(expression)​

用来强迫隐式转换,例如将non-const对象转为const对象,或将int转为double等等。还可以用来执行上述多种转换的反向转换,例如将void*指针转为typed指针,将point-to-base转为pointer-to-derived。

 

转型并非什么都没有做。有的时候需要内部会有一个偏移量来实现。对象的布局方式和它们的地址计算方式随编译器的不同而不同,那意味着“由于知道对象布局”而设计的转型,在某一平台行得通,在其他平台并不一定行得通。

dynamic_cast的许多实现版本执行速度相当慢。例如至少有一个很普遍的实现版本基于“class名称之字符串比较”,如果你在四层深的单继承体系内的某个对象身上执行dynamic_cast,这个实现版本每一次dynamic_cast可能会好用多达四次的strcmp调用,用以比较class名称。深度继承或多重继承的成本更高。

【其实这个的内容蛮多,我只把觉得重要的两点写了下来】

 

所以,

(1)如果可以,尽量避免转型,特别是在注重代码效率的代码中避免dynamic_cast。如果有个设计需要转型动作,试着发展无需转型的替代设计。​

(2)如果转型是必要的,试着将他们隐藏于某个函数背后。客户随后可以调用该函数,而不需将转型放进他们自己的代码内。​

(3)宁可使用C++-style转型,不要使用旧式转型。前者很容易辨识出来,而且也比较有着分门别类的执掌。

 

6.避免返回handles指向对象内部成分

这段代码可以通过编译。但实际上它是自我矛盾的。一方面upperleft和lowerRight被声明为const成员函数,因为它们的目的只是为了提供客户一个得知Rectangle相关坐标点的方法,而不是让客户修改Rectangle。另一方面两个函数却都返回reference指向private内部数据,调用者于是通过这些reference更改内部数据。如:​

Point coord1(0,0);​Point coord2(100,100);​const Rectangle rec(coord1,coord2);​rec.upperLeft().setX(50);​

这里,upperLeft的调用者能够使用被返回的reference来改变成员。但rec其实应该是不可改变的(const)。

上面这种情况是由于“成员函数返回reference”。如果它们返回的是指针或迭代器,相同的情况还是发生,原因也相同。reference、指针和迭代器都是所谓的handle,而返回一个“代表对象内部数据”的handle,随之而来的便是“降低对象封装性”的风险。​

要解决以上矛盾,只要对它们返回的类型加上const即可:

即使如此,还是可能在其他场合带来问题。

明确的说,他可能导致空悬的handle:这种handle所指的东西(所属对象)不复存在。

例如某个函数返回GUI对象的外框,

例如某个函数返回GUI对象的外框,现在客户有可能这么使用这个函数:

对boundingBox的调用获得一个新的、暂时的Rectangle对象。这个对象没有名称,权且称他temp。随后upperLeft作用于temp身上,返回一个reference指向temp的内部成分,具体说是指向一个用一标志temp的Point。于是pUpperLeft指向那个Point对象。目前为止一切都还好。

但是,在哪个语句结束之后,temp将被销毁,而那将直接导致temp内的Point析构。最终将导致pUpperLeft指向一个不再存在的对象。

 

7.不要忽略编译器的警告

这里希望以D::f重新定义virtual函数B::f,但其中有个错误:B中的f是个const成员,而在D中它未被声明为const。有些编译器可能这样说:​

warning:D::f() hides virtual B::f​

有些程序员对这个信息的反应是:“噢,当然,D::f遮掩了B::f,那正是想象中该有的事!”错,这个编译器视图告诉你声明与B中名称为f的所有函数并未在D中被重新声明,而是被整个遮掩掉了。

 

为了让被这样的名称重见天日,可使用using声明式或转交函数。【这是另外一条了】

转载于:https://www.cnblogs.com/betterwgo/p/9807336.html

你可能感兴趣的文章
自动测试用工具
查看>>
前端基础之BOM和DOM
查看>>
[T-ARA/筷子兄弟][Little Apple]
查看>>
编译Libgdiplus遇到的问题
查看>>
【NOIP 模拟赛】Evensgn 剪树枝 树形dp
查看>>
java学习笔记④MySql数据库--01/02 database table 数据的增删改
查看>>
两台电脑如何实现共享文件
查看>>
组合模式Composite
查看>>
程序员最想得到的十大证件,你最想得到哪个?
查看>>
我的第一篇CBBLOGS博客
查看>>
【MyBean调试笔记】接口的使用和清理
查看>>
07 js自定义函数
查看>>
jQueru中数据交换格式XML和JSON对比
查看>>
form表单序列化后的数据转json对象
查看>>
[PYTHON]一个简单的单元測试框架
查看>>
iOS开发网络篇—XML数据的解析
查看>>
[BZOJ4303]数列
查看>>
一般处理程序在VS2012中打开问题
查看>>
C语言中的++和--
查看>>
thinkphp3.2.3入口文件详解
查看>>