c语言易错点_c语言易错题 - CSDN
  • C语言易错点总结

    2019-05-13 22:08:24
    C语言使用=作为赋值运算符,==作为比较运算符的原因是赋值在代码中更常见,这样可以减少代码长度 C编译器将程序分解成符号的方法是贪心法 除了字符串和字符常量,符号的中间不能嵌有空白(空格符,制表符和换行符...
    1. 所有的注释都会被预处理器用空格进行替换,因此注释可以出现在任何空格可以出现的地方。
    2. 除了数组名被用作运算符sizeof的参数这一情况,其他所有情形代表指向数组中下标为0的元素的指针。
    3. C语言使用=作为赋值运算符,==作为比较运算符的原因是赋值在代码中更常见,这样可以减少代码长度
    4. C编译器将程序分解成符号的方法是贪心法
    5. 除了字符串和字符常量,符号的中间不能嵌有空白(空格符,制表符和换行符)
    6. 用单引号括起一个字符代表一个整数,用双引号括起一个字符代表一个指向无名数组起始字符的指针
    7. ‘abc’在vs中保存的最后一个字符,前面的被覆盖
    8. (a++)++表达式报错,自增和自减操作符的操作数必须是一个“左值”,而a++的结果是“右值”。
    9. 只有4种操作符的结果是“左值”,分别是[], ., ->, *
    10. 运算符的优先级:[],(),->,.最高,其次是单目运算符,接着是算术>移位>关系>逻辑>赋值>条件(?:),最后是逗号,只有单目、三目和赋值运算符是自右向左结合的
    11. 注意悬挂else
    12. 如果f是一个函数名,则f;语句计算函数f的地址,却不调用
    13. 在java和Python中,一个语句要么按照我们的预期正确执行,要么立即抛出异常,但在C++中还有一种情况是逻辑上出错了,C++标准却没有规定怎么处理。
    14. C语言中只有四个运算符(&&、||、?:和,)存在规定的求值顺序,其他所有运算符对其操作数求值的顺序是未定义的。逗号运算符先对左侧操作数求值,“丢弃”后再对右侧操作数求值。
    15. 分隔函数参数的逗号并非逗号运算符,所以在f(x,y)中的x,y的求值顺序是未定义的,而在g((x,y))中则是先对x求值,在对y求值,函数g只有一个参数
    16. 按位运算符(&、|、~)和逻辑运算符(&&、||、!)某些时候可以互换,不过只是因为巧合
    17. 函数main的返回值告知操作系统该函数的执行是成功还是失败,典型的处理方案是返回值为0表示执行成功,非0为失败
    18. 当两个操作数都是有符号整数时,“溢出”就可能发生,“溢出”的结果是未定义的。正确做法是强制转换为无符号整数或者将加法转换为减法
    19. 如果p是一个空指针,则printf(“%s”,p);的行为是未定义的。
    20. 数组下标从0开始和不对称边界是对程序设计的简化
    21. 一个返回值为整型的函数如果返回失败,实际上是隐含地返回了某个“垃圾”整数,只要该值不被用到,就无关紧要
    22. 如果一个未声明的标识符后跟一个开括号,那么它将被视为一个返回整型的函数
    23. 每个外部对象都必须在程序某个地方进行定义。如果一个程序中包括了语句extern int a;那么这个程序必须在别的某个地方包括int a;这个两个语句既可以位于同一个源文件中,也可以位于不同源文件中
    24. 如果一个程序对同一个外部变量的定义不止一次,结果是未定义的。例如:int a=1;出现在一个文件中,而int a=2;出现在另一个文件中,结果与系统有关。如果一个外部变量在多个源文件中定义却并没有指定初始值,那么某些系统会接受,另一些系统则不会接受。唯一的解决办法就是每个外部变量只定义一次。
    25. 如果在一个文件中定义char filename[] = “hello”,而在另一个文件中声明extern char* filename;在打印filename时,程序将不能正常工作
    26. 为了避免可能出现的命名冲突,如果一个函数仅仅被同一个源文件中的其他函数调用,则应该用static声明该函数
    27. getchar()返回类型为int(因为EOF是int类型),如果定义一个char类型变量接收,则程序可能不能正常工作
    28. 为了保持与过去不能同时进行文件读写操作的程序的向下兼容性,一个输入操作不能随后紧跟一个输出操作,反之亦然。如果要同时操作的话,必须在中间插入fseek函数的调用,其作用是改变文件状态
    29. C语言中仅有4种基本数据类型——整型、浮点型、指针和聚合类型(数组和结构等)
    30. 整型家族包括字符、短整型、整型和长整型,分为有符号和无符号两种。长整型至少应该和整型一样长,而整型至少应该和短整型一样长。
    31. Literal有时译为字面值,有时译为常量,它们含义相同,只是表达习惯不一。其中,string literal和char literal分别译为字符串常量和字符常量,其他的literal一般译为字面值
    32. 对于使用不同字符集的系统,使用字符常量可以提高程序的可移植性
    33. 枚举类型就是指它的值为符号常量而不是字面值的类型,其变量用整型存储
    34. 所有字符和NUL终止符都存储于内存的某个位置,具有相同的值的不同字符串在内存中是分开存储的。ANSIC声明如果对一个字符串常量进行修改,结果是未定义的
    35. 链接属性一共有3种——external(外部),internal(内部)和none(无),没有链接属性的标识符(none)总是被当做单独的个体,也就是说该标识符的多个声明被当做独立不同的实体
    36. 如果extern用于同一标识符的第2次或以后的声明时,不会更改由第一次声明所指定的链接属性
    37. 函数的形式参数不能声明为静态,因为实参总是在堆栈中传递给函数,用于支持递归
    38. 由于寄存器值的保存和恢复,某个特定的寄存器在不同的时刻所保存的值不一定相同,所以机器不提供寄存器变量的地址
    39. 由于显示的初始化将在代码块的起始处插入一条隐式的赋值语句,因此在声明变量的同时进行初始化和先声明后赋值只有风格只差,并无效率之别
    40. C语言不存在专门的“赋值语句”,赋值是一种操作,在表达式内进行
    41. 一个程序如果使用了有符号数的右移位操作,则不可移植
    42. 自增和自减操作符要求操作数必须是一个“左值”,因为操作符的结果是变量值的拷贝
    43. 逗号表达式自左至右逐个进行求值,整个表达式的值就是最后那个表达式的值
    44. 间接访问和下标引用的结果是个左值,其余操作符的结果则是右值
    45. 整型算术运算总是至少以缺省整型类型的精度来进行的,所以字符型和短整型操作数在使用之前被转换为普通整型,这种转换称为整型提升
    46. 把函数调用以操作符的方式实现意味着“表达式”可以代替“常量”作为函数名
    47. 变量名与内存位置之间的关联并不是硬件提供的,而是编译器实现的,变量名使得我们更方便记住地址,然而硬件仍然通过地址访问内存位置
    48. 不能简单地通过检查一个值的位来判断它的类型,必须观察程序中这个值的使用方式,值的类型并非值本身所固有的一种特性,而是取决于它的使用方式
    49. 机器内部NULL指针的实际值不一定是零值,编译器负责零值和内部值之间的翻译转换
    50. 对一个NULL指针进行间接访问,结果是未定义的
    51. 在表达式两端加上括号总是合法的
    52. 当程序调用一个无法见到原型的函数时,编译器认为该函数返回一个整型值
    53. 一个没有参数的函数的原型应该声明为“int *func(void);”而不是“int *func();”,后者表示只给出func函数的返回类型,目的是为了保持与ANSI标准之前的程序的兼容性
    54. 阅读递归函数最容易的方法不是纠缠于它的执行过程,而是相信递归函数会顺利完成它的任务。如果你的每个步骤正确无误,你的限制条件设置正确,并且每次调用之后更接近限制条件,递归函数总是能够正确地完成任务。
    55. Strcmp函数的返回值不应当作布尔值测试,也不应与1和-1进行比较
    56. 具有相同成员列表的结构声明产生不同类型的变量
    57. union变量可以被初始化,但初始值必须是union第一个成员的类型,而且它必须位于一对花括号里面
    58. 在许多机器中,可以把函数参数声明为寄存器变量,进一步提高指针传递的效率
    59. 常见的动态内存错误:对NULL指针进行解引用操作、对分配的内存进行操作时越过边界、释放非动态分配的内存、试图释放一块动态分配的内存的一部分以及一块动态内存被释放之后继续使用
    60. 不要仅仅根据代码的大小评估它的质量
    61. #progma是不可移植的,预处理器忽略它不认识的#progma指令,两个不同的编译器可能以两种不同的方式解释同一条#progma指令
    62. 不要在一个宏定义的末尾加上分号,使其成为一条完整的语句
    63. 标准输入是缺省的输入设置,标准输出是缺省的输出设置,具体的缺省值因编译器而异。标准错误就是错误信息写入的地方,为错误信息准备一个不同的流意味着,即使标准输出重定向到其他地方,错误信息仍将出现在屏幕或其他缺省的输出设备上
    64. 对于输出流,fclose函数在文件关闭之前刷新缓冲区
    65. 环境就是一个由编译器定义的名字/值对的列表,由操作系统维护
    66. 当exit函数被调用时,所有被atexit函数注册为退出函数的函数将按照它们注册的顺序被反序依次调用
    67. System函数把它的字符串参数传递给宿主操作系统,作为一条命令,由系统的命令处理器执行
    68. 使用断言检查内存是否分配成功是危险的
    69. 使用断言来防止非法操作,可以简化程序的调试
    70. Assert是一个宏,只适用于验证必须为真的表达式
    71. 迭代比尾部递归效率更高
    72. 局部变量声明和函数原型并不会产生任何汇编代码,但如果局部变量在声明时进行了初始化,就会出现指令用于执行赋值操作
    73. 是链接器而不是编译器决定外部标识符的最大长度
    74. 无法链接由不同编译器产生的程序
    75. 对信号进行处理将导致程序的可移植性变差
    76. 函数longjmp不能返回到一个已经不再处于活动状态的函数
    77. 函数clock可能只产生处理器时间的近似值
    78. 从异步信号的处理函数中调用exit或abort函数是不安全的
    79. 当每次信号发生时,必须重新设置信号处理函数
    80. 避免exit函数的多重调用
    81. 打印长整数时,使用l修饰符可以提高可移植性
    82. 不要忘了在一条调试用的printf语句后面跟一个fflush调用
    83. 改变文件的位置将丢弃任何被退回到流的字符
    84. 如果流当前处于文件尾,feof函数返回真。这个状态可以通过对流执行fseek、rewind或fsetpos函数来清除
    85. 在宿主式运行时环境中,操作系统可能执行自己的缓冲方式,不依赖于流
    86. 以#符号开头,后面不跟任何内容的一行,这条指令是无效指令,被预处理器简单地删除
    87. “xyz”+1表达式的结果是指针,指向字符串中的第2个字符:y
    88. *”xyz”表达式的结果是x,”xyz”[2]的结果是z
    89. cdecl程序可以帮助你分析复杂的声明

     

     

     

     

    展开全文
  • C语言易错点汇总(二)

    千次阅读 2020-05-04 10:10:54
    三、关键字 我们都知道ANSI C标准中 C 语言共有 32 个关键字。后面 ISO 的 C99 标准和 C11 标准又分别增加了 5 个和 7 个关键字。本文无意介绍各个标准之间的恩恩怨怨,感兴趣的可以上网查查,包括后面新增的这 12...

    三、关键字

           我们都知道 ANSI C 标准中 C 语言共有 32 个关键字。后面 ISO 的 C99 标准和 C11 标准又分别增加了 5 个和 7 个关键字。本文无意介绍各个标准之间的恩恩怨怨,感兴趣的可以上网查查,包括后面新增的这 12 个关键字。本节将集中介绍 ANSI C 标准中的 32 个关键字,不过有些简单或是出现频率较低的关键字将略掉不提。先来和这个 32 个关键字打个照面:

    auto break case char const continue

          default 

    do
    double else enum extern

            float

    for goto if
    int long register return short signed sizeof static
    struct switch typedef union unsigned

            void

    volatile

           while 

        《C语言深度剖析》一书中作者对每一个关键字都进行了非常详细的剖析,尤其是那些容易出错的关键字。本文中部分例子也引用自此书,想了解更多关于此书中关键字一章的内容,可以上网搜该书的电子版。

    3.1 if、else

    3.1.1 if内的比较问题

           关于 if 内的比较表达式代码风格争论已久,比较流行的风格会要求如下:

    bool in;
    if (in) / if (!in)
    
    int x;
    if (x == 0) / if (x != 0)
    
    int *ptr;
    if (ptr == NULL) / if (ptr != NULL)
    

    这么做完全合法,而且也非常清晰。但是阅读过Linux内核代码的人会发现,类似

    bool in;
    if (in) / if (!in)
    
    int x;
    if (x) / if (!x)
    
    int *ptr;
    if (ptr) / if (!ptr)

    这样的简写形式比比皆是,因为内核代码风格一直秉承简洁之道。我们无意去争论谁优谁劣,但是正如 C 语言设计之初秉承的理念一样,程序员对代码唯一负责。采样哪种形式都可以,但请确保代码行为和你的预期一致。后面的风格只区分 0 和非 0,任何非 0 的值都为真。

    3.1.2 else配对问题

    if (a == 0)
        if (b > 1)
            printf("error\n");
    else {
        b = 0;
    }

           上面的代码不仅仅是初学者容易犯,即便是多年经验的 C 老手也有在此失误过的。else 和最近的 if 匹配,上例中的 else 实际上要和 if (b > 1) 这个 if 配对:

    if (a == 0) {
        if (b > 1)
            printf("error\n");
        else {
            b = 0;
        }
    }

           良好的代码风格是完全可以避免这样的问题的。Linux 内核开发者制定了一套编码风格,对代码格式和布局做出了规定。当然并不是说内核代码风格有多么优秀或者其他的代码风格有多拙劣。保持一致的编码风格是为了提高编程效率。我们都知道,内核代码是由全世界许许多多的程序员一起开发完成的,如果每个人的代码风格都迥异,那么不管是对于代码的维护还是阅读,都将是十分不便,所以保持一个统一的代码风格就显得格外必要。如果你是一个在 Linux 内核下面开发的程序员,那么了解这种内核代码风格是十分必要的。假如某一天你有幸成为一名内核代码贡献者,代码风格不好,你的补丁都会提交不上去,搞不好 Linus 大哥哥还会向你爆粗口。关于内核代码风格,可以参阅《Linux内核设计与实现》一书的第20章,作者对这个话题做了非常详细的举例说明,以及一些关于补丁和代码风格的实用工具。另外内核源码树 Documentation/CodingStyle 中也有 Linus 大哥哥对我们的谆谆教诲。

    3.2 switch、case

           基本格式如下:

    switch (branch) {
    case A:
        /* do A thing */
        break;
    case B:
        /* do B thing */
        break;
    case C:
        /* do C thing */
        /* and then fall through */
    case D:
        /* do D thing */
        break;
    default:
        break;
    }

    注意点:

    1、每个 case 结束后一定要加 break,否则会导致分支代码重叠,除非你是有意这么为之。即便如此,也建议显式地标明你就是要这么做,就像 case C 的处理。

    2、default 分支最好加上,并且是处理那些真正的默认情况。这样做并非多此一举,可以避免让人误以为你忘了 default 处理。

    3、case 后面只能是整型或字符型的常量或常量表达式,其实字符型常量本质上也是一个整数。

    3.3 do、while、for

           C 语言一共有三种循环语句:

    while () {
    
    };
    
    do {
    
    } while();
    
    for (;;) {
    
    }

    3.3.1 do-while循环 

           在 Linux 内核中,经常会碰到 do () {} while (0); 这样的语句,而且多数情况以宏定义的形式出现。有人认为既然是 while (0),那么就只执行一次吗?那加不加这个 do () {} while (0) 效果不都是一样的吗?其实这是内核中的一个编程技巧。先来看一个实例:

    #define DUMP_WRITE(addr,nr)   do { memcpy(bufp, addr, nr); bufp += nr; } while(0)

    do-while 循环是先执行循环体然后再来判断循环条件,while (0) 说明循环体会且只会被执行一次。既然如此,那我可不可以这样定义呢?

    #define DUMP_WRITE(addr,nr)   memcpy(bufp, addr, nr); bufp += nr;

    定义本身没有问题,但是如果用在代码上下文中情况就不一样了,比如:

    if (addr)
        DUMP_WRITE(addr, nr);
    else
        pr_err("pointer address is NULL!\n");

    经过预处理以后就变成了这样:

    if (addr)
        memcpy(bufp, addr, nr); bufp += nr;;
    else
        pr_err("pointer address is NULL!\n");

    注意到问题了吗?if 和 else之间插入了 bufp += nr; 这条语句!这样 else 因为找不到与之配对的 if 语句而导致编译报错。有人可能很快就会想到加花括号,变成如下这样:

    #define DUMP_WRITE(addr,nr) { memcpy(bufp, addr, nr); bufp += nr; }

    可惜展开以后还是报错:

    if (addr)
        { memcpy(bufp, addr, nr); bufp += nr; };
    else
        pr_err("pointer address is NULL!\n");

    花括号后面的分号依旧把 else 和 if 分开了。如果用 do-while 循环则可以完美地避免上述问题:

    if (addr)
        do { memcpy(bufp,addr,nr); bufp += nr; } while (0);
    else
        pr_err("pointer addr is NULL!\n");

    3.3.2 for循环

           for 循环一般用于循环次数已知的情形,当然也不绝对,内核源码中像

    for (; ;) {
        /* do something */
        ......
    }

    这样的处理也是存在的。

    注意点:

    1、循环次数最好是采用 for (i = 0; i < 10; i++) 这样左闭右开的形式,由于循环次数计算错误导致的 bug 真的是痛心疾首,所以关于次数的问题最好是谨慎些。《C陷阱与缺陷》中作者用了很长的篇幅专门来说明这种边界计算的问题。

    2、千万不要在循环体内修改循环变量,防止循环失控。

    3、循环嵌套最好控制在 3 层 以内,否则建议重构你的代码。当循环嵌套超过 3 层,程序员对循环的理解能力会极大的降低。

    4、break 是结束本层循环,如果循环只有一层,那么直接跳出该循环。如果循环有多层,那么跳出到上层循环。continue 是结束本次循环,进入下次循环,注意仍然在本层循环。

    3.4 goto

           这是一个颇受争议的关键字,有人主张禁用,但是我认为过于极端了。滥用 goto 语句确实会造成程序结构混乱,造成一些难以发现的 bug,但是好的 goto 用法能让代码处理变得更高效。Linux内核中 goto 的使用非常广泛,不过用得最多的情形是在处理异常错误的时候,类似代码如下:

    static int xxx_probe(xxx)
    {
        ...
        if (!ptr1)
            goto err_ptr1;
        ...
        if (!ptr2)
            goto err_ptr2;
        ...
        if (!ptr3)
            goto err_ptr3;
        ...
    
        return 0;
    
    err_ptr3:
        ...;
    err_ptr2:
        ...;
    err_ptr1:
        ...;
        return -1;
    }

    这种 goto 的错误处理用法简单而高效,只需要保证错误处理时注销、资源释放等与注册、资源申请时顺序相反,请仔细观察三个错误标签的代码顺序。

           争论应不应该禁用没有太大意义,我们真正要搞清楚的是 goto 的用法。

    3.5 sizeof

           很多人一直以为 sizeof 是一个函数,因为它后面跟了一对圆括号。先看看下面这个例子:

    int i = 0;
    A、sizeof(int); B、sizeof(i); C、sizeof int; D、sizeof i;

    毫无疑问,32 位系统下 A 和 B 的值为 4。那 C 呢?D 呢?在 32 位系统下,我们发现 D 的结果也为 4。sizeof 后面没有括号居然也行!那函数名后面没有括号行吗?由此可知 sizeof 绝非函数。那么 C 呢?通过测试,我们发现编译器报错了。不是说 sizeof 是个关键字,其后面的括号可以没有吗?那你想想 sizeof int 表示什么呢?int 前面加一个关键字?类型扩展?明显不正确,我们可以在 int 前加 unsigned,const 等关键字但不能加 sizeof。事实上,sizeof 在计算 变量 所占空间大小时,括号可以省略,而计算 类型 大小时不能省略。为了避免出错,建议大家不管是计算哪种情况都加上括号。

           下面是几个易混淆的问题:

    int *p = NULL;
    char *ch = NULL;
    printf("sizeof(p) = %lu, sizeof(*p) = %lu; sizeof(ch) = %lu, sizeof(*ch) = %lu\n",
            sizeof(p), sizeof(*p), sizeof(ch), sizeof(*ch));
    
    int a[100];
    printf("sizeof(a) = %lu, sizeof(a[100]) = %lu, sizeof(&a) = %lu, sizeof(&a[0]) = %lu\n",
            sizeof(a), sizeof(a[100]), sizeof(&a), sizeof(&a[0]));
    
    int b[100];
    void fun(int b[100])
    {
        printf("sizeof(b) = %lu\n", sizeof(b));
    }

    三个 printf 打印出来的值分别是多少?不清楚的地方一定要实际调试看看,在后面指针和数组一章我们再回过头讨论这几个问题。另外,假如

    sizeof (int)*p

    这个表达式编译不会报错,那它表示什么意思?通过再三调试,上面表达式的含义是:

    sizeof(int) * p

    p 可以是 char/int/float 等变量类型,但不能是指针和数组地址。

    3.6 struct

           这个关键字绝对不好惹,Linux 内核源码中一个结构体少则上十行,多则上百行几百行的都有。而这些复杂的结构体正是阅读内核源码的一个巨大障碍之一。struct 非常类似面向对象语言中的 class,面向对象的设计思维在 Linux 内核源码中非常普遍。比如在驱动程序中,一个硬件设备会被抽象成一个 struct 结构体,里面的具体成员就用来描述这个硬件设备的各个属性特征,以及与其它硬件设备之间的关系。

    3.6.1 变长数组

           所谓变长数组就是数组长度待定的数组。变长数组的使用场景一般在结构体中。把结构体的最后一个成员定义为一个变长数组,以此达到灵活分配内存的目的,在 Linux 内核中这种技巧随处可见。比如 USB 的 Mass Storage 驱动里就会用到这么一个结构体(为方便阅读,删掉了无关成员):

    struct Scsi_Host {
        ...
    	
        /*
         * We should ensure that this is aligned, both for better performance
         * and also because some compilers (m68k) don't automatically force
         * alignment to a long boundary.
         */
        unsigned long hostdata[0]  /* Used for storage of host specific stuff */
            __attribute__ ((aligned (sizeof(unsigned long))));
    }

    关键字__attribute__ 用于对变量指定特殊属性,是 GNU C 对标准 C 的扩展之一,感兴趣的可以了解一下,在这里不做过多说明,这里指定的是内存对齐属性,从注释我们也可以看出。关于内存对齐在接下来一小节会详细说明。回到本节主题,hostdata 是一个成员个数为 0 的数组,它的作用是为了在 struct Scsi_Host 这个结构体后面紧跟着分配另一个结构体空间,以达到这两个结构体相互依赖的目的。用法如下(删掉了无关代码):

    static inline struct us_data *host_to_us(struct Scsi_Host *host)
    {
        return (struct us_data *) host->hostdata;
    }
    
    static int storage_probe(struct usb_interface *intf,
    			 const struct usb_device_id *id)
    {
        struct Scsi_Host *host;
        struct us_data *us;
    
        ...
    
        /*
         * Ask the SCSI layer to allocate a host structure, with extra
         * space at the end for our private us_data structure.
         */
        host = scsi_host_alloc(&usb_stor_host_template, sizeof(*us));
        if (!host) {
            printk(KERN_WARNING USB_STORAGE
                   "Unable to allocate the scsi host\n");
            return -ENOMEM;
        }
    
        us = host_to_us(host);
    
        ...
    }

    首先定义了两个结构体指针 struct Scsi_Host *host 和 struct us_data *us,这两个结构体在 Mass Storage 驱动里非常重要,在这里我们只是为了说明 C 语言里的变长数组特性,而不会去深究这两个结构体。然后通过 scsi_host_alloc() 这个函数分配了一个 struct Scsi_Host 结构体内存空间,并返回结构体地址,通过注释我们也可以猜出一二。暂时先把 scsi_host_alloc() 这个函数放一放,稍后我们再回过头来分析。接着往下看 us = host_to_us(host); 这条语句,展开以后就是

    us = (struct us_data *) host->hostdata;

    hostdata 是一个数组名,它是一个地址,然后通过类型强制转换赋值给了 us,两个结构体指针 host 和 us 就是通过这个变长数组名 hostdata 联系上了。接下来我们就来看看 scsi_host_alloc() 这个函数做了些什么:

    struct Scsi_Host *scsi_host_alloc(struct scsi_host_template *sht, int privsize)
    {
        struct Scsi_Host *shost;
        gfp_t gfp_mask = GFP_KERNEL;
    
        ...
    
        shost = kzalloc(sizeof(struct Scsi_Host) + privsize, gfp_mask);
        if (!shost)
            return NULL;
    
        ...
    }

    kzalloc() 函数是内核空间分配内存的接口,你可以把它和用户空间的 malloc() 作类比。第一个参数就是分配的内存大小,可以很清楚的看到,除了为 struct Scsi_Host 结构体分配 sizeof(struct Scsi_Host) 个字节外,还额外加了 privsize 个字节。注意到了吗?privsize 是上面传参传过来的,大小刚好是 sizeof(struct us_data)。在计算 sizeof(struct Scsi_Host) 时,变长数组并不会被计算在内,所以 hostdata 刚好是额外分配的 sizeof(struct us_data) 这片空间的首地址。内存示意图如下(假设开辟的这块内存首地址地址为 0xff810000):

    变长数组的合法性是在 C99 标准中才加入的,但是 GNU C 对 ANSI C 标准进行了一系列扩展,其中就包括变长数组。所以变长数组的使用在 Linux 内核代码中随处可见。

    3.6.2 内存对齐

           内存对齐的话题要搞清楚两个问题:什么是内存对齐?为什么要内存对齐?

           所谓内存对齐,是指变量的起始地址在内存中需要落在自然边界上。在内存中,某个变量的自然边界就是能被某个数整除的地址。这个 “某个数” 和 “某个变量”(包括位置)、编译器以及硬件体系都有关系。关于内存对齐有下面三个规则:

    首先,每个成员分别按自己的方式对齐,并能最小化长度。
    其次,复杂类型(如结构)的默认对齐方式是它最长的成员的对齐方式,这样在成员是复杂类型时,可以最小化长度。
    然后,对齐后的长度必须是成员中最大的对齐参数的整数倍,这样在处理数组时可以保证每一项都边界对齐。

    下面来逐条展开说明。每种变量类型的自然边界不一样,大体上来说 “某个数” 就是该变量所占的内存空间大小。

    看下面这个例子:

    struct test {
        char name[9];
        int score;
        struct test *next;
    };
    
    struct alignment {
        char a;
        short b;
        int c;
        char d[12];
        unsigned long int e;
        int f[3];
        struct test g;
        char *h;
    };
    
    int main()
    {
        struct alignment al;
        struct test tt;
        char *p;
    
        printf("sizeof(pointer) is %lu\n\n", sizeof(p));
    
        printf("name: %lu\n", (unsigned long)tt.name - (unsigned long)&tt);
        printf("score: %lu\n", (unsigned long)&tt.score - (unsigned long)&tt);
        printf("test: %lu\n", (unsigned long)&tt.next - (unsigned long)&tt);
        printf("tt: %lu\n\n", sizeof(tt));
    
        printf("a: %lu\n", (unsigned long)&al.a - (unsigned long)&al);
        printf("b: %lu\n", (unsigned long)&al.b - (unsigned long)&al);
        printf("c: %lu\n", (unsigned long)&al.c - (unsigned long)&al);
        printf("d: %lu\n", (unsigned long)&al.d - (unsigned long)&al);
        printf("e: %lu\n", (unsigned long)&al.e - (unsigned long)&al);
        printf("f: %lu\n", (unsigned long)&al.f - (unsigned long)&al);
        printf("g: %lu\n", (unsigned long)&al.g - (unsigned long)&al);
        printf("h: %lu\n", (unsigned long)&al.h - (unsigned long)&al);
        printf("al: %lu\n", sizeof(al));
    
        return 0;
    }

    打印结果:

    sizeof(pointer) is 8
    
    name: 0
    score: 12
    test: 16
    tt: 24
    
    a: 0
    b: 2
    c: 4
    d: 8
    e: 24
    f: 32
    g: 48
    h: 72
    al: 80

    TODO...

    一个字或双字操作数跨越了 4 字节边界,或者一个四字操作数跨越了 8 字节边界,被认为是未对齐的,从而需要两次总线周期来访问内存。一个字起始地址是奇数但却没有跨越字边界被认为是对齐的,能够在一个总线周期中被访问。某些操作双四字的指令需要内存操作数在自然边界上对齐。如果操作数没有对齐,这些指令将会产生一个通用保护异常。双四字的自然边界是能够被 16 整除的地址。其他的操作双四字的指令允许未对齐的访问(不会产生通用保护异常),然而,需要额外的内存总线周期来访问内存中未对齐的数据。

    3.6.3 根据成员地址找到结构体地址

           根据结构体的某个成员地址找到该结构体的地址,这个技巧在内核中使用非常广泛,而实现该技巧的正是著名的 container_of 宏函数。container_of 有内核第一宏的美誉其定义如下:

    /**
     * container_of - cast a member of a structure out to the containing structure
     * @ptr:        the pointer to the member.
     * @type:       the type of the container struct this is embedded in.
     * @member:     the name of the member within the struct.
     *
     */
    #define container_of(ptr, type, member) ({                      \
    	const typeof( ((type *)0)->member ) *__mptr = (ptr);    \
    	(type *)( (char *)__mptr - offsetof(type, member) );})

    注意宏函数的三个参数。第一个参数 ptr 指向我们所说的结构体成员的指针;第二个参数不是变量,而是类型名,类型名正是我们所求地址的结构体;第三个参数是结构体成员名。

           为了更好的理解,我们用一个实际例子来说明:

    struct city {
        char name[20];
        int position;
        unsigned int area;
        int people_count;
        strcut town tn;
        ......
    };
    
    struct city jingzhou = {
        .name = "jingzhou",
        .position = 7, /* means Hubei */
        .area = 1234,
        .people_count = 5678,
        .tn = xxx,
    };
    
    struct city *city = NULL;
    struct town *town = &jingzhou.tn;

    假设现在声明了一个结构体类型 city ,并且定义了一个结构体变量 jingzhou,并对其成员进行了初始化。现在已知有一个strcut town *类型的指针 town 指向了 jingzhou 的成员变量 tn,要求出结构体 jingzhou 的地址。如果使用 container_of 宏函数,那么可以非常方便的得到我们要求的地址:

    city = container_of(town, struct city, tn);

    我们现在抛开 container_of 这个现成的工具,如果要求出 jingzhou 的地址,那么很自然地,只需要求出 tn 这个成员在 jingzhou 内部的偏移量(假设是 offset)即可。对于本例而言,前面的成员变量都已知(只有name/position/area/people_count),offset 的值不难求出(注意内存对齐)。但是实际代码中,几乎不可能知道前面都有哪些成员变量,而且结构体类型也是千差万别,如果每碰到一次都要有针对性的专门计算一次 offset,这对内核开发者来说简直就是一种侮辱。于是内核开发者设计了这个 "万能公式" 般的宏函数,其基本思路当然也是求出 offset 值,然后用 town 的地址减去这个 offset,只不过其 offset 计算的精妙之处,让人惊叹。

           我们现在再来回过头细细分析。先只看第二行:

    const typeof( ((type *)0)->member ) *__mptr = (ptr); 

    首先是 0 地址的使用:((type *)0)->member,展开就是 ((strcut city *)0)->member,把 0 地址强转为 struct city *,这是合法的。typeof 的作用是根据括号里面的变量名求出该变量的类型名,对应到本例中就是 struct town。有人说 typeof 是 C 语言的关键字之一,但是我查了最新的 C99 和 C11标准新增的12个关键字,均没有 typeof,我认为应该是 GNU C 的扩展,但是也没有找到实锤的证据,哪位朋友如知道的更详细,还请不吝赐教。语句展开以后就变成了:

    const struct town *__mptr = (town); 

    定义了另外一个指针,而且把 town 赋值给它。有人可能会有疑问了,这不是多此一举吗?已经有了 town,为什么还要再额外定义一个指针,而且还是把 town 直接赋值过去?试想一下,如果使用  container_of 的编程人员不小心传过来的第一个参数 ptr 和 第三个参数 member 类型不一样怎么办呢?这条语句的作用就是为了防止这种情况的发生,因为如果不一样编译的时候就会提示报错或者肯定会有warning。不得不说内核开发者简直就是保姆般地考虑周到。

           再来来看第三行:

    (type *)( (char *)__mptr - offsetof(type, member) );

    最前面做了强转,毫无疑问,后面括号里计算出来的肯定是个地址,这个地址正是我们所要求的。括号里的 char * 强转是把指针加减操作的单位改为了1,也就是数学运算了。offsetof,根据名字也可以猜到七八,offset of,什么什么的偏移量。展开来我们才能知道这个什么什么到底是什么:

    #define offsetof(TYPE, MEMBER)    ((size_t) &((TYPE *)0)->MEMBER)

    又是 0 地址的使用,对 0 地址进行强转,相当于认为内存空间从 0 地址开始到 sizeof(TYPE) 这段空间装着一个 TYPE 的变量。具体到我们这里,TYPE 就是 struct city。所以,&((TYPE *)0)->MEMBER 就是第二个参数 MEMBER 的地址,强转为 size_t 就是我们梦寐以求的 offset 偏移量了。

           有人可能又会有疑问了,在 0 地址进行强转然后指向 MEMBER,这是合法的吗?这个就涉及到地址的引用和元素的引用之间的区别了。这里只是引用了地址,并没有对地址处的元素做任何改变,因此当然合法。这有点类似《C陷阱与缺陷》一书中谈到的对数组中越界元素的地址的引用。举例来说明(来自《C陷阱与缺陷》):

    #define  N  1024
    static char buffer[N];
    static char *bufptr = &buffer[0];
    
    buf_write(char *p, int len)
    {
        while (--len > 0) {
            if (bufptr == &buffer[N])
                flushbuffer();
            *bufptr++ = *p++;
        }
    }

    为了说明引用数组越界元素的地址的问题,这里只是截取了相关的核心代码,所以只看这部分代码的话会有 buffer 处理的bug,原书中此例还用来解释了其他问题,如有兴趣可以看书中的例子,很详细。if 里面的判断使用了 &buffer[N],这个元素肯定是不存在的,但这里是合法的,因为这里并不需要引用这个元素,而只是引用它的地址,并且这个地址确实是存在的。ANSI C 明确允许这种用法:

    数组中实际不存在的 "溢界" 元素的地址位于数组所占内存之后,这个地址可以用于进行赋值和比较。当然,如果要引用该元素,那就是非法的了。

    3.7 union

           union 类型和结构体类型外形长的非常像,但是本质却差远了。union 类型在内存中同一时刻只能存储其中的一个成员,所有数据成员共享同一块内存空间。因此,union 类型占用的内存大小等于其所有成员中最大长度的那个。举例来说:

    struct person {
        char name[7];
    };
    
    union utest {
        int a;
        char b;
        char *c;
        struct person d;
        double e;
        short f;
    };

    32 位系统下,上面 union 所有数据成员中最大长度的是结构体变量 d(注意内存对齐)和 double 变量 e,都是 8 字节,所以一个 union utest 类型的变量在内存中占用 8 个字节的空间。

    3.7.1 大小端存储模式

           所谓大小端指的是数据在内存中的存放方式:

    大端模式(Big_endian):数据的 高字节 存储在 低地址 中,低字节 则存放在 高地址 中。

    小端模式(Little_endian):数据的 高字节 存储在 高地址 中,低字节 则存放在 低地址 中。

    由于 union 类型所有数据成员共享同一块内存的这种特点,导致不同的存储模式对数据的结果有很大的影响。比如:

    union {
        int i;
        char a[2];
    } *p, u;
    
    int main()
    {
        p = &u;
        p->i = 0;
        p->a[0] = 0x12;
        p->a[1] = 0x34;
    
        printf("p->i = %d\n", p->i);
        return 0;
    }

    p->i 的值很显然会受大小端模式的影响。Linux 内核中有专门的的大小端转换函数,如果的你代码中有关于大小端情况的时候,要特别注意数据的存储。

    3.8 signed、unsigned

           很明显我们关键要搞清楚负数在内存中的存储方式。在计算机系统中,数值一律用补码来表示(存储)。正数的补码与其原码一致;负数的补码:符号位为 1,其余位为该数绝对值的原码按位取反,然后整个数加 1。主要原因是使用补码,可以将符号位和其它位统一处理;同时,减法也可按加法来处理。另外,两个用补码表示的数相加时,如果最高位(符号位)有进位,则进位被舍弃。举例来说,对于用 char 类型来存储的 -1 和 1,两者内存中的存储值分别是 1111 1111b 和 0000 0001b,相加的时候结果为1 0000 0000b,但是对于 char 类型来说只能存储 8 位的数据,最高位第 9 位舍弃。

           有符号和无符号的取值范围就不列出来了,这里需要提醒一点的是,缺省情况下编译器认为数据是 signed 类型的,可以省略不写。请思考下面这段代码的输出结果:

    int main()
    {
        int i;
        char a[1000];
    
        for (i = 0; i < 1000; i++)
            a[i] = -1 - i;
    
        printf("%lu", strlen(a));
        return 0;
    }

           按照负数补码的规则,-1 的补码为 0xff,-2 的补码为 0xfe…… 当 i 的值为 127 时,a[127] 的值为 -128,而 -128 是 char 类型数据能表示的最小负数。当 i 继续增加,a[128] 的值肯定不能是 -129,因为这时候发生了溢出,-129 需要 9 位才能存储下来,而 char 类型数据只有 8 位,所以最高位被丢弃。剩下的 8 位是原来 9 位补码的低 8 位的值,即 0x7f。当 i 继续增加到 255 的时候,-256 的补码的低 8 位为 0。然后当 i 增加到 256 时,-257 的补码的低 8 位全为 1,即低八位的补码为 0xff,如此又开始一轮新的循环……

           按照上面的分析,a[0] 到 a[254] 里面的值都不为 0,而 a[255] 的值为 0。strlen 函数是计算字符串长度的,并不包含字符串最后的 ‘\0’。而判断一个字符串是否结束的标志就是看是否遇到 ‘\0’,如果遇到 ‘\0’,则认为本字符串结束。分析到这里,strlen(a)的值为 255 就很清楚了。这个问题的关键就是要明白 char 类型默认情况下是有符号的,其表示的值的范围为[-128, 127],超出这个范围的值会产生溢出。另外还要清楚的就是负数的补码怎么表示。

           再看看下面的代码段有没有什么问题:

    unsigned i;
    
    for (i = 9; i >= 0; i--)
        printf("%u\n", i);

    3.9 static

    不要误以为关键字 static 很安静,其实它一点也不安静。

    关键字 static 主要修饰变量和函数。

    1、修饰变量。

           变量又分为局部和全局变量。

           静态全局变量,作用域仅限于变量被定义的文件中,其他文件即使用 extern 声明也没法使用他。准确地说作用域是从定义之处开始,到文件结尾处结束。同一个文件中就算在定义之处前面的那些代码行也不能使用它,想要访问就必须得在前面使用extern 声明,所以为了避免这种情况最好在文件顶端定义。

           静态局部变量,在函数体里面定义的,就只能在这个函数里用了,同一个文件中的其他函数也用不了。由于被 static 修饰的变量总是存在内存的静态区,所以即使这个函数运行结束,这个静态变量的值还是不会被销毁,函数下次使用时仍然能用到这个值。

    2、修饰函数。

           函数前加 static 使其成为静态函数。但此处 static 的含义不是指存储方式,而是指函数的作用域仅局限于本文件内,故又称内部函数。使用内部函数的好处是:不同的人编写不同的函数时,不用担心自己定义的函数是否会与其它文件中的函数同名。

           Is that all?

    3.9.1 进程的内存布局

           我们都知道静态(或全局)变量与普通临时变量最主要的差别就是生存期的长短,那为什么会有这个区别呢?是什么原因导致的呢?静态变量和普通临时变量存放的位置不一样吗?不一样的话分别放在哪儿呢?为什么静态(或全局)变量在整个程序执行期间都存在而其他文件却还是无法访问呢?

           这就涉及到程序在内存中的布局了。在开始介绍内存布局之前,有必要澄清进程和程序之间的区别。所谓进程,是指一个可执行程序的实例。进程属于操作系统的概念范畴,从内核的角度来说,进程就是著名的 tast_struct 数据结构,用来维护进程状态信息,这些信息包括进程ID号、虚拟内存表、打开文件描述符表、进程资源使用及限制、当前工作目录等等。从用户空间的角度来说,进程是一块内存空间,里面包含进程所要执行的代码及所使用的变量 。所谓程序,是指包含了一系列信息的文件,这些信息描述了如何在运行时创建一个进程,所包括的内容如下。

           1、二进制格式标识:每个程序文件都包含用于描述可执行文件格式的元信息(metainformation)。内核利用此信息来解释文件中的其他信息。历史上,UNIX 可执行文件曾有两种广泛使用的格式,分别为最初的 a.out(汇编程序输出)(是的,a.out 文件具有 a.out 格式,就像佛具有佛性)和更加复杂的 COFF(通用对象文件格式)。现在,大多数 Unix 实现(包括Linux)采用可执行连接格式(ELF),这一文件格式比老版本格式具有更多优点。在我的 Ubuntu14.04 上用 file 命令查看编译出来的可执行文件 a.out,可以看到确实是 ELF 格式。

    a.out: ELF 64-bit LSB  executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, BuildID[sha1]=bd24a5862f3c65373e92f43b6565c55bf5d092f2, not stripped

           2、机器语言指令:对程序算法进行编码。

           3、程序入口地址:标识程序开始执行时的起始指令位置。

           4、数据:程序文件包含的变量初始值和程序使用的字面常量值(比如字符串)。

           5、符号表及重定位表:描述程序中函数和变量的位置及名称。这些表格有多种用途,其中包括调试和运行时的符号解析(动态链接)。

           6、共享库和动态链接信息:程序文件所包含的一些字段,列出了程序运行时需要使用的共享库,以及加载共享库的动态链接器的路径名。

           7、程序文件还包含许多其他信息,用以描述如何创建进程。

           程序是一个静态的可执行文件,程序跑起来了才是进程。一个可执行程序可以创建多个进程,或者反过来说,多个进程运行的可以是同一程序。如下代码可以演示这个情况。

    int main()
    {
        while (1)
            sleep(10);
    
        return 0;
    }

    在 shell 终端以后台运行的方式执行两次,可以看到系统创建了两个进程:

    troy @ workpc 11:19:55:~/work$ ./a.out &
    [1] 21329
    troy @ workpc 11:20:03:~/work$ ./a.out &
    [2] 21330
    troy @ workpc 11:30:09:~/work$ ps -l
    F S   UID   PID  PPID  C PRI  NI ADDR SZ WCHAN  TTY          TIME CMD
    0 S  1000 21329 24467  0  80   0 -  1049 hrtime pts/24   00:00:00 a.out
    0 S  1000 21330 24467  0  80   0 -  1049 hrtime pts/24   00:00:00 a.out
    0 R  1000 21355 24467  0  80   0 -  3650 -      pts/24   00:00:00 ps
    0 S  1000 24467  3262  0  80   0 -  7112 wait   pts/24   00:00:00 bash

           说清楚了进程和程序的区别,现在来说明进程的内存布局。每个进程所分配的内存由很多部分组成,通常称之为 “段(segment)”。

    文本段:包含了进程运行的程序机器语言指令。文本段具有只读属性,以防止进程通过错误指针意外修改自身指令。因为多个进程可同时运行同一程序,所以又将文本段设为可共享,这样,一份程序代码的拷贝可以映射到所有这些进程的虚拟地址空间中。

    初始化数据段:包含显式初始化的全局变量和静态变量。当程序加载到内存时,从可执行文件中读取这些变量的值。

    未初始化数据段:包含了未进行显式初始化的全局变量和静态变量。程序启动之前,系统将本段内所有内存初始化为 0。出于历史原因,此段常被称为 BSS 段。将全局变量和静态变量分为初始化和未初始化并分开存放,其主要原因在于程序在磁盘上存储时,没有必要为未经初始化的变量分配存储空间。相反,可执行文件只需记录未初始化数据段的位置及所需大小,直到运行时再由程序加载器来分配这一空间。

    栈(stack):是一个动态增长和收缩的段,由栈帧(stack frames)组成。系统会为每个当前调用的函数分配一个栈帧,栈帧中存储了函数的局部变量(所谓自动变量)、实参和返回值。

    堆(heap):是可在运行时为变量动态分配内存的块区域。堆顶端称作 program break。

           下面这张图摘自《The Linux Programming Interface》(【德】 Michael Kerrisk 著)一书的第六章,展示了各种内存段在x86-32 体系结构中的布局。该图的顶部标记为 argv、environ 的空间用来存储程序命令行实参(通过 C 语言中 main() 函数的 argv 参数获得)和进程的环境列表。图中十六进制的地址会因内核配置和程序链接选项差异而有所不同。标灰的区域表示这些范围在进程虚拟地址空间中不可用,也就是说,没有为这些区域创建页表(稍后讨论)。注意到图中右下方的三个箭头了吗?Linux 为 C 语言编程环境提供了 3 个全局符号(symbol):etext、 edata 和 end,可在程序内使用这些符号以获取相应程序文本段、初始化数据段和非初始化数据段结尾处下一字节的地址。使用这些符号,必须显式声明如下:

    extern char etext, edata, end;

           size 命令可显示二进制可执行文件的文本段、初始化数据段、BSS段的大小。

    troy @ workpc 11:32:29:~/work$ size a.out 
       text	   data	    bss	    dec	    hex	filename
       1329	    568	      8	   1905	    771	a.out

           为了更好的说明各个段,我们结合上面的图和下面的这段代码来分析。

    #include <stdio.h>
    #include <string.h>
    #include <stdlib.h>
    
    int a;
    int b = 1;
    static int c;
    static int d = 2;
    
    extern char etext, edata, end;
    
    int main()
    {
        static int e, f = 3;
        int g, h = 4;
        char *p;
    
        p = malloc(20);
        strcpy(p, "hello world");
    
        printf("%p, %p, %p\n", &etext, &edata, &end);
        printf("&a = %p, &b = %p, &c = %p, &d = %p\n", &a, &b, &c, &d);
        printf("&e = %p, &f = %p, &g = %p, &h = %p, p = %p, &p = %p\n",
            &e, &f, &g, &h, p, &p);
        printf("the address of literal string abc is %p\n", "abc");
    
        free(p);
        p = NULL;
    
        return 0;
    }

    打印结果如下:

    troy @ workpc 11:57:09:~/work$ a.out 
    0x4006cd, 0x601054, 0x601068
    &a = 0x601060, &b = 0x601048, &c = 0x601058, &d = 0x60104c
    &e = 0x60105c, &f = 0x601050, &g = 0x7fff3ac00290, &h = 0x7fff3ac00294, p = 0x149f010, &p = 0x7fff3ac00298
    the address of literal string abc is 0x400745

    从打印结果可知:

    1、文本段在地址 0x4006cd 以下,初始化数据段的地址范围是:0x4006cd - 0x601054,BSS段的地址范围是:0x601054 - 0x601068。

    2、变量  a、c、e 确实在 BSS 段内,而 b、d、f 则在初始化数据段内。

    3、p 的值是 0x149f010,也就是由 malloc() 分配的一块内存空间的首地址,这块内存就在堆中。但是 p 本身存放在栈里,紧挨着临时变量 g 和 h。

    4、常量字符串存放在初始化数据段中。

           接下来再重点介绍下 BSS 段和栈。

           BSS 段这个名字是 “Block Started by Symbol”(由符号开始的块)的缩写,它是旧式 IBM 704 汇编程序的一个伪指令,UNIX 借用了这个名字,至今依然沿用。由于 BSS 段只保存没有值的变量,所以事实上它并不需要保存这些变量的映像。运行时所需要的 BSS 段的大小记录在目标文件中,但 BSS 段(不像其他段)并不占据目标文件的任何空间。所以有些人也喜欢把它记作 “Better Save Space”(更有效地节省空间)。有兴趣的同学可以做个实验,在上面的代码基础上定义一个类似 int test[10000] 这样很大的数组,然后编译,看看 a.out 文件的大小;然后把 test 数组初始化,再编译看看 a.out 文件的大小。

           栈这块内存区域最显著的特性就是 “后进先出”,就像快餐店里的餐盘,这些餐盘就是栈里的栈帧(stack frames)。很明显这只是一个形象的类比,实际上栈会更灵活一点。对于一摞餐盘而言,当上面的餐盘没有拿掉的时候,我们无法拿到位于底层的餐盘,但是我们却可以通过一个全局指针来访问位于底层栈帧里的局部变量。计算机有一个专门的SP(Stack Pointer)寄存器,也就是栈指针,用来跟踪记录当前栈顶。每次调用函数的时候,系统就会在栈上新分配一个栈帧,当函数返回时,再从栈上将此帧移去。每个用户空间的栈帧包含如下信息:

    1、函数实参和局部变量。这些变量是在函数被调用时自动创建的,因此在 C 语言中又叫 “自动变量”。函数返回时,由于栈帧会被释放,所以这些自动变量也会被销毁。前面说了 malloc() 分配的内存在堆中,那么通过 alloca() 分配的内存则在栈里。看看下面这个例子,有发现什么问题吗?

    char *get_index_info(int idx)
    {
        char *info[] = {
            "Linus Torvalds",
            "Brian W.Kernighan",
            "Dennis M.Ritchie",
            "Ken Thompson"
        };
    
        if (idx < 0 || idx >= sizeof(info) / sizeof(info[0]))
            return NULL;
    
        return info[idx];
    }

    2、函数调用的链接信息。每个函数都会用到一些 CPU 的寄存器,比如用来存放下一条将要执行的指令的程序计数器(PC)。每当一个函数调用另一个函数时,就会在被调用函数的栈帧中保存这些寄存器信息,以便被调用函数返回时能为调用者恢复这些寄存器,继续往下执行。另外,栈得以让函数能够实现嵌套调用。

           众所周知, goto 可以实现语句的跳转,但是不能跳出当前函数。库函数 setjmp() 和 longjmp() 则可以做到函数间的跳转(两个函数甚至可以来自不同的文件),其正是通过操作栈帧来实现的。感兴趣的同学可以自行查阅其使用方法,这里不做详细介绍。

           想了解更多关于这些段的知识,请查阅《C专家编程》一书的第六章或其他书籍。

    3.9.2 虚拟内存管理

           上述关于进程内存布局的讨论忽略了一个事实:这一布局存在于虚拟内存中。像多数现代内核一样,Linux 也采用了虚拟内存管理技术。该技术利用了大多数程序的一个典型特征,即访问局部性,以求高效使用 CPU 和 物理内存资源。大多数程序都展现了两种类型的局部性:1、空间局部性,是指程序倾向于访问在最近访问过的内存地址附近的内存(由于指令是顺序执行的,且有时会按顺序处理数据结构);2、时间局部性,是指程序倾向于在不久的将来再次访问最近刚访问过的内存地址,比如循环。正是由于访问局部性特征,使得程序即便仅有部分地址空间存在于 RAM 中,依然可能得以执行。

           虚拟内存的规划之一是将每个程序使用的内存切割成小型的、固定大小的 “页” 单元。相应地,将 RAM 划分成一系列与虚存页尺寸相同的页帧。任一时刻,每个程序仅有部分页需要驻留在物理内存页帧中。程序未使用的页拷贝保存在交换区(swap area)内,交换区是磁盘空间中的保留区域。作为计算机 RAM 的补充,交换区的内容仅在需要时才会载入物理内存。若进程欲访问的页面目前并未驻留在物理内存中,将会发生页面错误,内核即刻挂起进程的执行,同时从磁盘中将该页面载入内存。

           为支持这一组织方式,内核需要为每个进程维护一张页表(page table),示意图如下。该页表描述了当前可为进程所用的所有虚拟内存页面的集合。页表中的每个条目要么指出一个虚拟页面在 RAM 中的所在位置,要么表明其当前驻留在磁盘上。在进程虚拟地址空间中,并非所有的地址范围都需要页表条目。通常情况下,由于可能存在大段的虚拟地址空间并未投入使用,故而也无需为其维护相应的页表条目。若进程试图访问的地址并无页表条目与之对应,那么进程将收到一个 SIGSEGV 信号。由于内核能够为进程分配和释放页(和页表条目),所以进程的有效虚拟地址范围在其生命周期中是可以发生变化的。

           虚拟内存的实现需要硬件中分页内存管理单元(PMMU)的支持。PMMU 把要访问的每个虚拟内存地址转换成相应的物理内存地址,当特定虚拟内存地址所对应的页没有驻留于 RAM 中时,将以页面错误通知内核。更多关于 MMU 的硬件知识可以参阅《嵌入式Linux应用开发完全手册》(韦东山 著)一书的第七章,作者以 ARM9 为例,详细介绍了 MMU 的地址映射规则。

           虚拟内存管理使进程的虚拟地址空间与 RAM 物理地址空间隔离开来,这带来许多优点。

           1、进程与进程、进程与内核相互隔离,所以一个进程不能读取或修改另一进程或内核的内存。这是因为每个进程的页表条目指向 RAM(或交换区)中截然不同的物理页面集合。

           2、适当情况下,两个或者更多进程能够共享内存。这是由于内核可以使不同进程的页表条目指向相同的 RAM 页。内存共享常发生于如下两种场景:①. 执行同一程序的多个进程,可共享一份程序代码副本(只读),另外当多个程序执行相同的程序文件或加载相同的共享库时,也会隐式地实现这一类型的共享;②. 在需要进程间通信的时候,进程可以使用 shmget() 和 mmap() 系统调用显式地请求与其他进程共享内存区。

           3、便于实现内存保护机制。我们可以对页表条目进行标记,以表示相关页面内容是可读、可写、可执行亦或是这些保护措施的组合。多个进程共享 RAM 页面时,允许每个进程对内存采取不同的保护措施。例如,一个进程可能以只读方式访问某页面,而另一进程则以读写方式访问同一页面。

           4、程序员和编译器、链接器之类的工具无需关注程序在 RAM 中的物理布局。

           5、程序的加载和运行会更快,因为需要驻留在内存中的仅是程序的一部分。而且,一个进程所占用的内存(即虚拟内存)能够超出 RAM 容量,在进程所需要的内存比物理内存还要大的情况下也可以正常运行。

           6、由于每个进程使用的 RAM 减少了,RAM 中同时可以容纳的进程数量就增多了。这增大了如下事件的概率:在任一时刻,CPU 都可执行至少一个进程,因而往往也会提高 CPU 的利用率。

           关于进程内存布局的话题就此打住了,想了解更多关于内存管理的知识请参阅相关书籍。

           留一个问题:内核空间的内存布局又是什么样的?

    3.10 const

           const 是 constant 的缩写,是恒定不变的意思。我们都知道,被 const 修饰的变量在整个程序运行期间都不允许被修改。

    3.10.1 const VS 常量

           const 推出的初始目的,是为了取代预编译指令,消除它的缺点,同时继承它的优点。缺点就是 #define 定义的宏常量没有类型检查,优点就是宏常量“只读”,整个程序运行期间都不会被修改。const 的只读属性是由编译器来保证的,编译过程中如果发现 const 变量被修改了就会报错。虽然都是不能被修改,但 const 修饰的还是变量,本质上和诸如 1、‘a’、“hello world” 等常量是有区别的。网上很流行的一个例子:

    const int N = 10;
    char name[N];

    用 const 整型变量来作为数组的长度。是否会报错取决于编译器,如果是符合 ANSC C 标准的编译器,这段代码会报错,因为ANSI C 规定数组定义时长度必须是常量。但是 GCC 不会报错,不仅 const 只读变量的情况不会报错,用普通整形变量作为数组长度 GCC 也不会报错。

    3.10.2 const修饰指针

           下面来看看热身运动第三题中的各个指针。

    const int *ptr; /* 指针ptr指向的对象不可变,但是ptr本身可变 */
    int const *ptr; /* 和上面的ptr一样 */
    int * const ptr; /* 指针ptr指向的对象可变,ptr本身不可变 */
    const int * const ptr; /* 指针ptr指向的对象和ptr本身都不可变 */

    Talk is cheap, show me the code:

    int a = 1;
    const int b = 2;
    
    int *p;
    const int *p1;
    int const *p2;
    int * const p3_1 = &a;
    int * const p3_2 = &b;
    const int * const p4_1 = &a;
    const int * const p4_2 = &b;
    
    p = &a;
    p = &b;
    
    p1 = &a;
    p1 = &b;
    
    p2 = &a;
    p2 = &b;

    这段代码可以编译通过,但是会报两个warning:

    test.c: In function ‘main’:
    test.c:31:24: warning: initialization discards ‘const’ qualifier from pointer target type [enabled by default]
         int * const p3_2 = &b;
                            ^
    test.c:36:7: warning: assignment discards ‘const’ qualifier from pointer target type [enabled by default]
         p = &b;
           ^

           四种 const 指针我们逐个来分析。

           p1 和 p2 的赋值语句均没有编译报错,这说明它俩既可以指向 a,也可以指向 b,也就是说 p1 和 p2 本身的值是可以改变的。但是不能通过 p1、p2来改变所指向的变量的值。诸如

    *p1 = 12;
    *p2 = 34;

    这样的代码都会报错:error: assignment of read-only location ‘*p2’。报错的描述很严谨,不是 variable 而是 location,这个位置是只读的,不能进行赋值。

           p3_2 的初始化报错了,说初始化的时候指针目标类型丢弃了 const 修饰词。p3_2 指向的是 int 型变量,而不是 const int,而 b 是 const int,所以赋值的时候丢弃了 b 的 const 属性。编译器允许 int 和 const int 之间互相转换,只是 int 转化成 const int 不会报错(如 p1 = &a),const int 转化成 int 会报 warning(如上面两个警告)。稍后我们试图分析为什么可以转换的原因。

    如果之后再试图让 p3_2 指向 a:

    p3_2 = &a;

    则会报错:error: assignment of read-only variable ‘p3_2’。

    但是如果我们加上这样的语句:

    *p3_2 = 10;

    然后再把 b 的值打印出来,发现 b 变成10,也就是说通过 p3_2 改变了具有 const 属性的变量 b!C 语言的设计之初的哲学之一就是程序员对代码唯一负责,不要试图让编译器替你做很多工作。之所以在 p3_2 初始化的时候只是报 warning 而不是 error,是因为编译器的实现者认为我们只是想用到 b 的地址,而不会试图通过指针去改变它,否则你为什么在定义之初还要为 b 加上 const 属性呢?

           前面说了 p1 = &a; 不会报错,那么 const int * const ptr4_1 = &a; 当然也不会报错了。但是不要试图让 p4_1 再次指向别的地方,也不要试图通过 p4_1 去改变 a。

    3.10.3 const与函数

           const 另一个重要应用场景就是和函数有关,包括修饰函数参数和函数返回值。当不希望函数体修改传过去的参数变量时,可以在定义该函数的时候指定这个传参为 const 类型,这样就可以防止一些有意无意的修改。C 库中很多函数都用到了这一技巧,比如:

    char *strcat(char *dest, const char *src);
    char *strcpy(char* dest, const char *src);
    int strcmp(const char *s1, const char *s2);

    strcat() 函数是在第一个字符串的末尾处添加第二个字符串的一份拷贝,很明显我们不会改变第二个指针,但是第一个会被改变,函数声明也正体现了这一点。

           const 也可以修饰函数的返回值,比如:

    const char *foo(char *p, int i);

    3.10.4 const变量到底存放在哪里?

    int i;
    int j = 10;
    const int k;
    const int l = 10;
    
    int main()
    {
        int m, n = 11;
        const int o, p = 10;
    
        printf("%p, %p, %p\n", &etext, &edata, &end);
        printf("%p, %p, %p, %p\n", &i, &j, &k, &l);
        printf("%p, %p, %p, %p\n", &m, &n, &o, &p);
    
        return 0;
    }

    打印结果如下:

    0x40067d, 0x601044, 0x601050
    0x601048, 0x601040, 0x60104c, 0x4006c4
    0x7ffff8bf70a0, 0x7ffff8bf70a4, 0x7ffff8bf70a8, 0x7ffff8bf70ac

    可以看到,const 限定符并不会改变变量的存储段。但是 l 的地址位置有点变化,j 还是在初始化数据段的高地址处,但是 l 却在初始化数据段的低地址处,靠近文本段的顶部。不知道读者有没有注意到,前面我们在打印各个段的变量时顺便把常量字符串也打印出来了,它的地址也是在初始化数据段的低地址处。所以编译器把全局的初始化了的 const 变量放在了常量地址块(仍然在初始化数据段内)。为了进一步验证这个结论,我们把上面代码的第二个 printf 改一改:

    printf("%p, %p, %p, %p, %p, %p\n", &i, &j, &k, "abc", &l, "def");

    打印结果如下:

    troy @ workpc 14:33:48:~/work$ a.out 
    0x40068d, 0x601044, 0x601050
    0x601048, 0x601040, 0x60104c, 0x4006e4, 0x4006d4, 0x400700
    0x7fff0c557a90, 0x7fff0c557a94, 0x7fff0c557a98, 0x7fff0c557a9c

    进一步,我们在 main 函数里再定义一个变量:

    const static int q = 12;

    发现 q 的地址为 0x400718。这就说明,全局或者静态且已经初始化了的 const 变量被放在了和常量字符串一样的位置,但仍然在初始化数据段内,const 修饰其它变量不会改变变量的存储段。

           最后,根据上面实际调试的 const 变量位置,我们知道它和普通变量存放并没有特别之处,所以 const 变量在整个程序运行期间不能改变这一点是由编译器来保证的。

    3.11 volatile

           volatile,中文意思是易变的、不稳定的,这个关键字主要是为了编译器而设计的。这就得从编译器的代码优化功能说起。请看下面的代码:

    int x = 1;
    int val1, val2;
    
    val1 = x;
    
    /* 其他没有使用x的代码 */
    ......
    
    val2 = x;

    一个聪明的优化器可能意识到你的这段代码使用了两次 x,并且前后都没有改变它的值。这个时候编译器便会 “自作主张”,把变量 x 的值临时存储在寄存器里。当 val2 需要 x 的时候,就可以直接从寄存器里取出 x 的值,而不用再次访问内存来读出 x 的值。这个过程就被称为缓存(caching)。很明显这的确提高了代码执行效率,因为编译器不会生成汇编代码重新从内存里取 x 的值。

           但是很遗憾,并不是每次我们都需要这种优化,特别是在和硬件打交道的驱动代码里。做过底层驱动开发的同学都知道,驱动代码里有些变量是用来保存一些硬件设备的状态信息的,比如用来记录当前设备是否在充电、是否充满了等等这些信息,这些信息最原始的值就保存在硬件设备的寄存器里。很显然,在软件代码层面我们不会去主动修改这些保存了硬件状态信息的变量,这就给了编译器优化的用武之地。稍不注意,你用到的这些状态信息可能就是从缓存里拿出来的。有的同学可能要说了,我平时写的驱动代码这些变量也没有特别地去加 volatile 修饰词,也没有碰到过问题啊。那是因为每次要用到这些信息的时候你的代码都会主动的更新变量,即重新去读寄存器,这就相当于给了变量 volatile 的属性。本人曾经就碰到过一个关于 volatile 的问题,调试的是龙讯一颗HDMI switch(3 IN - 1 OUT)芯片。这是一颗用 IIC 通信的芯片,当时驱动代码使用内核的 regmap 框架来实现 IIC 通信。使用 regmap 框架需要填充 struct regmap_config 这么一个结构体,其中有个 bool (*volatile_reg)(struct device *dev, unsigned int reg); 的回调指针需要实现,用来指出芯片的哪些寄存器值是 volatile 的。这个回调的注释信息如下:

     * @volatile_reg: Optional callback returning true if the register
     *          value can't be cached. If this field is NULL but
     *          volatile_table (see below) is not, the check is performed on
     *                such table (a register is volatile if it belongs to one of
     *                the ranges specified by volatile_table).

    如果寄存器值不能被缓存,那你就需要返回 true。如果没有实现这个回调,那么 regmap core 就会使用 volatile table(如果有的话)里的寄存器信息。当时我既没有实现这个回调也没有填充这个 table,导致我在获取当前哪个 port 口有HDMI信号时一直无法准确获取到。后来用芯片原厂的 PC 端工具去查看的时候,发现寄存器实际上是有被更新到的。问题就出在 regmap core!它扮演了编译器的角色,优化了这个寄存器的值,当我使用 regmap_read 接口去读的时候 core 层实际上并未发起真正的 IIC 通信,而只是从缓存里拿出来给我了。

           优化显然是提高了代码执行效率,但是编译器并不知道我们什么时候需要这个优化,什么时候不需要这个优化。因此才有了 volatile 关键字。如果对变量没有使用 volatile 修饰词,那么编译器就会试着去优化代码;相反,如果对变量使用了 volatile,那么每次在用到这个变量时,编译器就会谨慎地重新从原始地址处(可能是内存,也可能是寄存器)读取这个变量的值。volatile 变量一般会用在以下几种情况:1、硬件设备的寄存器;2、中断函数可能会访问到的全局或静态变量;3、多线程编程中被几个任务共享的变量。

           以下定义语句有问题吗?

    volatile const int time;
    volatile int *p1;
    int * volatile p2;

    3.12 typedef

           typedef 关键字是给一个已经存在的 数据类型(注意:是类型不是变量)取一个别名,而非定义一个新的数据类型。在实际使用中,我们常常将一个结构体数据类型 typedef 成新的名字,比如:

    typedef struct city {
        char name[20];
        int position;
        unsigned int area;
        int people_count;
        strcut town tn;
        ......
    } City, *City_ptr;

    这样定义以后,

    struct city c1;
    City c1; 
    
    struct city *c2;
    City *c2;
    City_ptr c2;

    c1 的两种定义和 c2 的三种定义就没有区别了。再来看看下面的定义:

    const City_ptr c3;
    City_ptr const c4;

    c3 和 c4 是一样的类型吗?

    3.12.1 typedef VS #define

           1、请看下面的定义语句:

    #define INT32 int
    unsigned INT32 i = 10;
    
    typedef int INT32;
    unsigned INT32 j = 10;

    变量 i 的定义语句没问题,这很好理解。但是 j 的定义语句却会报错,因为用 typedef 取的别名不支持这种类型扩展,所以去掉 unsigned 才会合法。

           2、再请看下面两个 typedef:

    typedef static int sint;
    typedef const int cint;

    第一个 typedef 也是非法,编译报错error: multiple storage classes in declaration specifiers,声明说明符中有多个存储类型。所谓存储类说的变量的存放位置以及其生命周期。typedef 本身是一种存储类的关键字,与 auto、extern、static、register 等关键字不能出现在同一条语句中。第二个 typedef 是合法的。以下引用摘自《C Primer Plus》(Fifth Edition)第十二章,更多关于存储类的相关知识可以直接查阅该书。

    C语言中有 5 个作为存储类说明符的关键字,它们是 auto、register、static、extern 以及 typedef。关键字 typedef 与内存存储无关,由于语法原因被归入此类。特别地,不可以在一个声明中使用一个以上存储类说明符,这意味着不能将其他任一存储类说明符作为 typedef 的一部分。 

           3、再来看看下面的定义语句:

    #define PCHAR char*
    PCHAR p1, p2;
    
    typedef char* pchar;
    pchar p3, p4;

    两组定义语句编译都没有问题,但是,这里的 p2 并不是 char 指针类型,而是一个 char 类型。这种错误很容易被忽略,所以这样用 #define 的时候要慎之又慎。关于 #define 还有很多内容值得研究,后面预处理一章会继续讨论。p3 和 p4 都是指针变量,受此启发,可以知道上面定义的 c3 和 c4 变量是一样的,const 修饰的都是指针本身。对于编译器来说,只认为 City_ptr 是一个类型名。

           不知道大家有没有留意到,其实上面所有关于 typedef 的代码可以说都是多余的,或者说用比不用并没有表现出什么优势,即使是我们说的给结构体取别名。事实上,Linux 内核开发者们强烈反对使用 typedef,理由是:

    1、typedef 掩盖了数据的真实类型,很容易因此而犯错误;

    2、使用 typedef 往往是因为想偷懒。有些程序员往往为了少敲几个字母而使用 typedef,比如 typedef unsigned char uchar。

    当然 typedef 也有它施展身手的时候,当需要隐藏变量与体系结构相关的实现细节的时候,当某种类型将来有可能发生变化,而现有程序必须要考虑到向前兼容问题的时候,都需要 typedef。使用 typedef 要谨慎,只有在确实需要的时候再用它,如果仅仅是为了少敲打几下键盘,别使用它。

    3.13 void

           1、GNU C 中 void 指针变量和其他类型的指针变量可以直接相互赋值,而不用指定强转类型;

           2、GNU C 中允许对 void 指针进行自增自减操作,步进值是1;

           3、函数没有返回值时要声明为 void 类型,缺省时返回的是 int 型。

     

     

     

    鸣谢单位(排名不分先后):

    1、《C语言深度剖析》,作者:陈正冲,石虎;

    2、《C Traps and Pitfalls》(C缺陷与陷阱),作者:Andrew Koenig;

    3、《Expert C Programming:Deep C Secrets》(C专家编程),作者:Peter van der Linden;

    4、《The Linux Programming Interface》(Linux/UNIX 系统编程手册),作者:Michael Kerrisk;

    5、《C Primer Plus》(Fifth Edition),作者:Stephen Prata。

    展开全文
  • C语言易混易错知识

    千次阅读 2015-06-12 09:13:59
    1、声明可变长数组 Int a[*]; // 可变长数组不是动态的,可以变化的是数组大小 这样的写法不能用在全局或者共用体里 2、字符常量 字符常量只能用单引号括起来,不能用双引号。 字符常量只能是单个字符,不能是...

    1、声明可变长数组

    Int a[*];   // 可变长数组不是动态的,可以变化的是数组大小

    这样的写法不能用在全局或者共用体里

    2、字符常量

    字符常量只能用单引号括起来,不能用双引号。

    字符常量只能是单个字符,不能是字符串。

    字符串常量长度应加上\0

    3 非整形变量比较

    例如:float  a=1.0,b=1.0;

    if(a==b)//这句话是错的,因为计算机内部表示浮点数a=1.00003;

    正确方法为|a-b|<1e-6;

    4 %运算符

    如果一个操作数不为整数,编译器将无法通过

    5 int 与char类型的转换

    必须在ACSII范围内,即0-255,不然会出错

    6 register修饰的变量

    含register修饰的变量,能提高运行速度,但不能对register修饰的变量取地址&

    7 编译预处理

    每条指令必须单独占一行;每行末尾不能加分号

    8 空指针和野指针

    空指针指的是未指向任何数据单元的指针,一般原因为访问NULL指向的单元的后果

    野指针指的是指向不明的单元的指针,一般原因为free掉的指针,未初始化的指针,从函数返回的局部变量指针,对指针进行的数学运算不当。

    9.指针处理

    (1)在给指针赋值的时候,得注意指针的类型。

    eg: char a;   char *pa;  pa=&a;

           char  c[4];   char (*pb)[4];    pb=&c;

    (2)强制类型转换时,得注意大小端格式。

    int s;
    int *a;
    a=&s;
    s=0x11223344;

    如果*(char *)a 等于0x44则为小端格式,否则为大端格式。

    (3)字符串

    char s[20[="hello world!";      //可修改

    char *s="hello world!"  //不可修改,只读。

    下面的情况则可以:

    char s[20]="hello world!';

    char *ps;

    ps=s;//则可以通过ps对字符串修改了。

    (4)数组名

    不能自增,但通过函数传递,变为指针变量时,就可以了,函数传递之后,指向地址相同,但指针所表示的内容不同了

    10、scanf()函数

    scanf("%d,%d",&a,&b)输入时必须为3,4这样不能为3 4

    scanf("%d%d",&a,&b)此时输入为3 4,不能为3,4

    scanf("%*s",s)* 是scanf函数中的一种修饰符, 表示输入项输入后不转送给任何变量,%*s一起表示,

    跳过当前输入字符串,指向下一个。

    展开全文
  • C语言易错点

    2019-01-04 16:51:51
    重新将C语言的知识看了一遍,把自己遗忘以及容易出错的知识记录下来。各个知识之间没有什么联系。   定义变量时,我们使用了诸如"a" "abc" "mn12"这样的名字,它们都是...

    重新将C语言的知识点看了一遍,把自己遗忘以及容易出错的知识点记录下来。各个知识点之间没有什么联系。

     

    • 定义变量时,我们使用了诸如"a" "abc" "mn12"这样的名字,它们都是程序员自己起的,一般能够表达出变量的作用,这叫做标识符(Identifier)。标识符只能由字母(A-Z,a-z)、数字(0-9)和下划线(_)组成,并且第一个字符必须是字符或下划线。
    • 使用标识符的注意事项:

    1. C语言虽然不限制标识符的长度,但是它受到不同编译器的限制,同时也受到具体机器的限制。例如在某个编译器中规定标识符前128位有效,当两个标识符前128位相同时,则认为是同一个标识符。

    2. C语言的标识符区分大小写。

     

    • 小数的合法写法:C语言小数点两边有一个是零的话,可以不用写。例如:

    1.0在C语言中可写成1.
    0.1在C语言中可写成.1。

     

    • 复合的赋值表达式:

    int a = 2;
    a *= 2 + 3; //运行完后,a的值是10
    一定要注意,首先要在"2+3"的上面打上括号,变成(2+3)再运算。

     

    • 区别:

    z = (2, 3, 4); // z = 4
    z = 2, 3, 4;  // z = 2
    要知道"="运算符的优先级大于","运算符,而逗号运算符是返回最后一个值的。(2,3,4)的结果为4,然后再把4赋值给z。而式子2则是先执行z=2,然后返回4,但这个返回值没有赋给任何变量。


     

    • 按不同格式输出

    int x = 017;

    printf("%d", x);  //输出15

    printf("%o", x);  //输出17

    printf("%#o", x); //输出017

    printf("%x", x);  //输出11

    printf("%#x", x); //输出0x11

     

    int x = 12, y = 34;

    char z = 'a';

    printf("%d ", x, y); //一个格式说明,两个输出变量,后面的y不输出

    printf("%c", z);





    格式说明     表示内容
    %d             int
    %ld             long
    %f             float
    %lf             double
    %%             输出一个百分号
    %c             char
    %s             字符串
    %o             八进制
    %#o         带前导的八进制
    %x             十六进制
    %#x         带前导的十六进制


    scanf("%d%d%*d%d", &a, &b, &c); //跳过输入的第三个数据

     

    • 二维数组的初始化

    int a[2][3]={1,2,3,4,5,6};
    int a[2][3]={1,2,3,4,5,};  //合法,后面一个默认为0
    int a[2][3]={{1,2,3},{4,5,6}}; //合法,每行三个
    int a[2][3]={{1,2,},{4,5,6}};  //合法,第一行最后一个默认为0
    int a[2][3]={1,2,3,4,5,6,7};  //不合法,赋值个数多于数组的个数了
    int a[][3]={1,2,3,4,5,6};     //合法,可以缺省行的个数
    int a[2][]={1,2,3,4,5,6};     //不合法,不可以缺省列的个数
     

    • 数组与指针

    a[2]变成*(a+2)  a[2][3]变成*(a+2)[3],然后再可以变成*(*(a+2)+3)



     

    • C语言中没有字符串变量,所以用数组和指针存放字符串:

    1. char ch[10]={"abcd"};

    2. char ch[10]="abcd";

    3. char ch[10]={"a","b","c","d"};

    4. char *p="abcd";

    5. char *p;

             p="abcd";

    6. char ch[10]; //错了,数组名不可以赋值

        ch="abcd";

    7. char *p={"abcd"};  //错了,不能够出现大括号



     

    • 把s指针中的字符串复制到t指针中的方法

    1. while( (*t=*s)!= null ){s++; t++;} //完整版本

    2. while( *t=*s ){s++; t++; } //简单版本

    3. while( *t++=*s++ ); //高级版本



     

    • typedef是取别名,不会产生新类型,它同时也是关键字

    1. typedef int qq; 那么 int x 就可以写成 qq x

    2. typedef int *qq; 那么 int *x 就可以写成 qq x


     

    • 关于static

    static int x; //默认值为0

    int x;   //默认值为不定值


     

    • 使用共用体的注意事项:

    1. 每个成员的起始地址都是相同的,所以一次只能存放一种类型的成员,也就是每次只能有一个成员在起作用。

    2. 在存入一个新的成员信息后,原有的成员信息就会被覆盖而失去作用。

    3. 整个共用体型的起始地址与各成员的起始地址是同一地址。

    4. 关于共用体类型的变量,不能为其赋值,不能定义共用体变量时对其初始化,也不能引用共用体变量名得到一个值。

    5. 共用体类型变量不可以作为函数的参数传递,也不可以作为函数返回值,但是可以使用指向共用体的指针。

    6. 共用体可以出现在机构体类型的定义中,也可以定义共用体类型的数组,反之也成立。



     

    • 动态内存与静态内存在分配方式上的区别:

    1. 静态内存的分配是在程序开始编译时完成的,不占用CPU资源;而动态内存的分配是在程序运行时完成的,动态内存的分配与释放都是占用CPU资源;

    2. 静态内存是在栈上分配的;而动态内存是在堆上分配的。

    3. 动态内存分配需要指针和引用数据类型的支持,而静态内存不需要。

    4. 静态内存分配是在编译前就已经确定了内存块的大小,属于按计划分配内存;而动态内存分配是在程序运行过程中,按需分配。

    5. 静态内存的控制权交给编译器,动态内存的控制权交给程序员。

    展开全文
  • c语言易错点

    2020-05-13 22:08:45
    1.feof 2.strncat 3.*a[10] 4.const a = 10; a = 9 5.队列,栈空间 6.信号量回调函数 7.strncpy带不带\0 8.if else匹配 9.unsigned long long -1u
  • C语言易错点经典分析

    2020-07-30 23:30:44
    详细讲解了C语言中易错易混,能够让你在开发中避免很多错误,同时也能考察自己的C语言水平的必备试题,更是面试过程中不可或缺的面试题目
  • C语言易错点

    2019-06-21 22:22:03
    C语言易错点 1.每个C语言程序中main函数是有且只有一个的。 2.算法可以没有输入,但必须要有输出。 3.在函数中不可以再定义函数。 4.break可用于循环结构和switch语句。 5.break和continue的区别在于前者是跳出...
  • 本帖主要汇总我在学习C语言过程中自己出现的一些错误,踩的一些雷区,希望你看完之后能少犯一些错误 ヾ(๑╹◡╹)ノ" 标识符规范: 标识符必须由字母,下划线或数字组成。 标识符必须以字母或下划线开头。 ...
  • C语言易错点:字符串的连接标签:C语言 字符串by 小威威利用字符数组对字符串的连接,有一点需要注意,不要忘了最后加’\0’,不要忘了在最后加’\0’,不要忘了在最后加’\0’。还有一个,字符数组的长度要记得包含...
  • ## 菜鸟学习c语言易错点 近期刚开始学习c语言,发现实践非常重要,在不断的改错中,将错误不断减少;多敲一敲才发现自己在一次次进步,小小的细节也可能酿成大错; 1, 在数的乘除上:比如1/2并非等于0.5;而等于0...
  • C语言易错点随笔

    2018-04-28 14:26:12
    0&lt;=x&lt;100 表达式合理不会报错,从左到右依次判断,真1假0,判断一个赋一个值。 x+1=x+1 等式非法,左边不能为表达式。 char a[6] = {97,98,99,100,101,’\0’}; 因储存整数,不能加’ ’ ,输出的是...
  • C语言易错点解析(一)

    千次阅读 2014-12-02 18:46:11
    #include "stdio.h" int main() { int a,b,c,d; printf("第一次输入:"); scanf("%d%d",&a,&b); printf("%d%d",a,b); printf("第二次输入:"); scanf("%d,%d",&c,&d); printf("%d%d",c,d);... return 0
  • C语言 易错题整理

    2019-04-06 21:31:02
    #include<stdio.h> #include<stdlib.h> #include<math.h> #include<string.h> int main() { //方阵型 int i,j; for(i=1;i<10;i++){ for(j=1;...
  • C语言中指针的一些易错点

    千次阅读 2017-07-19 19:20:51
    1、字符数组与字符串的使用 注意:数组名是指向数组首个元素的地址。 先看个例子: #include int main() { char *str = "hello"; int len = strlen(str); int i; //直接输出字符串 ...
  • C语言易错的选择题

    2020-04-10 09:53:26
    1.若有定义:int a,b.c;以下选项中的赋值语句正确的是(A) A)a=(b=c)+1; B(a=b)=c=1: C)a=(b==c)=1; ...解析:赋值运算结合性为由右向左结合,赋值运算符左值为变量,右值为变量或常量,且左右两边数据...
  • 拷贝构造 浅赋值zhi
1 2 3 4 5 ... 20
收藏数 2,156
精华内容 862
关键字:

c语言易错点