2008年(884)
分类: C/C++
2008-08-06 09:57:16
你可以把C 当成一门更好的C语言来使用,因为它更安全更富于表现力。与这一点相关的特征有:类型安全连接,强制函数原型,内联函数,const限定词(是的,ANSI
C从C 中借鉴的这个词),函数重载,缺省参数,引用和语言提供的对动态内存管理的支持。你同样需要当心这两种语言不兼容的地方。C语言中有一个强大的子集,Plum
和 Saks 称其做"类型安全的 C"(参见 C Programming Guidelines, Plum and Saks,
Plum-Hall, 1992)。
正如我在这篇文章和下一篇文章中所陈述的一样,C 支持数据抽象--用户可以自己定义行为与内建类型相像的数据类型,这种数据抽象机制包括:类,存取限制,构造和析构函数,运算符重载,模板和异常处理。
面向对象的程序设计通过探求类与类之间的关系在数据抽象上更进一步。其中两个关键的概念是继承(通过声明一个新类与另一个类的相似与区别定义它,其中的相似被重用)和多态(为一族相关的操作提供同一个接口,运行时识别)。C 分别通过类的派生和虚汗数来支持继承和多态。
一个类就是一个扩展的struct。除了定义数据成员,你还可以为其添加成员函数。日期类的定义在文件data.h中的
Listing 1。它与上个月的C版本不同,因为在这里interval函数是一个成员函数而不是全局函数。Date::interval()的实现在
Listing 2 中。"::"叫做作用域运算符。它告诉编译器interval函数是Date类的成员函数。interval函数原型中的"&"说明这个函数的参数由应用传递(参见关于引用的选项)。Listing 3 中的程序展示了如何使用这个日期类。你必须使用结构成员的语法来调用 Date:: interval():
result = d1.interval (d2);
Date作为类型标识符,就像系统内建类型一样的发挥作用(例如,你可以定义Date的对象而不使用struct关键字)。永远也不必做如下的定义:
typedef struct Date Date;
事实上,类的概念是如此的基本,以至于C 已经将结构标签和普通的标识符结合成一个独立的名字空间。
注意我已经将isleap定义成了一个内联函数(在C版本中它是一个宏)。内联函数像宏一样将代码展开,但它也像普通函数一样进行作用阈和类型的检查。除非你要使用the stringizing or token-pasting operations of the preprocessor,,否则在C 中不需要使用 function-like 的宏。现在考虑
Listing 2 中的这个声明:
years = d2.year - year;
year指的是什么对象?在C版本中,这个声明如下:
years = d2.year - d1.year;
既然成员函数的调用总是与对象相关联(例如,d1. interval (d2)),因此当成员函数没有前缀修饰的时候,通常是相关联对象的成员(在这里,year 指的是d1.year)。this关键字代表一个指向潜在对象的指针,因此我可以做一个更加明确的声明:
years = d2.year - this->year;
但是这种用法很少。
在 Listing 4 中,我在类的定义中添加了如下的声明:
Date();
Date(int,int,int);
这是一种特殊的成员函数叫做构造函数。构造函数允许你在一个对象被创建的时候指定怎么样初始化这个对象。当你定义一个没有初始值的日期对象时,首先调用缺省构造函数(因为它没有任何参数):
Date d;
下面的声明调用第二个构造函数:
Date d(10,1,51);
当成员函数的实现比较简单的时候,你可以把它们的实现移到类的定义里面去,使它们成为内联函数(参见
Listing 7 ——不要忘记在
Listing 5 中移走它们)。Listing 6 中的测试程序推迟构造对象d1、 d2 和 result 直到需要它们的时候(在C 中,对象的定义可以出现在任何声明中)。
我几乎已经列举了数据抽象,也就是封装的主要特征。当一个用户自定义类型的内部表现和外部接口设计良好,就叫做一个封装。我确实定义了一个和系统内建类型一样作用的新类型,我不允许任何无意间的对它的内部表现的访问制。例如,像这样,用户可以执行如下的语句:
d1.month = 20;
一个行为良好的对象控制着对它的内部数据成员的访问。在一个实际的日期类中,我允许用户对年月日进行排队,但不允许直接设置它们的值。因此我定义它们为private,并且提供了存取函数来得到它们的值(参见
Listing 8)。因为具有私有成员是更普遍的情况,我通常用 class 关键字取代struct,
默认情况下其成员为 private (参见 Listing 9)。类似 get_month 这样的
存取函数不改变一个日期类的私有部分,因此我声明它们为 const 成员函数。(Date::interval()也是一个 const ——别忘了在实现文件 date3.cpp 中它的定义前加 const。)
现在我必须用 tdate3.cpp (参见 Listing 10)中的存取函数调用替代数据成员引用。
我们现在在完成一个 C 风格的日期类上只走了一半的路。下个月我们会把输入输出流、静态成员和运算符重载结合进来讨论。
C 中的引用是另一个对象的别名。它所引用的对象出现的地方,它本身就可以出现。下面的程序使用引用iref代替i:
/* ref1.c: Illustrate references */
#include
你可以把引用看作一个"灵巧"指针,因为它指向另一个对象却又不像指针一样需要明确的寻址和取值:
/* ptr.c: Do the same thing with pointers */
#include
指针和引用的主要区别在于:
然而,就像指针一样,引用也可以作为函数的返回值。既然引用被定义成一个左值,这就允许一个很特殊的习惯,那就是在完成某任务时,可以将对函数的调用放在=的左手边:
int &iref;
一旦初始化了一个引用,你不能使这个引用指向另外的对象。既然引用总是需要指向某些东西,你不能像对指针一样给它赋值为NULL。
/* ref2.cpp: Returning a reference */
#include
另一种引用的用法是实现引用传递语义,这意味着在被调用函数返回后改变调用进程中存在的函数参数值。你也可以用指针实现,但是引用更明确:
/* ref3.cpp:
Swap via references */
#include
即使你不打算修改函数的参数,为了提高效率用引用来传递大的对象也是一个好办法。例如,假如数据类型X很大,
struct X
{
// lotsa stuff
};
那么具有X类型参数、却不会修改该参数的函数f应该有类似下面的原型:
void f(const X&);
想要了解引用的更多内容,参见 Dan Saks'''' 在 1991 年第九期的专栏:
"Reference Types", CUJ Vol.9,No.9。