首先在这一章我们要讲的是结构与联合。
在平时,我们往往需要对数据进行成组的形式进行存储和访问,所以,在这里C提供了两种可以存储多种数据的数据类型:数组和结构。
数组,这是一个同类型元素的集合,而结构体,可以存储不同类型的元素,并且这两个类型的访问方式也是不一样的,数组是通过下标的形式进行访问的,而结构体是通过名字来访问。所以,一个结构变量在表达式中使用时,他并不被替换成一个指针。
对于结构体,我们先要认清其中的各种组成
struct A
{
int a ;
char b ;
};
首先在这个里面
在这个里面,我们要清楚,A是这个结构体的标签,里面的a,b是这个结构体的成员,在这里我们没有给这个结构体提供变量,所以并没有创建变量。想要创建变量,就可以这样来写,struct A x
;在这里,x是一个类型为struct A类型的变量。
另外,在写程序中,结构体我们会更多的和typedef进行搭配使用。
例:
typedef struct BiTNode
{
datatype data ;
struct BiTNode *lchild , * rchild;
} BiTNode, *BiTree;
在这里对这个结构体类型进行重定义,在以后BiTNode
就相当于struct BiTNode
,而BiTree就相当于struct BiTNode *
。当初学习数据结构的时候,记得无数人都因为这个疑问产生过疑惑。所以在书写链表等数据结构时,推荐大家都使用这种方式书写代码,另外,在这提一个技巧,我们可以把结构体放到头文件中,然后源文件只需要使用预处理指令就可以把头文件包含进来了。
其实这个就typedef int datatype
是一样的。用datatype把int给替代了。
结构体就是类的一个过渡。
访问结构体成员,需要结构体变量的名字,在对成员进行引用。
这种方式被称为是通过点操作符(.)进行访问,点操作符,接受两个操作数,它的左边是结构变量的名字,右边是需要访问的成员的名字。
struct A
{
int a ;
char b ;
};
int main()
{
struct A x;
x.a =5;
return 0 ;
}
在这里有一个有趣的((comp.sa)[4]).c
说的就是comp这个变量的成员sa是一个数组,然后选择(comp.sa)[4]
,即这个数组的第四个元素,而刚好,这个歌元素又是一个结构体,所以再访问这个元素的成员.
C语言的强大之处在于指针的存在,当然,我们的结构体也需要使用指针,所以产生了一种通过指针的间接访问的形式。
struct A
{
int a ;
char b ;
};
int main()
{
struct A x;
struct A *p ;
p = &x ;
p->a = 5;
return 0 ;
}
p->a
等价于(*p).a
,这个又等价于x.a
。
p所指向的结构体变量中的a成员。
->这个操作符我们常常称呼为箭头操作符,这个操作符左边必须是一个指向结构体类型的指针。而右边必须是一个结构体中的成员。
struct A
{
int a;
char b;
struct A c;
};
在这里面对于c而言就相当于又是另一个完整的struct A的结构,这样就会重复进行下去,其实这个就想我们给了一个没有限制条件的递归的一个程序,你说是不是呢?
另外,看下面着一个例子:
typedef struct A
{
int a;
char b;
NODE *c;
}NODE;
这个目的是为了创建类型名NODE,但是,失败了,类型名是在最后的末尾定义的,但是在结构体内部,你就引用了类型名,这样是肯定不符合逻辑的,相当于你把你没定义的东西拿出来用,用完才给它定义。
所以在链表等数据结构中,我们时常这样写:
typedef struct node
{
int data;
struct node *pnext;
}NODE,*PNODE;
对于一些一个结构中包含了另外一个结构的一个或者多个成员。那么那么我们应该怎么做呢,这时我们要使用一个不完整声明,它声明一个作为结构标签的标识符,然后,我们可以把这个标签用在不需要知道这个结构的长度的声明中,如果声明指向这个结构的指针,那么接下来的声明把这个标签与成员列表联系在一起。
struct B;
struct A
{
int a;
char b;
struct B *ptail;
};
struct B
{
int a;
char b;
struct A *ptail;
};
结构体的初始化,和数组十分类似。
例:
struct B
{
int a;
int arr[3];
}x = { 5, { 1, 2 } };
结构体变量不能加减乘除,但是可以相互赋值。
普通结构体变量和结构体指针变量作为函数传参的问题。
对于这个问题,我在前期有一篇博客已经讲述过了。大家可以去看一下。
C语言之内存字节对齐
在这里,为了让结构体方便访问,采取了一种牺牲空间换取时间的方式,在这里首先我们要知道,32位一般是采取4字节进行操作读取的。所以:
struct arr
{
int a;
char c;
int b;
char d;
};
对这个结构体,
如果我们这样存储的话:
所以,内存为了方便偏移读取:采用下面这种方式进行存储!
在这里我们要知道linux和VS下的对齐数是不同的,linux下是4,VS下是8。
对齐遵循的规则:
1.第一个成员永远对齐0偏移处。
2.每个变量的对齐数是它的大小与默认对齐数的较小值。
3.结构体的大小时最大对齐数的整数倍。
当出现结构体嵌套式,看所有对齐数中对打对齐数的整数倍。
另外,在这里,我们可以使用宏offsetof确定结构体中某个成员的实际位置。
offsetof(type,member)
它表示你的member存储的位置距离开始存储的位置多少个字节。
在这其实我感觉和数组作为参数的时候非常像,当我们把数组作为参数的时候,我们传递的是地址,因为如果我们传递值的话,会重新开辟很大一块空间,所以为了效率更高,我们直接把地址传递进去,用指针来接收地址。
说到这,我想你也就大概有些了解了,当一个函数你把一个结构体传进去,如果你采用传值的方式,那么当然要开辟一个相同的结构体作为形式参量。这样效率很低,所以我们也采用传地址的方式,然后用一个结构体类型的指针进行接收地址就好了。
void add(struct A x)
{
;
}
void add(struct A *P)
{
;
}
如果你学习过数据结构,关于链表顺序表好多都是采用传地址的方式进行操作的。
关于位段,C语言允许在一个结构体中以位为单位来指定其成员所占内存长度,这种以位为单位的成员称为“位段”或称“位域”( bit field) 。利用位段能够用较少的位数存储数据。
首先我们要知道,位段成员必须声明为int,signed int或者unsign int类型。然后,在成员名后面是一个冒号和一个整数,这个整数也就是说这个位段所占用的位的数目。
注意:当声明位段为int类型时,编译器会决定它是无符号还是有符号。另外,位段和平台会有很大的关系,所以我们在一些可移植性的程序中最好不要使用位段。
例:
#include<stdio.h>
#include<stdlib.h>
struct A
{
unsigned int a : 2;
unsigned int b : 20;
unsigned int c : 10;
};
int main()
{
struct A x;
printf("%d", sizeof(x));
system("pause");
return 0;
}
在这,我们就相当于把4个字节32位中,2位给了a,20位给了b,10位给了c。
对于位段的结构体,他的大小永远是unsigned int大小的整数倍,也就是说它的位段和如果小于等于一个unsigned int所占位大小时,他就是一个unsigned int的大小,如果超了,就是两个unsigned int的大小。
其实一提到联合,我第一下想到的是对于机器大小端的验证:
#include<stdio.h>
#include<stdlib.h>
union node
{
int num;
char ch;
};
int main()
{
union node p;
//方法一
p.num = 0x12345678;
if (p.ch == 0x78)
{
printf("Little endian\n");
}
else
{
printf("Big endian\n");
}
//方法二
int num = 0x12345678;
char *q = #
if (*q == 0x78)
{
printf("Little endian\n");
}
else
{
printf("Big endian\n");
}
return 0;
}
在这里的第一种方法所运用的就是union的特点。union进行存储是开辟最大的成员作为存储空间。所以,当你给num进行赋值,你访问ch也可以得到值。