标签 指针 下的文章

《Understanding and Using C Pointers》要点先睹为快

如果你问十个C程序员:你觉得C语言的核心是什么?这十个程序员都会回答:指针。

指针具备成为C语言核心的两个关键要素:强大争议

* 指针的强大源自于其天生与机器内存模型的适配。使用指针让代码紧凑,并可获得仅次于汇编代码的执行效率;使用指针可以让C程 序员毫不费力地尽情操纵着内存中的每个byte甚至是bit;使用指针可以为C程序员提供无与伦比的操作灵活性。总之,在C语言中指针几乎是无所 不能的代名词。得指针者得天下,没有指针,C语言将变得平庸。

* 成也指针,败也指针。指针的争议之处就在于其在赋予C程序员无比强大的Power的同时,也常常带来无穷的烦恼甚至灾祸,比如 内存问题、调试困难或因指针导致的程序崩溃等。就好比人类社会,做核心人物有争议是难免的,比如足球界有马拉多纳,跳水界有菲尔普斯,斯诺克界有 奥沙利文^_^。

好了,言归正传,我们回到C语言图书上来。目前市面上的C语言书籍,无论国内国外,无论经典还是山寨,基本都是百科大全型,将C语言讲的面面俱 到。比如最近的一本大而全的经典应当属《C Programming , A Modern Approach》,中文版书名为《C语言程序设计:现代方法》第2版。以至于发展到今天,C语言似乎也没啥可讲的了,新出的C语言书大多是与前辈们雷同 的作品。近两年来也有O'reilly出版的C语言书籍,比如:

*《Head First C
*《21st Century C – C Tips from the New School

前者是典型的Head First风格的C教程,后者则是另辟蹊径,结合C语言外延(构建、调试、打包、版本控制、面向对象与C、知名C语言开源库等)进行讲解。这两本书虽形式 有变化,但终究脱离不开百科大全型,针对C的核心-指针并未有较多的深入探讨。而市场上专门写指针的书也稀少的很(似乎鬼子国那边有一本,叫什么 《征服C指针》),唯一的一本书名与指针扯上关系的书《Pointers on C》(中文名“C和指针”)其实依旧是一本C语言大全。于是乎国外著名出版社O'Reilly今年5月出品了一本专门讲解C语言核心 – 指 针的书《Understanding and Using C Pointers》,以满足C程序员深入理解C语言核心并实现进阶的诉求。O'Reilly就是O'Reilly,总是能抓住C语言书籍方面的深度阅读需 求^_^。

《Understanding and Using C Pointers》是个小册子,拢共才200多页,但内容却全部是围绕C语言指针展开的,从最基本的指针声明与操作、C内存模型、动态内存分配,讲到指针 与数组、结构体、字符串的关系,再到最后指针的高级特性:强制转换、Strict Aliasing、线程共享、多态支持等,由浅入深的进行细致的剖析。其作者认为作为C语言核心的指针值得花200页篇幅去讲解,而且期望所有读者在读完 此书后能对C指针有个扎实的理解。总之,这本书对系统C程序员理解C语言的核心-指针是大有裨益的。在其中文版(已经由图灵出版社引进版权了)尚 未出版之前,这里带你先了解以下本书的要点:

第一章 简介

1、指针与内存

   【指针声明语法】
    int *pi;

   【理解复杂指针声明】
    方法:从后向前读,例子:

   const int *pci;

   pci is a variable                                   pci
   pci is a pointer variable                           *pci
   pci is a pointer variable to an integer             int *pci
   pci is a pointer variable to a constant integer     const int *pci

    【地址操作符】
     pi = #

    【输出指针值】
    通过%x、%o、%p输出(printf)指针的值,一般使用%p(%p输出结果不一定等同于%x,是与实现有关的)。例子如下:
     int num = 0;
     int *pi = #
     printf("Address of num: %d Value: %d\n",&num, num);
     printf("Address of pi: %d Value: %d\n",&pi, pi);

     Address of num: 4520836 Value: 0
     Address of pi: 4520824 Value: 4520836

    【通过间接访问操作符解引用指针】
      间接访问操作符*,使用例子如下:
     int num = 5;
     int *pi = #
     printf("%d\n",*pi); // Displays 5
     *pi = 200;
     printf("%d\n",num); // Displays 200

     【指向函数的指针】
        void (*foo)();  // 这个变量声明中的foo就是一个指向函数的指针

     【Null概念】

         null concept
             赋值为NULL的指针变量表示该指针不指向任何内存地址。

         null pointer constant
             null concept的具体支撑实现,其常量值可能是常量值0,也可能不是。依具体实现而定。

         NULL macro
             在许多标准库实现中,NULL定义如下:#define NULL ((void *)0),这也是我们对NULL的通常理解。当然这是依Compiler的具体实现而定的。如果编译 器使用非全0位模式实现了NULL,那该编译器就要保证在指针上下文中使用的NULL或0是null pointer。

         ASCII NUL
             一个全0的字节。

         null string
             一个不包含任何字符的空字符串。C字符串在最后都放置一个结尾0值。

         null statement
             只包含一个分号的空语句。

         指向void的指针
             指向void的指针被成为通用指针,可以用于引用任意类型的数据。它有两个属性:
                    – 指向void的指针与指向char类型的指针具有相同的内存表示与内存对齐约束。
                    – void指针永远不等于其他类型指针,两个赋值为NULL的void pointer是相等的。

             任何指针都可以被赋给一个void pointer,并且之后还可以被转换回其原来的类型。
             int num;
             int *pi = #                   
             void* pv = pi;
             pi = (int*) pv;

            
             void pointer用于数据指针,而不是函数指针。
             全局void pointer或static void pointer在程序启动时被初始化为NULL。

2、指针大小与类型
        在多数现代平台上,指针的大小都是相同的,与其类型无关。指向char的指针与指向结构体的指针大小相同。
        指向函数的指针可能与指向数据类型的指针大小有差异,这要依具体实现而定。
     
     【内存模型】
             在不同机器和编译器下,C语言原生类型的大小是不同的。
             描述不同数据模型的一般记法:I In L Ln LL LLn P Pn,例如LP64、ILP64、LP32等。
 
     【预定义的指针相关类型】
            size_t 用于表示对象的大小的一个安全类型。
            ptrdiff_t 用于处理指针运算
            intptr_t和uintptr_t 用于存 储指针地址

       int num;
       intptr_t *pi = #

3、指针操作符

     【指针运算】
       pointer + integer
           指针实际移动的字节数 = integer + sizeof(integer_type)
           void* pointer的指针运算操作行为是未定义的,依赖Compiler的具体实现。

       pointer – integer
           指针实际移动的字节树 = integer – sizeof(integer_type)。

       pointer1 – pointer2
           两个指针所指地址间的差值,常用于判断数组中元素的先后次序。

       比较pointers

     【指针比较】
              指针可以使用标准的比较操作符(> and <)进行比较,可用来判断数组中元素的先后次序。

4、指针的通常用法
    
     【多级间接寻址】
              双指针(double pointer) – 指向指针的指针。

            char *titles[] = {"A Tale of Two Cities",
                        "Wuthering Heights","Don Quixote",
                        "Odyssey","Moby-Dick","Hamlet",
                        "Gulliver's Travels"};
      char **bestBooks[3];
      bestBooks[0] = &titles[0];
      bestBooks[1] = &titles[3];
      bestBooks[2] = &titles[5];

          
            间接寻址的级数并没有限制,但过多的级数会让人难以理解。
  
    【常量和指针】

            指向常量的指针
         const int limit = 500;
         const int *pci = &limit;

                  *pci = 600;/* Error, 我们不能解引用一个常量指针并修改其所指的内存值 */
                 
                 const int *pci <=> int const *pci;

            指向非常量的常量指针
         int num;
         int *const cpi = &num;

                  *cpi = 25; /* 可以解引用常量指针并修改其所指的内存的值 */
         int limit;
         cpi = &limit; /* Error,我们不能为常量指针重新赋新值 */

         const int limit1 = 300;
         int *const cpi1 = &limit1; /* Warning: 指向非常量的常量指针被用常量 的地址初始化了 */
 
      指向常量的常量指针    
         const int limit = 300;
         const int *const cpci = &limit;
/* 声明后,我们不能通过cpci修改limit,也不能为cpci重新赋值 */

            指向“指向常量的常量指针”的指针
         const int limit = 300;
         const int *const cpci = &limit;
         const int *const *pcpci = &cpci;

第二章 C语言动态内存管理

在运行时通过函数手工从heap分配和释放内存的过程称为动态内存管理。

1、动态内存分配
    【使用malloc函数】
      int *pi = (int*) malloc(sizeof(int));
      *pi = 5;
      free(pi);

    【内存泄漏】
            – 丢失了内存地址
            – 没有调用free函数释放内存

 2、动态分配内存函数
      malloc、realloc、calloc、free
      是否对malloc出的内存起始地址进行强制转型
             int *p = (int*)malloc(4);
             void *pointer可以转换为任意类型指针,没有强制转型也可以。
             但显式的强制转型可以通过代码看出意图,并且与C++编译器(包括早期C编译器)兼容
                       
      你不能用内存分配函数分配的内存去初始化全局或Static变量。
      alloca函数用于在栈上动态分配内存,函数结束时,这块内存自动释放;但alloca不是标准C库函数,移植性差。
      C99支持可变长度数组(VLA),数组声明时的元素个数可以是运行时才能确定值的变量,但数组size一旦在运行时被确定,数组大小就无法再做改变:
       void compute(int size) {
           char* buffer[size];
           …
       }
         

 3、悬挂指针
     被free后依然引用原先内存地址的指针,称为dangling pointer。
     悬挂指针可能导致如下问题:
            – 如果访问其引用的内存,将导致不可预期的结果
            – 如果内存不可访问了,将导致段错误
            – 存在潜在的安全风险。

     悬挂指针引起的问题调试起来十分困难,以下几种方法用于避免发生悬挂指针问题或快速查找悬挂指针问题:
            – free后,设置指针为NULL;
            – 编写一个替代free的函数;
            – 用特定值填充free的内存块,便于快速定位dangling pointer问题
            – 使用第三方工具检查dangling pointer问题

第三章 指针与函数

当与函数一起使用时,指针有两个方面发挥重要作用:
   – 当指针以参数形式传递给函数时,允许函数修改指针所指内存区域的值,并且这种传递方式更加高效;
   – 声明函数指针时,函数的名字被求值为函数的地址。
 
1、程序栈和堆

    【程序栈】
      栈和堆共享一块内存区域。栈在这块区域的低地址部分,堆在高地址部分。
      程序栈用于存放栈帧(stack frame),栈帧中存放的是函数的参数与local变量。
      栈增长方向:向上;堆的增长方向:向下。

    【栈帧的组成】
     一个栈帧包含如下几个元素:
           – 返回地址
           – 本地变量
           – 函数参数
           – 栈指针(Stack pointer)和栈帧指针(base pointer or frame pointer)

     Stack pointer和frame pointer用于运行时系统对栈的管理。前者总是指向栈的顶端;后者指向栈帧内的某个地址,比如函数的返回地址;frame pointer辅助程序访问栈帧内的元素。

     栈帧的创建,见下面例子:
        float average(int *arr, int size) {
            int sum;
            printf("arr: %p\n",&arr);
            printf("size: %p\n",&size);
            printf("sum: %p\n",&sum);

            for(int i=0; i<size; i++) {
                sum += arr[i];
            }
            return (sum * 1.0f) / size;
    }

      average的栈帧中沿着栈“向上”的方向,依次推入的是:
            – 参数 size、arr (与声明的顺序恰好相反)
            – 函数average调用的返回地址
            – 本地变量sum(如果有多个本地变量,推入栈的顺序也与变量声明顺序相反)

      每个线程通常都在自己的栈中创建栈帧。

2、指针作为参数和返回值

      C语言的参数是“按值传递”的,包括指针本身,函数内使用的是参数的copy。
      在处理大数据结构时,将指针作为参数传递给函数或作为返回值会使得程序执行起来更加高效(只是copy一个指针大小的数据,而不是指针所指向的数据对象大 小)。
      另外一个以指针作为函数参数的目的是希望在函数内部对数据进行修改。
      当传递一个指向常量的指针给函数时,其意图为不希望函数内部对指针所指的数据进行修改。例如void passingAddressOfConstants(const int* num1, int* num2),不希望num1所指数据被修改。
      将指针作为返回值返回时,应避免以下几个常见问题:
            – 返回未初始化的指针
            – 返回指向非法地址的指针
            – 返回指向函数本地变量的指针
            – 返回指针后,没有释放其所指的内存块
 
      如果函数要修改的不是参数中指针所指的数据,而是指针本身所指的内存地址,那么应以double pointer形式作为函数参数:

        void allocateArray(int **arr, int size, int value) {
            *arr = (int*)malloc(size * sizeof(int));
            if(*arr != NULL) {
                for(int i=0; i<size; i++) {
                    *(*arr+i) = value;
                }
            }
        }

      int *vector = NULL;
      allocateArray(&vector,5,45);

3、函数指针
      函数指针就是存放函数地址的指针。 
      使用函数指针可能导致程序运行变慢(可能感知不到),因为函数指针的使用可能导致CPU无法正确的运用分支预测,导致CPU流水线中断。

    【声明函数指针】

      函数指针的声明看起来像函数原型,比如:void (*foo)(int i);
      程序员应该确保通过函数指针调用函数的正确使用,因为C编译器不会检查是否正确的为函数指针传入正确的参数(类型、顺序以及个数)。
      通常我们用typedef声明一个函数指针类型,比如:
          typedef void (*funcptr)(int i);
          funcptr fp = foo;

    【函数指针强制转型】
     
      一个类型的函数指针可以被强制转为另外一种类型函数指针。
      转型后的指针 == 转型前的指针
     
        typedef int (*fptrToSingleInt)(int);
        typedef int (*fptrToTwoInts)(int,int);
        int add(int, int);
        fptrToTwoInts fptrFirst = add;
        fptrToSingleInt fptrSecond = (fptrToSingleInt)fptrFirst;
        fptrFirst = (fptrToTwoInts)fptrSecond;
        printf("%d\n",fptrFirst(5,6));

      在函数指针间转换,很可能导致函数调用失败。

第四章 指针与数组

1、数组概述

数组与指针记法关系紧密,在特定上下文中可以相互替换。
数组内部表示中并没有数组长度信息。
 
  【一维数组】
    int vector[5];

    一维数组是一个线性结构。数组下标起始于0,终止于(元素个数-1)。

  【二维数组】
    int matrix[2][3] = {{1,2,3},{4,5,6}};

    二维数组使用行和列标识数组元素。这类数组需要被映射到一个一维地址空间中。
    在C中,二维数组的第一行放在内存的最开始处,接下来是第二行,…,直到最后一行,这就是所谓的“行主序”。

  【多维数组】
    int arr3d[3][2][4] = {
        {{1, 2, 3, 4}, {5, 6, 7, 8}},
        {{9, 10, 11, 12}, {13, 14, 15, 16}},
        {{17, 18, 19, 20}, {21, 22, 23, 24}}
  };

    二维以上的维数的数组称为多维数组,其元素内存分配依旧遵守二维数组那种映射方式。

2、指针记法(notation)与数组

    指针记法与数组记法在一定场合可以互换,但两者并不完全相同。
    数组名单独使用时,我们得到的是数组的地址;该地址等同于数组内第一个元素的地址。

  int vector[5] = {1, 2, 3, 4, 5};
  int *pv = vector;
  int (*pv)[5] = &vector;

    vector与&vector不同,前者返回指向一个整型变量的指针(int *),后者返回一个指向整个数组的指针(int[5] *)。
  pv[i] <=> *(pv + i)
  *(pv + i) <=> *(vector + i)

  【指针与数组间的不同】

    int vector[5] = {1, 2, 3, 4, 5};
  int *pv = vector;

    sizeof(vector) = 20 != sizeof(pv)

    pv是lvalue,可以被修改而指向不同的地址;比如pv = pv + 1
    而vector不能被修改。vector = vector + 1这个表达式是错误的,不过pv = vector + 1是ok的。

  【使用malloc创建一维数组】
    int *pv = (int*) malloc(5 * sizeof(int));
    pv[3] = 10;

     可使用realloc改变malloc创建的数组的大小。
    
3、传递一维数组
    两种记法:数组记法和指针记法,分别如下:
    void displayArray(int arr[], int size);
    void displayArray(int* arr, int size);

    无论哪种,displayArray函数体内int arr[]或int *arr都将以int *arr方式使用,即数组名退化为指针,sizeof(arr) = 指针长度,而不是数组总长度。

   【一维指针数组】
   
    int* arr[5];
    for(int i=0; i<5; i++) {
        arr[i] = (int*)malloc(sizeof(int));
        *arr[i] = i;
    }

   【指针与多维数组】
         多维数组可以看成是由子数组组成的,就好比二维数组的每行都可以看成是一个一维数组。
         int matrix[2][5] = {{1,2,3,4,5},{6,7,8,9,10}};
         int (*pmatrix)[5] = matrix;

4、传递多维数组

   void display2DArray(int arr[][5], int rows);<=>
   void display2DArray(int (*arr)[5], int rows);

      上面两个版本是等价的。两个版本都指定了列的值,因为编译器需要知道每行的元素个数。

     注意第二个版本不等价于void display2DArray(int *arr[5], int rows)

      在void display2DArrayUnknownSize(int *arr, int rows, int cols)的 函数体实现中,你不能使用arr[i][j],因为arr并未被声明为二维数组。

5、动态分配二维数组

     【采用不连续的内存分配方式】

    int rows = 2;
    int columns = 5;
    int **matrix = (int **) malloc(rows * sizeof(int *));
    for (int i = 0; i < rows; i++) {
        matrix[i] = (int *) malloc(columns * sizeof(int));
    }

     【采用连续内存分配的方式】

    int rows = 2;
    int columns = 5;
    int **matrix = (int **) malloc(rows * sizeof(int *));
    matrix[0] = (int *) malloc(rows * columns * sizeof(int));
    for (int i = 1; i < rows; i++)
        matrix[i] = matrix[0] + i * columns;

       or

    int *matrix = (int *)malloc(rows * columns * sizeof(int));

第五章 指针与字符串

1、字符串基础

     字符串:以ASCII结尾'\0'字符结尾的字符序列。
     分类:字节字符串(byte string) – char类型字符序列
               宽字符串(wide string) – wchar_t 类型字符序列(每个字符16bit or 32bit,依编译器实现而定)
     字符串声明:char header[32] or char *header

    【字符串字面量池(String literal pool)】
      字符串字面量定义后将被放在字面量池中。这块内存区域存放的是组成字符串的字符序列。当一个字面量多次使用时,通常在字面量池中只存储一份该字符串。这将 降低程序的内存使用量。并且通常情况下,字面量池中的字符串是immutable的。

      大多数编译器都提供了编译开关,用于指示是否关闭字符串字面量池,比如Gcc的-fwritable-strings。

     【字符串初始化】、
            char *header = "Media Player";

      or
      char header[] = "Media Player";

      or
      char header[13];
      strcpy(header,"Media Player");

      or
      char *header = (char*) malloc(strlen("Media Player")+1);
      strcpy(header,"Media Player");

2、标准字符串操作

      比较字符串:strcmp
      拷贝字符串:strcpy
      连接字符串:strcat

3、传递字符串

      传递简单字符串:
      size_t stringLength(char* string) ;
      size_t stringLength(char string[]);

      传递字符串常量:
      size_t stringLength(const char* string);

4、返回字符串

         返回一个字面量:return "Boston Processing Center"
         动态分配的内存:
         char* spaces = (char*) malloc(number + 1);
         … …
         return spaces;

         返回local字符串的地址是危险的。

5、函数指针与字符串

第六章 指针与结构体

1、简介

    【如何为结构体分配内存】      
      结构体的大小往往大于该结构体所有字段大小之和,因为有数据对齐的需求,导致编译器在进行结构体内存分配时进行了padding操作。特定数据类型具有一 定的对齐要求,比如short类型的字段要求其地址能被2整除,而integer类型的字段要求其起始地址能被4整除。

      考虑到这些多余分配的内存,你应该谨慎对待如下操作:
      – 小心使用指针运算
      – 结构体数组的元素间有多余内存空间

    【结构体内存释放】
      为结构体分配内存时,运行时不会自动为结构体内的指针字段分配内存;同理,释放结构体内存时,运行时也不会自动释放结构体内指针字段所指向的内存。

    【避免malloc和free的额外开销】
     malloc和free多次重复调用时,会给程序带来额外的开销。一个解决方法就是自己维护一份已分配的结构。需要时,从这个池里取出一份,释放时,直接 返回给池中。如果没有可用的结构时,才考虑新创建一个。

2、使用指针支持数据结构

无论是简单还是复杂的数据结构,指针都提供了更加灵活的支持,包括链表、队列、栈以及树等。

第七章 安全问题以及不当使用指针
   
深入理解指针以及其正确的使用方法有利于开发出安全可信赖的应用。

OS引入了一些提升安全的技术,比如 Address Space Layout Randomization和Data Execution Prevention。

【Address Space Layout Randomization (ASLR) ,地址空间布局随机化】
  ASLR技术使得程序的数据区域随机布局,数据区域包括:代码、栈、堆。随机的放置这些区域让代码攻击行为很难精确预测特定代码的内存地址并使用它们。

【Data Execution Prevention(DEP),数据执行保护】
  DEP技术会阻止执行非执行数据区域中的代码。在一些攻击中,一些非执行数据区域中的数据被恶意覆写为代码,执行权也被转移到那里。但有了DEP后,这些 恶意代码将无法执行。

1、指针声明与初始化

   【不正确的指针声明】
     int* ptr1, ptr2;
      ptr1是指针,但ptr2只是一个整型变量。

      正确声明方法:int *ptr1, *ptr2; /* 更好的做法是每行仅声明一个变量 */

      下面做法存在同样的问题:
   #define PINT int*
   PINT ptr1, ptr2;

      用typedef就没有问题了:
   typedef int* PINT;
   PINT ptr1, ptr2;

   【使用指针前未初始化】
     使用前未做初始化的指针,常称作野指针(wild pointer):

   int *pi;
    …
   printf(“%d\n”,*pi);

    【处理未初始化的指针】
      指针脸上没有写自己是否做过初始化^_^。通常有三种方法用于对付未初始化的指针:
        – 总是将指针初始化为NULL;
        – 使用assert函数
        – 使用第三方工具
       
2、指针使用问题
   
      缓冲区溢出(Buffer overflow)可能由以下原因导致:
      – 访问数组元素的时候没有检查下标值
      – 做数组指针相关运算时不够谨慎
      – 用gets之类的函数从标准输入读取字符串
      – 使用strcpy和strcat不当

     【测试NULL】
       调用malloc后,总是检查返回值是否为NULL。

     【误用解引用操作符】
       int num;
       int *pi;
       *pi = &num

     【悬挂指针】

     【访问数组越界】

       char firstName[8] = "1234567";
       char middleName[8] = "1234567";
       char lastName[8] = "1234567";
       middleName[-2] = 'X';
       middleName[0] = 'X';
       middleName[10] = 'X';

     【错误计算数组大小】
         当将数组作为参数传递给函数时,务必将函数的Size一并传入,这个Size信息将避免数组访问越界。

     【误用sizeof操作符】
        int buffer[20];
        int *pbuffer = buffer;
        for(int i=0; i<sizeof(buffer); i++) {
            *(pbuffer++) = 0;
        }

         sizeof(buffer)=>sizeof(buffer)/sizeof(buffer[0]);

      【总是匹配指针类型】
      【有界指针(bounded pointer)】
      【字符串安全问题】
        对strcpy和strcat使用不当,会导致缓冲区溢出。
        在C11标准中加入了strcat_s和strcpy_s函数,如果发生缓冲区溢出,它们会返回错误。

      【函数指针问题】
       不要将函数赋值给签名不同的函数指针,这很可能将导致未定义行为发生。
      
3、内存释放问题
      【两次free】
      【清除敏感数据】
         一个良好的实践是覆写哪些不再需要的敏感数据。

        char *name = (char*)malloc(…);
        …
        memset(name,0,sizeof(name));
        free(name);

4、使用静态分析工具

      比如Gcc -Wall等。

第八章  其他零碎的知识点

1、指针转型
      指针转型有几个原因:
      – 访问特定目的的地址
      – 分配一个地址代表一个端口
      – 决定机器的endianess

    【访问特定的地址】
      #define VIDEO_BASE 0xB8000
      int *video = (int *) VIDEO_BASE;
      *video = 'A';

    【访问一个端口】
      #define PORT 0xB0000000
      unsigned int volatile * const port = (unsigned int *) PORT;
      *port = 0x0BF4; // write to the port
      value = *port; // read from the port

    【判断机器的endianess】
      int num = 0×12345678;
      char* pc = (char*) &num;
      for (int i = 0; i < 4; i++) {
          printf("%p: %02x \n", pc, (unsigned char) *pc++);
      }

2、Aliasing、Strict Aliasing和restrict关键字

两个指针同时指向一块相同的内存地址,这两个指针被称为aliasing。

     int num = 5;
     int* p1 = &num;
     int* p2 = &num;

aliasing的使用对编译器生成的代码强加了限制。
如果两个指针引用相同位置,每个指针都可以修改这块地址。当编译器生成读写这块内存的代码时,不总是可以通过将值存储在寄存器中这种办法来优化代 码。对每次引用,将强制使用机器级别的低效load和store操作。

Strict Aliasing:另外一种形式的aliasing。strict aliasing不允许不同类型的指针指向同一块内存区域。下面代码:一个指向整型的指针alias了一个指向float类型的指针了,这违反了Strict Aliasing的规则。

    float number = 3.25f;
    unsigned int *ptrValue = (unsigned int *)&number;
    unsigned int result = (*ptrValue & 0×80000000) == 0;

如果仅仅是符号标志和修饰符不同,是不会影响strict aliasing的,下面的语句是符合Strict aliasing规则的:

    int num;
    const int *ptr1 = &num;
    int *ptr2 = &num;
    int volatile ptr3 = &num;

有些场合,相同数据的不同表示是很有用处的,下面一些方法可以避免与Strict aliasing规则冲突:
        – 使用Union: 多个数据类型的联合体可以规避strict aliasing
        – 关闭strict aliasing :利用编译器提供的开关将strict aliasing关闭(不建议这么做哦),
                     比如Gcc提供的一些开关:
                 -fno-strict-aliasing 关闭strict aliasing
                 -fstrict-aliasing 打开strict aliasing
                 -Wstrict-aliasing 针对strict aliasing相关问题给出警告

        – 使用char pointer:char pointer可以alias任何对象。

       【使用Union实现一个值的多种方式表示】
   
        typedef union _conversion {
            float fNum;
            unsigned int uiNum;
        } Conversion;
        int isPositive1(float number) {
            Conversion conversion = { .fNum =number};
            return (conversion.uiNum & 0×80000000) == 0;
        }

           由于没有指针,所以不存在违反Strict aliasing的问题。

       【Strict Aliasing】
         编译器假设多个不同类型的指针不会引用到同一个数据对象,这样在strict aliasing的规则下,编译器才能够实施一些优化。如果假设不成立,那很可能发生意料之外的结果。

         即使是两个拥有相同字段,但名字不同的结构体,其对应的指针也不能引用同一个对象。但通过typedef结构体类型指针与原类型指针可以引用同一个数据对象。

         typedef struct _person {
            char* firstName;
            char* lastName;
            unsigned int age;
        } Person;
        typedef Person Employee;
        Person* person;
        Employee* employee;

       【使用restrict关键字】
         使用restrict关键字,意即告诉编译器这个指针没有被alias,这样编译器将可以进行优化,生成更为高效的代码。通常的优化方法是缓存这个指针。
         不过即便使用了restrict关键字,对编译器来说也只是一个建议,编译器可自行选择是否进行优化。
         建议新代码中都要使用restrict关键字。

        void add(int size, double * restrict arr1, const double * restrict arr2) {
            for (int i = 0; i < size; i++) {
                arr1[i] += arr2[i];
            }
        }

        double vector1[] = {1.1, 2.2, 3.3, 4.4};
        double vector2[] = {1.1, 2.2, 3.3, 4.4};
        add(4,vector1,vector2);

         以上是add函数的正确用法。

        double vector1[] = {1.1, 2.2, 3.3, 4.4};
        double *vector3 = vector1;
        add(4,vector1,vector3);
        add(4,vector1,vector1);

        这个例子中vector3与vector1指向同一份数据,也许add可以正常工作,但这个函数的调用结果并不那么可靠。

        标准C库中有多个函数使用了restrict关键字,比如void *memcpy(void * restrict s1, const void * restrict s2, size_t n)等。

C,C++开源项目中的100个Bugs

俄罗斯OOO Program Verification Systems公司用自己的静态源码分析产品PVS-Studio对一些知名的C/C++开源项目,诸如Apache Http ServerChromiumClangCMakeMySQL等的源码进行了分析,找出了100个典型的Bugs。个人觉得这份列表对C/C++ 程序员有一定参考意义。与其说事后用静态工具分析,倒不如在编码时就提高自知自觉,避免这份列表上的错误发生在你的代码中,因此这里将部分摘录一些Bugs(Bug编号这里不连续,为的是对应原文的编号)并做简要说明。原文将这份Bug列表分为了几类,这里也将沿用这个思路。

一、数组和字符串处理错误

数组和字符串处理错误是C/C++程序中最多的一类缺陷类型。这也可以看作是我们为拥有高效地底层内存操作能力而付出的代价。

[#1] Wolfenstein 3D项目 -"只有部分对象被clear了"

void CG_RegisterItemVisuals( int itemNum ) {
    …
    itemInfo_t *itemInfo;
    …
    memset( itemInfo, 0, sizeof( &itemInfo ) );
    …
}

这里的Bug出现在memset那一行。代码的真实意图是clear iteminfo这块内存,但调用memset时,第三个参数传入的却是sizeof(&iteminfo),要知道 sizeof(&itemInfo) != sizeof(itemInfo_t),前者只是一个指针的大小罢了。正确的写法是:

memset(itemInfo, 0, sizeof(itemInfo_t)); 或memset(itemInfo, 0, sizeof(*itemInfo));

[#2] Wolfenstein 3D项目 -"只有部分Matrix被clear了"

ID_INLINE mat3_t::mat3_t( float src[ 3 ][ 3 ] ) {
    memcpy( mat, src, sizeof( src ) );
}

这里的Bug出现在memcpy一行。程序的原意是将clear src[3][3]这个二维数组。但这里有个坑:那就是作为函数形式参数的数组名已经退化为指针了,对其sizeof只能得到一个指针的长度,因此这里的 memcpy只是copy了一个指针的长度,没有copy全。这里的代码是C++代码,原文中给出了正确的改正方法 – 传reference:

ID_INLINE mat3_t::mat3_t( float (&src)[3][3] )
{
    memcpy( mat, src, sizeof( src ) );
}

[#4] ReactOS项目 – "错误地计算一个字符串的长度"

static const PCHAR Nv11Board = "NV11 (GeForce2) Board";
static const PCHAR Nv11Chip = "Chip Rev B2";
static const PCHAR Nv11Vendor = "NVidia Corporation";

BOOLEAN
IsVesaBiosOk(…)
{
    …
    if (!(strncmp(Vendor, Nv11Vendor, sizeof(Nv11Vendor))) &&
            !(strncmp(Product, Nv11Board, sizeof(Nv11Board))) &&
            !(strncmp(Revision, Nv11Chip, sizeof(Nv11Chip))) &&
            (OemRevision == 0×311))
    …
}

Bug处在IsVesaBiosOK中那一串strncmp调用中,代码将一个指针的size传入strncmp作为第三个参数,导致 strncmp实际只是比较了字符串的前4 or 8个字节,而不是字符串的全部内容。

[#6] CPU Identifying Tool项目 – 数组越界

#define FINDBUFFLEN 64  // Max buffer find/replace size

int WINAPI Sticky (…)
{
    …
    static char findWhat[FINDBUFFLEN] = {'\0'};
    …
    findWhat[FINDBUFFLEN] = '\0';
    …
}

bug出在"findWhat[FINDBUFFLEN] = ‘\0′;”这一行。数组的最大长度为FINDBUFFLEN,但下标的最大值应该是FINDBUFFLEN-1,而不是FINDBUFFLEN。因此这 行代码显然应该改为findWhat[FINDBUFFLEN-1] = '\0';

[#7] Wolfenstein 3D项目 – 数组越界

typedef struct bot_state_s
{
    …
    char teamleader[32]; //netname of the team leader
    …
}  bot_state_t;

void BotTeamAI( bot_state_t *bs ) {
    …
    bs->teamleader[sizeof( bs->teamleader )] = '\0';
    …
}

"sizeof( bs->teamleader )]"这行的结果值已经超出了数组的最大边界,正确的代码是:

bs->teamleader[
  sizeof(bs->teamleader) / sizeof(bs->teamleader[0]) – 1
  ] = '\0';

[#8] Miranda IM项目 – 只Copy了部分字符串

struct _textrangew
{
    CHARRANGE chrg;
    LPWSTR lpstrText;
} TEXTRANGEW;

const wchar_t* Utils::extractURLFromRichEdit(…)
{
    …
    ::CopyMemory(tr.lpstrText, L"mailto:", 7);
    …
}

这里的bug在于L"mailto:"是宽字符串,宽字符串中的每个字符占2或4个字节(依Compiler使用的字符集编码而定),因此这里只 copy 7个字节显然是不够的,应该是7 * sizeof(wchar_t)。

[#9] CMake项目 – 循环內的数组越界

static const struct {
    DWORD   winerr;
    int     doserr;
} doserrors[] =
{
    …
};

static void
la_dosmaperr(unsigned long e)
{
    …
    for (i = 0; i < sizeof(doserrors); i++)
    {
        if (doserrors[i].winerr == e)
        {
            errno = doserrors[i].doserr;
            return;
        }
    }
    …
}

作者原本意图la_dosmaperr中for循环的次数等于数组的元素个数,但sizeof(doserrors)返回的却是数组占用的字节个数,这远远大于数组元素个数,因此造成数组越界。正确的写法:

for (i = 0; i < sizeof(doserrors) / sizeof(*doserrors); i++)

[#10] CPU Identifying Tool项目 – 打印到自身的字符串

char * OSDetection ()
{
    …
    sprintf(szOperatingSystem,
                    "%sversion %d.%d %s (Build %d)",
                    szOperatingSystem,
                    osvi.dwMajorVersion,
                    osvi.dwMinorVersion,
                    osvi.szCSDVersion,
                    osvi.dwBuildNumber & 0xFFFF);
    …
    sprintf (szOperatingSystem, "%s%s(Build %d)",
                      szOperatingSystem, osvi.szCSDVersion,
                      osvi.dwBuildNumber & 0xFFFF);
    …
}

通过sprintf,szOperatingSystem字符串将自己打印到自己里面,这是十分危险的,将导致无法预知的错误结果,可能会导致栈溢出等严重问题。

[#12] Notepad++项目 – 数组局部clear

#define CONT_MAP_MAX 50
int _iContMap[CONT_MAP_MAX];

DockingManager::DockingManager()
{
    …
    memset(_iContMap, -1, CONT_MAP_MAX);
    …
}

代码的原本试图将数组_iContMap清零,但memset的第三个参数CONT_MAP_MAX并不能代表数组的真正大小,而只是数组的元素个数而已,显然其忘记乘以sizeof(int)了。

二、未定义行为

在C/C++的语言规范中,我们常常能看到“xx is undefined”。规范中并没有明确表明这类错误是什么样子的,只是说取决于Compiler的实现,也许Compiler会给出正确的结果,但这么使用却是不可移植的。

[#1] Chromium项目 – 智能指针的误用

void AccessibleContainsAccessible(…)
{
    …
    auto_ptr<VARIANT> child_array(new VARIANT[child_count]);
    …
}

这里的问题在于使用new[]分配的内存,在智能指针释放时却用了delete,这将会导致未定义行为。看看autoptr的destructor就知道了:

~auto_ptr() {
    delete _Myptr;
}

我们可以找一些更合适的类来fix这个问题,比如boost::scopedarray。

[#2] IPP Sample项目 – 经典未定义行为

template<typename T, Ipp32s size> void HadamardFwdFast(…)
{
  Ipp32s *pTemp;
  …
  for(j=0;j<4;j++) {
    a[0] = pTemp[0*4] + pTemp[1*4];
    a[1] = pTemp[0*4] – pTemp[1*4];
    a[2] = pTemp[2*4] + pTemp[3*4];
    a[3] = pTemp[2*4] – pTemp[3*4];
    pTemp = pTemp++;
    …
  }
  …
}

很多人一眼就看到了"pTemp = pTemp++"这行,对于这个代码编译器会产生两种结果截然不同的翻译:

pTemp = pTemp + 1;
pTemp = pTemp;

TMP = pTemp;
pTemp = pTemp + 1;
pTemp = TMP;

到底是哪种呢?依赖于编译器的实现,甚至是优化级别的设定。

三、与运算优先级相关的错误

[#1] MySQL工程 – !和&的运算优先级

int ha_innobase::create(…)
{
  …
  if (srv_file_per_table
            && !mysqld_embedded
            && (!create_info->options & HA_LEX_CREATE_TMP_TABLE)) {
  …
}

这段代码原意是想测试create_info->options变量中几个bit位的值是否set了,即!(create_info->options & HA_LEX_CREATE_TMP_TABLE),但由于!的运算优先级高于&,实际逻辑变成了(!create_info->options) & HA_LEX_CREATE_TMP_TABLE了。如果想要这段代码如期工作,就不要吝啬小括号了。

[#2] Emule工程 – *和++的运算优先级

STDMETHODIMP
CCustomAutoComplete::Next(…, ULONG *pceltFetched)
{
  …
  if (pceltFetched != NULL)
    *pceltFetched++;
  …
}

显然作者原意是想对pceltFetched所指向的long型变量进行++操作,但由于*和++的运算优先级没有搞对,导致实际上执行了*(pceltFetched++)的操作,而不是(*pceltFetched)++操作。

[#3] Chromium项目 – &和!=的运算优先级

#define FILE_ATTRIBUTE_DIRECTORY 0×00000010

bool GetPlatformFileInfo(PlatformFile file, PlatformFileInfo* info) {
  …
  info->is_directory =
    file_info.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY != 0;
  …
}

这个程序员的意图是通过测试file_info.dwFileAttributes的几个bit位的值来判定是否是目录,逻辑上应该是(file_info.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0,但由于!=优先级高于&,原代码中无括号,结果逻辑变成了file_info.dwFileAttributes & (FILE_ATTRIBUTE_DIRECTORY != 0),导致is_directory将永远求值为true。

[#4] BCmenu项目 – if和else弄混

void BCMenu::InsertSpaces(void)
{
  if(IsLunaMenuStyle())
    if(!xp_space_accelerators) return;
  else
    if(!original_space_accelerators) return;
  …
}

这又是C语言的一个“大坑”,无奈这个BCMenu项目的程序员掉坑里了。虽然从代码缩进上来看,else似乎是与最外层的if配对使用,但实际这段代码的效果是:

if(IsLunaMenuStyle())
{
   if(!xp_space_accelerators) {
     return;
   } else {
     if(!original_space_accelerators) return;
   }
}

这显然不是程序员原意,看来括号必要时还是不能省略的。修改后的代码如下:

if(IsLunaMenuStyle()) {
  if(!xp_space_accelerators) return;
} else {
  if(!original_space_accelerators) return;
}

四、格式化输出错误

[#1] ReactOS项目 – 错误地输出WCHAR字符

static void REGPROC_unescape_string(WCHAR* str)
{
  …
  default:
    fprintf(stderr,
            "Warning! Unrecognized escape sequence: \\%c'\n",
            str[str_idx]);
  …
}

%c是用来格式化输出非宽字符的,这里用来输出WCHAR显然会得到错误的结果,fix solution是将%c换位%C。

[#2] Intel AMT SDK项目 – 缺少%s

void addAttribute(…)
{
  …
  int index = _snprintf(temp, 1023, 
                        "%02x%02x:%02x%02x:%02x%02x:%02x%02x:"
                        "%02x%02x:02x%02x:%02x%02x:%02x%02x",
                        value[0],value[1],value[2],value[3],value[4],
                        value[5],value[6],value[7],value[8],
                        value[9],value[10],value[11],value[12],
                        value[13],value[14],value[15]);
  …
}

 

不解释了,自己慢慢数和对照吧。

[#3] Intel AMT SDK项目 – 未使用的参数

bool GetUserValues(…)
{
  …
  printf("Error: illegal value. Aborting.\n", tmp);
  return false;
}

显然tmp是多余的。

五、书写错误

[#1] Miranda IM项目 – 在if中赋值

void CIcqProto::handleUserOffline(BYTE *buf, WORD wLen)
{
  …
  else if (wTLVType = 0×29 && wTLVLen == sizeof(DWORD))
  …
}

“wTLVType = 0×29”显然是笔误,应该是“wTLVType == 0×29”才对。

[#3] Clang项目 – 对象名书写错误

static Value *SimplifyICmpInst(…) {
  …
  case Instruction::Shl: {
    bool NUW =
      LBO->hasNoUnsignedWrap() && LBO->hasNoUnsignedWrap();
    bool NSW =
      LBO->hasNoSignedWrap() && RBO->hasNoSignedWrap();
  …
}

从最后一行先后使用了LBO和RBO来看,前面只用了LBO的那行很可能是有问题的,正确的应该是:

bool NUW =
      LBO->hasNoUnsignedWrap() && RBO->hasNoUnsignedWrap();

[#6] G3D Content Pak项目 – 一对括号放错了地方

bool Matrix4::operator==(const Matrix4& other) const {
  if (memcmp(this, &other, sizeof(Matrix4) == 0)) {
    return true;
  }
  …
}

由于括号放错了地方,导致memcmp最后的参数变成了sizeof(Matrix4) == 0,这行代码的正确写法应该是:

if (memcmp(this, &other, sizeof(Matrix4)) == 0) {

[#8] Apache Http Server项目 – 多余的sizeof

PSECURITY_ATTRIBUTES GetNullACL(void)
{
  PSECURITY_ATTRIBUTES sa;
  sa  = (PSECURITY_ATTRIBUTES)
    LocalAlloc(LPTR, sizeof(SECURITY_ATTRIBUTES));
  sa->nLength = sizeof(sizeof(SECURITY_ATTRIBUTES));
  …
}

最后一行显然是笔误,sizeof(sizeof(SECURITY_ATTRIBUTES))应该写为sizeof(SECURITY_ATTRIBUTES)才对。

[#10] Notepad++项目 – 在本来应该用&的地方使用了&&

TCHAR GetASCII(WPARAM wParam, LPARAM lParam)
{
  …
  result=ToAscii(wParam,
                 (lParam >> 16) && 0xff, keys,&dwReturnedValue,0);
  …
}

(lParam >> 16) && 0xff没有什么意义,求值结果总是true。这里的代码应该是(lParam >> 16) & 0xff。

[#12] Fennec Media Project项目 – 额外的分号

int settings_default(void)
{
  …
  for(i=0; i<16; i++);
    for(j=0; j<32; j++)
    {
      settings.conversion.equalizer_bands.boost[i][j] = 0.0;
      settings.conversion.equalizer_bands.preamp[i]   = 0.0;
    }
}

这又是一个实际逻辑与代码缩进不符的例子。作者的原意是这样的:

for(i=0; i<16; i++) 
{
    for(j=0; j<32; j++)
    {
      settings.conversion.equalizer_bands.boost[i][j] = 0.0;
      settings.conversion.equalizer_bands.preamp[i]   = 0.0;
    }
}

但实际执行代码逻辑却是:

for(i=0; i<16; i++) 
{
    ;
}

for(j=0; j<32; j++)
{   
  settings.conversion.equalizer_bands.boost[i][j] = 0.0;
  settings.conversion.equalizer_bands.preamp[i]   = 0.0;
}

这一切都是那个;导致的。

六、对基本函数和类的误用

[#2] TortoiseSVN项目 – remove函数的误用

STDMETHODIMP CShellExt::Initialize(….)
{
  …
  ignoredprops = UTF8ToWide(st.c_str());
  // remove all escape chars ('\\')
  std::remove(ignoredprops.begin(), ignoredprops.end(), '\\');
  break;
  …
}

作者意图删除所有'\\',但他用错了函数,remove函数只是交换元素的位置,将要删除的元素交换到尾部trash,并且返回指向trash首地址的iterator。正确的做法应该是"v.erase(remove(v.begin(), v.end(), 2), v.end())"。

[#5] Pixie项目 – 在循环中使用alloca函数

inline  void  triangulatePolygon(…) {
  …
  for (i=1;i<nloops;i++) {
    …
    do {
      …
      do {
        …
        CTriVertex  *snVertex =
         (CTriVertex *)alloca(2*sizeof(CTriVertex));
        …
      } while(dVertex != loops[0]);
      …
    } while(sVertex != loops[i]);
    …
  }
  …
}

alloca函数在栈上分配内存,因此在循环中使用alloca可能会很快导致栈溢出。

七、无意义的代码

[#1] IPP Samples项目 – 不完整的条件

void lNormalizeVector_32f_P3IM(Ipp32f *vec[3],
                                 Ipp32s* mask, Ipp32s len)
{
  Ipp32s  i;
  Ipp32f  norm;

  for(i=0; i<len; i++) {
    if(mask<0) continue;
    norm = 1.0f/sqrt(vec[0][i]*vec[0][i]+
                     vec[1][i]*vec[1][i]+vec[2][i]*vec[2][i]);
    vec[0][i] *= norm; vec[1][i] *= norm; vec[2][i] *= norm;
  }
}

mask是Ipp32s类型指针,这样if (mask< 0)这句代码显然没啥意义,正确的代码应该是:

if (mask[i] < 0) continue;

[#2] QT项目 – 重复的检查

Q3TextCustomItem* Q3TextDocument::parseTable(…)
{
  …
  while (end < length
         && !hasPrefix(doc, length, end, QLatin1String("</td"))
         && !hasPrefix(doc, length, end, QLatin1String("<td"))
         && !hasPrefix(doc, length, end, QLatin1String("</th"))
         && !hasPrefix(doc, length, end, QLatin1String("<th"))
         && !hasPrefix(doc, length, end, QLatin1String("<td"))
         && !hasPrefix(doc, length, end, QLatin1String("</tr"))
         && !hasPrefix(doc, length, end, QLatin1String("<tr"))
         && !hasPrefix(doc, length, end, QLatin1String("</table"))) {

  …
}

这里对"<td"做了两次check。

八、总是True或False的条件

[#1] Shareaza项目 – char类型的值范围

void CRemote::Output(LPCTSTR pszName)
{

  …
  CHAR* pBytes = new CHAR[ nBytes ];
  hFile.Read( pBytes, nBytes );
  …
  if ( nBytes > 3 && pBytes[0] == 0xEF &&
             pBytes[1] == 0xBB && pBytes[2] == 0xBF )
  {
    pBytes += 3;
    nBytes -= 3;
    bBOM = true;
  }
  …
}

表达式"pBytes[0] == 0xEF"总是False。char类型的值范围是-128~127 < 0xEF,因此这个表达式总是False,导致整个if condition总是为False,与预期逻辑不符。

[#3] VirtualDub项目 – 无符号类型总是>=0

typedef unsigned short wint_t;

void lexungetc(wint_t c) {
  if (c < 0)
    return;
   g_backstack.push_back(c);
}

c是unsigned short类型,永远不会小于0,也就是说if (c < 0)永远为False。

[#8] MySQL项目 – 条件错误

enum enum_mysql_timestamp_type
str_to_datetime(…)
{
  …
  else if (str[0] != ‘a’ || str[0] != 'A')
    continue; /* Not AM/PM */
  …
}

if (str[0] != ‘a’ || str[0] != 'A')这个条件永远为真。也许这块本意是想用&&。

九、代码漏洞

导致漏洞的代码错误实际上也都是笔误、不正确的条件以及不正确的数组操作等。但这里还是想将一些特定错误划归为一类,因为入侵者可以利用这些错误来攻击你的代码,获取其利益。

[#1] Ultimate TCP/IP项目 – 空字符串的错误检查

char *CUT_CramMd5::GetClientResponse(LPCSTR ServerChallenge)
{
  …
  if (m_szPassword != NULL)
  {
    …
    if (m_szPassword != '\0')
    {
  …
}

第二个if condition check意图检查m_szPassword是否为空字符串,但却错误的将指针与'\0'进行比较,正确的代码应该是这样的:

if (*m_szPassword != '\0')

[#2] Chromium项目 – NULL指针的处理

bool ChromeFrameNPAPI::Invoke(…)
{
  ChromeFrameNPAPI* plugin_instance =
    ChromeFrameInstanceFromNPObject(header);
  if (!plugin_instance &&
      (plugin_instance->automation_client_.get()))
    return false;
  …
}   

一旦plugin_instance为NULL,!plugin_instance为True,代码对&&后面的子条件求值,引用plugin_instance将导致程序崩溃。正确的做法应该是:

if (plugin_instance &&
        (plugin_instance->automation_client_.get()))
  return false;

[#5] Apache httpd Server项目 – 不完整的缓冲区clear

#define MEMSET_BZERO(p,l)       memset((p), 0, (l))

void apr__SHA256_Final(…, SHA256_CTX* context) {
  …
  MEMSET_BZERO(context, sizeof(context));
  …
}

这个错误前面提到过,sizeof(context)只是指针的大小,将之改为sizeof(*context)就OK了。

[#7] PNG Library项目 – 意外的指针clear

png_size_t
png_check_keyword(png_structp png_ptr, png_charp key,
                    png_charpp new_key)
{
  …
  if (key_len > 79)
  {
    png_warning(png_ptr, "keyword length must be 1 – 79 characters");
    new_key[79] = '\0';
    key_len = 79;
  }
  …
}

new_key的类型为png_charpp,顾名思义,这是一个char**类型,但代码中new_key[79] = ‘\0′这句显然是要给某个char赋值,但new_key[n]得到的应该是一个地址,给一个地址赋值为’\0′显然是有误的。正确的写法应该是(*new_key)[79] = '\0'。

[#10] Miranda IM项目 – 保护没生效

void Append( PCXSTR pszSrc, int nLength )
{
  …
  UINT nOldLength = GetLength();
  if (nOldLength < 0)
  {
    // protects from underflow
    nOldLength = 0;
  }
  …
}

nOldLength椒UINT类型,其值永远不会小于0,因此if (nOldLength < 0)这行成了摆设。

[#12] Ultimate TCP/IP项目 – 不正确的循环结束条件

void CUT_StrMethods::RemoveSpaces(LPSTR szString) {
  …
  size_t loop, len = strlen(szString);
  // Remove the trailing spaces
  for(loop = (len-1); loop >= 0; loop–) {
    if(szString[loop] != ' ')
      break;
  }
  …
}

循环中的结束条件loop >= 0将永远为True,因为loop变量的类型是size_t是unsigned类型,永远不会小于0。

十、拷贝粘贴

和笔误不同,程序员们决不因该低估拷贝粘贴问题,这类问题发生了太多。程序员们花费了大量时间在这些问题的debug上。

[#1] Fennec Media Project项目 – 处理数组元素时出错

void* tag_write_setframe(char *tmem,
                         const char *tid, const string dstr)
{
  …
  if(lset)
  {
    fhead[11] = '\0';
    fhead[12] = '\0';
    fhead[13] = '\0';
    fhead[13] = '\0';
  }
  …
}

 

咋看一下,fhead[13]做了两次赋值,似乎没啥问题。但仔细想一下,最后那行程序员的原意极可能是想写fhead[14] = '\0'。问题就在这里了。

[#2] MySQL项目 – 处理数组元素时出错

static int rr_cmp(uchar *a,uchar *b)
{
  if (a[0] != b[0])
    return (int) a[0] – (int) b[0];
  if (a[1] != b[1])
    return (int) a[1] – (int) b[1];
  if (a[2] != b[2])
    return (int) a[2] – (int) b[2];
  if (a[3] != b[3])
    return (int) a[3] – (int) b[3];
  if (a[4] != b[4])
    return (int) a[4] – (int) b[4];
  if (a[5] != b[5])
    return (int) a[1] – (int) b[5];
  if (a[6] != b[6])
    return (int) a[6] – (int) b[6];
  return (int) a[7] – (int) b[7];
}

 

编写这类代码时,我猜绝大多数人会选择Copy-Paste,然后再逐行修改,问题就发生在修改过程中,上面的代码中当处理a[5] != b[5]时就忘记修改一个下标了:return (int) a[1] – (int) b[5];显然这里的正确代码应该是return (int) a[5] – (int) b[5]。

[#3] TortoiseSVN项目 文件名不正确

BOOL GetImageHlpVersion(DWORD &dwMS, DWORD &dwLS)
{
  return(GetInMemoryFileVersion(("DBGHELP.DLL"),
                                dwMS,               
                                dwLS)) ;            
}

BOOL GetDbgHelpVersion(DWORD &dwMS, DWORD &dwLS)
{
  return(GetInMemoryFileVersion(("DBGHELP.DLL"),
                                dwMS,                           
                                dwLS)) ;                        
}

GetImageHlpVersion和GetDbgHelpVersion都使用了"DBGHELP.DLL"文件,显然GetImageHlpVersion写错文件名了。应该用"IMAGEHLP.DLL"就对了。

[#4] Clang项目 – 等同的函数体

MapTy PerPtrTopDown;
MapTy PerPtrBottomUp;

void clearBottomUpPointers() {
  PerPtrTopDown.clear();
}

void clearTopDownPointers() {
  PerPtrTopDown.clear();
}

我们看到虽然两个函数名不同,但是函数体的内容是相同的,显然又是copy-paste惹的祸。做如下修改即可:

void clearBottomUpPointers() {
  PerPtrBottomUp.clear();
}

 

十一、Null指针的校验迟了

这里的“迟了”的含义是先使用指针,然后再校验指针是否为NULL。

[#1] Quake-III-Arena项目 – 校验迟了

void Item_Paint(itemDef_t *item) {
  vec4_t red;
  menuDef_t *parent = (menuDef_t*)item->parent;
  red[0] = red[3] = 1;
  red[1] = red[2] = 0;
  if (item == NULL) {
    return;
  }
  …
}

 

在校验item是否为NULL前已经使用过item了,一旦item真的为NULL,那程序必然崩溃。

十二、其他杂项

[#1] Image Processing 项目 – 八进制数

inline
void elxLuminocity(const PixelRGBus& iPixel,
                     LuminanceCell< PixelRGBus >& oCell)
{
  oCell._luminance = uint16(0.2220f*iPixel._red +
                            0.7067f*iPixel._blue + 0.0713f*iPixel._green);
  oCell._pixel = iPixel;
}

inline
void elxLuminocity(const PixelRGBi& iPixel,
                     LuminanceCell< PixelRGBi >& oCell)
{
  oCell._luminance = 2220*iPixel._red +
    7067*iPixel._blue + 0713*iPixel._green;
  oCell._pixel = iPixel;
}

第二个函数,程序员原意是使用713这个十进制整数,但0713 != 713,在C中,0713是八进制的表示法,Compiler会认为这是个八进制数。

[#2] IPP Sample工程 – 一个变量用于两个loop中

JERRCODE CJPEGDecoder::DecodeScanBaselineNI(void)
{
  …
  for(c = 0; c < m_scan_ncomps; c++)
  {
    block = m_block_buffer + (DCTSIZE2*m_nblock*(j+(i*m_numxMCU)));

    // skip any relevant components
    for(c = 0; c < m_ccomp[m_curr_comp_no].m_comp_no; c++)
    {
      block += (DCTSIZE2*m_ccomp[c][/c][/c].m_nblocks);
    }
  …
}

变量c用在了两个loop中,这会导致只有部分数据被处理,或外部循环中止。

[#3] Notepad++项目 – 怪异的条件表达式

int Notepad_plus::getHtmlXmlEncoding(….) const
{
  …
  if (langT != L_XML && langT != L_HTML && langT == L_PHP)
    return -1;
  …
}

代码中的那行if条件等价于 if (langT == L_PHP),显然似乎不是作者原意,猜测正确的代码应该是这样的:

int Notepad_plus::getHtmlXmlEncoding(….) const
{
  …
  if (langT != L_XML && langT != L_HTML && langT != L_PHP)
    return -1;
  …
}

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言进阶课 AI原生开发工作流实战 从 0 开始构建 Agent Harness Go语言精进之路1 Go语言精进之路2 Go语言第一课 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com
这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats