C++/C学习笔记(六)
1.结构(struct)
(1)定义
C语言支持把基本数据类型组合起来形成更大的构造数据类型,这就是C语言的struct,有时也称为用户自定义数据类型(UDT)。构造数据类型还可以嵌套(对象嵌入)和引用(对像关联)。
构造数据类型是一个递归的定义:
①由若干基本数据类型组合而成的类型是构造数据类型;
②由若干基本数据类型和构造数据类型组合而成的类型是构造数据类型;
③由若干构造数据类型组合而成的类型是构造数据类型;
语言本身的这种能力使我们能够定义非常复杂的数据结构,例如树(tree)、链表(list)、映射(map)等.
(2)关键字struct与class的困惑
C++的struct相对于C进行了改造,使其可以像class那样支持成员函数的声明和定义,变成真正的抽象数据类型(ADT)。
在C++语言中,如果不特别指明,struct成员的默认访问限定符为public,而class成员的默认访问限定符为private。因此,在C++程序中,只要能明确地声明每一个成员的访问权限,那么完全可以用class取代struct。
(简单从书上看到这些,当然有些研究深入的认为不同点完全不止这些,以后慢慢学习吧)
为了不使程序产生混乱和妨碍理解,建议还使用struct定义简单的数据集合,而定义一些具有行为的ADT时最好采用class。
(3)使用struct
在C++环境中,把C风格的struct叫做POD对象,它仅包含一些数据成员,这些成员可以是基本数据类型变量、任何类型的指针或引用、任何类型的数组及其他构造类型的对象等,例如:
struct Student
{
unsigned long ID;
char firstName[15];
char lastName[15];
char email[20];
};
虽然把数组当做参数传递给函数的时候,数组将自动转化为指针,但是包装在struct/class中的数组及其内存空间则完全属于该struct/class的对象所有。如果把struct/class当做参数传递给函数时,默认为值传递,其中数组将全部拷贝到函数堆栈中。例如:
void func(Student s)
{
cout<<sizeof(s)<<endl; //56
}
student s0;
func(s0);
因此,当你的UDT/ADT中包含数组成员的时候,最好使用指针或引用传递该类型的对象,并且一定要防止数组元素越界,否则它会覆盖后面的结构成员。
任何POD对象的初始化都可以使用memset()函数或者其他类似的内存初始化函数。例如 初始化s0:memset(&s0,0x00,sizeof(Student));
C风格的构造类型对象也还可以在定义的时候指定初始值,可以仅指定第一个成员的初始值来初始化POD对象,后面的成员将全部自动初始化为0,像数组初始化那样。例如:Student s={0};
结构体也可以嵌套定义,在一个结构的定义体内定义另一个结构:
struct Student
{
struct _Name
{
char firstName[15];
char lastName[15];
};
unsigned long ID;
_Name name;
char email[20];
};
构造类型虽然可以嵌套定义,但是嵌套定义的类型其对象不一定存在包含关系,存在包含关系的对象类型也不一定是嵌套定义的。例如上例中的_Name类型完全可以挪到Student定义的外面某处,而它们的对象之间的包含关系不会改变。当一个类型A只会在另一个类型B中被使用的时候,就可以把A定义在B的定义体内,这样可以减少暴露在外面的用户自定义类型的个数。
所谓对象之间的包含是指一个类型的对象充当了另一个类型定义的数据成员,从而也就充当了它的对象的成员,即两个对象之间存在着has-a关系。但是,一个对象不能自包含,无论是直接的还是间接的,因为编译器无法为它计算sizeof值,也就不知道该给这样的对象分配多少存储空间。例如:
struct A
{
int i;
B b;
}
struct B
{
char ch;
A a;
}
假设A定义在B的前面,于是计算A的大小就要知道B的大小,而计算B又要知道A....这样的代码编译通不过。
虽然对象不能自包含,但可以自引用,而且两个类型可以交叉引用,这种关系称为holds-a关系。因为任何类型的指针的大小都一样,给指针分配存储空间的时候不需要知道它指向的对象的类型细节。例如:
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是链表节点的类型。通过链表头节点可以遍历真个链表,每个链表节点还可以指向另一个链表,....,这样就形成了一个庞大的链式结构。
C++和C都支持相同类型对象之间的直接赋值操作(默认的operator=语义,就是对象按成员拷贝语义),但是不能直接比较大小和判等。
这是因为,相同类型对象的各数据成员在内存中的布局是一致的,编译器执行默认的位拷贝也是符合赋值操作语句的。而出于对齐(将大小调整到机器字的整数倍)的考虑,每个对象的存储空间中可能会存在填补字节,这些字节单元不会初始化而是具有随机值,若按逐位比较,结果肯定不对。
2.位域
位域是指信息在存储时,并不需要占用一个完整的字节, 而只需占几个或一个二进制位。例如在存放一个开关量时,只有0和1 两种状态, 用一位二进位即可。为了节省存储空间,并使处理简便,C语言又提供了一种数据结构,称为“位域”或“位段”。所谓“位域”是把一个字节中的二进位划分为几 个不同的区域, 并说明每个区域的位数。每个域有一个域名,允许在程序中按域名进行操作。 这样就可以把几个不同的对象用一个字节的二进制位域来表示。
位域以单个的位(bit)为单位来设计一个struct所需要的存储空间,可以根据数据成员的有效取值范围来仔细规划它们各自所需要的位数。
struct DateTime
{
unsigned int year;
unsigned int month :4;
unsigned int day :5;
unsigned int hour :5;
unsigned int minute :6;
unsigned int second :6;
};
cout<<sizeof(DateTime)<<endl; //8
C语言位域各成员的类型必须是int、unsigned int、signed int等类型,C++还允许使用char、long等类型,不允许使用指针类型或浮点类型作为位域的成员类型,因为它们可能导致无效的值。
不要定义超越类型最大位数的位域成员(与平台相关)。例如:
struct DateTime
{
unsigned int year :33; //位越界
};
如果允许这样做,不仅会导致结果上溢,还会导致一个成员跨越两个字节的边界却又不能占满整个字节,这又是巨大的浪费。C++/C语言拒绝这样的定义。
可以定义非具名的位域成员,作用是占位符,用来隔离两个相邻的位域成员。
struct DateTime
{
//...
unsigned int day :5;
unsigned int :2;//位域成员没有名字,不能直接访问它所在的位
unsigned int hour :5;
unsigned int minute :6;
unsigned int second :6;
};
可以定义长度为0的位域成员,作用是迫使下一个成员从下一个完整的机器字(Word)开始分配空间。
struct DateTime
{
//...
unsigned int day :5;
unsigned int :0;
unsigned int hour :5;
//...
};
Cout<<sizeof(DateTime)<<endl; //12
不能取一个位域对象的数据成员的地址,即使该成员完全与字节边界对齐,因为字节是编址的最小单位而不是位;但是可以取位域对象的地址,即使位域所有成员的位数总和达不到整字节的倍数,位域对象也会对齐到机器字长。
访问位域成员方法:位运算符(~、&、|、>>、<<、^及其与=的组合运算符),或者使用std:bitset<N>。
在设计位域的时候,不要让一个位域成员跨越一个不完整的字节来存放,因为这样会增加计算机运算的开销,上面的DateTime的比较好的设计应该是:
struct DateTime
{
unsigned int year;
unsigned int month :8;
unsigned int day :8;
unsigned int hour :8;
unsigned int minute :8;
unsigned int second :8;
};
使用位域节省存储空间会导致程序运行速度的下降,因为计算机无法直接寻址到单个字节的某些位,要通过额外代码来实现。在“内存空间”和“运行速度”无法同时优化的情况下,由应用需求决定优化哪一个。