C语言结构体
Posted 蓝乐
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C语言结构体相关的知识,希望对你有一定的参考价值。
结构体
结构体类型的声明
1)结构体的基本概念:结构是一些值的集合,这些值被称为成员变量。结构的每个成员可以是不同类型的变量。
2)结构的声明:
struct tag//tag:结构体标签
{
number - list;//成员列表
}valiable - list;//变量列表
例如描述一本书:
struct Book
{
char name[20];//书名
float price;//价格
int year;//出版时间
};//分号不能丢
3)特殊的声明:在声明结构的时候,可以不完全声明。比如:
//匿名结构体类型
struct
{
int a;
float b;
char c;
}s1;
struct
{
int a;
float b;
char c;
}s2, *p;
上面两个结构体在声明的时候省略掉了结构体标签(tag)。那么问题来了,
在上述基础上,下面这个代码合理吗?
int main()
{
*p = &s1;
return 0;
}
编译发现是不能通过的:
这是因为编译器会将上面两个声明当成完全不同的两个类型,因此是非法的。在使用匿名结构体时,需要在声明的同时定义变量,故使用匿名结构体时需谨慎。
结构体的自引用
在结构体中包含一个类型为该结构本身的成员是否可行呢?比如:
struct Node
{
int data;
struct Node next;
};
如果上述代码可行,那么sizeof(struct Node)
的大小是多少呢?
不难看出该结构体在类似递归的不断延伸,像套娃一样,其大小将是无穷大。在实际编译过程中,编译器也会对这段代码报错:
在声明结构体时,结构体并未被定义完成,因此若在结构中包含一个类型未该结构本身的变量时不可行的。
正确的结构体自引用方式是利用结构体指针:
struct Node
{
int data;
struct Node* next;
};
这种方式在数据结构中的链表将会被大量使用。
注意区分一下两种代码:
//代码1
typedef struct Node
{
int data;
Node* next;
}Node;
//代码2
typedef struct Node
{
int data;
struct Node* next;
}Node;
代码1在编译过程中是无法通过的:
这是因为在定义next变量时,将struct Node重命名为Node这一操作还未完成,故编译器无法辨识Node类型,因此无法通过,在实际使用过程中需注意这一点。
结构体变量的定义和初始化
声明了结构体类型后,结构体变量的定义和初始化就变得很简单了。
1)定义结构体变量
struct Point
{
int x;
int y;
}p1;//声明结构体的同时定义变量
struct Point p2;//定义结构体变量p2
2)初始化结构体变量
struct Point p3 = { 1,2 };//定义的同时赋初值
struct Node
{
int data;
struct Point p;
struct Node* next;
}n1 = { 3, {7,8}, NULL }; //结构体嵌套初始化
struct Node n2 = {5, {6, 9}, NULL};//结构体嵌套初始化
结构体内存对齐
计算结构体大小
掌握了结构体的基本使用后,接下来讨论计算结构体的大小。先思考一下以下几个结构体的大小是多少:
//结构体1
struct s1
{
char c1;
int i;
char c2;
};
//结构体2
struct s2
{
char c1;
char c2;
int i;
};
//结构体3
struct s3
{
double d;
char c1;
int i;
};
//结构体4
struct s4
{
char c;
struct s3 s;
long x;
};
在此之前先来了解一下结构体的内存对齐规则:
- 第一个成员在与结构体变量偏移量为0的地址处。
- 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
(VS中默认的值为8,Linux没有默认对齐数) - 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
- 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
如何理解上述规则呢?我们通过画图来理解结构体在内存中的存储
结构体内存对齐的意义
我们可以看到结构体内存对齐的过程中存在这内存的浪费,那么为什么会存在结构体对齐呢?
大部分的参考资料都是如是说的:
1)平台原因(移植原因): 不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2)性能原因: 数据结构(尤其是栈)应该尽可能地在自然边界上对齐。 原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要访问一次。
什么意思呢?如下图:
假设编译器以4个字节读取内存。对于内存对齐的结构体,编译器只需要对结构体读取一次内存即可读取所有数据;而对于为对齐内存的结构体,编译器则需要对结构体读取两次才能读取所有数据,故结构体的内存对齐使得编译效率提高。
总体来说:结构体的内存对齐是拿空间来换取时间的做法。
因此,在实际设计结构体的时候,我们既要满足对齐,又要节省空间,因此,尽量将占用空间小的成员变量集中在一起,比如结构体1和结构体2,显然结构体2更好一些。
修改默认对齐数
使用#pragma
这个预处理指令,我们就可以修改默认对齐数。
#pragma pack(2)//修改默认对齐数为2
struct s5
{
char c1;
long i;
char c2;
};
#pragma pack()//取消修改的默认对齐数,还原为默认
int main()
{
printf("%d\\n", sizeof(struct s5));
return 0;
}
默认对齐数修改为2,对于结构体s5,c1在偏移量为0的位置,i在偏移量为较小对齐数2的位置,c2在偏移量为6的位置,结构体s5的总大小为2的整数倍8.
因此,在结构体的对齐方式不合适的时候,我们可以修改默认对齐数。
结构体传参
我们知道函数传参有两种形式,一种是传值,一种是传址。而对于结构体而言,传参最好是传址。因为函数传参是对参数的一份临时拷贝,如果传址那么传的内存只有4或8个字节,而传值的话,若结构体本身很大,那么临时拷贝后占用的内存也很大。因此, 结构体传参的时候,要传结构体的地址。
结构体实现位段
介绍完结构体后就得介绍一下结构体实现位段的能力。
什么是位段
位段的声明和结构是类似的,但有两点不同:
- 位段的成员必须是int,unsigned int或signed int或char类型。
- 位段的成员名后有一个冒号和一个数字。
比如:
struct A
{
int a : 5;
int b : 7;
int c : 12;
int d : 28;
};
A就是一个位段类型,那么位段A的大小是多少呢?
为什么A的大小是8个字节呢?这就要知道位段成员后的数字是什么含义了:由于一个整型变量的大小是4个字节,不难猜出位段成员后的数字应该指的是该成员在内存中所占用的大小,单位是bit。
位段的内存分配
- 位段的成员可以是 int unsigned int signed int 或者是 char (属于整形家族)类型
- 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。
这是什么意思呢?举个例子来说:
struct Test
{
char a : 2;
char b : 3;
char c : 4;
char d : 5;
};
int main()
{
struct Test t = { 1,2,3,4 };
return 0;
}
如果说位段成员在内存中是从高地址往低地址存储的,那么结构体变量t在内存中的存储应该如下:
调试一下发现:
结果与我们预期的相同
3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。这是因为:
a)int位段有符号数还是无符号数是不确定的
b)位段中最大位的数目不能确定(16位机器中最大16,32位机器中最大32)
c)位段的成员在内存中是从左向右分配还是从右向左分配标准尚未定义
d)当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。
总结:位段与结构相比,节省了空间,但是存在跨平台的问题。
位段的应用
在实际生活中,我们可以将一些本就不大的信息压缩以节省空间,就像下图:
以上是关于C语言结构体的主要内容,如果未能解决你的问题,请参考以下文章