2015.09.08 – 09.15
读xtcxyczjh(系统程序员成长计划)---- 学习程序设计方法。
笔记中的表达极差。关于C语言的“创建双向链表”、“封装”、“通用”、“回调”代码保存地址:pxtcxyczjh-SourceI。
宿机:i7-4790+ Windows10_x64。
虚拟机:VMware-workstation-full-12.0.0+ ubuntu-12.04.5-desktop-i386。
Linux简单配置。
Windows与VMware内linux的交互 1. Vmware workstaion >> 查看 >> 自动调整大小 >> 自适应客户机; 2. 安装vmware tools可以实现虚拟机中Linux桌面与windows之间的文件拖拉。
putty链接VMware内linux 1. 在linux的root权限下在终端用“apt-get install openssh-server”命令下载并安装SSH服务; 2. 在linux终端用ifconfig命令查看linux的ip地址,打开putty登录linux。
下载vim编辑器。 1. 在linux的root终端用“apt-get install vim”命令下载并安装vim; 2. 用“vi /etc/vim/vimrc”命令打开vimrc对vim编辑器进行基本配置。 |
将从书中读到的本人不知的概念笔记在“2.1概念”模块,将根据书中需求所做习题笔记在“2.2练习”中。
本书的读者对象为想成为系统程序员的人。系统程序员:从事操作系统内核、DBMS、GUI系统、基础函数库、应用程序框架、编译器和虚拟机等基础软件开发的程序员。
目的:让读者学习软件设计方法。
封装,即隐藏对象的属性和实现细节,仅对外公开接口,控制在程序中属性的读和修改的访问级别;将抽象得到的数据和行为(或功能)相结合,形成一个有机的整体,也就是将数据与操作数据的源代码进行有机的结合,形成“类”,其中数据和函数都是类的成员。封装是站在调用的角度看问题的。
单件模式是一种用于确保整个应用程序中只有一个类实例(变量)且这个实例所占资源在整个应用程序中是共享时的程序设计方法。
浅拷贝,只是对指针的拷贝,拷贝后两个指针指向同一个内存空间,深拷贝不但对指针进行拷贝,而且对指针指向的内容进行拷贝,经深拷贝后的指针是指向两个不同地址的指针。
创建一个函数来调用这个调用者编写的函数,像这种调用调用者提供的函 数的方法,我们可以称它为回调函数法。
需求简述。
用C语言创建一个双向链表。
准备。
链表由多个节点相互“关联”构成。节点内包含两个东西:保存数据的元素和指向其它节点的指针。
双向链表描述的是这样的对象:在链表中,可通过元素ei得到ei-1和ei+1(i为整数,0 < i <链表元素个数);链表中第一个元素的前一个元素为链表中的最后一个元素,链表中最后一个元素的下一个元素为为链表中第一个元素。
代码。
用普通账号登录linux。在linux终端用mkdir命令创建y15m9d10 文件(mkdir y15m9d10),进入y15m9d10(cd y159d10),创建create_dlist.c文件(vi create_dlist.c),在create_dlist.c中编写C代码。
描述双向链表节点的数据结构类型。
/* Define data type */ typedef int DLElmtT; typedef struct _DListNode { DLElmtT e; struct _DListNode *pp; struct _DListNode *pn; }*pDlT, DLT;
双向链表的每个节点包含一个保存DLElmtT类型数据的元素e和两个指向本类型节点的指针(pp和pn分别用于指向链表中的上、下一个节点)。通过typedef定义了pDlT指针(struct _DListNode*)类型和DLT(struct _DListNode)类型。描述双向链表的数据结构可暂时如此,根据后续需求变化对双向链表的数据结构进行持续重构。
创建双向链表。
为创建双向链表写一个函数create_dlist。创建双向链表时,主要是根据链表长度用malloc函数为每个链表的节点分配内存空间,当malloc的某次调用的返回值为NULL时(节点的内存空间分配失败)要调用free函数将前面节点所指的内存空间释放掉。
/* Create len elements for double linked list */ pDlT create_dlist(UINT len) { int i, j; pDlT pf, pp, pn, tmp; if (0 > len) { return NULL; } /* Fisrt element */ pf = (pDlT)malloc( sizeof(DLT) ); if (NULL == pf) { return pf; } pf->pp = NULL; pf->pn = NULL; /* Follow-up (len - 1) elements */ pn = pf; for (i = 1; i < len; ++i){ pp = pn; /* pp->e = i; printf("%d ", pp->e); fflush(NULL); */ pn = (pDlT)malloc( sizeof(DLT) ); if (NULL == pn) { //Free front elements tmp = pp->pp; while (NULL != pp) { free(pp); pp = tmp; tmp = tmp->pp; } return pp; } // Insert an element pp->pn = pn; pn->pp = pp; pn->pn = NULL; } pf->pp = pn; pn->pn = pf; /* pn->e = i; printf("%d ", pn->e); fflush(NULL); */ return pf; }
释放链表。
为释放链表写一个函数free_dlist。由于链表的创建都是用malloc函数为其节点分配的内存空间,那么在程序结束时也一定要用free函数将每个节点所指的内存释放掉。
/* Free len elements for double linked list */ void free_dlist(pDlT pdl, UINT len) { int i; pDlT tmp; if (NULL == pdl) { return ; } tmp = pdl->pn; for (i = 0; i < len; ++i) { /* printf("%d ", pdl->e); fflush(NULL); */ free(pdl); pdl = tmp; tmp = tmp->pn; } }
在main中调用。
在main函数中先调用create_dlist,再调用free_dlist。作为辅助测试,在create_dlist函数中加入了打印每个链表值的语句。
/* Functions decalration */ pDlT create_dlist(UINT len); void free_dlist(pDlT pdl, UINT len); /* Entry of C */ int main(void) { pDlT pdl; pdl = create_dlist( 5 ); free_dlist(pdl, 5); return 0; }
将create_dlist函数中的打印语句的注释去掉。在linux终端用“gcc create_dlist.c –o create_dlist –Wall”命令编译create_dlist.c得到create_dlist。通过“./create_dlist”就可以运行create_dlist得到输出结构。
Figure1. create_dlist运行结果
编译出现局部变量未使用的警告,删掉create_dlist函数中的j变量的定义即可。
源代码目录。
../xtcxyczjh/y15m9d10/
需求简述。
在昨天双向链表程序的基础上实现简单的封装。
准备。
封装描述的现象;C语言能够实现封装的语法;读xtcxyczjh。
代码。
在这里只先考虑3个问题。
隐蔽数据结构。
(1) 如果是内部数据结构,外面完全不会引用,则直接放在C文件中; (2) 如果该数据结构在内外都要使用,则可以对外暴露结构的名字,而封装结构的实现细节。 |
将关于链表的数据结构和函数写在dlist.c中,将主函数main写在main.c中。隐蔽数据结构即是在main.c中可以引用在dlist.c中描述双向链表的数据结构类型但不能访问数据结构内的元素。实现过程如下。
dlist.c
typedef int DLElmtT; //Data sructure for describing doubly linked list struct _DListNode { DLElmtT e; struct _DListNode *pp; struct _DListNode *pn; }; typedef struct _DListNode *pDlT, DLT;
准确的说,struct _DListNode描述链表中一个节点的类型。将struct _DListNode类型与typedef脱离开来,因为接下来要利用struct _DListNode来向其它文件(main.c)暴露结构体的名字。
新建dlist.h,用来向其它文件对dlist.c的声明。
/* dlist.h */ /* Data type decalarations */ struct _DListNode; typedef struct _DListNode *pDlT; /* Golbal functions decalarations*/ pDlT create_dlist(unsigned int len); void free_dlist(pDlT pdl, unsigned int len);
只要在main.c中“#include “dlist.h””,那么在main.c中就可以用pDlT类型,但不能通过pDlT访问到struct _DListNode类型中的元素,这就实现数据的隐蔽性。除了在dlist.c中,调用时(者)无法更改数据结构内的元素。
main.c
/* main.c */ #include "dlist.h" /* Entry of C program */ int main(void) { pDlT pdl; pdl = create_dlist( 5 ); free_dlist(pdl, 5); return 0; }
在main.c中包含dlist.h(声明dlist.c中的全局函数),就可以调用dlist.c中的全局函数来访问链表。在linux终端用命令编译运行程序:
Figure 2. y15m9d11下程序运行结果
C语言的不完全类型和隐蔽数据。
在dlist.h中的“struct _DListNode;”语句是一个不完全类型的声明。由于struct _DListNode没有在dlist.h中定义,编译器将不能确定struct _DListNode内的元素,但因为C语言允许不完全数据类型特性,编译将会通过。编译阶段会做好去其它文件寻找struct _DListNode定义的记录并传给链接器,如果链接器在其它文件中找到struct _DListNode的定义则不报错。
也正是编译器在编译阶段不知道struct _DListNode内具体元素定义的原因,故而对于声明struct _DListNode的其它文件来说,不能生成引用struct _DListNode内元素的汇编代码,所以只要在定义struct _DListNode的其它文件中引用struct _DListNode内的元素时编译器就会报错。 因为不管什么类型的指针变量所占内存大小都是一样的,所以“typedef struct _DListNode *pDlT;”的定义没有问题。此语句定义了一个struct _DListNode指针类型pDlT;用pDlT类型定义的指针变量占4个字节(32bit系统),指向一个不完全类型struct _DListNode,但不能访问到结构体内的任何元素(编译阶段不能生成访问结构体元素的汇编代码)。
利用C语言的不完全类型实现了数据的封装 – 其它文件可引用在本文件定义的结构体数据类型,但不能访问本文件所定义结构体内的元素。 |
隐藏内部函数。
这里的内部函数是指只供dlist.c调用的函数。只要将内部函数限制为static,其它文件就访问不到此函数了。作为辅助测试链表和隐藏内部函数练习,在dlist.c中编写一个show_dlist函数,用于显示链表元素。
/* Show doubly linked list node */ static void show_dlist(pDlT pdl, unsigned int len) { int i; for (i = 0; i < len; ++i) { printf("%d ", pdl->e); pdl = pdl->pn; } printf("\n"); }
在dlist.c中在调用之前声明此函数即可。此函数被create_dlist函数调用。添加show_dlist函数的运行结果即为figure 2。
隐蔽内部函数的好处。
内部函数通常实现一些特定的算法(如果具有通用性,应该放到一个公共函数库里),对调用者没有多大用处,但它的暴露会干扰调用者的思路,让系统看起 来比实际的复杂。函数名也会污染全局名字空间,造成重名问题。它还会诱导调用者绕过正规接口走捷径,造成不必要的耦合。 |
禁止全局变量。
除了为使用单件模式(只允许一个实例存在)的情况外,任何时候都要禁止使用全局变量。 全局变量始终都会占用内存空间,共享库的全局变量是按页分配的,那怕只有一个字节的全局变量也占用一个page,所以这会造成不必要空间浪费。全局 变量也会给程序并发造成困难,想把程序从单线程改为多线程将会遇到麻烦。 |
本程序中无全局变量。
源代码目录。
../xtcxyczjh/y15m9d11/
需求简述。
先只考虑“存放任何数据类型”和“完整接口”两个方面在前面的基础上编写通用的链表程序,作为编写通用程序的入门。
准备。
读xtcxyczjh本节。
代码。
存放任何数据类型。在昨天的程序中,链表只可保存DLElmtT类型数据。
C语言中的void*。
C语言的void *类型可以指向任何数据类型。也可以通过强制类型转换将void *转换成任何类型去访问void *变量所指的内容。 |
更改在dlist.c中描述双向链表的数据结构。
//Data sructure for describing doubly linked list struct _DListNode { void *pe; struct _DListNode *pp; struct _DListNode *pn; }; typedef struct _DListNode *pDlT, DLT;
void*可以指向任何类型的数据,在dlist.c中为链表准备两组数据(简化了为链表准备数据的过程)。
//For doubly linked list node static int itmp_data[] = {1, 2, 3, 4, 10}; static char ctmp_data[] = "last data.";
更改create_dlist函数中为链表节点赋值的方式。
/* Create len nodes for double linked list */ pDlT create_dlist(unsigned int len) { …… /* Follow-up (len - 1) nodes */ pn = pf; for (i = 1; i < len; ++i){ pp = pn; pp->pe = &itmp_data[i - 1]; …… } pf->pp = pn; pn->pn = pf; pn->pe = ctmp_data; …… }
第9、14行为给链表节点元素赋值的新方式。
更改show_dlist函数。
/* Show doubly linked list node */ static void show_dlist(pDlT pdl, unsigned int len) { int i; for (i = 0; i < len - 1; ++i) { printf("%d ", *((int *)(pdl->pe)) ); pdl = pdl->pn; } printf("%s", ((char *)(pdl->pe)) ); printf("\n"); }
明确链表中最后一个元素数据类型为字符串,所以要以字符串的形式打印。
完整的接口。
在dlist.c中编写向链表中插入一个节点的函数。
/* Insert node into doubly linked list */ int insert_node2dlist(pDlT pdl, unsigned int len, pDlT pnd, unsigned int lc) { int i; pDlT tpdl; if (NULL == pdl) { return -1; } if (lc < 1 || lc > len + 1) { return -1; } tpdl = pdl; for (i = 1; i < lc; ++i) { tpdl = tpdl->pn; } pnd->pp = tpdl->pp; pnd->pn = tpdl; tpdl->pp->pn = pnd; tpdl->pp = pnd; show_dlist(pdl, len + 1); return 0; }
与之对应在dlist.c中再编写一个删除双向链表节点的函数。
/* Delete node form doubly linked list */ int delete_node8dlist(pDlT pdl, unsigned int len, unsigned int lc) { int i; pDlT tpdl; if (NULL == pdl) { return -1; } if (lc < 1 || lc > len) { return -1; } tpdl = pdl; for (i = 1; i < lc; ++i) { tpdl = tpdl->pn; } tpdl->pp->pn = tpdl->pn; tpdl->pn->pp = tpdl->pp; tpdl->pp = NULL; tpdl->pn = NULL; free(tpdl); len--; show_dlist(pdl, len); return 0; }
在dlist.h中声明插入/删除双向链表节点的函数。
/* dlist.h */ … int insert_node2dlist(pDlT pdl, unsigned int len, pDlT pnd, unsigned int lc); int delete_node8dlist(pDlT pdl, unsigned int len, unsigned int lc);
在main.c中调用这些函数来验证插入/删除节点函数的功能。
/* main.c */ #include "dlist.h" #include <stdio.h> /* Entry of C program */ int main(void) { pDlT pdl, pnd; int len = 5; pdl = create_dlist( len ); pnd = alloc_node(); if (NULL == pnd) { free_dlist(pdl, len); return -1; } insert_node2dlist(pdl, len, pnd, 2); delete_node8dlist(pdl, len + 1, 2); free_dlist(pdl, len); return 0; }
alloc_node为在dlist.c中新增的函数,其功能是分配一个双向链表节点(因为封装了双向链表的数据类型,在除dlist.c的其它文件中无法引用/计算双向链表节点的大小),它的原型被声明在dlist.h中。以下是alloc_node在dlist.h中的源码。
/* Allocate one node of doubly linked list */ pDlT alloc_node(void) { pDlT pnd; pnd = (pDlT)malloc( sizeof(DLT) ); if (NULL != pnd) { pnd->pe = &itmp_data[4]; } return pnd; }
在linux终端运行此程序。
Figure 3. y15m9d12-I程序运行结果
增加管理双向链表的数据结构。
在操作关于链表的节点个数过程中,需要在调用时确定,这对调用来说十分不便且易出错。可以通过增加一个管理双向链表的数据结构来解决这个问题。
/*dlist.c*/ …… //Data sructure for describing doubly linked list struct _Node { void *pe; //Point to element of any type struct _Node *pp; //Point to previous node struct _Node *pn; //Point to next node }; typedef struct _Node *pNdT, NDT; struct _DListNode { pNdT pnd; //Point to first address of nodes unsigned int num; //Recode nodes' number }; typedef struct _DListNode *pDlT, DLT;
增添DLT结构后,dlist.c中所有访问双向链表的函数都要通过加一条获取链表第一个节点的语句,代码的其余部分不变。
在main.c中调用dlist.c中的接口。在linux终端编译并运行。
Figure 4. y15m9d12-II下程序的运行
源码目录。
../xtcxyczjh/y15m9d12/
需求简述。
y15m9d12下的双向链表节点可以存储任意类型的数据。除了调用者外,其他人无法知道双向链表各节点的数据类型。输出链表内容作为一种辅助测试,应该提供用双向链表的接口来调用调用者来编写打印双向链表函数的机制 – 实现回调函数。
准备。
读xtcxyczjh本节。
回调函数实现。
回调函数机制的具体的实现方法是将调用者编写的函数的地址作为参数传递给双向链表的的接口。 |
代码I。
在dlist.c中,将原来的show_dlist函数更名为dlist_callback_show_dlist,再定义一个调用dlist_show_dlist的函数。
/* dlist.c*/ …… static void dlist_callback_show_dlist(pDlT pdl) { pNdT pnd; unsigned int i, len; len = pdl->num; pnd = pdl->pnd; for (i = 0; i < len - 1; ++i) { printf("%d ", *((int *)(pnd->pe)) ); pnd = pnd->pn; } printf("%s", ((char *)(pnd->pe)) ); printf("\n"); } …… /* Show doubly linked list, called in dlist.c only */ void dlist_show_dlist(pDlT pdl, pdlistCallerShowDlistFun pcallback_fun_show_dlist) { if (NULL == pcallback_fun_show_dlist) { return ; } pcallback_fun_show_dlist(pdl); }
在dlist.c的前面定义回调函数的类型。
typedef void (*pdlistCallerShowDlistFun)(pDlT); |
此语句将pdlistCallerShowDlistFun定义为一个指向参数为pDlT返回值为void的指针类型。
将这两个函数放到dlist.c前面进行声明。修改dlist.c中打印双向链表节点的代码。
/* dlist.c */ …… /* Create len nodes for double linked list */ pDlT create_dlist(unsigned int len) { int i; pNdT pf, pp, pn, tmp; pDlT pdl; …… pdl->pnd = pf; pdl->num = len; //Show dlist node on screen dlist_show_dlist(pdl, dlist_callback_show_dlist); return pdl; } …… /* Insert node into doubly linked list */ int insert_node2dlist(pDlT pdl, pNdT pnd, unsigned int lc) { int i, len; pNdT tpnd; …… pdl->num++; if (i == 1) { pdl->pnd = pnd; } dlist_show_dlist(pdl, dlist_callback_show_dlist); return 0; } /* Delete node form doubly linked list */ int delete_node8dlist(pDlT pdl, unsigned int lc) { int i, len; pNdT tpnd; …… tpnd->pp = NULL; tpnd->pn = NULL; free(tpnd); pdl->num--; dlist_show_dlist(pdl, dlist_callback_show_dlist); return 0; }
在main.c中验证以上提供的回调函数机制。代码保持不变。
Figure5. y15m9d13内部回调函数执行结果
代码II。
代码I中实现的回调函数机制dlist_show_dlist只能在dlist.c中被调用(由于参数的原因)。回调函数的面向对象是调用者,调用者要能够在包含dlist.h(声明dlist.c函数和数据结构名)的文件中调用dlist_show_dlist并能传入自己编写的打印双向链表函数。
保留dlist_show_dlist和dlist_callback_show_dlist,并在dlist_show_dlist前添加static限制符,让其作为dlist.c中的回调机制。在dlist.c和main.c中重新协定回调函数。
/* dlist.c */ …… /* Show doubly linked list by call callback function which writed by caller */ void show_dlist(pDlT pdl, pCallbackShowDlistFunT pcallback_show_dlist_fun) { pNdT pnd; unsigned int i, len; printf("Golbal call calback interface: "); len = pdl->num; pnd = pdl->pnd; for (i = 0; i < len; ++i) { pcallback_show_dlist_fun(pnd->pe, len); pnd = pnd->pn; } }
pCallbackShowDlistFunT类型在dlist.h中被指定,这里反正都要用到dlist.h中的类型。不妨将dlist.c中所有的声明都放在dlist.h中,然后去掉dlist.c中与dlist.h重复的声明(dlist.h主要是给其他文件提供声明,如果dlist.c中各函数按照先定义后使用的顺序定义,则可不必包含dlist.h)。
/* dlist.h */ /* Data type decalarations */ struct _Node; typedef struct _Node *pNdT, NDT; struct _DListNode; typedef struct _DListNode *pDlT, DLT; typedef void (*pCallbackShowDlistFunT)(void *, unsigned int); /* Golbal functions decalarations*/ pNdT alloc_node(void); void free_dlist(pDlT pdl); pDlT create_dlist(unsigned int len); int delete_node8dlist(pDlT pdl, unsigned int lc); int insert_node2dlist(pDlT pdl, pNdT pnd, unsigned int lc); void show_dlist(pDlT pdl, pCallbackShowDlistFunT pcallback_show_dlist_fun);
在main.c中定义符合pCallbackShowDlistFunT类型的回调函数。
/* Callback function for show_dlist() */ void main_callback_show_dlist(void *data, unsigned int len) { static int i = 1; if (i == len) { printf("%s", ((char *)data) ); printf("\n"); } else if (i < len) { printf("%d ", *((int*) data)); ++i; } }
将此函数声明到main()函数的前面,然后调用。
/* main.c */ …… void main_callback_show_dlist(void *data, unsigned int len); /* Entry of C program */ int main(void) { pDlT pdl; …… show_dlist(pdl, main_callback_show_dlist); free_dlist(pdl); return 0; }
main_callback_show_dlist函数可被用户改写以根据双向链表中的具体数据而输出,或者按照pCallbackShowDlistFunT类型编写函数其它的函数传递给show_dlist。在linux终端编译并运行程序。
Figure 6. y15m9d13程序运行
源码目录。
../xtcxyczjh/ y15m9d13/