面向对象基础讲座1

    技术2022-05-11  112

    建构子乃用来从零开始建立对象。 建构子就像个「初始化函数」;它把一堆散乱的字节成一个活生生的对象。最低限 度它会初始化内部用到的字段元,也可能会配置所须的资源(内存、档案、semaphore 、socket 等等)。 "ctor" 是建构子 constructor 最常见的缩写。 ======================================== Q18:怎样才能让建构子呼叫另一个同处一室的建构子? 没有办法。 原因是:如果你呼叫另一个建构子,编译器会初始化一个暂时的区域性对象;但并没 有初始化“这个”你想要的对象。你可以用预设参数(default parameter),将两 个建构子合并起来,或是在私有的 "init()" 成员函数中共享它们的程序代码。 ======================================== Q19:解构子(destructor)是做什么的? 解构子乃对象之葬礼。 解构子是用来释放该对象所配置到的资源,譬如:Lock 类别可能会锁住一个 semaphore,解构子则用来释放它。最常见的例子是:当建构子用了 "new" 以后,解 构子用 "delete"。 解构子是个「去死吧」的运作行为(method),通常缩写为 "dtor"。 ========================= ■□ 第6节:运操作数多载 ========================= Q20:运操作数多载(operator overloading)是做什么的? 它可让使用类别的人以直觉来操作之。 运操作数多载让 C/C++ 的运操作数,能对自订的型态(对象类别)赋予自订的意义。它 们形同是函数呼叫的语法糖衣 (syntactic sugar): class Fred { public: //... }; #if 0 Fred add(Fred, Fred); //没有运操作数多载 Fred mul(Fred, Fred); #else Fred operator+(Fred, Fred); //有运操作数多载 Fred operator*(Fred, Fred); #endif Fred f(Fred a, Fred b, Fred c) { #if 0 return add(add(mul(a,b), mul(b,c)), mul(c,a)); //没有... #else return a*b + b*c + c*a;             //有... #endif } ======================================== Q21:哪些运操作数可以/不能被多载? 大部份都可以被多载。 不能的 C 运操作数有 "." 和 "?:"(和以技术上来说,可算是运操作数的 "sizeof")。 C++ 增加了些自己的运操作数,其中除了 "::" 和 ".*". 之外都可以被多载。 底下是个足标(subscript)运操作数的例子(它会传回一个参考)。最前面是“不用 ”多载的: class Array { public: #if 0 int& elem(unsigned i) { if (i>99) error(); return data[i]; }    #else int& operator[] (unsigned i) { if (i>99) error(); return data[i]; } #endif    private: int data[100]; }; main() { Array a; #if 0 a.elem(10) = 42; a.elem(12) += a.elem(13); #else a[10] = 42; a[12] += a[13]; #endif } ======================================== Q22:怎样做一个 "**"「次方」运操作数? 无解。 运操作数的名称、优先序、结合律以及元数(arity)都被语言所定死了。C++ 里没有 "**" 运操作数,所以你无法替类别订做一个它。 还怀疑的话,考虑看看 "x ** y" 和 "x * (*y)",这两者是完全一样的(换句话说 ,编译器会假设 "y" 是个指针)。此外,运操作数多载只是函数呼叫的语法糖衣而已 ,虽然甜甜的,但本质上并未增加什么东西。我建议你多载 "pow(base,exponent)" 这个函数(它的倍精确度版本在 <math.h> 中)。 附带一提:operator^ 可以用,但它的优先序及结合律不符「次方」所需。 =================== ■□ 第7节:伙伴 =================== Q23:伙伴(friend)是什么? 让别的类别或函数能存取到你的类别内部的东西。 伙伴可以是函数或其它类别。类别会对它的伙伴开放存取权限。正常情况下,程序员 会下意识﹑技术性地控制该类别的伙伴与运作行为(否则当你想更动类别时,还得先 有其它部份的拥有者之同意才行)。 ======================================== Q24:「伙伴」违反了封装性吗? 若善用之,反而会「强化」封装性。 我们经常得将一个类别切成两半,当这两半各有不同的案例个数及生命期时。在此情 形之下,它们通常需要直接存取对方的内部(这两半“本来”是在同一个类别里面, 所以你并未“增加”存取数据结构的运作行为个数;你只是在“搬动”这些运作行为 所在之处而已)。最安全的实作方式,就是让这两半互为彼此的「伙伴」。 若你如上述般的使用伙伴,你依然是将私有的东西保持在私有的状态。遇到上述的情 况,如果还呆呆的想避免使用伙伴关系,许多人不是采用公共资料(糟透了!),就 是弄个公共的 get/set 存取运作行为来存取彼此的资料,事实上这些都破坏了封装 性。只有在类别的外面该私有资料「仍有其意义」(以使用者的角度来看)时,开放 出私有资料的存取运作行为才称得上是恰当的做法。多数情况下,「存取运作行为」 就和「公共资料」一样糟糕:它们对私有资料成员只隐其“名”而已,却未隐藏其“ 存在”。 同样的,如果将「伙伴函数」做为另一种类别公共存取函数的语法,那就和违反封装 性的成员函数一样破坏了封装。换句话说,对象类别的伙伴及成员都是「封装的界线 」,如同「类别定义」本身一样。 ======================================== Q25:伙伴函数的优缺点? 它提供了某种接口设计上的自由。 成员函数和伙伴函数都有同等的存取特权(100% 的权利),主要的差别在于:伙伴 函数用起来像是 "f(x)",而成员函数则是 "x.f()"。因此,伙伴函数可让对象类别 设计者挑选他看得最顺眼的语法,以降低维护成本。 伙伴函数主要的缺点在于:当你想做动态系结(dynamic binding)时,它需要额外 的程序代码。想做出「虚拟伙伴」的效果,该伙伴函数应该呼叫个隐藏的(通常是放在 "protected:" 里)虚拟成员函数;像这个样子:"void f(Base& b) { b.do_f(); }" 。衍生类别会覆盖(override)掉那个隐藏的成员函数("void Derived::do_f()") ,而不是该伙伴函数。 ======================================== Q26:「伙伴关系无继承及递移性」是什么意思? 伙伴关系的特权性无法被继承下来:伙伴的衍生类别不必然还是伙伴(我把你当朋友 ,但这不代表我也一定会信任你的孩子)。如果 "Base" 类别宣告了 "f()" 为它的 伙伴,"f()" 并不会自动对由 "Base" 衍生出来的 "Derived" 类别所多出来的部份 拥有特殊的存取权力。 伙伴关系的特权无递移性:伙伴类别的伙伴不必然还是原类别的伙伴(朋友的朋友不 一定也是朋友)。譬如,如果 "Fred" 类别宣告了 "Wilma" 类别为它的伙伴,而且 "Wilma" 类别宣告了 "f()" 为它的伙伴,则 "f()" 不见得对 "Fred" 有特殊的存取 权力。 ======================================== Q27:应该替类别宣告个成员函数,还是伙伴函数? 可能的话,用成员函数;必要时,就用伙伴。 有时在语法上来看,伙伴比较好(譬如:在 "Fred" 类别中,伙伴函数可把 "Fred" 弄成是第二个参数,但在成员函数中则一定得放在第一个)。另一个好例子是:二元 中序式算数运操作数(譬如:"aComplex + aComplex" 可能应该定义成伙伴而非成员函 数,因为你想让 "aFloat + aComplex" 这种写法也能成立;回想一下:成员函数无 法提升它左侧的参数,因为那样会把引发该成员函数的对象所属之类别给改变掉)。 在其它情况下,请选成员函数而不要用伙伴函数。 ==================================================== ■□ 第8节:输入/输出:<iostream.h> 和 <stdio.h> ==================================================== Q28:该怎样替 "class Fred" 提供输出功能? 用伙伴函数 operator<<: class Fred { public: friend ostream& operator<< (ostream& o, const Fred& fred) { return o << fred.i; } //...   private:  int i; //只为了说明起见而设的 }; 我们用伙伴而不用成员函数,因为 "Fred" 是第二个参数而非第一个。输入的功能亦 类似,只是要改写成: istream& operator>> (istream& i, Fred& fred); // ^^^^^------- 不是 "const Fred& fred"! ======================================== Q29:为什么我该用 <iostream.h> 而不是以前的 <stdio.h>? 增加型别安全、减少错误、增进效率、有延展性、提供衍生能力。 Printf 还好,而 scanf 除了容易写错之外也还算可以,然而和 C++ 的 I/O 系统相 比,它们都有其限制。C++ 的 I/O(用 "<<" 及 ">>" ),和 C( "printf()" 和 "scanf()" )相比:  * 型别安全--要做 I/O 的对象,编译器会静态地事先得知其型别,而不是动态地 由 "%" 一栏查知。  * 不易出错--冗余的信息会增加错误的机会。C++ 的 I/O 就不需要多余的 "%"。  * 更快速--printf 是个小型语言的「解译器」,该语言主要是由 "%" 这种东西 构成的;在执行期它用这些字段来选择正确的格式化方式。C++ 的 I/O 系统则是 静态的依各自变量真正的型别来挑选子程序,以增进执行效率。  * 延展性--C++ I/O 机制可在不改动原有程序代码的情况下,就加进使用者新设计 的型态(能想象如果大家同时把互不兼容的 "%" 字段塞入 printf 和 scanf,会 是怎样的混乱场面?!)。  * 可衍生(subclassable)--ostream 和 istream(C++ 的 FILE* 代替品)都是 真正的类别,因此可以被衍生下去。这意味着:你可以让其它自定的东西有着和 stream 雷同的外表与行为,但实际上做的却是你想做的特定事情。你自动就重用 了数以万计别人(你甚至不认识它们)写好的 I/O 程序代码,而他们也不需要知道 你所做的「延伸 stream」类别。 ======================================== Q30:为什么我处理输入时,会超过档案的结尾? 因为 eof(档案结尾)的状态,是到「将要超过档案结尾的动作」才会被设定。也就 是说,读档案的最后一个字节并不会设定 eof 的状态。 【译注】这也是 C 常见的错误。 如果你的程序像这样: int i = 0; while (! cin.eof()) { cin >> x; ++i; // work with x } 你的 i 变量就会多了一。 你真正该做的是这样: int i; while (cin >> x) { ++i; // work with x } ======================================== Q31:为什么我的程序执行完第一次循环后,会对输入的要求不加理睬? 因为读取数值的程序,把非数字的字符留在输入缓冲区 (input buffer) 里头了。 【译注】这也是 C,甚至 Pascal 常见的错误。 如果你的程序如下: char name[1000]; int age; for (;;) { cout << "Name: "; cin >> name; cout << "Age: "; cin >> age; } 你应该这样写: for (;;) { cout << "Name: "; cin >> name; cout << "Age: "; cin >> age; cin.ignore(INT_MAX, '/n'); } ======================================== Q32:在 DOS 及 OS/2 的 binary 模式下,要怎样来 "reopen" cin 及 cout? 有这个问题,最典型的情况就是:有人想对 cin、cout 做 binary 的 I/O,但是作 业系统(像是 DOS 或 OS/2)却总是会做 CR-LF 的转换动作。 解决法:cin、cout、cerr 这些事先定义好的串流,都是 text 的串流,没有标准做 法能把它们弄成 binary 模式。把串流关掉再设法以 binary 模式 reopen 它们,可 能会导致不可预期的结果。 在这两种模式有不同行为的系统上,一定有办法让它们变成 binary 串流,但是你得 去查查该系统的文件。

    最新回复(0)