- 数组是一系列相同类型元素的集合
- 结构体是一系列类型可能不同的元素的集合
比如我们想描述一个人,仅仅用C语言中的类型是无法准确描述的,这就需要我们自定义一个类型
struct Student
{
char name[20];//姓名
int age;//年龄
char sex[5];//性别
float score;//分数
};//分号不能缺失
struct
{
char name[20];
int age;
char sex[5];
float score;
}stu2;
这种没有结构体标签的声明叫做匿名结构体;这种结构体只能使用一次,也就是说,只能使用变量列表中的变量,不能用匿名结构体去创建新的变量
struct
{
char a;
int b;
}s;//匿名结构体变量s
struct
{
char a;
int b;
}* p;//匿名结构体指针变量p
int main()
{
p = &s;
return 0;
}
上述代码是否正确?
编译器会给我们警告,p和&s的类型不兼容
所以,即使两个匿名结构体的成员变量一样,编译器也会认为两个结构体是不同的类型
一个结构体中包含一个自身的指针,叫做结构体的自引用
struct Node
{
int val;
struct Node* n;
};
错误的自引用:
typedef struct Node
{
int val;
Node* n;
//typedef是将整个结构体重命名为Node
//而结构体还没创建出来就使用了Node,因此这种自引用方式是错误的
}Node;
struct Student
{
char name[20];
int age;
char sex[5];
float score;
};
int main()
{
struct Student stu1;//定义
struct Student stu2 = { "baiyahua",19,"男",20.2 };//初始化
printf("%s %d %s %.2f", stu2.name, stu2.age, stu2.sex, stu2.score)
return 0;
}
一个结构体的大小该怎么计算呢?是我们想的那样,将每个结构体成员的大小相加吗?
实际上,结构体的成员在分配内存时,需要遵守内存对齐规则,该规则如下:
- 第一个成员要在地址偏移量为0的地址处
- 之后的每个成员的位置都必须是对齐数的整数倍
- 对齐数 = 编译器默认对齐数与该成员的大小中的较小值
- VS的默认对齐数是8
- Linux中没有对齐数的概念,所以对齐数就是成员本身的大小
- 结构体的总大小必须是最大对齐数的整数倍
- 对于嵌套的结构体,需要对齐到自己最大对齐数的整数倍处;整个结构体的大小是最大对齐数的整数倍
//练习1
struct S1
{
char c1;
int i;
char c2;
};
//练习2
struct S2
{
char c1;
char c2;
int i;
};
//练习3
struct S3
{
double d;
char c;
int i;
};
//练习4-结构体嵌套问题
struct S4
{
char c1;
struct S3 s3;
double d;
};
int main()
{
printf("%zd\n", sizeof(struct S1));// 12
printf("%zd\n", sizeof(struct S2));// 8
printf("%zd\n", sizeof(struct S3));// 16
printf("%zd\n", sizeof(struct S4));// 32
return 0;
}
练习1:
练习2和练习3同理
练习4:
平台原因:
不是所有的平台都能访问任意地址处的内容的;某些平台只能访问特定地址处的内容
性能原因:
有时为了访问没有对齐的内存,处理器需要做两次处理,而对齐了的内存,处理器只需做一次处理
可以看到,为了内存对齐,会相应的浪费一些空间,那怎么能在内存对齐的情况下,尽量减小空间的浪费呢?
我们在定义结构体时,让占用空间小的成员尽量集中在一起,就比如上面的S1和S2
对于S1,两个char类型的变量分开了;对于S2,两个char类型的变量在一起,其结构体的大小比S1小
总的来说,结构体的内存对齐是用空间换取时间的做法
对于不同的编译器,默认对齐数是不同的,而我们也可以通过代码的形式修改默认对齐数
#pragma pack(1)//设置默认对齐数为1
struct S2
{
char c1;
int i;
char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认
int main()
{
printf("%d\n", sizeof(struct S2));// 6
return 0;
}
struct S
{
int arr[1000];
char c;
};
void print1(struct S s)
{
printf("%d %d %d %d %d %c\n", s.arr[0], s.arr[1], s.arr[2], s.c);
}
void print2(const struct S* s)
{
printf("%d %d %d %d %d %c\n", s->arr[0], s->arr[1], s->arr[2], s->c);
}
int main()
{
struct S s = { {1,2,3},'a' };
print1(s);//传值调用
print2(&s);//传址调用
return 0;
}
对于上面的两种调用方式,都能完成我们的任务,大家觉得哪种好?
我们的结构体大小可能是很大的,这种情况下使用传值调用,还得再开辟一块一样大的空间,对于栈帧的消耗是非常大的;而使用传址调用只需要开辟一个指针大小的空间,大大降低了时间和空间
并且对参数进行const修饰,也能避免结构体内容被修改的情况
因此在对结构体进行传参时,推荐使用传址调用
位段与结构体的定义相似,但又有所不同:
- 位段的成员类型必须都是char,unsigned char,int,unsigned int中的一种
- 位段的成员变量后要加上冒号和数字
struct A
{
char _a : 3;
char _b : 5;
char _c : 2;
};
int main()
{
printf("%zd\n", sizeof(struct A));// 2
return 0;
}
位段成员变量的内存开辟是按需所取,即先开辟一个类型的空间,如果不够,再往下开辟
对于上面的位段A,先开辟1字节的空间,其中有3bit位给了_a,还剩5bit;正好够_b,就把剩余的5bit给了_b;再另外开辟1字节,其中2bit位给了_c,所以位段A的大小就是2字节
但是位段的大小计算还存在一些问题:
- 分配给成员变量bit位时是从左往右分配还是从右往左分配,这是未确定的
- 剩余的bit位不够下一个变量存放时,是先用完剩余的bit位,再开辟新的空间,还是直接舍弃掉剩余的bit位,直接开辟新的空间,这也是未确定的
不同的平台,上面的规则需要我们去验证;对于VS,我们可以验证一下符合上面的哪种规则
struct S
{
char a : 3;
char b : 4;
char c : 5;
char d : 4;
};
int main()
{
struct S s = { 0 };
s.a = 10;
s.b = 12;
s.c = 3;
s.d = 4;
printf("%zd\n", sizeof(s));// 3
return 0;
}
我们假设在VS中,内存分配时是从右往左分配的;并且当剩余的bit位不够下一个变量存放时,会舍弃剩余的bit位,开辟新的空间存放
如果真的如我们假设所想,那么结构体的大小就是3字节,内存中的数据以十六进制打印出来就是62,03,04
经过验证,确实如我们所想,在VS环境下,位段的内存分配规则是从右往左使用空间;剩余空间不够存放变量时,会另外开辟新的空间
- int,char被当作有符号还是无符号类型是不确定的
- 位段中的成员变量最大位的数目是不确定的(16位机器下最大位数是16,32位机器下最大位数是32);假设在32位机器下的变量大小为28,那么移植到16位机器下就会出问题
- 内存分配是从右往左还是从左往右是不确定的
- 剩余位数无法容纳下一个变量时,是用完剩余的位数再开辟空间,还是浪费剩余的位数直接开辟空间,这是不确定的
总的来说,位段与结构体相比,能节约空间,但存在不少的跨平台问题
位段在网络中应用的比较多,比如IP数据包;日常生活中,我们用微信向他人发送了一条信息,别人是怎么接收到我们的信息的?实际上,信息还会被各种东西包装,形成数据包;数据包中有发送人的IP地址,接收人的IP地址…其中有些数据很小,是完全不需要char或int来存储,这时就用到我们的位段了,如果用结构体存储,数据包就会比较大,在网络中流动的也就慢
生活中,某些事物的取值是有限个,能被一一列举出来,比如三原色,一周的天数…这时就可以考虑使用枚举
枚举,就是一一列举
//三原色
enum Color
{
GREEN,
RED,
BLUE
};
当然,成员是可以赋值的,如果不赋值,默认从0开始递增;一个成员被赋值,下面没赋值成员的值递增
int main()
{
printf("%d\n", GREEN);// 0
printf("%d\n", RED);// 1
printf("%d\n", BLUE);// 2
return 0;
}
//三原色
enum Color
{
GREEN=3,
RED,
BLUE
};
int main()
{
printf("%d\n", GREEN);// 3
printf("%d\n", RED);// 4
printf("%d\n", BLUE);// 5
return 0;
}
枚举中的成员都是常量,定义完我们就可以直接拿来使用
另外,我们最好拿枚举常量给枚举变量赋值,如果给枚举常量赋值其他类型,有些编译器是不会报错的,但这样枚举就失去了意义,要赋整型值,为什么不直接定义整型变量呢?况且,用枚举常量给枚举变量赋值能大大增加代码的可读性,一看就知道变量的含义是什么
enum Color
{
GREEN,
RED,
BLUE
};
int main()
{
enum Color color = GREEN;
printf("%d\n", color);// 0
color = RED;
printf("%d\n", color);// 1
//不好的写法
color = 5;
printf("%d\n", color);// 5
return 0;
}
为什么使用枚举?我们用#define也能定义常量
#define GREEN 0
#define RED 1
#define BLUE 2
int main()
{
printf("%d\n", GREEN);// 0
printf("%d\n", RED);// 1
printf("%d\n", BLUE);// 2
return 0;
}
枚举的优点:
- 增加代码的可读性和可维护性
- 便于调试,因为预处理在编译期间会完成常量的替换
- 枚举定义的变量有类型检查,比#define更加严谨
- 使用方便,一次定义多个变量
联合体,顾名思义,它的特征是成员公用同一块空间
//联合体的定义
union S
{
int a;
char b;
};
int main()
{
printf("%zd\n", sizeof(union S));// 4
return 0;
}
怎么确定联合体的成员公用一块空间呢?我们可以通过成员的地址来验证
可以发现,联合体s的地址,联合体的成员a,b的地址,三者是一样的,也就是说变量b和a使用了同一块空间
题目:利用联合体判断当前编译器是大端字节序存储还是小端字节序存储
union S
{
int a;
char b;
}s;
int main()
{
s.a = 0x00000001;
if (s.b == 0x00)
printf("大端\n");
else
printf("小端\n");
return 0;
}
- 联合体的大小至少是最大成员的大小
- 当最大成员的大小不是对齐数的整数倍时,就要对齐到对齐数的整数倍处
union Un1
{
char c[5];
int i;
};
union Un2
{
short c[7];
int i;
};
int main()
{
//下面输出的结果是什么?
printf("%d\n", sizeof(union Un1));// 8
printf("%d\n", sizeof(union Un2));// 16
return 0;
}
关于自定义类型就讲到这!