之前认识过很多种数据类型,包括整数、小数、字符、数组等,通过使用对应的数据类型,就可以很轻松地将数据进行保存了,但是有些时候,这种简单类型很难去表示一些复杂结构。
结构体
比如现在要保存 100 个学生的信息(学号、姓名、年龄),似乎找不到一种数据类型能够同时保存这三种数据(数组虽然能保存一系列的元素,但是只能保存同种类型的)。但是如果把它们拆开单独存在,就可以使用对应的类型存放了,不过这样也太不方便了吧,这些数据应该是捆绑在一起的,而不是单独地去存放。
为了解决这种问题,C 语言提供了结构体类型,它能够将多种类型的数据集结到一起,让他们形成一个整体。
// 使用 (struct关键字 + 结构体类型名称) 来声明结构体类型,这种类型是自己创建的(同样也可以作为函数的参数、返回值之类的)
struct Student { // 结构体中可以包含多个不同类型的数据,这些数据共同组成了整个结构体类型(当然结构体内部也能包含结构体类型的变量)int id; int age;// 用户名可以用指针指向一个字符串,也可以用char数组来存,如果是指针的话,那么数据不会存在结构体中,只会存放字符串的地址,但是如果是数组的话,数据会存放在结构体中char* name;
};
#include <stdio.h>int main() {// 也可以以局部形式存在struct Student { int id; int age;char* name; };
}
定义好结构体后,只需要使用结构体名称作为类型就可以创建一个结构体变量了:
#include <stdio.h>struct Student {int id;int age;char * name;
};int main() {//类型需要写为struct Student,后面就是变量名称struct Student s = {1, 18, "小明"}; //结构体包含多种类型的数据(它们是一个整体),只需要把这些数据依次写好放在花括号里面就行了
}
#include <stdio.h>// 也可以直接在花括号后面写上变量名称(多个用逗号隔开),声明一个全局变量
struct Student {int id;int age;char* name;
} s;int main() {}
这样就创建好了一个结构体变量,而这个结构体表示的就是学号为 1、年龄 18、名称为小明的结构体数据了。
当然,结构体的初始化需要注意:
#include <stdio.h>struct Student {int id;int age;char* name;
};int main() {// 如果只写一半,那么只会初始化其中一部分数据,剩余的内容相当于没有初始值,跟数组是一样的struct Student s1 = {1, 18}; // 也可以指定去初始化哪一个属性 .变量名称 = 初始值struct Student s2 = {2, .name = "小红"}; // 结构体变量.数据名称 (这里.也是一种运算符) 就可以访问结构体中存放的对应的数据了printf("id = %d, age = %d, name = %s\n", s1.id, s1.age, s1.name);printf("id = %d, age = %d, name = %s", s2.id, s2.age, s2.name);
}
id = 1, age = 18, name = (null)
id = 2, age = 0, name = 小红
当然也可以通过同样的方式对结构体中的数据进行修改:
#include <stdio.h>struct Student {int id;int age;char* name;
};int main() {struct Student s = {1, 18, "小明"};s.name = "小红";s.age = 17;printf("id = %d, age = %d, name = %s", s.id, s.age, s.name);
}
id = 1, age = 17, name = 小红
那么结构体在内存中占据的大小是如何计算的呢?比如下面的这个结构体
struct Object {int a;short b;char c;
};
这里我们可以借助sizeof
关键字来帮助计算:
#include <stdio.h>int main() {// sizeof能够计算数据在内存中所占据的空间大小(字节为单位)printf("int类型的大小是:%lu", sizeof(int));
}
int类型的大小是:4
当然也可以计算变量的值占据的大小:
#include <stdio.h>int main() {int arr[10];// 在判断非类型时,sizeof 括号可省printf("int arr[10]占据的大小是:%lu", sizeof arr);
}
int arr[10]占据的大小是:40
同样的,它也能计算结构体类型会占用多少的空间:
#include <stdio.h>struct Object {char a;int b;short c;
};int main() {// 直接填入struct Object作为类型printf("%lu", sizeof(struct Object));
}
12
可以看到结果是 12,那么,这个 12 字节是咋算出来的呢?
int(4字节)+ short(2字节)+ char(1字节) = 7字节
实际上结构体中的各个数据要求字节对齐,规则如下:
- 规则一: 结构体中元素按照定义顺序依次置于内存中,但并不是紧密排列的。从结构体首地址开始依次将元素放入内存时,元素会被放置在其自身对齐大小的整数倍地址上(0默认是所有大小的整数倍)
- 规则二: 如果结构体大小不是所有元素中最大对齐大小的整数倍,则结构体对齐到最大元素对齐大小的整数倍,填充空间放置到结构体末尾。
- 规则三: 基本数据类型的对齐大小为其自身的大小,结构体数据类型的对齐大小为其元素中最大对齐大小元素的对齐大小。
这里以下面的为例:
struct Object {// char占据1个字节char a; // int占据4个字节// 因为前面存了一个char,按理说应该从第2个字节开始存放// 但是根据规则一,必须在自己的整数倍位置上存放// 因为2不是4的整数倍位置,这时离1最近的下一个整数倍地址就是4了// 所以前面空3个字节的位置出来,然后再放置int b; // 前面存完int之后,就是从8开始了,刚好满足short(2字节)的整数倍// 但是根据规则二,整个结构体大小必须是最大对齐大小的整数倍(这里最大对齐大小是int,所以是4)// 存完short之后,只有10个字节,所以后面再补两个空字节,这样就是12个字节了short c;
};
前面介绍了结构体,现在可以将各种类型的数据全部安排到结构体中一起存放了。
不过仅仅只是使用结构体,还不够,可能还需要保存很多个学生的信息,所以需要使用结构体类型的数组来进行保存:
#include <stdio.h>struct Student {int id;int age;char* name;
};int main() {// 声明一个结构体类型的数组,其实和基本类型声明数组是一样的// 多个结构体数据用逗号隔开struct Student arr[3] = {{1, 18, "小明"},{2, 17, "小红"},{3, 18, "小刚"}};// 先通过arr[1]拿到第二个结构体,然后再通过同样的方式 .数据名称 就可以拿到对应的值了printf("%s", arr[1].name);
}
小红
当然,除了数组之外,还可以创建一个指向结构体的指针。
拿到结构体类型的指针后,实际上指向的就是结构体对应的内存地址,和之前一样,也可以通过地址去访问结构体中的数据:
#include <stdio.h>struct Student {int id;int age;char* name;
};int main() {struct Student student = {1, 18, "小明"};// 同样的,类型后面加上*就是一个结构体类型的指针了struct Student* p = &student;// 由于.运算符优先级更高,所以需要先使用*p得到地址上的值,然后再去访问对应数据printf("%s\n", (*p).name);// 上面的写法写起来太累了,可以使用简便写法// 使用 -> 运算符来快速将指针所指结构体的对应数据取出printf("%s", p->name);
}
小明
小明
再来看看结构体作为参数在函数之间进行传递时会经历什么:
#include <stdio.h>struct Student {int id;int age;char* name;
};void test(struct Student student){// 对传入的结构体中的年龄进行修改student.age = 19;
}int main() {struct Student student = {1, 18, "小明"};test(student);// 最后会是修改后的值吗?printf("%d", student.age);
}
18
可以看到在其他函数中对结构体内容的修改并没有对外面的结构体生效,因此,实际上结构体也是值传递,修改的只是另一个函数中的局部变量而已。
所以如果需要在另一个函数中处理外部的结构体,需要传递指针:
#include <stdio.h>struct Student {int id;int age;char* name;
};// 这里使用指针,那么现在就可以指向外部的结构体了
void test(struct Student* student) { // 别忘了指针怎么访问结构体内部数据的student->age = 19;
}int main() {struct Student student = {1, 18, "小明"};// 传递结构体的地址过去test(&student); printf("%d", student.age);
}
19
一般情况下推荐传递结构体的指针,而不是直接进行值传递。因为如果结构体非常大的话,光是数据拷贝就需要花费很大的精力,并且某些情况下可能根本用不到结构体中的所有数据,所以完全没必要浪费空间,使用指针反而是一种更好的方式。
联合体
联合体也可以在内部定义很多种类型的变量,但是它与结构体不同的是,它所有的变量共用同一个空间。
// 定义一个联合体类型唯一不同的就是前面的union了
union Object { int a;char b;float c;
};
来看看一个神奇的现象:
#include <stdio.h>union Object {int a;char b;float c;
};int main() {union Object object;// 先给a赋值66object.a = 66; // 访问bprintf("%d", object.b);
}
66
可以看到,修改的是 a,但 b 也变成 66 了。
这是因为它们共用了内存空间,实际上先将 a 修改为 66,那么就将这段内存空间上的值修改为了 66,因为内存空间共用,所以当读取 b 时,也会从这段内存空间中读取一个 char 长度的数据出来,所以得到的也是 66。
#include <stdio.h>union Object {int a;char b;float c;
};int main() {union Object object;object.a = 128;printf("%d", object.b);
}
-128
因为:128 = 1000 0000,所以用 char 读取后,由于第一位是符号位,于是就变成了 -128。
那么联合体的大小又是如何决定的呢?
#include <stdio.h>union Object {int a;char b;float c;
};int main() {printf("%lu", sizeof(union Object));
}
4
实际上,联合体的大小至少是其内部最大类型的大小,这里最大是 int 所以就是 4。当然,当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。
联合体的其他使用基本与结构体差不多,这里就不提了。
枚举
最后来看一下枚举类型,枚举类型一般用于表示一些预设好的整数常量,比如风扇有低、中、高三个档位,所以总是希望别人使用预设好的这三个档位,而不希望使用其他的档位,因为风扇就只设计了这三个档位。
这时就可以告诉别人,风扇有哪几个档位,这种情况使用枚举就非常适合。在程序中,只能使用基本数据类型对这三种档位进行区分,这样显然可读性不够,别人怎么知道哪个代表哪个档位呢?而使用枚举就没有这些问题了。
可以创建多个自定义名称的枚举,命名规则和变量差不多。可以每一个枚举对应一个整数值,这样的话,就不需要去记忆每个数值代表的是什么档位了,直接根据枚举的名称来进行分辨:
#include <stdio.h>enum status { low = 1,middle = 2,high = 3 };int main() {// 直接定义即可,类型为enum + 枚举名称,后面是变量名称,值可以直接写对应的枚举enum status a = low;printf("%d", a);
}
1
进行判断也会方便很多:
#include <stdio.h>enum status { low = 1,middle = 2,high = 3 };int main() {enum status a = high;// 判断起来就方便多了if (a == low) {printf("低档位");} else if (a == high) {printf("高档位");} else {printf("中档位");}
}
当然也可以直接加入到switch
语句中:
#include <stdio.h>enum status { low = 1,middle = 2,high = 3 };int main() {enum status a = high;switch (a) {case low:printf("低档位");break;case high:printf("高档位");break;case middle:printf("中档位");break;default:printf("不存在的档位");}
}
高档位
不过在枚举变量定义时需要注意:
// 如果不给初始值的话,那么会从第一个枚举开始,默认值为0,后续依次+1
enum status {low, middle, high};
所以这里的 low 就是 0,middle 就是 1,high 就是 2 了。
如果中途设定呢?
// 这里我们给middle设定为6
enum status {low, middle = 6, high};
这时 low 由于是第一个,所以还是从 0 开始,不过 middle 这里已经指定为 6 了,所以紧跟着的 high 初始值就是middle 的值 +1 了,因此 low 现在是 0,middle就是 6,high就是 7 了。
typedef关键字
这里最后还要提一下 typedef
关键字,这个关键字用于给指定的类型起别名。
// typedef 类型名称 自定义类型别名
typedef int lbwnb;
比如这里给 int 起了一个别名 lbwnb,那么现在不仅可以使用 int 来表示一个 int 整数,而且也可以使用别名作为类型名称了:
#include <stdio.h>typedef int lbwnb;int main() {// 类型名称直接写成别名,实际上本质还是intlbwnb i = 666;printf("%d", i);
}
666
再比如:
#include <stdio.h>// const char * 我们就起个名称为String表示字符串
typedef const char* String; int main() {// 这样就很像Java了String str = "Hello World!"; printf(str);
}
Hello World!
当然除了基本类型之外,包括指针、结构体、联合体、枚举等都可以使用这个关键字来完全起别名操作:
#include <stdio.h>// 为了方便可以直接写到后面
typedef struct test {int age;char name[10];
} Student; int main() {// 直接使用别名,甚至struct关键字都不用加了Student student = {18, "小明"};
}