分享

周立功:安全有效地使用C掌握指针—声明与访问

 甲基丁酸 2017-07-09

周立功:安全有效地使用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.8  变量的存储与引用范例程序(用带参数的宏替换)

1       #include

2       #define PRINT_INT(i)                \

3       printf('%8s(): &%-5s = 0x%-6x,%-5s = 0x%-6x\n', __FUNCTION__, #i, &(i), #i, i);

4

5       #define PRINT_PTR(p)                \

6       printf('%8s(): &%-5s = 0x%-6x,%-5s = 0x%-6x, *%-5s = 0x%-6x\n', __FUNCTION__,  \

7          #p, &(p), #p, p, #p, *p)

8

9       int main(int argc, char *argv[])

10     {

11              int iNum = 0x64;

12              int *ptr;

13

14              PRINT_INT(iNum);

15              ptr= &iNum;

16              PRINT_PTR(ptr);

17              return0;

18     }


其相应的变量的存储与引用过程详见图1.7,虽然intiNum变量在内存中占用了4个字节,但&iNum的值仅仅是“iNum变量”所占用的那块内存单元中第一个字节的地址。

同理表达式“&ptr”的含义就是指针变量ptr所在的内存单元地址0x22FF70,即ptr指向iNum。这是定义指针变量与定义int等类型变量的不同之处,即可通过指针变量访问其所指向的变量。使用gcc for MinGW版本编译运行的结果如下:

阅读

''

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多