《C和指针》摘抄整理型笔记

来源:互联网 发布:大数据项目架构设计 编辑:程序博客网 时间:2024/06/06 17:24

/*

读书笔记类型多种多样,有摘抄型的,有感想型的,有批注型的,有摘要型的……

现在正在研读《C和指针》,其中有很多我认为必要的知识点,摘抄并简单整理了一下,记录在这里。

*/

1. 数值是以引用(reference)形式进行传递,也就是传址调用;标量和常量按值(value)传递。所有传递给函数的参数都是按值传递的,但是,数组名作为参数时就会产生按引用传递的效果。

/*个人观点:数组其实是指针应用的一个特例,所以编译器没有必要去检查数组下标是否越界,当然,这也提高了编译的效率。*/

2. 不良的风格和不良的文档是软件生产和维护代价高昂的两个重要原因。在函数定义中,返回类型出现在独立一行中,而函数名则出现在下一行起始处,这样在寻找函数定义时,你可以在一行的开头处找到函数的名字。

/*作者的经验,可以参考,但未必符合每个人的习惯*/

3. 数组使用方面的一个良好经验法则:如果下标值是从那些已知是正确的值计算得来,那么就无需检查它的值,如果一个用作下标的值是根据某种方法从用户输入的数据产生而来的,那么在使用它之前必须进行检测,确保它们位于有效范围之内。

4. 容易“被愚弄”的地方:

1)声明指针:

例如:int* b, c, d;

人们很自然地以为这条语句把所有三个变量都声明为指向整型的指针,但事实上星号是表达式*b的一部分,只对这个标识符有用。b是一个指针,其余两个变量c、d只是普通的整型。要声明三个指针,正确的语句是:

int *b, *c, *d;

2)声明指针并赋值:

例如: char *message = “Hello world”;

这条语句是指message声明为一个指向字符的指针,并用字符串常量中的第一个字符的地址对该指针进行初始化。这样的声明很容易引起误解,看上去好像初始值是赋给了表达式*message,事实上它是赋给了message本身。上面的声明相当于:

char *message;

message = “Hello world”;

5. 创建新的类型名,宜使用typedef而不用#define,因为后者无法正确处理指针类型,如:

#define d_ptr_to_char char *

d_ptr_to_char a, b;

正确地声明了a,但b却被声明为一个字符。如果使用typedef就会避免这个问题。

6. 常量与指针在一块使用的情况:

int const *pc;  //是一个指向整型常量的指针,可修改指针的值,但不能修改它所指向的值。

int *const cp;  //是一个指向整型的常量指什,此时,指针是常量,它的值无法修改,但可以修改它所指向的整型的值。

int const *const ccp; //无论指针本身还是它所指向的值,都是常量均不允许修改。

7. 对于auto变量,除了声明为const的变量外,在声明变量的同时进行初始化和声明后赋值只有风格之差,并无效率之别。

8. 当static用于函数定义时,或用于代码块之外的变量声明时,static关键字用于修改标识符的链接属性,从external改为internal,但标识符的存储类型和作用域不受影响,用这种方式声明的函数或变量只能在声明它们的源文件中访问。

当用于代码内部的变量声明时,static关键字用于修改变量的存储类型,从自动变量修改为静态变量,但变量的链接属性和作用域不受影响。用这种方式声明的变量在程序执行前创建,并在程序的整个执行期间一直存在,而不是每次在代码块开始执行时创建,在代码执行完毕后销毁。

无标题.jpg

9. for语句和while语句执行过程的区别在于出现continue语句时。在for语句中,continue语句跳过循环体的剩余部分,直接回到调整部分;在while语句中,调整部分是循环体的一部分,所以continue语句将会把它也跳过。

for循环有一个风格上的优势,它把所有用于控制循环的表达式收集在一起,放在同一处,便于寻找。

10. 跳出多层嵌套循环的方法:

1)使用goto语句,因为break语句只影响包围它的最内层循环体;

2)当希望退出所有循环时设置一个状态标志,但这个标志在每个循环中都必须进行测试;

3)把所有循环放在一个单独的函数中,当要从最内层循环跳出时,使用return语句离开这个函数。

 

11. 位操作技巧:

value |= 1 << bit_number;    /*指定位置1*/

value &= ~( 1 << bit_number ); /*指定位清0*/

12. 知识点1:

if(a>5)

  b = 3;

else

  b = -20;
    可以写成: b = a > 5 ? 3 : -20;

知识点2:

sizeof(a)返回的a的长度是字节数。

/*这里记录的两个知识点是最最基础的C语言点,之所以记录在这里,是因为自己的基础知识不牢固,在某次面试时被这些基础知识所虐。记录于此,起个警示作用吧*/

13. 变量的值就是分配给该变量的内存位置所存储的数值,即使是指针变量也不例外。

14. 所谓左值与右值——左值是那些能够出现在赋值符号左边的东西,右值是那些可以出现在赋值符号右边的东西。

15. 把整型值25存放在地址100的位置中:

*(int *)100 = 25;

首先将100强制转换成“指向整型的指针”类型,再进行间接访问。

16. 实际上,绝大多数编译器都不会检查指针表达式的结果是否位于合法边界之内。因此,程序员应该负起责任,确保这一点。类似,编译器将不会阻止你取一个标量变量的地址并对它执行指针运算,即使它无法预测运算结果所产生的指针指向哪个变量。越界指针和指向未知值的指针是两个常见的错误根源。当你使用指针运算时,必须非常小心,确信运算的结果将指向有意义的东西。

17. 声明一个指针变量并不会自动分配任何内存。在对指针执行间接访问前,指针必须进行初始化;或者使它指向现有的内存,或者给它分配动态内存。对未初始化的指针变量执行间接访问操作是非法的,而且这种错误常常难以检测。其结果常常是一个不相关的值被修改。这种错误是很难被调试发现的。

18. 使用函数原型最方便(且最安全)的方式是把原型置于一个单独的文件,当其他源文件需要这个函数的原型时,就使用#include指令包含这个文件。

19. 在C中,在几乎所有使用数组名的表达式中,数组名的值是一个指针常量,也就是数组第一个元素的地址。请不要根据这个事实得出数组和指针相同的结论。只有当数组名在表达式中使用时,编译器才会为它产生一个指针常量。

20. 只有两种场合下,数组名并不用指针常量来表示——就是当数组名作为sizeof操作符或单目操作符&的操作数时。

21. 你不能使用赋值符号把一个数组的所有元素复制到另一个数组,必须使用一个循环,每次复制一个元素。

22. 定义一个数组,比如 int array[10];

表达式2[array],是合法的!编译器会把它转换成对等的间接访问表达式*(2 + array)。2[array]与*(2 + array)、array[2]是无差别的。但在实际使用中,绝不应该写2[array],因为它会大大影响程序的可读性。

23. 数组下标表达式a[2]和指针表达式*(a + 2)可以互换。在可读性方面,下标有一定的优势,尤其是多维数组中,但下标绝不会比指针更有效率。而指针有时会比下标更有效率。

24. 指针比下标更有效率,前提是它们被正确地使用。不要为了效率上的细微差别而牺牲可读性,这点非常重要。

25. 函数原型中的一维数组形参无需写明它的元素数目,因为函数并不为数组参数分配内存空间。形参只是一个指针,它指向的是已经在其他地方分配好内存的空间。这也解释了为什么数组形参可以与任何长度的数组匹配——它实际传递的只是指向数组第一个元素的指针。另一方面,这种实现方法使函数无法知道数组的长度,如果需要知道数组的长度,它必须作为一个显式的参数传递给函数。

26. sizeof的用途,它用于对数组中的元素进行自动计数。如,定义了array[ ],sizeof(array)的结果是整个数组所占用的字节数,而sizeof(array[0])的结果则是数组每个元素所占用的字节数。两个值相除,结果就是数组元素的个数。

27. 头文件string.h包含了使用字符串函数所需的原型和声明。尽管并非必需,但在程序中包含这个头文件确实是个好主意,因为有了它所包含的原型,编译器可以更好地为你的程序执行错误检查。

28. 标准库函数有时是用汇编语言实现的,目的就是为了充分利用某些机器所提供的特殊字符串操作指令,从而追求最大限度的速度,即使在没有这类指令的机器上,你最好还是把更多的时间花在程序其他部分的算法改进上。寻找一种更好的算法比改良一种差劲的算法更有效率,复用已经存在的软件比重新开发一个更有效率。

29. memxxx函数提供了类似字符串函数的功能,但它们可以处理包括NUL字节在内的任意字节。这些函数都接受一个长度参数。
   memcpy从源函数向目标函数复制长度参数指定的字节数。
   memmove函数执行与memcpy相同的功能,但它能正确处理源参数与目标参数出现重叠的情况。
   memcmp函数比较两序列的字节。
   memchr函数在一个字节序列中查找一个特定的值。
   memset函数把一序列字节初始化为一个特定的值。
30. 结构成员的访问。直接访问用点操作符(.),左操作数就是结构变量的名字,右操作数就是需要访问的成员的名字;间接访问用箭头操作符(->),左操作数必须是一个指向结构的指针,右操作数同点操作符。
31. 结构自引用:
   struct SELF_REF1
   {
      int a;
      struct SELF_REF1 b;
      int c;
};
这种类型的自引用是非法的。这种声明进行下去,就像一个永远不会终止的递归程序。
合法的声明如下:
   struct SELF_REF2
   {
      int a;
      struct SELF_REF2 *b;
      int c;
   };
b是一个指针,而不是一个结构。事实上它所指向的是同一种类型的不同结构,可以用来实现更高级的数据结构,如链表和树。
32. 警惕下面的陷阱:
   typedef struct
   {
      int a;
      SELF_REF3 *b;
      int c;
   } SELF_REF3;
这个声明的目的是为了这个结构创建类型名为SELF_REF3. 但是它失败了,因为类型名直到声明的末尾才定义,所以在结构声明的内部,它尚未定义。
解决方案:
   typedef struct SELF_REF3_TAG
   {
      int a;
      struct SELF_REF3_TAG *b;
      int c;
}SELF_REF3;

33. 对指针进行解引用操作之前,检查一下它是否有效是非常重要的。(所谓“解引用”,即“*”操作,英文为dereference,其实意思就是取指针指向的地址的内容。)

34. 要确定结构中某个成员的实际位置,应考虑边界对齐因素,可以使用offsetof宏(定义于stddef.h)。

   offsetof(type, member)

type是结构的类型,member是所需要的那个成员表达式的结果是一个size_t值,表示这个指定成员开始存储的位置距离结构开始位置偏移了几个字节。

35. 向函数传递指针的缺陷在于函数现在可以对调用程序的结构变量进行修改。如果我们不希望如此,可以在函数中使用const关键字来防止这类修改。

36. 位段的声明和结构类似,但它的成员是一个或多个位的字段,这些不同长度的字段实际上存储于一个或多个整型变量中。

37. 位段声明与任何普通结构成员的不同点有两个,一是位段成员必须声明为int、signed int或unsigned int类型,其次是在成员名后面是一个冒号和一个整数,这个整数指定该位所占用的位的数目。

38. 注重可移植性的程序应该避免使用位段。注意下面这些:

①int位段被当作有符号数还是无符号数。

②位段中位的最大数目。许多编译器把位段成员的长度限制在一个整型值的长度之内,所以一个能够运行于32位整数的机器上的位段声明可能在16位整数的机器上无法运行。

③位段中的成员在内存中是从左右分配的还是从右向左分配的。

④当一个声明指定了两位段,第2个位段比较大,无法容纳于第1个位段剩余的位时,编译器有可能把第2位位段放在内存的下一个字,也可能直接放在第1个位段后面,从而在两个内存位置上形成重叠。

39. 位段提供的唯一优点是简化源代码。这个优点必须与位段的可移植性弱的缺点进行权衡。

40. 联合的所有成员引用的是内存中的相同位置,当你想在不同的时刻把不同的东西存储于同一位置,就可以使用联合。

41. 在一个成员长度不同的联合里,分配给联合的内存数量取决于它的最长成员长度。这样,联合的长度总是足以容纳它的最大成员,如果这些成员长度相差悬殊,当存储长度较短的成员时,浪费空间是相当可观的,在这种情况下,更好的方法是在联合中存储指向不同成员指针而不是直接存储成员本身。

42. 联合变量可以被初始化,但这个初始值必须是联合第一个成员的类型,而且它必须位于一对花括号里面。

/*

博主观点:

这一篇笔记内容较少,全部是关于union的,算是关于联合的专题笔记了,呵

最早对union有深入的认识,是在学习TI TMS320F2812 DSP的时候,在那之前仅仅对union有教科书上的认识。TI的官方例程,把对2812的寄存器的访问定义一个位段,可以按位访问,然后再定义一个union,这样既可以整体访问也可以按位访问。当时觉得union原来是这样使用的,非常非常棒。

再后来接触MISRA-C 2004标准,其中18.4规定,“不要使用联合”,而且这是一条强制规则,说白了就是“禁止使用联合”。MISRA-C是应用于汽车行业的C语言规范,对安全性的要求是放在第一位的,所以对union这种重用内存的行为可能导致的安全隐患不得不慎重。所以干脆就禁止使用了,当然这是我的猜测。

*/

MISRA-C规范可以参考:http://wenku.baidu.com/view/c6af1d51964bcf84b8d57b29.html?re=view

43. 动态分配内存函数

   void *malloc( size_t size );

malloc所分配的是一块连续的内存。

malloc的参数就是需要分配的内存字节(字符)数,其类型为size_t,定义于stdlib.h之中——

typedef unsigned int size_t;

malloc返回一个指向被分配的内存块起始位置的指针,该指针为void *类型的指针。一个void *类型指针可以转换为其他任何类型指针。对于要求边界对齐的机器,malloc所返回的内存的起始位置,将始终能满足对边界对齐要求最严格的类型的要求。如果没有足够的内存,malloc返回一个NULL指针,因此对每个从malloc返回的指针进行检查,确保它并非NULL是非常重要的。

44. 另一个内存分配函数

   void *calloc( size_t num_elements, size_t element_size);

calloc也用于分配内存。malloc和calloc的主要区别是后者返回指向内存的指针之前把内存的值初始化为0;另一个区别是二者参数不同,calloc的参数包括所需元素的数量和每个元素的字节数,calloc可以根据这些值计算出总共需要分配的内存。

45. 再一个内存分配函数

   void *realloc( void *ptr, size_t new_size );

realloc函数用于修改一个原先已经分配的内存块的大小。若使原内存扩大,则原内存的内容保持不变,新增加的内存添加到原内存的后面,且新的内存不以任何方法进行初始化;若使原内存缩小,则在原内存尾部部分被拿掉,剩余内存保持不变。如果原内存无法改变大小,realloc将分配另一块正确大小的内存,并把原先那块内存内容复制到新的内存块上。因此,在使用realloc之后,就不能使用指向旧内存的指针,而是应该改用realloc所返回的新指针。如果realloc函数的第1个参数是NULL,那么它的行为就和malloc函数一样了。

46. 释放内存的函数

   void free( void *pointer );

free的参数要么是malloc、calloc或realloc返回的值,要么是NULL,如果是NULL,那不会产生任何效果。

47. 如果要获得比如存储25个整数的内存,一个好的技巧是:

   int *pi;

   pi = malloc( 25 * sizeof( int ) );

这种方法可移植性好一些,即使整数长度不同的机器,也能获得正确的结果。

48. 常见动态内存错误:

①不检查malloc函数(calloc、realloc)返回的指针是否为NULL;

②访问动态分配的内存之外的区域;

③向free函数传递一个并非由malloc函数(calloc、realloc)返回的指针;

④在动态内存被释放之后再访问它。

49. 内存泄漏(memory leak)是指内存被动态分配后,当它不再使用时未被释放。内存泄漏会增加程序的体积,有可能会导致程序或系统崩溃。

50. 链表(linked list)是一些包含数据的独立数据结构(通常称为节点)的集合。链表中的每个节点通过链或指针连接在一起,程序通过指针访问链表中的节点。通常节点是动态分配的。

51. 单链表是一种使用指针来存储值的数据结构。链表中的每个节点包含一个link字段,用于指向链表的下一个节点。另外,有一个独立的根指针指向链表的第1个节点。由于节点在创建时有采用动态分配内存的方式,所以它们可能分布于内存之中。但是,遍历链表是根据指针进行的,所以节点的物理排列无关紧要。单链表只能以一个方向进行遍历。

52. 无序单链表插入新值,可以插到任何位置。把一个新节点链接到链表中需要两个步骤:

   1)新节点的link字段必须设置为指向它的后续节点;

   2)前一个节点的link字段必须设置为指向这个新节点。程序设计中,可以通过保存一个指向必须进行修改的link字段的指针,而不是保存一个指向前一个节点的指针,来解决在链表起始位置插入新值的特殊情况。

53. 双链表中的每个节点包含两个link字段,一个指向链表的下一个节点,另一个指向链表的前一个节点。双链表有两个根指针,分别指向第1个节点和最后一个节点。因此,遍历双链表可以从任何一端开始,而且在遍历过程中可以改变方向。为了把一个新节点插入到双链表中,必须修改4个指针。新节点的前向和后向link字段必须被设置,前一个节点的后向link字段和后一个节点的前向link字段也必须进行修改。

/*

链表这一部分在学生时代就被老师一笔带过了,印象中就是一串用指针串起来的结构体。今天研读《C和指针》,12章讲的就是链表,章标题就是《使用结构和指针》。到目前为止,实际的工作中还没有用到链表这么“复杂”的数据结构,在这里只是摘抄些自认为有价值的东西吧。

*/

54. 一些高级声明:

1) int *f( void );

   这个比较简单,f是一个函数,返回值类型是一个指向整型的指针。

2) int (*f) ( void );

   f是一个函数指针,它所指向的函数返回值类型是一个整型值。

3) int *(*f) ( void );

   f是一个函数指针,它所指向的函数返回值是一个整型指针,只有对其进行间接访问,才能得到一个整型值。

4) int *f[ ];

   这个也比较简单,f是一个数组,它的元素类型是指向整型的指针(指针数组)。

5) int (*f[ ]) ( void );

   f是一个数组,数组元素的类型是函数指针,它所指向的函数返回值是整型值。

6)int *(*f[ ]) ( void );

   f是一个数组,数组元素的类型是函数指针,它所指向的函数返回值是指向整型的指针。

55. 函数名被使用时总是由编译器把它转换成函数指针。

56. 用户把一个函数指针作为参数传递给其他函数,后者将“回调”用户的函数,这种技巧称为“回调函数”。如果编写的函数能够在不同的时刻执行不同类型的工作或执行只能由函数调用者定义的工作,都可以使用这个技巧。

57. 函数指针最常见的两个用途是转换表(jump table)和作为参数传递给另一个函数(回调函数)。


58. 转移表由一个函数指针数组组成(这些函数必须具有相同的原型),函数通过下标选择某个指针再通过指针调用对应函数。

/*

现在再回头去看我之前的一篇博文《TI蓝牙BLE 协议栈代码学习——OSAL(下)》,OSAL中进行任务处理的数组const pTaskEventHandlerFn tasksArr[ ]就是一个转换表,转换表里面保存的就是处理各任务的函数名,而这些函数名会被编译器转换成函数指针。使用转换表要特别注意的是,不要越界,这跟数组的使用一样,但是转换表一旦访问越界了,造成的后果可能会更恶劣。

转换表在书中是通过一个例子来讲述的,大家可以到书中去详细了解。

*/

 

0 0
原创粉丝点击