• C陷阱与缺陷学习笔记


    导读

    程序是由符号(token)序列所组成的,将程序分解成符号的过程,成为“词法分析”。

    符号构成更大的单元--语句和声明,语法细节最终决定了语义。

    词法陷阱

    符号(token)指的是程序的一个基本组成单元,其作用相当于一个句子中的单词。
    编译器中负责将程序分解为一个一个符号的部分,称作“词法分析器”。
    在C语言中,符号之间的空白(/b /t /n...)将被忽略。
    #include <stdio.h>
    int main()
    {
    	if
    	(
    	1
    	)
    	printf
    	(
    		"Hello World
    "
    	);
        return
        0;
    }
    C语言中char类型都是当做int类型来处理的
    如果一个整型常量的第一个字符是数字0,那么该常量将被视为八进制数。
    因此10与010的含义截然不同。
    用双引号引起来的字符串,代表的是一个指向无名数组起始字符的指针,
    该数组被双引号之间的字符以及一个额外的二进制为零的字符''初始化。
    #include <stdio.h>
    
    int main()
    {
    	printf("%c
    ", "hello"[0]);
    	printf("%c
    ", "hello"[1]);
    	printf("%c
    ", "hello"[2]);
    	printf("%c
    ", "hello"[3]);
    	printf("%c
    ", "hello"[4]);
    }
    输出
    h
    e
    l
    l
    o
    printf("%s ", "hello");
    char str[] = {'h', 'e', 'l', 'l', 'o', ''};
    printf("%s ", str);
    二者是等价的。

    语法陷阱

    函数调用:
    void fun()
    {
    printf("Hello World");
    }
    在C语言中,函数调用即使不带参数,也应该包括参数列表。
    调用方式:fun();(这仅仅是一种简写的形式)
    而实际上真正的调用语法为:(*fun)();
    理解函数声明:(使用添加括号的方式)




    语义陷阱

    单纯意义上的数组只有两种操作:
    1.确定数组的大小sizeof(arr) / sizeof(arr[0])
    2.获得指向该数组下标为0的元素的指针。
    其他操作都是通过指针进行的。
    关于数组名取地址:
    在C中, 在几乎所有使用数组的表达式中,数组名的值是个指针常量,也就是数组第一个元素的地址。 它的类型取决于数组元素的类型: 如果它们是int类型,那么数组名的类型就是“指向int的常量指针“。
    在以下两种场合下,数组名并不是用指针常量来表示,就是当数组名作为sizeof操作符和单目操作符&的操作数时。 sizeof返回整个数组的长度,而不是指向数组的指针的长度。 取一个数组名的地址所产生的是一个指向数组的指针,而不是一个指向某个指针常量的指针。所以&a后返回的指针便是指向数组的指针,跟a在指针的类型上是有区别的。
    #include <stdio.h>
    int main()
    {
    	int arr[] = {1, 2, 3, 4, 5};
    	int *p = arr;
    	printf("%d
    ", *p);
    	p++;
    	printf("%d
    ", *p);
    
    
    	int (*ptr)[5];
    	ptr = &arr;//&arr的类型为 int(*)[5]
    	printf("%d
    ", **ptr);
    	p = *ptr;
    	p++;
    	printf("%d
    ", *p);
    	return 0;
    }
    ptr/&arr类型为:int(*)[5];
    *ptr/p类型为:int*;
    arr类型为:int* const p;//指针常量
    #include <stdio.h>
    
     int main()
    {
    	int a[5] = {1, 2, 3, 4, 5};
    	int *ptr = (int*)(&a + 1);
    	int array[2][3] = {{1, 2, 3}, {4, 5, 6}};
    	printf("%d %d %d
    ", a, &a, ptr);
    	printf("%d %d
    ", *(a + 1), *(ptr - 1));
    	printf("%d %d %d %d
    ", array, array[0], &array[0][0], &array);
    	printf("%d %d %d %d
    ", array + 1, array[0] + 1, &array[0][0] + 1, &array + 1);
    }
    从以上输出我们可以看出:数组名和数组名取地址在数值上是相同的,均表示数组第一个元素的地址。但是二者的颗粒度不同。
    当数组是一维数组时,数组名是以一个数组元素为颗粒度,表现为“当数组名加1时,这里的1表示一个数组元素单元”,例子中的数组元素为整数,所以数组名加1时地址加4;而数组名取地址&以整个数组为颗粒度,表现为“当数组名取地址&加1时,这里的1是表示整个数组单元”,例子中的数组为有5个元素的整型数组,所以数组名取地址&加1时,地址加20.
    当数组是二维数组时,数组名array、array[0]、&array[0][0]以及数组名取地址&在数值上是相同的,同样各个之间的颗粒度不同。其中array[0]以及 &array[0][0] 的颗粒度相同,均是以一个数组元素为颗粒度,所以它们加1后,地址加4;而数组名和数组名取地址&颗粒度不同,前者以一行元素为颗粒度,后者以整个数组单元为颗粒度,所以前者加1,地址加3*4,后者加1,地址加6*4.

    二维数组:以数组作为元素的数组。
    计算机中只有一维数组,二维数组只是虚拟出来的概念。
    int arr[4][5];
    arr的类型为:int(*)[5]
    int (*ptr)[5];
    ptr = arr;//arr->指向数组的指针。

    #define NULL    ((void *)0)
    #include <stdio.h>
    int main()
    {
    	char *p = NULL;
    	if (p == 0)
    	{
    		puts("YES");
    	}
    	return 0;
    }
    输出:YES
    原书:
    char *p = "Hello";
    p[0] = 'h';
    这样的操作是是错误的,因为p的类型为const char *,即指向常量的指针。
    ANSI标准禁止对string literal作出修改。
    数组作为参数,数组名立刻被转换成指向该数组第一个元素的指针,因为C语言中我们没有办法将一个数组
    作为函数参数直接传递。
    所以:
    char *str = "12345";
    printf("%s ", str);完全等价于:printf("%s ", &str[0]);

    在C语言中,string literal代表了一块包括字符串中所有字符以及一个空字符('')的内存区域的地址
    C99允许变长数组,所以一下操作也是正确的(C89则错误):
    #include <stdio.h>
    #include <string.h>
    int main()
    {
    	char *p1 = "12345";
    	char *p2 = "67";
    	char arr[strlen(p1) + strlen(p2)];
    	//memset(arr, 0, sizeof(arr));
    	strcpy(arr, p1);
    	//strcat(arr, p1);
    	strcat(arr, p2);
    	puts(arr);
    	return 0;
    }

    c语言中存在两类整数算术运算,有符号运算和无符号运算。在无符号运算里,没有了符号位,所以是没有溢出的概念的。
    所有的无符号运算都是以2的n次方为模。如果算术运算符的一个操作数是有符号书,另一个是无符号数,那么有符号数
    会被转换为无符号数(表示范围小的总是被转换为表示范围大的),那么溢出也不会发生。但是,当两个操作数都是有符号数
    时,溢出就有可能发生。而且溢出的结果是未定义的。当一个运算的结果发生溢出时,任何假设都是不安全的。
    例如,假定a和b是两个非负的整型变量(有符号),我们需要检查a+b是否溢出,一种想当然的方式是:
    if (a + b < 0)
          溢出;
    实际上,在现实世界里,这并不能正常运行。当a+b确实发生溢出时,所有关于结果如何的假设均不可靠。比如,在某些
    机器的cpu,加法运算将设置一个内部寄存器为四种状态:正,负,零和溢出。在这种机器上,c编译器完全有理由实现以上
    的例子,使得a+b返回的不是负,而是这个内存寄存器的溢出状态。显然,if的判断会失败。
    一种正确的方式是将a和b都强制转换为无符号整数:
    if ( (unsigned)a + (unsigned)b  > INT_MAX)
          溢出;
    这里的int_max值为有符号整型的最大值。在一般的编译器里是一个预定义的常量。ANSI C在limits里定义了INT_MAX,值为
    2的31次方-1. 
    不需要用到无符号算数运算的另一种可行方法是:
    if (a > INT_MAX - b )
         溢出;
    PS : 有符号数的最高位(31位)为符号位,最高位为0的时候,表示正,为1的时候表示负。运算时,符号位不参加运算,但是如果两个数相加,30位需要进1时,那么即表示溢出。

    如何检测整型相加溢出(overflow)


    前言:


    本文主要讨论如何判断整型相加溢出(overflow)的问题. 我们知道计算机里面整型一般是有限个字节(4 bytes for int)表示, 正是因为只能用有限个字节表示一个整型变量, 由此带来一个可能的问题: 溢出(overflow). 所谓整型溢出(overflow), 是说一个整数的值太大或者太小导致没有用给定的有限个(比如四个字节没法存超过2^31 – 1的有符号正整数)字节存储表示. 这个整型溢出(overflow)问题一般的时候不会注意到也并不危险, 但是在做整型加法或者乘法的时候就有可能出现并且给程序带来未定义的行为. 这里我们主要讨论如何判断整型相加溢出(overflow)的两种方法以及各自优缺点.


    整型相加溢出(overflow)的原因:


    前言里面也已经提到了, 计算机中的的整数是用有限个字节表示的, 假设用k个字节表示一个整型变量, 那么这个变量可以表示的有符号整数的范围是-2^(8k-1) ~ 2^(8k-1) – 1  ,  那么两个正整数或者两个负整数相加就有可能超过这个整型变量所能表示的范围, 向上超出>2^(8k-1) – 1我们称之为向上溢出, 向下超出<-2^(8k-1), 我们称之为向下溢出. 注意这里两个整数符号相同是整型相加溢出(overflow)的必要条件, 也就是说只有符号相同的两个整数相加才有可能产生溢出(overflow)问题. 这个可以这么理解: 你想要是两个符号不同的两个整数, 他们相加, 那么这个和的值的绝对值一定是比单个相加数和被相加数都小, 既然相加数和被相加数都能用现有整型变量表示, 那么两个不同整数的相加结果怎么样都可以用现有的整型范围的变量存储下来而不溢出(overflow). 所以结论: 只有符号相同的整数相加才有可能才生溢出(overflow).


    整型相加溢出(overflow)的检测:


    那么接下来的问题就是如何检测到溢出(overflow)的产生的, 更具体的, 给定两个整型, 比如int a, int b, 我们做加法a+b, 如何去判断这个相加的结果是正确结果还是说是溢出(overflow)的结果. 下面我们给出两种方法(后面我们会讨论方法二是更好的方法), 并且做出解释或者说不太严格的证明方法的正确性, 也就是为什么这么做就能保证溢出(overflow)的检测的正确性.


    方法一, 计算相加的结果, 判断结果的符号, 两个正整数相加结果为负数, 或者两个负整数相加结果为正数, 那么就是溢出(overflow)了. 实现代码如下:


    int addInt(int a, int b) {
        int res = a + b;
        if(a > 0 && b > 0 && res < 0) throw overflow_exception;
        if(a < 0 && b < 0 && res > 0) throw overflow_exception;
        return res;
    }
    这个方法的原理是这样, 计算机里面有符号整数是利用补码的形式表示的, 第一位是符号位, 0表示整数, 1表示负数. 我们拿一个字节的整型来举例, 一个字节的有符号数可以表示的范围就是-128 ~ 127, 那么两个一个字节的正整数相加的最大范围就是254, 那么其中128 ~ 254就是溢出(overflow)的值, 是不能用一个字节存储下的值, 这个值用一个字节表示的时候最高位是1, 在有符号整数系统里面这个值其实被当成了负数. 同理, 负数相加的时候最小可以到达-256, 根据补码的表示对应的正整数取反加1就是对应的补码, 那么对应的正整数的最高位是1, 现在取反以后就变成0, 也就是说两个比较大的负数相加的结果其实变成了正数. 这就是上述方法的理论基础.


    方法二, 使用减法, 利用现有整型的最大或者最小极值减去某个加数(减法相当于变号, 从而保证没有溢出(overflow)的发生), 和另一个加数比较大小进行判断. 实现代码如下:


    int addInt(int a, int b) {
        if(a > 0 && b > 0 && a > INT_MAX - b) throw overflow_exception;
        if(a < 0 && b < 0 && a < INT_MIN - b) throw overflow_exception;
        return a + b;
    }
    这个方法其实不用太多的解释, 简单的数学知识就能解释其中的原理, 由于减法保证了不会溢出(overflow), 又前面我们保证了两个数都是正整数, 所以形如 a > INT_MAX – b的判断是安全并且总是正确的. 而且这个检测方法的正确性可以通过移位就看得懂了, 不像方法一, 需要一定的计算机底层的知识才能解释说通.


    方法一和方法二比较: 我自己一开始的时候思考利用方法一这样的结论去判断溢出(overflow), 但是我心里其实不放心, 因为方法一的前提是”两个正整数相加溢出的充要条件是符号位变成1, 也就是结果变成了负数”, 这样的结论或者事实对于我或者一般人来讲其实并不是那么的直观或者理所当然, 当然了我自己又用那个一个byte的例子试着去解释, 结论还是正确的,  所以方法一相比较方法二而言并不直观. 另一方面有说法说是溢出的时候结果其实不确定, 上面在方法一里面我们的分析只是理论上的分析, 编译器有可能做出相关的优化或者对溢出结果做出调整, 那么可能就出现未定义的行为了, 所以综上所述, 方法二应该是比较更为安全和合理并且更为直观的首选检测整数相加溢出(overflow)的方法.


    更新: 感谢网友Stanley的留言, 提供了第三种方法的检测, 其实也就是方法一的bit operation版本, 通过位操作, 我们可以判断求和结果x是否与a和b还同号, 如果同时不同号(也就是sign bit不相同了), 那我们就相当于检测到了溢出. Stanley的版本稍微反了反, 我认为应该是下面这种情况才是溢出, 如有错误, 敬请指正. Thanks!


    x = a + b;
    if ((x^a) < 0 && (x^b) < 0)
    {
        //overflow, do something
    }
    结束语:


    本文主要讨论了如何判断整型相加溢出(overflow)的问题, 主要总结了整型相加溢出(overflow)的原因, 并给出了两种检测整型相加溢出(overflow)的方法, 方法一基于计算结果的正负, 方法二基于把加法转化为减法. 本文同时给出了两种方法的比较, 并且指出方法二应该是首选的检测方法.
    //有符号整形a和b,如何判断a+b是否溢出
    #include <stdio.h>
    
    int ifo_add(int a,int b) 
    {
        __asm {
            mov eax,a
            add eax,b
            jo  overflowed
            xor eax,eax
            jmp no_overflowed
    overflowed:
            mov eax,1
    no_overflowed:
        }
    }
    int main()
    {
        int a, b;
    
        a=          1;b= 2;printf("%11d+(%2d) %d
    ",a,b,ifo_add(a,b));
        a=         -1;b=-2;printf("%11d+(%2d) %d
    ",a,b,ifo_add(a,b));
        a= 2147483647;b= 1;printf("%11d+(%2d) %d
    ",a,b,ifo_add(a,b));
        a=-2147483647;b=-1;printf("%11d+(%2d) %d
    ",a,b,ifo_add(a,b));
        a=-2147483647;b=-2;printf("%11d+(%2d) %d
    ",a,b,ifo_add(a,b));
    }
    //          1+( 2) 0
    //         -1+(-2) 0
    // 2147483647+( 1) 1
    //-2147483647+(-1) 0
    //-2147483647+(-2) 1



    main函数的返回值可以在cmd下echo %errorlevel%得到



    连接

    一个典型的连接(链接)实例:
    add.h
    #ifndef ADD_H
    #define ADD_H
    int add(int, int);
    #endif
    add.c
    #include "add.h"
    #include <stdio.h>
    
    int add(int x, int y)
    {
    	return (x + y);
    }
    test.c
    #include "add.h"
    
    int main(void)
    {
    	printf("%d
    ", add(10, 20));
    	return 0;
    }
    extern int x;//是声明,不是定义
    这就显式的说明了x的存储空间是在程序的其他地方分配的。

    所谓的函数链接错误:1、找到个一个函数的两个实现。
    2、一个实现也没有找到。

    库函数

    FILE *fp;
    fp = fopen("test.txt", "r+");
    为了保持与过去不能同时进行读写操作的程序的向下兼容性,一个输入操作不能随后直接紧跟着一个输出操作,
    反之亦然。

    根本原因就是文件指针的移动问题,使用fseek可以移动文件指针,实现读写同时进行的操作。


    预处理器

    宏:只是对程序的文本起作用,即简单的替换。
    #ifndef max
    #define max(a,b)            (((a) > (b)) ? (a) : (b))
    #endif
    
    #ifndef min
    #define min(a,b)            (((a) < (b)) ? (a) : (b))
    #endif
    优点:普通函数的调用涉及空间的申请,参数入栈、出栈,这样必然带来系统开销。而宏没有函数调用的开销
    缺点:由于只是简单的文本替换,没有类型检查,很容易形成看似正确实则错误的代码。
    c++的inline函数的涉及必然也是想保留宏的优点而摈弃宏的缺点。
    1、不能忽视宏定义中的空格。
    2、宏不是函数。
    3、使用时最好把每个参数都用括号括起来。
    4、宏不是类型定义,定义新类型最好使用typedef。
    assert宏的正确定义:
    #define assert(e) 
    		((void)((e) || _assert_error(__FILE__, __LINE__)))
    __FILE__, __LINE__是内建于C语言预处理器中的宏,它们会被扩展为所在文件的文件名和所处代码行的行号。

    Keep it simple!
    作者:N3verL4nd
    知识共享,欢迎转载。
  • 相关阅读:
    十一周
    第五次作业
    第十周
    测试作业
    2016.06.24-2016.06.30这周工作时间和内容
    2016.06.10-2016.06.16这周工作时间和内容
    2016.06.03-2016.06.09这周工作时间和内容
    2016.05.26-2016.06.02这周工作时间和内容
    2016.05.20-2016.05.26这周工作时间和内容
    分答
  • 原文地址:https://www.cnblogs.com/lgh1992314/p/6616388.html
Copyright © 2020-2023  润新知