回 帖 发 新 帖 刷新版面

主题:C++的一些FAQ

Bjarne Stroustrup对一些常见问题的答复,不一定因为是Bjarne Stroustrup说的就怎么怎么样,不过毕竟他是个很有水平的

原文的地址为:http://www.research.att.com/~bs/bs_faq.html
[建议E文好的看E文的,我最开始没找到翻译的,就是自己对着金山词霸慢慢看的,不过看了些就放弃了...大家不要学我]

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

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



如果大家感觉好,希望顶一顶,多让些学习C++的朋友们都学习学习. [em2]

加油.!~~~~~~~~~~~~~~~~~ 同志们!

回复列表 (共61个回复)

沙发

我如何写个非常简单的程序? 

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

板凳

为什么一个空类的大小不为 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)。

3 楼

为什么析构函数默认不是 virtual 的? 
  
因为很多类并不是被设计作为基类的。只有类在行为上是它的派生类的接口时(这些派生类
往往在堆中分配,通过指针或引用来访问),虚拟函数才有意义。 
  
那么什么时候才应该将析构函数定义为虚拟呢?当类至少拥有一个虚拟函数时。拥有虚拟函
数意味着一个类是派生类的接口,在这种情况下,一个派生类的对象可能通过一个基类指针
来销毁。例如: 
  
    class Base { 
        // ... 
        virtual ~Base(); 
    }; 
  
    class Derived : public Base { 
        // ... 
        ~Derived(); 
    }; 
  
    void f() 
    { 
        Base* p = new Derived; 
        delete p;   // 虚拟析构函数保证~Derived函数被调用 
    } 
  
如果基类的析构函数不是虚拟的,那么派生类的析构函数将不会被调用——这可能产生糟糕
的结果,例如派生类的资源不会被释放。 

4 楼

为什么不能有虚拟构造函数? 
  
虚拟调用是一种能够在给定信息不完全(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这样的类的信息被完全分离开来了。 

5 楼

我能够在构造函数中调用一个虚拟函数吗? 
  
可以,但是要小心。它可能不象你期望的那样工作。在构造函数中,虚拟调用机制不起作用,
因为继承类的重载还没有发生。对象先从基类被创建,“基类先于继承类(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)。这将会导致一片混乱。

6 楼

为什么重载在继承类中不工作? 
  
这个问题(非常常见)往往出现于这样的例子中: 
  
    #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()进行
调用。 

7 楼

怎样将一个整型值转换为一个字符串? 
  
最简单的方法是使用一个字符串流(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 节。 

8 楼

我应该将“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 常量的指针 

9 楼

“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*初始化了 
  
如果他们真的这么干了,编译器也会指出。 
  
每当事情可以有两种方法完成,有人就会迷惑。每当事情仅仅是一个风格的问题,争论就会
没完没了。为每一个指针写一条声明,而且永远都要初始化变量,这样,混乱之源就消失了。

10 楼

为什么 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)的引用传递右值 

我来回复

您尚未登录,请登录后再回复。点此登录或注册