list *options = read_data_cfg(datacfg);
这是darknet中训练yolo模型时读取datacfg文件的一条语句,这里详细解析一下其内部结构;首先先看list的定义,其实一个双向链表结构
typedef struct list{
int size;
node *front;
node *back;
} list;
然而我们发现它与其他链表不同,因为一般的链表都是使用自身来定义前行与后向指针,从而使得前向指针和后向指针指向自身链表的结点;但是这里很显然并没有使用自身来定义他们;而是使用了其他的双向链表来定义,使得其前后指针指向了node链表结点;其中node的链表结点结构如下:
typedef struct node{
void *val;//void *无类型指针,可以指向任何类型的数据;为了之后方便指向其他类型的数据
struct node *next;
struct node *prev;
} node;
首先跳转到函数主体
list *read_data_cfg(char *filename)
{
FILE *file = fopen(filename, "r");//打开文件
if(file == 0) file_error(filename);//若文件不存在,则显示错误,并跳出函数;
char *line;
int nu = 0;
list *options = make_list();//创建list链表结点,用来存储之后的文件数据,并返回指向list的指针;其函数主体如下所示
/*
list *make_list()
{
list *l = malloc(sizeof(list));
l->size = 0;
l->front = 0;
l->back = 0;
return l;
}
*/
while((line=fgetl(file)) != 0)//判断fgetl(file)读入的line是否为空,即line是否读入了数据
{
++ nu;
strip(line);//去除line中的空格换行和跳格符号,并在最后加上'\0';具体函数如下所示
/*
void strip(char *s)
{
size_t i;
size_t len = strlen(s);
size_t offset = 0;
for(i = 0; i < len; ++i)
{
char c = s[i];
if(c==' '||c=='\t'||c=='\n') ++offset;
else s[i-offset] = c;
}
s[len-offset] = '\0';
}
*/
switch(line[0])//使用switch语句判读line第一个字符是否是'\0'、'#'和';',若是则释放line并跳出,即排查第一个字符,'\0'、'#'和';'起到了注释作用;这里还使用了分支重叠,没有在每一个case下使用break,而是前面三个case共同由一个break跳转;
{
case '\0':
case '#':
case ';':
free(line);
break;
default:
if(!read_option(line, options))//函数主体见下面
{
fprintf(stderr, "Config file error line %d, could parse: %s\n", nu, line);
free(line);
}
break;
}
}
fclose(file);
return options;
}
跳转到fgetl函数主体,其作用就是以行来获取文件数据
char *fgetl(FILE *fp)
{
//feof函数功能:判其功能是检测流上的文件结束符,文件结束:返回非0值;文件未结束:返回0值
if(feof(fp)) return 0;//这里就是说如果文件结束,判断为真,直接退出函数;
size_t size = 512;//定义512个64位无符号整型数据空间
char *line = malloc(size*sizeof(char));//创建存储每行数据的空间
//fgets函数的功能是从 fp文件流中读取 size 个字符存储到字符指针变量line所指向的内存空间。它的返回值是一个指针,指向字符串中第一个字符的地址。
if(!fgets(line, size, fp))//判断文件是否读完,如果文件已经读完,则返回;
{
free(line);//释放内存单元
return 0;
}
size_t curr = strlen(line);//获取读取的文件行长度
//如果文件行的最后一个字符不等于'\n'且文件未结束则进入循环,即判断是否已经读完一行,为了防止文件行的数据大小大于size,而导致的fp指向的那一行数据未能全部读取,且line难以存储一行数据。循环含义就是为了扩容line,将一行之前没有读完的数据读入line中。
while((line[curr-1] != '\n') && !feof(fp))
{
if(curr == size-1)
{
size *= 2;
line = realloc(line, size*sizeof(char));
if(!line)
{
printf("%ld\n", size);
malloc_error();
}
}
size_t readsize = size-curr;
if(readsize > INT_MAX) readsize = INT_MAX-1;
fgets(&line[curr], readsize, fp);
curr = strlen(line);
}
//判断读入行数据中最后一个字符是不是换行,若是则将其改为'\0'
if(line[curr-1] == '\n') line[curr-1] = '\0';
return line;//返回读入的行line
}
read_option函数主体;此函数为读入链表的核心函数
int read_option(char *s, list *options)
{
size_t i;
size_t len = strlen(s);//得出现在文件行的长度
char *val = 0;//用来存储之后的文件数据
for(i = 0; i < len; ++i)
{
//判断若是等号则替换为'\0'将其断开,将后面的数据赋给val,然后跳出
if(s[i] == '=')
{
s[i] = '\0';
val = s+i+1;
break;
}
}
//判断等号后是否有数据,若没有则跳出
if(i == len-1) return 0;
//将等号前的键值赋给key
char *key = s;
option_insert(options, key, val);//将数据读入链表,函数主体见下
return 1;
}
option_insert函数
void option_insert(list *l, char *key, char *val)
{
kvp *p = malloc(sizeof(kvp));//声明一个指向kvp的指针p,并动态存储分配了kvp结构体大小的内存,用来存储key和val数据,其结构体结构如下:
/*kvp结构体
typedef struct{
char *key;
char *val;
int used;
} kvp;
*/
//将key、val分别赋给结构体中的变量
p->key = key;
p->val = val;
p->used = 0;
list_insert(l, p);
}
list_insert函数
void list_insert(list *l, void *val)
{
node *new = malloc(sizeof(node));//声明一个指向node结构体的指针,并动态存储分配内存
new->val = val;//将指针val赋给new->val,使其指向kvp结构体
new->next = 0;//将链表后向指针指向设置为0
if(!l->back)//判断list链表的后向指针指向是否为0,若是则执行if,不为0,则执行else
{
l->front = new;//使list链表的前向指针指向新建立的node链表
new->prev = 0;//将新建的node链表的前向指针指向0
}
else
{
l->back->next = new;//将list链表的后向指针域中的node的后向指针指向新建的node链表new
new->prev = l->back;//将新建的node链表new的前向指针指向list链表的后向指针,由于l->back未更新是指向了上一个结点,所以新建的node链表new的前向指针指向上一个结点;
}
l->back = new;//将list链表的后向指针指向新建的node链表new
++l->size;//记录传入的数据次数
}
**(1)**首先先了解传入 list_insert函数 的参数的结构,其中传入 list *l 的 options 的结构通过 make_list() 函数创建,如下所示:
另一个参数 void *val传入的则是由用来保存文件数据的kvp结构体指针p,也就是结构体的名字,即地址;其指向结构体中的数据;
其中p->key保存了文件行中等号前的数据,p->val用来保存等号后的数据;
(2)函数运行
第一步node *new = malloc(sizeof(node));//创建一个新的node双向链表结点,然后将指针val赋给new->val,使其指向kvp结构体,并将其后向指针指向设置为0;其结果如下图所示:
进入if判断语句,很显然当第一次传输数据时,l->back=0,则 !l->back != 0,为真,执行if语句;故第一次存入链表的过程图如下所示;
第二次传入数据时,l->back=new,不再等于0了,故会执行else语句,下图便是其执行结果图
这里为了方便,省略了kvp结构体;从上图中可以看出,l->front的功能就是一个头指针,永远都指向第一个结点,而l->back就相当于一个移动的temp指针,永远指向新的结点。
如下图所示:
list链表结点l中的l->front 是一个头指针,可以通过它来查询保存在node链表中的数据,l->back就是双向链表中的一个移动指针,其永远指向链表中的最后一个结点。这样和普通双向链表一样,只需要通过返回的头指针就能找到链表中的数据。