目录
结构体
结构体是将不同类型的数据成员组织到同一的名字之下,适合于对关系紧密、逻辑相关、具有相同或不同属性的数据进行处理
结构体变量的定义
声明结构体模板
struct 结构体名
{
数据类型 成员1的名字;
数据类型 成员2的名字;
……
数据类型 成员n的名字;
};
结构体得名字,称为结构体得标签,构成结构体得变量,称为结构体成员
注意:结构体模板只是声明了一种数据类型,定义了数据的组织形式,并未声明结构体类型的变量,因而编译器不为其分配内存
定义结构体变量
方法1:先声明结构体模板,再定义结构体变量,如果在主函数中定义结构体变量,那么这就是一个局部变量
struct A
{
char c;
int p;
};
struct A a;
方法2:在声明结构体模板的同时定义结构体变量,这种方式创建的结构体变量属于是全局变量
struct A
{
char c;
int p;
}a;
由于结构体标记是可选的,即也可不出现结构体名
struct
{
char c;
int p;
}a;
所以上面这种方法也是可行的,但是这种方法因为未指定结构体标签,不能再在程序的其他处定义结构体变量,因而不常用
tepedef定义数据类型
关键字typedef用于为系统固有的或程序员自定义的数据类型定义一个别名
typedef struct A
{
char c;
int p;
}TEST;
//下面两句是等价的
TEST a, b;
struct A a,b;
注意,typedef只是为一种已存在的类型定义一个新的名字而已,并没有定义一种新的数据类型
结构体变量的初始化
struct Stu
{
char name[20];
int age;
char id[5];
};
struct Stu z = { "李华",18,"1234" };
嵌套的结构体
struct A
{
char c;
int p;
};
struct Stu
{
struct A a;
char name[20];
int age;
char id[5];
};
struct Stu z = { {'z',123}, "李华",18,"1234" };
结构体变量的引用
访问结构体变量的成员必须使用成员选择运算符(也称圆点运算符),格式如下:
结构体变量名.成员名
当出现结构体嵌套的时候,必须以级联方式访问结构体成员,即通过成员选择运算符逐级找到最底层的成员时再引用
如上面例子中当需要访问字符c的内容时:
z.a.c
注意:C语言允许对相同结构体类型的变量进行整体赋值,但是并非所有的结构体成员都是可以使用赋值运算符来赋值,对字符数组类型的结构体成员进行赋值时,必须使用字符串处理函数strcpy()
以上面的结构体为例:
struct Stu z = { {'z',123}, "李华",18,"1234" };
struct Stu s;
s = z;
strcpy(s.name, z.name);
s.age = z.age;
结构体变量的地址是结构体变量所占内存空间的首地址,而结构体成员的地址值与结构体成员在结构体中所处的位置及该成员所占内存的字节数相关
结构体所占内存的字节数
typedef struct sample
{
char m1;
int m2;
char m3;
}SAMPLE;
int main()
{
SAMPLE s = {'a', 2, 'b'};
printf("%d\n", sizeof(s));//打印结构体类型所占内存字节数
return 0;
}
运行结果:
12
理论上来说不应该是1+4+1=6吗,为什么会是12呢?这是因为对多数计算机系统而言,为了提高内存寻址的效率,很多处理器体系结构为特定的数据类型引入了特殊的内存对齐需求,不同的系统和编译器的内存对齐方式会有所不同,为了满足处理器的对其要求,可能会在较小的成员后加入补位,从而导致实际所占内存字节数会比我们想的要多一些
32位体系中,short型数据要求从偶数地址开始存放,而int型数据则被对齐在4字节地址边界
按照这类计算机的体系结构要求,s的成员变量m1和m3的后面都要增加3个字节的补位,以达到与成员变量m2内存地址对齐的要求
总之,系统为结构体变量分配内存的大小,或者说结构体类型所占内存的字节数,并非是所有成员所占内存字节数的总和,它不仅与定义的结构体类型有关,还与计算机系统本身有关。由于结构体变量的成员的内存对齐方式和数据类型所占内存的大小都是与机器相关的,因此结构体在内存中所占的字节数也是与机器相关的
结构体内存对齐规则
1.根据定义顺序,第一个成员放在内存偏移量为0的位置
2.之后的每一个成员变量都要对齐到对齐数的整数倍的位置再开始对齐,对齐数就是编译器默认对齐数与该成员大小中的较小值(vs默认为8)
3.如果使用了嵌套结构体,那么嵌套得结构体就要对齐到自己的最大对齐数的整数倍的位置再对齐
4.结构体的总大小就是最大对齐数的整数倍
struct sample
{
short i;//2
float f;//4
char ch;//1
};
01__45678___
↑short ↑char
↑float
4*3=12
struct sample
{
short i;
char ch;
float f;
};
012_4567
↑short
↑char
↑float
4*2=8
所以,结构体的定义方式不同所占的内存大小也是不一样的
struct p
{
char a;
struct s S;//假设内存大小为16字节
double b;
return 0;
};
上面例子中,外层的结构体的大小是32,因为a的大小是1,根据对齐规则,S要对在对齐数的整数倍的位置,S的对齐数是16和8中的较小值,所以会对在8处,b又会对在24处,占8个字节,所以整体的大小是32字节
内存对齐的原因
平台原因(移植原因):不是所有的硬件平台都能访问任一地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要两次内存访问;而对齐的内存访问仅需要一次访问
两次内存访问是怎么来的?
假如一个结构体第一个成员时char,第二个成员是int,以32位机器为例,一次访问4个字节,如果不对齐的话,那就是自然排序,char后面就直接跟着int,如果想访问int,那么第一次访问四个字节,会读取到char的一个字节和int的三个字节,需要再次读取四个字节,才能读到int的第四个字节,所以说两次才能读取完成
修改默认对齐数
修改默认对齐数,就需要我们是用预处理命令pragma
#pragma pack(4)//设置默认对齐数为4
struct S
{
char c;
double d;
};
#pragma pack()//取消设置的默认对齐数
如此,定义上面这个结构体之后的大小,就是12了
offsetof()函数
<stddef.h>
size_t offsetof(structName, memberName)
函数作用
计算结构体成员相对于结构体起始位置的偏移量
struct S
{
char c;
int i;
double d;
};
int main()
{
printf("%d\n", offsetof(struct S, c));//注意是结构体名,不是结构体变量名
printf("%d\n", offsetof(struct S, i));
printf("%d\n", offsetof(struct S, d));
return 0;
}
运行结果:
0
4
8
结构体数组的定义和初始化
struct A
{
char c;
int p;
};
typedef struct Stu
{
struct A a;
char name[20];
int age;
char id[5];
}STUDENT;
STUDENT stu[30]={{{'z',123}, "李华",18,"1234"},{{'x',456},"张三",18,"5678"}}};
结构体指针的定义和初始化
指向运算符,也称箭头运算符
指向结构体的指针变量名->成员名
struct A
{
char c;
int p;
};
typedef struct Stu[30]
{
struct A a;
char name[20];
int age;
char id[5];
}STUDENT;
STUDENT stu={ {'z',123}, "李华",18,"1234" };
STUDENT *pt = stu;//等价于STUDENT *pt = &stu[0]; 等价于STUDENT *pt;pt = stu;
printf("%c %d %s %d %s\n", pa->a.c, pa->a.p, pa->name, pa->age, pa->id);
向函数传递结构体
(1)用结构体的单个成员作为函数参数,向函数传递结构体的单个成员
(2)用结构体变量作函数参数,向函数传递结构体的完整结构
struct date
{
int year;
int month;
int day;
};
void func(struct date p)//结构体变量作函数形参
{
p.year = 2000;
p.month = 5;
p.day = 22;
}
int main()
{
struct date d;
d.year = 1999;
d.month = 4;
d.day = 23;
printf("%d/%02d/%d\n", d.year, d.month, d.day);
func(d);//结构体变量作为函数实参,传值调用
printf("%d/%02d/%d\n", d.year, d.month, d.day);
return 0;
}
运行结果:
1999/04/23
1999/04/23
向函数传递结构体变量时,实际传递给函数的是该结构体变量成员值得副本,这就意味着结构体变量的成员值是不能在被调函数中被修改的。仅当将结构体的地址传递给函数时,结构体变量的成员值才可以在被调函数中被修改。
(3)用结构体指针或结构体数组作函数参数,向函数传递结构体的地址
struct date
{
int year;
int month;
int day;
};
void func(struct date* pt)//结构体指针变量作函数形参
{
pt->year = 2000;
pt->month = 5;
pt->day = 22;
}
int main()
{
struct date d;
d.year = 1999;
d.month = 4;
d.day = 23;
printf("%d/%02d/%d\n", d.year, d.month, d.day);
func(&d);//结构体变量的地址作为函数实参,传址调用
printf("%d/%02d/%d\n", d.year, d.month, d.day);
return 0;
}
运行结果:
1999/04/23
2000/05/22
结构体除了可作为函数形参的类型以外,还可以作为函数返回值的类型
struct date
{
int year;
int month;
int day;
};
struct date func(struct date p)//函数的返回值为结构体类型
{
p.year = 2000;
p.month = 5;
p.day = 22;
return p;//从函数返回结构体变量的值
}
int main()
{
struct date d;
d.year = 1999;
d.month = 4;
d.day = 23;
printf("%d/%02d/%d\n", d.year, d.month, d.day);
d = func(d);//函数返回值为结构体变量的值
printf("%d/%02d/%d\n", d.year, d.month, d.day);
return 0;
}
运行结果:
1999/04/23
2000/05/22
函数调用的参数压栈
写一个函数,可以打印结构体里面的内容
struct A
{
char c;
int p;
};
struct Stu
{
struct A a;
char name[20];
int age;
char id[5];
};
void print1(struct Stu a)
{
printf("%c %d %s %d %s\n", a.a.c, a.a.p, a.name, a.age, a.id);
}
void print2(struct Stu* pa)
{
printf("%c %d %s %d %s\n", pa->a.c, pa->a.p, pa->name, pa->age, pa->id);
}
int main()
{
struct Stu s = { {'z',123}, "李华",18,"1234" };
//函数传参时,参数时需要压栈的,如果传递一个结构体对象的时候,结构体过大,参数压栈的系统开销比较大,所以会导致性能的下降
print1(s);//传值调用,形参也需要创建等大空间来接受数据,容易造成空间浪费和时间浪费,但是不会修改原有数值,所以安全性更改、高一些
print2(&s);//传址调用,只需要产生4或8字节空间接收地址,效率更高,且可以进行修改数值,如果担心数值被修改,可以在函数定义中使用const修饰
return 0;
}
函数调用的参数压栈:
栈,是一种数据结构,特点:先进后出,后进先出
压栈:不断的向栈中放数据
出栈:从栈中删除数据,假如一共放入了5个数据,当我们想要删除第3个数据时,必须先把第5,4个数据删除
int add(int x, int y)//④在栈区中再开辟一块空间,在add的栈区中创建z的空间,访问a',b'的空间,进行相加计算,得到z的值,之后将z的值返回到c中去,此时调用结束,add,a',b'空间全部释放,
{
int z =0;
z = x + y;
return z;
}
int main()//①每一个函数的调用都会在内存的栈区上开辟一块空间
{
int a = 3, b = 5,c=0;//②在main函数栈区空间中放入a,b,c
c = add(a, b);//③函数传参,一般从右向左传,在栈区其他位置依次创建b'的空间,a'的空间,参数传参,就是压栈操作
return 0;
}
共用体
共用体,也称为联合,是将不同类型的数据组织在一起共同占用同一段内存的一种构造数据类型,共用体和结构体的声明方法类似,只是关键字变为union
共用体所占内存的字节数
union sample
{
short i;
char ch;
float f;
};
typedef union sample SAMPLE;
int main()
{
printf("%d\n", sizeof(SAMPLE));
return 0;
}
运行结果:
4
如果把上面的union改为struct,那么结果就会变成8
虽然共用体与结构体都是不同类型的数据组织在一起,但与结构体不同的是,共用体是从同一起始地址开始存放成员的值,即共用体中不同类型的成员公用同一段内存单元,因此必须有足够大的内存空间来存储占据内存空间最多的那个成员,所以共用体类型所占内存空间的大小取决于其成员中占内存空间最多的那个成员变量
C语言规定,共用体采用与开始地址对齐的方式分配内存空间,如上,成员i占2字节,ch占1字节,f占4字节,于是f的前1个字节就是为ch分配的内存空间,前2个字节就是为i分配的内存空间。共用体使用覆盖技术来实现内存的共用,即当对成员f进行赋值操作时,成员i的内容将被改变,于是i就失去其自身的意义
所以,共用体的大小至少是最大成员的大小,当最大成员的大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍
union Un
{
int a;
char arr[5];
};
上面这个例子中,a的对齐数为4(4和8中取较小值)大小为4,arr的对齐数是1,大小为5,所以最大对齐数是4,而5不是4的倍数,所以共用体的大小是8字节(会浪费3字节)
正是因为公用内存,所以共用体和其中所有成员的地址都是相同的
在每一瞬间起作用的成员就是最后一次被赋值的成员,所以不能为共用体的所有成员同时进行初始化,此外,共用体不能进行比较操作,也不能作为函数参数
SAMPLE u = {.ch = 'a' };
SAMPLE num;
num.i = 20;
共用体的使用情景
采用共用体存储程序中逻辑相关但是情形互斥的变量,使其共享内存空间的好处除了可以节省内存空间以外,还可以避免因操作失误引起逻辑上的冲突
示例1:未婚,已婚,离婚
struct date
{
int year;
int month;
int day;
};
struct marriedState//定义已婚结构体类型
{
struct date marryDay;
char sposeName[20];//配偶姓名
int child;//子女数量
};
struct divorceState//定义离婚结构体类型
{
struct date divorceDay;
int child;
};
union maritalState
{
int single;//未婚
struct marriedState;//已婚
struct divorceState;//离婚
};
struct person
{
char name[20];
char sex;
int age;
union maritalState marital;
int marryFlag;//婚姻状况标记,根据该数值来解释内存中婚姻状况的类型,如将0记为未婚,1为已婚,2为离婚等
};
示例2:共用体的主要应用是有效使用存储空间,此外还可以用于构造混合的数据结构
typedef union
{
int i;
float f;
}NUMBER;
NUMBER arr[100];
每个NUMBER类型的数组arr的数组元素都有两个成员,既可以存储int型数据,也可以存储float型数据
位段
位段的定义
位段的声明和结构是类似的,有两个不同:
1.位段的成员必须是int,unsigned int, signed int, short或char(通常要是int就全是int,要是char就全是char)
2.位段的成员名后边有一个冒号和一个数字
struct S
{
int a : 2;
int b : 5;
int c : 10;
int d : 30;
};
位段的内存分配
struct S
{
int a : 2;
int b : 5;
int c : 10;
int d : 30;
};
int main()
{
struct S s;
printf("%d\n",sizeof(s));//8个字节
return 0;
}
位段,“位”指二进制位。假如只需要表示0,1,2,3这四个数,那么只需要2个比特位就可以了,不需要32个比特位,所以位段中冒号后面的数字,指的是所需要的比特位
上述位段只需要47个比特位,即6个字节,但是由于其内存分配的方式,所以是8个字节
内存分配方式
1.位段的成员可以是int,unsigned int, signed int,short或char(属于整型家族)类型
2.位段的空间是按照需要以4个字节(int)或者1个字节(char)的方式来开辟的
3.位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段
上面那个问题实际上就是,先开辟4个字节,即32个比特位,依次分配给abc,还剩15个,但是不够给d,所以重新开辟4个字节(之前开辟剩余的15个浪费掉),32个比特位中30个分配给d,这样就是8个字节的内存大小了
struct S
{
char a : 3;
char b : 4;
char c : 5;
char d : 4;
};
int main()
{
struct S s = { 0 };
s.a = 10;//1010 只能存三个比特位,即010
s.b = 20;//10100 -> 0100
s.c = 3;//11 -> 00011
s.d = 4;//100 -> 0100
//所以假设一个字节中,从右向左分配,上面这个位段内存是这样的:
//00100010 00000011 00000100
//由于内存中以16进制表示,所以需要将上述2进制转换为16进制(4转1)
//2 2 0 3 0 4
//经过调试,可以看到&s内存显示为22 03 04,所以以上对于vs编译器中段位的分配猜想是正确的
return 0;
}
位段的跨平台问题
1.int位段被当成有符号数还是无符号数是不确定的
2.位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机器会出问题
3.位段中成员在内存中从左向右分配,还是从右向左分配标准尚未定义
4.当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的
所以,跟结构相比,位段可以达到同样的效果,可以很好的节省空间,但是有跨平台的问题存在
位段的应用
例如,网络上数据的传输,如果数据包的封装用结构体的话会导致很多的内存浪费,使用位段的话,4+4+8+16等等正好就是32个比特位
枚举
枚举类型的定义
enum 枚举类型
{
枚举的可能取值-常量
};
enum Sex
{
Male,
Female,
Secret
};
枚举类型的讲解
1.
enum Sex
{
Male,
Female,
Secret
};
int main()
{
enum Sex s = Male;//只能赋值三者之一,不能赋其他值
printf("%d %d %d\n", Male, Female, Secret);
printf("%d", sizeof(s));
return 0;
}
运行结果:
0 1 2
4
2.
enum Sex
{
Male='a',
Female,
Secret
};
int main()
{
enum Sex s = Male;//只能赋值三者之一,不能赋其他值
printf("%d %d %d\n", Male, Female, Secret);
printf("%d", sizeof(s));
return 0;
}
运行结果:
97 98 99
4
3.
enum Sex
{
Male=2,//由于是常量,所以是不允许更改的,但这里是初始化,初始化完成后就不能再从其他地方更改其值了
Female,
Secret
};
int main()
{
printf("%d %d %d\n", Male, Female, Secret);
return 0;
}
运行结果:
2 3 4
4.
enum Sex
{
Male = 1,
Female = 3,
Secret = 5
};
int main()
{
printf("%d %d %d\n", Male, Female, Secret);
return 0;
}
运行结果:
1 3 5
5.
enum Sex
{
Male = 1,
Female,
Secret = 5
};
int main()
{
printf("%d %d %d\n", Male, Female, Secret);
return 0;
}
运行结果:
1 2 5
6.
enum Sex
{
Male,
Female = 3,
Secret
};
int main()
{
printf("%d %d %d\n", Male, Female, Secret);
return 0;
}
运行结果:
0 3 4
枚举类型的优点
1.增加代码的可读性和可维护性
2.和#define定义的标识符比较,枚举有类型检查,更加严谨
C语言的源代码--预编译--编译--链接--可执行程序
3.防止了命名污染(封装)
4.便于调试
5.使用方便,一次可以定义多个常量
判断当前计算机的大小端存储方法2
int check_sys()
{
union Un
{
char c;
int i;
}u;
u.i = 1;
return u.c;
}