一. 结构类型的概念与意义
结构类型的概念:结构是一组相关的不同类型的数据的集合,且是原来没有、但应用户实际需要而生的新的数据类型,是一种自定义类型。
结构类型的意义:为处理复杂数据提供便利。(e.g.人是多面体,形容一个人很难用 int,char,double等 已有的简单数据类型单独说明,往往需要组合描述,e.g.char name[];int height;double weight;...)
二. 结构类型的定义(声明)
结构类型的定义:
在使用结构类型之前,对结构的组成进行描述,称为结构定义。
一般形式如下:
struct 结构类型名称
{
数据类型 成员名1;
数据类型 成员名2;
......
数据类型 成员名n;
};
//e.g.
struct Stu
{
char name[10];
char id[20];
int age;
int height;
double weight;
};
其中,struct为关键字,是结构的标识符;结构类型名称是所定义的结构的类型标识,由用户自己定义(和int等一个级别,是用来说明变量的类型名称);{ }中包括组成该结构的成员项。
结构成员的数据类型可以是简单的数据类型(e.g.int,double...),也可以是复杂的数据类型(即结构成员的数据类型也可为结构类型)。
匿名结构类型:结构类型名称是可以省略的,但只能使用一次,格式如下。
struct
{
char c;
int i;
double d;
} s;
int main()
{
struct s2;//err
return 0;
}
两个结构在声明是都省略了结构类型名称,即使成员组成一样,编译器还是会将这两个声明当作完全不同的两个类型。
struct
{
char c;
int i;
double d;
}s;
struct
{
char c;
int i;
double d;
}* ps;
int main()
{
ps=&s;//err
return 0;
}
结构中所有成员应是逻辑上紧密相关的,否则无任何实际意义。
结构定义的位置和作用域:
结构定义的位置:随意,可在一个函数的内部,也可在所有函数的外部。
结构定义作用域:分全局/局部,与其位置有关。
三. 结构变量的说明
定义说明了结构的组成,要使用该结构类型就要进一步说明结构类型的变量。
一般形式如下:(2种)
//先定义类型后说明变量
struct Stu
{
char name[10];
int age;
char id[20];
};
int main()
{
struct Stu H,Z;//定义了2个Stu类型的变量,变量名为H和Z,局部变量
return 0;
}
//定义类型和说明变量同时进行
struct Stu
{
char name[10];
int age;
char id[20];
}H,Z;//在函数外部,全局变量
结构的定义不会使系统为该结构分配内存空间,只有在说明结构变量时才分配内存空间,因为此时才产生实体,此前只是定义一种类型。
四. 结构变量的初始化
一般形式如下:
//先定义,后说明的同时初始化
struct per_standard
{
int height;
double weight;
char color[10];
};
struct Stu
{
char name[10];
struct per_standard h;
int age;
char id[20];
};
int main()
{
struct Stu H={"HWY",{167,65.0,"GREEN"},20,"1120223721"};//结构体嵌套初始化
return 0;
}
//定义,说明,初始化同步进行
struct Stu
{
char name[10];
int age;
char id[20];
}H={"HWY",20,"1120223721"};
五. 结构成员的访问和赋值
“.”——以结构变量形式;“->”——以结构指针的形式。
3种形式:
//结构变量名.结构成员名
//结构指针变量名->结构成员名
//(*结构变量名).结构成员名//比较拧巴,不常用
struct per_standard
{
int height;
double weight;
char color[10];
};
struct Stu
{
char name[10];
struct per_standard h;
int age;
char id[20];
};
int main()
{
struct Stu H = {"HWY", {167, 65.0, "GREEN"}, 20, "1120223721"};
struct Stu* pH = &H;
printf("%c\n",H.name);//HWY
printf("%d\n",H.h.height);//167
printf("%d\n",(*pH).h.height);//167;有点拧巴,尽量别用;
printf("%d\n",pH->h.height);//167
return 0;
}
为结构成员赋值:
struct person
{
char name [30];
char sex;
struct date birthday;
}man;
//为结构中的字符串成员赋值
strcpy(man.name,"zhang");//注:不能写成man.name="zhang";
//为结构中的字符成员赋值
man.sex='M';
//为嵌套定义的结构中的成员分别赋值
man.birthday.year=2003;
man.birthday.month=2;
man.birthday.day=9;
//为结构变量中的数组成员的一个元素赋值
man.name[2]='o'//也是改变字符串内容的方式
结构的成员,同一般变量,可参与各种操作和运算;但对结构整体而言,只可进行赋值“=”和取地址“&”操作。(进行结构变量整体赋值,即完成对应分量的赋值,前提条件:“=”两侧的结构变量类型必须相同)。
六. 结构体传参
结论:首选传址调用,但没选择的情况下传值调用也可以。
传址调用v.s.传值调用
传址的传统优势:可以通过改变形参来改变实参;对于而传值,形参只是实参的一份临时拷贝,无法对实参造成影响。
传址调用,参数压栈的系统开销比较小,效率高。简单来说,就是传值传的是结构体变量,形参接收时要开辟同样的内存空间,导致所需开辟的空间较大;而传址传过去的是地址,即指针变量,占4/8字节空间,明显在空间上开销较小。
3.函数调用的参数压栈(pgc语言—84—33:40-end)
七. 结构体的自引用
错误的自引用方式:(细想其内存会发现是死递归,所以肯定行不通)
//err
struct N
{
int d;//4
struct N n;//4+?
};
正确的自引用方式:
//链表
struct Node
{
int data;//当前节点存放的数据
struct Node* next;//下一个节点的地址
};
注:两种错误写法:
//匿名的不行
struct
{
int data;
struct Node* next;
};
//typedef也不行,会产生这种先有鸡还是先有蛋的问题
typedef struct
{
int data;
struct Node* next;
}Node;
//这么写是正确的
typedef struct
{
int data;
struct Node* next;
}Node;
八. 结构体内存对齐(有关计算结构体的大小)
四条对齐规则:
结构体的第一个成员放在结构体变量在内存中存储位置的0偏移处开始;
从第2个成员起,都要放在对齐数(取该成员大小和默认对齐数的较小值)的整数倍的地址处;
结构体的总大小要是结构体所有成员的对齐数中最大的那个的整数倍(不够的话就浪费空间来达到)
如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处;结构体的整体大小就是所有对齐数(含嵌套结构体的对齐数)中最大的那个的整数倍。
vs的默认对齐数为8,Linux无。
举例(画图算,pgc语言—142—40:10):
struct S4//最大对齐数:8
{
double d;//8-15
char c;//16-17
int i;//20-23 (对齐数:4)
} s4;
struct S5
{
char c1;//0-1
struct S4 s4;//8-23
double d;//24-31
} s5;
int main()
{
printf("%d\n",sizeof(s5));//32
return 0;
}
内存对齐的意义:
移植原因(平台原因):不是所有硬件平台都能访问任意地址上的任意数据的,某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。(e.g.只能四个字节四个字节访问)
性能原因:数据结构(尤其是栈)应尽可能在自然边界上对齐。原因在于,为了访问未对其的内存,处理器需要做两次内存访问;而对其的内存访问仅需一次。
总而言之,结构体的内存对齐是拿空间来换取时间的做法。(随着技术的发展,空间有很大提升,而时间仍是难题,所以这种换取有实际意义)
对结构体的合理设计:让占用空间小的成员尽量集中在一起;这样既满足对齐,有最大限度节省空间。
//S1,S2类型成员一模一样,但S2所占空间更小
struct S1
{
char c1;
int i;
char c2;
};
struct S2
{
char c1;
char c2;
int i;
};
修改默认对齐数(方法如下):
#pragma pack(2)//将默认对齐数修改为2
struct S
{
char c1;//0
//1
int i;//2-5
char c2;//6
//7
};
#pragma pack()//取消设置的默认对齐数,还原为默认
int main()
{
printf("%d\n",sizeof(struct S));//8
return 0;
}
结论:结构在对齐方式不合适的时候,可自行更改默认对齐数。
offsetof宏:计算结构体中当前变量相对于首地址的偏移量
九. 位段
位段的声明:和结构类似,但有如下两个不同:
位段的成员必须是int,unsigned int,signed int或char(因为其也属于整型家族);
位段的成员名后有一个冒号和一个数字。
struct A
{
int _a:2;
int _b:5;
int _c:10;
int _d:30;
};
//A就是一个位段类型
位段的意义:合理规划,节省内存空间
int _a:2;中的2指_a成员占2个bit位的空间,有实际情况可以用到
//性别只有male,female,secret三种
//所以用int型数据表示只需两位即可,如:
male-00
female-01
secret-10
位段的大小(内存分配)
位段的空间上是按照需要以4个字节(int)或1个字节(char)的方式来开辟的。连续使用,不足再开辟新的空间。(一般情况下位段成员的类型保持相同)
struct A
{
//首先按int型开辟4个字节的空间,即32个bit位
int _a:2;//_a成员占2个bit位
//还剩30个bit位
int _b:5;//_b成员占5个bit位
//还剩25个bit位
int _c:10;//_c成员占10个bit位
//还剩15个bit位,不足了,开辟一个新的32bit位的空间
int _d:30;//_d成员占30个bit位
//所以共用32+15=47个bit位,两个字节以内
};
int main()
{
printf("%d\n",sizeof(struct A));//8
return 0;
}
位段的不可移植性
位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
vs中是从右向左分配的,会舍弃剩余不够一个成员的位,检验如下(pgc—144—43:40)
位段的应用: