指针是C语言中较为重要的一环,也是C语言中学习较难的一部分。软件处理中的所有数据存储在内存中。内存中一个字节即一个内存单元,不同的数据类型所占用的内存单元数并不相同。因此,程序需要准确地找到内存单元,取出并执行。这个内存单元的编号称为地址,即指针。本节总结一些使用指针时较易出现的错误,提示开发人员在实际操作中注意。
1.非法指针
非法指针包括未初始化指针、空指针和指针越界等。常见的错误举例如下。
int * i; *i = 10;
很明显,上例中未对i变量进行初始化就直接赋值。因为声明一个指针不会创建对应的内存空间,就无法知道具体的数值存储在哪里,所以需要代码来分配空间并初始化。一般这样的错误编译时未必能被发现,但在Linux系统下运行时会出现“Segmentation fault”的异常,即程序访问了一个并未分配的内存地址。
若没有正确初始化同时这类指针内部又包含一个合法地址,那么这个地址的值很可能会被修改掉。调试时这样的错误难以发现,也难以定位,所以在访问指针前需要正确初始化。
空指针就是通常所用的NULL指针,它不指向任何内存单元。代码写成char *p=NULL或0,就可使指针变量为NULL。当选择0时,编译器会负责对0进行翻译。
NULL指针在C语言程序中使用较多。例如,在查找数组时若没找到相应的数据,可返回NULL指针作为结果。访问NULL指针可能会导致程序异常退出,因此在开发中,对于函数处理或参数传递的情形可用断言来判断指针的合法性,这种策略能节约大量的调试时间。
与前两者相比,指针越界是个不易被发现的错误,有时编译器不报错,或者不给出告警。指针越界问题一般出现在为其分配的空间大小上,一个简单的例子如下。
void func () { char buf[32]; memset (buf, 0, 64); }
上例中,可能会修改掉32以外的内容,导致出现意外错误,甚至破坏系统数据。如果系统中一些节点意外出现了空地址,很可能就是指针越界清洗掉这些节点的数据了。如果对此指针进行数据复制,同样会带来严重后果。因此,上例中可采用按大小赋值的方法,如memset (buf, 0, sizeof (buf))。
2.指向指针的指针和指针数组
指向指针的指针采用的形式为:类型**变量。举例如下。
int ** i; int j = 32; int *k = &j; i =&k;
可以这样理解,k指向j,i指向k。*操作符自右向左结合,修改了i也就改变了j的值。一个指针变量内部既可存储一个数字或字符串,也可以是另一个对象的地址,也就是说再通过这个指针变量指向这个对象。这就是指向指针的指针。使用指向指针的指针时需要注意,尤其是在初始化时易出现程序的异常退出。
指针数组是指在一个数组中,每个元素都是一个指针,而且所指类型相同,形式为:类型* 变量[元素个数],举例如下。
int *p[10];char *p[10];
指向指针的指针和指针数组不能混用。在一些字符串的复制中用指针数组,而不能直接用指向指针的指针。这些指针初始化时需要更多地注意内层指针的空值情形。
3.指针赋值和类型转换
指针赋值和类型转换需要注意以下情形。
(1)指针赋值和强制类型转换时,要注意内存字节对齐的情形。
例如,unsigned char * buf;
struct record { char * name; int age; ... }data;
这时如果采用直接转换buf = (unsigned char*)data; 可能会导致数据错位。转换过程中要注意类型的大小差异,大转小时可能会有数据丢失。
(2)同类型可直接赋值。
(3)涉及void *的类型指针转换,void *类型可赋值给任何类型的指针。
void *可接纳任何类型赋值,但反过来会出错,举例如下。
int * p = 32;void * q = p; p = q;//错误
(4)指针运算时注意内存泄露情形。
内存动态分配是指针的关键技术,前文就提到过,调用 malloc 等分配内存,一定要用函数free回收释放,否则会导致内存泄露(因为它不会被自动删除,而嵌入式系统中资源是有限的)。如果存在较多类似的小问题,积累到一定程度,最终可能会出现系统崩溃。
4.函数指针
在程序运行中函数代码是算法指令,同样要占用内存,也有相应的地址。这时可使用指针指向函数的首地址,将指向函数首地址的指针变量称为函数指针,其定义形式如下。
函数类型(*指针名) (参数表);
例如,int (*func) (int x);
int function(int x); /* 声明一个函数 */ int(*pointer)(int x); /* 声明一个函数指针 */ pointer=function; /* 将function函数的首地址赋给指针point*/
赋值后指针pointer指向函数function (x)首地址,操作pointer即调用function函数。
在定义函数指针时,该指针要和它指向的函数类型、返回值和参数表一致,即使是void指针,最好也保持类型一致。
函数指针常见的用途是回调函数和转换表,本节主要介绍这两点。
回调函数是不显示调用的函数,将回调函数的地址传给调用函数,举例如下。
void test () { int i; for (i=0; i<3; i++) { printf ("The result is good,\n"); } return; } void caller (void (*ptr) ()) { (*ptr) (); } int main () { caller (test); return 0; }
以上将回调函数的地址传给调用函数,因此在使用回调函数前要先定义好函数指针。注意,int (*p) (); 这里p是一个函数指针,其中 (*p)的括号不能省略,否则就变成了一个返回类型为void的函数声明。
回调函数的一个重要特点就是让函数的处理与具体的类型无关。程序员将一个函数指针传给其他函数,这样使所编写的函数能在不同时刻执行不同类型的具有相同功能的任务。许多Windows下的窗口应用程序也使用回调来连接。
转换表可以用来处理参数类型相同且功能类似的任务,也可以用函数指针数组来声明。例如,有一组函数,分别定义了计算整数之间的加法、减法和乘法,具体如下。
int add (int, int); int sub (int, int); int mul (int, int);
对此,创建转换表需要分为以下两个步骤。
(1)声明一个指针数组,元素是函数指针,并初始化该数组。
int (*process[3]) (int, int) = { add, sub, mul };
(2)运算时需要从数组中选择正确的函数指针,这样,函数调用操作符就会调用该指针对应的函数。
这种转换表是用数组来表示的,因此一些数组应用不当带来的问题同样会出现在转换表中。例如,数组越界是非法的,而且这类问题查询起来更难以诊断,所以在起始定义时就要注意使用合法的下标。
5.字符串指针与字符数组
字符串指针和字符数组都可操作字符串,但两者有区别。在使用时应注意以下几个问题。
(1)字符串存放在一段连续的内存空间中。字符串指针则用来存放这段字符串内存空间的首地址;而字符数组用来存放整个字符串,每个依序保存一个字符。
相比之下使用指针变量要方便些,但如果使用不当,如一个指针变量未取得确定地址就使用,则容易引起错误,导致程序崩溃。
(2)初始化字符数组时,采用全局类型或静态类型,例如,static char data[]={"data"};(不能写成:char data[20]; data = {"data"};) 可对数组元素逐个赋值。而字符串指针变量使用起来灵活些,如char * data = "data"。
6.指针与const
const限定符和指针结合起来的常见情形如下。
(1)int * const data;
变量data是指向整型的const指针,作为常量指针,data不能修改。
(2)const int * data;
变量data是指向常量整型的指针,它所指向的内存单元不能修改,但data可以改写。若函数形式参数是类似形式,调用时传送int *或const int *均合法,不会改写内存空间。
(3)int const * const data;
变量data是指向常量整型的const指针,*data和data都不可以改写。
(4)指向变量的指针或者变量的地址可以传给指向常量的指针,举例如下。
char a = 'a'; const char * p = &a;
上例中,编译器可以做隐式类型转换。
(5)指向常量的指针或者常量的地址不能传给指向变量的指针,因为后者可能会修改了前者所指向的内存单元而导致数据出错,举例如下。
const char a = 'a'; char *p = &a;
上例中,代码编译器会给出警告。
在处理字符串时应尽可能多用const指定符,将不会变化或不应被修改的字符串声明成只读类型,这样程序运行时可防止发生意外改写了数据。
指针的应用在嵌入式软件开发中是非常重要的。