Weijian 的个人资料Enjoy My Life照片日志列表 工具 帮助

日志


10月27日

C语言获得整数类型和浮点类型的大小限制zz

这篇文章对C语言中的基本数据类型的各种config进行了说明,很基础。

/*c头文件limits.h和float.h分别提供有整数类型和浮点类型的大小限制的详细说明.*/

/*可以用文本编辑器来打开和查看系统的头文件.*/

#include <conio.h>
#include <sio.h>
#include <limits.h>
#include <float.h>

int main(void)
{
     /* char相关 */
     printf("char的位数:%u\n",CHAR_BIT);
     printf("char类型的最大值:%d\n",CHAR_MAX);
     printf("char类型的最小值:%d\n",CHAR_MIN);
     printf("signed char类型的最大值:%d\n",SCHAR_MAX);
     printf("signed char类型的最小值:%d\n",SCHAR_MIN);
     printf("unsigned char类型的最大值:%u\n",UCHAR_MAX);
     /* short相关 */
     printf("short类型的最大值:%hd\n",SHRT_MAX);
     printf("short类型的最小值:%hd\n",SHRT_MIN);
     printf("unsigned short类型的最大值:%u\n",USHRT_MAX);
     /* int相关 */
     printf("int类型的最大值:%d\n",INT_MAX);
     printf("int类型的最小值:%d\n",INT_MIN);
     printf("unsigned int类型的最大值:%u\n",UINT_MAX);
     /* long 相关 */
     printf("long类型的最大值:%ld\n",LONG_MAX);
     printf("long类型的最小值:%ld\n",LONG_MIN);
     printf("unsigned long类型的最小值:%lu\n",ULONG_MAX);
     /* float相关 */
     printf("float类型的尾数位数:%u\n",FLT_MANT_DIG);
     printf("float类型的最小有效数字位数:%u\n",FLT_DIG);
     printf("带有全部有效数字位数的float类型的负指数的最小值:%d\n",FLT_MAX_10_EXP);
     printf("带有全部有效数字位数的float类型的正指数的最大值:%d\n",FLT_MIN_10_EXP);
     printf("保留全部精度的float类型正数的最小值:%e\n",FLT_MIN);
     printf("保留全部精度的float类型正数的最大值:%e\n",FLT_MAX);
     printf("1.00和比1.00大的最小的float类型值之间的差值:%e\n",FLT_EPSILON);
     /* double相关 */
     printf("double类型的尾数位数:%u\n",DBL_MANT_DIG);
     printf("double类型的最小有效数字位数:%u\n",DBL_DIG);
     printf("带有全部有效数字位数的double类型的负指数的最小值:%u\n",DBL_MAX_10_EXP);
     printf("带有全部有效数字位数的double类型的正指数的最大值:%d\n",DBL_MIN_10_EXP);
     printf("保留全部精度的double类型正数的最小值:%e\n",DBL_MIN);
     printf("保留全部精度的double类型正数的最小值:%e\n",DBL_MAX);
     printf("1.00和比1.00大的最小的double类型值之间的差值:%e\n",DBL_EPSILON);
     /* long double相关 */
     printf("long double类型的尾数位数:%d\n",LDBL_MANT_DIG);
     printf("long double类型的最小有效数字位数:%d\n",LDBL_DIG);
     printf("带有全部有效数字位数的long double类型的负指数的最大值:%d\n",
               LDBL_MAX_10_EXP);
     printf("带有全部有效数字位数的long double类型的正指数的最小值:%d\n",
               LDBL_MIN_10_EXP);
     printf("保留全部精度的long double类型正数的最小值:%le\n",LDBL_MIN);
     printf("保留全部精度的long double类型正数的最大值:%le\n",LDBL_MAX);
     printf("1.00和比1.00大的最小的long double类型值之间的差值:%le\n",LDBL_EPSILON);

     return 0;
}

10月14日

在Lex&Yacc中使用内存缓冲区作为输入源

最近在使用Lex&Yacc解析SQL语句,需要解决得两个重要问题是:1) SQL语句保存在内存缓冲区中;2) 要解析的SQL语句量非常大(90w/day)。而Lex在默认情况下以外部文件作为输入源,因此需要利用一些Lex的额外功能才行解决上面的两个问题。经过几天的探索,有两种解决方法,总结如下:
 

1.  使用Lex提供的API(yy_scan_string, yy_delete_string)

    此时解析的主函数结构为:
        int  sql_exec(const char *str)
        {
            // load the input string to Lex buffer
            YY_BUFFER_STATE yy_buf = yy_scan_string(str);
            // parse the sql statement
            int ret = yyparse();
            // delete current Lex buffer
            yy_delete_buffer(yy_buf);
            // execute the sql
            if (!ret)
            {
                // execute the sql 
                ......
            }
            else    
            {
                // deal with the parse error
                ......
            }
        
            return 0;
        }
    其中用到了几个Lex提供的函数及结构体:
    YY_BUFFER_STATE: Lex中的buffer状态结构体,可以用来指定一个待解析的buffer;
    yy_scan_string(const char*): 创建一个新的YY_BUFFER_STATE,并把参数中的字符串拷贝到该buffer中,与之具有相似功能的函数还有yy_scan_bytes(const char*, int),它以一个指定长度的缓冲区作为输入,而不是字符串。这两个函数都需要调用malloc以创建一个新的缓冲区;
    yy_delete_buffer(YY_BUFFER_STATE): 删除当前的输入缓冲区,这个函数在底层调用free以删除一个缓冲区。
    • 优点:实现简单,代码比较直观;缓冲区根据要解析的目标自动生成,不会浪费空间。
    • 缺点:需要调用Lex的API,因此这段代码最好放在.l文件中,因此造成代码结构比较乱;每次解析都要malloc->free,容易造成碎片。

2.  重新定义Lex的YY_INPUT

    YY_INPUT是Lex的读取待解析内容到缓冲区的函数,默认情况下从文件中读取字符,即getc(yyin)。如果我们把它重新定义为从内存缓冲区中读取字符,就可以使用我们的目标。
    首先,在.l文件中声明对YY_INPUT的重新定义:
        #undef YY_INPUT
        #define YY_INPUT(b, r, ms) { r = my_yyinput(b, ms); }
    之后,在主程序文件中使用全局变量的方式声明接收SQL语句的缓冲区:
        char g_sqlstr[SQL_LEN];  // SQL语句缓冲区
        char *g_sql_cur;              // 当前读取指针
        char *g_sql_end;             // 缓冲区结尾
        由于Lex中的YY_INPUT的使用并不灵活,因此在主程序文件中必须使用全局变量来暂存要解析的内容,SQL_LEN是要解析的SQL语句可能的最大长度。
    再后,定义my_yyinput:
        int my_yyinput(char *buf, int max_size)
        {
            long n = g_sql_end - g_sql_cur;
            if (n > max_size)           
            n = max_size;           
  
            if (n > 0)                  
            {
                memcpy(buf, g_sql_cur, n);
                g_sql_cur += n;         
            }
          
            return n;                   
        }
        Lex调用YY_INPUT的方式比较诡异,即使Lex和主程序文件中的buffer相比于要解析的SQL语句足够长,在一次解析中也不是只被调用一次。如果Lex内部的buffer长度(定义在#define YY_READ_BUF_SIZE 8192中)小于SQL语句的长度,YY_INPUT肯定会被多次调用。因此,在实现my_yyinput中,一定要记录当前buffer的读取位置,这样使得在一次解析中无论它被调用几次,读入的内容都不会出错。之前我使用了简单的memset(buf, g_sqlstr, SQL_LEN)结果总是出错,就是这个原因。
    最后,解析的主函数结构为:
        int sql_exec(const char *str)
        {
            // initialize the global buffer and positions
            memcpy(g_sqlstr, str, SQL_LEN);
            g_sql_cur = g_sqlstr;
            g_sql_end = g_sqlstr + strlen(g_sqlstr);
            // parse the sql statement
            int ret = yyparse();
            // flush current Lex buffer
            yy_flush_buffer(yy_buf);
            // execute the sql
            if (!ret)
            {
                // execute the sql 
                ......
            }
            else    
            {
                // deal with the parse error
                ......
            }
        
            return 0;
        }
    其中用到了Lex提供的一个函数:
    yy_flush_buffer(YY_BUFFER_STATE): 清空Lex的当前buffer,内部的主要操作是b->yy_n_chars = 0,并清空状态信息等。通过调用这个函数可以使用下次解析SQL语句时,从buffer头部开始。  
    •  优点:Lex和主程序文件中的缓冲区都是事先开辟的,在Lex中if ( ! yy_current_buffer ) yy_current_buffer = yy_create_buffer( yyin, YY_BUF_SIZE );,因此不用来回申请和释放,不容易造成碎片。
    • 缺点:容易造成空间浪费,在运行前必须申请一个SQL语句可能最大长度的buffer;代码不直观,因此my_yyinput是在yylex中自动调用的。

    这种方法需要强调的是在Lex中有两个和buffer长度有关的宏:YY_BUF_SIZE和YY_READ_BUF_SIZE,在默认情况下,YY_BUF_SIZE = 16384,YY_READ_BUF_SIZE = 8192。其中,YY_BUF_SIZE是在yy_create_buffer中使用,它是Lex一次申请的缓冲区长度;YY_READ_BUF_SIZE是在YY_INPUT中使用,它是Lex读一次缓冲区读取的最大程度,也就是my_yyinput中的max_len。当这两个长度小于一条SQL语句的长度时,我做了如下的实验:

实验一:#define SQL_LEN 1024, #define YY_BUF_SIZE 10, #define YY_READ_BUF_SIZE 16,解析结果是:
[update tb set col1 = 123|24]
!9!7!7!1!0  execute the sql ...
[delete from tb where col1 in (1, 2, 3) or col2 <= .1 - 1|56]
!9!7!8!6!9!9!8!0  execute the sql ...
[insert into tb values ('abc', 'de') (1)|39]
!9!7!8!8!7!0  parse error !
[update tb set col1 = 1 where (col1 = 1 or col2 = 2) and col3 = 'a'|66]
!16!16!16!16!2!0  execute the sql ...
[replace into tb values (1, 2, 3)|32]
!16!16!0  execute the sql ...

实验二::#define SQL_LEN 1024, #define YY_BUF_SIZE SQL_LEN+2, #define YY_READ_BUF_SIZE 16,解析结果是:
[update tb set col1 = 123|24]
!16!8!0  execute the sql ...
[delete from tb where col1 in (1, 2, 3) or col2 <= .1 - 1|56]
!16!16!16!8!0  execute the sql ...
[insert into tb values ('abc', 'de') (1)|39]
!16!16!7!0  parse error !
[update tb set col1 = 1 where (col1 = 1 or col2 = 2) and col3 = 'a'|66]
!16!16!16!16!2!0  execute the sql ...
[replace into tb values (1, 2, 3)|32]
!16!16!0  execute the sql ...

解析结果格式说明:
[sql_statement|sql_length]
!each_read_len!each_read_len...parse_result

这两个实验说明当YY_BUF_SIZE和YY_READ_BUF_SIZE 小于SQL_LEN时,Lex并没有报错(缓冲区没有溢出)。不过这个结论在代码中暂时还没有找到证据。

 

10月10日

一篇很有趣的关于野指针的文章(zz)

      在c中,野指针也许性子野,但是控制起来也是有章可循。然而事情在c++中出现了变化。
 
    什么是野指针?

  一个母亲有两个小孩(两个指针),一个在厨房,一个在卧室,(属于不同的代码块,其生存期不同)母亲让在厨房的小孩带一块蛋糕(指针指向的对象)给在卧室的小孩,这样在卧室的孩子才肯写作业。但这个在厨房的小孩比较淘气,他在走出厨房时自己将蛋糕吃了,没能带出来。而在卧室的没有吃到蛋糕,所以不肯完成他的作业。结果母亲却不知道卧室的孩子没有吃到蛋糕,还以为作业完了。结果第二天她就被老师召唤到办公室了。事情麻烦了。

  这样,那个在卧室的孩子就是野指针了,因为他没有得到应得的蛋糕,不能完成母亲交给他的作业。

  这就是c中所讲的野指针。上面的小剧本不过演示了一种最基本的野指针的形成过程。更容易出现的情形是coder在编码时,大意之下使用了已经free过的指针。

  对于年轻点的经验欠缺的coder来说是比较容易犯的错误,经验老到的程序员或者慎重采取成对编程的形式避免这种失误,或者使用引用计数器防止形成野指针。

  总之,在c中,野指针也许性子野,但是控制起来也是有章可循。然而事情在c++中出现了变化。

  coder们面临更大的麻烦了。c++程序员无可避免的要写很多这样那样的类。谁让c++是面向对象的呢?

  我们在写类的时候难免要用new给类的数据成员分配内存。这本来没什么,动态分配内存是一种很常见的基本操作,我们在学数据结构时经常这么做,不是么? 

  但是伙计,事情并非这么简单。类是一种高级的用户自定义数据类型,看起来和结构、枚举这样的用户自定义类型没啥太大差别。如果你这样认为....?那你会死的很惨。类太复杂了,普通情况下使用类的对象并没有太大的问题,但是,当你要复制一个对象时,问题就来了。


  比如我们知道,你要用一个对象初始化另一个对象时,c++是按位进行拷贝的,即在目标对象里创建了初始化对象的一个完全相同的拷贝。这在多数情况下已经足够了。但是,当你的类在创建时为每个对象分配内存,也就是说类中有new操作。当你的对象创建好后,类也为对象分配了一块内存。如果你用这个对象去初始化另一个对象时,被初始化的对象和初始化的对象完全一样。这意味着,他们使用同一块内存,而不是重新为被初始化的对象分配内存。

  这样麻烦就大了。如果一个对象销毁了,那么分配的内存也就销毁了(别忘了,类是有析构函数的,它负责在对象销毁时,释放动态分配的内存。难道你说你不在类中写上析构部分?那么可怜的孩子,那你就走向了另一个深渊,当你的程序运行数小时之后,系统会告诉你,内存不够用了。想象一下把你的程序用在腾讯的服务器上),另一个对象就残缺不全了,这就像一对连体婴儿,他们共用了一部分器官,心脏或者肝脏。要救活一个,就牺牲了另一个。一个得病了,另一个也要遭殃。

  可以说,这就是c++中更加变态的野指针。

  什么?你说我不用对象初始化对象?那么我们会不会将一个对象作为变元传递给函数呢?我们很多时候都这样做。有时我们不得不将对象按值传递给一个函数,但是你要知道,按值传递是什么意思?它的意思就是,把实参的一个拷贝传递给函数。这和刚才的初始化没什么两样,按位拷贝,函数体内的对象与外面的对象共用一块内存,即便在函数中的对象没有对这块内存进行过操作,但是当函数结束时。。。。析构函数将会被调用......

  还有一种与之相反的情况...... 当你想要把一个在函数内的对象值返回给外面的对象时,这时候,会自动产生一个临时对象,由它容纳函数的返回值,并在函数结束时把结果传给目标。那么这个临时对象迅速的被创建,并被迅速的释放。。。一块内存被释放了两次。其后果是不可预见的。
 

  当你把一个对象的值赋给另一个对象时,如果你没有重载赋值运算符,那么也会导致按位拷贝。最终产生一个野指针(一个隐藏在类内的毒瘤),或者释放同一块内存多次。
 

  看到了么?害怕了么?是不是感到C++到处都是陷阱呢?不但有陷阱,到处都是危险品。所有c中的疑难问题,到了c++就成了一般问题了。好了不废话了,我们继续讲讲解决之道。


  对于最后的这种赋值的情况,我们只有通过重载赋值运算符才能解决,也就是避免按位拷贝。

  至于前面的都属于初始化,概括下来就是三种情况:

  1.当一个对象初始化另一个对象时,例如在声明中;

  2.把所创建的对象拷贝(按值)传递给一个函数时;

  3.生成临时对象时,最常见的就是函数的返回值。

  解决初始化时的按位拷贝问题,我们通过创建拷贝构造函数来解决。

  基本的拷贝构造函数形式为:

        classname (const classname &o)
        {
               //body here
        }

  拷贝构造函数就是针对这个问题而设计的。

  恩,大家都明白了吧?不要让你的对象都变成可怜的连体人啊~~~~