知识巩固源码落实之6:c语言拼接字符串与切割字符串(strsep)代码

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 知识巩固源码落实之6:c语言拼接字符串与切割字符串(strsep)代码

网络数据是以流的形式进行传输的(我们在构造客户端/服务端待发送数据时,肯定有特定的格式)。

关于发送一次,一个包的完整性可靠接收(tcp的可靠传输,以及取数据的半包粘包问题)这里不关注,可以参考上文。

这里的目的是

===》备份c语言字符处理相关方案

===》备份自己在业务实现时,对字符串切割实现的一个接口(strstr,strcasestr,strsep,strtok,strdup相关接口)

有如下几个细节可以关注

===》1:实现不同类型的数据拼接字符串的方案 (3.1)

===》2:char**通过传参的方式再函数内部进行构造 (3.2.4.2)

===》3:对字符串进行解析处理的几种方案,尤其是分割字符串的实现(strsep实现了demo)(3.2.4)

1:背景描述

网络传输过程中,如tcp的客户端可服务端进行通信时,send函数实际发送的数据是用字符数组+长度表示的,即流的形式。

有关一次发送,一个包的完整接收,依赖于tcp底层的可靠流式传输,需要进行半包和粘包处理,可以参考上文。

但是,这里针对一个包的结构,我们其实也会需要根据我们的业务场景进行设计,按照特定的结构进行构造,与解析。

这里关注一次发送一个包的构造与解析方案,以及代码实现做备份

2:逻辑描述

2.1:大概描述

网络传输中,对字符流的处理非常重要,建立在我们制定的特定的规范上

===》除了关注一个包的完整接收(粘包半包问题),还需要关注数据本身

===》1:类似tcp/ip协议栈那样,用特定的协议栈,特定的字节表示特定的含义进行解析(固定字节大小表示含义)

===》2:我们可以自定义规范,如用特定的字符/字符串进行识别,分割不同字符所表示的含义(如 msg_type|other_type|msg_len|msg_data).

这里针对以上所描述的两种方案的数据的构造与解析,进行梳理及代码测试。。。

2.2:数据的构造(目的构造特定格式待发送的char*及获取长度)

一般发送端和接收端对数据的处理都是协商好的,按照特定的格式构造,必然会按照特定的格式进行解析。

数据的构造本质:其实就是按照特定的格式给char指针中塞入协商的格式的数据,以及获取到实际发送的数据的长度

===》1:类似tcp/ip协议头,可以通过结构体指针强转,以及获取实际长度进行发送

===》2:构造要发送的char*指针及获取长度方案:使用 memcpy

===》3:构造要发送的char*指针及获取长度方案:c库函数,strcpy, strcat

===》4:构造要发送的char*指针及获取长度方案:使用sprintf、sprintf_s觉得最好用

2.3:数据的解析(目的对接收到的char*按协商的格式进行解析)

以流的形式,接收到对待的数据(实际是char*指针和数据长度),对其按照协商进行解析。

解析的本质:按照特定的字节,或者对整个流进行处理依次取特定位置数据

===》1:指针强转,类似tcp/ip协议栈处理,可以把接收到的报文,直接转为结构体指针进行操作

===》2:先取特定长度,再按这个长度取实际数据。 (发送先发送长度+data(可以自己定义解析格式))

===》3:解析接收到的流,进行解析(如按照“|”进行分割

本文的目的之一也是为了备份按照“|”进行分割的一个实践代码

3:代码梳理

3.1:待发送数据构造的demo

3.1.1:结构体指针转为char*实现/解析逻辑

构造结构体,计算实际长度,send时直接转为char*传参进行发送。。。

再linux环境上使用gcc进行编译测试过,代码执行ok

//1:类似tcp/ip协议栈,其实都是按照特定的结构体进行类型强转数据解析
void parse_struct_format_data()
{
printf("parse_struct_format_data test: \n");
  //假设定义了柔性数组,构造数据并解析数据的流程如下
  struct my_data_t
  {
    int msg_type;
    int data_len;
    char data[0];
  };
  //假设要发送的数据为client send data example. \n, 类型为1
  const char* data = "client send data example. \n";
  //发送端构造最终的数据如下
  struct my_data_t *send_data = NULL;
  send_data = (struct my_data_t*)malloc(sizeof(struct my_data_t)+strlen(data)+1);
  if(send_data == NULL)
  {
    return ;
  }
  memset(send_data, '\0', sizeof(struct my_data_t)+strlen(data)+1);//只是预留了一个位而已
  send_data->msg_type = 1;
  send_data->data_len = strlen(data);
  memcpy(send_data->data, data, strlen(data));
  //这里其实 send_data就是我们最终构造的一个网络数据格式包,可以进行发送
  //使用send进行发送时,只是传参char*以及待发送的数据长度
  char *send_para_data = (char *)send_data; //结构体类型强转  tcp识别到的是这个结构里的流数据
  int send_para_data_len = sizeof(struct my_data_t)+strlen(data);
  //如果客户端使用send_para_data 及长度send_para_data_len 进行发送及校验 。。。
  //我们收到的流,以recv进行接收,就是一个字符流,内容其实就是send_para_data 长度为send_para_data_len 
  //这里要完整的接收到一个包(半包粘包问题,可参考上文)
  //进行逆向解析就好
  struct my_data_t *recv_data = (struct my_data_t*)send_para_data;
  //可以按照柔性数组的逻辑 按照长度对实际数据进行解析打印
  printf("\trecv_data type is [%d] \n",recv_data->msg_type);
  printf("\trecv_data len is [%d] \n",recv_data->data_len);
  //这里只是测试  注意如果这个data中有特殊的字符如\0等,不能这样打印,要按照十六进制按照长度打印
  printf("\trecv_data len is [%s] \n", recv_data->data);
  printf("recv_data ASSII is [");
  for(int i=0; i<recv_data->data_len; i++)
  {
    printf("%02x ", recv_data->data[i]);
  }
  printf("]\n");
  if(send_data!= NULL)
  {
    free(send_data);
    send_data = NULL;
  }
}

3.1.2:使用memcpy实现字符串的拼接,构造char*

目的是获得最终的char*指针位置以及实际发送的数据长度。

这里因为懒,使用了to_string把int转为string进行测试,所以编译的时候用c++11,可以修改用itoa…

//1:使用memcpy实现字符串的拼接 splicing test of string1 and string2
int use_memcpy_splic_string()
{
printf("use_memcpy_splic_string test:\n");
  const char* str1 = "splicing test of ";
  const char* str2 = "string";
  const char* and1 = " and ";
  int one = 1;
  int two = 2;
  //这里用到了C++中的to_string函数  c语言可以用itoa,这里主要是理解字符串拼接,,, 有int类型的字符串转换拼接常用sprintf
  int size = strlen(str1) + strlen(str2) * 2 + (strlen(to_string(one).c_str())) + strlen(and1) + (strlen(to_string(two).c_str())) + 1;
  printf("\tget the len is : %d %lu\n", size, strlen("splicing test of string1 and string2"));
  int pos = 0;
  char* result = (char*)malloc(size);
  if (result == NULL)
  {
    return -1;
  }
  memset(result, '\0', size);
  memcpy(result, str1, strlen(str1));
  pos += strlen(str1);
  memcpy(result + pos, str2, strlen(str2));
  pos += strlen(str2);
  memcpy(result + pos, to_string(one).c_str(), strlen(to_string(one).c_str()));
  pos += strlen(to_string(one).c_str());
  memcpy(result + pos, and1, strlen(and1));
  pos += strlen(and1);
  memcpy(result + pos, str2, strlen(str2));
  pos += strlen(str2);
  memcpy(result + pos, to_string(two).c_str(), strlen(to_string(two).c_str()));
  pos += strlen(to_string(two).c_str());
  printf("\tthe result is [%lu][%s]  \n", strlen(result), result);
  printf("\tpos is [%d] \n", pos);
  if (result != NULL)
  {
    free(result);
    result = NULL;
  }
  return 0;
}

3.1.3:使用c库函数(strcpy, strcat)

偷懒用了to_string(),编译测试用c++11

//2:使用c库函数实现字符串的拼接 splicing test of string1 and string2
//写代码的时候要注意目标字符串的长度一定要够用 
int use_clibrary_strcat_splic_string()
{
printf("use_clibrary_strcat test: \n");
  const char* str1 = "splicing test of ";
  const char* str2 = "string";
  const char* and1 = " and ";
  int one = 1;
  int two = 2;
  //这里用到了C++中的to_string函数  c语言可以用itoa,这里主要是理解字符串拼接,,, 有int类型的字符串转换拼接常用sprintf
  int size = strlen(str1) + strlen(str2) * 2 + (strlen(to_string(one).c_str())) + strlen(and1) + (strlen(to_string(two).c_str())) + 1;
  printf("\tget the len is : %d %lu\n", size, strlen("splicing test of string1 and string2"));
  char* result = (char*)malloc(size);
  if (result == NULL)
  {
    return -1;
  }
  memset(result, '\0', size); 
    //vs上测试时要用strcpy_s
  // strcpy_s(result, size, str1);
  // strcat_s(result, size, str2);
  // strcat_s(result, size, to_string(one).c_str());
  // strcat_s(result, size, and1);
  // strcat_s(result, size, str2);
  // strcat_s(result, size, to_string(two).c_str());
  //注意字符串处理时的不安全性!!!
  strcpy(result,  str1);
  strcat(result,  str2);
  strcat(result,  to_string(one).c_str());
  strcat(result,  and1);
  strcat(result,  str2);
  strcat(result,  to_string(two).c_str());
  printf("\tthe result is [%lu][%s]  \n", strlen(result), result);
  if (result != NULL)
  {
    free(result);
    result = NULL;
  }
  return 0;
}

3.1.4:使用sprintf(sprintf_s):最实用和方便

vs上测试时需要使用sprintf_s

int use_sprintf_splic_string()
{
  printf("use_sprintf_splic_string test: \n");
  const char* str1 = "splicing test of ";
  const char* str2 = "string";
  const char* and1 = " and ";
  int one = 1;
  int two = 2;
  //定义目标字符串,为其申请内存都是必须的
  int size = strlen(str1) + strlen(str2) * 2 + (strlen(to_string(one).c_str())) + strlen(and1) + (strlen(to_string(two).c_str())) + 1;
  printf("\tget the len is : %d %lu\n", size, strlen("splicing test of string1 and string2"));
  char* result = (char*)malloc(size);
  if (result == NULL)
  {
    return -1;
  }
  memset(result, '\0', size);
  //使用sprintf一步到位
  sprintf(result, "%s%s%d%s%s%d", str1, str2, one, and1, str2, two);
  //sprintf_s(result, size, "%s%s%d%s%s%d", str1, str2, one, and1, str2, two);
  printf("\tthe result is [%lu][%s]  \n", strlen(result), result);
  if (result != NULL)
  {
    free(result);
    result = NULL;
  }
  return 0;
}

3.2:接收数据进行解析的demo

3.2.1:直接把char*转为结构体(类似tcp/ip协议栈处理)

参考3.1.1

3.2.2:按照协商的自己含义,使用memcpy按字节处理

参考3.1.2

3.2.3:先接收长度,再接收数据的解析

网络数据有个场景,可以先发送实际数据的长度,recv时先接收特定字节长度,再接收实际数据,保证报的完整.

//2:特定字节表示长度,对后面的数据进行处理==》其实和结构体格式差不多,柔性数组
void parse_len_and_data_networkdata()
{
printf("parse_len_and_data_networkdata test: \n");
  //在网络传输中,可以用特定字节表示长度+实际数据的格式
  const char *send_data = "msg_type | msg_len |msg_data ...\n"; //实际数据有特殊字符的话,长度要传进来或者结构体其他方案
  //在网络发送中,如果想简单实现不想用结构那么麻烦,可以这样用 
  int send_len = strlen(send_data);
  printf("\tsend_len [%d][%s] \n",send_len, send_data);
  //可以类似上个函数用结构体  struct data_t{int len; char data[0];};构造
  //我试试这种:
  char * real_send_data = (char *)malloc(send_len +4+1);
  memset(real_send_data, 0, send_len +4+1);
  memcpy(real_send_data, (char*)&send_len, sizeof(int)); //前四个字节拷贝长度,也可以以字符串形式直接存进去 to_string(send_len).c_str()
  memcpy(real_send_data +sizeof(int), send_data, send_len);
  //real_send_data 就是我们实际send的流   我们可以先接收取前四个字节获取数据长度,再接收后面的字段
  //先定义一个int,从recv取四个字节,转为int表示的长度
  int recv_len = *(int *)real_send_data; //先recv取四个字节  解析成真正的数据
  char* recv_data = real_send_data+4;  //这里应该是recv读出来的 长度为recv_len 
  printf("\trecv_len[%d] [%s] \n",recv_len, recv_data);
  if(real_send_data != NULL)
  {
    free(real_send_data);
    real_send_data = NULL;
  }
}

3.2.4:如果是按字符串进行分割的设计,对其进行解析

这里只是实现了切割字符串的其中一种方案。

3.2.4.1:数据的构造(符合”|“切割的字符串:”msg_type|other_type|msg_len|msg_data“):
//模拟一个完整的包,返回一个符合特定格式的拼接起来的包数据
//假设 msg_type|other_type|msg_len|msg_data 格式
int get_concatenate_strings(char ** result_data, int* len)
{
  // 假设格式msg_type|other_type|msg_len|msg_data 格式
  const char * data = "mytest of spilt of send data ... \n\t test";//注意网络数据这里的复杂,用memcpy处理
  int msg_type = 1;
  int other_type = 2;
  int msg_len = strlen(data); //实际后面的数据长度
  //这里估算一下最终最大长度 用20+strlen(data)肯定够用
  char *send_data = NULL;
  send_data = (char*)malloc(20+msg_len);
  memset(send_data, 0, 20+msg_len);
  sprintf(send_data, "%d|%d|%d|", msg_type, other_type, msg_len);
  //通过传参的方式传递出去
  *len = strlen(send_data) +msg_len;
  memcpy(send_data+strlen(send_data), data, msg_len);
  *result_data = send_data;
  printf("\t result_data is [%d][%s] \n",*len, send_data);
}
3.2.4.2:按特定字符对字符串实现切割解析(解析提取”msg_type|other_type|msg_len|msg_data“各字段含义)

这里可以关注一下这几个库函数实现字符串切割的方案,测试只是其中一种

char * strstr(const char *haystack, const char *needle); 函数定位子串的位置,然后实现字符串的切割

char * strcasestr(const char *haystack, const char *needle); 和strstr同样的功能,只是忽略两个参数的大小写

char *strtok(char *str, const char *delim); 分解字符串 str 为一组字符串,delim 为分隔符

char *strsep(char **stringp, const char *delim); strtok的升级,

//这里使用"|"对长度为len目标数据data进行切割,通过参数result返回解析后的数据
int check_recv_data_by_spilit(const char * data, int len, char **result, const char* delim)
{
  //直接根据'|'对字符串进行切割 根据切割后的个数进行校验
  char *src = strdup(data); //复制了一份数据 strsep会修改原字符串
  char * src_free = src; 
  //msg_type|other_type|msg_len|msg_data  按照协商的格式 定义了暂存的指针
  char * delim_buff[4] = {0};
  char* token = NULL;  //切割后返回的字符指针
  int i = 0;       //切割的个数
  //返回的是符合的分割的字符   同时原字符从分割后的位置开始
  for(token = strsep(&src, delim); token!=NULL && i<4; token=strsep(&src, delim))
  {
    delim_buff[i++] = token;
    printf("\tspilt data [%d:%lu:%s] \n", i, strlen(token), token);
    printf("\t\t src:[%s] \n",src);
  }
  if(i != 4) //必然是协商的格式 
  {
    printf("\tvps spilit data error \n");
    free(src_free);
    return -1;
  }
  int msg_type = (int)atoi(delim_buff[0]);
  int dev_type = (int)atoi(delim_buff[1]);
  int data_len = (int)atoi(delim_buff[2]);
  char * cli_data = delim_buff[3];
  printf("\nmsg_type:%d, dev_type:%d, data_len:%d:%lu:[%s] \n", msg_type, dev_type, data_len, strlen(cli_data), cli_data);
  //把解析后的数据进行处理 使用传参传出去
  int ret = 0;
  struct client_recv_t *result_t = NULL;
  result_t = (struct client_recv_t *)malloc(sizeof(struct client_recv_t) + data_len+1);
  if(result_t == NULL)
  {
    printf("malloc error \n");
    *result = NULL;
    ret = -1;
  }else
  {
    memset(result_t, sizeof(struct client_recv_t) + data_len+1, 0);
    result_t->msg_type = msg_type;
    result_t->dev_type = dev_type;
    result_t->data_len = data_len;
    memcpy(result_t->data, cli_data, data_len);
    *result = (char *)result_t;
  }
  memset(src_free, 0, len);
  free(src_free);
  src_free = NULL;
  return ret;
}
3.2.4.2:测试代码:

再linux环境直接用gcc进行编译测试,代码ok。。。

//定义一个保存解析后数据的结构
struct client_recv_t
{
  int msg_type;
  int dev_type;
  int data_len;
  char data[0];
};
int parse_spilt_string_and_getdata(const char * data, int len)
{
  printf("\tneed parse data is [%d][%s] \n", len, data);
  //使用特定字符串对字符串进行切割,这里举例是"|",可以是其他的字符串。。。
  //协商的协议是:msg_type|other_type|msg_len|msg_data 
  //实际数据是:[47][1|2|40|mytest of spilt of send data ... \n\t test
  char * result_data_t = NULL; //存储解析后的数据,也可以是其他方案,这里只是举例
  //这里使用"|"对长度为len目标数据data进行切割,通过传参获取 result_data_t
  if(check_recv_data_by_spilit(data, len, &result_data_t, "|") != 0)
  {
    printf("vps parse spilit error \n");
    return -1;
  }
  //对解析后的数据进行打印
  struct client_recv_t *result_data = (struct client_recv_t *)result_data_t;
  printf("\t parse test data is [%d][%d][%d][%s] \n", result_data->msg_type, result_data->dev_type, result_data->data_len, result_data->data);
  // memset(result_data, sizeof(struct client_recv_t)+ (result_data->data_len+1), 0);
  if(result_data != NULL)
  {
    free(result_data);
    result_data = NULL;
    printf("free success \n");
  }
  return 0;
}
//真正的入口
void parse_string_spilt_data()
{
printf("parse_string_spilt_data test: \n");
  //假设协商的协议是msg_type|other_type|msg_len|msg_data 
  //构造一个数据,获取到最终发送的数据和数据长度
  char * send_data = NULL;
  int send_len = 0;
  if(get_concatenate_strings(&send_data, &send_len) < 0)
  {
    printf("\t make send_data error \n");
    return;
  }
  printf("\t last_result_data is [%d][%s] \n",send_len, send_data);
  //假设对这个数据进行发送了,报的完整性参考上文
  //接收其实就是这样格式的数据,解析一下这个 这里recv接收时要关注包的完整性
  parse_spilt_string_and_getdata(send_data, send_len);
  if(send_data != NULL)
  {
    free(send_data);
    send_data = NULL;
  }
}

我开始试着积累一些常用代码:自己代码库中备用

我的知识储备更多来自这里,推荐你了解:Linux,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK等技术内容,立即学习

目录
相关文章
|
9天前
|
安全 C语言
在C语言中,正确使用运算符能提升代码的可读性和效率
在C语言中,运算符的使用需要注意优先级、结合性、自增自减的形式、逻辑运算的短路特性、位运算的类型、条件运算的可读性、类型转换以及使用括号来明确运算顺序。掌握这些注意事项可以帮助编写出更安全和高效的代码。
21 4
|
23天前
|
存储 C语言
【C语言基础考研向】10 字符数组初始化及传递和scanf 读取字符串
本文介绍了C语言中字符数组的初始化方法及其在函数间传递的注意事项。字符数组初始化有两种方式:逐个字符赋值或整体初始化字符串。实际工作中常用后者,如`char c[10]=&quot;hello&quot;`。示例代码展示了如何初始化及传递字符数组,并解释了为何未正确添加结束符`\0`会导致乱码。此外,还讨论了`scanf`函数读取字符串时忽略空格和回车的特点。
|
23天前
|
存储 Serverless C语言
【C语言基础考研向】11 gets函数与puts函数及str系列字符串操作函数
本文介绍了C语言中的`gets`和`puts`函数,`gets`用于从标准输入读取字符串直至换行符,并自动添加字符串结束标志`\0`。`puts`则用于向标准输出打印字符串并自动换行。此外,文章还详细讲解了`str`系列字符串操作函数,包括统计字符串长度的`strlen`、复制字符串的`strcpy`、比较字符串的`strcmp`以及拼接字符串的`strcat`。通过示例代码展示了这些函数的具体应用及注意事项。
|
26天前
|
存储 算法 C语言
数据结构基础详解(C语言):单链表_定义_初始化_插入_删除_查找_建立操作_纯c语言代码注释讲解
本文详细介绍了单链表的理论知识,涵盖单链表的定义、优点与缺点,并通过示例代码讲解了单链表的初始化、插入、删除、查找等核心操作。文中还具体分析了按位序插入、指定节点前后插入、按位序删除及按值查找等算法实现,并提供了尾插法和头插法建立单链表的方法,帮助读者深入理解单链表的基本原理与应用技巧。
|
26天前
|
存储 C语言 C++
数据结构基础详解(C语言) 顺序表:顺序表静态分配和动态分配增删改查基本操作的基本介绍及c语言代码实现
本文介绍了顺序表的定义及其在C/C++中的实现方法。顺序表通过连续存储空间实现线性表,使逻辑上相邻的元素在物理位置上也相邻。文章详细描述了静态分配与动态分配两种方式下的顺序表定义、初始化、插入、删除、查找等基本操作,并提供了具体代码示例。静态分配方式下顺序表的长度固定,而动态分配则可根据需求调整大小。此外,还总结了顺序表的优点,如随机访问效率高、存储密度大,以及缺点,如扩展不便和插入删除操作成本高等特点。
|
26天前
|
存储 人工智能 C语言
C语言程序设计核心详解 第八章 指针超详细讲解_指针变量_二维数组指针_指向字符串指针
本文详细讲解了C语言中的指针,包括指针变量的定义与引用、指向数组及字符串的指针变量等。首先介绍了指针变量的基本概念和定义格式,随后通过多个示例展示了如何使用指针变量来操作普通变量、数组和字符串。文章还深入探讨了指向函数的指针变量以及指针数组的概念,并解释了空指针的意义和使用场景。通过丰富的代码示例和图形化展示,帮助读者更好地理解和掌握C语言中的指针知识。
|
26天前
|
存储 C语言
数据结构基础详解(C语言): 栈与队列的详解附完整代码
栈是一种仅允许在一端进行插入和删除操作的线性表,常用于解决括号匹配、函数调用等问题。栈分为顺序栈和链栈,顺序栈使用数组存储,链栈基于单链表实现。栈的主要操作包括初始化、销毁、入栈、出栈等。栈的应用广泛,如表达式求值、递归等场景。栈的顺序存储结构由数组和栈顶指针构成,链栈则基于单链表的头插法实现。
151 3
|
26天前
|
存储 算法 C语言
C语言手撕实战代码_二叉排序树(二叉搜索树)_构建_删除_插入操作详解
这份二叉排序树习题集涵盖了二叉搜索树(BST)的基本操作,包括构建、查找、删除等核心功能。通过多个具体示例,如构建BST、查找节点所在层数、删除特定节点及查找小于某个关键字的所有节点等,帮助读者深入理解二叉排序树的工作原理与应用技巧。此外,还介绍了如何将一棵二叉树分解为两棵满足特定条件的BST,以及删除所有关键字小于指定值的节点等高级操作。每个题目均配有详细解释与代码实现,便于学习与实践。
|
26天前
|
存储 算法 C语言
C语言手撕实战代码_二叉树_构造二叉树_层序遍历二叉树_二叉树深度的超详细代码实现
这段代码和文本介绍了一系列二叉树相关的问题及其解决方案。其中包括根据前序和中序序列构建二叉树、通过层次遍历序列和中序序列创建二叉树、计算二叉树节点数量、叶子节点数量、度为1的节点数量、二叉树高度、特定节点子树深度、判断两棵树是否相似、将叶子节点链接成双向链表、计算算术表达式的值、判断是否为完全二叉树以及求二叉树的最大宽度等。每道题目均提供了详细的算法思路及相应的C/C++代码实现,帮助读者理解和掌握二叉树的基本操作与应用。
|
26天前
|
存储 算法 C语言
C语言手撕实战代码_循环单链表和循环双链表
本文档详细介绍了用C语言实现循环单链表和循环双链表的相关算法。包括循环单链表的建立、逆转、左移、拆分及合并等操作;以及双链表的建立、遍历、排序和循环双链表的重组。通过具体示例和代码片段,展示了每种算法的实现思路与步骤,帮助读者深入理解并掌握这些数据结构的基本操作方法。