1、结构体的基础知识
在存储数据时,有时候需要将某些值存放在一起,C语言提供了两种可以同时存储多个数据的类型,一个是数组,一个是结构体。数组是相同类型的元素的集合,结构也是一些值的集合,这些值称为成员变量,结构的每个成员可以是不同类型的变量。
2、结构声明
在声明结构时,必须列出它包含的所有成员。如:
struct Stu
{
char name[10];
char sex[5];
int age;
char tele[15];
};//分号不能丢
如上所示,这是一种类型,不分配内存,就相当与int。 可以通过该类型来创建变量。
在声明结构的时候,可以不完全声明,即匿名结构体类型。
struct
{
char name[10];
char sex[5];
int age;
char tele[15];
}x;
struct
{
char name[10];
char sex[5];
int age;
char tele[15];
}*p;
创建了一个结构体变量x和结构体指针变量p,那么p = &x;
可以吗?
警告: 这两个声明被编译器当做两种不同的类型,即使它们的成员列表完全相同。因此p = &x
这条语句是非法的。
解决方法:
struct Stu
{
char name[10];
char sex[5];
int age;
char tele[15];
}x;
struct Stu
{
char name[10];
char sex[5];
int age;
char tele[15];
}*p;
p = &x;
3、结构成员
结构成员可以是标量、数组、指针甚至是其它结构体。
结构成员的访问
- 结构体变量可以通过点操作符(.)访问
- 指向结构体的指针可以通过箭头操作符(->)访问
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <string.h>
int main()
{
struct Stu
{
char name[10];
int age;
};
struct Stu s;//创建结构体变量
struct Stu *ps = &s;//创建一个指向结构体变量s的指针
strcpy(s.name,"zhangsan");
s.age = 18;
printf("name:%s,age=%d\n",s.name,s.age);
printf("name:%s,age=%d\n",ps->name,ps->age);
return 0;
}
运行结果:
4、结构的自引用
有一个数组a,该数组有5个元素1,2,3,4,5假如要在2和3之间插入一个10,那么3,4,5应该依次往后移,假如要删除3,那么应该将4和5向前移以达到删除的效果。
可见这种方法太过麻烦,我们可以在内存中找到几个内存块来存放数据,可以让第一个数据能找到第二个数据,第二个找到第三个,依次类推。如图所示,在1和2之间插入一个8,只需让1指向8,8指向2即可,后面的数据都不需要挪动;删除3的话只需让2指向4,将4释放掉即可。
显然,这种方法更加方便、高效。这就可以用结构的自引用来实现。更加高级的数据结构,如链表和树,都是用这种技巧实现的。
在一个结构体内部包含一个该结构本身的成员是否合法?下面我们来看一个例子。
struct Stu
{
int age;
struct Stu s;
};
这种自引用是非法的,因为成员s是另一个完整的结构体,其内部还包含它自己的成员s。这样重复下去,有点像不会终止的递归程序。而且sizeof也不能求取该结构体的大小。
正确的自引用方式:
struct Stu
{
int age;
struct Stu *s;
};
typedef struct
{
int age;
Stu *s;
}Stu;
//这样的代码可以吗?
这个声明的目的是为这个结构体创建类型名stu,但是这样做是错误的,类型名直到声明的末尾才定义,所以在结构声明的内部它尚未定义。
//解决方案
typedef struct Stu
{
int age;
struct Stu *s;
}Stu;
5、结构的不完整声明
先来看一段代码:
struct A
{
int a;
struct B *pb;
};
struct B
{
int b;
struct A *pa;
};
在这段代码中,结构体A包含了一个指向结构体B的指针,而结构体B中也包含了一个指向结构体A的指针,那么应该先声明哪个结构体呢?解决这个问题的方案是不完整声明。它声明一个作为结构标签的标识符,然后我们可以把这个标签用在不需要知道这个结构大小的声明中,如声明指向这个结构的指针。接下来的声明把这个标签与成员列表联系在一起。
struct B;
struct A
{
int a;
struct B *pb;
};
struct B
{
int b;
struct A *pa;
};
6、结构体变量的定义和初始化
struct point
{
int x;
int y;
}p1;//声明类型的同时定义结构体变量p1
struct Point p2;//定义结构体变量p2
//初始化:定义变量的同时进行赋值
struct Point p3 = {1,3};
//结构体嵌套
struct Node
{
int data;
struct Point p;
}n={16,{2,3}};
7、结构体内存对齐
如何计算结构体的大小呢?我们先来看一下这个结构体占几个字节。
struct S
{
char c1;
int i;
char c2;
};
printf("%d\n",sizeof(struct S));
这段代码输出的结果是多少呢?我们可以看到结构体S的元素c1占1个字节,i占4个字节,c2占1个字节,也就是结构体S的元素总共占6个字节,那么这个结构体的大小是多少呢?
运行结果是12,这是为什么呢,完全出乎意料,我们刚才计算元素总共占6个字节,可是结构体的大小编译器计算出来是12个字节。在计算结构体大小的时候就要考虑到内存对齐的问题,下来我们就来了解一下内存对齐。
在谈到内存对齐的时候一般有两个常见的问题:
1. 为什么存在内存对齐?
2. 结构体内存对齐的规则是什么?
1、为什么存在内存对齐?
大部分资料中认为内存对齐存在的原因主要有以下几点。
平台原因(移植原因)
不是所有的硬件平台都能访问任意地址上的数据,某些硬件平台只能在某些地址处取某些特定类型的数据,否则就会抛出硬件异常。性能原因
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问,而对齐的内存访问只需要一次访问。
2、结构体内存对齐的规则是什么?
- 第一个成员在与数据结构变量偏移量为0的地址处
- 从第二个成员开始要对齐到对齐数的整数倍的地址处
对齐数:该成员的大小与编译器的默认对齐数的较小值
在VS中默认对齐数为8,在linux中默认对齐数为4 - 结构体的总大小为最大对齐数的整数倍
如果嵌套了结构体,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的总大小就是最大对齐数(含嵌套结构体的对齐数)的整数倍
我们再来看一下这段代码:
struct S
{
char c1;
char c2;
int i;
double d;
};
int main()
{
struct S s;
printf("%d\n",sizeof(s));
printf("%d\n",offsetof(struct S,c1) );
printf("%d\n",offsetof(struct S,c2) );
printf("%d\n",offsetof(struct S,i) );
printf("%d\n",offsetof(struct S,d) );
return 0;
}
运行结果是16、0、1、4、8,在这段代码中offsetof
这个宏是计算成员相对于结构体变量起始位置的偏移量,该宏的头文件是<stddef.h>
。
- 根据结构体的对齐规则,c1在偏移量为0的地址处,占1个字节;
- 从c2开始就要对齐到对齐数的整数倍处,c2本身的大小为1,VS的默认对齐数为8,取两者的较小值也就是1,即c2对齐到1的整数倍处,c2在偏移量1的地址处,占1个字节;
- 变量i本身大小为4,默认对齐数为8,故对齐数为4,需放到4的整数倍处,2和3都不是4的整数倍,浪费掉,i放到了偏移量4的地址处,占4个字节;
- d本身大小为8,故对齐数为8,放在偏移量8的地址处,占8个字节。
所以结构体的大小为16(最大对齐数8的整数倍),后边不需要再浪费。
那么现在我们来看一下开始的那段代码,它的大小是12个字节,(考虑内存对齐)在内存中的存储如下图所示。
我们也可以自己来修改对齐数,控制结构体在内存中的存储方式。
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stddef.h>
#include <windows.h>
#pragma pack(4)
struct S1
{
double d;
int i;
};
#pragma pack()
#pragma pack(1)
struct S2
{
char c1;
int i;
char c2;
};
#pragma pack()
int main()
{
struct S1 s1;
struct S2 s2;
printf("%d\n",sizeof(s1));
printf("%d\n",sizeof(s2));
return 0;
}
运行结果:
这是我们通过#pragma pack(4);
将默认对齐数设置为4,#pragma pack();
恢复对齐数,将s1的对齐数设置为4,s2的对齐数设置为1,则两个结构体的大小分别为12,6。如果不设置默认对齐数,这两个结构体的大小如下图所示。
8、结构体传参
结构变量是一个标量,因此,可以将结构体作为参数传递给函数。
struct S
{
int data[1000];
int num;
};
struct S s = {0};
//结构体传参
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;
}
函数传参的时候,参数是需要压栈的,要创建临时变量。如果传递一个结构体对象的时候,结构体过大,参数压栈的系统开销比较大,会导致效率比较低。结构体越大,传结构体地址的效率就越高。
我们可以通过代码来测试传值和传地址所花费的时间。GetTickCount()
返回从操作系统启动到当前所经过的毫秒数,头文件是windows.h。
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <windows.h>
struct S
{
int data[2000];
int num;
};
void test1(struct S s)
{
}
void test2(struct S *ps)
{
}
int main()
{
struct S s;
int start1 = 0;
int end1 = 0;
int start2 = 0;
int end2 = 0;
int i = 0;
start1 = GetTickCount();
for(i=0;i<1000000;i++)
{
test1(s);
}
end1 = GetTickCount();
start2 = GetTickCount();
for(i=0;i<1000000;i++)
{
test2(&s);
}
end2 = GetTickCount();
printf("%d %d\n",end1-start1,end2-start2);
return 0;
}
运行结果:
结论:结构体传参的时候,要传结构体的地址。
9、位段
关于结构,我们最后来看看它们实现位段(bit field)的能力。位段的声明和结构类似,但它的成员是一个或多个位的字段。位段的声明和任意普通的结构声明相同,但有两个例外。
- 位段成员必须声明为int、unsigned int、signed int(在实际应用中也可以是char类型)。
- 位段的成员名后面有一个冒号和一个整数,这个整数指定该位段所占用的位的个数。
struct A
{
int a:2;
int b:3;
int c:10;
int d:28;
};
struct B
{
char ch1:3;
char ch2:4;
char ch3:5;
char ch4:4;
};
A和B就是位段类型,那么A和B的大小是多少?
printf("%d\n",sizeof(struct A));
printf("%d\n",sizeof(struct B));
运行结果:
- 位段A每次创建一个int类型的空间即4个字节32位,a、b、c各占2、3、10位,共占15位,还剩下17位,d占28位,剩下的17位不够存放d,浪费掉,在创建4个字节,剩下4位,浪费掉。故位段A的大小为8个字节。
- 同理,位段B每次创建一个char类型的空间即1个字节8位,ch1、ch2、ch3、ch4各占3、4、5、4位,故位段B的大小为3个字节。
位段的内存分配
- 位段的成员可以是int、unsigned int、signed int或者是char类型。
- 位段的空间上是按照需要以4个字节(int)或者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;
在调试的内存窗口可以观察到s的存储,如图所示。
位段的跨平台问题
- int位段被当成有符号数还是无符号数是不确定的
- 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32)
- 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义
- 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是浪费剩余的位还是继续使用,这是不确定的
总结:跟结构相比,位段可以达到同样的效果,但是可以很好的节省空间,简化源代码,但是存在跨平台问题。