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

[分享]c++综合技术应用文章合集^^

aipb2007 发布于 2007-05-05 11:44, 7116 次点击
整理了想各个地方收集的c++综合性文章,分享给广大c++学习者作为参考。将不断更新。

希望支持

目录:

1 关于编程风格的讨论 (2楼)
2 C++代码优化方法 (3楼)
3 Microsoft Visual C++ 浮点优化 (4楼)
4 Effective C++ 的52个条款列表 (5楼)
5 C/C++中的整型常识 (6楼)
6 C++的底层机制 (7楼)
7 C++的中抽象 (8楼)
8 C++实用技巧两则 (9楼)
9 C++之运算符重载 (10楼)
10 C++中函数指针数组的使用 (11楼)
11 C++中结构体的的慨念和使用方法 (12楼)
12 C++中数组和指针类型的关系 (13楼)
13 C++中用函数模板实现和优化抽象操作 (14楼)
14 浅析c/c++中的指针 (15楼)
15 理解复杂的C/C++声明 (16楼)
16 BJ大师防止类被继承的探讨 (25楼)

----------------------------------------------------------------------------
多多顶啊!

[此贴子已经被作者于2007-6-2 0:20:48编辑过]

27 回复
#2
aipb20072007-05-05 11:45

关于编程风格的讨论
**软件公司软件开发规范**(这只是其中一类较好的编程规范,可以借鉴)

在公司团队协作开发的情况下,编程时应该强调的一个重要方面是程序的易读性,在保证软件的速度等性能指标能满足用户需求的情况下,能让其他程序员容易读懂你的程序。一套鲜明的编程风格,可以让协作者、后继者和自己一目了然,在很短的时间内看清程序的结构,理解设计的思路。大大的提高代码的可读性、可重用性、程序健壮性、可移植性和可维护性。
制定本编程规范的目的是为了提高公司的软件开发效率及所开发的软件的可维护性,提高软件的质量。本规范由程序风格、命名规则、注释规范、程序健壮性、可移植性、错误处理以及软件的模块化规范等部分组成。

一、程序风格:
1、严格采用阶梯层次组织程序代码:
各层次缩进的分格采用VC的缺省风格,即每层次缩进为4格,括号位于下一行。要求相匹配的大括号在同一列,对继行则要求再缩进4格。例如:
void main()
{
......
long lI; //循环变量
long lSum;//用来记录和
float fAvg;//用来求平均值
......
//对数进行累加。
for( lI=0;lI<10;lI++)
{
lSum=lSum+lI;
...... }
//求平均值。
fAvg=lSum/10.0;
......
}

2、提示信息字符串的位置
在程序中需要给出的提示字符串,为了支持多种语言的开发,除了一些给调试用的临时信息外,其他所有的提示信息必须定义在资源中。
3、对变量的定义,尽量位于函数的开始位置。
二、命名规则:
1、变量名的命名规则
①、变量的命名规则要求用“匈牙利法则”。即开头字母用变量的类型,其余部分用变量的英文意思或其英文意思的缩写,尽量避免用中文的拼音,要求单词的第一个字母应大写。
即: 变量名=变量类型+变量的英文意思(或缩写)
对非通用的变量,在定义时加入注释说明,变量定义尽量可能放在函数的开始处。
见下表:
bool(BOOL) 用b开头 bIsParent
byte(BYTE) 用by开头 byFlag
short(int) 用n开头 nStepCount
long(LONG) 用l开头 lSum
char(CHAR) 用c开头 cCount
float(FLOAT) 用f开头 fAvg
double(DOUBLE) 用d开头 dDeta
void(VOID) 用v开头 vVariant
unsigned short(WORD) 用w开头 wCount
unsigned long(DWORD) 用dw开头 dwBroad
HANDLE(HINSTANCE) 用h开头 hHandle
DWORD 用dw开头 dwWord
LPCSTR(LPCTSTR) 用str开头 strString
用0结尾的字符串 用sz开头 szFileName

对未给出的变量类型要求提出并给出命名建议给技术委员会。

②、指针变量命名的基本原则为:
对一重指针变量的基本原则为:
“p”+变量类型前缀+命名
如一个float*型应该表示为pfStat
对多重指针变量的基本规则为:
二重指针: “pp”+变量类型前缀+命名
三重指针: “ppp”+变量类型前缀+命名
......
③、全局变量用g_开头,如一个全局的长型变量定义为g_lFailCount,即:变量名=g_+变量类型+变量的英文意思(或缩写)
④、静态变量用s_开头,如一个静态的指针变量定义为s_plPerv_Inst,即: 变量名=s_+变量类型+变量的英文意思(或缩写)
⑤、成员变量用m_开头,如一个长型成员变量定义为m_lCount;即:变量名=m_+变量类型+变量的英文意思(或缩写)
⑥、对枚举类型(enum)中的变量,要求用枚举变量或其缩写做前缀。并且要求用大写。
如:enum cmEMDAYS
{
EMDAYS_MONDAY;
EMDAYS_TUESDAY;
……
};
⑦、对struct、union、class变量的命名要求定义的类型用大写。并要加上前缀,其内部变量的命名规则与变量命名规则一致。
结构一般用S开头
如:struct ScmNPoint
{
int nX;//点的X位置
int nY; //点的Y位置
};
联合体一般用U开头
如: union UcmLPoint
{
long lX;
long lY;
}
类一般用C开头
如:
class CcmFPoint
{
public:
float fPoint;
};
对一般的结构应该定义为类模板,为以后的扩展性考虑
如:
template <class TYPE>
class CcmTVector3d
{
public:
TYPE x,y,z;
};
⑧、对常量(包括错误的编码)命名,要求常量名用大写,常量名用英文表达其意思。
如:#define CM_FILE_NOT_FOUND CMMAKEHR(0X20B) 其中CM表示类别。
⑨、对const 的变量要求在变量的命名规则前加入c_,即:c_+变量命名规则;例如:
const char* c_szFileName;
2、 函数的命名规范:
函数的命名应该尽量用英文表达出函数完成的功能。遵循动宾结构的命名法则,函数名中动词在前,并在命名前加入函数的前缀,函数名的长度不得少于8个字母。
例如:
long cmGetDeviceCount(……);
3、函数参数规范:
①、 参数名称的命名参照变量命名规范。
②、 为了提高程序的运行效率,减少参数占用的堆栈,传递大结构的参数,一律采用指针或引用方式传递。
③、 为了便于其他程序员识别某个指针参数是入口参数还是出口参数,同时便于编译器检查错误,应该在入口参数前加入const标志。如:
……cmCopyString(const char * c_szSource, char * szDest)
4、引出函数规范:
对于从动态库引出作为二次开发函数公开的函数,为了能与其他函数以及Windows的函数区分,采用类别前缀+基本命名规则的方法命名。例如:在对动态库中引出的一个图象编辑的函数定义为 imgFunctionname(其中img为image缩写)。
现给出三种库的命名前缀:
①、 对通用函数库,采用cm为前缀。
②、 对三维函数库,采用vr为前缀。
③、 对图象函数库,采用img为前缀。
对宏定义,结果代码用同样的前缀。
5、文件名(包括动态库、组件、控件、工程文件等)的命名规范:
文件名的命名要求表达出文件的内容,要求文件名的长度不得少于5个字母,严禁使用象file1,myfile之类的文件名。
三、注释规范:
1、函数头的注释
对于函数,应该从“功能”,“参数”,“返回值”、“主要思路”、“调用方法”、“日期”六个方面用如下格式注释:
//程序说明开始
//================================================================//
// 功能: 从一个String 中删除另一个String。
// 参数: strByDelete,strToDelete
// (入口) strByDelete: 被删除的字符串(原来的字符串)
// (出口) strToDelete: 要从上个字符串中删除的字符串。
// 返回: 找到并删除返回1,否则返回0。(对返回值有错误编码的要// 求列出错误编码)。
// 主要思路:本算法主要采用循环比较的方法来从strByDelete中找到
// 与strToDelete相匹配的字符串,对多匹配strByDelete
// 中有多个strToDelete子串)的情况没有处理。请参阅:
// 书名......
// 调用方法:......
// 日期:起始日期,如:2000/8/21.9:40--2000/8/23.21:45
//================================================================//
函数名(……)
//程序说明结束
①、 对于某些函数,其部分参数为传入值,而部分参数为传出值,所以对参数要详细说明该参数是入口参数,还是出口参数,对于某些意义不明确的参数还要做详细说明(例如:以角度作为参数时,要说明该角度参数是以弧度(PI),还是以度为单位),对既是入口又是出口的变量应该在入口和出口处同时标明。等等。
②、 函数的注释应该放置在函数的头文件中,在实现文件中的该函数的实现部分应该同时放置该注释。
③、 在注释中应该详细说明函数的主要实现思路、特别要注明自己的一些想法,如果有必要则应该写明对想法产生的来由。对一些模仿的函数应该注释上函数的出处。
④、 在注释中详细注明函数的适当调用方法,对于返回值的处理方法等。在注释中要强调调用时的危险方面,可能出错的地方。
⑤、 对日期的注释要求记录从开始写函数到结束函数的测试之间的日期。
⑥、 对函数注释开始到函数命名之间应该有一组用来标识的特殊字符串。
如果算法比较复杂,或算法中的变量定义与位置有关,则要求对变量的定义进行图解。对难以理解的算法能图解尽量图解。
2、变量的注释:
对于变量的注释紧跟在变量的后面说明变量

#3
aipb20072007-05-05 11:45

C++代码优化方法
在C++层次进行优化,比在汇编层次优化具有更好的移植性,应该是优化中的首选做法。

确定浮点型变量和表达式是 float 型

为了让编译器产生更好的代码(比如说产生3DNow! 或SSE指令的代码),必须确定浮点型变量和表达式是 float 型的。要特别注意的是,以 ";F"; 或 ";f"; 为后缀(比如:3.14f)的浮点常量才是 float 型,否则默认是 double 型。为了避免 float 型参数自动转化为 double,请在函数声明时使用 float。

使用32位的数据类型

编译器有很多种,但它们都包含的典型的32位类型是:int,signed,signed int,unsigned,unsigned int,long,signed long,long int,signed long int,unsigned long,unsigned long int。尽量使用32位的数据类型,因为它们比16位的数据甚至8位的数据更有效率。

明智使用有符号整型变量

在很多情况下,你需要考虑整型变量是有符号还是无符号类型的。比如,保存一个人的体重数据时不可能出现负数,所以不需要使用有符号类型。但是,如果是要保存温度数据,就必须使用到有符号的变量。

在许多地方,考虑是否使用有符号的变量是必要的。在一些情况下,有符号的运算比较快;但在一些情况下却相反。比如:整型到浮点转化时,使用大于16位的有符号整型比较快。因为x86构架中提供了从有符号整型转化到浮点型的指令,但没有提供从无符号整型转化到浮点的指令。看看编译器产生的汇编代码,不好的代码:

编译前 编译后

double x; mov [foo + 4], 0

unsigned int i; mov eax, i

x = i; mov [foo], eax

flid qword ptr [foo]

fstp qword ptr [x]


上面的代码比较慢。不仅因为指令数目比较多,而且由于指令不能配对造成的FLID指令被延迟执行。最好用以下代码代替,推荐的代码:

编译前 编译后

double x; fild dword ptr [i]

int i; fstp qword ptr [x]

x = i;


在整数运算中计算商和余数时,使用无符号类型比较快。以下这段典型的代码是编译器产生的32位整型数除以4的代码,不好的代码 推荐的代码

编译前 编译后

int i; mov eax, i

i = i / 4; cdq

and edx, 3

add eax, edx

sar eax, 2

mov i, eax


编译前 编译后

unsigned int i; shr i, 2

i = i / 4;


总结:

无符号类型用于:

除法和余数

循环计数

数组下标

有符号类型用于:

整型到浮点的转化

while VS. for

在编程中,我们常常需要用到无限循环,常用的两种方法是while (1) 和 for (;;)。这两种方法效果完全一样,但那一种更好呢?然我们看看它们编译后的代码:

编译前 编译后

while (1); mov eax,1

test eax,eax

je foo+23h

jmp foo+18h


编译前 编译后

for (;;); jmp foo+23h


for (;;)指令少,不占用寄存器,而且没有判断跳转,比while (1)好。

使用数组型代替指针型

使用指针会使编译器很难优化它。因为缺乏有效的指针代码优化的方法,编译器总是假设指针可以访问内存的任意地方,包括分配给其他变量的储存空间。所以为了编译器产生优化得更好的代码,要避免在不必要的地方使用指针。一个典型的例子是访问存放在数组中的数据。C++ 允许使用操作符 [] 或指针来访问数组,使用数组型代码会让优化器减少产生不安全代码的可能性。比如,x[0] 和x[2] 不可能是同一个内存地址,但 *p 和 *q 可能。强烈建议使用数组型,因为这样可能会有意料之外的性能提升。

不好的代码 推荐的代码

typedef struct

{

float x,y,z,w;

} VERTEX;

typedef struct

{

float m[4][4];

} MATRIX;

void XForm(float* res, const float* v, const float* m, int nNumVerts)

{

float dp;

int i;

 const VERTEX* vv = (VERTEX *)v;

 for (i = 0; i <; nNumVerts; i++)

{

dp = vv->;x * *m ++;

dp += vv->;y * *m ++;

dp += vv->;z * *m ++;

dp += vv->;w * *m ++;

*res ++ = dp;// 写入转换了的 x

dp = vv->;x * *m ++;

dp += vv->;y * *m ++;

dp += vv->;z * *m ++;

dp += vv->;w * *m ++;

*res ++ = dp; // 写入转换了的 y

dp = vv->;x * *m ++;

dp += vv->;y * *m ++;

dp += vv->;z * *m ++;

dp += vv->;w * *m ++;

*res ++ = dp;// 写入转换了的 z

dp = vv->;x * *m ++;

dp += vv->;y * *m ++;

dp += vv->;z * *m ++;

dp += vv->;w * *m ++;

*res ++ = dp;// 写入转换了的 w

vv ++;  // 下一个矢量

m -= 16;

}

}

typedef struct

{

float x,y,z,w;

} VERTEX;

typedef struct

{

float m[4][4];

} MATRIX;

void XForm (float* res, const float* v, const float* m, int nNumVerts)

{

int i;

const VERTEX* vv = (VERTEX*)v;

const MATRIX* mm = (MATRIX*)m;

VERTEX* rr = (VERTEX*)res;

for (i = 0; i <; nNumVerts; i++)

{

rr->;x = vv->;x * mm->;m[0][0] + vv->;y * mm->;m[0][1]

+ vv->;z * mm->;m[0][2] + vv->;w * mm->;m[0][3];

rr->;y = vv->;x * mm->;m[1][0] + vv->;y * mm->;m[1][1]

+ vv->;z * mm->;m[1][2] + vv->;w * mm->;m[1][3];

rr->;z = vv->;x * mm->;m[2][0] + vv->;y * mm->;m[2][1]

+ vv->;z * mm->;m[2][2] + vv->;w * mm->;m[2][3];

rr->;w = vv->;x * mm->;m[3][0] + vv->;y * mm->;m[3][1]

+ vv->;z * mm->;m[3][2] + vv->;w * mm->;m[3][3];

}

}


注意: 源代码的转化是与编译器的代码发生器相结合的。从源代码层次很难控制产生的机器码。依靠编译器和特殊的源代码,有可能指针型代码编译成的机器码比同等条件下的数组型代码运行速度更快。明智的做法是在源代码转化后检查性能是否真正提高了,再选择使用指针型还是数组型。


充分分解小的循环

要充分利用CPU的指令缓存,就要充分分解小的循环。特别是当循环体本身很小的时候,分解循环可以提高性能。BTW:很多编译器并不能自动分解循环。

不好的代码 推荐的代码

// 3D转化:把矢量 V 和 4x4 矩阵 M 相乘

for (i = 0; i <; 4; i ++)

{

r[i] = 0;

for (j = 0; j <; 4; j ++)

{

r[i] += M[j][i]*V[j];

}

}

r[0] = M[0][0]*V[0] + M[1][0]*V[1] + M[2][0]*V[2] + M[3][0]*V[3];

r[1] = M[0][1]*V[0] + M[1][1]*V[1] + M[2][1]*V[2] + M[3][1]*V[3];

r[2] = M[0][2]*V[0] + M[1][2]*V[1] + M[2][2]*V[2] + M[3][2]*V[3];

r[3] = M[0][3]*V[0] + M[1][3]*V[1] + M[2][3]*V[2] + M[3][3]*v[3];


避免没有必要的读写依赖

当数据保存到内存时存在读写依赖,即数据必须在正确写入后才能再次读取。虽然AMD Athlon等CPU有加速读写依赖延迟的硬件,允许在要保存的数据被写入内存前读取出来,但是,如果避免了读写依赖并把数据保存在内部寄存器中,速度会更快。在一段很长的又互相依赖的代码链中,避免读写依赖显得尤其重要。如果读写依赖发生在操作数组时,许多编译器不能自动优化代码以避免读写依赖。所以推荐程序员手动去消除读写依赖,举例来说,引进一个可以保存在寄存器中的临时变量。这样可以有很大的性能提升。下面一段代码是一个例子:

不好的代码 推荐的代码

float x[VECLEN], y[VECLEN], z[VECLEN


#4
aipb20072007-05-05 11:45

Microsoft Visual C++ 浮点优化

Eric Fleegal

Microsoft Corporation

适用于:Microsoft Visual C++

C++ 中的浮点代码优化
C++ 优化编译器不仅能够将源代码转换为机器码,而且能够对机器指令进行适当的排列以便改善性 能和/或减小大小。遗憾的是,许多常用的优化在应用于浮点计算时未必安全。在下面的求和算法 [1] 中,可以看到这方面的一个恰当的示例:

float KahanSum( const float A[], int n )

{

float sum=0, C=0, Y, T;

for (int i=0; i<n; i++)

{

Y = A[i] - C;

T = sum + Y;

C = T - sum - Y;

sum = T;

}

return sum;

}


该函数将数组向量 A 中的 n 个浮点值相加。在循环体中,算法计算 一个“修正”值,然后将其应用于求和的下一步。与简单的求和相比,该方法大大减小了累积性舍入 误差,同时保持了 O(n) 时间复杂性。

一个不完善的 C++ 编译器可能假设浮点算法遵循与实数算法相同的代数规则。这样的编译器可能 继而错误地断定

C = T - sum - Y ==> (sum+Y)-sum-Y ==> 0;

也就是说,C 得到的值总是常量零。如果随后将该常量值传播到后续表达式中,循环体将化简为简 单的求和。更准确地说,就是

Y = A[i] - C ==> Y = A[i]

T = sum + Y ==> T = sum + A[i]

sum = T ==> sum = sum + A[i]


因此,对于不完善的编译器而言,KahanSum 函数的逻辑转换将是:

float KahanSum( const float A[], int n )

{

float sum=0; // C, Y & T are now unused

for (int i=0; i<n; i++)

sum = sum + A[i];

return sum;

}


尽管转换后的算法更快,但它根本没有准确表达程序员的意图。精心设计的误差修正已经 被完全消除,只剩下一个具有所有其关联误差的简单的直接求和算法。

当然,完善的 C++ 编译器知道实数算法的代数规则通常并不适用于浮点算法。然而,即使是完善 的 C++ 编译器,也可能错误地解释程序员的意图。

考虑一种常见的优化措施,它试图在寄存器中存放尽可能多的值(称为“登记”值)。在 KahanSum 示例中,这一优化可能试图登记变量 C、Y 和 T,因为这些变量仅在循环体内使用。如果寄存器精度为 52 位(双精度)而不是 23 位(单精度),这一优化可以有效地将 C、Y 和 T 的类 型提升为 double。如果没有以同样的方式登记 sum 变量,则它仍将编 码为单精度。这会将 KahanSum 的语义转换为下面的语义

float KahanSum( const float A[], int n )

{

float sum=0;

double C=0, Y, T; // now held in-register

for (int i=0; i<n; i++)

{

Y = A[i] - C;

T = sum + Y;

C = T - sum - Y;

sum = (float) T;

}

return sum;

}


尽管现在 Y、T 和 C 以更高的精度进行计算,但新的编码可能产生精确性较低的结果,具体取决 于 A[] 中的值。因而,即使看起来无害的优化也可能具有消极的后果。

这些种类的优化问题并不局限于“棘手”的浮点代码。即使是简单的浮点算法,在经过错误的优化 后也可能失败。考虑一个简单的直接求和算法:

float Sum( const float A[], int n )

{

float sum=0;

for (int i=0; i<n; i++)

sum = sum + A[i];

return sum;

}


因为一些浮点单元能够同时执行多个运算,所以编译器可能选择采用标量简化 优化。这一 优化有效地将简单的 Sum 函数从上述形式转换为以下形式:

float Sum( const float A[], int n )

{

int n4 = n-n%4; // or n4=n4&(~3)

int i;

float sum=0, sum1=0, sum2=0, sum3=0;

for (i=0; i<n4; i+=4)

{

sum = sum + A[i];

sum1 = sum1 + A[i+1];

sum2 = sum2 + A[i+2];

sum3 = sum3 + A[i+3];

}

sum = sum + sum1 + sum2 + sum3;

for (; i<n; i++)

sum = sum + A[i];

return sum;

}


该函数现在保持了四个独立的求和运算,它们可以在每个步骤同时处理。尽管优化后的函数现在要 快得多,但优化结果可能与非优化结果完全不同。在进行这一变化时,编译器采用了具有结合律的浮 点加法;即以下两个表达式等效:(a+b)+c == a+(b+c)。然而,对于浮点数而言,结合律并不总是适 用。现在,转换后的函数不是按以下方法求和:

sum = A[0]+A[1]+A[2]+...+A[n-1]


而是按以下方法计算结果:

sum = (A[0]+A[4]+A[8]+...)

+(A[1]+A[5]+A[9]+...)

+(A[2]+A[6]+A[10]+...)

+(A[3]+A[7]+A[11]+...)

+...


对于 A[] 的某些值而言,不同的加法运算顺序可能产生意外的结果。更为复杂的是,某些程序员 可能选择预先针对此类优化做准备,并相应地对这些优化进行补偿。在此情况下,程序可以按不同的 顺序构建数组 A,以便优化的 sum 产生预期的结果。而且,在许多情况 下,优化结果的精确性可能“足够严密”。当优化提供了令人信服的速度优点时,尤其如此。例如, 视频游戏要求具有尽可能快的速度,但通常并不要求进行高度精确的浮点计算。因此,编译器制造商 必须为程序员提供一种机制,以便控制速度和精确性之间经常背离的目标。

某些编译器通过为每种类型的优化单独提供“开关”在速度和精确性之间进行折衷。这使开发人员 可以禁用可能为其特定应用程序的浮点精确性带来变化的优化。尽管该解决方案可能提供对编译器的 高度控制,但它也会带来其他一些问题:

? 通常很难搞清楚需要启用或禁用哪些开关。

? 禁用任一优化都可能对非浮点代码的性能带来不利影响。

? 每个附加的开关都会引起许多新的开关组合;组合数目将很快变得难以控制。


因此,尽管为每种优化提供单独的开关看起来似乎很有吸引力,但使用此类编译器可能非常麻烦并 且不可靠。

许多 C++ 编译器提供了“一致性”浮点模型(通过 /Op 或 /fltconsistency 开关),从而使开 发人员能够创建符合严格浮点语义的程序。采用该模型时,可以防止编译器对浮点计算使用大多数优 化,同时允许其对非浮点代码使用这些优化。但是,该一致性模型具有一个缺点。为了在不同的 FPU 体系结构中返回可预测的结果,几乎所有 /Op 实现都将中间表达式舍入到用户指定的精度;例如,考 虑下面的表达式:

float a, b, c, d, e;

. . .

a = b*c + d*e;


为了在使用 /Op 开关时产生一致的且可重复的结果,该表达式的计算方式按如下方式实现:

float x = b*c;

float y = d*e;

a = x+y;


现在,最终结果在计算该表达式的每一步 中都产生了单精度舍入误差。尽管这种解释在严 格意义上并未破坏任何 C++ 语义规则,但它几乎肯定不是计算浮点表达式的最佳方法。通常,以 尽可能高的可行精度计算中间结果 更为可取。例如,以如下所示的较高精度计算表达式 a=b*c+d*e 将会更好:

double x = b*c;

double y = d*e;

double z = x+y;

a = (float)z;


或者,采用以下方式会更好:

long double x = b*c;

long double y = d*e

long double z = x+y;

a = (float)z;


在以较高精度计算中间结果时,最终结果显然会更为精确。具有讽刺意味的是,如果采用一致性模 型,则当用户试图通过禁用不安全的优化来减少误差时,出现误差的可能性却恰恰增加了。因此,一 致性模型不仅严重降低了效率,同时还无法对精确性的提高提供任何保证。对于认真的数值程序员而 言,这看起来不像是一个很好的折衷,这也是该模型没有被广泛接受的主要原因。

从版本 8.0 (Visual C++?2005) 开始,Microsoft C++ 编译器提供了一种更好的选择。它使程序 员可以选择以下三种常规浮点模式之一:fp:precise、fp:fast 和 fp:strict。

? 在 fp:precise 模式下,仅对浮点代码执行安全优化,并且与 /Op 不同,以最 高可行 精度一致性地执行中间计算。

? fp:fast 模式放松了浮点规则,允许以牺牲精确性为代价进行更为积极的优化。

? fp:strict 模式提供了 fp:precise 的所有常规正确性,同时启用了 fp- exception 语义,并禁止在存在 FPU 环境更改(例如,对寄存器精度、舍入方向的更改等等)时进行 非法转换。


#5
aipb20072007-05-05 11:46

Effective C++ 的52个条款列表

第一章 从C转向C++

条款1:尽量用const和inline而不用#define

条款2:尽量用而不用

条款3:尽量用new和delete而不用malloc和free

条款4:尽量使用c++风格的注释

第二章 内存管理
条款5:对应的new和delete要采用相同的形式

条款6:析构函数里对指针成员调用delete

条款7:预先准备好内存不够的情况

条款8 写operator new和operator delete时要遵循常规

条款9 避免隐藏标准形式的new

条款10 如果写了operator new就要同时写operator delete

第三章 构造函数,析构函数和赋值操作符

条款11 为需要动态分配内存的类声明一个拷贝构造函数和一个赋值操作符

条款12 尽量使用初始化而不要在构造函数里赋值

条款13 初始化列表中成员列出的顺序和它们在类中声明的顺序相同

条款14 确定基类有虚析构函数

条款15 让operator=返回this的引用

条款16 在operator=中对所有数据成员赋值

条款17 在operator=中检查给自己赋值的情况

第四章 类和函数:设计与声明

条款18 争取使类的接口完整并且最小

条款19 分清成员函数,非成员函数和友元函数

条款20 避免public接口出现数据成员

条款21 尽可能使用const

条款22 尽量用“传引用”而不用“传值”

条款23 必须返回一个对象时不要试图返回一个引用

条款24 在函数重载和设定参数缺省值间慎重选择

条款25 避免对指针和数字类型重载

条款26 当心潜在的二义性

条款27 如果不想使用隐式生成的函数就要显式地禁止它

条款28 划分全局名字空间

第五章 类和函数 实现

条款29 避免返回内部数据的句柄

条款30 避免这样的成员函数:其返回值是指向成员的非const指针或引用,但成员的访问级比这个函数要低

条款31 千万不要返回局部对象的引用,也不要返回函数内部用new初始化的指针的引用

条款32 尽可能地推迟变量的定义

条款33 明智地使用内联

条款34 将文件间的编译依赖性降至最低

第六章 继承和面向对象设计

条款35 使公有继承体现 是一个 的含义

条款36 区分接口继承和实现继承

条款37 决不要重新定义继承而来的非虚函数

条款38 决不要重新定义继承而来的缺省参数值

条款39 避免 向下转换 继承层次

条款40 通过分层来体现 有一个 或 用...来实现

条款41 区分继承和模板

条款42 明智地使用私有继承

条款43 明智地使用多继承

条款44 说你想说的;理解你所说的

第七章 杂项

条款45 弄清C++在幕后为你所写、所调用的函数

条款46 宁可编译和链接时出错,也不要运行时出错

条款47 确保非局部静态对象在使用前被初始化

条款48 重视编译器警告

条款49 熟悉标准库

条款50 提高对C++的认识

#6
aipb20072007-05-05 11:46

C/C++中的整型常识

很多人对C/C++中的整型不太了解,导致代码移植的时候出现问题,在此总结一下:

a. C/C++对整型长度的规定是为了执行效率,将int定义为机器字长可以取得最大的执行速度;

b. C/C++中整型包括:int, char 和 enum, C++中还包含bool类型,C99中bool是一个宏,实际为_Bool;

c. C 和 C++ 对 enum 的规定有所不同,这里不描述;

d. 修饰整型正负的有 signed 和 unsigned,对于 int 默认为 signed;

e. 修饰 int 大小的有 short 和 long, 部分编译器还扩展了一些更长的整型,比如 long long 和 __int64, C99中增加了long long和unsigned long long;

f. int 的长度 与 机器字长相同, 16位的编译器上int长16位,32位的编译器上int长32位;

g. short int 的长度 小于等于 int 的长度,注意她们可能长度相等,这取决于编译器;

h. long int 的长度 大于等于 int 的长度,注意她们可能长度相等,这取决于编译器;

i. char 的长度应当可以包容得下一个字符,大部分系统中就是一个字节,而有的系统中可能是4个字节,因为这些系统中一个字符需要四个字节来描述;

j. char 的正负取决于编译器,而编译器的决定取决于操作系统,在不同的编译器中char可能等同于signed char,也可能等同于unsigned char;

总结:

a. 出于效率考虑,应该尽量使用int和unsigned int;

b. 当需要指定容量的整型时,不应该直接使用short、int、long等,因为在不同的编译器上她们的容量不相同。此时应该定义她们相应的宏或类型,比如在VC++6.0中,可以如下定义:

typedef unsigned char UBYTE;

typedef signed char SBYTE;

typedef unsigned short int UWORD;

typedef signed short int SWORD;

typedef unsigned int UDWORD;

typedef signed int SDWORD;

typedef unsigned __int64 UQWORD;

typedef signed __int64 SQWORD;

然后在代码中使用 UBYTE、SBYTE、UWORD 等,这样当代码移植的时候只需要修改相应的类型即可。

定义自己的类型虽然在代码移植的时候只需要修改一处即可,但仍然属于源代码级别的修改,所以 C++ 2.0 中将这些类型定义在模板中,可以做到代码移植时无需修改代码。

c. 在定义char时,一定要加上 signed 或 unsigned,因为她的正负在不同的编译器上并不相同。

d. 不要想当然的以为char是1字节长,因为她的长度在不同的编译器上并不相同

#7
aipb20072007-05-05 11:46

C++的底层机制

c++为我们所提供的各种存取控制仅仅是在编译阶段给我们的限制,也就是说是编译器确保了你在完成任务之前的正确行为,如果你的行为不正确,那么你休想构造出任何可执行程序来。

但如果真正到了产生可执行代码阶段,无论是c,c++,还是pascal,大家都一样,你认为c和c++编译器产生的机器代码会有所不同吗,你认为c++产生的机器代码会有访问限制吗?那么你错了。什么const,private,统统没有(const变量或许会放入只读数据段),它不会再给你任何的限制,你可以利用一切内存修改工具或者是自己写一个程序对某一进程空间的某一变量进行修改,不管它在你的印象中是private,还是public,对于此时的你来说都一样,想怎样便怎样。

另外,你也不要为c++所提供的什么晚期捆绑等机制大呼神奇,它也仅仅是在所产生的代码中多加了几条而已,它远没有你想象的那么智能,所有的工作都是编译器帮你完成,真正到了执行的时候,计算机会完全按照编译器产生的代码一丝不苟的执行。

(以下的反汇编代码均来自visial c++ 7.0)

一.让我们从变量开始-----并非你想象的那么简单

变量是什么,变量就是一个在程序执行过程中可以改变的量。换一个角度,变量是一块内存区域的名字,它就代表这块内存区域,当我们对变量进行修改的时候,会引起内存区域中内容的改变。但是你若是学习过汇编或是计算机组成原理,那么你就会清楚对于一块内存区域来说,根本就不存在什么名字,它所仅有的标志就是他的地址,因此我们若想修改一块内存区域的内容,只有知道他的地址方能实现。看来所谓的变量一说只不过是编译器给我们进行的一种抽象,让我们不必去了解更多的细节,降低我们的思维跨度而已。例如下面这条语句:

int a=10;

按照我们的思维习惯来讲,就是“存在一个变量a,它的值是10”,一切都显得那么的自然。我们不必去在乎什么所谓的地址以及其他的一些细节。然而在这条语句的底层实现中,a已经不能算是一个变量了,它仅仅是一个标记,代表一个地址的标记:

mov dword ptr[a],0Ah;

怎么样,这条语句不像上面那条易于接受吧,因为它需要了解更多的细节,你几乎不能得到编译器的任何帮助,一切思维上的跨越必须由你自己完成。这条语句应该解释为“把10写入以a为地址的内存区域”。你说什么?a有些像指针?对,的确像,但还不是,只不过他们的过程似乎是类似的。这里所说的跨越实际上就是从一个现实问题到具体地址以及内存区域的跨越。

二.引用:你可以拥有引用,但编译器仅拥有指针(地址)

看过了第一条,你一定对编译器的工作有了一定的了解,实际上编译器就是程序员与底层之间的一个转换层,它把一个高级语言代码转换为低级语言代码,一个编译器完成的转换跨度越大,那么它也就会越复杂,因为程序员的工作都由他代为完成了。C++编译器必然比汇编编译器复杂就是这个道理。如果我问你引用和指针是一样的吗?你或许会说当然不一样了,指针容易产生不安全的因素,引用却不会,真的不会吗?我们来看下面这段代码:

int *e=new int(10);

int &f=*e;

delete e;

f=30;

你认为上面这段代码怎么样,我感觉就不很安全,它和指针有相同的隐患。因为它所引用的内存区域就不合法。

我个人认为,所谓的引用其实就是一种指针,只不过二者的接口并不相同,引用的接口有一定的限制。指针可以一对多,而引用却只能一对一,即&refer不能被改变,但却并不能说一对一就是安全的,只不过危险的系数降低罢了。引用比指针更容易控制。

Ok,下面来说说指针,曾经有过汇编经验的人一定会说,恩,指针的某些地方有些像汇编,尤其是那个“*”,怎么就那么像汇编中的“[]”啊。的确,它也涵盖了一个寻址的过程。看来指针的确是个比较低级的东西。然而引用却并不那么直接,虽然程序员用起来方便安全了许多。但是你要清楚,只有你可以拥有引用,编译器可没有这个工具,计算机并不认识这个东西。因此,它的底层机制实际上是和指针一样的。不要相信只有一块内存拷贝,不要认为引用可以为你节省一个指针的空间,因为这一切不会发生,编译器还是会把引用解释为指针。不管你相不相信,请看下面这段代码:

int& b=a;

lea eax,[a];

mov dword ptr[b],eax;把a的地址赋给地址为b的一块内存

b=50;

mov eax,dword ptr[b];

mov dword ptr[eax],32h;

int *d=&a;

lea eax,[a];

mov dword ptr[d],eax

*d=60;

mov eax,dword ptr[d]

mov dword ptr[eax],3ch;

以上的代码均来自具体的编译器,怎么样,相信了吧,好,让我再来做一个或许不怎么恰当的比拟,你一定编过有关线性表和栈的程序吧,线性表是一个非常灵活的数据结构,在他上面有许多的操作,然而栈呢,它是一个限制性操作的线性表,它的底层操作实际上是由线性表操作实现的。就好比stack与vector的关系,因此指针和引用的关系就好比线性表和栈的关系,引用也就是受限的指针,它对外的接口和指针虽然并不一样,但底层是相同的。

下面再来看看引用的一个重要用途,作为函数的参数传递的时候是怎样的情形:

void swapr(int &a, int &b);

void swapr(int* a, int *b);

int a=10;

int b=20;

swapr(a, b);

lea eax,[a];

push eax; //把a的地址压入堆栈

lea ecx,[b];

push ecx;

call swapr;

swapr(&a, &b);

lea eax,[a];

push eax;

lea ecx,[b];

push ecx;

call swapr;

怎么样,用引用和指针传递参数无论是在效率上还是在空间上都是完全一样的,如果妄想不传入地址就修改实参的值,简直就是天方夜谭,这就说明引用的本质就是指针。毕竟它们的行为都太相似了,如果不是这样,你还有什么方法去实现引用吗?记住,引用只不过是编译器为你提供的一个有用且安全的工具,对于机器代码可无法表示它,它把指针一对多的缺点去除,禁止了你的不安全的操作。但回到问题的本源,他们没有任何区别。

#8
aipb20072007-05-05 11:48

C++的中抽象

在C++中,以类、虚函数等为代表的数据抽象功能一直是C++的核心和难点。我认为C++的抽象应该是指:从我们需要解决的问题出发,在与该问题相关的一组关联对象中提取出主要的或共有的部分――说简单一点,就是用相同的行为来操作不同的对象。

从提出问题到找出与该问题相关的对象,这是一个互动的、反复的过程。在对相关对象的抽象中,随着认识的深入,我们可能会修改最初的目标,而最初目标的修改又可能使一组新的相关对象被加入进来。如:假设现在要设计一个基于广域网的邮件服务器,首先可能需要通过socket对底层协议进行封装,为高层的pop3、smtp协议提供一组标准的接口。开始为了使问题简化我们可能计划只封装TCP/IP协议,不过基于以下两点我们有理由修改最初的需求:

1、 pop3、smtp需要的底层接口很简单。除了连接,仅需要发送、接收一块数据的功能

2、 用socket进行网络编程,大多数常见协议间的差别很小,有许多都仅仅只是初始化和连接不同而已我们只需要做很小的努力就可以兼容大多数常用协议(如:ATM、Ipx、红外线协议等)。

现在决定修改需求,除了TCP/IP协议,还要支持一些其他的的常用协议。通过对最初目标的修改,除了TCP/IP协议对象,又会有一组相关的协议对象被加入进来。我们可以很容易从这组相关对象中提出共有的部分,将他抽象到另一个公共对象中。当然,根据具体应用环境不同,这可能并不是最佳方案。

C++中常规的抽象是在一组相互间有“血缘”关系的类中展开的。如:

Class Parent

{

virtual ~Parent(){};

virtual void GetValue(){ .... };

virtual void Test(){ ... };

};

class child1 : public parent

{

virtual ~child1(){};

virtual void GetValue(){...};

virtual void Test(){ ... } const;

};

class child2 : public parent

{

virtual ~child2(){};

virtual void GetValue(){...};

virtual void Test(){ ... } ;

};

(顺便说一句,child1::Test() const 不是基类 parent::Test() 的重载。)

由上可总结出C++中抽象的一些基本特点:

1、被抽象对象必须直接或间接派生至某个类对象

2、如果你不用没有类型安全的操作,如:向下转型操作或强制类型转化操作(像COM那样)。那么派生类中需要抽象的动作必须在某个基类中出现。

3、 基类的析构函数必须是一个虚函数

上述特点一般而言不会影响我们的抽象,但在一些特殊情况下就很难说了。比如:

假设为某个项目进行二次开发,到手的资料可能就是一堆dll、一堆头文件和一堆文档。这些dll里输出了很多的类,其中有一大堆都是离散的、毫无关系的类对象。经过一段时间的开发,你可能发现为了分别操作这些对象,程序中充满了switch...case.../if....else....语句。更扰人的是其实这些对象完全可以从某个基类派生,有些操作完全可以定义成virtual function。但在不能修改source code 的情况下(其实就算有源代码这样的修改也不可行)如何对这组对象进行抽象呢?

还有一些例子,比如:在MFC中,假设我们从Cdialog派生一组对话框类,如果我们在某个派生类中定义了一个自己的virtual function。那么除了重新在Cdialog和派生类之间再派生一个类层次,我们无法从外部以抽象的方式直接调用这个虚函数。但为了一个派生类和一个virtual function就添加一个类层次,这也太.....

将以上特例总结一下:C++中进行抽象的一组类之间必须有“血缘”关系。但在实际应用中我们有

时候有必要对一组离散的、没有关系的类对象(如来自不同的类库或者根本就没有virtual function)进行一些抽象操作――可能因为工作关系,我接触这种情况的机会比较多。传统的C++没有直接提供这方面的支持。在实际应用中我经常使用如下方法:

#include <list>

class parent

{

public:

virtual ~parent(){};

virtual void DoSomething( void ) const = 0;

};

template< typename T >

class child : public parent

{

public:

virtual ~child()

{

delete tu;

}

child( ):

{

tu = new T;

}

void DoSomething( void ) const

{

tu->InitObj();

}

private:

T *tu;

};

class test

{

public:

void InitObj( void )

{

::MessageBox( NULL, "Test", "test...ok!", MB_OK );

}

};

int main()

{

using namespace std;

list< parent* > plist;

parent *par = new child<test>();

plist.push_back( par );

}

以上方法用模板的方式来产生对象的代理。优点是完全未损失C++类型安全检查的特性,class object的一般普通成员函数就可以进行抽象调用了。缺点是调用的函数名被事先确定了――但这往往是不能接受的。为了改进这一点我在后来的使用中引入了member function pointer。代码如下:

#include<list>

class parent

{

public:

virtual ~parent(){};

virtual void do1( void ) const = 0;

virtual int do2( char* ) const = 0;

};

template< typename T >

class child : public parent

{

typedef void (T::*PFUN1)( void );

typedef int (T::*PFUN2)( char* );

public:

virtual ~child()

{

delete tu;

}

//////////////////////////////////////

child( PFUN1 p1 ):

fun1(p1), fun2(NULL)

{

tu = new T;

}

//------------------------------------

child( PFUN2 p2 ):

fun1(NULL), fun2(p2)

{

tu = new T;

}

//-------------------------------------

child( PFUN1 p1, PFUN2 p2 ):

fun1(p1), fun2(p2)

{

tu = new T;

}

////////////////////////////////////////

int do2( char *pch ) const

{

return fun2?(tu->*fun2)( pch ) : -1;

}

void do1( void ) const

{

fun1?(tu->*fun1)() : -1;

}

private:

T *tu;

PFUN1 fun1;

PFUN2 fun2;

};

class test

{

public:

void test1( void )

{

::MessageBox( NULL, "Test", "test...ok!", MB_OK );

}

};

int main()

{

using namespace std;

list< parent* > plist;

parent *par = new child<test>( test::test1 );

plist.push_back( par );

}

在这个例子中我只引用了两种类型的member function pointer:

typedef void (T::*PFUN1)( void );

typedef int (T::*PFUN2)( char* );

按上面的方法很容易扩展到其他函数类型。Construct child( PFUN1 p1, PFUN2 p2 )只是为了说明一个class object可以注册多种方法。更好的做法可能是将函数注册功能独立成一个函数。

总体来说以上方法只能作为一个特例来看。我们总是应该以常规的C++的方式进行抽象。C++中关于抽象的一些限制并不是我们进行抽象的阻碍。我们应该把它们看作“桥上的栏杆”,他们可以保证我们的逻辑思维沿着正确地方向前进!

#9
aipb20072007-05-05 11:49

C++实用技巧两则

在准标准C++中,有关缺省变量值的限制非常模糊。基于此,很多编译器允许开发人员将缺省变量值包含在函数声明,指向函数的指针和引用,成员函数的指针,以及typedef声明中。

请看一下以下的程序:

struct A

{void func(int x=5) {}

};

void g(int n=12)

{

}

// 根据C++标准,不能在以下声明中使用缺省变量值。

void (*pf)(inti=120);

void (A::*pmf)(int j=50);

typedef void (*PF)(inti=100);

// 函数的引用

typedef void (&PRF)(inti=100);

int main()

{pf=g;

PF pf2=g;

pmf=&A::func;

A a;

//这些调用使用了哪些缺省值?

pf();

pf2();

(a.*pmf)();

}

A::func()和g()具有缺省变量值,这是合理的。然而,指针pmf,pf以及typedef PF也定义了缺省的变量值。根据C++标准,这是不规范的。

这一代码的实际使用中,其中的一个问题是这些声明中提供的缺省值与A::func()和g()函数提供的值不一致。也就是说,很多编译器将这些代码作为非标准的扩展。当调用g()函数时,我的编译将120作为pf的缺省值;然而,对于pf2,它使用100作为它的缺省值。

作为一种规则,应该避免使用指向函数的指针,成员函数的指针,以及typedef命名的缺省变量值。即使你的编译器接受了它们,在更高版本中它也可能不被接受。而且,这些代码也会降低程序的灵巧性,也会给那些无法判别哪些编译器接收何种缺省变量的开发人员带来误导。在使用这些缺省变量值的合法代码中,我的建议是添加一些必要的注释,以说明需要哪些缺省变量值。

#10
aipb20072007-05-05 11:51

C++之运算符重载

有了C++语言,你就可以重载函数和运算符。重载是一种应用,它在同一范围中为一个给定函数名称提供了多种定义。委托编译器依据调用该函数的参量选择合适的函数或运算符的版本。例如:

double max(double d1,double d2)

{

return (di>d2)?d1:d2;

}
int max (int e1,int e2)

{

return (e1>e2)?e1:e2;

}

作为一个重载函数,函数max在程序中使用如下:

main()

{

int e=nax(12,8);

double d=max(123.4,12.3);

return e+(int)d;

}

在第一个例子中,要求出两个整型变量的最大值,故调用函数(int,int)。然而,在第二种情况下,两个参量是浮点型,因此调用的函数是max(double,double)。

重载函数之间的区别在于带有不同初始值的参量类型。因而对一个给定类型的参量以及对于该类型的引用,在重载的意义上来说是完全相同的。它们被看成是相同的,因为它们采用了相同的初始值。例如:max(double,double)和(double&,double &)是完全相同的,说明两个这样的函数会引起错误。出于相同的原因,用修饰符const和volatile进行修饰的函数参量类型同基本类型,在重载的意义上看没有什么不同。然而重载函数的机制可以区分由const或volatile修饰的引用以及基本类型的引用。指向const和volatile对象的指针和指向其基本类型的指针在重载意义上是不同的。

一组重载函数是否是可接受的如下限制:

·该组重载函数中任何两个都必须有不同的参量表。

·具有相同类型参量表、仅在返回值类型上不同的重载函数会引起错误。

·成员函数的重载不能仅基于一个说明为静态的,另一个说明为非静态的。

·typedef说明并未定义新的类型,它们仅为已存在的类型引入了一个同义词。它们不能影响重载机制。

·枚举类型是一些可区分的类型,故可以区分重载函数。

·从区分重载函数的意义上说,类型“数组”和“指针”是相同的。对于一维数组来说是正确的。

了解了函数重载,下面将具体的介绍运算符重载

注意,下面的规则约束着重载运算符如何实现,但它们并不适用于new和delete运算符。

·运算符必须要么是成员函数,或者带有某个类的参量,或者是枚举类型的参量、或者带有某个类的引用、或某个枚举的类型的引用。

·运算符要遵守它们同内部类型一起使用所指定的优先原则、分组及操作数的个数。因此,无法表达把2及3加到一个Point对象中的含义,除了把2加到X坐标中,把3加到Y坐标中。

·单目运算符说明为成员函数不带参量;如果说明为全局函数,要带一个参量。双目运算符说明为成员函数只带一个参量;如果说明为全局函数,要带两个参量。

·所有的重载运算符除了赋值(operator=)外均可被派生类继承。

·重载运算符的成员函数的第一个参量总是激活该运算符的对象的类类型参量(运算符被定义的类,或者定义了运算符的类的派生类)。对于第一个参量也不支持转换。

任何运算符的意义都可能被完全地改变了,这包括取地址(&)、赋值(=)、函数调用运算符的含义。同理,内部的类型可由于使用了运算符重载而改变。例如:下面四条语句在完成求值以后完全等同:

s=s+1;

s+=1;

s++;

++s;

对于重载了运算符的类类型来说,这种确信是靠不住的,而且,对于在基本类型中使用这些运算符的隐含条件,对于重载的运算符来说是放松了。例如:加法/赋值操纵符,在应用于基本类型时,要求其左操作数是l值的;但此运算符重载以后就没有这种要求了。

下面给出可重载的单目运算符:

运算符 名称
! 逻辑非
& 取地址
~ 求补
* 指针间接引用
+ 单目加
++ 增1
- 单目取反
-- 减1

如果说明一个单目运算符为一个非静态成员,必须按如下形式进行说明:

ret-type operator op()

其中ret-type是返回类型,而op是上表中的某个运算符。

说明一个单目运算符为全局函数,必须按如下形式说明:

ret-type operator op(arg)

其中ret-type和op的含义同成员运算符函数中的描述,而arg是对其操作的类类型参量。

注意:对于单目运算符的返回值没有限制。例如,对于一个逻辑非运算符,返回一个必要的值是合适的。但这一点不是必须的。

双目运算符

下表给出了可被重载的运算符。

运算符 名称
, 逗号
!= 不等

% 取模
%= 取模/赋值
& 按位和
&& 逻辑和
&= 按位和/赋值
* 乘法
*= 乘法/赋值
+ 加法
+= 加法/赋值
- 减法
-= 减法/赋值
-> 成员选择
->* 指向成员的指针选择
/ 除法
/= 除法/赋值
< 小于
<< 左移
<<= 左移/赋值
<= 小于等于
= 赋值
== 等于
> 大于

[此贴子已经被作者于2007-5-5 11:54:11编辑过]

#11
aipb20072007-05-05 11:54

C++中函数指针数组的使用

笔者在开发某软件过程中遇到这样一个问题,前级模块传给我二进制数据,输入参数为 char* buffer和 int length,buffer是数据的首地址,length表示这批数据的长度。数据的特点是:长度不定,类型不定,由第一个字节(buffer[0])标识该数据的类型,共有256(28 )种可能性。

我的任务是必须对每一种可能出现的数据类型都要作处理,并且我的模块包含若干个函数,在每个函数里面都要作类似的处理。若按通常做法,会写出如下代码:

void MyFuntion( char* buffer, int length )

{__int8 nStreamType = buffer[0];

switch( nStreamType )

{case 0:

function1();

 break;

 case 1:

 ......

 case 255:

 function255();

 break;

 }

如果按照这种方法写下去,那么在我的每一个函数里面,都必须作如此多的判断,写出的代码肯定很长,并且每一次处理,都要作许多次判断之后才找到正确的处理函数,代码的执行效率也不高。针对上述问题,我想到了用函数指针数组的方法解决这个问题。

函数指针的概念,在潭浩强先生的C语言程序设计这本经典的教程中提及过,在大多数情况下我们使用不到,也忽略了它的存在。函数名实际上也是一种指针,指向函数的入口地址,但它又不同于普通的如int*、double*指针,看下面的例子来理解函数指针的概念:

1 int funtion( int x, int y );

2 void main ( void )

 {

3  int (*fun) ( int x, int y );

4  int a = 10, b = 20;

5  function( a, b );

6  fun = function;

7  (*fun)( a, b );

8 ……

}

语句1定义了一个函数function,其输入为两个整型数,返回也为一个整型数(输入参数和返回值可为其它任何数据类型);语句3定义了一个函数指针,与int*或double*定义指针不同的是,函数指针的定义必须同时指出输入参数,表明这是一个函数指针,并且*fun也必须用一对括号括起来;语句6将函数指针赋值为funtion,前提条件是*fun和function的输入参数和返回值必须保持一致。语句5直接调用函数function(),语句7是调用函数指针,二者等效。

当然从上述例子看不出函数指针的优点,目的主要是想引出函数指针数组的概念。我们从上面例子可以得知,既然函数名可以通过函数指针加以保存,那们也一定能定义一个数组保存若干个函数名,这就是函数指针数组。正确使用函数指针数组的前提条件是,这若干个需要通过函数指针数组保存的函数必须有相同的输入、输出值。

这样,我工作中所面临的问题可以解决如下:

首先定义256个处理函数(及其实现)。

void funtion0( void );

……..

void funtion255(void );

其次定义函数指针数组,并给数组赋值。

void (*fun[256])(void);

fun[0] = function0;

…….

fun[255] = function();

最后,MyFunction()函数可以修改如下:

void MyFuntion( char* buffer, int length )

{

__int8 nStreamType = buffer[0];

(*fun[nStreamType])();

}

只要2行代码,就完成了256条case语句要做的事,减少了编写代码时工作量,将nStreamType作为数组下标,直接调用函数指针,从代码执行效率上来说,也比case语句高。假如多个函数中均要作如此处理,函数指针数组更能体现出它的优势。

#12
aipb20072007-05-05 11:55

C++中结构体的的慨念和使用方法
结构体

结构体就是一个可以包含不同数据类型的一个结构,它是一种可以自己定义的数据类型,它的特点和数组主要有两点不同,首先结构体可以在一个结构中声明不同的数据类型,第二相同结构的结构体变量是可以相互赋值的,而数组是做不到的,因为数组是单一数据类型的数据集合,它本身不是数据类型(而结构体是),数组名称是常量指针,所以不可以做为左值进行运算,所以数组之间就不能通过数组名称相互复制了,即使数据类型和数组大小完全相同。

定义结构体使用struct修饰符,例如:

C++ 代码

struct test

{

 float a;

 int b;

};

上面的代码就定义了一个名为test的结构体,它的数据类型就是test,它包含两个成员a和b,成员a的数据类型为浮点型,成员b的数据类型为整型。

由于结构体本身就是自定义的数据类型,定义结构体变量的方法和定义普通变量的方法一样。

test pn1;

这样就定义了一test结构体数据类型的结构体变量pn1,结构体成员的访问通过点操作符进行,

pn1.a=10 就对结构体变量pn1的成员a进行了赋值操作,

注意:结构体生命的时候本身不占用任何内存空间,只有当你用你定义的结构体类型定义结构体变量的时候计算机才会分配内存。

结构体,同样是可以定义指针的,那么结构体指针就叫做结构指针。

结构指针通过->符号来访问成员,下面我们就以上所说的看一个完整的例子:

C++ 代码

//程序作者:管宁

//所有稿件均有版权,如要转载,请务必注明出处和作者

#include<iostream>

#include<string>

usingnamespacestd;

structtest//定义一个名为test的结构体

{

 inta;//定义结构体成员a

 intb;//定义结构体成员b

};

voidmain()

{

 testpn1;//定义结构体变量pn1

 testpn2;//定义结构体变量pn2

 pn2.a=10;//通过成员操作符.给结构体变量pn2中的成员a赋值

 pn2.b=3;//通过成员操作符.给结构体变量pn2中的成员b赋值

 pn1=pn2;//把pn2中所有的成员值复制给具有相同结构的结构体变量pn1

 cout<<pn1.a<<"|"<<pn1.b<<endl;

 cout<<pn2.a<<"|"<<pn2.b<<endl;

 test*point;//定义结构指针

 point=&pn2;//指针指向结构体变量pn2的内存地址

 cout<<pn2.a<<"|"<<pn2.b<<endl;

 point->a=99;//通过结构指针修改结构体变量pn2成员a的值

 cout<<pn2.a<<"|"<<pn2.b<<endl;

 cout<<point->a<<"|"<<point->b<<endl;

 cin.get();

}

总之,结构体可以描述数组不能够清晰描述的结构,它具有数组所不具备的一些功能特性。

#13
aipb20072007-05-05 11:55

C++中数组和指针类型的关系
一个整数类型数组如下进行定义: C++ 代码 int a[]={1,2,3,4};

如果简单写成: C++ 代码 a;//数组的标识符名称

这将代表的是数组第一个元素的内存地址,a;就相当于&a[0],它的类型是数组元素类型的指针,在这个例子中它的类型就是int*

如果我们想访问第二个元素的地址我们可以写成如下的两种方式!

C++ 代码

&a[1];

a+1//注意这里的表示就是将a数组的起始地址向后进一位,移动到第二个元素的地址上也就是a[0]到a[1]的过程!

数组名称和指针的关系其实很简单,其实数组名称代表的是数组的第一个元素的内存地址,这和指针的道理是相似的!

#14
aipb20072007-05-05 12:02

C++中用函数模板实现和优化抽象操作
在创建完成抽象操作的函数时,如:拷贝,反转和排序,你必须定义多个版本以便能处理每一种数据类型。以 max() 函数为例,它返回两个参数中的较大者:

double max(double first, double second);

complex max(complex first, complex second);

date max(date first, date second);

//..该函数的其它版本

尽管这个函数针对不同的数据类型其实现都是一样的,但程序员必须为每一种数据类型定义一个单独的版本:

double max(double first, double second)

{

 return first>second? first : second;

}

complex max(complex first, complex second)

{

 return first>second? first : second;

}

date max(date first, date second)

{

 return first>second? first : second;

}

这样不但重复劳动,容易出错,而且还带来很大的维护和调试工作量。更糟的是,即使你在程序中不使用某个版本,其代码仍然增加可执行文件的大小,大多数编译器将不会从可执行文件中删除未引用的函数。

用普通函数来实现抽象操作会迫使你定义多个函数实例,从而招致不小的维护工作和调试开销。解决办法是使用函数模板代替普通函数。

使用函数模板

函数模板解决了上述所有的问题。类型无关并且只在需要时自动实例化。本文下面将展示如何定义函数模板以便抽象通用操作,示范其使用方法并讨论优化技术。

第一步:定义

函数模板的声明是在关键字 template 后跟随一个或多个模板在尖括弧内的参数和原型。与普通函数相对,它通常是在一个转换单元里声明,而在另一个单元中定义,你可以在某个头文件中定义模板。例如:

// file max.h

#ifndef MAX_INCLUDED

#define MAX_INCLUDED

template <class T> T max(T t1, T t2)

{

 return (t1 > t2) ? t1 : t2;

}

#endif

<class T> 定义 T 作为模板参数,或者是占位符,当实例化 max()时,它将替代具体的数据类型。max 是函数名,t1和t2是其参数,返回值的类型为 T。你可以像使用普通的函数那样使用这个 max()。编译器按照所使用的数据类型自动产生相应的模板特化,或者说是实例:

int n=10,m=16;

int highest = max(n,m); // 产生 int 版本

std::complex<double> c1, c2;

//.. 给 c1,c2 赋值

std::complex<double> higher=max(c1,c2); // complex 版本

第二步:改进设计

上述的 max() 的实现还有些土气——参数t1和t2是用值来传递的。对于像 int,float 这样的内建数据类型来说不是什么问题。但是,对于像std::complex 和 std::sting这样的用户定义的数据类型来说,通过引用来传递参数会更有效。此外,因为 max() 会认为其参数是不会被改变的,我们应该将 t1和t2声明为 const (常量)。下面是 max() 的改进版本:

template <class T> T max(const T& t1, const T& t2)

{

 return (t1 > t2) ? t1 : t2;

}

额外的性能问题

很幸运,标准模板库或 STL 已经在 <algorithm> 里定义了一个叫 std::max()的算法。因此,你不必重新发明。让我们考虑更加现实的例子,即字节排序。众所周知,TCP/IP 协议在传输多字节值时,要求使用 big endian 字节次序。因此,big endian 字节次序也被称为网络字节次序(network byte order)。如果目的主机使用 little endian 次序,必须将所有过来的所字节值转换成 little endian 次序。同样,在通过 TCP/IP 传输多字节值之前,主机必须将它们转换成网络字节次序。你的 socket 库声明四个函数,它们负责主机字节次序和网络字节次序之间的转换:

unsigned int htonl (unsigned int hostlong);

unsigned short htons (unsigned short hostshort);

unsigned int ntohl (unsigned int netlong);

unsigned short ntohs (unsigned short netshort);

这些函数实现相同的操作:反转多字节值的字节。其唯一的差别是方向性以及参数的大小。非常适合模板化。使用一个模板函数来替代这四个函数,我们可以定义一个聪明的模板,它会处理所有这四种情况以及更多种情形:

template <class T> T byte_reverse(T val);

为了确定 T 实际的类型,我们使用 sizeof 操作符。此外,我们还使用 STL 的 std::reverse 算法来反转值的字节:

template <class T> T byte_reverse(T val)

{

 // 将 val 作为字节流

 unsigned char *p=reinterpret_cast<unsigned char*> (&val);

 std::reverse(p, p+sizeof(val));

 return val;

}

使用方法

byte_reverse() 模板处理完全适用于所有情况。而且,它还可以不必修改任何代码而灵活地应用到其它原本(例如:64 位和128位)不支持的类型:

int main()

{

 int n=1;

 short k=1;

 __int64 j=2, i;

 int m=byte_reverse(n);// reverse int

 int z=byte_reverse(k);// reverse short

 k=byte_reverse(k); // un-reverse k

 i=byte_reverse(j); // reverse __int64

}

注:模板使用不当会影响.exe 文件的大小,也就是常见的代码浮肿问题。

#15
aipb20072007-05-05 12:03

浅析c/c++中的指针
在学习c/c+过程中,指针是一个比较让人头痛的问题,稍微不注意将会是程序编译无法

通过,甚至造成死机。在程序设计过程中,指针也往往是产生隐含bug的原因。下面就来

谈谈指针的应用以及需要注意的一些问题,里面也许就有你平时没有注意到的问题,希

望能帮助各位读者理解好指针。

一、我们先来回忆一下指针的概念吧,方便下面的介绍

指针是存放地址值的变量或者常量。例如:int a=1;&a就表示指针常量(“&”表示

取地址运算符,也即引用)。int *b,b表示的是指针变量(注意,是b表示指针变量而

不是*b),*表示要说明的是指针变量。大家注意int *b[2]和int(*b)[2]是不同的,

int *b表示一个指针数组,而int (*b)[2]表示含有两个元素的int指针,这里要注意

运算优先级问题,有助于理解指针问题。

在这里大概介绍基本概念就够了,至于具体使用方法,如赋值等,很多书都有介绍

,我就不多说了。

二、应用以及注意的问题

1、 理解指针的关键所在——对指针类型和指针所指向的类型的理解

①、 指针类型:可以把指针名字去掉,剩下的就是这个指针

例如:int *a;//指针类型为int *

int **a;//指针类型为int **

int *(*a)[8];//指针类型为 int *(*)[8]

②、 指针所指向的类型:是指编译器将把那一片内存所看待成的类型。这里只要把

指针声明语句中的指针名字和名字右边的“*”号去掉就可以了,剩下的就是指针所指向

的类型。

我之所以把他们放在第一位,是因为弄清楚他们是学c/c++指针的重点,正确理解他

们才能使你打好c/c++的编程基础。

2、 指针的应用——传递参数。

其实它可以相当于隐式的返回值,这就比return的方法更加灵活了,可以返回更多

的值,看看下面的例子自然就明白了:

#include "iostream.h"

void example(int *a1,int &b1,int c1)

{

 *a1*=3;

 ++b1;

 ++c1;

}

void main()

{

 int *a;

 int b,c;

 *a=6;

 b=7;c=10;

 example(a,b,c);

 cout <<"*a="<<*a<

 cout <<"b="<

 cout <<"c="<

}

输出:*a=18

b=8

c=10

注意到没有,*a和b的值都改变了,而c没有变。这是由于a1是指向*a(=6)的指针

,也即与a是指向同一个地址,所以当a1指向的值改变了,*a的值也就改变了。在函数中

的参数使用了引用(int &b1),b1是b的别名,也可以把它当作特殊的指针来理解,所

以b的值会改变。函数中的参数int c1只是在函数中起作用,当函数结束时候便消失了,

所以在main()中不起作用。

3、 关于全局变量和局部变量的一个问题

先不废话了,先看看程序:

#include “iostream.h”

int a=5;

int *example1(int b)

{

a+=b;

return &a;

}

int *example2(int b)

{

int c=5;

b+=c;

return &b;

}

void main()

{

int *a1=example1(10);

int *b1=example2(10);

cout <<”a1=”<<*a1<

cout <<”b1=”<<*b1<

}

输出结果:

a1=15

b1=4135

*b1怎么会是4135,而不是15呢?是程序的问题?没错吧?

由于a是全局变量,存放在全局变量的内存区,它一直是存在的;而局部变量则是存

在于函数的栈区,当函数example2()调用结束后便消失,是b指向了一个不确定的区域

,产生指针悬挂。

下面是对example1()和example2()的反汇编(用TC++ 3.0编译):

example1():

push bp;入栈

mov bp,sp

mov ax,[bp+04];传递参数

add [00AA],ax;相加

mov ax,00AA ;返回了结果所在的地址

.

.

.

pop bp;恢复栈,出栈

ret;退出函数

example2():

push bp;入栈

mov bp,sp

sub sp,02

mov word ptr [bp-02],0005

mov ax,[bp-02];传递参数

add [bp+04],ax;相加

lea ax,[bp+04];问题就出在这里

.

.

.

mov sp,bp

pop bp;恢复栈,出栈

ret;退出函数

对比之后看出来了吧?ax应该是存储的是结果的地址。而在example2()中,返回

的却是[bp+04]的内容,因此指针指向了一个不确定的地方,由此产生的指针悬挂。exa

mple1()中,ax返回了正确的结果的地址。

4、 内存问题:使用指针注意内存的分配和边界。

使用指针过程中应该给变量一个适当的空间,以免产生不可见的错误。

请看以下代码:

#include “iostream.h”

void main()

{

char *a1;

char *a2;

cin >>a1;

cin >>a2;

cout <<”a1=”<

cout <<”a2=”<

}

输入:abc

123

输出:

a1=123

a2=

Null pointer assignment

指针指向了“空”。解决办法就是分配适当的内存给这两个字符串。修正后的代码

如下:

#include “iostream.h”

void main()

{

char *a1;

char *a2;

a1=new char [10];

a2=new char [10];

cin >>a1;

cin >>a2;

cout <<”a1=”<

cout <<”a2=”<

delete(a1);注意,别忘了要释放内存空间

delete(a2);

}

到此就能输出正确的结果了。

分配了适当的内存之后要注意释放内参空间,同时还应该注意不要超出所分配的内

存的大小,否则会有溢出现象产生,导致不可预料的结果。

5、 关于特殊的指针——引用

引用有时候应用起来要比指针要灵活,用它做返回的时候是不产生任何变量的副本

的这样减小了内存的占用,提高执行的速度。引用使用起来要比指针好理解,比较直观

。当引用作为参数时,不会改变参数的地址,因此可以作为左值。

下面请看一个例子:

#include “iostream.h”

char ch[5]=”ABCD”;

char &example(int b)

{

return ch;

}

void main()

{

cout <<”ch=”<

example(2)=”c”;

cout<<”ch=”<

}

输出结果:

ch=ABCD

ch=ABcD

在实际编程过程中,可以灵活地引用或指针,尽量提高程序的可读性和执行效率。

三、小结:

指针是学习c/c++的重点难点,主要原因是指针较为抽象,不容易理解。使用指针千

万要明白让指针指向什么地方,如何让指针指向正确的地方。在深入系统底层之中需要

应用到大量的指针,因此需要理解好指针的基本概念,例如:指针类型和指针所指向的

类型。平时应该对留心观察,了解程序的工作过程,必要时候可以对程序进行反汇编,

加深对指针的理解,这种方法同样适合学别的编程方面的知识。

四、结束:

指针的应用是很广泛的,利用指针可以做很多事情,要成为一个好的程序员,必须

对指针有比较深刻的了解。写本文的目的在于让大家对指针有更深一层的了解,提高指

针的应用能力,内容大都是我在实际编程中遇到的问题。相信能给大家一定的帮助。

#16
aipb20072007-05-05 12:04

教你理解复杂的C/C++声明
介绍

曾经碰到过让你迷惑不解、类似于int * (* (*fp1) (int) ) [10];这样的变量声明吗?本

文将由易到难,一步一步教会你如何理解这种复杂的C/C++声明:我们将从每天都能碰到的

较简单的声明入手,然后逐步加入const修饰符和typedef,还有函数指针,最后介绍一个

能够让你准确地理解任何C/C++声明的“右左法则”。需要强调一下的是,复杂的C/C++声

明并不是好的编程风格;我这里仅仅是教你如何去理解这些声明。注意:为了保证能够在

同一行上显示代码和相关注释,本文最好在至少1024x768分辨率的显示器上阅读。

基础

让我们从一个非常简单的例子开始,如下:

int n;

这个应该被理解为“declare n as an int”(n是一个int型的变量)。

接下去来看一下指针变量,如下:

int *p;

这个应该被理解为“declare p as an int *”(p是一个int *型的变量),或者说p是一

个指向一个int型变量的指针。我想在这里展开讨论一下:我觉得在声明一个指针(或引用

)类型的变量时,最好将*(或&)写在紧靠变量之前,而不是紧跟基本类型之后。这样可

以避免一些理解上的误区,比如:

int* p,q;

第一眼看去,好像是p和q都是int*类型的,但事实上,只有p是一个指针,而q是一个最简

单的int型变量。

我们还是继续我们前面的话题,再来看一个指针的指针的例子:

char **argv;

理论上,对于指针的级数没有限制,你可以定义一个浮点类型变量的指针的指针的指针的

指针...

再来看如下的声明:

int RollNum[30][4];

int (*p)[4]=RollNum;

int *q[5];

这里,p被声明为一个指向一个4元素(int类型)数组的指针,而q被声明为一个包含5个元

素(int类型的指针)的数组。

另外,我们还可以在同一个声明中混合实用*和&,如下:

int **p1; // p1 is a pointer to a pointer to an int.

int *&p2; // p2 is a reference to a pointer to an int.

int &*p3; // ERROR: Pointer to a reference is illegal.

int &&p4; // ERROR: Reference to a reference is illegal.

注:p1是一个int类型的指针的指针;p2是一个int类型的指针的引用;p3是一个int类型引

用的指针(不合法!);p4是一个int类型引用的引用(不合法!)。

const修饰符

当你想阻止一个变量被改变,可能会用到const关键字。在你给一个变量加上const修饰符

的同时,通常需要对它进行初始化,因为以后的任何时候你将没有机会再去改变它。例如

const int n=5;

int const m=10;

上述两个变量n和m其实是同一种类型的--都是const int(整形恒量)。因为C++标准规定

,const关键字放在类型或变量名之前等价的。我个人更喜欢第一种声明方式,因为它更突

出了const修饰符的作用。

当const与指针一起使用时,容易让人感到迷惑。例如,我们来看一下下面的p和q的声明:

const int *p;

int const *q;

他们当中哪一个代表const int类型的指针(const直接修饰int),哪一个代表int类型的

const指针(const直接修饰指针)?实际上,p和q都被声明为const int类型的指针。而i

nt类型的const指针应该这样声明:

int * const r= &n; // n has been declared as an int

这里,p和q都是指向const int类型的指针,也就是说,你在以后的程序里不能改变*p的值

。而r是一个const指针,它在声明的时候被初始化指向变量n(即r=&n;)之后,r的值将不

再允许被改变(但*r的值可以改变)。

组合上述两种const修饰的情况,我们来声明一个指向const int类型的const指针,如下:

const int * const p=&n // n has been declared as const int

下面给出的一些关于const的声明,将帮助你彻底理清const的用法。不过请注意,下面的

一些声明是不能被编译通过的,因为他们需要在声明的同时进行初始化。为了简洁起见,

我忽略了初始化部分;因为加入初始化代码的话,下面每个声明都将增加两行代码。

char ** p1; // pointer to pointer to char

const char **p2; // pointer to pointer to const char

char * const * p3; // pointer to const pointer to char

const char * const * p4; // pointer to const pointer to const char

char ** const p5; // const pointer to pointer to char

const char ** const p6; // const pointer to pointer to const char

char * const * const p7; // const pointer to const pointer to char

const char * const * const p8; // const pointer to const pointer to const char

注:p1是指向char类型的指针的指针;p2是指向const char类型的指针的指针;p3是指向

char类型的const指针;p4是指向const char类型的const指针;p5是指向char类型的指针

的const指针;p6是指向const char类型的指针的const指针;p7是指向char类型const指针

的const指针;p8是指向const char类型的const指针的const指针。

typedef的妙用

typedef给你一种方式来克服“*只适合于变量而不适合于类型”的弊端。你可以如下使用

typedef:

typedef char * PCHAR;

PCHAR p,q;

这里的p和q都被声明为指针。(如果不使用typedef,q将被声明为一个char变量,这跟我

们的第一眼感觉不太一致!)下面有一些使用typedef的声明,并且给出了解释:

typedef char * a; // a is a pointer to a char

typedef a b(); // b is a function that returns

// a pointer to a char

typedef b *c; // c is a pointer to a function

// that returns a pointer to a char

typedef c d(); // d is a function returning

// a pointer to a function

// that returns a pointer to a char

typedef d *e; // e is a pointer to a function

// returning a pointer to a

// function that returns a

// pointer to a char

e var[10]; // var is an array of 10 pointers to

// functions returning pointers to

// functions returning pointers to chars.

typedef经常用在一个结构声明之前,如下。这样,当创建结构变量的时候,允许你不使用

关键字struct(在C中,创建结构变量时要求使用struct关键字,如struct tagPOINT a;

而在C++中,struct可以忽略,如tagPOINT b)。

typedef struct tagPOINT

{

int x;

int y;

}POINT;

POINT p; /* Valid C code */

函数指针

函数指针可能是最容易引起理解上的困惑的声明。函数指针在DOS时代写TSR程序时用得最

多;在Win32和X-Windows时代,他们被用在需要回调函数的场合。当然,还有其它很多地

方需要用到函数指针:虚函数表,STL中的一些模板,Win NT/2K/XP系统服务等。让我们来

看一个函数指针的简单例子:

int (*p)(char);

这里p被声明为一个函数指针,这个函数带一个char类型的参数,并且有一个int类型的返

回值。另外,带有两个float类型参数、返回值是char类型的指针的指针的函数指针可以声

明如下:

char ** (*p)(float, float);

那么,带两个char类型的const指针参数、无返回值的函数指针又该如何声明呢?参考如下

void * (*a[5])(char * const, char * const);

“右左法则”[重要!!!]

The right-left rule: Start reading the declaration from the innermost parenthe

ses, go right, and then go left. When you encounter parentheses, the direction

should be reversed. Once everything in the parentheses has been parsed, jump

out of it. Continue till the whole declaration has been parsed.

这是一个简单的法则,但能让你准确理解所有的声明。这个法则运用如下:从最内部的括

号开始阅读声明,向右看,然后向左看。当你碰到一个括号时就调转阅读的方向。括号内

的所有内容都分析完毕就跳出括号的范围。这样继续,直到整个声明都被分析完毕。

对上述“右左法则”做一个小小的修正:当你第一次开始阅读声明的时候,你必须从变量

名开始,而不是从最内部的括号。

下面结合例子来演示一下“右左法则”的使用。

int * (* (*fp1) (int) ) [10];

阅读步骤:

1. 从变量名开始 -------------------------------------------- fp1

2. 往右看,什么也没有,碰到了),因此往左看,碰到一个* ------ 一个指针

3. 跳出括号,碰到了(int) ----------------------------------- 一个带一个int参数

的函数

4. 向左看,发现一个* --------------------------------------- (函数)返回一个指

5. 跳出括号,向右看,碰到[10] ------------------------------ 一个10元素的数组

6. 向左看,发现一个* --------------------------------------- 指针

7. 向左看,发现int ----------------------------------------- int类型

总结:fp1被声明成为一个函数的指针,该函数返回指向指针数组的指针.



#17
neverDie2007-05-05 16:47
好多哦,学习学习。
#18
华龙2007-05-06 21:45

以后慢慢看,谢谢楼主了。
不可以申请加精的?

#19
yuyunliuhen2007-05-07 00:24
俺顶一个
#20
xxstar182007-05-08 20:26

That's just what i need. thanks!~

#21
超级新人2007-05-12 08:45
谢谢了
#22
feitianyjx2007-05-12 18:05
好东东。。保存了。。。。。。。。。。。谢谢楼主。。。。。
#23
lifujun1172007-06-01 21:58
对我很有帮助,多谢版主分享。
#24
aipb20072007-06-02 00:23

////////////////////////////////////////原文////////////////////////////////////////////////////////////
Q: 我能防止别人从我的类继承吗?
A: 可以的,但何必呢?好吧,也许有两个理由:

出于效率考虑——不希望我的函数调用是虚的
出于安全考虑——确保我的类不被用作基类(这样我拷贝对象时就不用担心对象被切割(slicing)了)[译注:“对象切割”指,将派生类对象赋给基类变量时,根据C++的类型转换机制,只有包括在派生类中的基类部分被拷贝,其余部分被“切割”掉了。]
根据我的经验,“效率考虑”常常纯属多余。在C++中,虚函数调用如此之快,和普通函数调用并没有太多的区别。请注意,只有通过指针或者引用调用时才会启用虚拟机制;如果你指名道姓地调用一个对象,C++编译器会自动优化,去除任何的额外开销。
如果为了和“虚函数调用”说byebye,那么确实有给类继承体系“封顶”的需要。在设计前,不访先问问自己,这些函数为何要被设计成虚的。我确实见过这样的例子:性能要求苛刻的函数被设计成虚的,仅仅因为“我们习惯这样做”!

好了,无论如何,说了那么多,毕竟你只是想知道,为了某种合理的理由,你能不能防止别人继承你的类。答案是可以的。可惜,这里给出的解决之道不够干净利落。你不得不在在你的“封顶类”中虚拟继承一个无法构造的辅助基类。还是让例子来告诉我们一切吧:

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; // error: DD::DD() cannot access
// Usable_lock::Usable_lock(): private member

//////////////////////////////////////////////////////////////////////////////////////////////////////////

如果把
class Usable : public /*virtual*/ Usable_lock //虚继承Usable_lock类

这样,代码就可以通过,就没有达到防止Usable 被继承的作用。

////////////////////////////////////////////////////////////////////////////////
先说说上面的代码,包括继承原理:

首先,把我们不想被继承的类作为一个派生类去继承一个没哟意义的Usable_lock类,注意,这个类显示的把构造函数
声明为私有,然后把Usable作为友员类,这就是关键。
再来,继承中派生类创建的过程:首先要调用父类的构造函数,即先建立一个父类的对象。
这里Usable类是公有继承,所以是访问不到私有的构造函数,但是作为友员是可以的,所以消除了这个限制。由此,我们的Usable类是可以被创建的。

到这里,看看为什么普通的非虚拟继承,能使dd的创建合法。
首先,按照我们先前说讲,DD类公有继承Usable类,创建dd对象的过程是先调用父类构造函数(Usable类构造函数),这一步合法,因为可以访问到Usable的构造函数。这时就要创建一个Usable对象,要创建Usable 对象,就要调用Usable 的父类构造函数,就来到Usable_lock类,开始我们说了,由Usable访问Usable_lock是没有问题的。所以这样一来,dd对象就被正确的创建了。他间接的访问过Usable_lock。

////////////////////////////////////////////////////////////////////////
下面说说BJ在这个例子中是怎样通过虚拟技术防止Usable类被继承的:
虚拟继承,通过在声明派生类继承父类时加上virtual来指定。
虚拟继承的作用:当系统碰到多重继承的时候保证继承类成员函数的唯一性。
怎么理解他的作用,看下面这个图1:
class base //基类
class derive1 : public base
class derive2 : public base

class derive3 : public derive1,public derive2

这种情况下,derive3就同时拥有了两套base的函数。所以在调用时产生的模糊性会使程序出错。具体我就不谈那么多了。

解决这个问题,就用到了虚拟技术。
修改上面的代码 图2:
class base //基类
class derive1 : virtual public base
class derive2 : virtual public base

class derive3 : public derive1,public derive2

采用虚拟继承的时候,在derive3中,一旦一次继承了base类,再次继承base的时候就会忽略重复的base类。所以保证继承类成员函数的唯一性。

///////////////////////////////////////////////////////////////////
在这个问题中,BJ大师用到的不是这个原理,而是由此产生的一个构造函数调用顺序问题。
在非虚拟继承中,比如图1中。在构造derive3时,其父类调用顺序为derive1-derive2-base;
然后在虚拟继承中。比如图2中。在构造derive3时,其父类调用顺序为base-derive1-derive2;

同样,这样的顺序,作用显而易见,是防止base被多次继承。

再回到我们最初的话题,在DD类创建对象dd时,最先被调用的是Usable_lock类的构造函数,但是,由于Usable_lock构造函数声明被私有,就出现不能访问的错误。由此,是不能创建DD对象的。用这个方法,完成了防止了Usable被继承。


[此贴子已经被作者于2007-6-2 0:27:22编辑过]

#25
xujyule2007-06-11 23:45
复制下来到手机慢慢细读
#26
yuki2007-08-24 09:22

受益匪浅

#27
blueboy820062007-08-24 09:59

感激涕零

#28
荒野的雄狮2008-10-19 19:51
好贴啊,顶一个!
1