您的位置:首页 > 编程语言 > C语言/C++

C语言结构、联合、位操作、位域和枚举

2013-12-03 20:46 253 查看
1、结构

结构是派生的数据类型,可以使用其他数据类型来构造它们。

1.1 定义结构和结构类型的变量

关键字struct引入了结构定义,用一个标识符作为结构标记,来命名一个结构类型。结构定义大括号内声明的变量是结构的成员。同一结构的成员必须具有具有独一无二的名称,但两个不同的结构可能包含相同名称的结构成员,而不会相互冲突。每个结构定义必须用分号结束。

结构标记与关键字struct一起用来定义结构类型的变量,也可以通过在结构定义的右大括号和结束结构定义的分号之间加入逗号分隔的变量名列表来定义结构变量。关键字typedef也提供了为前面定义的数据类型创建别名(或者同义词)的机制。因此,也可以用typedef为结构类型建立比较短的类型名称。typedef不会创建新类型只会为已经创建的类型定义了新的名称,可以用作已创建类型的别名。

结构不能包含本身的实例,但是可以包含指向结构自身的指针,这里称为自引用结构。也就是说,在定义结构A的时候,不能有A类型的结构成员变量,但是可以有struct A *类型的成员变量(也就是说可以包含指向A类型的指针变量,也就是成员变量引用A自身,这很好理解吧)。需要注意的是结构的定义并没有在内存中分配任何空间,而只是创建了用户需要的用于定义变量的新的类型。定义结构类型变量的时候才会导致内存空间的分配。可以在结构上执行的操作有:将结构变量赋给相同类型的结构变量,获得结构变量的地址(&),访问结构变量的成员和使用sizeof运算符来确定结构变量的大小。

下面是几种关于结构和结构变量的定义是等价的:

(1)
struct card
{
char *face;
char *suit;
};
struct card aCard,deck[52],*cardPtr;
(2)
struct card
{
char *face;
char *suit;
}aCard,deck[52],*cardPtr;
(3)
struct
{
char *face;
char *suit;
}aCard,deck[52],*cardPtr;
(4)
typedef struct
{
char *face;
char *suit;
}Card;
Card aCard,deck[52],*cardPtr;

可以看出结构标记是可选的,如果没有结构标记,那么就只能在结构定义的时候定义结构类型的变量。为了养成良好的编程习惯和风格,定义结构时提供结构标记还是很有必要的,能够方便代码中稍后定义的结构类型的变量。

下面是图的邻接表存储结构中的顶点结构的定义,下面的两种定义也是等价的:

(1)
struct EdgeNode //边表结点
{
int adjvex; //邻接点域,存储该顶点对应的下标
EdgeType weight; //用于存储权值,对于非网图可以不需要
struct EdgeNode *next; //链域,指向下一个邻接点
};
typedef struct EdgeNode EdgeNode;
(2)
typedef struct EdgeNode/* 边表结点  */
{
int adjvex;/* 邻接点域,存储该顶点对应的下标 */
EdgeType weight;/* 用于存储权值,对于非网图可以不需要 */
struct EdgeNode *next; /* 链域,指向下一个邻接点 */
} EdgeNode;

这里之所以用typedef给一个结构声明一个别名,实际上是为了在定义或声明该结构类型的变量的时候,能够直接用EdgeNode而不必struct EdgeNode,少写一个struct。由于该结构成员中有一个指向EdgeNode类型的指针,可是到这里,还没有声明结构标志的别名,因此,这个struct是没法省略直接用EdgeNode的。

例:从矩阵邻接表存储的结构定义看struct定义和别名

typedef struct EdgeNode //边表结点
{
int adjvex; //邻接点域,存储该顶点对应的下标
EdgeType weight; //用于存储权值,对于非网图可以不需要
struct EdgeNode *next; //链域,指向下一个邻接点
} EdgeNode;

typedef struct VertexNode //顶点表结点
{
VertexType data; //顶点域,存储顶点信息
EdgeNode *firstedge; //边表头指针
} VertexNode, AdjList[MAXVEX];

typedef struct
{
AdjList adjList;
int numNodes, numEdges; //图中当前顶点数和边数
} GraphAdjList;
//上面的代码非来自本博,仅用来举例说明
上面的例子中比较好的一点就是在定义结构体VertexNode的时候,不但为struct VertexNode定义了别名VertexNode,而且将AdjList声明为了一个VertexNode数组类型的别名(注意,这里不是定义了一个VertexNode类型的数组,而是为VertexNode数组类型声明了一个别名。)。这样,再想定义这种类型的数组,就没必要VertexNode adjList[MAXVEX];了,而只需要AdjList adjList;即可。Clean and
Tidy.

1.2 结构的内存对齐问题

由于结构成员不一定是存储在内存中的连续字节中,所以不能用==和!=来比较结构。有时候,因为计算机可能仅在某些内存边界上存储特定的数据类型,如半个字、字或者双字(字是标准内存单元,它用于在计算机中存储数据,通常是两个字节或者四个字节)边界,所以在存储结构的内存区域中可能会有“洞”。这就是传说中的内存对齐问题。

struct E
{
char c;
int i;
}e1,e2;
上面的例子中,使用两个字节作为字的计算机可能需要在字边界上对齐struct E的每个成员,也就是在字的开头处(这是和机器相关的)进行对齐。下图中,说明了结构体成员变量在内存中的对齐情况,其中变量已经被赋值为字符‘a’和整数97。



如果成员存储在字边界的开头处,则在类型struct E变量的存储空间中有一个字节的空洞,如上图中所示。空洞中的值是没有定义的。如果e1和e2成员变量的值实际上相等,但可能因为在空洞中包含不同的值,所以结构比较并不一定相等。

1.3 结构的初始化

可以像数组那样使用初始值列表来初始化结构。要初始化结构,需要在定义的结构变量名后面加入的等号,以及使用逗号分隔的初始化列表,并在外面加大括号。例如语句struct card aCard={"Three","Hearts"};在定义变量aCard是struct card类型的同时也将其成员face初始化为“Three”,suit初始化为"Hearts"。如果列表中初始值的个数小于结构成员的个数,则将把剩余的成员自动初始化为0,如果成员变量是指针,则初始化为NULL。如果在函数外部定义的结构变量没有进行显示地初始化,那么其结构变量将初始化为0或者NULL。我们也可以在赋值语句中初始化结构变量:相同类型的结构变量之间赋值,或者对结构的单个成员进行赋值。

1.4 访问结构成员

有两个运算符可以用于访问结构成员:结构成员运算符(.)和结构指针运算符(->)。结构成员运算符通过结构变量名来访问结构成员,如printf("%s\n",aCard.suit);便可以访问到aCard结构变量的成员suit;结构指针运算符通过结构指针来访问结构成员,它由负号和大于号构成,两个符号之间没有空格,假设指针cardPtr是指向struct card的指针,那么语句printf("%s\n",cardPtr->suit);便可以访问到cardPtr所指向对象的suit成员。cardPtr->suit等价于(*cardPtr).suit。(由于结构成员运算符比解参考运算符的优先级要高,所以要加括号。)

1.5 在函数中使用结构

可以把单个结构成员、整个结构或者结构指针传递给函数。当把结构或者单个结构成员传递给函数时,它们采用的是值调用传递。所以,被调函数不能修改主调函数中结构的成员。要使用引用调用来传递结构,则需要传递结构变量的地址。结构数组和所有其它数组一样,都是自动使用引用传递。应该知道的是,使用引用调用来传递结构要比使用值调用来传递结构效率要高,因为引用调用不用复制整个结构。

1.6 一个结构的例子

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

struct card
{
const char *face;
const char *suit;
};
typedef struct card Card;

void fillDeck(Card * const wDeck,const char *wFace[],const char *wSuit[]);
void shuffle(Card *const wDeck);
void deal(const Card * const wDeck);

int main()
{
//定义数组存储纸牌
Card deck[52];
//牌面数组
const char *face[]={"Ace","Deuce","Three","Four","Five","Six","Seven","Eight","Nine","Ten","Jack","Queen","King"};
//花色
const char *suit[]={"Hearts","Diamonds","Clubs","Spades"};

//设置随机数发生器的种子
srand(time(NULL));
//load the card to deck
fillDeck(deck,face,suit);
//shuffle the card
shuffle(deck);
//distribute
deal(deck);
return 0;
}
void fillDeck(Card * const wDeck,const char *wFace[],const char *wSuit[])
{
int i;
for (i=0;i<=51;i++)
{
wDeck[i].face=wFace[i%13];
wDeck[i].suit=wSuit[i%4];
}
}
void shuffle(Card *const wDeck)
{
int i;
int j;
Card temp;
//随机选出一张放在第i个位置
for (i=0;i<=51;i++)
{
j=rand()%52;
temp=wDeck[i];
wDeck[i]=wDeck[j];
wDeck[j]=temp;
}
}
void deal(const Card *const wDeck)
{
int i;
for (i=0;i<=51;i++)
printf("%5s of %-8s%c",wDeck[i].face,wDeck[i].suit,(i+1)%3?'\t':'\n');
}
运行结果:



这是一个关于发牌的例子,还是比较好的。首先定义纸牌结构的时候,成员变量的类型是const char *,这样,便不能通过对该指针进行解参考来修改指针指向的变量的值,却可以修改指针本身的指向,很明显这样是安全(符合一般程序设计中的最低权限原则)而且合理的。void fillDeck(Card * const wDeck,const char *wFace[],const char *wSuit[]);、void shuffle(Card *const wDeck);和void deal(const Card *
const wDeck);等函数中const的运用十分巧妙的控制了函数对参数的访问权限,十分漂亮。

2、联合(union)

联合是一种派生的数据类型,它与结构类似,区别在于联合的成员共享相同的存储空间。在程序中有这样的情况,某些变量之间是相关的。这样就可以使用联合来对这些变量的聂存空间进行共享,来避免不必要的内存空间浪费。联合的成员可以是任意类型,存储联合所需要的字节数必须至少能够存储联合中占空间最大的成员。多数情况下,联合包含两种或两种以上的数据类型,每次只能引用一个成员,也就是只能引用一种数据类型。使用时,应该注意用正确的类型来引用联合中的数据。

在联合上可以执行的操作有:将联合赋值给相同类型的另一个联合、获取联合的地址(&)以及使用结构成员运算符和结构指针运算符来访问联合成员。类似地,也不能用==和!=运算符来来比较联合。

2.1 定义和初始化

联合的定义和结构类似,下面是一个例子:

union number
{
int x;
double y;
};
和struct一样,上面仅仅是创建了一个类型,还没有用该类型来定义变量。在联合变量的定义中,仅能用于第1个联合类型相同的值来初始化联合,因为上面的例子中联合成员的第一个类型是int,所以语句union number value={10};正确地初始化了变量value。但语句union number value={3.1415}将截断浮点数值的小数部分,并通常会产生编译警告。

2.2 联合的例子

#include <stdio.h>
union number
{
char c;
unsigned int i;
};
int main()
{
union number value;

value.c='A';
printf("%s:\nchar:%c\nunsigned int:%d\n","Put a value int the char member",value.c,value.i);

value.i=97;
printf("%s:\nchar:%c\nunsigned int:%d\n","Put a value int the unsigned int member",value.c,value.i);

value.c='A';
printf("%s:\nchar:%c\nunsigned int:%d\n","Put a value int the char member",value.c,value.i);
return 0;
}
运行结果:



需要说明的是联合这种数据结构可能不能轻易地移植到其它计算机系统上。联合能否被移植通常依赖于给定系统上存储联合成员数据类型时所使用的对齐方式。

2.3 联合的使用

从上面看,仿佛联合这个东西是一个怪物,谁会自找麻烦去用这个啊。我也是这么想的,结果在网上找到一篇文章专门写联合的使用的。摘了一写东西,贴在这里。

2.3.1 增加代码的可读性

struct  Matrix
{
union
{
struct
{
float  _f11, _f12, _f21, _f22;
};
float  f[2][2];
}_matrix;
};
struct  Matrix m;
该例子中,struct和float f[][]共享内存空间,没有造成内存空间的浪费。这样,用矩阵的时候可以用m._matrix.f(比如说传参,或者是整体赋值等);需要用其中的几个元素的时候可以用m._matrix._f11,可以避免用m.f[0][0](不直观,且容易出错)。

2.3.2 union和强制类型转换

这里需要说明的是union里面的成员都是从低地址开始对齐的。拿上面定义的number联合作为例子,其中的char成员c和unsigned int成员i,它们在内存中的分布应该如下图:



下面是一个利用判断大小端的例子,分别采用union和非union的实现:

#define TRUE 1
#define FALSE 0
#define BOOL int
//不用union
BOOL  isBigEndian()
{
unsigned int  i = 1;   /* i = 0x00000001*/
char  c = *(char  *)&i; /* 注意不能写成 char c = (char)i; */
return  (int )c != i;
}
//用union
BOOL  isBigEndian()
{
union
{
unsigned int  i;
char  c;
}test;
test.i = 2;
return  test.c != 2;
}
3、位运算

在计算机内部是使用位序列来表示所有数据的。每一位的值可以是0或者是1。因此,利用位运算比较方便,而且通常比较高效。

3.1 C语言提供了的位运算符

&|^ 对两个操作数按位进行与、或、异或操作
<<左移 将第一个操作数的各位向左移动第二个操作数所指定的位数;在右边用0来填充空位,向左移动到边界之外的1将丢失。
>>右移 将第一个操作数的各位向右移动第二个操作数所指定的位数;填充左边的方法依赖于计算机。对于unsigned整数执行右移将使得左边的空位用0代替,移动到右边界之外的1将丢失。如果右边的操作数是负值,或者右边的操作数大于存储左边操作数的位数,则移位的结果是不确定的。
~取反 将操作数按位取反
3.2 一个例子:以二进制的形式输出无符号整数

#include <stdio.h>
void displayBits(unsigned value);
int main()
{
unsigned x;
printf("Enter an unsigned integer:\n");
scanf("%u",&x);
displayBits(x);
return 0;
}
void displayBits(unsigned value)
{
unsigned c;
unsigned displayMask=1<<31;
printf("%10u= ",value);
for (c=1;c<=32;c++)
{
putchar(value&displayMask?'1':'0');
value<<=1;//左移一位
if(c%4==0)
{
putchar(' ');
if(c%8==0)
putchar(' ');
}
}
putchar('\n');
}
运行结果:



4、位域

在C语言中,可以指定结构或者联合中unsigned或者int成员的位数,这些位称为位域。位域在所需要的最小位数内存存储数据,因此,可以更好地利用内存。位域成员必须声明为int或者unsigned。

4.1 位域的声明

struct bitCard
{
unsigned face : 4;
unsigned suit : 2;
unsigned color : 1;
};
该定义包含3个unsigned位域,即face、suit和color,用于表示52张纸牌中的一张纸牌。在unsigned或者int成员名称的后面加入冒号和表示位宽度的整数常量(也就是存储成员所需的位数),就可以声明位域。表示宽度的常量必须是0和系统上存储int的总位数之间的一个整数。

struct example
{
unsigned a : 13;
unsigned   : 19;
unsigned b : 4;
};
如上面的例子,我们也可以指定没有命名的位域,其中的字段可以用来做填充内容。上面例子中没有命名的19位作为填充内容,在这19位中不能存储任何东西,只是用来保证成员b存储在另一个存储单元(这里讨论的是4字节的机器)。

struct example
{
unsigned a : 13;
unsigned   : 0;
unsigned b : 4;
};
宽度为0的无名位域可以用于在新的存储单元边界上对齐下一个位域。上面的例子中使用没有命名且宽度为0的位域来跳过存储a的存储单元中的剩余位(无论有多少),这使得b和下一个存储单元边界对齐。

4.2 使用位域的好处和缺点

因为位域没有地址,所以不能用&来获得位域的地址,另外,也不能像数组元素那样访问位域内的单个位。尽管位域可以节省空间,但是它们会使编译器产生执行速度较慢的机器语言代码。因为机器语言代码与需要额外的步骤来访问可寻址存储单元中的部分。这是一种典型的时间换空间的实现。

4.3 例子

#include<stdio.h>
struct bitCard
{
unsigned face : 4;
unsigned suit : 2;
unsigned color: 1;//1 bit
};
typedef struct bitCard Card;
void fillDeck(Card *const wDeck);
void deal(const Card * const wDeck);

int main()
{
Card deck[52];
fillDeck(deck);
deal(deck);
return 0;
}
void fillDeck(Card *const wDeck)
{
int i;
for (i=0;i<=51;i++)
{
wDeck[i].face=i%13;
wDeck[i].suit=i/13;
wDeck[i].color=i/16;
}
}
void deal(const Card * const wDeck)
{
int i;
for (i=0;i<=51;i++)
printf("Card:%2d Suit:%2d Color:%2d%s",wDeck[i].face,wDeck[i].suit,wDeck[i].color,(i+1)%3?" | ":"\n");
printf("\n");
}
运行结果:



5、枚举

C语言提供的最后一个用户自定义类型称为枚举。由关键字enum声明的枚举是用标识符表示的一组整数常量。实际上,这些枚举常量是可以自动设置值的符号常量。枚举中的值从0开始,每次增加1(除非特别指定)。

5.1 枚举声明

如声明enum months{JAN,FEB,MAR,APR,MAY,JUN,JUL,AUG,SEP,OCT,NOV,DEC};创建了一个新类型enum months,其中标识符设置为整数0到11。如果要记录月份1-12,可以采用enum months{JAN=1,FEB,MAR,APR,MAY,JUN,JUL,AUG,SEP,OCT,NOV,DEC};,第一个值被显示地设置为1,后续的其余值从1开始累加,从而产生1到12的值。枚举中的标识符必须是唯一的。通过标识符赋值,可以明确地在定义中设置枚举的每个常量值。枚举的多个成员可以具有相同的常量值。

5.2 例子

#include <stdio.h>
//声明一个枚举类型
enum months {JAN=1,FEB,MAR,APR,MAY,JUN,JUL,AUG,SEP,OCT,NOV,DEC};
int main()
{
//定义一个枚举变量
enum months month;
const char *monthName[]={"*Error*","January","February","March","April",
"May","June","July","August","September","October","November","December"};
for (month=JAN;month<=DEC;month=(enum months)(1+(int)month))
{
printf("%2d %10s%c",month,monthName[month],month%2?'\t':'\n');
}
return 0;
}
运行结果:



好了,又over了一篇。尽管充其量只能算是笔记,我还是像耐着性子把它们写完,就算是锻炼心态了。


参考:/article/2841977.html和<C语言程序设计经典教程>
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: