文章目录
1、结构体
聚合数据类型能够同时存储超过一个的单独数据,C语言有两种这种类型,数组和结构体。数组是相同类型的集合,而结构体能包括不同的类型。
1.1、结构体的声明
struct tag
{
member-list;
}variable-list;
如果我们需要描述一个学生:
那他应该有着姓名、年龄、性别、学号这些元素。
struct stu
{
char name[20];
int age;
char sex[10];
char id[15];
}; //注意分号不能丢
1.2、特殊的声明
在声明结构体类型的时候,可以不完全的声明。
如:
以下这种不加标签(tag)的声明,叫做匿名结构体。
struct
{
char name[20];
int age;
char sex[10];
char id[15];
} x;
//这个声明创建了一个x变量,它包含四个成员变量。
struct
{
char name[20];
int age;
char sex[10];
char id[15];
} a[20],*p;
//这个声明创建了a和p,a是一个指向了20个这样结构体的数组,p作为指针指向结构体。
这次声明省去了结构体标签(tag),并且在结构体声明之后创建了变量。
值得注意的是:
这两个声明会被编译器当成两个截然不同的类型,虽然成员一样,但如果进行
p=&x操作,程序会报错,原因是它们之间的类型不同。
但是
如果给结构体加上标签,标签允许多个声明使用同一个成员列表,并且创建同一种类型的结构。
如:
struct stu
{
char name[20];
int age;
char sex[10];
char id[15];
} ;
struct stu x;
struct stu a[20], *p;
现在如果使用p=&x,这是合法的。
1.3、结构体的自引用
看下面一个代码是否正确
struct Node
{
int a;
struct Node next;
};
这显然是不正确的,如果正确那么sizeof(struct Node)将是算不清的,因为next是无限伸展的。
下面改进一下。
struct Node
{
int a;
struct Node *next;
};
通过改为指针,这样不仅能自引用,也能确定类型struct Node的大小。
我们看到结构体的类型是否有点长?
通过typedef,为结构体定义一个新的类型名,能够更加方便。
比如:
typedef struct Node
{
int a;
struct Node *next;
}Node;
通过typedef,将struct Node简化为Node。(注意Node是一个新的类型名,不是变量。)
下面看一个代码是否有问题。
typedef struct
{
int data;
Node* next;
}Node;
这段代码省略了标签,因为是先声明结构体类型再typedef定义新类型名的,所以在声明结构体类型的时候并不知道Node是什么类型,因此这个代码是错误的。
//解决方案:
typedef struct Node
{
int data;
struct Node* next;
}Node;
先声明结构体类型再typedef定义新类型名,声明没问题,类型名定义也自然没问题。
1.4、结构体变量定义和初始化
第一种:
在声明后的尾部,定义加上初始化。
struct Point
{
int x;
int y;
};
struct Node
{
int data;
struct Point p;
struct Node* next;
}n1 = {10, {4,5}, NULL};
第二种:
在声明后,通过类型+标识符的格式定义,再到=后初始化。
struct Stu
{
char name[15];
int age;
};
struct Stu s = {"zhangsan", 20};//初始化
1.5、结构体内存对齐
在结构体中,如果要求一个结构体类型的大小,那么必须考虑它的内存对齐。
struct S1
{
char c1;
int i;
char c2;
};
printf("%d\n", sizeof(struct S1)); //这个结果是12
你可能会纳闷为什么是12而不是6。
下面来看这个问题
在计算之前,我们得看看内存对齐的规则:
- 第一个成员默认从0地址处开始计算。
- 其它成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
- 对齐数 = 编译器默认(vs默认为8)的一个对齐数与该成员大小的较小值。
- 结构体总大小为最大对齐数的整数倍。
- 如果嵌套了结构体,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(包含嵌套的)的整数倍。
下面来分析
struct S1
{
//从0开始,对齐数是1,所以对齐在1的整数倍地址处,0也是1的整数倍,所以存储在0地址的位置。
char c1;
//从1开始,对齐数是4(4<8),所以对齐在4的整数倍地址处,1不是,最少4才是,所以从4地址处开始
//4~7(int 4个字节),这时候总共8个字节了。
int i;
//从8开始,对齐数也是1,所以在8地址处存储。
char c2;
};
c1、i、c2分别在0、4~7、8地址处存储,目前总共9个字节,又因为第4条规则,最大对齐数为4,所以总大小应该为12。
在1~4、9 ~ 12这些空间不使用。
下面再计算以下大小
struct S3
{
double d;//0~7 对齐数为8 (8==8),0是8的整数倍
char c; //8 对齐数为1 (1<8),8是1的整数倍
int i;//12~15 对齐数为4 (4<8),12才是4的整数倍 9不是
//16是最大对齐数8的整数倍,所以总共 16字节
};
printf("%d\n", sizeof(struct S3));//16
struct S4
{
char c1; //0 对齐数1 (1<8), 0是1的整数倍
struct S3 s3; //8~23 对齐数8 (16>8), 8是8的整数倍
double d; //24~31 对齐数8 (8==8), 24是8的整数倍
//0~31 32个字节
//32是最大对齐数8的整数倍,所以总共32字节。
};
printf("%d\n", sizeof(struct S4)); //32
为什么要有内存对齐?
大部分参考资料是这样说的:
1.平台原因
不是所有的硬件平台都能访问任意地址上任意数据的;某些硬件平台只能再某些地址处取某些特定类型的数据,否则抛出硬件异常。
2.性能原因
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。
原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
1.6、修改默认对齐数
通过#pragma pack(对齐数) 可以修改默认对齐数
如:
#pragma pack(1)//设置默认对齐数为1
struct S2
{
char c1;
int i;
char c2;
};
这样结果就是:6
1.7、结构体传参和调用
看代码
struct S
{
int data[1000];
int num;
};
struct S s = {{1,2,3,4}, 1000};
//结构体传参
void print1(struct S s)
{
printf("%d\n", s.num);
}
//结构体地址传参
void print2(struct S* ps)
{
printf("%d\n", ps->num);
}
int main()
{
print1(s); //传结构体
print2(&s); //传地址
return 0;
}
结构体传值时用 结构体变量.结构体成员名进行调用。
结构体传地址时用 结构体变量->结构体成员名进行调用。
两种传参方式哪个好点?
print1(s);
print2(&s);
答案是第二种
因为函数在传参的时候,参数是会压栈的,结构体传地址相对于传值,地址大小通常是更小的,所有时间和空间上的开销是比较小的。
所以,结论:
结构体传参的时候,要传结构体的地址。
2、位段
2.1、介绍位段
位段的声明是由结构体来实现的。
位段的特点:
- 位段的成员类型只有unsigned int、signed int、char和int。
- 位段的成员名后边有一个冒号和一个数字。
struct A
{
int _a:2;
int _b:5;
int _c:10;
int _d:30;
};
A就是一个位段。
下面通过解决问题来深入了解它。
A的大小是多少?
2.2、位段的内存分配
- 位段的成员类型只有unsigned int、signed int、char和int。
- 所以它的内存分配一次只有4个字节或者1个字节(char)。
- 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
//一个例子
struct S
{
char a:3;
char b:4;
char c:5;
char d:4;
};
struct S s = {0};
s.a = 10;
s.b = 12;
s.c = 3;
s.d = 4;
我们来看看位段里的数据存储。
首先 对于char类型,第一次先给s开辟了1个字节。
char a:3; 对应着3个bit位。
所以在第一个字节中,占了3个bit位。
char b:4; 4个bit位 第一个字节还剩5个bit位,所以也能存下。
char c:5; 5个bit位 需要第二次开辟1个字节 在一块新的1个字节空间中占5个bit位。
char d:4; 第二块1个字节空间只剩3个bit空间,所以需要再开辟一块1个字节的空间。
所以总共3个字节。
而在赋值过程中
s.10=10; 但a:3 只有3个bit位,所以1010取断3个bit位,结果为010。
s.b=12; b:4 只有4个bit位,所以1100,结果就是1100。
s.c=3; c:5 有5个bit位,11,结果为00011。
s.d=4; d:4 有4个bit位,100,结果为0100。
2.3、位段的跨平台问题
- int 位段被当成有符号数还是无符号数是不确定的。
- 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机
器会出问题。 - 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
- 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是
舍弃剩余的位还是利用,这是不确定的。
3、枚举
3.1 枚举类型的定义
枚举由关键词enum构成。
枚举也是列举。
下面是枚举的一些表示。
enum Day//星期
{
Mon,
Tues,
Wed,
Thur,
Fri,
Sat,
Sun
};
enum Sex//性别
{
MALE,
FEMALE,
SECRET
};
enum Color//颜色
{
RED,
GREEN,
BLUE
};
enum Day、enum Sex、enum Color 这些都是类型。
在{}中,都是枚举常量,一般未特定初始化的情况下,第一个枚举常量从0开始,依次增加1。
比如Mon=0,Tues=1,Wed=2…
当然这些取值是可以初始化的。
比如
这里a=1,b=2,c=3,d=200,e=201,f=202。
enum x
{
a=1,
b,
c,
d=200,
e,
f
};
3.2 枚举的优点
我们可以使用 #define 定义常量,为什么非要使用枚举?
枚举的优点:
- 增加代码的可读性和可维护性
- 和#define定义的标识符比较枚举有类型检查,更加严谨。
- 防止了命名污染(封装)
- 便于调试 (宏是不会进入调试的)
- 使用方便,一次可以定义多个常量
3.3 枚举的使用
enum Color//颜色
{
RED=1,
GREEN=2,
BLUE=4
};
int main()
{
enum Color clr = GREEN;//只能拿枚举常量给枚举变量赋值,才不会出现类型的差异。
clr = 5; //5与clr的类型不同,不能这样赋值。
return 0;
}
4. 联合(共用体)
4.1、认识联合
联合也是一种自定义类型。
联合的特点是:联合内的成员都用的是同一块内存空间,空间大小取决于大的类型。
比如
union Un
{
char c;
int i;
};
这个联合体的大小只有4字节。
4.2、使用联合
通过联合体的特点,也可以确定大小端。
union Un
{
char c;
int i;
};
int main()
{
union Un n = {0};
n.i = 0x11223344;
n.c = 0;
printf("%x", n.i); //11223300
return 0;
}
4.3、联合大小的计算
联合体大小的计算有两个规则
- 联合的大小至少是最大成员的大小。
- 当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。
union Un1
{
char c[5]; //5字节 对齐数为1
int i; //对齐数为4
};
union Un2
{
short c[7]; //14字节 对齐数为2
int i; //对齐数为4
};
//下面输出的结果是什么?
printf("%d\n", sizeof(union Un1)); //5不是4的整数倍,所以最后大小为8
printf("%d\n", sizeof(union Un2)); //14不是4的整数倍,所以最后大小为16
本章完