14、结构体指针
结构体是一个变量里集合了多个变量。结构体为一个聚合力不同类型或相似类型变量的变量。结构体与指针的相关性非常强。结构体提供了非常直观的方式来模拟用户定义的实体(记录、分组格式、图像头,等等)。
1、定义结构体
C语言常用关键字struct定义结构体。结构体能包括任何C语言允许的类型变量。他也能在其内部包含另一个结构体变量,以下为定义结构体的典型示例:
struct variable_name { viriable_type1 variable_name1; viriable_type2 variable_name2; viriable_type3 viriable_name3; ... }
例如:
struct header { int header_version; char tagid; char signature[4]; int data_offset; }
根据不同习惯,有时候也会有以下几种定义形式
第一种是最基本的结构体定义,其定义了一个结构体A。
struct A //第一种 { int a; };
第二种则是在定义了一个结构体B的同时定义了一个结构体B的变量m。
struct B //第二种 { int b; }m;
第三种结构体定义没有给出该结构体的名称,但是定义了一个该结构体的变量n,也就是说,若是想要在别处定义该结构体的变量是不行的,只有变量n这种在定义结构体的同时定义变量才行。
struct //第三种 { int c; }n;
第四种结构体定义在第一种结构定义的基础上加了关键字typedef,此时我们将struct D{int d}看成是一个数据类型,但是因为并没有给出别名,直接用D定义变量是不行的。如D test;,不能直接这样定义变量test。但struct D test;可行。
typedef struct D //第四种 { int d; };
第五种结构体定义在第四种结构体定义的基础上加上了别名x,此时像在第四种结构体定义中说得那样,此时的结构体E有别名x,故可以用x定义E的结构体变量。用E不能直接定义,需要在前面加struct,如struct E test;。
typedef struct E //第五种 { int e; }x;
第六种结构体定义在第五种的基础上减去了结构体名,但是若是直接使用y来定义该结构体类型的变量也是可以的。如y test;。(常用)
typedef struct //第六种 { int f; }y;
2、声明结构体变量
结构体变量的声明与C语言中其他变量相似,声明过程通常包括两步。
(1)定义结构体
(2)声明相应结构体类型的变量。
示例:
struct date { int day; int month; int year; }; struct date currentdate;//声明date结构体类型变量”currentdate"
3、访问结构体成员
使用点(.)操作符访问结构体变量的成员字段。其语法如下:
<变量名>.<成员字段名>
下面源码演示了如何用点操作符访问结构体成员字段:
#include <stdio.h> #include <string.h> int main() { struct date { int day; int month; int year; }; struct date current; current.day = 15; current.month = 3; current.year = 2018; return 0; }
4、初始化结构体变量
有两种初始化结构体变量的方法。
(1)声明结构体变量后,单独初始化每个成员。点操作用于访问成员变量,如上例。
(2)通过集合符号对结构体变量进行初始化。在集合符号中,值以有序形式,用逗号隔开,左右用花括号括起来。成员字段按指定顺序查值并初始化。如下例
#include <stdio.h> #include <string.h> int main() { struct date { int day; int month; int year; }; struct date current = { 15,3,2018 }; return 0; }
5、结构体嵌套
上述代码给出了结构体如何聚合其他数据类型。有时结构体内也可能嵌入结构体变量。如下定义所示:
#include <stdio.h> #include <string.h> int main() { struct header { int version; int signature; struct tagname { int id; int offset; }tigid; }; struct header hdrinfo; hdrinfo.version = 0; hdrinfo.signature = 5; hdrinfo.tigid.id = 1; hdrinfo.tigid.offset = 10; return 0; }
加载到内存时结构体类型变量与数组非常相似。所有成员字段占用连续的内存位置。
struct data
{
int i;
char j;
int k;
}
6、结构体存储
结构体内成员在存储空间的位置如下图:
结构体变量的大小等于它包含所有变量的总大小。假定字符型变量占用一个字节、整型变量占用四个字节,下面运用一个示例来对结构体占用字节进行理解:
#include <stdio.h>
#include <string.h>
int main()
{
struct data
{
int i;
int j;
int k;
};
struct data v1;
printf("Size of struct data=%d\n", sizeof(struct data));
return 0;
}
结果如下:
7、结构体填充
结构体填充是编译器用来对其内存偏移数据的步骤。
数据对齐
当CPU读写内存时,他都在通过小块内进行(成为字长或4字节)。这种安排增加了系统性能,能有效的将数据放在字长整数被的偏移/地址。
下图中,我们假定处理器的任务是从内存读取四个字并将器放入寄存器。这是理想情况,因为偏移量是字长(0,1,2)整数倍。处理器取一个字需要一个周期。
另外一种情况数据没有存储在那些字长整数倍的偏移量中,如下图所示,字大小的数据存储位置从第二到第三到第四到第五。这里,假定第0位置和第1位置要么是空的,要么已存数据。
首先,处理器从0位置加载一个字节并且项左偏移两个字节得到最高两个字节。接着从第一位置取另一个字节并且向后偏移两个字节得到最低两个字节。如此操作后,合并两个字节得到最后字节。
最后,当要去字节未对齐时,处理器取一个字节需要两个周期。额外周期会对代码产生极大影响。有时某些处理器会出现对齐异常情况,读取过程会越来越慢。
实际上,不同数据类型需要按其大小自然对其。对于char类型需要对齐1个字节,对于short int类型需要对齐4个字节,对于double类型,需要对齐8个字节等。
字节填充
由上可知,为了提高性能,编译器尽量在结构体中利用结构体填充方法进行数据对齐。这里就可以发现一个问题。
当结构体定义为
struct data { int i; char j; char k; };
所占字节为8,但是定义为
struct data { char j; int i; char k; };
所占空间字节为12,前者比后者占用内存空间节省了33%,这是因为系统禁止编译器在一个结构的起始位置跳过几个字节来,满足对齐要求,只有当存储成员满足正确的对齐要求时,成员之间才可能出现用于填充的额外内存空间,所以编译器添加需要的字节数来对齐结构体数据成员。
示例如下:
#include <stdio.h> #include <string.h> int main() { struct data { int i; char j; int k; }; struct data v1; struct data *dsptr; dsptr = (struct data*)malloc(sizeof(struct data)); printf("Size of struct data=%d\n", sizeof(struct data)); printf("Address of number int i=%u\n", &(dsptr->i)); printf("Address of number char j=%u\n", &(dsptr->j)); printf("Address of number int k=%u\n", &(dsptr->k)); return 0; }
运行结果如下:
不难看出k从j后的4个字节偏移处开始。
8、一些应该避免结构体填充的地方
数据结构体经常用于图像、数据包等应用,此时可能不希望出现结构体填充的现象。比如编写一个如下假定的GIF图像结构体
结构体如下:
struct git_hdr { char signature[3]; char version[3]; int width; int heigth; char colormap; char bgcolor; char ratio; };
由结构体填充可知,这个结构体中的内存分配如下:
下面通过代码进行检验:
#include <stdio.h> #include <string.h> int main() { struct git_hdr { char signature[3]; char version[3]; int width; int height; char colormap; char bgcolor; char ratio; }; struct git_hdr v1; struct git_hdr *dsptr; printf("Size of struct data=%d\n", sizeof(struct git_hdr)); dsptr = (struct git_hdr*)malloc(sizeof(struct git_hdr)); printf("Offset of signature=%d\n", &(dsptr->signature[0])-&(dsptr->signature[0])); printf("Offset of version=%d\n", &(dsptr->version[0]) - &(dsptr->signature[0])); printf("Offset of width=%d\n", (char*)&(dsptr->width) - &(dsptr->signature[0])); printf("Offset of height=%d\n", (char*)&(dsptr->height) - &(dsptr->signature[0])); printf("Offset of colormap=%d\n", &(dsptr->colormap) - &(dsptr->signature[0])); printf("Offset of bgcolor=%d\n", &(dsptr->bgcolor) - &(dsptr->signature[0])); printf("Offset of ratio=%d\n", &(dsptr->ratio) - &(dsptr->signature[0])); return 0; }
运行结果:
可以看出结构体填充使我们的GIF文件解码会产生错误偏移量的值。所以,当我们使用图像头文件、二进制文件头和网络数据包,以及试图访问TCP/IP报头时,必须避免使用结构体填充。
9、避免结构体填充的方法
为避免结构体填充,我们使用#pragma指令或在GNU C编译器下使用pack指令。
PRAGMA指令使用如下:
#pragma pack(1)//1-byte alignment struct data { int I; char c; int j ; }
指令有两种使用方法
1、直接处理结构体成员
struct data { int i _attribute_((_packed_)); char c _attribute_((_packed_)); int _attribute_((_packed_)); }
2、处理整个结构体
struct data { int i; char c; int k; }_attribute_((_packed_));
使用前面#include <stdio.h>
#include <string.h> #include <malloc.h> int main() { #pragma pack(pop) struct git_hdr { char signature[3]; char version[3]; int width; int height; char colormap; char bgcolor; char ratio; }__attribute__((packed)); struct git_hdr *dsptr; printf("Size of struct data=%d\n", sizeof(struct git_hdr)); dsptr = (struct git_hdr*)malloc(sizeof(struct git_hdr)); printf("Offset of signature=%d\n", &(dsptr->signature[0])-&(dsptr->signature[0])); printf("Offset of version=%d\n", &(dsptr->version[0]) - &(dsptr->signature[0])); printf("Offset of width=%d\n", (char*)&(dsptr->width) - &(dsptr->signature[0])); printf("Offset of height=%d\n", (char*)&(dsptr->height) - &(dsptr->signature[0])); printf("Offset of colormap=%d\n", &(dsptr->colormap) - &(dsptr->signature[0])); printf("Offset of bgcolor=%d\n", &(dsptr->bgcolor) - &(dsptr->signature[0])); printf("Offset of ratio=%d\n", &(dsptr->ratio) - &(dsptr->signature[0])); return 0
程序运行结果如下(这里在linux下运行是因为windows下不支持__attribute__((packed))):
10、结构体赋值于复制
将结构体变量赋值给另一个结构体变量的工作于正常赋值一样,将各个成员变量从一个结构体复制到另一个结构体。
#include <stdio.h> #include <string.h> #include <malloc.h> int main() { struct data { int i; char c; int j; int arr[2]; }; struct datawptr { int i; char *c; }; struct datawptr dptr1; struct datawptr dptr2; struct data svar1; struct data svar2; svar1.c = 'a'; svar1.i = 1; svar1.j = 2; svar1.arr[0] = 10; svar1.arr[1] = 20; svar2 = svar1; printf("Value of second variable \n"); printf("Member c=%c\n", svar2.c); printf("Member c=%c\n", svar2.c); printf("Member c=%c\n", svar2.c); printf("Member c=%c\n", svar2.c); printf("Member c=%c\n", svar2.c); dptr1.i = 10; dptr1.c = (char*)malloc(sizeof(char)); *(dptr1.c) = 'c'; dptr2.c = (char*)malloc(sizeof(char)); dptr2 = dptr1; printf("int member=%d\n", dptr2.i); printf("char ptr member =%c\n", *(dptr2.c)); return 0; }
程序运行结果如下:
也可使用库函数memcpy()赋值操作实现相同效果。但是当data结构体包含指针类型成员时要小心,因为赋值操作符不仅仅复制值,也复制指针变量的值(即结构体指针指向其他变量的地址) 。之后,当被赋值变量修改了该地址的存储值,会导致最终修改了源变量地址的存储值。
#include <stdio.h> #include <string.h> #include <malloc.h> int main() { struct datawptr { int i; char *c; }; struct datawptr dptr1; struct datawptr dptr2; dptr1.i = 10; dptr1.c = (char*)malloc(sizeof(char)); *(dptr1.c) = 'c'; dptr2.c = (char*)malloc(sizeof(char)); memcpy(&dptr2, &dptr1, sizeof(struct datawptr)); printf("int member value of 2nd variable =%d\n", dptr2.i); printf("char ptr member of 2nd variable =%c\n", *(dptr2.c)); printf("value of char ptr in 1st variable=%p\n", dptr1.c); printf("value of char ptr in 2nd variable=%p\n", dptr2.c); printf("changing value of 2nd memeber in 2nd variable (dptr2)\n"); *(dptr2.c) = 'a'; printf("value of char ptr of 2nd variable =%c and 1st variable =%c\n", *(dptr2.c), *(dptr1.c)); return 0; }
程序运行结果如下:
在上述情况下,如果我们试图分别释放两个变量的内存,会产生段错误(分段错误),因为通过第一个变量第一次调用free会再次释放内存,通过第二个变量第二次调用free会导致段错误,原因为试图第二次释放相同的内存空间。
11、结构体指针
结构体指针声明
结构体指针变量声明与其他指针变量声明类似。
struct <结构体名>*<变量名>
实例:
struct data { int i; char c; int k; }; struct data *var;//声明data结构体类型指针变量"var"
访问成员变量
采用结构体指针访问结构体数据类型的成员变量用到两种操作符。我们假定variable_name为某个struct data类型的指针变量。
1、点操作符(.)方法
本方法使用点操作符访问结构体变量的各个成员片段。
(*变量名).成员字段名; 示例: (*var).c;
由于我们通过指针变量访问成员字段,首先需要解引用变量,然后利用点操作符访问成员字段。
需要注意的是:点操作符(.)比“取值”“操作符(*)有更高的优先级。如果我们分析没有括号的优先级时,应该注意其优先级先后关系。比如:
*(var.c );
这是错误的,因为有更高优先级编译用点操作符编译代码,最后指令将试图作为一个值来访问指针变量,这是错误的。应该改为
*var.c
2、箭头操作符(->)方法
本方法中使用箭头操作符访问结构体变量的各个成员字段。
变量名->成员字段 var->c;
下列源码演示了上面两种方法的具体用法
#include <stdio.h> #include <string.h> #include <malloc.h> int main() { struct data { int i; char c; int j; int arr[2]; }; struct data *sptr; struct data svar; sptr = (struct data*)malloc(sizeof(struct data));//下述代码使用箭头操作符->访问成员字段 sptr->c='c'; sptr->i = 10; sptr->j = 20; printf("%c ,%d ,%d\n", sptr->c, sptr->i, sptr->j); (*sptr).c = 'd'; (*sptr).i = 30; (*sptr).j = 40; printf("%c ,%d ,%d\n", sptr->c, sptr->i, sptr->j); svar.c = 'a'; svar.i = 1; svar.j = 2; printf("%c ,%d ,%d\n", svar.c, svar.i, svar.j); (&svar)->c = 'c'; (&svar)->i = 3; (&svar)->j = 4; printf("%c ,%d ,%d\n", svar.c, svar.i, svar.j); return 0; }
程序运行结果如下:
传递结构体指针变量
结构体指针变量能够传递给函数。传递结构体指针比传递值有优势。如前所述,当指针变量被传递并且如果值被修改,则在调用着作用域内更新是有效的。假定我们有一个超过15个数据成员的非常大的结构体变量,相比传递地址(这时使用指针变量),如果按值传递这个变量到函数将会花费更多的时间。
struct node { int data; char c; }; int main() { struct node v1; struct nod* p1 = &v1; foo_passbyvalue(v1); foo_passbyaddr(p1); } void foo_passbyvalue(struct node v)//按值传递 { //做某事 } void foo_passbyaddr(stuct node* p)//按址传递 { //做某事 }
12、常见错误
#include <stdio.h> #include <string.h> #include <malloc.h> struct node { int data; }; void addnode(struct node* n1) { n1 = (struct node*)malloc(sizeof(struct node)); n1->data = 9; } int main() { struct node* n1 = NULL; addnode(n1); return 0; }
上述针对指针赋值,以传递指针变量给函数企图修改结构体的做法是错误的,这种方法在函数调用中很有效,但是上述情况中传递的是指针变量的值。所以调用addnode()后,变量n1仍然指向NULL。需要修改如下
#include <stdio.h> #include <string.h> #include <malloc.h> struct node { int data; }; void addnode(struct node** n1) { *n1 = (struct node*)malloc(sizeof(struct node)); (*n1)->data = 9; } int main() { struct node* n1 = NULL; addnode(&n1); return 0; }
13、结构体指针的强制转换
结构体指针类型转换是使用普通结构体指针编程时经常用到的方法。类型转换就是将一种数据类型强制转换成另一种数据类型变量的方法。下面给出一个实例:
#include <stdio.h> #include <string.h> #include <malloc.h> struct signature* extractsignatue(struct data* d) { struct signature* sig = (struct signature*)d; return sig; } struct id* extructid(struct data* d) { struct id* idv = (struct id*)d; return idv; } int main() { struct signature { char sign; char version; }; struct id { char id; char platform; }; struct data { struct signature sig; struct id idv; char data[100]; }; struct data* img; //receivedata(img); struct signature* sign = extractsignatue(&img); struct id* idval = extructid(&img); }
13、自引用结构体
结构体能够将指针变量作为其成员字段,具体一点,我们能声明一个包含它的结构体相同的指针变量类型的成员字段。
例如:
struct node { int data; struct node* self; }
通过自引用结构体,也就产生了许多复杂的数据结构(链表、树、图等)的构造模块。下面选择一些基本的进行入门。
1、链表
链表也可以称为对象链,依照第一个和最后一个对象的特殊规则,其每个对象指向下一个。第一个对象总指向根对象,最后一个对象总指向某些特殊值(NULL)来标记列表/链的末端。总通过特殊对象根访问链表类型。其关系图如下:
按照如下步骤创建链表:
1、在开始出添加节点。
2、在末尾出添加节点。
3、插入排序。
链表的其他执行操作如下;
1、搜索链表。
2、删除链表节点。
3、统计链表节点。
示例:
#include <stdio.h> #include <string.h> #include <malloc.h> struct node { int data; struct node* next; }; struct node* createnode(int data) { struct node* n1 = (struct node*)malloc(sizeof(struct node)); n1->data = data; n1->next = NULL; return n1; } void addatend(struct node** root, struct node* n) { struct node* temp = *root; if (temp == NULL) { *root = n; } else { while (temp->next != NULL) temp = temp->next; temp->next = n; } } int main() { struct node* root = NULL; int i=0; for ( i=0; i < 10; i++) { addatend(&root, createnode(i)); } return 0; }
上面的代码演示了如何用函数addatend(struct node** root,struct node* n)构造一个链表。使用辅助函数createnode(int data)取数据并返回一个包含复制的数据部分和下一个链表设置为NULL的新节点。将该节点传递给addatend()函数,。在addatend()函数中,检查第一个(根节点)是否为NULL。如果为NULL,则将新节点链接到根部,否则代码遍历寻找到最后一个为NULL的节点并在这里添加新节点。
2、二叉树(BST)
二叉搜索数,其结构如下图所示,且要求,在任何层、任何节点的直接左节点的存储值总是小于或等于其节点本身的值。所以节点的左子树总包含哪些存储值小于或等于它的节点。同理,右子树总包含那些存储值大于或等于它的节点。
二叉搜索树(BST)的数据结构体如下:
struct node { int data; struct node*left; struct node*right; };
通常我们有数据字段,每个节点上的任何信息都能被保存。另外两个最重要的字段为左子指针和右子指针。这两个变量有助于构建实际的树。
示例:创建BST
#include <stdio.h> #include <string.h> #include <malloc.h> struct node { int data; struct node*left; struct node*right; }; struct node* createnode(int data) { struct node* n1 = (struct node*)malloc(sizeof(struct node)); n1->data = data; n1->left = NULL; n1->right = NULL; return n1; } void insertnode(struct node** root, struct node* n) { struct node* temp = *root; if (temp == NULL) { *root = n; } else { if (n->data < temp->data) { insertnode(&(temp->left), n); } else if (n->data > temp->data) { insertnode(&(temp->right), n); } } } int main() { struct node* root = NULL; int i=0; for ( i=0; i < 10; i++) { insertnode(&root, createnode(i)); } return 0; }
上述代码从根节点开始,并将当前节点的数据部分与正在插入的新节点进行比较。如果新节点的数据值小于当前节点的数据值,通过左节点指针递归调用相同的函数;否则,通过右子节点指针递归调用相同的函数。
3、遍历节点
以下是便利BST各节点的算法:
1、有序搜索。
2、前序搜索。
3、后序搜索。
BST的其他辅助函数如下:
查找数深度。
比较两个BST.
查找叶结点数量,等等。