结构体
结构体在C语言中是一种重要的数据类型,或者说是一种用户自定义的相同或不同数据类型的集合。可以帮助我们封装一组相关数据,使其数据呈现更直观。例如我们想要统计一个学校学生的基本信息。可以将一个同学的信息按照如下存储。
typedef struct student{
int grade;
int age;
char *name;
char score;
}student;
这样我们输入和查询数据的时候都可以更快的定位到每个学生的具体信息。
#include<stdio.h>
#include<stdlib.h>
typedef struct student{
int grade;
int age;
char *name;
char score;
}student;
student edit(int grade, int age, char* name, char score){
student stu;
stu.grade = grade;
stu.age = age;
stu.name = name;
stu.score = score;
return stu;
}
void search(student stu){
printf("[stu.name] = %s\n", stu.name);
printf("[stu.grade] = %d\n", stu.grade);
printf("[stu.age] = %d\n", stu.age);
printf("[stu.score] = %c\n", stu.score);
}
int main(){
student nic;
char nicname[] = "Nicholas";
nic = edit(4, 13, nicname, 'A');
search(nic);
}
输出结果:
>>[stu.name] = Nicholas
[stu.grade] = 4
[stu.age] = 13
[stu.score] = A
直接引用和间接引用
在访问结构体内部数据时,根据访问形式的不同,分为直接引用和间接引用
- 直接引用:通过结构体实例访问内部数据 访问形式如上代码示例(object.data)
- 间接引用:通过指针访问结构体内部数据,访问形式为->(pointer->data)
匿名结构体
匿名结构体,在声明的时没有定义结构体名称,所以匿名结构体只有在声明的时候可以被访问。匿名结构体我们会在下文共用体中演示。
结构体的大小
结构体作为很多数据类型的集合,所以其结构体本身的大小与集合内部的数据类型数量以及大小相关。但是它开辟空间的大小也有遵循一定的原则。如下两段代码
#include<stdio.h>
typedef struct stu_a{
int age;
char score;
char gender;
}stu_a;
typedef struct stu_b{
char score;
int age;
char gender;
}stu_b;
int main(){
printf("sizeof(stu_a) = %lu\n", sizeof(stu_a));
printf("sizeof(stu_b) = %lu", sizeof(stu_b));
}
运行结果
>>sizeof(stu_a) = 8
sizeof(stu_b) = 12
结构体开辟空间会根据内部最大的数据类型大小为单位开辟。上述示例中int 占4字节而char占1字节,所以每次开辟空间都是会加4字节。如上述示例结构体stu_a,先申请4个字节,age刚好占满,而后继续申请四个字节,下面的score占一个字节,还剩三个,所以gender还可以占一个不需要额外申请。所以总共是8字节但是,使用的字节数只有6。
但是看到结构体stu_b同样的数据类型,只是位置不一样了,但是空间也略有不同。刚开始申请4个字节,score占一个字节,剩下三个字节,下面的age是int类型需要四个字节,那么他就会重新申请一块空间,这里需要注意的是,申请空间是以4字节为单位的,所以此时为8个字节,score占新申请的四个字节,因为数据是往后排放的,所以age后没有空间,则需要再申请4个字节,将gender放入。所以说结构体中数据的声明位置也很重要,应尽量让相同类型的变量相邻,以免造成空间浪费。
共用体
共用体同结构一样也是一种可以自定义的数据结构,在共用体中可以定义结构体,但是与结构体不同的是,我们可以再内存上的相同位置存储和访问不同的数据。
#include<stdio.h>
union node{
struct{
unsigned char a1;
unsigned char a2;
unsigned char a3;
unsigned char a4;
}a;
int num;
};
int main(){
union node node1;
printf("sizeof(node1) = %lu", sizeof(node1));
}
运行结果:
>>sizeof(node1) = 4%
可以看到我们在共用体中定义了两个变量,一个含四个字符的结构体a,一个整型num。两种数据类型各占四个字节,但是该联合体大小也只有四字节,也就是说a与num存储在一块内存上。那么我们容易想到,存储在一块内存上,一组数据变动是不是会影响到另一组?下面通过一个ip转整数的示例来验证一下我们的想法。
#include<stdio.h>
union ip{
struct{
unsigned char a1;
unsigned char a2;
unsigned char a3;
unsigned char a4;
}n;
int num;
};
int main(){
char str[20];
int arr[4];
union ip test;
while(~scanf("%s", str)){
sscanf(str,"%d.%d.%d.%d",arr, arr + 1 , arr + 2, arr + 3);
test.n.a1 = arr[0];
test.n.a2 = arr[1];
test.n.a3 = arr[2];
test.n.a4 = arr[3];
printf("ip = %d.%d.%d.%d\n", arr[0], arr[1], arr[2], arr[3]);
printf("num = %d\n",test.num);
printf("---------------------------------------------\n");
}
}
这里我们当我们分别存入不同的数据时,共用体中的num也在不断的发生变化。当时在这我们发现了一个问题,就是我们在输入后面两个样例的时候,按道理来讲因为二者存储IP的值只相差1,所以在我们访问num的时候,应该是相邻的两个整型才对,但是这里可以看到值相差很多,这就涉及到了另外一部分知识。
大端机和小端机
大端机:数字的低位存储在地址的高位
小端机:数字的低位存储在地址的低位
上面这两个概念只需要清楚数字低位与地址高位的含义即可,如下图:
那么我们可以很清楚的看到如果是大端机的话,就会是我们预期的那样两次num的结果是相邻的两个数字,但是我们现在使用的计算机普遍是小端机,所以在计算机中存储顺序是反过来的即1 .0 168 192。也就是如果我们倒着输入存储的话,那么就会出现我们预期的结果了。
检验自己的计算机是大端机还是小端机
我们知道一个int类型大小为4个字节,char类型为一个字节,那么我们通过类型转换,打印出这四个字节所在地址的信息便可知道我们的机器是不是小端机了。
#include<stdio.h>
//测试是否为小端机
void is_little(){
int num = 1;
printf("int[0] = %d\n", ((char *)(&num))[0]);
printf("int[1] = %d\n", ((char *)(&num))[1]);
printf("int[2] = %d\n", ((char *)(&num))[2]);
printf("int[3] = %d\n", ((char *)(&num))[3]);
}
int main(){
is_little();
}
可以看到存储的1应该是个位,但是现在在地址的地位上。
或者我们用更清晰的例子来看一下
这里的205只是进制的转换,没有涉及到符号位,我们手算一下
11001101
=
−
(
1001101
)
对
括
号
里
面
取
反
码
后
加
一
−
(
0110010
+
1
)
=
−
(
32
+
16
+
2
+
1
)
=
−
51
11001101 = -(1001101)\\对括号里面取反码后加一\\-(0110010 + 1) = -(32+16+2+1) = -51
11001101=−(1001101)对括号里面取反码后加一−(0110010+1)=−(32+16+2+1)=−51
由此可以验证我们的计算机是小端机
由该部分我们可以拓展出字节序的概念。这里不过多深入,字节序分为本地字节序(主机字节序)和网络字节序,本地字节序描述的内容即为我们使用的计算机为大端机还是小端机。当我们使用网络编程的时候,就需要把本地字节序转换成网络字节序,网络字节序会根据对端的自动转换成主机字节序。