目录
1. 结构体
1.1 结构体的声明
struct tag
{
member-list;
}variable-list;
- 仅声明结构体类型,不定义结构体变量
struct Person
{
char name[20];
char tele[20];
char sex[3];
int age;
};
- 声明结构体类型的同时,定义结构体变量
struct Book
{
char bookName[20];
char authorName[20];
int publishYear;
}b1, b2;
1.2 匿名结构体
在声明结构体的时候,可以不完全的声明。比如省略结构体标签(tag),就称为匿名结构体。匿名结构体变量只能在声明结构体类型的同时被定义。
struct
{
struct Person p; // 其他结构体作为该结构体的成员
char id[20]; // 学号
double score; // 成绩
}x; // 声明匿名结构体的同时定义匿名结构体变量x
struct
{
struct Person p;
char id[20];
double score;
}*p; // 声明匿名结构体的同时定义匿名结构体指针p
在上面代码的基础上,下面的代码是非法的。编译器会把上面的两个声明当成完全不同的两个类型。
p = &x; // err
1.3 结构体的自引用
// err
struct Node
{
int data;
struct Node next;
};
// ok
struct Node
{
int data;
struct Node* next;
};
// err
typedef struct Node
{
int data;
Node* next;
}Node;
// ok
typedef struct Node
{
int data;
struct Node* next;
}Node; // 把struct Node类型重命名为Node
1.4 结构体变量的定义和初始化
- 定义
struct Person
{
char name[20];
char tele[20];
char sex[3];
int age;
}p1, p2; // 声明结构体类型的同时定义结构体变量p1、p2
struct Peo p3; // 定义结构体变量p3
struct Student
{
struct Person p;
char id[20];
double score;
}s1; // 声明结构体类型的同时定义结构体变量s1
- 初始化
struct Person
{
char name[20];
char tele[20];
char sex[3];
int age;
};
// 顺序初始化
struct Person p1 = { "黄蓉","123456","女",35 };
// 指定初始化(C99)
struct Person p2 = { .sex = "女",.tele = "666",.age = 20,.name = "赵敏" };
// 结构体变量的成员全初始化为0
struct Person p3 = { 0 };
struct Student
{
struct Person p;
char id[20];
double score;
};
// 顺序初始化
struct Student s1 = { { "乔峰","987654321","男",30 },"101",99.999 };
// 指定初始化(C99)
struct Student s2 = { .id = "777",.score = 60.0,.p = { "慕容复","415784814","男",30 } };
// 结构体变量的成员全初始化为0
struct Student s3 = { 0 };
1.5 结构体内存对齐
1.5.1 结构体内存对齐的规则
- 第一个成员在与结构体变量偏移量为0的地址处。
- 其他成员变量要对齐到对齐数的整数倍的地址处。对齐数是编译器默认的一个对齐数与该成员大小的较小值。VS中默认的值为8。
- 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
- 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
1.5.2 结构体内存对齐的示例
#include <stddef.h>
size_t offsetof(type, member)
// offsetof是一个宏,用于求结构体中一个成员在该结构体中的偏移量
示例1
#include <stdio.h>
#include <stddef.h>
struct S1
{
char c1; // 对齐数=min{8,1}=1
int i; // 对齐数=min{8,4}=4
char c2; // 对齐数=min{8,1}=1
}; // 最大对齐数=max{1,4,1}=4
int main()
{
printf("c1的偏移量:%d\n", offsetof(struct S1, c1));
printf("i 的偏移量:%d\n", offsetof(struct S1, i));
printf("c2的偏移量:%d\n", offsetof(struct S1, c2));
printf("结构体的大小:%d\n", sizeof(struct S1));
return 0;
}
示例2
#include <stdio.h>
#include <stddef.h>
struct S2
{
char c1; // 对齐数=min{8,1}=1
char c2; // 对齐数=min{8,1}=1
int i; // 对齐数=min{8,4}=4
}; // 最大对齐数=max{1,1,4}=4
int main()
{
printf("c1的偏移量:%d\n", offsetof(struct S2, c1));
printf("c2的偏移量:%d\n", offsetof(struct S2, c2));
printf("i 的偏移量:%d\n", offsetof(struct S2, i));
printf("结构体的大小:%d\n", sizeof(struct S2));
return 0;
}
示例3
#include <stdio.h>
#include <stddef.h>
struct S3
{
double d; // 对齐数=min{8,8}=8
char c; // 对齐数=min{8,1}=1
int i; // 对齐数=min{8,4}=4
}; // 最大对齐数=max{8,1,4}=8
int main()
{
printf("d的偏移量:%d\n", offsetof(struct S3, d));
printf("c的偏移量:%d\n", offsetof(struct S3, c));
printf("i的偏移量:%d\n", offsetof(struct S3, i));
printf("结构体的大小:%d\n", sizeof(struct S3));
return 0;
}
示例4
#include <stdio.h>
#include <stddef.h>
struct S3
{
double d; // 对齐数=min{8,8}=8
char c; // 对齐数=min{8,1}=1
int i; // 对齐数=min{8,4}=4
}; // 最大对齐数=max{8,1,4}=8
struct S4
{
char c; // 对齐数=min{8,1}=1
struct S3 s3; // struct S3的最大对齐数=8
double d; // 对齐数=min{8,8}=8
}; // 最大对齐数=max{1,8,8}=8
int main()
{
printf("c 的偏移量:%d\n", offsetof(struct S4, c));
printf("s3的偏移量:%d\n", offsetof(struct S4, s3));
printf("d 的偏移量:%d\n", offsetof(struct S4, d));
printf("结构体的大小:%d\n", sizeof(struct S4));
return 0;
}
1.5.3 为什么存在结构体内存对齐
- 平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
- 性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
结构体内存对齐是用空间换时间的做法。
在设计结构体的时候,既要满足对齐,又要节省空间,就要让占用空间小的成员尽量集中在一起。
struct S1
{
char c1; // 对齐数=min{8,1}=1
int i; // 对齐数=min{8,4}=4
char c2; // 对齐数=min{8,1}=1
}; // 最大对齐数=max{1,4,1}=4
// sizeof(struct S1)=12
struct S2
{
char c1; // 对齐数=min{8,1}=1
char c2; // 对齐数=min{8,1}=1
int i; // 对齐数=min{8,4}=4
}; // 最大对齐数=max{1,1,4}=4
// sizeof(struct S2)=8
struct S1和struct S2类型的成员一模一样,但是所占空间的大小不同。
1.5.4 修改默认对齐数
#pragma pack(1) // 设置默认对齐数为1
struct S1
{
char c1;
int i;
char c2;
};
#pragma pack() // 取消设置的默认对齐数,还原为默认
结构在对齐方式不合适的时候,我么可以自己更改默认对齐数。
1.6 结构体成员的访问
- 结构体变量.成员变量
- 结构体指针->成员变量
#include <stdio.h>
struct Book
{
char bookName[20];
char authorName[20];
int publishYear;
};
struct Novel
{
struct Book b;
char type[10];
char length[10];
};
int main()
{
struct Novel n = { { "射雕英雄传","金庸",1957 },"武侠","长篇" };
struct Novel* p = &n;
printf("%s %s %d %s %s\n", n.b.bookName, n.b.authorName, n.b.publishYear, n.type, n.length);
printf("%s %s %d %s %s\n", (*p).b.bookName, (*p).b.authorName, (*p).b.publishYear, (*p).type, (*p).length);
printf("%s %s %d %s %s\n", p->b.bookName, p->b.authorName, p->b.publishYear, p->type, p->length);
return 0;
}
1.7 结构体作为函数参数
#include <stdio.h>
struct Book
{
char bookName[20];
char authorName[20];
int publishYear;
};
struct Novel
{
struct Book b;
char type[10];
char length[10];
};
void print1(struct Novel n)
{
printf("%s %s %d %s %s\n", n.b.bookName, n.b.authorName, n.b.publishYear, n.type, n.length);
}
void print2(struct Novel* p)
{
printf("%s %s %d %s %s\n", p->b.bookName, p->b.authorName, p->b.publishYear, p->type, p->length);
}
int main()
{
struct Novel n = { { "射雕英雄传","金庸",1957 },"武侠","长篇" };
print1(n); // 传结构体变量
print2(&n); // 传结构体指针
return 0;
}
结构体传参最好传结构体的地址(结构体指针)。因为函数传参的时候,参数是需要压栈的,会有时间和空间上的系统开销。如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。
2. 位段
位段又称位域,C语言允许在一个结构体中以位为单位来指定其成员所占内存长度。
2.1 位段的声明
位段的声明与结构体类似,不同之处在于:
- 位段成员名后加一个冒号和一个整数,整数规定了成员所占用的位数,且不能超过成员名类型的大小。
- C99之前,位段的成员必须是(signed/unsigned) int、(signed/unsigned) char,C99支持bool类型作为位段的成员类型。
struct S1 // 结构体
{
int a;
int b;
int c;
int d;
};
struct S2 // 位段
{
int a : 2; // a占2bit
int b : 5; // b占5bit
int c : 10; // c占10bit
int d : 30; // d占30bit
};
2.2 位段的内存分配
- 位段的空间上是按照需要以4个字节(int)或者1个字节(char/bool)的方式来开辟的。
- 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
示例1
#include <stdio.h>
struct S
{
int a : 2;
int b : 5;
int c : 10;
int d : 30;
};
int main()
{
printf("%d\n", sizeof(struct S)); // 8
return 0;
}
以上代码,先开辟4个字节(32b),存放a(2b),还剩30b,存放b(5b),还剩25b,存放c(10b),还剩15b,不够存放d(30b),所以再开辟4个字节(32b),足够存放d(30b)。一共开辟了8个字节,struct S类型的大小为8个字节。
示例2
struct S
{
char a : 3;
char b : 4;
char c : 5;
char d : 4;
};
int main()
{
struct S s = { 0 };
s.a = 10; // 1010
s.b = 12; // 1100
s.c = 3; // 11
s.d = 4; // 100
return 0;
}
以上代码,空间是如何开辟的?
小端字节序:
3. 联合体
联合体又称共用体,与结构体不同的是,联合体成员变量相互覆盖,共同占用同一块内存空间。
3.1 联合体的声明
联合体的声明与结构体类似。
union tag
{
member-list;
}variable-list;
3.2 联合体变量的定义和初始化
联合体变量的定义和初始化方式也与结构体类似,但结构体能初始化所有成员,联合体只能初始化一个成员。
C99之前,只有联合体的第一个成员可以获得初始值;C99支持指定初始化,只能初始化一个成员,但不一定是第一个。
union Un
{
char c;
int i;
double d;
};
union Un u1 = { 1 };
union Un u2 = { .i = 9 };
3.2 用联合体判断大小端
联合体的重要性质:成员变量共同占用同一块内存空间。
#include <stdio.h>
int check_sys()
{
union Un
{
char c;
int i;
}u;
u.i = 1;
return u.c;
}
/*
int check_sys()
{
int a = 1;
return *(char*)&a;
}
*/
int main()
{
int ret = check_sys();
if (ret == 1)
printf("小端\n");
else
printf("大端\n");
return 0;
}
3.3 联合体大小的计算
- 联合体的大小至少是最大成员的大小。
- 联合体的大小为最大对齐数的整数倍。
union Un1
{
char arr[5]; // 对齐数=min{8,1}=1
int i; // 对齐数=min{8,4}=4
}; // 最大对齐数=max{1,4}=4
// union Un1的大小至少是5×1=5个字节,还必须是4的整数倍
// sizeof(union Un1)=8
union Un2
{
short s[7]; // 对齐数=min{8,2}=2
int i; // 对齐数=min{8,4}=4
}; // 最大对齐数=max{2,4}=4
// union Un2的大小至少是7×2=14个字节,还必须是4的整数倍
// sizeof(union Un1)=16
4. 枚举
4.1 枚举的声明
虽然枚举和结构体、联合体没有什么共同的地方,但是它们的声明方法很类似。与结构体或联合体的成员不同,枚举常量的名字必须不同于作用域范围内声明的其他标识符。
enum 枚举名
{
枚举常量 [= 整型常量],
枚举常量 [= 整型常量],
...
枚举常量 [= 整型常量]
}枚举变量;
如果枚举常量都没有被显式初始化,则从第一个枚举常量开始依次默认为0,1,2……
如果某个枚举常量被显式初始化,且其后的一些成员没有被显式初始化,则其后的成员按依次加1的规则确定其值。
enum Season
{
SPR, // 0
SUM, // 1
FAL, // 2
WIN // 3
};
enum Day // 星期
{
Mon = 1, // 1
Tues, // 2
Wed, // 3
Thur, // 4
Fri, // 5
Sat, // 6
Sun // 7
};
enum Color
{
RED, // 0
GREEN = 5, // 5
BLUE // 6
};
4.2 枚举的使用
#include <stdio.h>
enum Color
{
RED = 1,
GREEN = 2,
BLUE = 4
}clr1;
int main()
{
enum Color clr1 = RED; // ok
printf("clr1 = %d\n", clr1);
int clr2 = GREEN; // ok
printf("clr2 = %d\n", clr2);
enum Color clr3 = 10; // C语言ok,C++err
printf("clr3 = %d\n", clr3);
return 0;
}