目录
1. 结构体
1.1 结构体的声明
1.2 特殊的声明
1.2.1 匿名结构体类型
1.3 结构体的自引用
1.4 结构体变量的创建和初始化
1.4.1 结构体变量的创建
1.4.2 结构体变量的初始化
1.5 结构体内存对齐
1.5.1 结构体的内存对齐规则:
1.5.2 为什么要存在内存对齐?
1.5.3 那在设计结构体的时候,我们既要满足对齐,又要节省空间,如何做到:
1.6 修改默认对齐数
1.7 结构体传参
1.7.1结构体传参:要传结构体的地址
1.7.2 const修饰结构体指针:
(详见上篇博客:初阶C语言 - 结构体)
struct:结构体关键字,它表明是个结构体,结构体的声明必须有struct。
结构体标签:它是你给你的结构体类型取的名字
struct 结构体标签:这表示一个结构体类型
成员列表:可以有多个成员,每个成员有各自的数据类型和成员变量名。
结构体变量列表:可以有多个变量,是使用结构体类型创建的变量,一般结构体变量列表省略不写,先先进行结构体声明,之后再创建结构体变量。
举个结构体类型的声明的例子:
#include
//这是一般的结构体类型的声明
struct Stu
{
char name[20];
int age;
}s1;//可以在声明结构体类型的同时捎带着创建变量
int main()
{
return 0;
}
(1)特点:
只能使用一次,只能在声明结构体类型的同时顺带创建变量。
后面就不能使用了。
因为结构体类型没有名字,而我们需要通过结构体类型来创建变量,不能通过结构体关键字struct来创建变量,所以之后这个结构体类型就不能再使用了。
#include
//匿名结构体类型:只能使用一次
struct Stu
{
char name[20];
int age;
}s1;//只能在声明结构体类型的同时顺带创建变量,之后就不能用了
//因为结构体类型没有名字,之后我们不能只通过struct来创建变量
int main()
{
return 0;
}
(2)一些需要注意的点:
匿名结构体类型即使成员列表完全一致,也会因为没有名字被编译器认为是两个不同的结构体类型。
如下:
#include
//匿名结构体类型
struct
{
char name[20];
int age;
}x;//用匿名结构体类型创建了变量x
struct
{
char name[20];
int age;
}a[20],*p;//用匿名结构体类型创建了数组a,数组中可以放20个这种类型的元素
//用匿名结构体类型创建了指针变量p
//观察可以看到,这两个匿名结构体类型是相同的
int main()
{
//就像 int x;
// int* p = &x;这样
p = &x;//按道理这个代码也是可以的,但是因为是匿名结构体类型,结构体类型没有名字
//会报警告,编译器认为&x和p的类型是不一致的,编译器认为是两种不同的类型
//所以这样写是非法的。
return 0;
}
类型不兼容,认为是两个不同的结构体类型。
结构体中包含一个同类型的结构体指针的成员,
通过结构体中的指针可以找到自己同类型的另一个数据。
通过链表来举个例子:
数据在内存中随意存放,如图:
如何找到所有的数据呢?
假设绿色方块为一个个结点,每个结点可以被拆成两部分,
每个结点中放数据和下一个结点的地址,最后一个结点中放数据和空指针NULL
如1结点中放数据1和2结点的地址,就能通过指针找到2结点接着找到2结点中的数据2;2结点中放的是数据2和3结点的地址,就能通过指针找到3结点接着找到3结点中的数据3;以此类推,这样最终就能找到所有数据。
那如何描述结点呢?
因为结点中既有数据又有下一个结点的地址
所以用结构体类型(struct Node)来描述结点。
又因为类型中有一个成员变量是下一个结点的地址,下一个结点也是结点,类型也是struct Node
所以这个成员变量的类型是同类型的结构体指针(struct Node*)
最终这个结构体类型如下:
struct Node
{
int date;
struct Node* next;
};
结构体(struct Node)中包含了一个整型(int)和一个同类型的结构体指针(struct Node*),指针可以找到同类型(struct Node)的一个数据。
上面类型还可以用typedef进行重命名:
//方式1:既声明了结构体类型,同时又对结构体类型进行重命名
typedef struct Node
{
int date;
struct Node* next;
}node;
//方式2:先声明结构体类型,然后在对类型进行重命名
struct Node
{
int date;
struct Node* next;
};
typedef struct Node node;
int main()
{
//类型重命名后创建变量用这两种方式都行:
struct Node s1;
node s2;
return 0;
}
//方式1:既声明了结构体类型,同时又对结构体指针进行重命名
typedef struct Node
{
int date;
struct Node* next;
}*linklist;
//方式2:先声明结构体类型,然后在对结构体指针进行重命名
struct Node
{
int date;
struct Node* next;
};
typedef struct Node* linklist;//linklist是指针类型
可以在声明结构体类型的同时捎带着创建结构体变量;也可以在声明结构体类型后,再用结构体类型来创建变量
初始化:创建变量的同时给变量赋值。
#include
struct Point
{
int x;
int y;
}p1 = {2,3};//第一种初始化
struct Score
{
int n;
char ch;
};
struct Stu
{
char name[20];
int age;
struct Score s1;
};
int main()
{
//第二种初始化
struct Stu s = { "zhangsan",20,{100,'q'} };
printf("%d %d\n", p1.x, p1.y);
printf("%s %d %d %c\n",s.name,s.age,s.s1.n,s.s1.ch);
return 0;
}
偏移量:与起始位置中间相差的字节数就是偏移量。
如:内存中的第一个字节相对于起始位置的偏移量是0
1. 第一个成员在与结构体变量偏移量为0的位置。
2. 其他成员变量要对齐到偏移量为某个数字(对齐数)的整数倍的位置。
对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
VS中默认的值为8 其它没有默认对齐数的编译器,它的对齐数就是成员自身大小
3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
4. 如果嵌套了结构体的情况,嵌套的结构体对齐到偏移量为自己的最大对齐数的整数倍处,结构体的整体大小就是最大对齐数(含嵌套结构体的对齐数)的整数倍。
下面我们来看一个例子:
由上图可以看到,c1相对于起始位置的偏移量是0,i的偏移量是4,c2的偏移量是8
下面可以通过宏offsetof来验证上图是否正确,内存是否真的浪费了6个字节:
type:结构体类型,member:结构体成员名
offsetof的功能是:返回一个结构体成员(如:c1)在这个结构体类型(如:struct S1)创建的变量(s1)中的偏移量
头文件是
#include
#include
struct S1
{
char c1;
int i;
char c2;
};
int main()
{
struct S1 s1;
printf("%d\n", sizeof(struct S1));//12
printf("%d\n", offsetof(struct S1, c1));//0
//返回的是c1在S1中的偏移量
printf("%d\n", offsetof(struct S1, i));//4
//返回的是i在S1中的偏移量
printf("%d\n", offsetof(struct S1, c2));//8
//返回的是c2在S1中的偏移量
return 0;
}
经验证,c1在S1中的偏移量是0,i在S1中的偏移量是4,c2在S1中的偏移量是8
所以,结构体在内存中确实是这么放的,会有一些内存是分配了但未使用的。
下面,我们再来看一个例子:
结构体嵌套问题:
通过上图,我们已经知道,结构体struct S2的总大小是8字节,那么结构体struct S3中嵌套一个结构体类型的成员struct S2 s2时,结构体struct S3的总大小是多少呢?
#include
struct S2
{
char c1;
char c2;
int i;
};
struct S3
{
char c;
struct S2 s2;
double d;
};
int main()
{
struct S3 s3;
printf("%d\n", sizeof(struct S3));
return 0;
}
1. 平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
所以我们必须把某些数据放到我们能够访问的那些地址处,对齐到某些对齐边界上,这样我们才能访问到我们需要的数据。
2. 性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。 原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
总体来说: 结构体的内存对齐是拿空间来换取时间的做法。
对比发现,S1与S2这两个类型明明是相同的,但是因为成员变量的顺序不同,造成了不同的内存情况,很明显,S2更节约内存空间
所以得出结论,在设计结构体的时候:可以尽量让占用空间小的成员尽量集中在一起。
#pragma:预处理指令,作用是设定编译器的状态或者指示编译器完成一些特定动作
我们可以使用#pragma pack来修改默认对齐数。
我们一般设置完默认对齐数后会加一条语句来取消默认对齐数的修改,还原为默认,让我们设置的默认对齐数只作用于一部分代码。
设置默认对齐数为1,那么对齐数就全是1了,最大对齐数也是1,偏移量只要是1的倍数就可以存放,那么内存中任意地方都可以存放,即不会有空间浪费,数据直接依次存放在内存中。结构体的总大小就是结构体成员的类型的总大小。
#include
//默认对齐数为8
struct S1
{
int i;
double d;
};
#pragma pack(4)//设置默认对齐数为4
struct S2
{
int i;
double d;
};
#pragma pack()//取消设置的默认对齐数,还原为默认
#pragma pack(1)//设置默认对齐数为1
struct S3
{
char c1;
int i;
char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认
int main()
{
printf("%d\n", sizeof(struct S1));//16
printf("%d\n", sizeof(struct S2));//12
printf("%d\n", sizeof(struct S3));//6
return 0;
}
#include
struct S
{
int data[1000];
int num;
};
//结构体传参
void print1(struct S s)
{
int i = 0;
for (i = 0; i < 3; i++)
{
printf("%d ",s.data[i]);
}
printf("%d\n", s.num);
}
//结构体地址传参
void print2(const struct S* ps)//指针ps接收的是&s,为了防止ps误操作改变了s,
{ //可以在指针类型前面加上const,保护指针指向的值。
int i = 0;
for (i = 0; i < 3; i++)
{
printf("%d ", ps->data[i]);
}
printf("%d\n", ps->num);
}
int main()
{
struct S s = { {1,2,3,4}, 1000 };
print1(s); //传结构体
print2(&s); //传地址
return 0;
}
比较而言,print2更好
原因:
函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。 如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。
结论:结构体传参的时候,要传结构体的地址。
从左向右看,跳过类型,看修饰的是哪个字符:
若是*,说明const修饰的是指针指向的值,则指针指向的值不可以改,指针指向可以改
若是指针变量,说明const修饰的是指针,则指针的指向不可以改,指针指向的值可以改