回 帖 发 新 帖 刷新版面

主题: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个回复)

11 楼

我能够写“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()的返回类型。 

12 楼

我如何定义一个类内部(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 
    // ... 
}

13 楼

我为什么必须使用一个造型来转换*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) { /* ... */ } 

14 楼

有没有“指定位置删除”(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。 

15 楼

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

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

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
化都是必要的。 

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()是一个私有成员

18 楼

为什么不能为模板参数定义约束(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),
不过这往往是一个更好的约束。为约束想一个好名字是很难的。 

19 楼

什么是函数对象(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),因为它没有涉及到
任何指针,后者可能导致拒绝优化。与之形成对比的是,现有的优化器几乎不能(或者完全
不能?)将一个通过函数指针的调用内联化。 
  
在标准库中,函数对象被广泛地使用以获得弹性。 

20 楼

我应该如何对付内存泄漏? 
  
写出那些不会导致任何内存泄漏的代码。很明显,当你的代码中到处充满了 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)。

我来回复

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