| 网站首页 | 业界新闻 | 小组 | 威客 | 人才 | 下载频道 | 博客 | 代码贴 | 在线编程 | 编程论坛
欢迎加入我们,一同切磋技术
用户名:   
 
密 码:  
共有 12984 人关注过本帖, 3 人收藏
标题:[分享]C++的一些FAQ
只看楼主 加入收藏
woodhead
Rank: 3Rank: 3
等 级:新手上路
威 望:9
帖 子:1124
专家分:0
注 册:2005-7-18
收藏
得分:0 
为什么 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)的引用传递右值

2006-07-06 17:13
woodhead
Rank: 3Rank: 3
等 级:新手上路
威 望:9
帖 子:1124
专家分:0
注 册:2005-7-18
收藏
得分:0 
我能够写“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()的返回类型。

2006-07-06 17:13
woodhead
Rank: 3Rank: 3
等 级:新手上路
威 望:9
帖 子:1124
专家分:0
注 册:2005-7-18
收藏
得分:0 
我如何定义一个类内部(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
// ...
}

2006-07-06 17:14
woodhead
Rank: 3Rank: 3
等 级:新手上路
威 望:9
帖 子:1124
专家分:0
注 册:2005-7-18
收藏
得分:0 
我为什么必须使用一个造型来转换*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) { /* ... */ }

2006-07-06 17:14
woodhead
Rank: 3Rank: 3
等 级:新手上路
威 望:9
帖 子:1124
专家分:0
注 册:2005-7-18
收藏
得分:0 
有没有“指定位置删除”(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。

2006-07-06 17:15
woodhead
Rank: 3Rank: 3
等 级:新手上路
威 望:9
帖 子:1124
专家分:0
注 册:2005-7-18
收藏
得分:0 
为什么编译要花这么长的时间?

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

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

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

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;
};

2006-07-06 17:16
woodhead
Rank: 3Rank: 3
等 级:新手上路
威 望:9
帖 子:1124
专家分:0
注 册:2005-7-18
收藏
得分:0 
我必须在类声明处赋予数据吗?

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

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

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

2006-07-06 17:16
woodhead
Rank: 3Rank: 3
等 级:新手上路
威 望:9
帖 子:1124
专家分:0
注 册:2005-7-18
收藏
得分:0 
我能防止别人继承我自己的类吗?

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

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

根据我的经验,效率原因往往是不必要的担心。在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()是一个私有成员


2006-07-06 17:17
woodhead
Rank: 3Rank: 3
等 级:新手上路
威 望:9
帖 子:1124
专家分:0
注 册:2005-7-18
收藏
得分:0 
为什么不能为模板参数定义约束(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),
不过这往往是一个更好的约束。为约束想一个好名字是很难的。

2006-07-06 17:17
woodhead
Rank: 3Rank: 3
等 级:新手上路
威 望:9
帖 子:1124
专家分:0
注 册:2005-7-18
收藏
得分:0 
什么是函数对象(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),因为它没有涉及到
任何指针,后者可能导致拒绝优化。与之形成对比的是,现有的优化器几乎不能(或者完全
不能?)将一个通过函数指针的调用内联化。

在标准库中,函数对象被广泛地使用以获得弹性。

2006-07-06 17:18
快速回复:[分享]C++的一些FAQ
数据加载中...
 
   



关于我们 | 广告合作 | 编程中国 | 清除Cookies | TOP | 手机版

编程中国 版权所有,并保留所有权利。
Powered by Discuz, Processed in 0.017686 second(s), 7 queries.
Copyright©2004-2024, BCCN.NET, All Rights Reserved