因为C语言有描述对象的需求,而结构体就是用来保存对象信息的,

大家千万不要误认为只有面向对象的语言才有描述对象的需求,任何一种语言都有描述对象的需求,
至于为什么有“面向过程语言”和“面向对象语言”的区别,我们后面再介绍这个问题


一、结构体 与 对象

显然结构体非常适合用来描述对象

  • 结构体类型,这个类型还不能称为对象,只是描述对象的一个模型
  • 在编译之后,类型不再起作用,在程序运行过程中,起作用的是变量
struct People
{
    int Height;
    short age;
    char name[30];
    char NativePlace[100]; //籍贯
    char gender;
};

struct People ZhangSan = {172, 25, "zhangsan", "hubei", 'M'}; //结构体变量

1. C是面向过程语言

C语言属于非常典型的“面向过程语言”,C语言只有最基本的结构体封装特性,没有继承和多态的特性,
因此c语言只是一个面向过程的语言

而且C结构体的封装特性与“面向对象语言”类的封装特性相比,C结构体不能封装函数,
也就是说在C语言中,函数不能定义在结构体里面,

在面向过程的语言中,最大的特点就是,函数都是独立外部的,
访问函数时都是直接访问的,不需要通过类/对象去访问

struct People
{
    int Height;
    short Age;
    char Name[30];
    char NativePlace[100];  //籍贯
    char Gender;
};

//函数只能在外部独立存在,不能定义在结构体
void print(struct People pel)
{
    printf("%d", pel.Height);
    printf("%d", pel.age);
    printf("%d", pel.name);
    printf("%d", pel.NativePlace);
    printf("%d", pel.gender);
}

//其它函数
...

正是由于以上差别,使得面向过程语言与面向对象之间的直观区别就是,
在面向过程的语言中,函数都是一个一个独立的存在,
但是在面向对象语言中,函数定义都被包含在了类(结构体)的内部


2. C模拟面向对象思想

虽然在C的结构体中不能直接定义函数,但是可以定义函数指针变量,用于存放函数指针,
以函数指针的方式来变相的封装函数,像这样的情况,我们称为模拟面向对象的思想,有关这一点后面还会讲到

struct People
{
    int Height;
    short Age;
    char Name[30];
    char NativePlace[100];  // 籍贯
    char Gender;
    void (*printFun)(); // 函数指针变量
};
struct People zhangSan = {172, 25, "zhangsan", "hubei", 'M', print};

//函数只能在外部独立存在,不能定义在结构体
void print(struct People pel)
{
    printf("%d", pel.Height);
    printf("%d", pel.age);
    printf("%s", pel.name);
    printf("%s", pel.NativePlace);
    printf("%c", pel.gender);
}

不过这里只是在模拟而已,与真正的面向对象还是有非常大的差距的,因为C不支持“继承”和“多态”,
并不是真正的面向对象,或者说不是完整的面向对象


二、定义结构体变量

1. 普通情况

struct People
{
    int Height;
    short Age;
    char Name[30];
    char NativePlace[100];  //籍贯
    char Gender;
}zhangSan; // 可以直接定义变量

struct People zhangSan;

2. 特殊情况:将结构体类型名省略

struct           //people可以被省略
{
    int Height;
    short Age;
    char Name[30];
    char NativePlace[100];  //籍贯
    char Gender;
}zhangSan;

不过省略了结构体类型名后,结构体变量只能放在}的后面,这种方式用的不多,
一般只有临时使用结构体类型时,才会这么定义


三、初始化

对于数组来说,初始化分为完全初始化、部分初始化、个别初始化,
事实上结构体的初始化也分这几种初始化,我们还是以struct People为例来介绍

struct People
{
    int Height;
    short Age;
    char Name[30];
    char NativePlace[100];  //籍贯
    char Gender;
};

初始化为 0

int buf[10] = {};

struct People zhangSan = {}; //与数组一样,将整个结构体变量空间初始化为 0

完全初始化

struct People zhangSan = {
    172, 
    25, 
    "zhangsan",
    "hubei",
    'M'
}; //所有成员都被初始化

初始化值的顺序必须与成员的顺序一直,否者就会出问题

也可以使用另一个结构体变量来初始化

struct People wangWu = zhangSan;

部分初始化

struct People zhangSan = {172, 25, "zhangsan"};//只初始化部分成员

进行部分初始化时必须是顺序进行的,不能跳跃着初始化

个别初始化

int buf[10] = {[0]=10, [8]=20, [2]=3};

struct People zhangSan = {
    .Height=172,
    .Gender='M',
    .Age=25
};

个别初始化时,需要通过.来访问成员,然后对其进行初始化,个别初始化时,对成员顺序没有什么要求


结构体数组的初始化

  • 1)整个为数组,就按照数组的初始化方式来初始化,比如完全初始化、部分初始化、个别初始化
  • 2)每个元素为一个结构体变量,就按照结构体变量的初始化方式来初始化,同样也支持完全初始化、部分初始化、个别初始化
struct People zhangSan[3] = {
    {172, 25, "zhangsan", "hubei", 'M'},
    {.Height=172, .Gender='M', .Age=25},
};

整个数组为部分初始化,第一个元素为完全初始化,第二个为个别初始化。


四、赋值

1. 个别成员赋值

struct People zhangSan;

zhangSan.Height = 172;
zhangSan.Age    = 25;

strcpy(zhangSan.Name, "zhangsan");
strcpy(zhangSan.NativePlace, "hubei");

zhangSan.Gender = 'M';

NameNativePlace是数组,数组是不能整体赋值的,
要么通过循环给数组的每个元素赋值,要么调用strcpy函数来赋值

也可以使用memcpy来赋值:

// 把字符串常量直接复制给 zhangSan.Name
strcpy(zhangSan.Name, "zhangsan");

// 直接把内存区域内容拷贝
memcpy((void *)zhangSan.Name, (void *)"zhangsan", sizeof("zhangsan"));

2. 整体赋值

前面说过数组是不允许整体赋值的,比如:

int buf[5];
buf = {0, 1, 2, 3, 4}; //不可以

int buf[5] = {0, 1, 2, 3, 4};
int buf1[5];
buf1 = buf;            //不可以

但是结构体变量可以,不过只能使用另一个结构体变量来整体赋值:

struct People zhangSan;
zhangSan = {172, 25, "zhangsan", "hubei", 'M'}; //不可以

但是如下是可以的:

struct People zhangSan = {172, 25, "zhangsan", "hubei", 'M'};
struct People wangWu;
wangWu = zhangSan;      //可以

五、结构体变量的成员访问

1. 使用.访问

通过“结构体变量”访问成员时使用的都是.,在前面进行结构体变量的个别初始化、以及赋值时,就已经使用.

2. 使用->访问

通过“结构体变量的指针”来访问成员时,使用的就是->,事实上结构体指针也可以使用.来访问成员

struct People zhangSan;

//将结构体变量zhangSan的指针放到p1中,结构体变量的指针就是变量第一个字节的地址
struct People *p1 = &zhangSan;

p1指向了zhangSan结构体变量空间,使用*解引用后,*p1代表的就是zhangSan这个变量空间,
得到结构体变量空间后,就可以使用.来访问成员

(*p1).Height = 172;
(*p1).Age = 25;

strcpy((*p1).Name, "zhangSan");
strcpy((*p1).NativePlace, "hubei");

(*p1).Gender = 'M';

这里一定要加(),因为.的优先级要高于*,所以如果不加()的话,
就变成了*p1.Gender(等价于*(p1.Gender)),这句话在语法上是不同的,编译会出错

显然(*p1).这种访问方式使用起来很不方便,很容易写错,比如错写成*(p1).等形式,
所以C语言给出了->的写法,(*p1).等价于->

p1->Height = 172;
p1->Age = 25;

strcpy(p1->Name, "zhangSan");
strcpy(p1->NativePlace, "hubei");

p1->Gender = 'M';

p1:为结构体指针变量,里面放的是结构体变量指针
p1->:等价于(*p1).,代表的是p1所指向的zhangSan这个结构体变量空间
p1->Gender:等价于(*p1).Gender,为zhangSanGender成员


六、结构体的传参

根据需求,我们可以传递整个结构体变量、整个结构体变量的指针,传递某个成员的值、成员的指针

数组是不能整体传参的,结构体可以整体传参,因为结构体可以整体初始化

void fun(struct People arg)
{
    ...
}

int main(void)
{
    struct People zhangSan = {172, 25, "zhangsan", "hubei", 'M'};

    fun(zhangSan);

    return 0;
}

struct People arg = zhangSan;

传递整个结构体变量,也就是在使用实参去初始化结构体形参变量,
但是为了提高效率,我们建议传递结构体变量的指针