C语言中的构造数据类型如结构、联合、枚举等在C++中仍然有效。由于C++新增了一种类型名class,许多人错误地认为struct只能用来包装数据,或者class必须定义成员函数。
C++
对C
的结构、联合、枚举等进行了必要的改造和增强,本章比较分析了异同点,总结了使用要点,对于那些正在从C
语言向C++
语言过渡的程序员有较好的参考价值。
如果只能使用基本数据类型来编程,那将是一件痛苦的事情。C语言支持把基本数据类型组合起来形成更大的构造数据类型,这就是C语言的struct,有时也称为用户自定义数据类型(User Defined Type, UDT)。构造数据类型还可以嵌套(对象嵌入)和引用(对象关联),实际上,构造数据类型是一个递归的定义:
(1)由若干基本数据类型组合而成的类型是构造数据类型。
(2)由若干基本数据类型和构造数据类型组合而成的数据类型是构造数据类型。
(3)由若干构造数据类型组合而成的数据类型是构造数据类型。
语言本身的这种能力使我们能够定义非常复杂的数据结构,如树(tree)、链表(list)和映射(map)等。
关键字struct与class的困惑
C++语言对C语言的struct进行了改造,使其也可以像class那样支持成员函数的声明和定义,从而使struct变成真正的抽象数据类型(Abstract Data Type, ADT),这使得许多人对struct和class倍感困惑。
当语言支持某种特征时,是否使用这种特征则完全取决于程序员。因此,并不是说class支持成员函数的定义,我们就一定要在每一个class中都定义成员函数;也并不是说struct过去不支持成员函数定义,我们就非得用class完全取代struct。实际上就C++语言本身来讲,struct和class除了“默认的成员访问权限”这一点不同外,没有任何区别。
【提示
8-1
】:
|
在C++语言中,如果不特别指明,struct成员的默认访问限定符为public,而class成员的默认访问限定符为private。
|
因此,在C++程序中,只要你明确地声明每一个成员的访问权限,那么完全可以用struct取代class,也完全可以用class取代struct,见示例8-1。
示例8-1
struct SA
{
public :
const char * GetName( ) const;
private :
char *m_name ;
int m_height ;
int m_weight ;
};
|
class CA
{
public :
const char * GetName( ) const;
private :
char *m_name ;
int m_height ;
int m_weight ;
};
|
本例中SA和CA这两个类型在C++中没有任何不同。就像Lippman所说的那样,“在C++中,选择使用关键字struct还是class来定义UDT或ADT完全是一种观念上的差异,而关键字本身并没有代表这种差异”。
我们再看一看C++鼻祖Bjarne Stroustrup是如何说的:“
带类的C和C语言几乎是‘代码兼容’的,并且也是连接兼容的。C的函数可以在带类的C程序中调用,带类的C函数也可以在C程序中调用;带类的C程序中的struct和C中的struct在两个语言里的布局都一致,所以可以在两个语言之间传递简单对象或组合对象。这种连接兼容性一直保持到C++中。”
C++仍然支持C风格的struct,并且还做了增强,主要是为了兼容遗留的C代码以使它们可以在新的C++环境下重新编译而继续“发挥余热”,可以让“过程式和结构化思想根深蒂固”的C程序员比较容易地过渡到面向对象的C++语言。关于这个问题更具哲学性的讨论请参考Lippman所著的《Inside The C++ Object Model》一书。
【建议8-1】:
|
为了不使程序产生混乱和妨碍理解,建议还使用struct定义简单的数据集合;而定义一些具有行为的ADT时最好采用class,如果采用struct似乎感觉不到面向对象的味道了。
|
使用struct
在C++环境中,我们把C风格的struct叫做POD(Plain Old Data)对象,从字面上你也可以知道它仅包含一些数据成员,这些数据成员可以是基本数据类型变量、任何类型的指针或引用、任何类型的数组及其他构造类型的对象等,见示例8-2。
示例8-2
【提示
8-2
】:
|
虽然把数组当作参数传递给函数的时候,数组将自动转换为指针,但是包装在struct/class中的数组,其内存空间则完全属于该struct/class的对象所有。如果把struct/class当作参数传递给函数时,默认为值传递,其中的数组将全部复制到函数堆栈中。例如:
void func (Student s)
{
cout << sizeof (s) << endl ; // 56
}
Student s0 ;
func (s0) ;
因此,当你的UDT/ADT中包含数组成员的时候,最好使用指针或引用传递该类型的对象,并且一定要防止数组元素越界,否则它会覆盖后面的结构成员。
|
任何POD对象的初始化都可以使用memset()函数或者其他类似的内存初始化函数。假设s是Student的一个对象,用memset()初始化s的方法如下:
memset (&s, 0x00, sizeof (Student)) ;
C风格的构造类型对象也可以在定义的时候指定初始值。我们可以仅指定第一个成员的初值来初始化POD对象,后面的成员将全部自动初始化为0,就像数组的初始化一样。例如:
Student s = { 0 };
结构可以嵌套定义,也就是在一个结构的定义体内定义另一个结构,见示例8-3。
示例8-3
【提示
8-3
】:
|
构造类型虽然可以嵌套定义,但是对于嵌套定义的类型,其对象不一定存在包含关系,存在包含关系的对象类型也不一定是嵌套定义的。例如,上例中的_Name类型完全可以挪到Student定义的外面某处,而它们的对象之间的包含关系不会改变。当一个类型A只会在另一个类型B中被使用的时候,就可以把A定义在B的定义体内,这样可以减少暴露在外面的用户自定义类型的个数。
|
所谓对象之间的包含是指一个类型的对象充当了另一个类型定义的数据成员,从而也就充当了它的对象的成员,即两个对象之间存在
has-a关系。但是要注意:一个对象不能自包含,无论是直接的还是间接的,因为编译器无法为它计算sizeof值,也就不知道该给这样的对象分配多少存储空间,见示例8-4。
示例8-4
struct A
{
int i ;
B b ;
};
|
struct B
{
char ch ;
A a ;
};
|
假设A定义在B的前面,于是计算A的大小就需要知道B的大小,而计算B的大小又需要A的大小,……,于是陷入了“鸡生蛋还是蛋生鸡”的怪圈!这样的代码在编译的时候肯定通不过。
虽然对象不能自包含,但可以自引用,而且两个类型可以交叉引用,这种关系称为
holds-a关系。因为任何类型的指针的大小都一样,给指针分配存储空间的时候不需要知道它指向的对象的类型细节,见示例8-5。
示例8-5
struct A
{
int count ;
char *pName; // A holds-a string
B *pb ; // A holds-a B
};
|
struct B
{
char ch ;
A *pa; // B holds-a A
B *pNext ; // B自引用
};
|
上面的两个结构可以组成一个链表,A是链表头的类型,B是链表节点的类型。通过链表头节点可以遍历整个链表,每个链表节点还可以指向另一个链表,……,这样就形成了一个庞大的链式结构。
利用对象之间的引用关系,我们就可以实现链表、树、队列等复杂的数据结构,或者实现一些复杂的对象管理,比如对象之间的索引和定位。
【提示
8-4
】:
|
C++和C都支持相同类型对象之间的直接赋值操作(默认的“operator=语义”,就是对象按成员拷贝语义),但是不能直接比较大小和判断是否相等。
|
这是因为,相同类型对象的各数据成员在内存中的布局是一致的,编译器执行默认的位拷贝也是符合赋值操作语义的。而出于对齐(将大小调整到机器字的整数倍)的考虑,每个对象的存储空间中可能会存在填补字节,这些字节单元不会初始化而是具有上次使用留下的“脏值”(随机值)。显然每个对象填补字节的内容是不会相同的。这就是说,如果编译器支持使用逐位比较的默认方法来比较同类型对象,结果肯定是不对的,而有意义的大小关系是与具体应用相关的,显然编译器并不对应用领域的东西做任何假设。例如:
Student a, b;
cout << ((a.ID > b.ID) ? "a larger than b" : "a less than b") << endl;
所以,当默认的赋值语义不能满足我们的要求的时候,就需要定义自己的赋值语义。在C语言中只有定义一些函数来完成这样的功能,而C++则提供了运算符重载机制可以解决赋值和比较等问题。(本质上仍然是函数调用,只是形式不同而已!)
本文节选自《高质量程序设计指南:C++/C语言》
林锐,韩永泉编著
电子工业出版社出版