转贴:C++语言常见问题解:#33 ~ #53

类别:编程语言 点击:0 评论:0 推荐:

这是我从台湾的http://www.cis.nctu.edu.tw/chinese/doc/research/c++/C++FAQ-Chinese/发现的《C++ Frequently Asked Questions》的繁体翻译,作者是:叶秉哲,也是《C++ Programming Language》3/e繁体版的译者,该文章是非常的好,出于学习用途而将它转贴,本人未取得作者的授权,原文章的版权仍然归属原作者.

C++语言常见问题解

== Part 2/4 ============================

comp.lang.c++ Frequently Asked Questions list (with answers, fortunately).
Copyright (C) 1991-96 Marshall P. Cline, Ph.D.
Posting 2 of 4.
Posting #1 explains copying permissions, (no)warranty, table-of-contents, etc

=============================
■□ 第9节:自由内存管理
=============================

Q33:"delete p" 会删去 "p" 指针,还是它指到的资料,"*p" ?

该指针指到的资料。

"delete" 真正的意思是:「删去指针所指到的东西」(delete the thing pointed
to by)。同样的英文误用也发生在 C 语言的「『释放』指针所指向的内存」上
("free(p)" 真正的意思是:"free_the_stuff_pointed_to_by(p)" )。

========================================

Q34:我能 "free()" 掉由 "new" 配置到的、"delete" 掉由 "malloc()" 配置到的
内存吗?

不行。

在同一个程序里,使用 malloc/free 及 new/delete 是完全合法、合理、安全的;
但 free 掉由 new 配置到的,或 delete 掉由 malloc 配置到的指针则是不合法、
不合理、该被痛骂一顿的。

========================================

Q35:为什么该用 "new" 而不是老字号的 malloc() ?

建构子/解构子、型别安全性、可被覆盖(overridability)。

建构子/解构子:和 "malloc(sizeof(Fred))" 不同,"new Fred()" 还会去呼叫
Fred 的建构子。同理,"delete p" 会去呼叫 "*p" 的解构子。

型别安全性:malloc() 会传回一个不具型别安全的 "void*",而 "new Fred()" 则
会传回正确型态的指针(一个 "Fred*")。

可被覆盖:"new" 是个可被对象类别覆盖的运操作数,而 "malloc" 不是以「各个类别
」作为覆盖的基准。

========================================

Q36:为什么 C++ 不替 "new" 及 "delete" 搭配个 "realloc()" ?

避免你产生意外。

当 realloc() 要拷贝配置区时,它做的是「逐位 bitwise」的拷贝,这会弄坏大
部份的 C++ 对象。不过 C++ 的对象应该要能自我拷贝才对:用它们自己的拷贝建构
子或设定运操作数。

========================================

Q37:我该怎样配置/释放数组?

用 new[] 和 delete[] :

Fred* p = new Fred[100];
//...
delete [] p;

每当你在 "new" 表达式中用了 "[...]",你就必须在 "delete" 陈述中使用 "[]"。
^^^^
这语法是必要的,因为「指向单一元素的指针」与「指向一个数组的指针」在语法上
并无法区分开来。

========================================

Q38:万一我忘了将 "[]" 用在 "delete" 由 "new Fred[n]" 配置到的数组,会发生
什么事?

灾难。

这是程序者的--而不是编译器的--责任,去确保 new[] 与 delete[] 的正确配
对。若你弄错了,编译器不会产生任何编译期或执行期的错误讯息。堆积(heap)被
破坏是最可能的结局,或是更糟的,你的程序会当掉。

========================================

Q39:成员函数做 "delete this" 的动作是合法的(并且是好的)吗?

只要你小心的话就没事。

我所谓的「小心」是:

1) 你得 100% 确定 "this" 是由 "new" 配置来的(而非 "new[]",亦非自订的
"new" 版本,一定要是最原始的 "new")。

2) 你得 100% 确定该成员函数是此对象最后一个会呼叫到的。

3) 做完自杀的动作 ("delete this;") 后,你不能再去碰 "this" 的对象了,包
括资料及运作行为在内。

4) 做完自杀的动作 ("delete this;") 后,你不能再去碰 "this" 指针了。
换句话说,你不能查看它﹑将它与其它指针或是 NULL 相比较﹑印出其值﹑
对它转型﹑对它做任何事情。

很自然的,这项警告也适用于:当 "this" 是个指向基底类别的指针,而解构子不是
virtual 的场合。

========================================

Q40:我该怎么用 new 来配置多维数组?

有很多方法,端视你对数组大小的伸缩性之要求而定。极端一点的情形,如果你在编
译期就知道所有数组的维度,你可以静态地配置(就像 C 一样):

class Fred { /*...*/ };

void manipulateArray()
{
Fred matrix[10][20];

//使用 matrix[i][j]...

//不须特地去释放该数组
}

另一个极端情况,如果你希望该矩阵的每个小块都能不一样大,你可以在自由内存
里配置之:

void manipulateArray(unsigned nrows, unsigned ncols[])
//'nrows' 是该数组之列数。
//所以合法的列数为 (0, nrows-1) 开区间。
//'ncols[r]' 则是 'r' 列的行数 ('r' 值域为 [0..nrows-1])。
{
Fred** matrix = new Fred*[nrows];
for (unsigned r = 0; r < nrows; ++r)
matrix[r] = new Fred[ ncols[r] ];

//使用 matrix[i][j]...

//释放就是配置的反动作:
for (r = nrows; r > 0; --r)
delete [] matrix[r-1];
delete [] matrix;
}

========================================

Q41:C++ 能不能做到在执行时期才指定数组的长度?

可以。STL 有一个 vector template 提供这种行为。请参考“链接库”一节的 STL
项目。

不行。内建的数组型态必须在编译期就指定它的长度了。

可以,内建的数组可以在执行期才指定第一个索引的范围。譬如说,和上一则 FAQ
相较,如果你只需要第一个维度大小能够变动,你可以 new 一个数组的数组(而不
是数组指针的数组 "an array of pointers to arrays"):

const unsigned ncols = 100;
//'ncols' 不是执行期才决定的变量 (用来代表数组的行数)

class Fred { ... };

void manipulateArray(unsigned nrows)
//'nrows' 是执行期才决定的变量 (用来代表数组的列数)
{
Fred (*matrix)[ncols] = new Fred[nrows][ncols];

//用 matrix[i][j] 来处理

//deletion 是对象配置的逆运算:
delete [] matrix;
}

如果你不光是需要在执行期改变数组的第一个维度的话,就不能这样做了。

========================================

Q42:怎样确保某类别的对象都是用 "new" 建立的,而非区域或整体/静态变量?

确定该类别的建构子都是 "private:" 的,并定义个 "friend" 或 "static" 函数,
来传回一个指向由 "new" 建造出来的对象(把建构子设成 "protected:",如果你想
要有衍生类别的话)。

class Fred { //只允许 Fred 动态地配置出来
public:
static Fred* create() { return new Fred(); }
static Fred* create(int i)          { return new Fred(i); }
static Fred* create(const Fred& fred) { return new Fred(fred); }
private:
Fred();
Fred(int i);
Fred(const Fred& fred);
virtual ~Fred();
};

main()
{
Fred* p = Fred::create(5);
...
delete p;
 }


===============================
■□ 第10节:除错与错误处理
===============================

Q43:怎样处理建构子的错误?

丢出一个例外(throw an exception)。

建构子没有传回值,所以不可能采用它传回的错误码。因此,侦测建构子错误最好的
方法,就是丢出一个例外。

在 C++ 编译器尚未提供例外处理之前,我们可先把对象置于「半熟」的状态(譬如
:设个内部的状态位),用个查询子("inspector")来检查该位,就可让用户
查看该对象是否还活着。也可以用另一个成员函数来检查该位,若该对象没存活
下来,就做个「没动作」(或是更狠的像是 "abort()" )的程序。但这实在很丑陋。

========================================

Q44:如果建构子会丢出例外的话,该怎么处理它的资源?

对象里面的每个资料成员,都该自己收拾残局。

如果建构子丢出一个例外的话,该对象的解构子就“不会”执行。如果你的对象得回
复些曾做过的事情(像是配置内存、开启档案、锁定 semaphore),该对象内的资
料成员就“必须”记住这个「必须恢复的东西」。

举例来说:不要单单的把配置到的内存放入 "Fred*" 资料成员,而要放入一个「
聪明的指针」(smart pointer) 资料成员中;当该“聪明指针”死掉的话,它的解构
子就会删去 Fred 对象。

【译注】「聪明的指针」(smart pointer) 在 Q4 中有提到一点。


=============================
■□ 第11节:Const 正确性
=============================

Q45:什么是 "const correctness"?

好问题。

「常数正确性」乃使用 "const" 关键词,以确保常数对象不会被更动到。譬如:若
"f()" 函数接收一个 "String",且 "f()" 想确保 "String" 不会被改变,你可以:

 * 传值呼叫 (pass by value): void f( String s ) { /*...*/ }
 * 透过常数参考 (reference): void f(const String& s ) { /*...*/ }
 * 透过常数指针 (pointer) : void f(const String* sptr) { /*...*/ }
 * 但不能用非常数参考 : void f( String& s ) { /*...*/ }
 * 也不能用非常数指针  : void f( String* sptr) { /*...*/ }

在接收 "const String&" 参数的函数里面,想更动到 "s" 的话,会产生个编译期的
错误;没有牺牲任何执行期的空间及速度。

宣告 "const" 参数也是另一种型别安全方法,就像一个常数字符串,它会“丧失”各
种可能会变更其内容的行为动作。如果你发现型别安全性质让你的系统正确地运作
(这是真的;特别是大型的系统),你会发现「常数正确性」亦如是。

========================================

Q46:我该早一点还是晚一点让东西有常数正确性?

越越越早越好。

延后补以常数正确性,会导致雪球效应:每次你在「这儿」用了 "const",你就得在
「那儿」加上四个以上的 "const"。

========================================

Q47:什么是「const 成员函数」?

一个只检测(而不更动)其对象的成员函数。

class Fred {
public:
void f() const;
}; // ^^^^^--- 暗示说 "fred.f()" 不会改变到 "fred"

此乃意指:「抽象层次」的(用户可见的)对象状态不被改变(而不是许诺:该对象
的「每一个位内容」都不会被动到)。C++ 编译器不会对你许诺「每一个位」这
种事情,因为不是常数的别名(alias)就可能会修改对象的状态(把 "const" 指针
黏上某个对象,并不能担保该对象不被改变;它只能担保该对象不会「被该指针的动
作」所改变)。

【译注】请逐字细读上面这句话。

"const" 成员函数常被称作「查询子」(inspector),不是 "const" 的成员函数则
称为「更动子」(mutator)。

========================================

Q48:若我想在 "const" 成员函数内更新一个「看不见的」资料成员,该怎么做?

使用 "mutable" 或是 "const_cast"。
【译注】这是很新的 ANSI C++ RTTI (RunTime Type Information) 规定,Borland
C++ 4.0 就率先提供了 const_cast 运操作数。

少数的查询子需要对资料成员做些无害的改变(譬如:"Set" 对象可能想快取它上一
回所查到的东西,以加速下一次的查询)。此改变「无害」是指:此改变不会由对象
的外部接口察觉出来(否则,该运作行为就该叫做更动子,而非查询子了)。

这类情况下,会被更动的资料成员就该被标示成 "mutable"(把 "mutable" 关键词
放在该资料成员宣告处前面;也就是和你放 "const" 一样的地方),这会告诉编译
器:此数据成员允许 const 成员函数改变之。若你不能用 "mutable" 的话,可以用
"const_cast" 把 "this" 的「常数性」给转型掉。譬如,在 "Set::lookup() const"
里,你可以说:

Set* self = const_cast<Set*>(this);

这行执行之后,"self" 的位内容就和 "this" 一样(譬如:"self==this"),但
是 "self" 是一个 "Set*" 而非 "const Set*" 了,所以你就可以用 "self" 去修改
"this" 指针所指向的对象。

========================================

Q49:"const_cast" 会不会丧失最佳化的可能?

理论上,是;实际上,否。

就算编译器没真正做好 "const_cast",欲避免 "const" 成员函数被呼叫时,会造成
缓存器快取区被清空的唯一方法,乃确保没有任何「非常数」的指针指向该对象。这
种情况很难得会发生(当对象在 const 成员函数被启用的范围内被建立出来;当所
有非 const 的成员函数在对象建立间启用,和 const 成员函数的启用被静态系结住
;当所有的启用也都是 "inline";当建构子本身就是 "inline";和当建构子所呼叫
的任何成员函数都是 inline 时)。

【译注】这一段话很难翻得好(好啦好啦!我功力不足... :-< ),所以附上原文:
Even if a compiler outlawed "const_cast", the only way to avoid flushing
the register cache across a "const" member function call would be to
ensure that there are no non-const pointers that alias the object. This
can only happen in rare cases (when the object is constructed in the scope
of the const member fn invocation, and when all the non-const member
function invocations between the object's construction and the const
member fn invocation are statically bound, and when every one of these
invocations is also "inline"d, and when the constructor itself is "inline"d,
and when any member fns the constructor calls are inline).


=====================
■□ 第12节:继承
=====================

Q50:「继承」对 C++ 来说很重要吗?

是的。

「继承」是抽象化资料型态(abstract data type, ADT)与 OOP 的一大分野。

========================================

Q51:何时该用继承?

做为一个「特异化」(specialization) 的机制。

人类以两种角度来抽象化事物:「部份」(part-of) 和「种类」(kind-of)。福特汽
车“是一种”(is-a-kind-of-a) 车子,福特汽车“有”(has-a) 引擎、轮胎……等
等零件。「部份」的层次随着 ADT 的流行,已成为软件系统的一份子了;而「继承
」则添入了“另一个”重要的软件分解角度。

========================================

Q52:怎样在 C++ 中表现出继承?

用 ": public" 语法:

class Car : public Vehicle {
//^^^^^^^^---- ": public" 读作「是一种」("is-a-kind-of-a")
//...
};

我们以几种方式来描述上面的关系:

 * Car 是「一种」("a kind of a") Vehicle
 * Car 乃「衍生自」("derived from") Vehicle
 * Car 是个「特异化的」("a specialized") Vehicle
 * Car 是 Vehicle 的「子类别」("subclass")
 * Vehicle 是 Car 的「基底类别」("base class")
 * Vehicle 是 Car 的「父类别」("superclass") (这不是 C++ 界常用的说法)
【译注】"superclass" 是 Smalltalk 语言的关键词。

========================================

Q53:把衍生类别的指针转型成指向它的基底,可以吗?

可以。

衍生类别是该基底类别的特异化版本(衍生者「是一种」("a-kind-of") 基底)。这
种向上的转换是绝对安全的,而且常常会发生(如果我指向一个汽车 Car,实际上我
是指向一个车子 Vehicle):

void f(Vehicle* v);
void g(Car* c) { f(c); } //绝对很安全;不需要转型

注意:在这里我们假设的是 "public" 的继承;后面会再提到「另一种」"private/
protected" 的继承。

========================================    

本文地址:http://com.8s8s.com/it/it30165.htm