c语言进阶--实现一个简易的json库

本文作者分享了学习C语言过程中遇到的问题和解决方案,包括静态函数的作用,内存泄漏的检测,内联函数与宏的优缺点,以及左移和右移操作的深入理解。此外,还探讨了数据结构的选择和assert的使用,以及浮点数转换为整数时的注意事项。
摘要由CSDN通过智能技术生成

目录

前言

一、 不足之处

二、学习过程中记录的问题

问题1,在函数前面声明static

问题2

问题3 内存泄漏

问题4

问题5

问题6 gcc优化选项

问题7,内联函数和宏

问题8 ,左移和右移的问题

问题9,数据结构和算法

问题10 ,assert

问题11,0x20,以及\u

问题12 ,gdb调试

问题13,浮点数转int

问题14 free

问题15 递归问题。

问题16,%zu

问题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中有作者的仓库链接。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值