结构体

结构体是一个或多个变量的集合,这些变量可能为不同的类型,为了方便处理而将这些变量组织在一个名字之下(某些语言称之为“记录”,比如Pascal语言)。
区分于数组,结构中变量类型可以不同,结构将一组复杂数据组织在同一连续(一般情况,但不保证)存储单元下,而不是视为独立个体,有助于分析处理复杂的数据,特别是在大型程序中,如:

工资记录就是用来描述结构的一个传统例子,每个雇员由一组属性描述,包括姓名、地址、社会保险号、工资等

其中某些属性也可以是结构,例如姓名可以分几部分,地址甚至公司也可能出现类似的情况
c中更典型的一个例子来自于图形领域:点由一对坐标定义,矩形由两个点定义等

struct point{int x,y;};struct rect{struct point pt2,pt2;};

ANSI标准在结构方面最主要的变化是定义了结构的赋值操作———结构可以拷贝、赋值、传递给函数,函数也可以返回结构类型的返回值。实际在ANSI标准多年以前这些操作就已经被大多数编译器所支持,但直到这一标准才对其属性进行了精确定义。ANSI标准中,自动(非外部的)结构和数组现在也可以通过成员赋值进行初始化。

结构体类型声明和定义

关键字struct引入结构声明,结构声明由包含在花括号内的一系列声明组成,关键字struct后面的名字是可选的,称为结构标记,用于为结构命名
struct声明定义了一种数据类型,在标志结构成员表结束的右花括号之后可以跟一个变量表,这与其他基本类型的变量声明是相同的
定义结构体基本格式为:

struct 结构标记 struct 关键字
{
类型名1 成员1; 每个成员定义与普通变量相同
类型名2 成员2; 每个成员既可以是基本数据类型,也可以是结构等复杂数据类型

类型名n 成员n;
};

结构类型定义(声明)以后需要加一个分号(struct后面相当于声明语句,需要加分号),如:

struct{…;}x,y,z; 从语法角度来说,这种方式的声明与声明int x,y,z;具有类似的意义

这两个声明都将x、y与z声明为指定类型的变量,并为它们分配存储空间,如果结构声明后面不带变量表,则不需要为它分配存储空间,它仅仅描述了一个结构的模版或轮廓。在定义之后,结构标记代表花括号内的声明,可作为该声明的简写形式,结构中定义的变量称为“成员”,如:

struct student
{
char name[20];
int id;
float chinese;
float math;
float english;
};

结构体命名

结构成员、结构标记和普通变量(非结构的成员)可以采用相同的名字(结构标记在使用时要加struct),不同结构的成员也可以使用相同名字,相同名字之间不会冲突,因为通过上下文分析总可以对它们进行区分,但是从编程风格方面来说,通常只有密切相关的对象才会使用相同的名字。
定义完结构类型,结构变量后,就可以引用,结构变量中各成员的引用格式为:

结构变量名.成员名

其中的结构成员运算符’.'将结构名和成员名连接起来,如:

student.name 表示student结构的name成员 .是成员运算符,可以理解为“的”

在表达式中也可以使用该形式引用某个特定结构中的成员,如:

printf("%d,%d",pt.x,pt.y); 打印点pt的坐标
double dist=sqrt((double)pt.x*pt.x+(double)pt.y*pt.y); 计算原点(0,0)到pt的距离

结构可以嵌套,如:

struct rect{struct point pt1,pt2;}; 结构包含两个point类型成员,可以代表矩形的一对对角线上的坐标
struct rect screen;screen.pt1.x; 可以引用矩形结构体screen的成员pt1的x坐标 结构变量初始化

如果结构声明中带有标记,则在以后定义结构实例时便可以用该结构标记定义结构变量,如:

struct point pt; 定义了一个struct point类型的变量pt

结构的初始化可以在定义的后面使用初值表进行,初值表中同每个成员对应的初值必须是常量表达式,如:

struct point maxpt={320,200};

初始化基本格式为:

struct 结构标记 结构变量名={结构成员数据1,结构成员数据2…结构成员数据n}

直接定义的话,struct也在数据类型名当中,花括号中初始化数据依次赋给结构变量各成员,数据类型,顺序要与成员相匹配。
与数组不同,结构变量名不代表结构变量首地址,因此可以用相同类型的结构变量名在他们之间相互赋值,如:

struct weather{float temp, wet;};
struct weather today={21,5.5},yesterday={25.5,4};
today = yesterday;
printf("%.1f %.1f",today.temp,today.wet); 输出结果:25.5,4.0 自引用结构

一个结构包含其自身的实例是非法的,但是包含其指针是合法的,在结构的定义中,可以采用类似于递归(不是真正的递归)的方式,如:

struct tnode
{
char*word;
int count;
struct tnode*left,*right;
};

这种对结点的递归声明方式看上去好像是不确定的,但它的确是正确的,它将left声明为指向tnode的指针,而不是tnode实例本身。

偶尔也可能会使用自引用结构的一种变体:两个结构相互引用,如:

struct t{
struct s*p; p指向一个s结构
};
struct s{
struct t*q; q指向一个t结构
};

结构体运算

结构的合法操作只有几种:

  • 作为一个整体进行赋值和拷贝,通过&运算符取地址,访问成员
  • 赋值和拷贝包括向函数传递参数以及从函数返回值
  • 可以用一个常量成员值列表初始化结构,自动结构也可以通过赋值进行初始化
  • 结构之间不可以进行比较,加减等算数和关系运算

结构体的长度

注意:千万不要认为结构的长度等于各成员长度的和
由于不同对象有不同的对齐要求,因此结构中可能出现未命名的“空穴”(hole),如:

假设char类型占用一个字节,int类型占用4个字节
则struct{char c;int i;};可能占用8个字节而不是5个字节

结构示例程序:要求存储本年级100名学生学号,姓名,语文,数学,外语三门课程成绩,根据语文成绩递减排序,按名次输出学生信息

#include<stdio.h>
int main()
{
    const int m=20,n=4;
    int i,j,max;
    typedef struct student
    {
        char name[m];
        int id;
        float chinese,math,english;
    }student;
    student st[n],tmp;
    for(i=0;i<n;i++)
    {
        scanf("%s %d %f %f %f",st[i].name,\
            &st[i].id,&st[i].chinese,&st[i].math,&st[i].english);
    }
    for(i=0;i<n;i++)
    {
        max=i;
        for(j=i+1;j<n;j++)
        {
            if(st[j].chinese>=st[max].chinese)max=j;
            tmp=st[i];
            st[i]=st[max];
            st[max]=tmp;
        }
    }
    for(i=0;i<n;i++)
    {
        printf("%s %d %f %f %f\n",st[i].name,st[i].id,st[i].chinese,st[i].math,st[i].english);
    }
}

学生信息排序程序分析
程序整体可分为四个部分:

  • 声明student类型和各变量
    • const int m,n 分别为name字符串长度 学生个数
    • 使用typedef定义student结构类型,
    • 这部分关键在于定义名为st,长度为n的结构数组,其中每个元素都是一个student结构类型
    • 然后定义一个student类型的变量tmp,以及i,j,max用来处理循环和排序的问题
  • 输入学生数据
    • 使用for循环为数组输入初始化数据,初始化数据的内容是结构数组中每个元素的各个成员数据
  • 根据语文成绩排序
    • 简单选择排序
    • 这部分关键在于交换a,b的值,使用一个中间变量c,令a=c,a=b,b=c即可完成两个数据的对换
    • 此程序的方法是对st[i]的元素的Chinese成员与其之后的元素依次进行对比,遇到比st[i]大的就互换两元素的值,保证st[i]总是比之后的元素大
    • 首先max=i,如果st[j].chinese>=st[max].chinese就将max值换为j
    • 然后用tmp存放st[i]的值,令st[max](此时就是st[j])的值换成st[i],最后令st[i]=st[max],st[j]=tmp,就完成了两数据的互换
  • 输出学生信息
    • 两层循环完成后,st[n]中的每个元素的Chinese值都保证比之后的元素大即排序完成
    • 此时按顺序输出st[n]中元素的成员即可

类型定义(typedef)

c提供了一个被称为typedef的功能,用于建立新的数据类型名,如:

声明typedef int Length; 则Length可用于类型声明、类型转换等,它和类型int完全相同
Length len,maxlen;
Length >*lengths[];
typedef char*String;
String p,lineptr[MAXLINES];
int strcmp(String,String);
p=(String)malloc(100); 注意,typedef中声明的类型在变量名的位置出现,而不需要紧接在typedef后面

typedef在语法上类似于修饰符extern,static等,一般将typedef定义的类型的首字母定义为大写字母
typedef用于为结构标记命名的情况非常常见,如果想在结构类型中省略struct 可使用typedef关键字定义习惯的数据类型名,如:

typedef struct student{;}student;
student stu1,stu2; 这时student等效于struct student
typedef struct tnode*Treeptr; 声明一个指向struct tnode结构的指针Treeptr
typedef struct tnode{
char*word;
int count;
Treeptr left,right;
}Treenode; 创建一个struct tnode实例

需要强调的是,从任何意义上讲,typedef声明没有创建一个新类型,它只是为某个已存在的类型(可能是已有类型的组合)增加了一个新的名称而已,typedef声明也没有增加任何新的语义:通过这种方式声明的变量与通过普通声明方式声明的变量具有完全相同的属性。实际上,typedef类似于#define语句,但由于typedef是由编译器解释的,因此它的文本替换功能要超过预处理器的能力,如:

typedef int (*PFI)(char*,char*);PFI strcmp,numcmp;

该语句定义了类型PFI是一个“指向函数的指针,该函数有两个char*类型的参数,并且返回值类型为int”,并且与宏扩展不同,typedef定义的函数指针可以指定返回类型并创建数据类型,能更好地适应上下文。
除表达式简洁外,使用typedef还有另外的重要原因

  • typedef可以使程序中的数据类型参数化,以提高程序的可移植性
  • 如果typedef声明的数据类型同机器有关,那么当程序移植到其他机器上时,只需改变typedef类型定义就可以了

一个经常用到的情况是,对于各种不同大小的整型值来说,都使用通过typedef定义的类型名,然后分别为不同的宿主机选择一组合适的int、short和long类型大小进行声明即可,标准库中有一些例子,如:

size_t,ptrdiff_t等

此外,在c++中,像std::string和std::ofstream这样的typedef还隐藏了长长的,难以理解的模板特化语法,如:

basic_string<char, char_traits<char>,allocator<char>> 和 basic_ofstream<char, char_traits<char>>。

其次,typedef的第二个作用是为程序提供更好的说明性,如:

Treeptr类型显然比一个声明为指向复杂结构的指针更容易理解

typedef不能和static等存储类型指示符同时使用,因为每个变量只能有一种存储类型,因此假设把一种存储类型的变量直接取别名的话,用这个名字定义的变量就不能加其他存储类型前缀,这样的就会影响typedef声明变量的语法,因此c语言干脆不允许typedef定义某种特定存储类型的变量,如:

typedef static int i;是非法的

实际上,根据c语言规范,在进行句法分析的时候,typedef的用法和存储类型指示符是相同的,因此任何存储类型类型可以出现的地方typedef同样可以出现,如:

static int i;
typdef int i;
extern int i();
typedef int i();
int const register i;
int const typedef i;

联合

联合可以在(不同时刻)保存不同类型和长度的对象的变量,在此过程中编译器负责跟踪对象的长度和对齐要求。联合提供了一种方式以在单块存储区中管理不同类型的数据,而不需要在程序中嵌入任何同机器有关的信息,它类似于Pascal语言中的变体记录,特定类型的常量值必须保存在合适类型的变量中,而如果常量的不同类型占据相同大小的存储空间且保存在同一位置,表管理将最方便。这就是联合的目的——一个变量可以合法地保存多种数据类型中的任何一种类型的对象
其基本语法格式是基于结构的:

union 联合标记
{
类型名1 成员1;
类型名2 成员2;

类型名n 成员n;
}u;

联合必须足够大,以保存成员类型中最大的一种,具体长度和具体实现有关。上例中任意一种成员类型的对象都可以赋值给u,且可使用在随后的表达式中,但必须保证是一致的,读取的类型必须是最后一次存储后更新的类型,对当前保存在联合中的类型由程序员负责跟踪。如果保存的类型与读取的类型不一致,其结果取决于具体的实现。同样的,与访问结构的方式相同,可根据下列语法访问联合中的成员:

联合名.成员 或 联合指针->成员
如:if(utypeINT)cout<<u.idval<<endl;
else if(utype
FLOAT)cout<<uptr->fval<<endl;

联合可以使用在结构和数组中,反之亦可,访问结构中的联合(或反之)的某一成员的表示法与嵌套结构相同,如:

struct{
char*name;
int flags,utype;
union{
int ival;
float fval;
char*sval;
}u;
}symtab[NSYM];
symtab[i].u.ival; 可引用联合成员ival
symtab[i].u.sval; 可引用联合成员sval的第一个字符

注意,联合的值只能使用其第一个成员类型的值进行初始化,因为联合的值默认是按第一个成员类型进行存储。联合中只存有一个值,所以不同通过哪一个成员进行引用都是将联合的值(第一个成员类型的值)转化成对应成员的类型再引用,如果联合中存储的值无法转换成被访问成员的数据类型,则会出错,如:

union
{
int ival;
float fval;
char*sval;
}u={0.5};
cout<<u.fval; 输出0
cout<<u.sval; 运行错误

实际上,联合就是一个结构,特殊在于它的所有成员相对于基地址的偏移量都为0,此结构空间只需要大到足够容纳最“宽”的成员即可,并且能够保证其对齐方式适合于联合中所有类型的成员。
对联合允许的操作与对结构允许的操作相同:作为一个整体单元进行赋值、复制、取地址以及访问其中一个成员。

位字段

在存储空间很宝贵的情况下,有可能需要将多个对象保存在一个机器字(8bit)中,一种常用的方法是,使用类似于编译器符号表的单个二进制位标志集合。另外,外部强加的数据格式(如硬件设备接口)也经常需要从字的部分位中读取数据。考虑编译器中符号表操作的有关细节:程序中的每个标识符都有与之相关的特定信息,如:

是否为关键字、是否是外部的且(或)是静态的等等

对这些信息进行编码的最简洁的方法就是在一个char或int对象(flags)中使用位标志操作集合来操作(尤其是一些布尔类型的判断实际只需1位就可完成),通常采用的方法是,定义一个与相关位的位置对应的“屏蔽码”集合,如:

#define KEYWORD 01 或 enum{KEYWORD=01,EXTERNAL=02,STATIC=04};
#define EXTERNAL 02
#define STATIC 04

这些数字都是2的整数次幂,因而访问这些位就可以通过使用移位运算、屏蔽运算及补码运算进行简单的位运算
下列语句在程序中经常出现,如:

flags|=EXTERNAL|STATIC; 该语句将flags中的EXTERNAL和STATIC位(第2和第3位)置为1
flags&=~(EXTERNAL|STATIC); 该语句将flags中上面的位置为0
当这两位都为0时,表达式:if((flags&(EXTERNAL|STATIC))==0)值为真

使用这种方法就实现了记录数据特定二进制位的信息,从而使用一个字节的char就可以记录多种不同数据,尽管这种方法很容易掌握,但c仍然提供了另一种可替代的方法:在结构(或联合)中直接定义和访问一个字中的位字段的能力,而不需通过按位逻辑运算符。

位字段(bit-field,或简称字段,是“字”中相邻位的集合,主要用于一些使用空间很宝贵的程序设计中,如嵌入式程序设计。
“字”(word)是机器中单个的存储单元,同具体实现有关,连续声明多个位字段,编译器会将它们合并成一个机器字。位字段是C语言中一种存储结构,一般常作为结构体成员出现,不同于一般结构体成员的是它在定义时需要指定所占位数,如:

struct{
unsigned int is_keyword:1;
unsigned int is_extern:1;
unsigned int is_static:1;
}flags;

结构变量flags包含3个一位的位字段成员,冒号后的数字表示位字段的宽度(用二进制位数表示),位字段声明为unsigned int类型以保证它们是无符号量。

单个位字段的引用方式与其他结构成员相同,如:

flags.is_keyword、flags.is_extern等

位字段的作用与屏蔽码相似,同其他整数一样,字段可以出现在算术表达式中,如:

flags.is_extern=0,flags.is_static=1; 该语句将flags的is_extern位置为0、is_static位置为1
if(!flags.is_extern&&!flags.is_static)用于对is_extern和is_static进行测试

位字段的所有属性几乎都同具体的实现有关

  • 位字段也可以仅仅声明为int,不过为方便移植一般需要显式声明该int类型是signed还是unsigned类型
  • 位字段不是数组,并且没有地址,因此不能对它们使用&运算符(位字段通常不会占据可寻址的内存位置)
  • 位字段是否能覆盖字边界由具体的实现定义(一般必须小于等于指定类型的位宽),特殊宽度0可以用来强制在下一个字边界上对齐。
  • 位字段可以不命名,无名字段(只有一个冒号和宽度)起填充作用

某些机器上字段的分配是从字的左端至右端进行的(大端小端),而某些机器上则相反,这意味着尽管字段对维护内部定义的数据结构很有用。但在选择外部定义数据的情况下,必须仔细考虑哪端优先的问题,而依赖于这些问题的程序是不可移植的。