C++学习(7)--转向effective c++(3)

本文探讨了C++编程中的关键设计原则与模式,包括接口设计、函数选择、继承使用等方面,旨在帮助开发者构建高效、易维护的软件系统。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

条款18: 争取使类的接口完整并且最小
本条款中的一些基本概念:
// 返回可以读/写的元素
t& operator[](int index);
// 返回只读元素
const t& operator[](int index) const;
概括起来就是说,无端地在接口里增加函数不是没有代价的,所以在增加一个新函数时要仔细考虑:它所带来的方便性(只有在接口完整的前提下才应该考虑增加一个新函数以提
供方便性)是否超过它所带来的额外代价,如复杂性,可读性,可维护性和编译时间等。
完全看的明白,但完全没有什么特别感性的认识,不过这一条对于后期的维护和管理很有用。收藏!
条款19: 分清成员函数,非成员函数和友元函数
·虚函数必须是成员函数。如果f必须是虚函数,就让它成为c的成员函数。
·operator>>和operator<<决不能是成员函数。如果f是operator>>或operator<<,让f成为非成员函数。如果f还需要访问c的非公有成员,让f成为c的友元函数。
·只有非成员函数对最左边的参数进行类型转换。如果f需要对最左边的参数进行类型转换,让f成为非成员函数。如果f还需要访问c的非公有成员,让f成为c的友元函数。
·其它情况下都声明为成员函数。如果以上情况都不是,让f成为c的成员函数。
条款20: 避免public接口出现数据成员
reason1 “如果public接口里都是函数,用户每次访问类的成员时就用不着抓脑袋去想:是该用括号还是不该用括号呢?——用括号就是了!因为每个成员都是函数。一生中,这
可以避免你多少次抓脑袋啊!”
reason2 ;如果用函数来获取或设定它的值,就可以实现禁止访问、只读访问和读写访问等多种控制。
class accesslevels {
public:
  int getreadonly() const{ return readonly; }
  void setreadwrite(int value) { readwrite = value; }
  int getreadwrite() const { return readwrite; }
  void setwriteonly(int value) { writeonly = value; }
private:
  int noaccess;             // 禁止访问这个int
  int readonly;             // 可以只读这个int
  int readwrite;            // 可以读/写这个int
  int writeonly;            // 可以只写这个int
};
reason 3 功能分离(functional abstraction)。如果用函数来实现对数据成员的访问,以后就有可能用一段计算来取代这个数据成员,而使用这个类的用户却一无所知。
结论是,在public接口里放上数据成员无异于自找麻烦,所以要把数据成员安全地隐藏在与功能分离的高墙后。如果现在就开始这么做,那我们就可以无需任何代价地换来一致性
和精确的访问控制。
条款21: 尽可能使用const
对指针来说,可以指定指针本身为const,也可以指定指针所指的数据为const,或二者同时指定为const,还有,两者都不指定为const:
语法并非看起来那么变化多端。一般来说,你可以在头脑里画一条垂直线穿过指针声明中的星号(*)位置,如果const出现在线的左边,指针指向的数据为常量;如果const出现在
线的右边,指针本身为常量;如果const在线的两边都出现,二者都是常量。
让函数返回一个常量值经常可以在不降低安全性和效率的情况下减少用户出错的几率。
就比如const rational operator*(const rational& lhs,const rational & rhs);
为啥让他返回一个const值呢?
如果你敢干出这种事的话……
(a*b)=c;
先不说这么做的人有多么操蛋,为防止他继续操蛋下去,返回的const起到了作用
const成员函数的目的是为了指明哪个成员函数可以在const对象上被调用。仅在const方面有不同的成员函数可以重载
一个成员函数为const的确切含义是什么?有两种主要的看法:数据意义上的const(bitwise constness)和概念意义上的const(conceptual constness)。
bitwise constness的坚持者认为,当且仅当成员函数不修改对象的任何数据成员(静态数据成员除外)时,即不修改对象中任何一个比特(bit)时,这个成员函数才是const的。
不幸的是,很多不遵守bitwise constness定义的成员函数也可以通过bitwise测试。特别是,一个“修改了指针所指向的数据”的成员函数,其行为显然违反了bitwise constness
定义,但如果对象中仅包含这个指针,这个函数也是bitwise const的,编译时会通过。这就和我们的直觉有差异
这就导致conceptual constness观点的引入。此观点的坚持者认为,一个const成员函数可以修改它所在对象的一些数据(bits) ,但只有在用户不会发觉的情况下。
还有一些内容就更深奥更广了,先不看了。
条款22: 尽量用“传引用”而不用“传值”
“通过值来传递一个对象”的具体含义是由这个对象的类的拷贝构造函数定义的。这使得传值成为一种非常昂贵的操作。
引用几乎都是通过指针来实现的,所以通过引用传递对象实际上是传递指针。
传值调用的话会调用大量的拷贝构造函数,构造函数和析构函数,尤其是在派生的子类中更是如此。
而传应用相当于传递指针,无需消耗太多。
所谓传值所讨论的问题从刚开始学c就开始一直在接触,无奈的事真正搞懂的好象没有几个(从我周边的人来说),希望下面的话能够给以启示:
// 一个受“切割问题”困扰的函数
void printnameanddisplay(window w)
{
  cout << w.name();
  w.display();
}
想象当用一个windowwithscrollbars对象来调用这个函数时将发生什么:
windowwithscrollbars wwsb;
printnameanddisplay(wwsb);
参数w将会作为一个windows对象而被创建(它是通过值来传递的,记得吗?),所有wwsb所具有的作为windowwithscrollbars对象的行为特性都被“切割”掉了。
printnameanddisplay内部,w的行为就象是一个类window的对象(因为它本身就是一个window的对象),而不管当初传到函数的对象类型是什么。尤其是,printnameanddisplay内
部对display的调用总是window::display,而不是windowwithscrollbars::display。
解决切割问题的方法是通过引用来传递w:
// 一个不受“切割问题”困扰的函数
void printnameanddisplay(const window& w)
{
  cout << w.name();
  w.display();
}
现在w的行为就和传到函数的真实类型一致了。为了强调w虽然通过引用传递但在函数内部不能修改,就要采纳条款21的建议将它声明为const。
条款23: 必须返回一个对象时不要试图返回一个引用
传递一个并不存在的对象的引用。这就不是好事了。
无论何时看到一个引用的声明,就要立即问自己:它的另一个名字是什么呢?
所以说传递一个不存在对象的引用的话是不可能成功的。
当需要在返回引用和返回对象间做决定时,你的职责是选择可以完成正确功能的那个。至于怎么让这个选择所产生的代价尽可能的小,那是编译器的生产商去想的事。
条款24: 在函数重载和设定参数缺省值间慎重选择
条款25: 避免对指针和数字类型重载
0是一个int——准确地说,一个字面上的整数常量
解决的方法我觉得有点得不偿失,还不如就不要重载呢
所以,作为重载函数的设计者,归根结底最基本的一条是,只要有可能,就要避免对一个数字和一个指针类型重载。

条款26: 当心潜在的二义性
注意:c++中,潜在的二义性不是一种错误。
一种情况的二义是
class
B;  // 对类B提前声明
  //
class A {
public:
 A(const B&); // 可以从B构造而来的类A
};
class B {
public:
 operator A() const; // 可以从A转换而来的类B
};
void f(const A&);
B b;
f(b);// 错误!——二义
其原因是 ,调用函数f(),编译器会认为传给了一个a的对象,结果穿了个b的,咋办尼,有两种方法解决(二义)一种方法是调用类A的构造函数,它以b为参数构造一个新的A的对象。另一种方法是调用类B里自定义的转换运算符,它将b转换成一个A的对象。因为这两个途径都一样可行,编译器拒绝从他们中选择一个。
就像一个美女俩帅哥,美女犹豫不定选哪个,结果俩都没有了……惨~
另一种类似的二义的形式源于C++语言的标准转换——甚至没有涉及到类:
void f(int);
void f(char);
double d = 6.02;
f(d);   // 错误!——二义
d是该转换成int还是char呢?两种转换都可行,所以编译器干脆不去做结论。
多继承充满了潜在二义性的可能。最常发生的一种情况是当一个派生类从多个基类继承了相同的成员名。
解决的方法就是显式地通过指明函数所需要的基类来消除二义。即使将其中一个设置为私有也是徒劳,因为“改变一个类成员的访问权限不应该改变程序的含义。”
既然写程序和函数库时有这么多不同的情况会产生潜在的二义性,那么,一个好的软件开发者该怎么做呢?最根本的是,一定要时时小心它。想找出所有潜在的二义性的根源几乎是不可能的,特别是当程序员将不同的独立开发的库结合起来使用时,但在了解了导致经常产生潜在二义性的那些情况后,你就可以在软件设计和开发中将它出现的可能性降到最低。

条款27: 如果不想使用隐式生成的函数就要显式地禁止它
所谓隐式生成的函数,应该就是声称类的时候自动滚出来的那四个么,构造,希购,赋值,复制构造,他们有时候会自己闹事,与其这样,不如把他们声明出来,放到私有成员里面,然后让他们什么都不要去干!! 

条款28: 划分全局名字空间

学而时习之,不宜乐乎,不知不觉就已经看到条款28了,除去内存管理那块被我跳过去(我想当个专题找个时间专门研究)以外,也就是说,全部的五十个条款我已经看过了一半(没敢说学……),总结一下。
这本书从小到大的讲述了c++程序设计中所需要注意到的问题。
第一章的四个条款分别说服我去掉预处理,stdio头文件,malloc,free内存分配函数等基础的c操作。言之凿凿,无懈可击,除了用心遵守还能说什么呢。
第三章从一个类所能够隐式构造的几个函数讲起,并说服我在什么时候,用什么方式去显示的声明,实现和调用他们。包括为什么类声明拷贝构造函数和赋值操作符,怎样给构造函数里的冬冬赋值,类成员的声明顺序;析构函数何时为虚,重载=的时候要注意的问题。
第四章讲述类在设计和声明中需要注意到的问题,包括类的接口如何设计才尽如人意,成员函数,非成员函数和友元函数各自的使命,const的作用,穿引用的优点,重载时所遇到的三个问题(缺省值问题,0问题,二义性问题)和一个解决方案(划分全局名字空间)
从中的确受益匪浅,为日后规范c++编程奠定了一定的基础。
条款29: 避免返回内部数据的句柄
对于const成员函数来说,返回句柄是不明智的,因为它会破坏数据抽象。对于非const成员函数来说,返回句柄会带来麻烦,特别是涉及到临时对象时。句柄就象指针一样,可以是悬浮(dangle)的。所以一定要象避免悬浮的指针那样,尽量避免悬浮的句柄。
同样不能对本条款绝对化。在一个大的程序中想消灭所有可能的悬浮指针是不现实的,想消灭所有可能的悬浮句柄也是不现实的。但是,只要不是万不得已,就要避免返回句柄,这样,不但程序会受益,用户也会更信赖你。

条款30: 避免这样的成员函数:其返回值是指向成员的非const指针或引用,但成员的访问级比这个函数要低
指向非const指针或引用,就会使原被希望被隐藏或保护的冬冬暴露在用户面前,这样原本的意图毫无意义了就。
虽然前面说了那么多,有一天你可能为了程序的性能还是不得不写象上面那样的函数--------返回值是某个访问级较低的成员的指针或引用。但同时,你又不想牺牲private和protected为你提供的访问限制。这种情况下,你可以通过返回指向const对象的指针或引用来达到两全其美的效果。详细介绍参见条款21。

条款31: 千万不要返回局部对象的引用,也不要返回函数内部用new初始化的指针的引用
如果返回局部对象的引用,那个局部对象其实已经在函数调用者使用它之前被销毁了。
不要使用局部对象,可以用new来解决这个问题,但却引发了新的难题。大家都知道,为了在程序中避免内存泄漏,就必须确保对每个用new产生的指针调用delete,但是,这里的问题是,对于这个函数中使用的new,谁来进行对应的delete调用呢?
所以要记住你的教训:写一个返回废弃指针的函数无异于坐等内存泄漏的来临。
条款32: 尽可能地推迟变量的定义
如果定义了一个有构造函数和析构函数的类型的变量,当程序运行到变量定义之处时,必然面临构造的开销;当变量离开它的生命空间时,又要承担析构的开销。这意味着定义无用的变量必然伴随着不必要的开销,所以只要可能,就要避免这种情况发生。
不仅要将变量的定义推迟到必须使用它的时候,还要尽量推迟到可以为它提供一个初始化参数为止。这样做,不仅可以避免对不必要的对象进行构造和析构,还可以避免无意义的对缺省构造函数的调用。
推迟变量定义可以提高程序的效率,增强程序的条理性,还可以减少对变量含义的注释。看来是该和那些开放式模块的变量定义吻别了。
条款33: 明智地使用内联
条款34: 将文件间的编译依赖性降至最低
完全受不了了,33和三十四对于设计大型的软件来说至关重要,但是对于现在连编个小程序都要不停调试很久才能通过的我来说就一点用处都没有了……
看是砍下来了,想要深究,不好意思,等我用到的时候再说!!先看继承,这个最弱!

条款35: 使公有继承体现 "是一个" 的含义
从公有类A中继承下来的冬冬B是一个A
听不懂?
好,人(A)生下来的孩子(B)是一个人(A)
懂了吧?
而且,因为孩子是一个人,所以孩子能干的事情都是人干得!(撒娇,尿床,是个人都能~)
但是,因为不是每一个人都是孩子,所以不是每一个人会干得事情孩子都能干~
言归正传,只要是程序中有对A的调用,对其赋以B是不会出错的……
但是由于语言的严谨性,就会出现问题,比如正方形(B)是长方形的继承(A)
也就是说,长方形中所有的函数正方形都可以执行喽,假如有个函数叫做
长变宽不变(长方形*)//声明
按理说就可以这样赋值 长变宽不变(正方形)//对A的调用赋以B
可以么(杨丞灵)语……
废话,不可以……
所以说,在这里,正方形不是长方形地~~
“公有继承的精彩世界,在这里,你在其它研究领域养成的直觉 ---- 包括数学 ---- 可能不象你所期望的那样为你效劳。对于上面例子中的情况来说,最根本的问题在于:对矩形适用的规则(宽度的改变和高度没关系)不适用于正方形(宽度和高度必须相同)。但公有继承声称:对基类对象适用的任何东西 ---- 任何!---- 也适用于派生类对象。”

条款36: 区分接口继承和实现继承
纯虚函数最显著的特征是:它们必须在继承了它们的任何具体类中重新声明,而且它们在抽象类中往往没有定义。把这两个特征放在一起,就会认识到:
· 定义纯虚函数的目的在于,使派生类仅仅只是继承函数的接口。
有时,声明一个除纯虚函数外什么也不包含的类很有用。这样的类叫协议类(Protocol class),它为派生类仅提供函数接口,完全没有实现。
简单虚函数的情况和纯虚函数有点不一样。照例,派生类继承了函数的接口,但简单虚函数一般还提供了实现,派生类可以选择改写它们或不改写它们。
· 声明简单虚函数的目的在于,使派生类继承函数的接口和缺省实现。
当一个成员函数为非虚函数时,它在派生类中的行为就不应该不同。实际上,非虚成员函数表明了一种特殊性上的不变性,因为它表示的是不会改变的行为 ---- 不管一个派生类有多特殊。
· 声明非虚函数的目的在于,使派生类继承函数的接口和强制性实现。
条款39: 避免 "向下转换" 继承层次
这条讲的就是前面说的第12条:不能使用基类的指针对象调用派生类的方法。
而方法就是用static_cast将基类的指针显式的转换成为派生类的指针,这就叫做向下转换了……
nnd,为什么又要避免这种转换呢,因为向下转换难看、容易导致错误,而且使得代码难于理解、升级和维护
"向下转换" 可以通过几种方法来消除。最好的方法是将这种转换用虚函数调用来代替,同时,它可能对有些类不适用,所以要使这些类的每个虚函数成为一个空操作。第二个方
法是加强类型约束,使得指针的声明类型和你所知道的真的指针类型之间没有出入。为了消除向下转换,无论费多大工夫都是值得的

条款40: 通过分层来体现 "有一个" 或 "用...来实现"
使某个类的对象成为另一个类的数据成员,从而实现将一个类构筑在另一个类之上,这一过程称为 "分层"(Layering)。
"分层" 这一术语有很多同义词,它也常被称为:构成(composition),包含(containment)或嵌入(embedding)。
直接用“是一个”的概念不能解决类似问题
神经病人是一个人,所以一切人能做的事情他都能做,可是因为它是神经病人,所以确实有些东西他不能做或相同的事情和人做的不一样,这样是一个就不适用了
要说神经病人使用一个人能做的事情来实现的,也就是说,告诉神经病人,你可以调用任何人的行为……
正因为它是神经病人,他能做的事情他可以自己去可以调用人的行为,他做相同的事情和人做的不一样,那么即使他可以调用,他也不会调用,他会以他的方式去做。
这就使是一个和用……来实现的区别(例子略显粗鲁,抱歉)

条款41: 区分继承和模板
区分继承与模版的关系,便是区分“类的行为”和“类所操作的对象的类型”之间的关系。
模版:即使对T一无所知,你还是能够写出每个成员函数。为操作所写的代码不会变。类的行为在任何地方都不依赖于T。这就是模板类的特点:行为不依赖于类型。
继承:必须为每种不同的类实现不同的行为。不可能写一个函数来处理所有的类,所能做的只能是制定一个函数接口,所有的类都必须实现它。
衍生一个函数接口的方法只能是去声明一个纯虚函数。这就需要使用继承了
结论:
当对象的类型不影响类中函数的行为时,就要使用模板来生成这样一组类。
当对象的类型影响类中函数的行为时,就要使用继承来得到这样一组类。

公有继承告一段落……
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值