ANSI/ISO C++ Professional Programmer's Handbook(2)

来源:互联网 发布:心事谁人知 陈小春 编辑:程序博客网 时间:2024/05/19 04:51
  摘自:http://sttony.blogspot.com/search/label/C%2B%2B

2


标准简报:ANSI/ISO C++的最新附加部分



by Danny Kalev






简介


今天的C++和1983年它第一次被命名为C++时已大不同了。从那以后许多特性加入到语言中;许多旧的特性修改了,少数特性被反对或被完全取消了。许多扩展激进的修改了语言的风格和概念。例如,在RTTI标准化之前指向基类的派生类指针被认为是糟糕和不安全的设计做法。今天,“向下看”是安全的,有时甚至是不可避免的。扩展包括const成员函数,异常 处理,模板,新的cast运算符,命名空间,STL标准模板库,bool类型,等等。在今天这些扩展使C++成为强大的和强壮的多用途程序设计语言。


C++的发展是一个连续和进步的过程,这胜于一场唐突的革命。3,5年前学习C++的程序员和不想更上扩展的程序员经常发现语言滑过自己的手指:现存的代码片段无须从新编译,其他代码编译时会有编译器警告,并且在面向对象仓库中的源代码清单与以前看上去没有什么本质的不同。“命名空间?以前从来没听说过,”和“C风格的cast有什么问题吗?为什么我要用这个呢?”这是在关于C++的不同之处的论坛和会议上最频繁的问题。


理解ANSI/ISO 标准


即使是通过订阅新闻组、看杂志书籍或与公司头交换邮件来紧更变化的熟练的C++程序员仍然会发现:有时在一些专业著作中C++的术语很不清楚。ANSI/ISO标准是用简洁和被叫做standardese 技术性的行话写的,并不是纯粹的英语。例如,One Definition Rule(标准3.2节)——它定义在什么条件之下单独定义的相同的实体是有效的——在课本中以一种简单有时甚至是缺少的方式,同标准比较而言。standardese的使用确定写编译器需要的准确性和程序有效性。为了这个目的,标准定义了在标准中广泛应用的众多术语;例如,它区别template idtemplate name, 而一般的程序员简单地将两者作为模板。熟悉这些具体的术语是正确地阅读和解释标准的关键。


本章的目的和结构


本章的目的有三个。第一个,本章展示一些关键性的在标准和本书中广泛应用的术语,比如,Undefined Behavior(未定义行为)deprecated features。(注意,象一些标题性的术语象argument-dependent lookuptrivial constructor在与他们相关的章节讨论比在这里好。第二,将讨论的是新加入到C++中的特性——比如bool 类型, 新的类型转换运算符和mutable数据成员。 因为这些主题不会在别处讨论,他们将在这里详细的被讨论,连同代码例子。在这之后,将给出一份覆盖本书其他章节中大部分新加特性的列表。


在这里这些主题只是简要的讨论。目的是提供你一个executive summary(主要摘要)——最近加到ANSI/ISO C++标准特性的全景。它可以作为一个主题核对清单使用。当阅读简报主题概述时,你可能遇见不熟悉的主题;在这些例子中,你会经常被提醒关于主题更进一步细节在那章讨论。最后,有一个被反对特性的概述,和建议用于替换他们的列表。


标准的术语


这一部分解释在本书中用到的标准中的关键术语。这些术语在第一次出现时以斜体显示。注意这些定义并不是从标准中引用的标准定义;他们作为解释更合适一些。


实参(Argument) 和 形参(Parameters)


实参形参 是在文献中交替使用的两个词,尽管标准对两者做了严格的区别。在讨论函数和模板时两者的区别是主要的问题。


实参


实参是下列其中一种:函数调用时在括号中的由逗号分隔开的一个表达式;在带参数的宏的括号中的一个或多个预处理记号序列;抛出声明或一个表达式的运算数,类型,或模板实例化时在尖括号中由逗号分隔开的模板名。argument也叫actual parameter


形参


形参是下列中的一个:在函数声明或定义中声明的对象或应用(或在异常处理程序的catch块中);在带参数宏定义中紧更宏名字的括号中由逗号分隔开的列表中的一个标识符;或模板参量。parameter 也叫formal parameter.


下列例子示范了形参与实参的不同:



void func(int n, char * pc); //n and pc 是形参
template <class T> class A {}; //T 是形参
int main()
{
char c;
char *p = &c;
func(5, p); //5 and p 是 实参
A<long> a; //'long' 是实参
A<char> another_a; //'char' 是实参
return 0;
}

Translation Unit


translation unit 包含一个或多个声明的序列。标准使用术语translation unit而不是source file,因为一个translation unit 可以由多个源文件组成:一个源文件和其#included的多个头文件是一个单独的translation unit。


Program(程序)


program由一个或多个一起连接的translation units组成。


Well-Formed Program


well-formed program 是根据标准的语法和语义规则构造的,并且符合One Definition Rule(在以后的章节中解释)。ill-formed program则是不符合这些要求的。


左值和右值


object是存储器相邻的区域。左值是指出这样一个object的表达式。 The original definition of 左值原始的定义是指能出现在一个任务(assignment)左边的对象。然而,const对象是不能出现在任务左边的左值。同样的,一个可以出现在表达式右边的表达式(但不是在左边)是右值。例如



#include <string>
using namespace std;
int& f();
void func()
{
int n;
char buf[3];
n = 5; // n 是左值; 5 是右值
buf[0] = 'a'; // buf[0] 是左值, 'a' 是右值
string s1 = "a", s2 = "b", s3 = "c"; // "a", "b", "c" 是右值
s1 = // 左值
s2 +s3; //s2 和 s3 是被隐含转换为右值的左值
s1 = //左值
string("z"); //临时的右值
int * p = new int; //p 是左值; 'new int'是右值
f() = 0; //一个返回引用的函数调用是左值
s1.size(); //否则,函数调用是右值表达式
}

左值可能出现在需要右值的上下文中;在这种情况下,左值被隐含的转化成右值。右值不能转换成左值。所以,在使用右值的例子中使用左值是可能的,但反过来不行。


行为类型(Behavior Types)


标准列举了在以后章节中将详细讨论的许多程序行为类型。


没有规定编译器实现方法的行为(Implementation-Defined Behavior)


没有规定编译器实现方法的行为(对于well-formed程序和正确数据)依赖于具体编译器的实现;它是每一种实现都必须给出文档的行为。例如,基本类型的大小,char是否可以接受负值,以及堆栈是否在未捕捉的异常中崩溃等等,实现方法都必须给出文档。没有规定编译器实现方法的行为也叫implementation-dependent behavior


Unspecified Behavior(未指明的行为)


Unspecified behavior(对于well-formed 程序和正确的数据)依赖于具体编译器的实现。实现无需给出文档说明行为产生了什么后果(但是允许其这么做)。例如,是否运算符new调用了标准C库函数malloc()是未指明的行为。接下来的例子: 异常对象的临时拷贝的存储器分配以未指定方式分配(但是,它不能分配在自由空间里)。


Implementation-defined behavior 和unspecified behavior 是类似的。两者都指依赖于编译器的具体实现行为。 然而,未指定行为通常指实现底层的机制,这些是一般用户不会直接访问的。决定实现行为指能被用户直接使用的语言结构。


Undefined Behavior(未定义行为)


Undefined Behavior 是标准未强加任何要求的。这种定义听上去象是一个谨慎的说法,因为Undefined Behavior(未定义行为)指来自错误程序和错误数据导致的一般性的状态。Undefined Behavior(未定义行为)可以作为运行时崩溃或不稳定和不可靠的程序状态出现——或则可能根本不会引起注意。写缓冲区溢出,访问一个超界的数组下标,废弃一个“摇摆”(dangling )的指针,以及其他一些类似的操作导致Undefined Behavior。


总结


Unspecified behavior 和 implementation-defined behavior是相容的——虽然不常用——行为。两者都是C++标准有意留在那的,允许有效和简单的不同平台上的编译器实现。相反的,Undefined Behavior总是不受欢迎的和不允许发生的。


唯一定义原则(The One Definition Rule)


类,枚举,有外部连接的inline函数,类模板。非静态函数模板,成员函数模板,模板类的静态数据成员,或模板参数未指定的特殊模板,以上可以在程序中定义多次——假如每个定义出现在不同的translation unit中,且定义符合要求。要求在以后章节中深入讨论。


记号的一致性(Token-by-Token Identity)


每一个定义必须包含相同的记号顺序。例如



//file fisrt.cpp
inline int C::getVal () { return 5; }
//file sec.cpp
typedef int I;
inline I C::getVal () { return 5; } // 违反了ODR,
// I 和 int 不是同样的记号

另一方面,空格和注释无关紧要:



//file fisrt.cpp
inline int C::getVal () { return 5; }
//file sec.cpp
inline int C::getVal () { /*complies with the ODR*/
return 5; }

语义等价(Semantic Equivalence)


在单独定义的完全相同的序列中每一个记号都有相同的语义。例如



//file first.cpp
typedef int I;
inline I C::getVal () { return 5; }
//file second.cpp
typedef unsigned int I;
inline I C::getVal () { return 5; } //error; I有不同的语义

Linkage Types


一个对象、引用、类型、函数、模板、命名空间或在别的范围内定义的值的名字含有linkage。linkage可以是external internal的一种。另外,名字也可没有linkage。


外部 Linkage


来自其他translation units 或来自本translation unit 其他范围的名字有external linkage。下面是一些例子:



void g(int n) {} //g 有external linkage
int glob; //glob 有external linkage
extern const int E_MAX=1024; //E_MAX 有external linkage
namespace N
{
int num; //N::num 有external linkage
void func();//N::func 有external linkage
}
class C {}; //the name C 有external linkage

内部Linkage


被来自本translation unit其他范围的名字指定的名字,但不是别的translation unit的名字,有internal linkage。下面是一些例子:



static void func() {} //func 有internal linkage
union //非本地匿名联合的成员有internal linkage
{
int n;
void *p;
};
const int MAX=1024; //非外部的常量变量有internal linkage
typedef int I; //类型有internal linkage

没有Linkage的名字


只能指自己定义范围能的名字没有linkage。例如



void f()
{
int a; //a 没有linkage
class B {/**/}; //局部类没有linkage
}

Side effect


side effect(副作用)是在执行环境中的改变。改变对象,访问一个volatile对象,调用一个I/O库函数,以及调用一个执行了以上任何一种操作的函数是所有的副作用(side effects)。


附加部分


这部分深入讨论了近几年被C++标准采用的新特性——以及对已有特性的扩展。


新的类型转换运算符


C++ 仍支持C风格的类型转换,象



int i = (int) 7.333;

但是,C风格的类型转换有几个问题。第一,运算符()已在语言中使用的太多了:在函数调用,在表达式的优先级上,在运算符重载,在其他按语法的构造上。第二,C风格的类型转换在不同的上下文中完成不同的运算——如此的不同以至你不知道谁是谁。它可以做无害的标准转化,比如将enum值转换成int;但它也可将不相关的类型转换成其他。另外,C风格的类型转换可以被用来移去对象的constvolatile限制(在早期的C++中,语言也可进行有效的动态类型转换)。


由于多种原因,C风格的类型转换是晦涩的。有时读者很难清楚的理解用C风格类型转换代码的作者的原意。考虑如下例子:



#include <iostream>
using namespace std;
void display(const unsigned char *pstr)
{
cout<<pstr<<endl;
}
void func()
{
const char * p = "a message";
display( (unsigned char*) p); //有符号到无符号的转换是必须的
// 但const也被移除了。这是有意还是程序员的疏忽?

}

新的类型转换运算符使的程序员的意图更清晰,也更好读。另外,他们使编译器能检测到用C风格类型转换时不能检测到的错误。新的类型转换运算符正在取代C风格的类型转换;C++程序员被鼓励用新的运算符取代C风格的类型转换符号。这种运算符在后续章节中详细讨论。


static_cast


static_cast <Type> (Expr) 执行很规范和比较规范的类型转换。它的一个用处是显式的指出将由编译器隐含执行的类型转换。


例如



class Base{};
class Derived : public Base {};
void func( Derived * pd)
{
Base * pb = static_cast<Base *> (pd); //显式的
}

一个指向派生类的指针能被自动转换为基类的指针。使用显式的转换使的程序意图更清晰。


static_cast 能被用于说明用户定义的转换。例如



class Integer
{
public: operator int ();
};
void func(Integer& integer)
{
int num = static_cast<int> (integer); //显式的
}

范围缩小的数据类型转换也可以用static_cast显式的执行。例如



void func()
{
int num = 0;
short short_num = static_cast<short> (num);
}

这种转换比前面的转换危险。一个short可能不能表示int所有的值;显式的转换在这里代替隐含的转换,以指出类型转换执行了。整型到enum的转换也是危险的操作,因为不能保证一个int的值被一个enum完全表示。注意,在这种情况下,显式的转换时间必须的:



void func()
{
enum status {good, bad};
int num = 0;
status s = static_cast<status> (num);
}

你可以在类一级上用static_cast。但是,不象dynamic_cast,它完全依赖于编译期可用的信息——所以不要用它取代dynamic_cast。为这个目的使用static_cast比使用C风格的类型转换安全,因为它不会在没关系的类型之间转换。例如



class A{};
class B{};
A *pa;
B * pb = static_cast<B *> (pa); //error,指针不是相关的

const_cast


const_cast <T> (Expr)仅仅移去Exprconstvolatile限定以及将他们转换成类型TT必须和Expr类型相同,除了相差constvolatile属性。例如



#include <iostream>
using namespace std;
void print(char *p) //parameter should have been declared as const; alas,
{
cout<<p;
}
void f()
{
const char msg[] = "Hello World/n";
char * p = const_cast<char *> (msg); //移去常量特性
print(p);
}

const_cast也能将一个对象改为constvolatile对象:



void read(const volatile int * p);
int *p = new int;
read( const_cast<const volatile int *> (p) ); //显式的

注意,移去一个对象的const限制不能保证其值能改变;仅仅保证能在需要非const对象的地方使用它。为使你理解这些限制,下面的小节中将近一步测试const语法。


(f)const 语法


有两种类型的consttrue constcontractual const。true const 对象是最初被定义为const的左值。例如



const int cn = 5; // true const
const std::string msg("press any key to continue"); // true const

另一方面,有contractual const限制的对象是定义时无const限制的,但被认为有const 限制。例如



void ReadValue(const int& num)
{
cout<<num; // 在ReadValue()中num 不能被改变
}
int main()
{
int n =0;
ReadValue(n); //contractual const, n 被作为const对待
}

当一个真const变量被显式的转换为一个非const变量时,试图改变其值的后果时未定义的。因为实现真const的一种方式是在ROM中存储它,试图写它常常会触发一个硬件异常。(使用显式类型转换移去const不会改变一个变量的物理内存属性。)例如



const int cnum = 0; //真const, 可能存储在机器的ROM中
const int * pci = &cnum;
int *pi = const_cast<int*> (pci); // 强行试图取消变量的const限制
cout<< *pi; //OK,cnum的值没有改变
*pi = 2; //未定义的,试图改变真const变量的值

另一方面,移去对象的contractual const限制使的可以安全的修改其值:



int num = 0;
const int * pci = &num; // *pci是一个contractual const int
int *pi = const_cast<int*> (pci); // 除去contractual const
*pi = 2; // OK, 改变num的值

总的来说,const_cast用于除去对象的constvolatile限制。改变后的对象可以在需要非constvolatile对象的环境中使用。转换后的值不被改变类型转换就是安全的。只有当原来的操作数不是真const时,改变转换后对象的值时可能的。


reinterpret_cast


reinterpret_cast <to> (from) 于底层的,不安全的转换。reinterpret_cast只是返回操作数位模式的重新解释。但是注意,reinterpret_cast 不能改变操作数的cv-资格。使用reinterpret_cast是危险的也是非常不方便的——谨慎的 使用它。下面的例子是关于reinterpret_cast的用法。


reinterpret_cast可以用于转换完全无关的两个指针,就象



#include <cstdio>
void mem_probe()
{
long n = 1000000L; long *pl = &n;
unsigned char * pc = reinterpret_cast <unsigned char *> (pl);
printf("%d %d %d %d", pc[0], pc[1], pc[2], pc[3]); //内存泄漏
}

reinterpret_cast 可以将整数转换成指针,反之亦然。例如



void *pv = reinterpret_cast<void *> (0x00fffd);
int ptr = reinterpret_cast<int> (pv);

reinterpret_cast 也可以用来转换不同类型的函数指针。使用转换后的函数指针进行不匹配参数的函数调用,其结果是未定义的。


不要用reinterpret_cast代替static_cast——其结果是未定义的。例如,使用reinterpret_cast来改变一个有多继承的对象的类层次,可能产生错误的结果。考虑如下代码:



class A
{
private:
int n;
};
class B
{
private:
char c;
};
class C: public A, public B
{};
void func(B * pb)
{
C *pc1 = static_cast<C*> (pb); //正确的偏移量调整
C *pc2 = reinterpret_cast<C*> (pb); //不能计算偏移量
}
int main()
{
B b;
func(&b);
}

在我的机器上,pc1 被赋予0x0064fdf0,然而pc2被赋予0x0064fdf4。这个示范了两个类型转换运算符的不同。使用编译期的可用信息,static_cast 将指向B的指针转换成了指向C的指针。编译器减去子对象B的偏移量导致了pc1指向C的起点。另一方面,reinterpret_cast简单的将pb的二进制值赋给pc2,不作进一步的调整;由于这个原因,导致了错误。


dynamic_cast


在先前的C++标准中早就注意到,C风格的类型转换也可用于执行dynamic cast。类型转换可以是静态的也可是动态的,依赖于操作数的类型。但是,标准化委员会反对这种方法。一种十分象静态类型转换的开销昂贵的运行期运算可能会误导用户。因为这个原因,一种新的运算符引入语言:dynamic_cast (dynamic_cast在第七章“RTTI”中详细讨论)。dynamic_cast的名字和语法和C风格的截然不同。所有新的类型转换运算符符合这个要求。下面是dynamic_cast的例子:



Derived *p = dynamic_cast<derived *> (&base); //指针形态
Derived & rd = dynamic_cast<derived &> (base); //引用形态

总结


新的类型转换运算符更清楚,能更直接的表示他们的含义。一个象dynamic_cast的名字 例如警告它的使用者,这将招致运行期开销。然而更重要的是,新的运算符更安全,因为他们个了编译器检测程序员错误的机会。


用户可能发现运算符的增加让人糊涂。特别的,static_castreinterpret_cast之间的选择似乎还不能立即决定。怎样决定?作为一条规则,优先使用static_cast。如果编译器拒绝接受它,使用reinterpret_cast替换。


内建的bool 类型


在考虑到一些其他提议之后,内建的bool数据类型加到了标准中。没有一种解决方法是令人满意的。接下来是这些提议的总揽,这些提议同时显示了bool类型的性质。


typedef Boolean


一种建议是用typedef来作Boolean 数据类型:



typedef int bool;

然而,一个依赖于其他内建类型的typedef使得Boolean类型不能和一些其他的语言特性一起共存。例如,使用这种Boolean类型在函数重载时产生歧义:



void f(bool);
void f(int);//error,void f(bool)重定义;

另外,typedef不是强类型。因此,它不能保证,Boolean值仅仅在需要Boolean值的上下文中使用。


enum Type


另一个可选择的解决方案是使用enum类型:



enum bool { false, true};

enums 是强类型。但是,委员会想对使用int值作为Boolean值的旧代码提供兼容性。例如



#include <ctype.h>
enum bool {false, true};
void f()
{
enum bool b;
b = islower('a'); //编译期错误,int赋值给enum
}

Class bool


第三个解决方案是使用类,如下:



class bool
{
private:
int val;
public:
operator int();
};

这样的类保证类型是确定的,所以它能被用于函数重载和指定模板。另外,它有与整数Boolean 类型的向后兼容性。但是,类的解决方法有许多缺点。首先,用户不得不#include一个申明头文件以及连接这个类的编译后的代码。更糟糕的是,这个转换运算符可能影响定义在其他类中的用户自定义的转换运算符。最后,一个定义了构造器和转换运算符的“庞大”的类明显不如一个基本类型有效率。因为这些原因,标准化委员会决定增加一种内建类型。


内建的bool


bool 是一个能接受truefalse的靠整数实现的类型。一个合乎标准的Boolean 类型有这些优点:




  • 可移植性——所有支持标准的编译器支持bool类型。当代码移植到不同的平台上时,它将工作正常。




  • 易读性——使用清楚的如truefalsebool的关键字,使得程序易读,并且比使用int更明显。


  • 类型独特性——因为bool是一种独特的类型,下面的函数现在也是独特的:



void f(bool b);
void f(int n);


  • 性能——通过其实现bool类型的内存使用能最优化,使用bool代替int使得可以只用一个字节表示bool类型。另外,使用内建类型而不是类,保证最好的性能。


bool数据类型的介绍一起,内建运算符也要作相应的修改以适应bool值。逻辑运算符&&||,和!现在接受bool值作为形参并且返回bool 结果。同样的,关系运算符<><=>===,和!=返回bool结果。另外,iostream类作了调整以支持新的类型。


显示bool变量的字面上的方式


默认的,iostream对象用01显示bool变量。通过向流对象插入格式标志boolalpha来忽略默认设置也是可以的。改变之后,将显示falsetrue来取代01。例如



#include <iostream>
using namespace std;
int main()
{
bool b = true;
cout<<b; // 默认设置;显示1
cout<<boolalpha; //以后,显示'true' 和 'false' 代替1 和 0
cout<<b; // 输出:true
cout<<!b; // 输出: false
return 0;
}

异常处理


异常处理用于报告和处理运行期错误。补充的特性,即exception specifications(异常申明)function try blocks(函数try块),近几年被加入到标准中。下面的章节提供了这些特性的总揽。(异常处理和补充的特性将在第六章“异常处理”中讨论。)


异常申明


函数可以通过指定一份异常的列表来指出它可能抛出的潜在的异常。当函数的用户只能看其原型而不能访问其源代码时,异常申明是十分有用的。下面是一个异常申明的例子:



class Zerodivide{/*..*/};
int divide (int, int) throw(Zerodivide); //函数可以抛出类型是
//Zerodivide的异常,而不是其他

函数try块


function try block是一个函数,这个函数是由一个try块和与之关联的处理块组成的。 函数try块使你能够捕捉到由基类构造器或由成员对象的构造器抛出的异常。过去的异常处理规范让用户不能处理由构造器或本地成员初始化列表抛出的异常;函数try块修正了这个漏洞。下面是一个函数try块的例子:



class Err{};
A::A(const string& s) throw (Err); //仅仅允许抛出
//类型为Err的异常
try
: str(s) //str的构造器可能抛出一个分配内存失败
//异常,这个异常违反了C的异常申明
{
// 构造器的函数体
}
catch (...) //当构造str 或 C时如果有异常抛出
//流程到了这里
{
throw Err(); //用Err 异常替代了内存分配失败异常
}
译注:应该是A,原文是C

内存管理


标准现在定义了运算符new的三种不同的版本:plain newnothrow new,和placement new。每一运算符也有它的数组版本。标准也定义了六种不同的运算符delete,以对应new的相应的版本。内存管理和最近添加的newdelete的不同版本将在第十一章“内存管理”中作进一步讨论。


在失败的情况下运算符new抛出一个异常


在C++早期发展的过程中,当不能分配所要求的内存时运算符new返回NULL指针。C++标准化委员会改变运算符new的规范,以便使在它失败时抛出类型std::bad_alloc的异常。这样优于返回一个NULL指针。一个直接或间接使用new的程序必须处理潜在的std::bad_alloc异常。例如



void f(int size) //符合标准的运算符new的用法
{
char *p = new char [size];
//...安全的使用p
delete [] p;
return;
}
#include <stdexcept>
#include <iostream>
using namespace std;
const int BUF_SIZE = 1048576L;
int main()
{
try
{
f(BUF_SIZE);
}
catch(bad_alloc& ex) //处理从f()抛出的异常
{
cout<<ex.what()<<endl;
//...其他的诊断和补救
}
return -1;
}

nothrow new(无抛出的new)


标准也定义了运算符new的不抛出异常的版本。它在分配失败时返回NULL指针而不是抛出异常。这种版本的new带一个额外的参数nothrow。例如



#include <new>
#include <string>
using namespace std;
void f(int size) //nothrow new的范例
{
char *p = new (nothrow) char [size]; //数组nothrow new
if (p == 0)
{
//...使用p
delete [] p;
}
string *pstr = new (nothrow) string; //简单nothrow new
if (pstr == 0)
{
//...使用pstr
delete [] pstr;
}
return;
}

Placement new


运算符new的另外一个版本允许用户在预定的内存位置构造对象。这个版本叫placement new。下面时使用placement new的例子:



#include <new>
#include <iostream>
using namespace std;
void placement()
{
int *pi = new int; //简单new
int *p = new (pi) int (5); //placement new
//...use p
delete pi;
}

构造器和销毁器


基本类型可以通过专用构造器初始化。另外,标准也为这些类型定义了“虚假”的销毁器(参见第四章“特殊成员函数:默认构造器,拷贝构造器,销毁器和分配运算符特殊成员函数:默认构造器,拷贝构造器,销毁器和赋值运算符”)。


构造器和基本类型


可以通过显式调用其构造器来初始化基本类型变量。例如



void f()
{
int n = int(); //初始化0
char c = char(); // 同样初始化0
double d = double(0.5); //初始化为其他值也是可以的值
}

语言的这个扩展使得在模板中可以统一的处理基本类型和用户自定义类型。


显式的构造器


默认的,只带一个参数的构造器是一个隐含的转换运算符(将这个参数转换为这个类的对象)。为了避免这种隐含的转换,带一个参数的构造器必须显式的申明。例如



class C
{
public:
explicit C(int size); // 不进行隐含的转换
};

Pseudo Destructors(虚假的销毁器)


pseudo destructor是一个按语法构造的产物。它的唯一目标是适应泛型算法和容器的需要。这是非op代码,并且对它的对象没有实际的影响。例如



typedef int N;
void f()
{
N i = 0;
i.N::~N(); // 虚假的销毁器调用
i = 1; // i不受调用虚假销毁器的影响
}

局域定义和作用域规则


定义在for语句中的变量的作用域规则作了改变。另外,在 if的条件中也可以定义并初始化变量。


局域循环计数器的范围


C++允许在需要变量的地方定义它,无论在程序的什么地方。这使得直接初始化成为可能。一个好的 例子是循环计数器,它能在for语句内部申明。例如



void f()
{
for (int i = 0; i < 10; i++) // i 申明并初始化
// 在for语句内部
{
cout << i <<endl; //输出0到9
}
int n = i; //编译错误,i不在范围内
}

在C++早期的发展中,这样申明的局域变量在其后的块中依然有效。这是bug和名字隐藏的源泉。因此,标准作了修订以修补这个漏洞,也就是说:以这种方法创建的局域变量在for语句之外不能使用。在前面的例子中,当循环结束时变量i也就超出了范围。


在if的条件中申明变量


你可以在if语句的条件中定义和初始化变量。例如



class Base {/*..*/};
class Derived: public Base {/*..*/};
void func (Base& b)
{
if ( Derived *pd = dynamic_cast < Derived* > (&b) ) //申明
// 在if的条件中
{
//dynamic_cast成功;在这里可以使用pd
return;
}//在这里pd超出了其使用范围
//dynamic_cast将失败;变量pd超出了范围
}

申明局域指针pd的优点是显而易见的:它总是初始化成正确的值,而且对于其他不使用它的程序块来说它是不可见的。(参见第十二章“优化你的代码”)。


命名空间


Namespaces(命名空间)是加入语言最新的特性。命名空间用于避免命名冲突和方便管理大型工程以及控制大型工程的版本。标准库中的大部分组件在命名空间std中。有三种方法将命名空间成员加入到一个范围 :using directive,using declaration,或fully qualified name。 Argument-dependent查询,或Koenig查询,同样通过自动名字查找过程使用命名空间。命名空间将在地、第八章“命名空间”详细讨论。


模板


模板一个模子,相关联的函数和类从这个模子实例化。自从1991年模板第一次引入以来,它走过了很长的路。从其引入以来,他们只是“智能”的宏。但是,STL的采用要求极大的扩展这种特性。在之后的章节中给出这些扩展的总揽。模板在第九章“模板”中详细讨论。


模板作为模板的参数


模板现在可以接受一个模板参数。例如



int send(const std::vector<char*>& );
int main()
{
std::vector <std::vector<char*> > msg_que(10);
//...填充msg_que
for (int i =0; i < 10; i++) //传递消息
send(msg_que[i]);
return 0;
}

默认类型形参


模板可以有默认类型形参。例如



template <class T, class S = size_t > class C //使用默认参数
{/**/};

成员模板


模板可以嵌套;模板可以在其他类或类模板中申明。这种模板叫member template。下面是一个例子:



template<class T> class C
{
public:
template<class T2> int func(const T2&); //成员模板申明
//...
};
template<class T> template<class T2> int C<T>::func(const T2& s) // 定义
{
//...
}

typename关键字


为了支持成员模板和模板继承,关键字typename加到语言中。默认的,编译器假定一个合法的名字指non-type。typename关键字指示编译器代替默认的解释,并用typename 代替以解决不确定性。


引出模板


只将模板定义编译一遍是可能的,在不同的translation units中使用模板而只定义一次也是可能的。为了单独的编译模板再使用它的申明,模板必须被引出。这通过在模板申明前加export关键字来实现。


STL标准模板库


根据Bjarne Stroustrup的提议,自1991年以来C++最重要的改变不是改变语言,而是扩充了标准库。标准模板库,或STL,构成了标准库的一个重要组成部分。STL是泛型容器的集合——比如vector,list,和stack——并且是排序、查找、合并以及变化这些容器等通用算法的丰富集合。第十章“STL和泛型程序设计”详细讨论STL。


国际化和本地化


现在的C++标准是由ISO发布的一个国际化标准。为了实现这个目标,C++标准做了完全的国际化。国际化由许多改变和扩展组成。包括增加关键字wchar_t(在ISO C中wchar_t以由 typedef定义但不是一个保留关键字)。另外,标准流和字符串类模板化了,以同时支持窄和宽字符集。最后,<locale>库定义了封装、操作地域相关信息的类模板和函数,如monetary,numeric和time conventions。地域特性封装在了用户可扩展的类中(或facets)。


宽字符流


C++提供了四个在程序开始之前就实例化的标准I/O流。他们在头文件<iostream>中定义:



cin //标准char输入流
cout //标准char输出流
cerr //标准无缓冲错误消息输出流
clog //标准错误消息输出流

这四个流现在都有相应的宽字符版本:



wcin
wcout
wcerr
wclog

杂项


最近两个扩展加到标准中:在类中初始化const static数据成员和mutable存储指定。下面的章节讨论这两个扩展。


初始化const static数据成员


整数const static数据成员现在可以在类中初始化。在这种情况下,初始化也是定义,因此不需要在类的外面在定义。例如



#include <string>
class Buff
{
private:
static const int MAX = 512; //初始化+定义
static const char flag = 'a'; //初始化+定义
static const std::string msg; //非整数类型;必须在类
//外面定义
//..
};
const std::string Buff::msg = "hello";

可变的对象成员


const成员函数不能改变它对象的状态。但是,有时辅助数据成员(标志,引用记数)需要被const成员函数改变。这样的数据成员可以被申明为mutable。一个mutable成员决不是const,即使它的对象是const;因此,它能被const成员函数改变。 下面是使用这种特性的范例:



class Buffer
{
private:
void * data; //用于在网络上传送的数据缓冲
size_t size;
mutable int crc;//由于校验传送过程中是否有错误产生
public:
int GetCrc() const;
void Transmit() const; //在这里计算crc
};
void f()
{
Buffer buffer;
//...填充缓冲区
buffer.Transmit(); //crc在这里能被改变;非mutable成员不行
}

并不是每次附加数据到缓冲就计算一次crc。而是在缓冲发送之前通过成员函数Transmit()计算crc。但是,Transmit()不需要改变对象的状态,所以它被申明为 const成员函数。为了允许正确的访问,数据成员crc被申明为mutable;因此,它可以被const成员函数改变。


被反对的特性


被反对的特性是当前版本的标准将其作为标准,但不保证将来的版本仍然将其作为标准的特性。此外,这个定义有一点保守。发展、标准化一种程序设计语言的一个结果就是移去那些不受欢迎的、危险的或多余的特性和构造。通过被反对的特性,标准化委员会表达了要将这些特性从语言中移去的愿望。完全移去这些特性是不切实际的,因为现存的代码不会在也不编译。“反对”给了用户足够的时间用标准认可的特性去代替被反对的特性。标准列举了这些特性,下面的章节将讨论这些特性。


bool类型使用后缀++运算符


为了兼容以前使用inttypedef作为bool类型的代码,允许在bool类型变量使用后缀++运算符。就象



bool done = false;
while(!done)
{
if(condition)
//...
else done++; //被反对的
}

++一个bool总是产生true,无论以前是什么值。但,这个操作是被反对的;因此将来不被支持。记住,--bool变量是非法的。


在命名空间范围中用static申明对象


用关键字static来申明一个translation unit中的局域函数或对象是被反对的。作为替代,用unnamed namespace来达到这个目的(详见第八章)。


访问权申明


基类成员的访问权可被派生类通过访问权申明改变。例如



class A
{
public:
int k;
int l;
};
class D : public A
{
private:
A::l; //访问权申明将A::l的访问权改成了private;被反对的
};

使用访问权申明是被反对的。用using申明代替:



class D : public A //using申明版本
{
private:
using A::l; //using申明将A::l的访问权改成private
};
void func()
{
D d;
d.l = 0; //error;不能访问private成员
}

文本串从const到non-const的隐含类型转换


文本串可以隐含的由指向const char的指针转换到指向char的指针。同样的,宽文本串也能隐含的由指向const wchar_t的指针转换到指向wchar_t的指针。例如



char *s = "abc"; //文本串隐含的转换成non-const;被反对的

文本串"abc"的类型是const char[],而s是指向非const char类型的指针。象这样,文本串的从const到非const的类型转换是被反对的,因为这可能导致下面错误的代码:



strcpy(s, "cde"); //未定义行为

推荐的方式是



const char *s = "abc"; //OK

<name.h>这种形式的标准C的头文件


为了兼容标准C库,C++仍然支持<xxx.h>形式的C头文件命名习惯——但是这种命名习惯是被反对的(在第八章中详细讨论)。例如



#include <stdlib.h> //被反对的

用新的命名习惯代替<cxxx>



#include <cstdlib> //OK,'c'前缀并且不需要".h"后缀

这样修改的原因是".h"头文件将他们的名字注入了全局命名空间,而新的头文件<cname>将他们的名字保持在命名空间std中。


隐含的int申明


象下面这样省略申明的默认类型是int



static k =0; //'int'类型;被反对的
const c =0; //'int'类型;被反对的

这种习惯现在是被反对的,应该显式的申明:



static int k =5;
const int c = 0;

其他被反对的特性


标准反对旧iostream类的一些成员(article 4.6)。另外,标准反对在头文件<strstream>将字符数组和流缓冲关联的三个类型(article 4.7)。


从用户的观点来看,不要在新的代码中使用被反对的特性,因为在语言将来的版本中这些特性将作为错误标出。在过度时期,编译器可能对这些特性作警告。


总结


如你所见,标准C++和4、5或10年前用的C++大不相同了。许多改变和修改是由软件厂商发起的(例如bool数据类型)。其他是由标准化委员会发起的(例如,新的类型转换运算符和STL)。在面红耳赤的标准化过程中,一方面抱怨“随便的接受改变”,另一方面又抱怨“缺少特性”,这样的事经常听到。但是,标准化委员会非常细致和谨慎的在这些扩展和修改中选择。幸运的是,1998年发布的标准至少保持稳定性5年。 这一段稳定期使得编译器厂商和语言的用户都能消化现在的标准并且有效的使用它。这么做的时候,你经常会发现今天的C++比以前要好。


原创粉丝点击