结构体

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>

  1. 根据结构体的对齐规则,c1在偏移量为0的地址处,占1个字节;
  2. 从c2开始就要对齐到对齐数的整数倍处,c2本身的大小为1,VS的默认对齐数为8,取两者的较小值也就是1,即c2对齐到1的整数倍处,c2在偏移量1的地址处,占1个字节;
  3. 变量i本身大小为4,默认对齐数为8,故对齐数为4,需放到4的整数倍处,2和3都不是4的整数倍,浪费掉,i放到了偏移量4的地址处,占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个字节。
位段的内存分配
  1. 位段的成员可以是int、unsigned int、signed int或者是char类型。
  2. 位段的空间上是按照需要以4个字节(int)或者1个字节(char)的方式来开辟的。
  3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
    注意:位段不存在内存对齐问题。
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的存储,如图所示。
这里写图片描述

位段的跨平台问题
  1. int位段被当成有符号数还是无符号数是不确定的
  2. 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32)
  3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义
  4. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是浪费剩余的位还是继续使用,这是不确定的

总结:跟结构相比,位段可以达到同样的效果,但是可以很好的节省空间,简化源代码,但是存在跨平台问题。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值