目录
问题17:对于一个树形结构我该如何free(之前malloc的)
前言
经过十多天的学习,终于把这个 库写完了。非常感谢那位作者,从他的教程中学到了许多的东西,这篇文章就是我对这十多天学习的总结。
一、 不足之处
在完成代码后,我将自己的代码与作者的代码做了比较,当然凭我的水平还没办法评价大佬的代码,我感觉我的代码的一个大问题是函数的命名比较的混乱,反观作者的代码,你可以非常直观的根据函数名来推测函数的用途。当然我的程序还有许多的其他问题,因为水平有限,我无法具体的描述。 你可以在这里看到我的代码myjson 。
二、学习过程中记录的问题
问题1,在函数前面声明static
我在刚开始学习的c的后,我记得那个老师老说,因为这个函数不需要对外扩展,所以我们在前面声明static。我当时还非常的奇怪。等到我写这个库的时候,我才发现,我在一个函数的前面声明static,那么这个函数只可以在定义它的文件内访问,否者编译出错。下面的内容来自newbing:
你好,这是必应。根据我的搜索结果,c语言关键字static的所有知识可以概括为以下几点:
- static的字面意思是**静态的**,它可以用来修饰变量和函数,表示它们在静态存储区分配内存,具有持久性和默认值0¹。
- static修饰的变量和函数具有**隐藏性**,即它们只能在定义它们的文件内部访问,不能被其他文件引用¹²⁴。
- static修饰的局部变量(在函数内部定义的变量)不会在函数调用结束后销毁,而是保留上一次的值,可以实现计数器等功能。
- static修饰的全局变量(在函数外部定义的变量)只能在本文件内使用,不会与其他文件中同名的变量冲突¹。
- static修饰的函数只能在本文件内调用,不会与其他文件中同名的函数冲突。
问题2
初始化的问题。
我在增加stack_top栈顶指针的时候(当然实际他是size_t),没有初始化它,结果在++的时候出错。所以请注意初始化的问题,尤其是你需要对一个量惊醒加或减。
问题3 内存泄漏
内存泄漏检测工具的使用valgrind 检测内存泄漏和一些未初始化的问题,同时也要去看看他的其他功能。(2条消息) valgrind 详解_Ruo_Xiao的博客-CSDN博客 ,看了一下基本的介绍,目前还是我还是检测内存泄漏用的多点。
问题4
number too big 的问题,到底要不要设置errno == eagain(哦,原来我搞错了,是ERANGE),怪不得下面的代码错了。
#if 1
if((errno == EAGAIN) && (number == HUGE_VAL || number == -HUGE_VAL))
return S_PARSE_NUMBER_TOOBIG;
#else
if ((number == HUGE_VAL || number == -HUGE_VAL))
return S_PARSE_NUMBER_TOOBIG;
#endif
问题5
碰到的一个问题Invalid write of size 1 ==103893== at 0x4065C1: s_parse_set_string (in /home/jiawen/mycfile/myjson/build/test) ==103893== by 0x401A05: test_access_null (in /home/jiawen/mycfile/myjson/build/test) ==103893== by 0x405A50: test_parse (in /home/jiawen/mycfile/myjson/build/test) ==103893== by 0x405A5C: main (in /home/jiawen/mycfile/myjson/build/test) ==103893== Address 0x4a242c3 is 0 bytes after a block of size 3 alloc'd ==103893== at 0x483577F: malloc (vg_replace_malloc.c:299) ==103893== by 0x4065A4: s_parse_set_string (in /home/jiawen/mycfile/myjson/build/test) ==103893== by 0x401A05: test_access_null (in /home/jiawen/mycfile/myjson/build/test) ==103893== by 0x405A50: test_parse (in /home/jiawen/mycfile/myjson/build/test) ==103893== by 0x405A5C: main (in /home/jiawen/mycfile/myjson/build/test) ==103893==
这个问题来自
value->data.string.s = malloc(size + 1);
value->data.string.s[size] = '\0';
assert(value->data.string.s != NULL);
当我设置value->data.string.s[size + 1] = '\0';时,带来了这个问题。为什么呢?因为s[size + 1] 越界了,如果数组大小为a[size + 1],我只能访问到 a[size],所以越界带了了写入错误。太粗心了。
问题6 gcc优化选项
在第3章解答中的性能优化问题。我找了一下有关gcc的优化选项的相关介绍,好像这个程序使用优化,没什么感觉,那优化的问题就留着后面讨论吧。
问题7,内联函数和宏
内联函数的问题,文章的评论区有说使用内联函数来代替宏。
宏的优点:
使用带参数的宏定义既可以完成函数调用的功能,又可以避免函数的出栈入栈操作,减少系统开销,提高运行效率。宏是由预处理器处理的,通过字符串操作可以完成很多编译器无法实现的功能,比如##连接符
例如:
#define CHR(x,y) (x##y)
则CHR(hello,world),则(x##y) ,就是helloworld. ##在定义变参也会用到。
缺点:
宏的定义容易产生二义性,宏定义没有参数检查不安全。
内联函数的优点:
1.直接将代码插入调用处,减少普通函数调用时的资源消耗
2.有参数检测更安全
3.inline关键字只是对编译器的一个定义,如果函数不符合内联函数的标准,编译器就会把这个函数当成普通函数
缺点:
1.内联函数以复制为代价,活动产生开销
2.如果函数代码过长,使用内联函数会消耗过多内存
3.如果内联函数体有循环,执行函数代码时间调用开销大
那么,使用inline函数需要注意什么呢?
1、inline函数在第一次被调用前必须进行完整的定义,否则编译器无法知道应该插入什么代码。
2、在inline函数里一般不能含有复杂的控制语句,如for、switch等
3、inline函数是一种用空间换时间的措施,函数体不宜太长,否则反而会增大系统开销,一般为1~5条语句。
4、inline和宏定义相似,但不完全相同,宏定义只做简单的字符替换而不做语法检查,往往会出现意想不到的错误。
问题8 ,左移和右移的问题
在程序中接触到了许多左移和右移的问题,同时我也对左移就是乘2,右移是除2产生了疑问。来看下面的例子:
对于二进制100 000,如果左移不就变成了000 000了吗?后来我发现左移是100 000 0,对的就是添0,相当于增加了一位。其实这与计算机存储它的时候又多少位有关,如果他就是6位当然是错的,可是在计算机中他不止有6位。他可能是8 位,16位等
那么接来就来仔细的看看左移和右移的问题。
1.逻辑左移和逻辑右移
这个简单,空余位都是补零。所以这个时候便会出现左乘2和右移除2的情况。(稍后我们来讨论一下溢出的情况)。
2.算术右移
因为算术左移和逻辑左移是一样的,不需要讨论。对于算术右移,首先是要保留符号位,如果符号位是1,则补1,否则补零。
那么算术右移也是除2吗?带着这个问题,我找了一些资料,我们先从最开始的源码,反码,补码开始吧。(突然间发现以前学的这个都忘了,所以复习一下)。
1.对于正数,它的原码,反码,补码是相同的。
2.对于负数,它的反码是,保持原码的符号位不变,其余各位取反,然后它的补码是在反码的基础上加1。计算机中用补码来表示负数,这样做便于计算。
现在来看一个负数 1101 1000,它表示-40。我开始还奇怪这怎么是-40呢?原来计算机中用补码来表示负数,这样以来算术右移也是除2.,如我右移3位,变成1111 1011,那就是-5。
现在对这个问题也有了初步的理解,现在我们再来考虑一下溢出的问题。现在随便看一个正数, 1111 0000(在这里我们假设它存储的时候就是八位),那么左移2位就是1100 0000,显然这是不符合规定的,原来这种倍数关系只适用于左移后被舍弃的高位不含1的情况,否则会溢出。这对于右移来说同样是适用的。来看一段程序:
首先是正数的情况:
#include <stdio.h>
int main()
{
int a = 8;
printf("0x%x\n", a);
a <<= 31;
printf("0x%x\n", a);
}
#include <stdio.h>
int main()
{
int a = 8;
printf("0x%x\n", a);
a >>= 1;
printf("0x%x\n", a);
int b = 8;
printf("0x%x\n", b);
b >>= 4;
printf("0x%x\n", b);
}
负数的情况:
#include <stdio.h>
int main()
{
int a = -8;
printf("0x%x->%d\n", a, a);
a >>= 1;
printf("0x%x->%d\n", a, a);
int b = -8;
printf("0x%x->%d\n", b, b);
b >>= 4;
printf("0x%x->%d\n", b, b);
}
在移位运算时,byte、short和char类型会先被转换为int类型,然后再进行移位。对于byte、short、char和int进行移位时,如果编译器没有进行优化(优化后的结果可能不同),那么实际移动的次数是移动次数对32取模,也就是移位33次和移位1次得到的结果相同。对于long型的数值进行移位时,如果编译器没有进行优化,那么实际移动的次数是移动次数对64取模,也就是移位66次和移位2次得到的结果相同。
问题9,数据结构和算法
虽然这个库代码没有很多,但是对比我之前写的程序,它的数据结构还是极好的。我以前在定义数据结构的时候都是不加思考的,所以一看到他定义的数据结构,感觉豁然开朗。我还记得有次我的同学告诉我用枚举来报错,我一直没有做,这里做了。来看一下:
/**
* @enum S_DATATYPE [sjson.h]
* @brief all the datatype for sjson
*
*
*/
typedef enum {
S_NULL = 0,
S_TRUE,
S_FALSE,
S_NUMBER,
S_ARRAY,
S_STRING,
S_OBJIECT
}S_DATATYPE;
typedef struct s_member s_member;
typedef struct s_value s_value;
/**
* @struct s_value [sjson.h]
* @brief store the sjson datatype
*
*
*/
struct s_value {
union {
struct {char *s; size_t str_len;}string;
struct {s_value *element; size_t len; size_t capacity;}array;
struct {s_member *m; size_t size; size_t capacity;}member;
double number;
}data;
S_DATATYPE type;
};
/**
* @struct s_member [sjson.h]
* @brief store the sjson object
*
*
*/
struct s_member {
char *key;
size_t key_len;
s_value v;
};
/**
* @struct s_content [sjson.h]
* @brief sjson structure content
*
*
*/
typedef struct {
const char *s_json;
char *stack;
size_t stack_size, stack_top;
}s_content;
/**
* @enum in sjson.h
* @brief
*
*
*/
enum {
S_PARSE_OK = 0, //PARSE OK means parse success
S_PARSE_INVALID_VALUE, //value is invalid
S_PARSE_EXPECT_VALUE, //value is not expect
S_PARSE_VALUE_ERR, //value has section error
S_PARSE_NUMBER_TOOBIG, //number so big
S_PARSE_STRING_MISS_QUOTATION, //MEET '\0'
S_PARSE_STRING_INVALID_VALUE, //at now ,i don't konw ESCAPE
S_PARSE_STRING_INVALID_CHAR, //char err
S_PARSE_STRING_UNICODE_INVALID_SURROGATE, //H,L INVALID
S_PARSE_STRING_INVALID_UNICODE_HEX, //unicode is invalid ,eg: 0xuu;
S_PARSE_ARR_ERR, //array error
S_PARSE_OBJECT_MISS_KEY, //miss key
S_PARSE_OBJECT_MISS_COLON, //miss ':'
S_PARSE_OBJECT_ERR //i don't know
};
写到这里,我感觉我接下来的任务应该是学习数据结构。
问题10 ,assert
这次是我第一次使用assert,以前我都是使用if。参考 C语言:断言assert函数完全攻略_可以仔细的了解assert。
问题11,0x20,以及\u
我开始对下面的代码有点疑问,不知道具体是干什么的:
if (ch < 0x20)
{
*p++ = '\\';
*p++ = 'u';
*p++ = '0';
*p++ = '0';
*p++ = d[ch >> 4];
*p++ = d[ch & 15];
}
else
*p++ = s[i];
(unsigned char)ch < 0x20,为什么是0x20呢,因为0x20之前都是不可打印的【C语言笔记】ASCII码可见字符与不可见字符,所以在这里我们需要转义。那么接下来就来看看转义。ch >> 4,是为了获得高4位,ch&15是为了获得低四位。
问题12 ,gdb调试
待研究!!!
问题13,浮点数转int
下面的代码,s_get_number的返回值是浮点数,但是i是int 没有加浮点数时,返回的浮点数会转化为int,导致错误,所以写程序的时候要注意。,特别要注意的是我们在格式化输出的时候,先强转一下,否者可能出错。
for (i = 0; i < 3; i++) {
s_value *value1 = s_parse_get_array_element(s_parse_get_member_value(&value, 5), i);
EXPECT_EQ_INT(S_NUMBER, s_get_type(value1));
EXPECT_EQ_DOUBLE(i + 1.0, s_get_number(value1));
}
问题14 free
一般在free(k)后记得,k = null;
问题15 递归问题。
我觉得我要仔细的分析一下这个问题。
花了点时间画了个图,箭头表示叶子节点,没有箭头的表示中间节点,虚线表示这可能是个数组。
这里面最复杂的是,用来存放数据的array和member,这也是我使我写程序时最顾忌的一点,怕写错。这里我要打消我的顾忌。首先我在传数据的时候,传的struct s_value, 如果我要向它写入数据,我都会备注这是什么数据类型:S_DATATYPE type。可根据这个我就可以判断里面的数据是叶子节点还是中间节点,如果是中间节点的话,我可以根据类型来判断是否需要使用递归,并根据他们的size来进行遍历。
问题16,%zu
%zu输出size_t型 ,第一次见,记录一下。
#define EXPECT_EQ_SIZE_T(expect, actual) EXPECT_EQ_BASE((expect == actual), (size_t)expect, (size_t)actual, "%zu")
问题17:对于一个树形结构我该如何free(之前malloc的)
参考s_value *value。
void s_parse_value_free(s_value *value)
{
assert(value != NULL);
size_t i;
switch (value->type)
{
case S_STRING:
free(value->data.string.s);
break;
case S_ARRAY:
// size_t i; error ,can not decalaration i after case ,only if use {size_t i;....}
for (i = 0; i < value->data.array.len; i++)
{
s_parse_value_free(&value->data.array.element[i]);
}
free(value->data.array.element);
break;
case S_OBJIECT:
for (i = 0; i < value->data.member.size; i++)
{
free(value->data.member.m[i].key);
s_parse_value_free(&value->data.member.m[i].v);
}
// if (value->data.member.m->key != NULL) {
// free(value->data.member.m->key);
// }
free(value->data.member.m);
break;
default:
break;
}
value->type = S_NULL;
}
再次感谢作者,提高了我的编程的能力。我gitee中有作者的仓库链接。