自定义类型(一):结构体和位段

结构体:是一些值的集合,这些值称为成员变量,每一个成员可以是不同类型的变量。

结构体的声明:

struct tag
{
    member_list;
}variable_list;

//例如使用结构体来描述一个学生
struct Stu
{
    char name[20];//名字
    int age;//年龄 
    char id[20];//学号
};//分号不能丢

在声明结构体的时候可以不完全的声明。(在C语言中,结构体不能为空)
例如:

//匿名结构体类型
struct
{
    int a; 
    char b; 
    float c;
}x;

struct
{
    int a; 
    char b; 
    float c;
}a[20], *p;
//这里的a[20],是一个有20个元素的数组
//数组中的每一个元素就是一个结构体

//以上这两个结构体在声明的时候省略了结构体标签(tag)。

一个问题:p = &x是否合法???
编译器会把上面的两个声明当成完全不同的两个类型,所以是非法的。
再来看一看:p = &a;
p是一个结构体指针,而&a得到的是数组的地址,因此该表达式不合法
再来看:p = a;
p是一个结构体指针,而a代表的是数组a的首元素的地址,刚好这里的数组a是包含20个结构体元素的数组,所以p指向了数组的首元素(是一个结构体),因此该表达式合法。

结构体的成员
结构体的成员可以是标量(整形,double型等),数组,指针,甚至是其他结构体。
结构体成员的访问
1、结构体变量访问成员,是通过点操作符‘ · ’访问的,点操作符要接受两个操作数。

struct Stu
{
    char name[20];
    int age;
};

struct Stu s;//定义一个结构体变量s

//访问
strcpy(s.name,"wangtao");//使用点操作符访问name成员
s.age = 20;//访问age成员

2、结构体访问指向变量的成员,有时候我们得到的不是一个结构体变量,而是指向一个结构体的指针。此时我们使用‘->’来访问成员变量。

void print(struct S* ps)
{
    printf("name = %s,age = %d\n", (*ps).name, (*ps).age);   
    printf("name = %s,age = %d\n", ps->name, ps->age);
}

结构体的自引用
在结构中包含一个类型为该结构本身的成员是否可以呢?

//代码1
struct Node
{
    int data;
    struct Node next;
};
//可行否?(不行)
//如果可以,那sizeof(struct Node)是多少?(无法得出结论)

正确的自引用方式:

//代码2
struct Node
{
    int data;
    struct Node* next;
};

注意:

//代码3
typedef struct
{
    int data; 
    Node* next;
}Node;
//这样写代码,是不可行的

//解决方案:前面加上typedef(类型重定义关键字)
typedef struct Node
{
    int data;
    struct Node* next;
}Node;

结构体的不完整声明

struct A
{
    int _a;
    struct B* pb;
};

struct B
{
    int _b;
    struct A* pa;
};
//注意这样的代码时行不通的

//解决方案:结构体互相包含的情况推荐使用结构体的不完整声明(提前声明)
struct B; //提前声明

struct A
{
    int _a;
    struct B* pb;
};

struct B
{
    int _b;
    struct A* pa;
};

结构体变量的定义和初始化
1、定义结构体变量

struct Point
{
    int x;
    int y;
}p1;//声明一个结构体时顺便定义了一个结构体变量p1

struct Point p2;//定义结构体变量p2

2、结构体变量的初始化

//初始化:定义变量的同时赋初值。
struct Point p3 = {3, 6};
struct Stu//类型声明
{
    char name[15];//名字
    int age;    //年龄
};
struct Stu s = {"zhangsan", 20};//初始化
struct Node
{
    int data;
    struct Point p;
    struct Node* next;
}n1 = {10, {4,5}, NULL};//结构体嵌套初始化
//声明一个结构体时顺便定义一个变量并且初始化
//该结构体中有变量值结构体变量,初始化时也对其进行初始化
//这叫做结构体嵌套初始化
struct Node n2 = {20, {5, 6}, NULL};//结构体嵌套初始化

注意:结构体只允许整体初始化,不允许整体赋值。

结构体传参

首先说明一点:结构体传参不能传结构体变量,而是选择指针。所有的结构体传参都统一采用指针传参。

struct S
{
    int data[1000];
    int num;
};
struct S s = {{1,2,3,4}, 1000};
//结构体传参(不会发生降级问题)
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;
}
//print1函数和print2函数哪一个好些呢???
//答案是print2函数
//因为函数在进行传参的时候,参数是需要压栈的。
//如果传递一个结构体对象的时候,该结构体过大
//那么参数压栈的系统开销就会比较大,所以就会导致性能的下降

再次强调:结构体传参的时候,要传结构体的指针

结构体内存对齐(重点)
1、为什么存在内存对齐???

  1. 平台原因(移植原因): 不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
  2. 性能原因: 数据结构(尤其是栈)应该尽可能地在自然边界上对齐。
    原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。

2、结构体的对齐规则:

  1. 第一个成员在与结构体变量偏移量为0的地址处。此处需要注意的是第一个成员也是有对齐数的。
  2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。 对齐数 = 编译器默认的一个对齐数(vs下默认为8,Linux默认为4) 与 该成员大小的较小值。
  3. 结构体总大小为最大对齐数的整数倍。
  4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
  5. 结构体的对齐数就是结构体内部的最大对齐数

练习一下:默认在Linux环境下,32位平台

//练习1
struct S1
{
    char c1; 
    int i; 
    char c2;
};
printf("%d\n", sizeof(struct S1));
//结果为:12
//解析:有规则1,所以第一个成员变量c1在偏移量为0的地址处。
//i在32位平台上为大小为4字节,编译器默认对齐数为4,
//所以对齐到4(对齐数)的整数倍的地址处,即偏移量为4的地址处,
//占据4 、5 、6 、7这4个地址。
//c2在32位平台上为大小为1字节,编译器默认对齐数为4,
//所以对齐到1(对齐数)的整数倍的地址处,即偏移量为8的地址处。
//总共占据0~8  共9个地址被占据
//有规则3,结构体总大小为最大对齐数的整数倍。
//最大对齐数为4,因此该结构体的大小就为12

//练习2
struct S2
{
    char c1; 
    char c2; 
    int i;
};
printf("%d\n", sizeof(struct S2));
//结果为:8
//解析:有规则1,所以第一个成员变量c1在偏移量为0的地址处。
//c2在32位平台上为大小为1字节,编译器默认对齐数为4,
//所以对齐到1(对齐数)的整数倍的地址处,即偏移量为1的地址处。
//i在32位平台上为大小为4字节,编译器默认对齐数为4,
//所以对齐到4(对齐数)的整数倍的地址处,即偏移量为4的地址处,
//占据4 、5 、6 、7这4个地址。
//总共占据0~7  共8个地址被占据
//有规则3,结构体总大小为最大对齐数的整数倍。
//最大对齐数为4,因此该结构体的大小就为8

这里写图片描述

//练习3
struct S3
{
    double d; 
    char c; 
    int i;
};
printf("%d\n", sizeof(struct S3));
//结果为:16
//解析:有规则1,所以第一个成员变量d在偏移量为0的地址处。
//double类型的数据在32为平台下大小为8个字节,占据0~7共占据8个地址
//第二个成员c大小为1个字节,编译器默认对齐数为4,
//所以对齐到1(对齐数)的整数倍的地址处,即偏移量为8的地址处,
//i在32位平台上为大小为4字节,编译器默认对齐数为4,
//所以对齐到4(对齐数)的整数倍的地址处,即偏移量为12的地址处,
//占据12 、13 、14 、15这4个地址。
//总共占据0~15  共16个地址被占据
//有规则3,结构体总大小为最大对齐数的整数倍。
//最大对齐数为8,因此该结构体的大小就为16

这里写图片描述

//练习4-结构体嵌套问题
struct S4
{
    char c1;
    struct S3 s3;
    double d;
};
printf("%d\n", sizeof(struct S4));
//结果为:24
//解析:有规则1,所以第一个成员变量d在偏移量为0的地址处。
//第二个成员是一个结构体大小为16个字节,
//有规则4和规则5,所以第二个成员(结构体)对齐到4的整数倍的地址处,
//即偏移量为4的地址处,占据4~13共占据16个地址。
//double类型的数据在32为平台下大小为8个字节,编译器默认对齐数为4,
//所以对齐到4(对齐数)的整数倍的地址处,即偏移量为16的地址处,
//占据16~23共占据8个地址。
//有规则3,结构体总大小为最大对齐数的整数倍。
//最大对齐数为4,因此该结构体的大小就为24

这里写图片描述

学习完结构体,我们也顺带了解一下位段。
位段
位段是通过结构体实现的,所以位段的声明和结构体类似,其中有两个不同:

1.位段的成员必须是 int 、unsigned int和signed int。
2.位段的成员名后边有一个冒号和一个数字。
比如:

struct A
{
    int a:2; 
    int b:5; 
    int c:10; 
    int d:30;
};
//这里的A就是一个位段类型

前面我们讲解了结构体,结构体中的一大重点就是结构体的内存分配。那么问题来了,通过结构体实现的位段是否也存在内存对齐的问题呢???答案是一般情况下不需要内存对齐的,因为位段的内部成员都是一样的类型,但是在位段与常规的结构体混用的情况下是需要雷村对齐的。那么,上面的位段A的大小又是多少呢???(答案是:8)(看图文详细讲解)

struct A
{
    int a:2; 
    int b:5; 
    int c:10; 
    int d;
};
//例如这里的位段就需要内存对齐

位段的内存分配

  1. 位段的成员可以是 int、unsigned int、 signed int 或者是 char (属于整形家族)类型
  2. 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。
  3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。

现在我们来看一看刚刚提出的位段A的大小时多少的问题:
这里写图片描述
位段数据的引用
同结构体成员中的数据引用一样,但应注意位段的最大取值范围不要超出二进制位数定的范围,否则超出部分会丢弃,不会影响到其他元素。
例如:

struct Data
{
    int a:2; 
    int b:5; 
    int :10; //无名位段
    int d:30;
};
struct Data data;
data.a=2; 
data.a=10;//超出范围(a占2位,最大只能到3)

位段的跨平台问题

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

关于位段需要注意的几点:

  1. 在某些机器上, 位段总是作为 unsigned 处理, 而不管它们是否被说明成 unsigned 的。
  2. 位段不可标明维数; 即, 不能说明位段数组, 例如 flag:l[2]。
  3. 一个位段必须存储在同一存储单元(即字)之中,不能跨两个单元。如果其单元空间不够,则剩余空间不用,从下一个单元起存放该位段。
  4. 可以通过定义长度为0的位段的方式使下一位段从下一存储单元开始。
  5. 可以定义无名位段。
  6. 位段的长度不能大于存储单元的长度,并且大多数C 编译器都不支持超过一个字长的位段。
  7. 位段无地址,不能对位段进行取地址运算。
  8. 位段可以以%d,%o,%x格式输出。
  9. 当一个结构位段若出现在表达式中,将被系统自动转换成整数。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章