周立功:安全有效地使用C掌握指针—声明与访问 2017-07-04 电子工程师时间 电子工程师时间 ee-technotes 分享电子工程相关技术文档:设计技巧,应用笔记,参考设计,设计方案 经周立功教授授权,特对本书内容进行连载。第一章为程序设计基础,本文为1.3.1节声明与访问。 周立功 1.1 指针变量与指针的指针实际上,长期以来指针和它所提供给开发者的原始功能已经成为人们对C的主要批评,很多人说指针太危险了。其根源在于人们对真相了解得不够透彻,从而认为这些语言是危险的。其实,要安全有效地使用C,掌握指针是必要的。幸运的是,在弄清楚一些基本原则后,就很容易掌握如何正确地使用指针。 1.1.1 声明与访问1. 指针变量既然&iNum是指向变量iNum的指针,那么存放指针&iNum的变量就是“指针变量”。从根本上来看,指针变量是一个值为内存地址的变量。如果有以下定义: int iNum = 0x64; int *ptr = &iNum; 其中,“*”将变量声明为指针,这是一个重载过的符号。显然,重载就是为某个操作符定义新的含义,因为*用于乘法和间接访问或解引用指针时,其意义完全不一样。 iNum变量设置为0x64,在声明ptr的同时将它初始化为iNum的地址。当声明一个指针时,仅仅只是为指针本身分配了空间,并没有为指针所引用的数据分配空间。 通常该声明被解读为ptr是指向int的变量iNum的指针,“int *”类型名被解读为指向int的变量iNum的指针类型。虽然ptr与&iNum的值相等,但它们的类型不一样。ptr的类型为int *,&iNum的类型为int *const,即&iNum是一个指向非常量的常量指针,虽然指针不可变,但它所指向的数据可变。比如: int iNum = 0x64; int * const ptr =&iNum; 有了这个声明,则ptr必须被初始化为指向非常量变量,虽然不能修改ptr,但可以修改ptr指向的数据。如果试图初始化ptr使其指向常量iNum,则ptr就成为了一个指向常量的指针,即不能通过指针修改它所引用的值。比如: const int iNum = 0x64; int * const ptr =&iNum; 就会产生一个警告,因为常量是不可以修改的。 小知识:const 无论怎样,const修饰的是紧跟在它后面的单词。比如,使用“const int a[2] ={5, 2};”是将数组a的元素修饰为只读(它的值保持不变),而不是将变量a修饰为只读。注意,int const与const int是等价的。 既然可以用const修饰变量,当然也可以用const修饰指针变量。比如,“char * const src”是将src自身修饰为只读,而“const char * const src”是src和src指向的对象(变量的值)都修饰为只读。此外还有const char *src和char const *src,其效果是一样的。注意,当指针作为传递参数时,const常用于将指针指向的对象设定为只读。 注意,只读变量与指令一起保存在“只读段”中,而变量保存在“读写段”中。具有存储器保护功能的操作系统可能将只读段保护起来,不让程序修改值。 类似Cortex-M0+这样的MCU,其只读段与读写段是分别分配在Flash与RAM中的,因此无法改变只读变量的值。由于Flash比RAM便宜很多,且容量大很多,因此建议尽量将变量定义为只读变量,以达到节省存储空间的目的。 变量是值的表现形式,这个值实际上存储在一个特定的内存地址。一旦有了变量的地址,就可以从这个地址中取出存储在其中的数据。如果恰好要将一个巨大的数据块传递给一个函数,则只要在程序运行时将数据块的位置传递给函数即可,这种方法比复制数据块的所有数据要高效得多。其思路是将数据的内存地址传递给函数,而不是将该数据块复制一份。 虽然指针&iNum是指内存地址本身,指针变量ptr是指存储内存地址的变量,但两者之间的区别并不重要。因为传递一个指针变量给函数,其实就是在传递该指针的值——内存地址。通常在讨论一个内存地址时,将它称为地址;而在讨论一个存储内存地址的变量时,才会将它称为指针。当一个变量存储另一个变量的地址时,通常会说它指向了某个变量。 2. NULL由于在声明一个指针时,指针中的数据是随机产生的。因此它可能指向一个合法,也可能非法的位置。如果此时使用指针相当危险,则等于使用了一个随机生成的地址,使用此数值可能导致程序崩溃或数据损坏。当指针不指向任何地方时,就必须在使用指针前将它初始化为NULL空指针。通常将其解释为二进制的0,在C语言中表示为假。比如: if(ptr == NULL) ... if(ptr != NULL) ... 即如果ptr包含了NULL,则else分支就会执行。如果将null值赋给ptr,比如: int *ptr = NULL; null指针和未初始化的指针不同的是,未初始化的指针可能包含任何值,而包含NULL的指针,不会引用内存中的任何地址,使用NULL或0是在语言层面表示null指针的符号。 指针也可以作为逻辑表达式的唯一操作数,用于测试指针是否设置成了NULL。比如: if(ptr){ // 不是NULL }else{ // 是NULL } 即便这样,但检查空值也很麻烦,可以使用assert函数(assert.h)测试指针是否为空值: assert(ptr != NULL); 如果表达式为真,则什么也不会发生;如果表达式为假——指针为空,则程序终止。 NULL宏是强制类型转换为vid指针的整数常量0,很多库都有定义: #define NULL ((void *)0) 通常将其理解为null指针,常见于stdio.h、stddef.h和stdlib.h等头文件中。 ASCII字符NUL(null)定义为全0字节,这和null指针不一样。C的字符串表示为以0值结尾的字符序列。null字符串是空字符,不包含任何字符。 3. 奇怪的声明 在这里,“int iNum = 0x64;”使用了“类型 变量名;”的形式书写,而“指向int的指针”类型的变量,却要声明为“int *ptr = &iNum;”,似乎声明了一个名为“*ptr”的变量。而实际上,这里声明的变量是ptr,ptr的类型是“指向int的指针”,即int*类型。因此不能将&iNum赋给*ptr,即不能错误地写成: *ptr = &iNum; //错误的赋值方式 由于这种方式不好理解,于是有人提出将*靠近类型这一边进行书写。比如: int* ptr= &iNum; 虽然这样的书写方式符合了“类型变量名;”的形式,但在同时声明多个变量的情况下就会出现问题,比如: int* ptr1, ptr2; 看起来好象声明了2个指向int的指针,而ptr2却是int型的变量,显然这样的声明方式非常怪异。对于初学者来说,学到后面将会感到越来越别扭,甚至使人无法学下去。 也许有人会这样认为,一旦在ptr之前加上*,就可以和int变量iNum一样使用了,这个声明意味着在ptr之前加上*后就成为int类型了。如果这样考虑,看起来似乎有一定的道理,那是不是可以很自然地解释C的声明了呢?当你看到: int (*pf)(); 这样的声明时,是不是更加糊涂了呢?因此要在一条语句中声明多个指针,则必须在每个变量前都加上星号“*”。 为何没有一种更简单的方式声明指针呢?比如,用pointer p_pointer这样的语句。如果要让编译器正确地解释和使用内存地址,则必须让它知道地址中存储哪种类型的数据。比如,内存中相同字节数的变量,如果其类型不一样,则它们的意义完全不相同。与其为每种指针类型创建单独的名字,比如,用int_ptr表示int类型指针,char_ptr表示char类型指针,还不如总是用*和类型名声明指针。 4. 指针变量类型别名类似变量的类型定义,其实也可以用typedef定义指针类型的别名。比如: typedef int *PINT; PINT ptr; 或许,你会产生这样的疑问,为什么不使用#define创建新的类型名?比如: #define PINT int* PINT ptr1, ptr2; 由于有了“#define PINT int*”,因此“PINT ptr1, ptr2;”可以展开为 int * ptr1, ptr2; 所以ptr2不是int *型指针变量,而是int型变量。如果用typedef来定义: typedef int* PINT; PINT ptr1, ptr2; 则“PINT ptr1, ptr2;”等价于 int *ptr1, *ptr2; 显然ptr1、ptr2都是指针变量,对于#define来说,仅在编译前对源代码进行了字符串替换处理;而对于typedef来说,它建立了一个新的数据类型别名。由此可见,使用#define的代码,只是将ptr1定义为指针变量,却并没有实现程序员的意图,而是将ptr2定义成了int型变量,而不是int *。 显然,使用PINT不仅可以声明一个指针,还可以声明一个数组: PINT*pPtr; //声明一个指针的指针,等同于int **pPtr; PINTpData[2]; //声明一个指针数组,等同于int *pData[2]; 为了深入理解指针变量,其示例详见程序清单 1.8。
其相应的变量的存储与引用过程详见图1.7,虽然int型iNum变量在内存中占用了4个字节,但&iNum的值仅仅是“iNum变量”所占用的那块内存单元中第一个字节的地址。 同理表达式“&ptr”的含义就是指针变量ptr所在的内存单元地址0x22FF70,即ptr指向iNum。这是定义指针变量与定义int等类型变量的不同之处,即可通过指针变量访问其所指向的变量。使用gcc for MinGW版本编译运行的结果如下:
阅读 '' |
|