20世纪80年代初期, C++起源于AT&T,称为带类的C,当时Biarne Stoustrupi试图用Simula-67编写仿真程序。“类”在Sinula中是表示用户定义类型的术话,编写好的仿真程序的关键是能够定义对象反映现实世界。除了把类加到C中使其成为最快的过程语言外,还有什么更好的方法可以得到快速仿真呢?选择C语言不仅为类提供了有效的工具,并且也提供可移植性。虽然在C++出现之前已经有其他语言可以通过类文持数据抽象,但是, C++现在是应用最广泛的,几乎每个有C语言编译器的主要平台都支持C++。
第一次看C++就可能被它不可抵抗的魅力所吸引。如果有C语言基础,需要将下列术话(然后少许)增加到自己的词汇表中:
抽象类、存取限定符、适配器、(空间)分配器、基类、类、类的作用域、构造函数、复制构造函数、默认参数、默认构造函数、delete运算符、派生类、析构函数、异常、异常处理器、异常特化、显式构造区数、显式特化、导出、facet.友元、函数对象、继承、内联函数、迭代器、操纵器、成员函数、成员模板、多继承、不定性、名字空间、嵌套类、new处理器、new运算符、新风格类型转换、一次定义规则、运算符函数、重载、局部特化、指向成员的指针、多态、私有、保护、公有、纯虚函数、引用、运行期类型识别、静态成员、流、模板、模板特化、this指针、显著特性、y块、类型标识、类型安全连接、using指令、虚基类、虚析构函数、虚函数。
C++的优点在于它是一种能够处理复杂应用的强大的、高效的、面向对象的语言。因此它的缺点是它本身一定有些复杂,并且比C语言掌握起来更加困难。当然C语言自己本身也是问题的一部分。C++是一个混合的语言,它将面向对象特征与流行的系统编程语言混合在一起。如果不是一个主语言绑定很少内容的话,介绍如此丰富的一组新特征是不可能的。因此与C语言的兼容性是C++设计的一个主要目标,就像1989年Biarne在ANSI C++委员会的主题演讲中所陈述的那样, C++是“工程上的妥协” ,并且必须要使它"越接近c越好,但不能过度”。
C++事实上是一种多范例语言,像C和Pascal那样,它支持传统的过程编程方式;像Ada一样,它支持数据抽象和通用性(模板);像其他所有面向对象语言一样,它文持继承性和多态性。所有这些可能都或多或少导致了C++成为"不纯”的编程语言,但是这也使C++成为产品化编程中更具实践性的选择。无疑C++拥有最好的性能,它可以在混合语言环境中很好地运行(不仅和C语言,而且也和其他语言),并且不需要像Smallalk和LISp运行时所需的庞大运行期资源(后者是环境的,不只是编译和连接过程) 。
下面将介绍其更多的优点。
1.2循序渐进在没有完全掌握C++的情况下也可以有效地使用它。事实上,面向对象技术承诺如果供应商为重用、可扩展性提供设计好的类库,那么建立应用程序的工作就很容易了。现有的开发环境,及其应用程序框架和可视化组件,正在兑现这一承诺。
如果觉得必须要掌握这种语言,可以一步步地去做,并且在这一过程中可以取得丰硕的成果。已出现的3个“顶峰”是:
1.更好的C;
2.数据抽象;
3.面向对象编程。
由于C++比C更安全、更富于表达,所以可以将它作为一个更好的C使用。这个顶峰上的特征包括类型安全连接、强制的函数原型、内缺函数、const限定修饰符(C从C++中借用了它) ,函数重载、默认参数、引用、动态内存管理的直接语言支持。也需要意识到C++和它前身之间存在着不兼容性。在这章中将探究一些使C++成为更好的C的非面向对象的特征。因为如果不说明基于类的起源就想阐明某些更好的C特征是很困难的,所以我也将解释C++的类机制。
1.3类型系统理解C++最重要的部分,也许就是它对于类型安全(type safety)的贡献。上面所提及的其他面向对象语言实质上是无类型的,或最多也只能说是弱类型的,因为它们主要是在程序运行期间执行错误检查,换句话说, C++要求声明每个程序实体的类型,并且在编译期内它要一丝不苟地检查相同用法。正是类型安全而不是其他别的特点,使C++成为更好的C,成为常用编程工作的景合理的选择。类型系统的特征包括函数原型、类型安全连接、新风格的类型转换、运行期类型识别(RTTI) (有关类型转换和RTTI的内容请参见第10章) 。
1.4函数原型在C++中,函数原型不是可选的。事实上,在ANSI C委员会采用原型机制以前,它是为C++发明的。在你第一次使用函数前必须声明或定义每个函数,编译器将检查每个函数调用时正确的参数数目和参数类型。此外,在其应用时将执行自动转换。下列程序揭示一个在C中不使用原型时出现的普通错误。
/* convert1.c * /
#include <stdio. h>
main{
{
dpr int (123);
dor int (123. 0):
return 0;
}
dpr int (d)
double d; //老式的函数定义
{
printf ("%fn", d);
}
/*输出:
0.000000
123.000000
*/
函数aprint要求带有一个double型参数,如果不知道dprint的原型,编译器就不知道调用dprint (123)是个错误。当为dprint提供原型时,编译器自动将123变换成double型:
/* convert2. c */
#include <stdio. h>
void dpr int (double); /*原型*/
main ()
{
dpr int (123);
dpr int (123. 0);
return o;
}
void dorint (double d)
{
printf ("%fn", d);
}
/*输出
123.000000
123.000000
*/
除类型安全外,在C++中关键的新特征是类(class) ,它将结构(struct)机制扩展到除了数据成员之外,还允许函数成员。与结构标记同名的一个成员函数称为构造函数,并且当声明一个对象时,它负责初始化该对象。由于C++允许定义具有与系统预定义类型一样性能的数据类型,因此,对于用户自定义类型也允许隐式转换。下面的程序定义了一个新类型A,它包含了一个double型的数据成员和一个带有一个double型参数的构造函数。
//convert3. cpp
#include <stdio. h>
struct A
{
};
double x;
A (double d)
{
printf ("A:: A (double) n");
x=d;
}
void f(const A& a)
{
}
printf("f: %fn", a.x);
main()
{
A a(1);
f (a);
f(2);
}
//输出:
A::A (double)
f: 1
A:: A (doub le)
f:2
由于struct A的构造函数期望一个double型参数,编译器自动地将整数1转换为所定义的double型用于a。在main函数的第一行调用f (2)函数产生下面的功能:
1.将2转换为double型;
2.用值2.0初始化一个临时的A对象;
3.将对象传递给f。
换句话说,编译器生成的代码等同于:
f (A (double (2))):
注意到C++的函数风格的强制类型转换。表达式
double (2)
等同于
(double) 2
然而,在任一转换序列里只允许有一个隐式用户定义的转换。程序清单1.1程序中要求用一个B对象去初始化一个A对象。B对象转而要求一个double型,因为它唯一的构造函数是B: : B (double) 。表达式
A a (1)
变为
a (B (double (1) ) )
它只有一个用户定义的转换。然而,表达式f (3)是非法的,这是因为它要求编译器提供两个自动的用户定义转换:
//不能隐式地既做A的转换又做B的转换
f(A (B (double (3)) //非法
表达式f (B (3) )是允许的,因为它显式地请求转换B (double (3) ) ,因此编译器仅提供剩余的转换到A。
通过单一参数的构造函数的隐式转换对于泥合模式表达式是很方便的。例如,标准的字符串类允许将字符串和字符数组混台,如:
string s1=" Read my lips..." //初始化s1
string s2-s1+" no new taxes."; //将81和常字符连接
程序清单1.1仅允许一个用户定义的转换
// convert4. cpp
#inc lude <stdio. h>
struct B;
struct A
{
double x;
A (const B& b);
};
void f (const A& a)
{
}
printf ("f: sfn", a.x);
struct B
{
double y;
B(double d) : y(d)
{
}
printf ("B:: B (double)n");
};
A::A (const B& b) : x(b. y)
}
printf ("A: :A(const B&)n");
main()
{
A a(1);
f(a);
вb(2);
f(b);
//f(3); //将不编译
f(B(3)); //隐式B到A的变换
f(A(4);
}
//输出:
B:: B (double)
A::A (const B&)
f:1
B:: B (doub le)
A::A(const B&)
f:2
B::B (double)
A::A(const B&)
f:3
B::B (double)
A::A(const B&)
f: 4
第二行等价于:
string s2=s1 + string ("no new taxes, ");
这是因为标准的字符串类提供了一个带有单一const char*型参数的构造函数,但有时你可能不希望编译器如此轻松,例如,假设有一个字符串构造函数带有一个单一的数字参数(其实没有) ,也就是说将字符申初始化为一个具体的空格数,那么下面表达式的结果将会是什么呢?
string s2=s1+5;
上式右边变为s1-string(5),意思是给s1增加5个空格,这多少是一个让人困惑的“特征”。你可以通过声明单参数构造函数explicit来防止这种隐式转换。由于我们假设了字符串的构造函数是这样声明的,上面的话句就是错误的形式。但是string s (5)这个声明是合法的,因为它显式地调用了构造函数,与此类似,如果用explicit A (double d)
替换程序清单1.3中A的构造函数的声明,编译器将把表达式f(2)按错误处理。
1.5类型安全连接C++甚至可以通过编译单元检测出不正确的函数调用,程序清单1.2的程序调用了程序清单1.3中的一个函数。当把它作为C程序编译时,会得到一个错误的输出结果:
f: 0.0000000
程序清单1.2解释程序连接(也见程序清单1.3)
void f(int);
main()
{
}
f(1);
程序清单1.3要与程序清单1.2连接的函数
#include <stdio. h>
void f (double x)
{
}
printf ("f: sfn",x);
C无法区分出函数的不同。常规作法是把正确的函数原型放到所有编译单元都包含的头文件里。然而,在C++里,一个函数的调用仅连接与之有相同标记的函数定义,即函数名称和它的参数类型顺序的组合。当作为一个C++程序进行编译时,在一个流行的编译器中程序清单1.2和程序清单1.3的输出结果是:
Error :undefined symbol f(int) in module safel. cpp
大多数编译器通过把函数标记和函数一起编码来获得这种类型安全连接。这种技巧经常称为函数名编码、名字修饰、或者(我景喜欢的)名字改编。例如,函数fin)可能以下面的形式出现在连接器中:
f_F //f是一个带整型参数的函数
但是函数(double )则是:
f_Fd //f是一个带双精度型参数的函数
由于名字的不同,在这个例子中连接器不能找到fin)并报错。
1.6引用由于C函数的参数是按值传递的,若传递大型结构给函数,既费时又占用空间。大多数C程序员使用指针来代替按值传递,例如,如果struct Foo是一个大型记录结构,可以采用如下方法:
void f (struct Foo * fp)
{
/通过fp来访问Fo0结构*/
fр->x...
等等.
当然,为了使用这个函数,必须传递Foo结构的地址:
struct Foo a;
f (&a);
C++的引用机制是符号上的便捷,这样做可以减少采用指针变量的显式间接访问的烦恼。在C++上面的代码可以被描述为:
void f (Foo &fr)
{
/*直接访问Fo0的成员
*/fr. x=...
等等.
}
现在可以像这样调用坏使用地址操作符:
Foo а;
...
f(a);
f原型里的&符号指导编译器通过引用来传递参数,这实际上为你处理了所有的间接访问。(对于Pascal程序员而言,引用参数等价于Var参数。 )
引用调用意味着对函数参数所做的任何修改也会影响到主调程序中的原始参数。这就是说你可以编写一个实际运行的交换函数(而不是一个宏) (参见程序清单1.4) 。如果不打算修改一个引用参数,就可以像我在程序清单1.1中所做的那样将它声明为常引用。常引用参数具有安全性、按值调用的符号方便性以及引用调用的有效性。
如程序清单1.5所示,也可以通过引用从函数中返回一个对象,在赋值语句的左边是一个函数调用,这看起来有些奇怪,但是这在运算符重载时是方便的(尤其是=和[]) 。
本文摘自《C和C++代码精粹》
本书基于作者备受好评的C/C++ User Journal杂志上的每月专栏,通过大量完全符合ISO标准C++的程序集合,讲解了指针、预处理器、C标准库、抽象、模板、异常、算法、容器、文件处理、动态内存管理等不同层次的知识。