在C中,使用结构可以把不同类型的值存储在一起。数组是通过下标访问,因为数组的元素长度相同,但是结构并不是这样,每个结构成员都有自己的名字,它们是通过名字来访问的。
结构变量属于标量类型,并不是指针类型,和其他任何标题一样,当结构名在表达式中作为右值使用时,它表示存储在结构中的值。当它作为左值使用时,它表示结构存储的内存位置。但是,当数组名在表达式中作为右值使用时,它的值是一个指向数组第一个元素的指针,由于它的值是一个常量指针,所以数组名不能作为左值使用。可以声明指向结构的指针与结构数组。
struct 标识 { 成员列表} 变量列表 ;
struct {
inta;
charb;
floatc;
} x;
struct {
inta;
charb;
floatc;
} y[10], *z;
注意,上面是两种截然不同的类型,所以不能这样:z = &x
struct SIMPLE {
inta;
charb;
floatc;
};
该声明并没有提供变量列表,所以它并未创建任何变量,下面才开始创建变量。
struct SIMPLE x;
struct SIMPLE y[10], *z;
但现在与上前面的声明不同的是 x y z 都是同一种类型的结构变量了,所以z = &x可以
typedefstruct {
inta;
charb;
floatc;
} Simple;
Simple现在是一个类型名而不是一个结构标签,所以后续的声明可以是下面这样:
Simple x;
Simple y[10], *z;
结构可以嵌套其他结构:
struct COMPLEX {
struct SIMPLE sa[10];
struct SIMPLE *sp;
} comp, *cp = ∁
结构的访问
typedefstruct EX {
inta;
charb[3];
struct EX2 {
inta;
shortb[2]
} c;
struct EX *d;
} EX;
EX x = { 10, "Hi", { 5, { -1, 25 } }, 0 };
EX *px = &x;
EX y;
x.d = &y;
printf("%d", x.a);//10//结构体变量的访问方式
printf("%d", (*px).a);//10//结构体指针的访问方式
printf("%d", px->a);//10//此种为上面结构指针的简化访问方式
printf("%d", *px->c.b);//-1//混合使用
printf("%d", px->d->c.b[1]);//30656
px->d->c.b[1]=11;
printf("%d", px->d->c.b[1]);//11
结构的自引用
struct SELF_REF1 {
inta;
struct SELF_REF1 b;//非法
};
----
struct SELF_REF2 {
inta;
struct SELF_REF2 *b;//合法
};
printf("%d", sizeof(a.a));//4
printf("%d", sizeof(a.b));//4
第一个声明会尝试递归下去,但最终结构的长度不能确定下来而编译时出错误;但第二个是一个指针,其在编译时能够确定结构体的长度,一般链表和树都是用这种技巧实现。
在32位机上,一个指针通常占4个字节,而不管它指向什么类型的变量,因为指针是用来存储存储地址的:
char *p = NULL;
printf("%d", sizeof(p));//4
printf("%d", sizeof (char *));//4
书上说下面是非法的(原因就是类型名直到声明的末尾才定义,在结构声明的内部它尚定义),但实际上编译可以通过:
typedefstruct {
inta;
struct SELF_REF3 *b;//合法
} SELF_REF3;
SELF_REF3 a = { 3, NULL };
printf("%d", a.a);//3
相互引用的结构
有时需要声明一些相互之间存在依赖的结构,先被引用的结构需要使用不完整声明(如下面的struct B;),来声明一个作为结构标签的标识符,然后我们可以把这个标签与用在结构指针的声明中(如下面的struct B *partner;):
struct B;
struct A {
inta;
struct B *partner;
};
struct B {
struct A partner;
};
经实际测试,struct B;可以注释掉,编译时都不会出错,甚至将后面的B结构的定义一起去掉都没有问题,这也进一肯说声明结构体指针时不需要知道它所指向的结构体的具体类型,甚至不需要定义都可以;
结构体的初始化
结构体的初始化类似于或多维数组的初始化:
struct A {
inta;
shortb[10];
Simplec;
} x = { 10, { 1, 2 }, { 25, 'x', 1.9 } };
结构的地址与第一个成员的地址是相同的:
struct A {
inta;
shortb[10];
} x;
printf("%p\n", &x);//0022FF38
printf("%p\n", &x.a);//0022FF38
printf("%p\n", &x);//0022FF38
printf("%p\n", &x.a);//0022FF38
结构的存储分配
上面章节中(“结构的访问”),显示的是结构的逻辑存储结构(图中分配的空间好像不连续),那结构在内存中是如何实际存储的呢?上面那张图并不完整,实际上编译器会按照成员列表的顺序(声明的顺序)一个接一个地给每个成员分配内存,只有当存储成员时需要满足正确的边界要求时,成员之间才可能出现用于填充的额外内存空间(被浪费掉了的),如下面图中的灰色地带:
#include<stddef.h>
typedefstruct ALIGN {
chara;
intb;
charc;
}ALIGN;
printf("%d", offsetof(struct ALIGN,a));//0
printf("%d", offsetof(struct ALIGN,b));//4
printf("%d", offsetof(struct ALIGN,c));//8
printf("%d", sizeof (struct ALIGN));//12
如果编译器有成员对齐这一要求时,会按照最长类型来分配每一个成员,如上面的a成员,在整型类型长度为4字节的机器上,由于a是一个字符型,虽然本身只需一个字节,但也为a分配了4个字节,这将浪费3个字节的空间,c也是一样,此时需要总共12个字节的空间。如果将短的类型放在一起,这将会节约空间(此时只需要8个字节的空间):
typedefstruct ALIGN {
intb;
chara;
char c;
}ALIGN;
printf("%d", offsetof(struct ALIGN,b));//0
printf("%d", offsetof(struct ALIGN,a));//4
printf("%d", offsetof(struct ALIGN,c));//5
printf("%d", sizeof (struct ALIGN));//8
下面与上面也是一样节约空间:
typedefstruct ALIGN {
chara;
char c;
int b;
}ALIGN;
printf("%d", offsetof(struct ALIGN,a));//0
printf("%d", offsetof(struct ALIGN,c));//1
printf("%d", offsetof(struct ALIGN,b));//4
printf("%d", sizeof (struct ALIGN));//8
最后看看下面这个程序:
typedefstruct ALIGN {
chara;
charc;
chard;
shorte;
charf;
charj;
intb;
}ALIGN;
printf("%d", offsetof(struct ALIGN,a));//0
printf("%d", offsetof(struct ALIGN,c));//1
printf("%d", offsetof(struct ALIGN,d));//2
printf("%d", offsetof(struct ALIGN,e));//4
printf("%d", offsetof(struct ALIGN,f));//6
printf("%d", offsetof(struct ALIGN,j));//7
printf("%d", offsetof(struct ALIGN,b));//8
printf("%d", sizeof (struct ALIGN));//12
sizeof操作符能够得出一个结构体的整体长度,包括因边界对齐而路过的那些字节,如果想得到每个成员偏离结构首的字节数,则可以使用offsetof宏。
降序排列结构成员的声明可以最大限度地减少结构存储中浪费的内存空间。sizeof返回的值包含了结构中浪费的内存空间。
参数结构体
把结构体作为参数传递给一个函数是合法的,但这种做法效率低,因为在传递过程中需要复制整个结构体。
void print(registerstruct Trans const * const trans);
在许多的机器中,你可以把参数声明为寄存器变量,从而进一步提高指针传递方案的效率,这对于需要多次访问的变量有很大的效率提升。
位段
用signed或unsigned整数地声明位段是个好主意,如果只是声明为int类型,它究竟被解释为有符号数还是无符号数是由编译器决定的。
位段的声明与结构类似,但它的成员是一个或多个位的字段,这些不同长度的字段实际上存储于一个或多个整型变量中。
位段的成员必须声明为int、signed int、或unsigned int类型。其次,在成员名的后面是一个冒号和一个整数,这个整数指定该位段所占用的位的数目。
struct CHAR {
unsignedch :7;
unsignedfont :6;
unsignedsize :19;
};
struct CHAR ch1;
该程序可以处理128个不同的字符值、64种不同的字段、524288种字体大小。成员size位段过于庞大,无法容纳于一个短整型中,但其他的位段成员又比一个字符还短,所以这里能够利用存储ch和font所剩余的位来用在size位段上,这样就可以使用一个32位的整数来存储整个内容了。上面程序在16位机上是非法的,因为最长的位段定义成了19,而最长也只可能为16,但在32位机上,这个声明将根据下面两种可能的方法创建ch1:
位段的好处:它能够把长度不同的类型数据包装在一起,节省存储空间。另一个好处是,可以很方便地访问一个整型值的部分内容。下面是一个软盘控制器寄存器,每位的结构如下:
在一个从右向左分配位段的机器上,下面的这个声明允许程序方便地对这个寄存器的不同位段进行访问:
struct DISK_REGISTER_FORMAT {
unsignedcommand :5;
unsignedsector :5;
unsignedtrack :9;
unsignederror_code :8;
unsignedhead_loaded :1;
unsignedwrite_protect :1;
unsigneddisk_spinning :1;
unsignederror_occurred :1;
unsignedready :1;
};
假如寄存器是在内存地址 0xc0200142 ,我们可以声明下面的指针常量:
#define DISK_REGISTER ((struct DISK_REGISTER_FORMAT*)0xc0200142)
下面开始访问:
/*
* 告诉控制器从哪个扇区哪个磁道开始读取
*/
DISK_REGISTER->sector = new_sector;
DISK_REGISTER->track = new_track;
DISK_REGISTER->command = READ;
/*
* 等待,直到操作完成(ready变量变成真)
*/
while (!DISK_REGISTER->ready);
/*
* 检查错误
*/
if(DISK_REGISTER->error_code){
}
使用位段只是基于方便的目的,任何可以用位段实现的任务都可以使用移位和屏蔽来实现。下面的代码功能和前面的赋值功能是一样:
#define DISK_REGISTER ((unsignedint *)0xc0200142)
*DISK_REGISTER &= 0xfffffc1f;//将sector字段清零
*DISK_REGISTER |= (new_sector & 0x1f) << 5;//赋值
在源代码中,用位段表示这个处理过程更简单一些,但在目标中,这两种方法并不存在任何区别,无论是否使用位段,相同的移位和屏蔽都是必需的。位段提供的唯一优点是简化了源代码,但需要与位段的移植性较弱这个缺点进行权衡。
注重可移植性的程序应该避免使用位段,位段在不同的系统中可能有不同的结果:
1、 int位段被当作有符号数还是无符号数。
2、 位段中字段的位最大数目。许多编译器把位段成员的位数限制在一个整型的长度之内,所以能够运行于32位整数的机器上的位段声明可能在16位整数的机器上无法运行,如unsignederror_code :32; 肯定是不能移植了。
3、 位段中的成员在内存中是从左向右分配的还是从右向左分配的。
联合
联合的所有成员引用的是内存中的相同位置,当你想在不同的时刻把不同的东西存储于一个位置时,就以使用联合。通过访问不同类型的联合成员时,内存中相同的位组合可以被解释为不同的东西。
union u_tag {
inti;
floatf;
char *s;
} u;
int main(int argc, char * argv[]) {
printf("%d\n", u.i);//0
u.i=1;
printf("%d\n", u.i);//1
printf("%f\n", u.f);//0.000000
u.f=1.1;
printf("%f\n", u.f);//1.100000
//打印的还是最后一次存储的内容
printf("%f\n", u.i);//1.100000
//float转换成int
printf("%d\n", u.i);//1066192077
return 0;
}
联合与结构可以相互嵌套。
实际上,联合就是一个结构,它的所有成员相对于基地址的偏移量都为0,此结构空间要大到足够容纳最“宽”的成员。
在一个成员长度不同的联合里,分配给联合的内存数量取决于它的最长成员的长度。如果成员的长度相关悬殊,会浪费很多的空间,在这种情况下,最好的方法是在联合中存储指向不同成员的指针而不是直接存储成员本身,因为所有指针的长度都是相同的,这样就解决了内存浪费的问题了。
联合会默认使用第一个成员类型的值进行初始化。
联合初始化必须是联合第1个成员的类型,而且它必须位于一对花括号里:
union {
charc;
inta;
floatb;
} x={'a'};
printf("%c\n",x.c);//a
printf("%d\n",x.a);//97
当你声明数组时,你必须用一个编译时常量指定数组的长度,但是,数组的长度常常在运行时才知道,所以我们通常采用的方法是声明一个较大的数组,它可以容纳可能出现的最多元素。
malloc从内存池中提取一块合适的内存,并向程序返回一个指向这块内存的指针。分配与释放的函数原型如下:
void *malloc(size_t size);
void free(void *pointer);
size_t是一个无符号类型,定义于stdlib.h中,该参数指定了需要分配的内存字节数。
malloc所分配的是一块连续的内存。malloc实际分配的内存有可能比你请求的稍微多一点,这是由编译器定义的。
如果操作系统无法向malloc提供更多的内存,malloc就返回一个NULL指针。
malloc是不知道你所请求的内存是用来存储整数、浮点数、结构还是数组的,所以它返回的是一个类型为void*的指针,而这个类型的指针可以转换为其他任何类型的指针,在某些老式的编译器中,可能要求你在转换时使用强制类型转换。
另外还有两个内存分配函数,原型如下:
void *calloc(size_t num_elements, size_t element_size);
void realloc(void *ptr, size_t new_size);
calloc与malloc之间的主要区别是前者在返回指向内存的指针之前把内存初始化为0,另一个区别是它们请求内存数量的方式不同,calloc的参数包括所需要元素的数量和每个元素的字节数,根据这些值,它能够计算出总共需要分配的内存。
realloc函数用于修改一个碑已经分配的内存块的大小,可以使用一块内存扩大或缩小。扩大时原先的内容依然保留,新增加的内存添加到原先内存块的后面,新内存并未以任何方法进行初始化。缩小时,该内存块尾的部分内存被拿掉,剩余部分内存的原先内容依然保留。如果原先的内存无法改变大小,realloc将分配另一块正确大小的内存,并把原先那块内存的内容复制到新的块上。因此,在使用realloc之后,你主不能再使用指向旧内存的指针,而是应该改用realloc所返回的新的指针。
int i, *pi, *pi2;
//这块内存将被当作25个整型元素的数组,
//因为pi是一个指向整型的指针
pi = malloc(25 * sizeof(int));
if (pi == NULL) {
printf("Out of memory!\n");
exit(1);
}
pi2 = pi;
for (i = 0; i < 25; i++) {
*pi2++ = 0;
//或者使用下标,当作数组来使用 pi[i]=*(pi + i)
//pi[i]=0;
}
常见的动态内存错误:对NULL指针进行解引用操作、对分配的内存进行操作时越过边界、释放并非动态分配的内存、试图释放一块动态分配的内存的一部分、一块动态内存被释放之后被继续使用。
使用MALLOC自定义宏来避免上面这些错误,下面程序由三部分组成:一个是定义接口alloc的头文件alloc.h,第二个是接口,第三个是使用接口:
/*
* 定义一个不易发生错误的内存分配器接口
*/
#include<stdlib.h>
#define malloc 不要直接调用malloc!
#define MALLOC(num,type) (type*)alloc((num) * sizeof(type))
externvoid * alloc(size_t size);//接口
——alloc.h
/*
* 实现
*/
#include<stdio.h>
#include"alloc.h"
#undef malloc
void * alloc(size_t size) {
void * new_mem;
//请求所需的内存,并检查确实分配成功
new_mem = malloc(size);
if (new_mem == NULL) {
printf("Out of memory!\n");
exit(1);
}
return new_mem;
}
——alloc.c
#include"alloc.h"
/*
* 使用
*/
void function() {
int * new_memeory;
//获取一串整型数的空间
new_memeory = MALLOC(25,int);
}
——a_client.c
free的参数必须要么是NULL,要么是一个先前从malloc、calloc或realloc返回的值,向free传递一个NULL参数不会产生任何效果。free试图释放一块动态分配内存的一部分也有可能引起问题:
pi = malloc(25 * sizeof(int));
//下面会引发问题
free(pi +5);
释放一内存的一部分是不允许的,动态分配的内存必须整块一起释放。但是,realloc函数可以缩小一块动态分配的内存,有效地释放它尾部的内存。
不能访问已经被free函数释放了的内存。
分配内存但在使用完毕后不释放将引起内存泄漏
动态分配实例
动态分配最常见的一个应用就是为那些长度在运行时才知道的数组分配内存空间。下面是读取一列整数,并排序:
/*
** 读取、排序和打印一列整数.
*/
#include<stdlib.h>
#include<stdio.h>
/*
** 该函数由 qsort 用于比较整型值
*/
int compare_integers(voidconst *a, voidconst *b) {
registerintconst *pa = a;
registerintconst *pb = b;
return *pa > *pb ? 1 : *pa < *pb ? -1 : 0;
}
int main() {
int *array;
int n_values;
int i;
/*
** 观察共有多少个值.
*/
printf("How many values are there? ");
if (scanf("%d", &n_values) != 1 || n_values <= 0) {
printf("Illegal number of values.\n");
exit(EXIT_FAILURE);
}
/*
** 分配内存,用于存储这些值.
*/
array = malloc(n_values * sizeof(int));
if (array == NULL) {
printf("Can't get memory for that many values.\n");
exit(EXIT_FAILURE);
}
/*
** 读取这些值.
*/
for (i = 0; i < n_values; i += 1) {
printf("? ");
if (scanf("%d", array + i) != 1) {
printf("Error reading value #%d\n", i);
exit(EXIT_FAILURE);
}
}
/*
** 调用库函数进行排序.
*/
qsort(array, n_values, sizeof(int), compare_integers);
/*
** 输出.
*/
for (i = 0; i < n_values; i += 1)
printf("%d\n", array[i]);
/*
** 释放内存并且退出.
*/
free(array);
return EXIT_SUCCESS;
}
动态复制字符串:
/*
** 用动态分配内存制作一个字符串的一份拷贝。注意:
** 调用程序应该负责检查这块内存是否成功分配!这样
** 做允许调用程序以任何它所希望的方式对错误作出反应
*/
#include<stdlib.h>
#include<string.h>
char *strdup(charconst *string) {
char *new_string;
/*
** 请求足够长度的内存,用于存储字符串和它的结尾NUL字节.
*/
new_string = malloc(strlen(string) + 1);
/*
** 如果我们得到内存,就复制字符串.
*/
if (new_string != NULL)
strcpy(new_string, string);
return new_string;
}
该程序将输入的字符串存储到缓冲区,每次读取一行。调用此函数时才可以确定字符串的长度,然后就分配内存用于存储字符串,最后字符串被复制到新内存,这样缓冲区又可以用于读取下一个输入行。这个函数非常方便,也非常有用,尽管标准没有提及,但许多环境都把它作为函数库的一部分。
有些C编译器提供了一个称为alloca的函数,它与malloc函数不现是在于它在栈上分配内存,而malloc是在堆上分配内存。在栈上分配内存的主要优点是当分配内存的返回时,这块内存会被自动释放,这是由栈的工作方式决定的,它可以保证不会出现内存泄漏,但这种方式存在缺点,由于函数返回时被分配的内存将消失,所以它不能用于存储那些回传给调用程序的数据。
单链表
/*
** 单链表的插入,链表为升序
*/
#include<stdlib.h>
#include<stdio.h>
typedefstruct NODE {
struct NODE *link;
intvalue;
} Node;
#define FALSE 0
#define TRUE 1
/*
* 如果节点插在最前面,则需要使用根指针的值,
* 所以这里使用了二级指针将根指针的地址也传递
* 过去,这样便于修改根指针的指向
*/
int sll_insert(Node **rootp, int new_value) {
/*
* 前驱节点,会成为新节点的前驱节点,也可能为NULL
*/
Node *previous;
/*
* next节点,即第一个大于或等于新节点的节点,最后会
* 成会新节点的后继节点,可能为NULL
*/
Node *next;
Node *new;//新节点
previous = NULL;//刚开始时前驱节点指针指向NULL
//初始化后继节点,刚开始时与根节点一样指向第一个节点
next = *rootp;
/*
** 如果没有到达链尾,且没有找到一个大于或等于新节点
** 的节点时,继续往下找
*/
while (next != NULL && next->value < new_value) {
previous = next;
next = next->link;
}
//动态创建新的节点
new = (Node *) malloc(sizeof(Node));
if (new == NULL)
return FALSE;
new->value = new_value;
//设定next域,让next节点成为新节点的下一节点
new->link = next;
//如果需要在最前面插入时
if (previous == NULL)
//此时需要修改根节点的指向,让它指向新的节点
*rootp = new;
else//如果插入是在链表的中间或者末尾时
previous->link = new;
return TRUE;
}
int main(int argc, char **argv) {
Node *root = NULL;
Node **rootp = &root;
sll_insert(rootp, 2);
sll_insert(rootp, 5);
sll_insert(rootp, 3);
sll_insert(rootp, 0);
sll_insert(rootp, 1);
sll_insert(rootp, 4);
while (root != NULL) {
printf("%d", root->value);//012345
root = root->link;
}
}
单链表的优化插入操作
看上去,把一个节点插入到链表的起始位置必须作为一种特殊情况进行处理,因为此时插入新节点需要修改的是指针是根指针,而对于其他任何节点,修改的是前一个节点(previous)的link字段,这两个看上去不同的操作实际上是一样的。
消除这种特殊情况的关键在于:我们必须认识到,链表中的每个节点都有一个指向它的指针。对于第1个节点,这个指针是根指针;对于其他节点,这个指针是前一节点的link字段,相同点是每个节点都有一个指针指向它,至于该指针是不是位于一个节点内部则不重要,我们完全可以将root与节点内部的link指针同等看待,root即可看成某个节点的link字段。
让我们再次观察这个链表,弄清这个概念,这是第1个节点和指向它的指针:
如果新值插入到第1个节点之前,这个指针就必须进行修改。下面是第2个节点和指向它的指针:
如果新值需要插入到第2个节点之前,那么前一节点的link指针必须进行修改。
现在我们只需要拥有一个指向插入位置的下一节点的指针(next),以及一个“指向next节点的link字段”的指针(linkp),除此外,我们就不再需要一个用来保存指向插入位置的前一节点的指针(previous),下面是赋值语句(next = *linkp)执行后的各变量情况:
当移动到下一个节点时,我们保存一个“指向next节点的link字段的”指针(linkp),而不是保存一个指向前一个节点的指针(previous):
注,这里的linkp并不指向节点本身,与上面实现不现的是,它是指向节点内部的link字段,这是简化插入操作的关键所在,这可以将root指针与节点内的link指针字段同等看待,我们可以用一种和修改节点的link字段完全一样的方式来修改root变量。插入函数的原型与上面的实现还是一样,只是将rootp的名称改成了linkp,因为这里的实现是让rootp可以指向其他任何节点内部的link字段,所以就形象的称为linkp,而不仅仅是根指针了。我们再也不需要previous指针了,因为我们的linkp指针可以负责寻找到需要修改的link字段,下面是实现:
result = sll_insert(&root, 12);
int sll_insert(registerNode **linkp, int new_value) {
registerNode *next;
registerNode *new;
next = *linkp;//刚开始时next与root同指向第一个节点
//如果没有到达链尾且next节点的值小于或等于新节点值时继续
while (next != NULL && next->value < new_value) {
/*
* 这里与上面的实现是不一样的,这里保存的是节点内link字段的
* 地址,而不像上面那样保存的是节点本身,这是简化的关键所在
*/
linkp = &next->link;
next = *linkp;
}
new = (Node *) malloc(sizeof(Node));
if (new == NULL)
return FALSE;
new->value = new_value;
//这个与上面的实现还是一样,让当前节点成为新节点的下一节点
new->link = next;
/*
* 即使在空链表时,可以将root根指针与节点中的link字段同等看待
* 链表为空时,修改的就是root的指向,否则修改的就是其他节点的
* link指向
*/
*linkp = new;
return TRUE;
}
节点的删除与查找也可以使用上面这种简化的操作来实现。
双链表(非循环)
双链表的根节点允许我们可以从链表的任何一端(第一个节点还是最后一个节点)开始遍历链表。根节点的fwd字段指向链表的第1个节点,根节点的bwd字段指向链表最后一个节点(如果只有一个节点,则这两个都指向第一个节点)。如果链表为空,这两个字段都为NULL。链表第1个节点的bwd字段和最后一个节点的fwd字段都为NULL。并将根节点与其他节点等同看待,只是value没有值而已。
1、 如果链表为空,则新增节点的fwd与bwd字段都为NULL。
2、 如果新增节点位于起始位置,则新增节点的bwd字段为NULL,新节点的fwd字段指向下一节点(next指向的节点),下一节点(next指向的节点)的bwd字段指向这个新的节点。
3、 如果新增节点位于结束位置,则新增节点的fwd字段为NULL,新节点的bwd字段指向前一节点(previous指向的节点),前一节点(previous指向的节点)的fwd字段指向这个新的节点。
4、 如果新增节点位于链表中间,则新增节点的fwd字段为下一节点(next指向的节点),新节点的bwd指向前一节点(previous指向的节点),下一节点(next指向的节点)的bwd指向新节点,前一节点(previous指向的节点)的fwd也指向新节点。
/*
** 把一个值插入到一个双链表,rootp是一个指向根节点的指针,
** value是欲插入的新值
** 返回值:如果欲插值已存在于链表中,函数返回0;如果内存不
** 足,返回-1;如果插入成功,函数返回1。
*/
#include<stdlib.h>
#include<stdio.h>
typedefstruct NODE {
struct NODE *fwd;
struct NODE *bwd;
intvalue;
} Node;
int dll_insert(Node *rootp, int value) {
Node *previous;//指向待插入位置的前一节点,
Node *next;//指向待插入位置的后一节点,
Node *new;//指向新的节点
previous = rootp;//初始化时指向根节点
//previous不能为NULL,rootp不能传递为NULL
next = previous->fwd;//初始化时指向第一个节点或NULL
/*
* 如果没有达到链尾,且新的值比next值大时继续向后找
*/
while (next != NULL && next->value < value) {
//查看value是否已经存在于链表中,如果是就返回
if (next->value == value) {
return 0;
}
previous = next;
next = previous->fwd;
}
new = (Node *) malloc(sizeof(Node));
if (new == NULL) {
return -1;
}
new->value = value;
if (rootp->fwd == NULL && rootp->bwd == NULL) {//如果链表为空
new->fwd = NULL;
rootp->fwd = new;
new->bwd = NULL;
rootp->bwd = new;
} elseif (previous == rootp) {//如果插入位置为链表首时
new->fwd = next;
rootp->fwd = new;
new->bwd = NULL;
next->bwd = new;
} elseif (next == NULL) {//如果插入位置为链表尾时
new->fwd = NULL;
previous->fwd = new;
new->bwd = previous;
rootp->bwd = new;
} else {
//如果插入位置为链表中间时
new->fwd = next;
previous->fwd = new;
new->bwd = previous;
next->bwd = new;
}
return 1;
}
int main(int argc, char **argv) {
// Node root = { NULL, NULL, 0 };
/*
* 为了节省空间,root的值成员是多余的,
* 可以使用动态分配出来
*
* 如果不是动态分配出来的,则可以使用如
* 下结构来实现:
* struct DLL_NODE;
* struct DLL_POINTERS{//根节点结构
* struct DLL_NODE * fwd;
* struct DLL_NODE * bwd;
* };
* struct DLL_NODE{//节点
* struct DLL_POINTERS pointers;
* int value;
* };
*
*/
Node* root2 = malloc(sizeof(Node) - sizeof(int));
root2->bwd = NULL;
root2->fwd = NULL;
dll_insert(root2, 2);
dll_insert(root2, 5);
dll_insert(root2, 3);
dll_insert(root2, 0);
dll_insert(root2, 1);
dll_insert(root2, 4);
//从前向后遍历
Node* tmp = root2->fwd;
while (tmp != NULL) {
printf("%d", tmp->value);//012345
tmp = tmp->fwd;
}
//从后向前遍历
tmp = root2->bwd;
while (tmp != NULL) {
printf("%d", tmp->value);//543210
tmp = tmp->bwd;
}
}