注册 登录
编程论坛 C++教室

[分享]C++的一些FAQ

woodhead 发布于 2006-07-06 17:01, 12986 次点击
Bjarne Stroustrup对一些常见问题的答复,不一定因为是Bjarne Stroustrup说的就怎么怎么样,不过毕竟他是个很有水平的

原文的地址为:http://www.research.att.com/~bs/bs_faq.html

中文的从网上发现的,翻译为左轻侯,这里只是一部分的问题

问题 楼层
我如何写个非常简单的程序? 2
为什么一个空类的大小不为 0? 3
为什么析构函数默认不是 virtual 的? 4
为什么不能有虚拟构造函数? 5
我能够在构造函数中调用一个虚拟函数吗? 6
为什么重载在继承类中不工作? 7
怎样将一个整型值转换为一个字符串? 8
我应该将“const”放在类型之前还是之后? 9
“int* p”正确还是“int *p”正确? 10
为什么 delete 不会将操作数置 0? 11
我能够写“void main()”吗? 12
我如何定义一个类内部(in-class)的常量? 13
我为什么必须使用一个造型来转换*void? 14
有没有“指定位置删除”(placement delete)? 15
为什么编译要花这么长的时间? 16
我必须在类声明处赋予数据吗? 17
我能防止别人继承我自己的类吗? 18
为什么不能为模板参数定义约束(constraints)? 19
什么是函数对象(function object)? 20
我应该如何对付内存泄漏? 21
我为什么在捕获一个异常之后就不能继续? 22
怎样从输入中读取一个字符串? 23
为什么 C++不提供“finally”的构造? 24
为什么我不能重载点符号,::,sizeof,等等? 25
使用宏有什么问题? 26
69 回复
#2
woodhead2006-07-06 17:01
我如何写个非常简单的程序?

特别是在一个学期的开始,我常常收到许多关于编写一个非常简单的程序的询问。这个问题
有一个很具代表性的解决方法,那就是(在你的程序中)读入几个数字,对它们做一些处理,
再把结果输出。下面是一个这样做的例子:

#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;

int main()
{
vector<double> v;

double d;
while(cin>>d) v.push_back(d); // 读入元素
if (!cin.eof()) { // 检查输入是否出错
cerr << "format error\n";
return 1; // 返回一个错误
}

cout << "read " << v.size() << " elements\n";

reverse(v.begin(),v.end());
cout << "elements in reverse order:\n";
for (int i = 0; i<v.size(); ++i) cout << v[i] << '\n';

return 0; // 成功返回
}

对这段程序的观察:

这是一段标准的 ISO C++程序,使用了标准库(standard library)。标准库工具在命
名空间std中声明,封装在没有.h 后缀的头文件中。

如果你要在 Windows 下编译它,你需要将它编译成一个“控制台程序”(console
application)。记得将源文件加上.cpp 后缀,否则编译器可能会以为它是一段 C 代码
而不是 C++。

是的,main()函数返回一个 int值。

读到一个标准的向量(vector)中,可以避免在随意确定大小的缓冲中溢出的错误。读到一
个数组(array)中,而不产生“简单错误”(silly error),这已经超出了一个新手的能
力——如果你做到了,那你已经不是一个新手了。如果你对此表示怀疑,我建议你阅读我的
文章“将标准 C++作为一种新的语言来学习”("Learning Standard C++ as a New
Language"),你可以在本人著作列表(my publications list)中下载到它。

!cin.eof()是对流的格式的检查。事实上,它检查循环是否终结于发现一个
end-of-file(如果不是这样,那么意味着输入没有按照给定的格式)。
见你的C++教科书中的“流状态”(stream state)部分。

vector知道它自己的大小,因此我不需要计算元素的数量。

这段程序没有包含显式的内存管理。Vector维护一个内存中的栈,以存放它的元素。当一
个 vector 需要更多的内存时,它会分配一些;当它不再生存时,它会释放内存。于是,
使用者不需要再关心 vector中元素的内存分配和释放问题。

程序在遇到输入一个“end-of-file”时结束。如果你在 UNIX 平台下运行它,
“end-of-file”等于键盘上的 Ctrl+D。如果你在 Windows平台下,那么由于一个BUG
它无法辨别“end-of-file”字符,你可能倾向于使用下面这个稍稍复杂些的版本,它使
用一个词“end”来表示输入已经结束。

#include<iostream>
#include<vector>
#include<algorithm>
#include<string>
using namespace std;

int main()
{
vector<double> v;

double d;
while(cin>>d) v.push_back(d); // 读入一个元素
if (!cin.eof()) { // 检查输入是否失败
cin.clear(); // 清除错误状态
string s;
cin >> s; // 查找结束字符
if (s != "end") {
cerr << "format error\n";
return 1; // 返回错误
}
}

cout << "read " << v.size() << " elements\n";

reverse(v.begin(),v.end());
cout << "elements in reverse order:\n";
for (int i = 0; i<v.size(); ++i) cout << v[i] << '\n';

return 0; // 成功返回
}

[此贴子已经被作者于2006-7-10 8:57:26编辑过]

#3
woodhead2006-07-06 17:02
为什么一个空类的大小不为 0?

要清楚,两个不同的对象的地址也是不同的。基于同样的理由,new总是返回指向不同对象
的指针。
看看:

class Empty { };

void f()
{
Empty a, b;
if (&a == &b) cout << "impossible: report error to compiler supplier";

Empty* p1 = new Empty;
Empty* p2 = new Empty;
if (p1 == p2) cout << "impossible: report error to compiler supplier";
}

有一条有趣的规则:一个空的基类并不一定有分隔字节。

struct X : Empty {
int a;
// ...
};

void f(X* p)
{
void* p1 = p;
void* p2 = &p->a;
if (p1 == p2) cout << "nice: good optimizer";
}

这种优化是允许的,可以被广泛使用。它允许程序员使用空类以表现一些简单的概念。现在
有些编译器提供这种“空基类优化”(empty base class optimization)。

[此贴子已经被作者于2006-7-10 8:58:22编辑过]

#4
woodhead2006-07-06 17:09
为什么析构函数默认不是 virtual 的?

因为很多类并不是被设计作为基类的。只有类在行为上是它的派生类的接口时(这些派生类
往往在堆中分配,通过指针或引用来访问),虚拟函数才有意义。

那么什么时候才应该将析构函数定义为虚拟呢?当类至少拥有一个虚拟函数时。拥有虚拟函
数意味着一个类是派生类的接口,在这种情况下,一个派生类的对象可能通过一个基类指针
来销毁。例如:

class Base {
// ...
virtual ~Base();
};

class Derived : public Base {
// ...
~Derived();
};

void f()
{
Base* p = new Derived;
delete p; // 虚拟析构函数保证~Derived函数被调用
}

如果基类的析构函数不是虚拟的,那么派生类的析构函数将不会被调用——这可能产生糟糕
的结果,例如派生类的资源不会被释放。
#5
woodhead2006-07-06 17:10
为什么不能有虚拟构造函数?

虚拟调用是一种能够在给定信息不完全(given partial information)的情况下工作
的机制。特别地,虚拟允许我们调用某个函数,对于这个函数,仅仅知道它的接口,而不知
道具体的对象类型。但是要建立一个对象,你必须拥有完全的信息。特别地,你需要知道要
建立的对象的具体类型。因此,对构造函数的调用不可能是虚拟的。

当要求建立一个对象时,一种间接的技术常常被当作“虚拟构造函数”来使用。有关例子,
请参见《C++程序设计语言》第三版 15.6.2.节。

下面这个例子展示一种机制:如何使用一个抽象类来建立一个适当类型的对象。

struct F { // 对象建立函数的接口
virtual A* make_an_A() const = 0;
virtual B* make_a_B() const = 0;
};

void user(const F& fac)
{
A* p = fac.make_an_A(); // 将A 作为合适的类型
B* q = fac.make_a_B(); // 将B 作为合适的类型
// ...
}

struct FX : F {
A* make_an_A() const { return new AX(); } // AX是A 的派生
B* make_a_B() const { return new BX(); } // AX是 B的派生
};

struct FY : F {
A* make_an_A() const { return new AY(); } // AY是A 的派生
B* make_a_B() const { return new BY(); } // BY是B的派生

};

int main()
{
user(FX()); // 此用户建立 AX 与BX
user(FY()); // 此用户建立 AY 与BY
// ...
}

这是所谓的“工厂模式”(the factory pattern)的一个变形。关键在于,user 函数
与 AX 或AY这样的类的信息被完全分离开来了。
#6
woodhead2006-07-06 17:10
我能够在构造函数中调用一个虚拟函数吗?

可以,但是要小心。它可能不象你期望的那样工作。在构造函数中,虚拟调用机制不起作用,
因为继承类的重载还没有发生。对象先从基类被创建,“基类先于继承类(base before
derived)”。

看看这个:

#include<string>
#include<iostream>
using namespace std;

class B {
public:
B(const string& ss) { cout << "B constructor\n"; f(ss); }
virtual void f(const string&) { cout << "B::f\n";}
};

class D : public B {
public:
D(const string & ss) :B(ss) { cout << "D constructor\n";}
void f(const string& ss) { cout << "D::f\n"; s = ss; }
private:
string s;
};

int main()
{
D d("Hello");
}

程序编译以后会输出:

B constructor
B::f
D constructor

注意不是 D::f。设想一下,如果出于不同的规则,B::B()可以调用 D::f()的话,会产
生什么样的后果:因为构造函数 D::D()还没有运行,D::f()将会试图将一个还没有初始
化的字符串 s赋予它的参数。结果很可能是导致立即崩溃。

析构函数在“继承类先于基类”的机制下运行,因此虚拟机制的行为和构造函数一样:只有
本地定义(local definitions)被使用——不会调用虚拟函数,以免触及对象中的(现
在已经被销毁的)继承类的部分。


有人暗示,这只是一条实现时的人为制造的规则。不是这样的。事实上,要实现这种不安全
的方法倒是非常容易的:在构造函数中直接调用虚拟函数,就象调用其它函数一样。但是,
这样就意味着,任何虚拟函数都无法编写了,因为它们需要依靠基类的固定的创建
(invariants established by base classes)。这将会导致一片混乱。
#7
woodhead2006-07-06 17:11
为什么重载在继承类中不工作?

这个问题(非常常见)往往出现于这样的例子中:

#include<iostream>
using namespace std;

class B {
public:
int f(int i) { cout << "f(int): "; return i+1; }
// ...
};

class D : public B {
public:
double f(double d) { cout << "f(double): "; return d+1.3; }
// ...
};

int main()
{
D* pd = new D;

cout << pd->f(2) << '\n';
cout << pd->f(2.3) << '\n';
}

它输出的结果是:

f(double): 3.3
f(double): 3.6

而不是象有些人猜想的那样:

f(int): 3
f(double): 3.6

换句话说,在 B 和 D 之间并没有发生重载的解析。编译器在 D 的区域内寻找,找到了一个
函数double f(double),并执行了它。它永远不会涉及(被封装的)B的区域。在C++
中,没有跨越区域的重载——对于这条规则,继承类也不例外。更多的细节,参见《C++语
言的设计和演变》和《C++程序设计语言》。

但是,如果我需要在基类和继承类之间建立一组重载的 f()函数呢?很简单,使用 using
声明:

class D : public B {
public:
using B::f; // make every f from B available
double f(double d) { cout << "f(double): "; return d+1.3; }
// ...
};

进行这个修改之后,输出结果将是:

f(int): 3
f(double): 3.6

这样,在 B 的 f()和 D 的 f()之间,重载确实实现了,并且选择了一个最合适的 f()进行
调用。
#8
woodhead2006-07-06 17:11
怎样将一个整型值转换为一个字符串?

最简单的方法是使用一个字符串流(stringstream):

#include<iostream>
#include<string>
#include<sstream>
using namespace std;

string itos(int i) // 将int 转换成string
{
stringstream s;
s << i;
return s.str();
}

int main()
{
int i = 127;
string ss = itos(i);
const char* p = ss.c_str();

cout << ss << " " << p << "\n";
}

自然地,这种技术能够将任何使用<<输出的类型转换为字符串。对于字符串流的更多说明,
参见《C++程序设计语言》21.5.3 节。
#9
woodhead2006-07-06 17:12
我应该将“const”放在类型之前还是之后?

我把它放在前面,但那仅仅是个人爱好问题。“const T”和“T const”总是都被允许
的,而且是等效的。例如:

const int a = 1; // ok
int const b = 2; // also ok

我猜想第一种版本可能会让少数(更加固守语法规范)的程序员感到迷惑。

为什么?当我发明“const”(最初的名称叫做“readonly”,并且有一个对应的
“writeonly”)的时候,我就允许它出现在类型之前或之后,因为这样做不会带来任何
不明确。标准之前的 C和C++规定了很少的(如果有的话)特定的顺序规范。

我不记得当时有过任何有关顺序问题的深入思考或讨论。那时,早期的一些使用者——特别
是我——仅仅喜欢这种样子:

const int c = 10;

看起来比这种更好:

int const c = 10;

也许我也受了这种影响:在我最早的一些使用“readonly”的例子中

readonly int c = 10;

比这个更具有可读性:

int readonly c = 10;

我创造的那些最早的使用“const”的(C 或 C++)代码,看来已经在全球范围内取代了
“readonly”。

我记得这个语法的选择在几个人——例如 Dennis Ritchie——当中讨论过,但我不记得
当时我倾向于哪种语言了。

注意在固定指针(const pointer)中,“const”永远出现在“*”之后。例如:

int *const p1 = q; // 指向 int变量的固定指针
int const* p2 = q; //指向int 常量的指针
const int* p3 = q; //指向int 常量的指针

#10
woodhead2006-07-06 17:12
“int* p”正确还是“int *p”正确?

二者都是正确的,因为二者在 C 和 C++中都是有效的,而且意义完全一样。就语言的定义
与相关的编译器来说,我们还可以说“int*p”或者“int * p”。

在“int* p”和“int *p”之间的选择与正确或错误无关,而只关乎风格与侧重点。C
侧重表达式;对声明往往比可能带来的问题考虑得更多。另一方面,C++则非常重视类型。

一个“典型的 C 程序员”写成“int *p”,并且解释说“*p 表示一个什么样的 int”以
强调语法,而且可能指出 C(与 C++)的语法来证明这种风格的正确性。是的,在语法上*
被绑定到名字p 上。

一个“典型的 C++程序员”写成“int* p”,并且解释说“p是一个指向 int 的指针类型”
以强调类型。是的,p 是一个指向 int 的指针类型。我明确地倾向于这种侧重方向,而且
认为对于学好更多的高级C++这是很重要的。

严重的混乱(仅仅)发生在当人们试图在一条声明中声明几个指针的时候:

int* p, p1; // 也许是错的:p1不是一个 int*

把*放到名字这一边,看来也不能有效地减少这种错误:

int *p, p1; // 也许是错的?

为每一个名字写一条声明最大程度地解决了问题——特别是当我们初始化变量的时候。人们
几乎不会这样写:

int* p = &i;
int p1 = p; // 错误:int用一个int*初始化了

如果他们真的这么干了,编译器也会指出。

每当事情可以有两种方法完成,有人就会迷惑。每当事情仅仅是一个风格的问题,争论就会
没完没了。为每一个指针写一条声明,而且永远都要初始化变量,这样,混乱之源就消失了。
#11
woodhead2006-07-06 17:13
为什么 delete 不会将操作数置 0?

考虑一下:

delete p;
// ...
delete p;

如果在...部分没有涉及到 p 的话,那么第二个“delete p;”将是一个严重的错误,因
为 C++的实现(译注:原文为 a C++ implementation,当指 VC++这样的实现了 C++
标准的具体工具)不能有效地防止这一点(除非通过非正式的预防手段)。既然delete 0
从定义上来说是无害的,那么一个简单的解决方案就是,不管在什么地方执行了“delete
p;”,随后都执行“p=0;”。但是,C++并不能保证这一点。

一个原因是,delete 的操作数并不需要一个左值(lvalue)。考虑一下:

delete p+1;
delete f(x);

在这里,被执行的 delete 并没有拥有一个可以被赋予 0 的指针。这些例子可能很少见,
但它们的确指出了,为什么保证“任何指向被删除对象的指针都为 0”是不可能的。绕过这
条“规则”的一个简单的方法是,有两个指针指向同一个对象:

T* p = new T;
T* q = p;
delete p;
delete q; // 糟糕!

C++显式地允许delete操作将操作数左值置 0,而且我曾经希望C++的实现能够做到这一
点,但这种思想看来并没有在C++的实现中变得流行。

如果你认为指针置0 很重要,考虑使用一个销毁的函数:

template<class T> inline void destroy(T*& p) { delete p; p = 0; }

考虑一下,这也是为什么需要依靠标准库的容器、句柄等等,来将对 new 和 delete 的显
式调用降到最低限度的另一个原因。

注意,通过引用来传递指针(以允许指针被置 0)有一个额外的好处,能防止 destroy()
在右值上(rvalue)被调用:

int* f();
int* p;
// ...
destroy(f()); // 错误:应该使用一个非常量(non-const)的引用传递右值
destroy(p+1); // 错误:应该使用一个非常量(non-const)的引用传递右值
#12
woodhead2006-07-06 17:13
我能够写“void main()”吗?

这种定义:

void main() { /* ... */ }

在 C++中从未被允许,在 C 语言中也是一样。参见 ISO C++标准 3.6.1[2]或者 ISO C
标准5.1.2.2.1。规范的实现接受这种方式:

int main() { /* ... */ }



int main(int argc, char* argv[]) { /* ... */ }

一个规范的实现可能提供许多版本的 main(),但它们都必须返回 int类型。main()返回
的 int 值,是程序返回一个值给调用它的系统的方式。在那些不具备这种方式的系统中,
返回值被忽略了,但这并不使“void main()”在C++或C 中成为合法的。即使你的编译
器接受了“void main()”,也要避免使用它,否则你将冒着被C 和C++程序员视为无知
的风险。

在 C++中,main()并不需要包含显式的return语句。在这种情况下,返回值是0,表示
执行成功。例如:

#include<iostream>

int main()
{
std::cout << "This program returns the integer value 0\n";
}

注意,无论是ISO C++还是C99,都不允许在声明中漏掉类型。那就是说,与 C89和 ARM
C++形成对照,当声明中缺少类型时,并不会保证是“int”。于是:

#include<iostream>

main() { /* ... */ }

是错误的,因为缺少 main()的返回类型。
#13
woodhead2006-07-06 17:14
我如何定义一个类内部(in-class)的常量?

如果你需要一个通过常量表达式来定义的常量,例如数组的范围,你有两种选择:

class X {
static const int c1 = 7;
enum { c2 = 19 };

char v1[c1];
char v2[c2];

// ...
};

乍看起来,c1的声明要更加清晰,但是要注意的是,使用这种类内部的初始化语法的时候,
常量必须是被一个常量表达式初始化的整型或枚举类型,而且必须是static 和const 形
式。这是很严重的限制:

class Y {
const int c3 = 7; // 错误:不是 static
static int c4 = 7; // 错误:不是 const
static const float c5 = 7; // 错误:不是整型
};

我倾向使用枚举的方式,因为它更加方便,而且不会诱使我去使用不规范的类内初始化语法。

那么,为什么会存在这种不方便的限制呢?一般来说,类在一个头文件中被声明,而头文件
被包含到许多互相调用的单元去。但是,为了避免复杂的编译器规则,C++要求每一个对象
只有一个单独的定义。如果 C++允许在类内部定义一个和对象一样占据内存的实体的话,这
种规则就被破坏了。对于C++在这个设计上的权衡,请参见《C++语言的设计和演变》。

如果你不需要用常量表达式来初始化它,那么可以获得更大的弹性:

class Z {
static char* p; // 在定义中初始化
const int i; // 在构造函数中初始化
public:
Z(int ii) :i(ii) { }
};

char* Z::p = "hello, there";

你可以获取一个 static成员的地址,当且仅当它有一个类外部的定义的时候:

class AE {
// ...
public:
static const int c6 = 7;
static const int c7 = 31;
};

const int AE::c7; // 定义

int f()
{
const int* p1 = &AE::c6; // 错误:c6 没有左值
const int* p2 = &AE::c7; // ok
// ...
}
#14
woodhead2006-07-06 17:14
我为什么必须使用一个造型来转换*void?

在C语言中,你可以隐式地将*void 转换为*T。这是不安全的。考虑一下:

#include<stdio.h>

int main()
{
char i = 0;
char j = 0;
char* p = &i;
void* q = p;
int* pp = q; /* 不安全的,在C 中可以,C++不行 */

printf("%d %d\n",i,j);
*pp = -1; /* 覆盖了从 i开始的内存 */
printf("%d %d\n",i,j);
}

使用一个并不指向T 类型的 T*将是一场灾难。因此,在 C++中,如果从一个void*得到一
个 T*,你必须进行显式转换。举例来说,要得到上列程序的这个令人别扭的效果,你可以
这样写:

int* pp = (int*)q;

或者使用一个新的类型造型,以使这种没有检查的类型转换操作变得更加清晰:

int* pp = static_cast<int*>(q);

造型被最好地避免了。

在 C 语言中,这种不安全的转换最常见的应用之一,是将 malloc()的结果赋予一个合适
的指针。例如:

int* p = malloc(sizeof(int));

在C++中,使用类型安全的 new操作符:

int* p = new int;

附带地,new 操作符还提供了胜过malloc()的新特性:

new 不会偶然分配错误的内存数量;
new 会隐式地检查内存耗尽情况,而且
new 提供了初始化。

举例:

typedef std::complex<double> cmplx;

/* C 风格: */
cmplx* p = (cmplx*)malloc(sizeof(int)); /* 错误:类型不正确 */
/* 忘记测试p==0 */

if (*p == 7) { /* ... */ } /* 糟糕,忘记了初始化*p */

// C++风格:
cmplx* q = new cmplx(1,2); // 如果内存耗尽,将抛出一个 bad_alloc 异

if (*q == 7) { /* ... */ }
#15
woodhead2006-07-06 17:15
有没有“指定位置删除”(placement delete)?

没有,不过如果你需要的话,可以自己写一个。

看看这个指定位置创建(placement new),它将对象放进了一系列Arena中;

class Arena {
public:
void* allocate(size_t);
void deallocate(void*);
// ...
};

void* operator new(size_t sz, Arena& a)
{
return a.allocate(sz);
}

Arena a1(some arguments);
Arena a2(some arguments);

这样实现了之后,我们就可以这么写:

X* p1 = new(a1) X;
Y* p2 = new(a1) Y;
Z* p3 = new(a2) Z;
// ...

但是,以后怎样正确地销毁这些对象呢?没有对应于这种“placement new”的内建的
“placement delete”,原因是,没有一种通用的方法可以保证它被正确地使用。在C++
的类型系统中,没有什么东西可以让我们确认,p1一定指向一个由 Arena类型的a1分派
的对象。p1可能指向任何东西分派的任何一块地方。

然而,有时候程序员是知道的,所以这是一种方法:

template<class T> void destroy(T* p, Arena& a)
{
if (p) {
p->~T(); // explicit destructor call
a.deallocate(p);
}
}

现在我们可以这么写:

destroy(p1,a1);
destroy(p2,a2);
destroy(p3,a3);

如果 Arena 维护了它保存着的对象的线索,你甚至可以自己写一个析构函数,以避免它发
生错误。

这也是可能的:定义一对相互匹配的操作符 new()和 delete(),以维护《C++程序设计
语言》15.6中的类继承体系。参见《C++语言的设计和演变》10.4 和《C++程序设计语言》
19.4.5。
#16
woodhead2006-07-06 17:16
为什么编译要花这么长的时间?

你的编译器可能有问题。也许它太老了,也许你安装它的时候出了错,也许你用的计算机已
经是个古董。在诸如此类的问题上,我无法帮助你。

但是,这也是很可能的:你要编译的程序设计得非常糟糕,以至于编译器不得不检查数以百
计的头文件和数万行代码。理论上来说,这是可以避免的。如果这是你购买的库的设计问题,
你对它无计可施(除了换一个更好的库),但你可以将你自己的代码组织得更好一些,以求
得将修改代码后的重新编译工作降到最少。这样的设计会更好,更有可维护性,因为它们展
示了更好的概念上的分离。

看看这个典型的面向对象的程序例子:

class Shape {
public: // 使用Shapes的用户的接口
virtual void draw() const;
virtual void rotate(int degrees);
// ...
protected: // common data (for implementers of Shapes)
Point center;
Color col;
// ...
};

class Circle : public Shape {
public:
void draw() const;
void rotate(int) { }
// ...
protected:
int radius;
// ...
};

class Triangle : public Shape {
public:
void draw() const;
void rotate(int);
// ...
protected:
Point a, b, c;
// ...
};

设计思想是,用户通过 Shape 的 public 接口来操纵它们,而派生类(例如 Circle 和
Triangle )的实现部分则共享由 protected 成员表现的那部分实现
(implementation)。

这不是一件容易的事情:确定哪些实现部分是对所有的派生类都有用的,并将之共享出来。
因此,与public接口相比,protected 成员往往要做多得多的改动。举例来说,虽然理
论上“中心”(center)对所有的图形都是一个有效的概念,但当你要维护一个三角形的“中
心”的时候,是一件非常麻烦的事情——对于三角形,当且仅当它确实被需要的时候,计算
这个中心才是有意义的。

protected 成员很可能要依赖于实现部分的细节,而 Shape的用户(译注:user此处译
为用户,指使用 Shape 类的代码,下同)却不见得必须依赖它们。举例来说,很多(大多
数?)使用Shape 的代码在逻辑上是与“颜色”无关的,但是由于Shape 中“颜色”这个
定义的存在,却可能需要一堆复杂的头文件,来结合操作系统的颜色概念。

当 protected 部分发生了改变时,使用 Shape 的代码必须重新编译——即使只有派生类
的实现部分才能够访问protected 成员。

于是,基类中的“实现相关的信息”(information helpful to implementers)对
用户来说变成了象接口一样敏感的东西,它的存在导致了实现部分的不稳定,用户代码的无
谓的重编译(当实现部分发生改变时),以及将头文件无节制地包含进用户代码中(因为“实
现相关的信息”需要它们)。有时这被称为“脆弱的基类问题”(brittle base class
problem)。

一个很明显的解决方案就是,忽略基类中那些象接口一样被使用的“实现相关的信息”。换
句话说,使用接口,纯粹的接口。也就是说,用抽象基类的方式来表示接口:

class Shape {
public: //使用Shapes 的用户的接口
virtual void draw() const = 0;
virtual void rotate(int degrees) = 0;
virtual Point center() const = 0;
// ...

// 没有数据
};

class Circle : public Shape {
public:
void draw() const;
void rotate(int) { }
Point center() const { return center; }
// ...
protected:
Point cent;
Color col;
int radius;
// ...
};

class Triangle : public Shape {
public:
void draw() const;
void rotate(int);
Point center() const;
// ...
protected:
Color col;
Point a, b, c;
// ...
};

现在,用户代码与派生类的实现部分的变化之间的关系被隔离了。我曾经见过这种技术使得
编译的时间减少了几个数量级。

但是,如果确实存在着对所有派生类(或仅仅对某些派生类)都有用的公共信息时怎么办呢?
可以简单把这些信息封装成类,然后从它派生出实现部分的类:

class Shape {
public: //使用Shapes 的用户的接口
virtual void draw() const = 0;
virtual void rotate(int degrees) = 0;
virtual Point center() const = 0;
// ...

// no data
};

struct Common {
Color col;
// ...
};

class Circle : public Shape, protected Common {
public:
void draw() const;
void rotate(int) { }
Point center() const { return center; }
// ...
protected:
Point cent;
int radius;
};

class Triangle : public Shape, protected Common {
public:
void draw() const;
void rotate(int);
Point center() const;
// ...
protected:
Point a, b, c;
};
#17
woodhead2006-07-06 17:16
我必须在类声明处赋予数据吗?

不必须。如果一个接口不需要数据时,无须在作为接口定义的类中赋予数据。代之以在派生
类中给出它们。参见“为什么编译要花这么长的时间?”。

有时候,你必须在一个类中赋予数据。考虑一下复数类的情况:

template<class Scalar> class complex {
public:
complex() : re(0), im(0) { }
complex(Scalar r) : re(r), im(0) { }
complex(Scalar r, Scalar i) : re(r), im(i) { }
// ...

complex& operator+=(const complex& a)
{ re+=a.re; im+=a.im; return *this; }
// ...
private:
Scalar re, im;
};

设计这种类型的目的是将它当做一个内建(built-in)类型一样被使用。在声明处赋值是
必须的,以保证如下可能:建立真正的本地对象(genuinely local objects)(比如
那些在栈中而不是在堆中分配的对象),或者使某些简单操作被适当地inline 化。对于那
些支持内建的复合类型的语言来说,要获得它们提供的效率,真正的本地对象和 inline
化都是必要的。
#18
woodhead2006-07-06 17:17
我能防止别人继承我自己的类吗?

可以,但你为什么要那么做呢?这是两个常见的回答:

效率:避免我的函数被虚拟调用
安全:保证我的类不被用作一个基类(例如,保证我能够复制对象而不用担心出事)

根据我的经验,效率原因往往是不必要的担心。在C++中,虚拟函数调用是如此之快,以致
于它们在一个包含虚拟函数的类中被实际使用时,相比普通的函数调用,根本不会产生值得
考虑的运行期开支。注意,仅仅通过指针或引用时,才会使用虚拟调用机制。当直接通过对
象名字调用一个函数时,虚拟函数调用的开支可以被很容易地优化掉。

如果确实有真正的需要,要将一个类封闭起来以防止虚拟调用,那么可能首先应该问问为什
么它们是虚拟的。我看见过一些例子,那些性能表现不佳的函数被设置为虚拟,没有其他原
因,仅仅是因为“我们习惯这么干”。

这个问题的另一个部分,由于逻辑上的原因如何防止类被继承,有一个解决方案。不幸的是,
这个方案并不完美。它建立在这样一个事实的基础之上,那就是:大多数的继承类必须建立
一个虚拟的基类。这是一个例子:

class Usable;

class Usable_lock {
friend class Usable;
private:
Usable_lock() {}
Usable_lock(const Usable_lock&) {}
};

class Usable : public virtual Usable_lock {
// ...
public:
Usable();
Usable(char*);
// ...
};

Usable a;

class DD : public Usable { };

DD dd; // 错误: DD::DD() 不能访问
// Usable_lock::Usable_lock()是一个私有成员

#19
woodhead2006-07-06 17:17
为什么不能为模板参数定义约束(constraints)?

可以的,而且方法非常简单和通用。

看看这个:

template<class Container>
void draw_all(Container& c)
{
for_each(c.begin(),c.end(),mem_fun(&Shape::draw));
}

如果出现类型错误,可能是发生在相当复杂的 for_each()调用时。例如,如果容器的元
素类型是int,我们将得到一个和for_each()相关的含义模糊的错误(因为不能够对对一
个int值调用Shape::draw 的方法)。

为了提前捕捉这个错误,我这样写:

template<class Container>
void draw_all(Container& c)
{
Shape* p = c.front(); // accept only containers of
Shape*s

for_each(c.begin(),c.end(),mem_fun(&Shape::draw));
}

对于现在的大多数编译器,中间变量 p 的初始化将会触发一个易于了解的错误。这个窍门
在很多语言中都是通用的,而且在所有的标准创建中都必须这样做。在成品的代码中,我也
许可以这样写:

template<class Container>
void draw_all(Container& c)
{
typedef typename Container::value_type T;
Can_copy<T,Shape*>(); // accept containers of only
Shape*s

for_each(c.begin(),c.end(),mem_fun(&Shape::draw));
}

这样就很清楚了,我在建立一个断言(assertion)。Can_copy 模板可以这样定义:

template<class T1, class T2> struct Can_copy {
static void constraints(T1 a, T2 b) { T2 c = a; b = a; }
Can_copy() { void(*p)(T1,T2) = constraints; }
};

Can_copy(在运行时)检查 T1 是否可以被赋值给 T2。Can_copy<T,Shape*>检查 T 是
否是 Shape*类型,或者是一个指向由 Shape 类公共继承而来的类的对象的指针,或者是
被用户转换到Shape*类型的某个类型。注意这个定义被精简到了最小:

一行命名要检查的约束,和要检查的类型
一行列出指定的要检查的约束(constraints()函数)
一行提供触发检查的方法(通过构造函数)

注意这个定义有相当合理的性质:

你可以表达一个约束,而不用声明或复制变量,因此约束的编写者可以用不着去设想变量如
何被初始化,对象是否能够被复制,被销毁,以及诸如此类的事情。(当然,约束要检查这
些属性的情况时例外。)
使用现在的编译器,不需要为约束产生代码
定义和使用约束,不需要使用宏
当约束失败时,编译器会给出可接受的错误信息,包括“constraints”这个词(给用户
一个线索),约束的名字,以及导致约束失败的详细错误(例如“无法用 double*初始化
Shape*”)。

那么,在C++语言中,有没有类似于 Can_copy——或者更好——的东西呢?在《C++语言
的设计和演变》中,对于在 C++中实现这种通用约束的困难进行了分析。从那以来,出现了
很多方法,来让约束类变得更加容易编写,同时仍然能触发良好的错误信息。例如,我信任
我在 Can_copy 中使用的函数指针的方式,它源自 Alex Stepanov 和 Jeremy Siek。
我并不认为 Can_copy()已经可以标准化了——它需要更多的使用。同样,在 C++社区中,
各种不同的约束方式被使用;到底是哪一种约束模板在广泛的使用中被证明是最有效的,还
没有达成一致的意见。

但是,这种方式非常普遍,比语言提供的专门用于约束检查的机制更加普遍。无论如何,当
我们编写一个模板时,我们拥有了C++提供的最丰富的表达力量。看看这个:

template<class T, class B> struct Derived_from {
static void constraints(T* p) { B* pb = p; }
Derived_from() { void(*p)(T*) = constraints; }
};

template<class T1, class T2> struct Can_copy {
static void constraints(T1 a, T2 b) { T2 c = a; b = a; }
Can_copy() { void(*p)(T1,T2) = constraints; }
};

template<class T1, class T2 = T1> struct Can_compare {
static void constraints(T1 a, T2 b) { a==b; a!=b; a<b; }
Can_compare() { void(*p)(T1,T2) = constraints; }
};

template<class T1, class T2, class T3 = T1> struct Can_multiply {
static void constraints(T1 a, T2 b, T3 c) { c = a*b; }
Can_multiply() { void(*p)(T1,T2,T3) = constraints; }
};

struct B { };
struct D : B { };
struct DD : D { };
struct X { };

int main()
{
Derived_from<D,B>();
Derived_from<DD,B>();
Derived_from<X,B>();
Derived_from<int,B>();
Derived_from<X,int>();

Can_compare<int,float>();
Can_compare<X,B>();
Can_multiply<int,float>();
Can_multiply<int,float,double>();
Can_multiply<B,X>();

Can_copy<D*,B*>();
Can_copy<D,B*>();
Can_copy<int,B*>();
}

// 典型的“元素必须继承自Mybase*”约束:

template<class T> class Container : Derived_from<T,Mybase> {
// ...
};

事实上,Derived_from并不检查来源(derivation),而仅仅检查转换(conversion),
不过这往往是一个更好的约束。为约束想一个好名字是很难的。
#20
woodhead2006-07-06 17:18
什么是函数对象(function object)?

顾名思义,就是在某种方式上表现得象一个函数的对象。典型地,它是指一个类的实例,这
个类定义了应用操作符operator()。

函数对象是比函数更加通用的概念,因为函数对象可以定义跨越多次调用的可持久的部分
(类似静态局部变量),同时又能够从对象的外面进行初始化和检查(和静态局部变量不同)。
例如:

class Sum {
int val;
public:
Sum(int i) :val(i) { }
operator int() const { return val; } // 取得值

int operator()(int i) { return val+=i; } // 应用
};

void f(vector v)
{
Sum s = 0; // initial value 0
s = for_each(v.begin(), v.end(), s); // 求所有元素的和
cout << "the sum is " << s << "\n";

//或者甚至:
cout << "the sum is " << for_each(v.begin(), v.end(), Sum(0)) <<
"\n";
}

注意一个拥有应用操作符的函数对象可以被完美地内联化(inline),因为它没有涉及到
任何指针,后者可能导致拒绝优化。与之形成对比的是,现有的优化器几乎不能(或者完全
不能?)将一个通过函数指针的调用内联化。

在标准库中,函数对象被广泛地使用以获得弹性。
#21
woodhead2006-07-06 17:18
我应该如何对付内存泄漏?

写出那些不会导致任何内存泄漏的代码。很明显,当你的代码中到处充满了 new 操作、
delete操作和指针运算的话,你将会在某个地方搞晕了头,导致内存泄漏,指针引用错误,
以及诸如此类的问题。这和你如何小心地对待内存分配工作其实完全没有关系:代码的复杂
性最终总是会超过你能够付出的时间和努力。于是随后产生了一些成功的技巧,它们依赖于
将内存分配(allocations)与重新分配(deallocation)工作隐藏在易于管理的类型
之后。标准容器(standard containers)是一个优秀的例子。它们不是通过你而是自
己为元素管理内存,从而避免了产生糟糕的结果。想象一下,没有 string 和 vector 的
帮助,写出这个:

#include<vector>
#include<string>
#include<iostream>
#include<algorithm>
using namespace std;

int main() // small program messing around with strings
{
cout << "enter some whitespace-separated words:\n";
vector<string> v;
string s;
while (cin>>s) v.push_back(s);

sort(v.begin(),v.end());

string cat;
typedef vector<string>::const_iterator Iter;
for (Iter p = v.begin(); p!=v.end(); ++p) cat += *p+"+";
cout << cat << '\n';
}

你有多少机会在第一次就得到正确的结果?你又怎么知道你没有导致内存泄漏呢?

注意,没有出现显式的内存管理,宏,造型,溢出检查,显式的长度限制,以及指针。通过
使用函数对象和标准算法(standard algorithm),我可以避免使用指针——例如使用
迭代子(iterator),不过对于一个这么小的程序来说有点小题大作了。

这些技巧并不完美,要系统化地使用它们也并不总是那么容易。但是,应用它们产生了惊人
的差异,而且通过减少显式的内存分配与重新分配的次数,你甚至可以使余下的例子更加容
易被跟踪。早在 1981 年,我就指出,通过将我必须显式地跟踪的对象的数量从几万个减少
到几打,为了使程序正确运行而付出的努力从可怕的苦工,变成了应付一些可管理的对象,
甚至更加简单了。

如果你的程序还没有包含将显式内存管理减少到最小限度的库,那么要让你程序完成和正确
运行的话,最快的途径也许就是先建立一个这样的库。

模板和标准库实现了容器、资源句柄以及诸如此类的东西,更早的使用甚至在多年以前。异
常的使用使之更加完善。

如果你实在不能将内存分配/重新分配的操作隐藏到你需要的对象中时,你可以使用资源句
柄(resource handle),以将内存泄漏的可能性降至最低。这里有个例子:我需要通过
一个函数,在空闲内存中建立一个对象并返回它。这时候可能忘记释放这个对象。毕竟,我
们不能说,仅仅关注当这个指针要被释放的时候,谁将负责去做。使用资源句柄,这里用了
标准库中的 auto_ptr,使需要为之负责的地方变得明确了。

#include<memory>
#include<iostream>
using namespace std;

struct S {
S() { cout << "make an S\n"; }
~S() { cout << "destroy an S\n"; }
S(const S&) { cout << "copy initialize an S\n"; }
S& operator=(const S&) { cout << "copy assign an S\n"; }
};

S* f()
{
return new S; // 谁该负责释放这个 S?
};

auto_ptr<S> g()
{
return auto_ptr<S>(new S); // 显式传递负责释放这个S
}

int main()
{
cout << "start main\n";
S* p = f();
cout << "after f() before g()\n";
// S* q = g(); // 将被编译器捕捉
auto_ptr<S> q = g();
cout << "exit main\n";
// *p产生了内存泄漏
// *q被自动释放
}

在更一般的意义上考虑资源,而不仅仅是内存。

如果在你的环境中不能系统地应用这些技巧(例如,你必须使用别的地方的代码,或者你的
程序的另一部分简直是原始人类(译注:原文是 Neanderthals,尼安德特人,旧石器时
代广泛分布在欧洲的猿人)写的,如此等等),那么注意使用一个内存泄漏检测器作为开发
过程的一部分,或者插入一个垃圾收集器(garbage collector)。
#22
woodhead2006-07-06 17:19
我为什么在捕获一个异常之后就不能继续?

换句话说,C++为什么不提供一种简单的方式,让程序能够回到异常抛出点之后,并继续执
行?

主要的原因是,如果从异常处理之后继续,那么无法预知掷出点之后的代码如何对待异常处
理,是否仅仅继续执行,就象什么也没有发生一样。异常处理者无法知道,在继续之前,有
关的上下文环境(context)是否是“正确”的。要让这样的代码正确执行,抛出异常的
编写者与捕获异常的编写者必须对彼此的代码与上下文环境都非常熟悉才行。这样会产生非
常复杂的依赖性,因此无论在什么情况下,都会导致一系列严重的维护问题。

当我设计C++的异常处理机制时,我曾经认真地考虑过允许这种继续的可能性,而且在标准
化的过程中,这个问题被非常详细地讨论过。请参见《C++语言的设计和演变》中的异常处
理章节。

#23
woodhead2006-07-06 17:20
怎样从输入中读取一个字符串?

你可以用这种方式读取一个单独的以空格结束的词:

#include<iostream>
#include<string>
using namespace std;

int main()
{
cout << "Please enter a word:\n";

string s;
cin>>s;

cout << "You entered " << s << '\n';
}

注意,这里没有显式的内存管理,也没有可能导致溢出的固定大小的缓冲区。

如果你确实想得到一行而不是一个单独的词,可以这样做:


#include<iostream>
#include<string>
using namespace std;

int main()
{
cout << "Please enter a line:\n";

string s;
getline(cin,s);

cout << "You entered " << s << '\n';
}

#24
woodhead2006-07-06 17:20
为什么 C++不提供“finally”的构造?

因为 C++提供了另外一种方法,它几乎总是更好的:“资源获得即初始化”(resource
acquisiton is initialization)技术。基本的思路是,通过一个局部对象来表现资
源,于是局部对象的析构函数将会释放资源。这样,程序员就不会忘记释放资源了。举例来
说:

class File_handle {
FILE* p;
public:
File_handle(const char* n, const char* a)
{ p = fopen(n,a); if (p==0) throw Open_error(errno); }
File_handle(FILE* pp)
{ p = pp; if (p==0) throw Open_error(errno); }

~File_handle() { fclose(p); }

operator FILE*() { return p; }

// ...
};

void f(const char* fn)
{
File_handle f(fn,"rw"); //打开fn进行读写
// 通过f 使用文件
}

在一个系统中,需要为每一个资源都使用一个“资源句柄”类。无论如何,我们不需要为每
一个资源获得都写出“finally”语句。在实时系统中,资源获得要远远多于资源的种类,
因此和使用“finally”构造相比,“资源获得即初始化”技术会产生少得多的代码。

#25
woodhead2006-07-06 17:20
为什么我不能重载点符号,::,sizeof,等等?

大多数的运算符能够被程序员重载。例外的是:

. (点符号) :: ?: sizeof

并没有什么根本的原因要禁止重载?:。仅仅是因为,我没有发现有哪种特殊的情况需要重
载一个三元运算符。注意一个重载了 表达式1?表达式2:表达式 3 的函数,不能够保证
表达式 2:表达式3 中只有一个会被执行。

Sizeof 不能够被重载是因为内建的操作(built-in operations),诸如对一个指向
数组的指针进行增量操作,必须依靠它。考虑一下:

X a[10];
X* p = &a[3];
X* q = &a[3];
p++; // p指向a[4]
// 那么p 的整型值必须比 q的整型值大出一个 sizeof(X)

所以,sizeof(X)不能由程序员来赋予一个不同的新意义,以免违反基本的语法。

在 N::m 中,无论 N 还是 m 都不是值的表达式;N 和 m 是编译器知道的名字,::执行一个
(编译期的)范围解析,而不是表达式求值。你可以想象一下,允许重载 x::y的话,x 可
能是一个对象而不是一个名字空间(namespace)或者一个类,这样就会导致——与原来
的表现相反——产生新的语法(允许 表达式 1::表达式 2)。很明显,这种复杂性不会带
来任何好处。

理论上来说,.(点运算符)可以通过使用和->一样的技术来进行重载。但是,这样做会导
致一个问题,那就是无法确定操作的是重载了.的对象呢,还是通过.引用的一个对象。例
如:


class Y {
public:
void f();
// ...
};

class X { // 假设你能重载.
Y* p;
Y& operator.() { return *p; }
void f();
// ...
};

void g(X& x)
{
x.f(); // X::f还是Y::f还是错误?
}

这个问题能够用几种不同的方法解决。在标准化的时候,哪种方法最好还没有定论。更多的
细节,请参见《C++语言的设计和演变》。
#26
woodhead2006-07-06 17:21
使用宏有什么问题?

宏不遵循C++中关于范围和类型的规则。这经常导致一些微妙的或不那么微妙的问题。因此,
C++提供更适合其他的 C++(译注:原文为 the rest of C++,当指 C++除了兼容 C 以
外的部分)的替代品,例如内联函数、模板与名字空间。

考虑一下:

#include "someheader.h"

struct S {
int alpha;
int beta;
};

如果某人(不明智地)地写了一个叫“alpha”或“beta”的宏,那么它将不会被编译,
或者被错误地编译,产生不可预知的结果。例如,“someheader.h”可能包含:

#define alpha 'a'
#define beta b[2]

将宏(而且仅仅是宏)全部大写的习惯,会有所帮助,但是对于宏并没有语言层次上的保护
机制。例如,虽然成员的名字包含在结构体的内部,但这无济于事:在编译器能够正确地辨
别这一点之前,宏已经将程序作为一个字符流进行了处理。顺便说一句,这是 C 和 C++程
序开发环境和工具能够被简化的一个主要原因:人与编译器看到的是不同的东西。

不幸的是,你不能假设别的程序员总是能够避免这种你认为“相当白痴”的事情。例如,最
近有人报告我,他们遇到了一个包含 goto 的宏。我也见过这种情况,而且听到过一些——
在很脆弱的时候——看起来确实有理的意见。例如:

#define prefix get_ready(); int ret__
#define Return(i) ret__=i; do_something(); goto exit
#define suffix exit: cleanup(); return ret__

void f()
{
prefix;
// ...
Return(10);
// ...
Return(x++);
//...
suffix;
}

作为一个维护的程序员,就会产生这种印象;将宏“隐藏”到一个头文件中——这并不罕见
——使得这种“魔法”更难以被辨别。

一个常见的微妙问题是,一个函数风格的宏并不遵守函数参数传递的规则。例如:

#define square(x) (x*x)

void f(double d, int i)
{
square(d); // 好
square(i++); // 糟糕:这表示 (i++*i++)
square(d+1); //糟糕:这表示(d+1*d+1); 也就是 (d+d+1)
// ...
}

“d+1”的问题,可以通过在“调用”时或宏定义时添加一对圆括号来解决:

#define square(x) ((x)*(x)) /*这样更好 */

但是, i++被执行了两次(可能并不是有意要这么做)的问题仍然存在。

是的,我确实知道有些特殊的宏并不会导致 C/C++预处理宏这样的问题。但是,我无心去
发展 C++中的宏。作为替代,我推荐使用 C++语言中合适的工具,例如内联函数,模板,
构造函数(用来初始化),析构函数(用来清除),异常(用来退出上下文环境),等等。
#27
song42006-07-07 09:37

怎么没人说话
支持一下

#28
myajax952006-07-07 09:39
开始一篇一篇看。
#29
song42006-07-07 09:41
考完试再看
下午c++最后一个
现在我可不想整理我思路
#30
myajax952006-07-07 09:43
主考试成功。
#31
woodhead2006-07-07 10:38
给置顶了,那有空就翻翻以前的贴子,看看有什么经常性的问题,弄个索引什么之类的。

---------------------------------------------

晕,水平不够,分辨不出来。

[此贴子已经被作者于2006-7-9 9:10:28编辑过]

#32
gototheworld2006-07-07 23:26
真的是很精华
在3楼的帖子里,
struct X : Empty {
int a;
// ...
};

void f(X* p)
{
void* p1 = p; //为什么这里不需要做类型转换呢?
void* p2 = &p->a;
if (p1 == p2) cout << "nice: good optimizer";
}

[此贴子已经被作者于2006-7-7 23:33:32编辑过]

#33
woodhead2006-07-08 09:13
p1 是void*型的指针,可以指向其他类型。
#34
song42006-07-16 17:25
复制下来
回去拜读
不能上网........
#35
song42006-07-16 17:34
希望多多发这种帖子
这种帖子人人都需要
特别我这样的隔绝人
#36
子时之龙2006-07-29 11:12

苍天那,
大哥你太强了,
我不不顶你不行了、、、

#37
混世卓人2006-08-06 20:37

想问下为什么对cin对象可以直接进行逻辑判断.
也就是while(cin)中cin处于正常状态的时候是true.而非正常状态为false.
自己写类的时候怎样编写可以达到同样效果?

[此贴子已经被作者于2006-8-8 13:09:51编辑过]

#38
DarkHero2006-08-12 23:36
因为它采用了转换操作符的方法(好象叫这名字吧。。),例如:

class MyClass {

bool isOK() {//...}

operator bool() { //自动转换为bool类型
return isOK();
}
};
#39
静显锋芒2006-08-21 11:01

小弟刚刚上路,
楼主好贴,
学习中...
仰望并感谢之...

#40
yeshirow2006-08-26 03:46

很好, ...
我對 13 樓的說法有些疑問,
爲甚麽
class Y
{
static const int a = 10;
}; 在我這裏會報錯呢?

一直都不知道你說的這些情形, 我在對類的常量成員的初始化是這樣的:

class Y
{
public:
Y() : a(10), b(3.5f) { }
private:
const int a;
const float b;
};

就是在前導中初始化類的常量成員.

#41
woodhead2006-08-26 12:30
我可以通过编译,编译器的问题可能。

static const int a = 10;

a是静态常量,属于类的常量,只有一份
你那样写,的常量属于类对象,有多少非静态的对象,就有多少个。
#42
z1089799792006-08-27 22:53
#43
gengshen1262006-09-03 12:07
好贴,赞一个。
By the way FAQ是什么的缩写?
#44
海鸟2006-09-11 10:52
只有谢谢了,没什么好说的,
#45
stefanie5182006-09-13 20:03
这些是个好东西!
#46
cr77772006-09-19 10:32

收获很大,谢谢!

#47
sunjian2006-10-08 23:13
好地方啊
#48
song42006-10-29 17:17
WOOD,听说全国大学生挑战杯了么
参加了么
我们现在要参加,但是苦于题目
我现在学完C++,在学数据结构
MFC也在努力中,如果需要我还会去看数据库
你有什么意见
#49
woodhead2006-10-29 19:03
恩恩,回了
#50
song42006-10-30 17:14
哦 谢谢了
#51
xinghe692006-11-01 09:59
谢列
12