自定义类型:结构体

一、结构体类型的声明

1.1 什么是结构体?

  • 结构是⼀些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。
  • 结构体中可以包含各种类型的数据,用来描述一个复杂对象的各种属性!

1.2 结构的声明

// 名字可以自己指定
struct tag      
{
 member-list;      // 可以有多个成员
}variable-list;    // 全局变量

代码示例:

struct Stu
{
 char name[20];  // 名字
 int age;        // 年龄
 char sex[5];    // 性别
 char id[20];    // 学号
 };   // 分号不能丢

1.3 特殊的声明

在声明结构 的时候,可以不完全的声明。
代码示例:

// 匿名结构体类型
struct
{
    int a;
    char b;
    float c;
} s = {0};
struct
{
    int a;
    char b;
    float c;
}* ps;

int main()
{
    * ps = &s;   // error
    return 0;
}

代码分析:

虽然结构体的成员一模一样,但是编译器会把 = 的两边当成完全不同的两个类型,所以是非法的。
匿名的结构体类型,如果没有对结构体类型重命名的话,基本上只能使用一次。

1.4 结构的自引用

在结构中包含⼀个类型为该结构本⾝的成员是否可以呢?
比如,定义一个链表的节点:

struct Node
{
int data;
 struct Node next;
}; 

代码分析:

上述代码是错误的,因为⼀个结构体中再包含⼀个同类型的结构体变量,这样结构体变量的大小就会无穷的大,是不合理的。

正确的自引用方式:

struct Node
{
 int data;
 struct Node* next;
};

在结构体自引用的使用过程中,夹杂了typedef对匿名结构体类型重命名,也容易引入问题,下面这段代码可行吗?

typedef struct
{
 int data;
 Node* next;
}Node;

答案是不行的,因为Node是对前面的匿名结构体类型的重命名产生的,但是在匿名结构体内部提前使用Node类型来创建成员变量,这是不行的。
解决方案如下:定义结构体不要使用匿名结构体了

typedef struct Node
{
 int data;
 struct Node* next;
}Node;

二、结构体变量的创建和初始化

2.1 结构体变量的创建

示例代码:

#include <stdio.h>
struct Stu
{
    char name[20];
    int age;
    float score;
} s4, s5;   // 全局变量

int main()
{
    struct Stu s1, s2, s3;   // 局部变量
    return 0;
}

代码分析:

在上述代码片段中,变量创建可以是局部变量,也可以是全部变量。

2.2 结构体变量的初始化

示例代码:

#include <stdio.h>
struct Stu
{
    char name[20];
    int age;
    float score;
} s3  = { "wangwu" , 23, 67.5f };

int main()
{
    // 初始化方法一:
    struct Stu s1 = { "zhangsan" , 18, 98.5f };
    struct Stu s1 = { "lisi" , 20, 92.5f };
    //  初始化方法二:
    struct Stu s5 = {.age = 15, .name = "haha", .score = 60.5f};
    return 0;
}

代码分析:

在上述代码中,变量初始化的方法有两种。

  • 按照结构体成员的顺序进行初始化,即:初始化的顺序要和结构体成员的顺序保持一致。
  • 按照指定的顺序进行初始化,初始化的值前面要加 . 成员。

2.3 结构成员访问操作符

结构成员访问操作符有两个,一个是  .  一个是  ->
形式如下:

  • 结构体变量 . 成员名
  • 结构体指针 -> 成员变量名

代码示例:

#include <stdio.h>
#include <string.h>
struct Stu
{
    char name[15];//名字
    int age; //年龄
};
void print_stu(struct Stu s)
{
    printf("%s %d\n", s.name, s.age);
}
void set_stu(struct Stu* ps)
{
    strcpy(ps->name, "李四");
    ps->age = 28;
}
int main()
{
    struct Stu s = { "张三", 20 };
    print_stu(s);
    set_stu(&s);
    print_stu(s);
    return 0;
}

三、结构体内存对齐

3.1 对齐规则

  • 结构体的第⼀个成员对齐到相对结构体变量起始位置偏移量为0的地址处
  • 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
    对齐数 = 编译器默认的⼀个对齐数 与 该成员变量大小的较小值。
  • VS中默认的值为8
  • Linux中没有默认对齐数,对齐数就是成员自身的大小
  • 结构体总大小为最大对齐数(结构体中每个成员变量都有⼀个对齐数,所有对齐数中最大的)的整数倍。
  • 如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员中最⼤对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍。

3.2 为什么存在内存对齐?

  • 平台原因(移植原因):
    不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
  • 性能原因:
    数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要⼀次访问。假设⼀个处理器总是从内存中取8个字节,则地址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对齐成8的倍数,那么就可以用⼀个内存操作来读或者写值了。否则,我们可能需要执行两次内存访问,因为对象可能被分放在两个8字节内存块中。
  • 总体来说:结构体的内存对齐是拿空间来换取时间的做法。

3.3 修改默认对齐数

  • #pragma 这个预处理指令,可以改变编译器的默认对齐数。
    代码示例:
#include <stdio.h>
#pragma pack(1)//设置默认对⻬数为1
struct S
{
    char c1;
    int i;
    char c2;
};
#pragma pack()//取消设置的默认对⻬数,还原为默认
int main()
{
    //输出的结果是什么?
    printf("%d\n", sizeof(struct S));
    return 0;
}

结构体在对齐方式不合适的时候,我们可以自己更改默认对齐数。

四、结构体传参

代码示例:

#include <stdio.h>
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;
}

代码分析:

在上述代码中,print2 函数更好些,原因如下:

  • 函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。如果传递⼀个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。
  • 结论:结构体传参的时候,要传结构体的地址。

好了~ 以上内容就是结构体的相关知识了!希望能对你有所帮助。我们下期再见!

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容