C++ 的优点是什麽?
「封装性 encapsulation」:藉由隐藏内部的资料结构,让我们可以改变系统的某部份,而不必更动其他部份。我们为软体元件(称之为 class,类别)提供一个安全的介面,用户只碰得到这个介面而已;而相对起来比较容易变动的介面「实作」部份,就被封装起来(就像被包在胶囊里),以避免用户过於依赖他一时的实作决定。在比较简单的 C 里头,可由模组内的静态(static)资料来办到,以避免其他模组存取
到它。
C++ 对 ANSI-C 回溯相容吗?
几乎是。C++ 尽可能地和 C 相容,但不能更相容了。事实上,主要的不同在於 C++ 要求函数原型:"f()" 宣告的是无参数的函数(在 C 里,"f()" 和 "f(...)" 是一样的)。还有些细微的差别,像在 C++ 里 sizeof('x') 等同於 sizeof(char),但在 C 里
面却是等同於 sizeof(int)。 而且,C++ 直接就把结构的标签(tag)当成是型别的名字,但 C 就需要加个 "struct" 字("typedef struct Fred Fred" 这种技巧仍然能用,但在 C++ 中是累赘的)。
什麽是物件(object)?
一块赋有某种语意的储存空间。在宣告 "int i;" 之後,我们称「i 是个 int 型态的物件」。在 C++/OOP 里,「物件」通常意指「类别的案例(an instance of a class)」,因此类别定义了数个物件(案例)的行为。
如果设定某值给参考会怎麽样?
会更动到被参考者(referrent,该「参考」所参考到的物件)。记住:「参考」就是「被参考者」,因此动了参考就会改动到被参考者(「参考」是「被参考者」的左值 "Lvalue"〔出现在设定陈述的左边〕)。更进一步,我们也允许参考被传回。这样子函数呼叫就可放在设定陈述的左边,这对运算子多载的场合很有用。
怎样才能将参考改设成别的物件?
没有办法。和指标不同,一旦参考被系结到某个物件,它就不能再被改设到其他物件去。「参考」本身不是一个物件(它自己没有位址;「取参考的位址」只会得到被参考者的位址;切记:「参考」就是「被参考者」)。 将「参考」与「被参考者」分离开来是不可能的。
何时该用参考,何时又该用指标?
可以时,用参考;必要时,就用指标。当你不需要“重设”它时(见前一个问题),参考会比指标好。这通常意味著:在物件类别的公共介面中参考最有用。参考大多用於物件的表层,而指标则多用於里层。但有一个例外:当函数参数或传回值需要一个「临界」(sentinel)的参考值时,最好是用指标来做,以 NULL 指标做为一个特别值(「参考」应该是个实质物件的「别名」,而不是个解参用的〔dereferenced〕NULL 指标)。
建构子(constructor)是做什麽的?
建构子乃用来从零开始建立物件。建构子就像个「初始化函数」;它把一堆散乱的位元组成一个活生生的物件。最低限度它会初始化内部用到的栏位,也可能会配置所须的资源(记忆体、档案、semaphore、socket 等等)。"ctor" 是建构子 constructor 最常见的缩写。
怎样才能让建构子呼叫另一个同处一室的建构子?
没有办法。原因是:如果你呼叫另一个建构子,编译器会初始化一个暂时的区域性物件;但并没有初始化“这个”你想要的物件。你可以用预设参数(default parameter),将两个建构子合并起来,或是在私有的 "init()" 成员函数中共享它们的程式码。
解构子(destructor)是做什麽的?
解构子乃物件之葬礼。解构子是用来释放该物件所配置到的资源,譬如:Lock 类别可能会锁住一个semaphore,解构子则用来释放它。最常见的例子是:当建构子用了 "new" 以後,解构子用 "delete"。解构子是个「去死吧」的运作行为(method),通常缩写为 "dtor"。
怎样做一个 "**"「次方」运算子?
无解。运算子的名称、优先序、结合律以及元数(arity)都被语言所定死了。C++ 里没有"**" 运算子,所以你无法替类别订做一个它。还怀疑的话,考虑看看 "x ** y" 和 "x * (*y)",这两者是完全一样的(换句话说,编译器会假设 "y" 是个指标)。此外,运算子多载只是函数呼叫的语法糖衣而已,虽然甜甜的,但本质上并未增加什麽东西。我建议你多载 "pow(base,exponent)"这个函数(它的倍精确度版本在中)。附带一提:operator^ 可以用,但它的优先序及结合律不符「次方」所需。
「夥伴」违反了封装性吗?
若善用之,反而会「强化」封装性。我们经常得将一个类别切成两半,当这两半各有不同的案例个数及生命期时。在此情形之下,它们通常需要直接存取对方的内部(这两半“本来”是在同一个类别里面,所以你并未“增加”存取资料结构的运作行为个数;你只是在“搬动”这些运作行为
所在之处而已)。最安全的实作方式,就是让这两半互为彼此的「夥伴」。
「夥伴关系无继承及递移性」是什麽意思?
夥伴关系的特权性无法被继承下来:夥伴的衍生类别不必然还是夥伴(我把你当朋友,但这不代表我也一定会信任你的孩子)。如果 "Base" 类别宣告了 "f()" 为它的夥伴,"f()" 并不会自动对由 "Base" 衍生出来的 "Derived" 类别所多出来的部份拥有特殊的存取权力。夥伴关系的特权无递移性:夥伴类别的夥伴不必然还是原类别的夥伴(朋友的朋友不一定也是朋友)。譬如,如果 "Fred" 类别宣告了"Wilma" 类别为它的夥伴,而且"Wilma" 类别宣告了 "f()" 为它的夥伴, 则 "f()" 不见得对 "Fred" 有特殊的存取权力。
"delete p" 会删去 "p" 指标,还是它指到的资料,"*p" ?
该指标指到的资料。"delete" 真正的意思是:「删去指标所指到的东西」(delete the thing pointedto by)。同样的英文误用也发生在 C 语言的「『释放』指标所指向的记忆体」上("free(p)" 真正的意思是:"free_the_stuff_pointed_to_by(p)" )。
为什麽该用 "new" 而不是老字号的 malloc() ?
建构子/解构子、型别安全性、可被覆盖(overridability)。建构子/解构子:和 "malloc(sizeof(Fred))" 不同,"new Fred()" 还会去呼叫Fred 的建构子。同理,"delete p" 会去呼叫 "*p" 的解构子。型别安全性:malloc() 会传回一个不具型别安全的 "void*",而 "new Fred()" 则会传回正确型态的指标(一个 "Fred*")。可被覆盖:"new" 是个可被物件类别覆盖的运算子,而 "malloc" 不是以「各个类别」作为覆盖的基准。
我该怎样配置/释放阵列?
用 new[] 和 delete[] :
Fred* p = new Fred[100];
//...
delete [] p;
每当你在 "new" 运算式中用了 "[...]", 你就必须在 "delete" 陈述中使用 "[]"。这语法是必要的,因为「指向单一元素的指标」与「指向一个阵列的指标」在语法上并无法区分开来。
怎样确保某类别的物件都是用 "new" 建立的,而非区域或整体/静态变数?
确定该类别的建构子都是 "private:" 的, 并定义个 "friend" 或 "static" 函数,来传回一个指向由 "new" 建造出来的物件(把建构子设成 "protected:",如果你想要有衍生类别的话)。
怎样处理建构子的错误?
丢出一个例外(throw an exception)。
我该早一点还是晚一点让东西有常数正确性?
越越越早越好。
什麽是「const 成员函数」?
一个只检测(而不更动)其物件的成员函数。
何时该用继承?
做为一个「特异化」(specialization) 的机制。人类以两种角度来抽象化事物:「部份」(part-of) 和「种类」(kind-of)。福特汽车“是一种”(is-a-kind-of-a) 车子,福特汽车“有”(has-a) 引擎、轮胎……等等零件。「部份」的层次随著 ADT 的流行,已成为软体系统的一份子了;而「继承」则添入了“另一个”重要的软体分解角度。
我该遮蔽住由基底类别继承来的公共成员函数吗?
绝对绝对绝对绝对不要这样做!想去遮蔽(删去、撤消)掉继承下来的公共成员函数,是个很常见的错误。这通常是脑袋塞满了浆糊的人才会做的傻事。
当我改变了内部的东西,怎样避免子类别被破坏?
物件类别有两个不同的介面,提供给不同种类的用户:
* "public:" 介面用以服务不相关的类别。
* "protected:" 介面用以服务衍生的类别。
除非你预期所有的子类别都会由你们的工作小组建出来,否则你应该将基底类别的资料位元内容放在 "private:" 处,用 "protected:" 行内存取函数来存取那些资料。这样的话,即使基底类别的私有资料改变了,衍生类别的程式也不会报废,除非你改变了基底类别的protected 处的存取函数。
若基底类别的建构子呼叫一个虚拟函数,为什麽衍生类别覆盖掉的那个虚拟函数却不会被呼叫到?
在基底类别 Base 的建构子执行过程中,该物件还不是属於衍生 Derived 的,所以如果 "Base::Base()" 呼叫了虚拟函数 "virt()",则 "Base::virt()" 会被呼叫,即使真的有 "Derived::virt()"。类似的道理,当 Base 的解构子执行时,该物件不再是个 Derived 了,所以当Base::~Base() 呼叫 "virt()",则 "Base::virt()" 会被执行,而非覆盖後的版本"Derived::virt()"。
分离介面与实作是做什麽用的?
介面是企业体最有价值的资源。设计介面会比只把一堆独立的类别拼凑起来来得耗时,尤其是:介面需要花费更高阶人力的时间。既然介面是如此重要,它就应该保护起来,以避免被资料结构等等实作细节之变更所影响。因此你应该将介面与实作分离开来。
何时该把解构子弄成 virtual?
当你可能经由基底的指标去 "delete" 掉衍生的类别时。
为什麽我的执行档会这麽大?
很多人对这麽大的执行档感到惊讶,特别是当原始码只有一点点而已。例如一个简单的 "hello world" 程式居然会产生大家都想不到的大小(40+K bytes)。一个原因是:有些 C++ 执行期程式库被连结进去了。有多少被连结进去,就要看看你用到多少,以及编译器把程式库切割成多少块而定。例如,iostream 很大,包含一大堆类别及虚拟函数,即使你只用到一点点,因为各元件之间的交互参考依存关系,可能会把整个 iostream 程式码都塞进来了。(【译注】如果 linker 做得好的话,应该能把完全用不到的元件 object code 砍掉,不随之塞入你的执行档中。)不要用静态的,改用动态连结的程式库版本,就可以使你的程式变小。