C语言:结构体


什么是结构体

结构体是一种构造类型,允许用户自定义,用于保存一组不同类型的数据。例如,书籍信息,书的编号应为整型;书名应为字符型。显然这组数据不能用数组来存放, 为了解决这个问题,C语言中给出了一种构造数据类型——“结构(structure)”或叫“结构体”。

定义结构体类型

通过定义结构体类型来告诉编译器,我们的结构体中需要存储哪些类型的数据。

struct 结构体名 {
    类型名1 成员名1;
    类型名2 成员名2;
    ......
    类型名n 成员名n;
};

示例:

struct Books
{
    int book_id;
    char  *title;
};

注意不要丢掉末尾的分号

定义结构体变量

定义好结构体类型后,我们就可以利用定义的结构体类型来定义结构体变量。

 
格式:struct 结构体名 结构体变量名;

  • 先定义结构体类型,再定义变量
struct Books book1;
  • 定义结构体类型的同时定义变量
struct Books {
    int book_id;
    char *title;
} book2;
  • 匿名结构体定义结构体变量 (这种方法可以省略结构体类型名,但不能复用)
struct {
    int book_id;
    char *title;
} book3;

访问结构体成员

使用成员访问运算符.来访问结构体成员,形式为:结构体变量名.成员名

struct Books {
    int book_id;
    char *title;
};
struct Books book1;
boo1.book_id = 1000;
book1.title = "C语言";
printf("%d : %s", book1.book_id, book1.title);

结构体变量的初始化

  • 定义的同时按顺序初始化
struct Books book1 = {1, "C语言"};
  • 定义的同时不按顺序初始化
struct Books book2 = {.title = "C++", .book_id = 2};
  • 先定义再逐个初始化
struct Books book3;
book3.book_id = 3;
book3.title = "Python";
  • 先定义再一次性初始化
struct Books book4;
book4 = (struct Books) {4, "Java"};

结构体类型作用域

  • 结构体类型定义在函数内部的作用域与局部变量的作用域是相同的
    • 从定义的那一行开始,直到遇到return或者大括号结束为止
  • 结构体类型定义在函数外部的作用域与全局变量的作用域是相同的
    • 从定义的那一行开始,直到本文结束为止
struct Books {
    int book_id;
    char *title;
};
struct Books book1 = {1, "C语言"};
int main() {
    //定义一个同名的局部结构体,会屏蔽掉全局结构体
    struct Books {
        int book_id;
        char *author;
    };
    struct Books book1;
    book1.book_id = 3;
    // book1.title = "Python"; // 由于屏蔽掉了全局结构体,此处会报错
    book1.author = "Brian";
    printf("%d : %s\n", book1.book_id, book1.author);
    f();
    return 0; 
} //局部结构体作用域到这结束
void f() {
    //此处使用的是全局的结构体
    printf("%d : %s\n", book1.book_id, book1.title);
} 

结构体数组

结构体数组与数据值型数组不同之处在于每个数组元素都一个结构体类型的数据,它们分别包括各个成员项。

 
格式:struct 结构体类型名称 数组名称[元素个数]

struct Books book[3];

初始化

  • 定义的同时做初始化
struct Books book[3] = {{1, "C"}, {2, "C++"}}; // 初始化了前两个
  • 先定义在初始化
book[2].book_id = 3;
book[2].title = "Python";

结构体指针

指向结构体变量的指针。

 
格式:struct 结构体类型名 *结构体指针变量名;

通过结构体指针访问结构体成员的两种方式:

  • (*结构体指针变量名).成员名
  • 结构体指针变量名->成员名
struct Books {
    int book_id;
    char *title;
};
struct Books book1 = {1, "C"};

int main() {
    struct Books *p;
    p = &book1;
    printf("%d : %s\n", (*p).book_id, (*p).title);
    printf("%d : %s\n", p->book_id, p->title);
    return 0; 
}

(*结构体指针变量名)的 ( ) 不能省略,因为成员符.的优先级高于*

结构体内存分析

  • 给结构体变量开辟存储空间和给普通开辟存储空间一样, 会从内存地址大的位置开始开辟
  • 给结构体成员开辟存储空间和给数组元素开辟存储空间一样, 会从所占用内存地址小的位置开始开辟
  • 结构体变量占用的内存空间是其最大成员的大小的倍数(对齐问题)

计算机系统对基本类型数据在内存中存放的位置有限制,它们会要求这些数据的起始地址的值是某个数 k 的倍数,这就是所谓的内存对齐,而这个 k 则被称为该数据类型的对齐模数(alignment modulus)。
对齐的优点:

  • 简化了处理器与内存之间传输系统的设计
  • 可以提升读取数据的速度。
    比如:处理器每次读写内存的时候都从某个8倍数的地址开始,一次读出或写入8个字节的数据,假如软件能保证double类型的数据都从8倍数地址开始,那么读或写一个double类型数据就只需要一次内存操作。否则,我们就可能需要两次内存操作才能完成这个动作,因为数据或许恰好横跨在两个符合对齐要求的8字节内存块上。

对齐规则

  1. 结构体变量的首地址能够被其最大基本类型成员的大小所整除。

备注:编译器在给结构体开辟空间时,首先找到结构体中最宽的基本数据类型,然后寻找内存地址能被该基本数据类型所整除的位置,作为结构体的首地址。将这个最宽的基本数据类型的大小作为上面介绍的对齐模数。

  1. 结构体每个成员相对于结构体首地址的偏移量是当前成员大小的整数倍。

备注:为结构体的一个成员开辟空间之前,编译器首先检查预开辟空间的首地址相对于结构体首地址的偏移是否是该成员的整数倍,若是,则存放该成员,反之,则在该成员和上一个成员之间填充一定的字节,以达到整数倍的要求,也就是将预开辟空间的首地址后移几个字节。

  • 在GCC中,对齐模数最大只能是4,也就是说,即使结构体中有double类型,对齐模数还是4。
  • 除了平台差异外,还有预编译指令#pragma pack(n)手动设置,n–只能填1 2 4 8 16
  1. 结构体的总大小为其最大基本类型成员大小的整数倍。

备注:结构体总大小是包括填充字节,最后一个成员满足上面两条以外,还必须满足第三条,否则就必须在最后填充几个字节以达到本条要求。

// 基本数据类型大小, win 64位。
sizeof(char)   // 1
sizeof(short)  // 2
sizeof(int)    // 4
sizeof(long)   // 4
sizeof(float)  // 4
sizeof(double) // 8

示例

struct Str1 {                                        
    char a;    
    char b;        
}str1;
sizeof(Str1)  = 2

struct Str2 {
    char a; 
    char b; 
    int  c; 
}str2;
sizeof(Str2)  = 8

struct Str3 {
    char a;
    int  c;
    char b;
}str3;
sizeof(Str3)  = 12    

struct Str4 {
    char a;
    char b;
    int  c;
    double d;
}str4;
sizeof(Str4)  = 16    

struct str5 {
    char a;
    short b;
    int c;
    double d;
    char e[3];
}str5;
sizeof(Str4)  = 24 

结构体嵌套定义

成员也可以又是一个结构,即构成了嵌套的结构 。

例如:在Books中嵌套Date结构

struct Date {
    int year;
    int month;
    int day;
};
struct Books {
    int book_id;
    char *title;
    struct Date publication_time;
};

连续使用成员运算符.来访问嵌套结构体的成员

book1.publication_time.year = 2021;
book1.publication_time.month = 12;
book1.publication_time.day = 6;

结构作为函数参数

  • 结构体虽然是构造类型,但是结构体之间的赋值是值拷贝,而不是地址传递。
  • 结构体变量作为函数参数形参时也是值传递,在函数内修改形参,不会影响外界实参。
#include <stdio.h>
struct Books {
    int book_id;
    char *title;
};
void f(struct Books book);
int main() {
    struct Books book1 = {1, "C"};
    struct Books book2;
    book2 = book1;
    book2.title = "C++";
    printf("%d : %s\n", book1.book_id, book1.title); // 1 C
    printf("%d : %s\n", book2.book_id, book2.title); // 1 C++
    f(book1); // 1 Python
    printf("%d : %s\n", book1.book_id, book1.title); // 1 C
    return 0; 
} 
void f(struct Books book) {
    book.title = "Python";
    printf("%d : %s\n", book.book_id, book.title);
}

文章作者: cfxin
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 cfxin !
  目录