分类:
2012-09-26 21:36:51
原文地址:C语言指针导学(6) 作者:kinganeng
“C语言诡异离奇,陷阱重重,却获得了巨大成功!”——C语言之父Dennis M. Ritchie。Ritchie大师的这句话体现了C语言的灵活性以及广泛的使用,但也揭示了C是一种在应用时要时刻注意自己行为的语言。C的设计哲学还是那句话:使用C的程序员应该知道自己在干什么。有时用C写的程序会出一些莫名其妙的错误,看似根源难寻,但仔细探究会发现很多错误的原因是概念不清。在我们经常掉进去的这些“陷阱”中,围绕着指针的数量为最。这一讲将对使用指针时遇到的一些问题做出分析,以避免在日后落入此类“陷阱”之中。
1.指针与字符串常量
在第二讲指针的初始化中提到可以将一个字符串常量赋给一个字符指针。但有没有朋友想过为什么能够这样进行初始化呢?回答这个问题之前,我们先来搞清楚什么是字符串常量。字符串常量是位于一对双引号内部的字符序列(可以为空)。
当一个字符串常量出现于表达式中,除以下三种情况外:
1. 不是 & 操作符的操作数;
2. 不是sizeof操作符的操作数;
3. 不作为字符数组的初始化值
字符串常量都会被转化为由一个指针所指向的字符数组。例如:char *cp = "abcdefg"; 不满足上述3个条件,所以"abcdefg"会被转换为一个没有名字的字符数组,这个数组被abcdefg和一个空字符'\0'初始化,并且会得到一个指针常量,它的值为第一个字符的地址,不过这些都是由编译器来完成的。现在可以解释用一个字符串常量初始化一个字符指针的原因了,一个字符串常量的值就是一个指针常量。那么对于下面的语句,朋友们也不该感到迷惑了:
printf("%c\n", *"abcdefg");
printf("%c\n", *("abcdefg" + 1));
printf("%c\n", "abcdefg"[5]);
*"abcdefg":字符串常量的值是一个指针常量,指向的是字符串的第一个字符,对它解引用即可得到a;
*("abcdefg" + 1):对这个指针进行算术运算则其指向下一个字符,再对它解引用,得到b;
"abcdefg"[5]:既然"abcdefg"是一个指针,那么"abcdefg"[5]就可以写成*("abcdefg" + 5),所以得到f。
回忆一下大家所学的初始化数组的方法:char ca[ ] = {'a', 'b', 'c', 'd', 'e', 'f', 'g', '\0'};这种方法实在太笨拙了,所以标准提供了一种快速方法用于初始化字符数组:char ca[ ] = "abcdefg"; 这个字符串常量满足了上面的第3条:用来初始化字符数组,所以不会被转换为由一个指针所指向的字符数组。它只是用单个字符来初始化字符数组的简便写法。再来对比以下两个声明:
char ca[ ] = "abcdefg";
char *cp = "abcdefg";
它们的含义并不相同,前者是初始化一个字符数组的元素,后者才是一个真正的字符串常量,如下图所示:
char ca[ ] = "abcdefg";
图1
char *cp = "abcdefg";
图2
要注意的是:用来初始化字符数组的字符串常量,编译器会在栈中为字符数组分配空间,然后把字符串中的所有字符复制到数组中;而用来初始化字符指针的字符串常量会被编译器安排到只读数据存储区,但也是按字符数组的形式来存储的,如图2。我们可以通过一个字符指针读取字符串常量但不能修改它,否则会发生运行时错误。正如下面的例子:
1.char ca[ ] = "abcdefg";
2.char *cp = "abcdefg";
3.ca[0] = 'b';
4.printf ("%s\n", ca );
5.cp[0] = 'b';
6.printf ("%s", cp );
此程序第3行修改的不是只读数据区中的字符串常量,而是由字符串常量复制而来的存在于栈中的字符数组ca的一个元素。但第5行却修改了用于初始化字符指针的位于只读数据区的字符串常量,所以会发生运行时错误。大家不要认为所有的字符串常量都存储在不同的地址,标准C允许编译器为两个包含相同字符的字符串常量使用相同的存储地址,而且现实中大多数厂商的编译器也都是这么做的。来看下面的程序:
char str1[] = "abc";
char str2[] = "abc";
char *str3 = "abc";
char *str4 = "abc";
printf("%d ", str1 == str2 );
printf("%d\n", str3 == str4 );
输出的结果是:0 1
str1,str2是两个不同的字符数组,分别被初始化为"abc",它们在栈中有各自的空间;而str3,str4是两个字符指针分别被初始化为包含相同字符的字符串常量,它们指向相同的区域。
2.strlen( )和sizeof
请看下面程序:
char a[1000];
printf("%d, ", sizeof(a));
printf("%d\n", strlen(a));
这段代码的输出可不一定是1000, 0。sizeof(a)的结果一定是1000,但strlen(a)的结果就不能确定了。根本原因在于:strlen( )是一个函数,而sizeof是一个操作符,这导致了它们的种种不同:
1.sizeof可以用类型(需要用括号括起来)或变量做操作数,而strlen( )只接受char*型字符指针做参数,并且该指针所指向的字符串必须是以'\0'结尾的;
2.sizeof是操作符,对数组名使用sizeof时得到的是整个数组所占内存的大小,而把数组名作为参数传递给strlen( )后数组名会被转换为指向数组第一个元素的指针;
3.sizeof的结果在编译期就确定了,而strlen( )是在运行时被调用。
由于上例中的数组a[1000]没有初始化,所以数组内的元素及元素个数都是不确定的,可能是随机值,所以用strlen(a)会得到不同的值,这取决于产生的随机数,但sizeof的结果一定是1000,因为sizeof是在编译时获取char a[1000]中char和1000这两个信息来计算空间的。
3.const指针与指向const的指针
对于常量指针(const pointer)和指针常量大家应该可以分清楚了。常量指针:指针本身的值不可以改变,可以把const理解为只读的,如:int *const c_p;指针常量:一个指针类型的常量,如:(int *)0x123456ff。现在引入一个新的概念:指向const的指针,即一个指针它所指向的是一个const对象,如:const int *p_to_const; 表明p_to_const是一个指向const int型变量的指针,p_to_const自身的值是可以改变的,但是不能通过对p_to_const解引用来改变所指的对象的值,看下面的例子会更加清晰:
int *p = NULL; //定义一个整型指针并初始化为NULL
int i = 0; //定义一个整型变量并初始化为0
const int ci = 0; //定义一个只读的整型变量并初始化,程序中不能再对它赋值
const int *p_to_const = NULL; //定义一个指向只读整型变量的指针,初始化为NULL
p = &i; //ok,让p指向整型变量i
p_to_const = &ci; //ok,让p_to_const指向ci
*p = 5; //ok,通过指针p修改i的值
*p_to_const = 5; /*error,p_to_const所指向的是一个只读变量,不能通过p_to_const对
ci进行修改
p_to_const = &i; //ok,让指向const对象的指针指向普通对象
p_to_const = p; //ok,将指向普通对象的指针赋给指向const对象的指针
p = (int *) ⁣ //ok,强制转化为(int *)型,赋值操作符两侧操作数类型相同
p = (int *) p_to_const; //ok,同上
p = ⁣ // error,错误原因下述
p = p_to_const; //error,同上
对于最后两行的赋值,需要说明一下。C语言中对于指针的赋值操作(包括实参与形参之间的传递)应该满足:两个操作数都是指向有限定符或都是指向无限定符的类型相兼容的指针;或者左边指针所指向的类型具有右边指针所指向的类型的全部限定符。例如const int *表示“指向一个具有const限定符的int类型的指针”,即const所修饰的是指针所指向的类型,而非指针。因此,p = ⁣ 中的&ic得到的是一个指向const int型变量的指针,类型和p_to_const一样。p_to_const所指向的类型为const int,而p所指向的类型为int,p在赋值操作符左边,p_to_const在赋值操作符右边,左边指针所指向的类型并不具有右边指针所指向类型的全部限定符,所以会出错。
小扩展:{让我们再深入一些,如果现在有一个指针int **bp和一个指针const int **cbp那么这样的赋值也时错误的:cbp = bp; 因为const int **表示“指向有const限定符的int类型的指针的指针”。int ** 和const int **都是没有限定符的指针类型,它们所指向的类型是不一样的(int **指向int *,而const int **指向const int *),所以它们是不兼容的,根据指针赋值条件来判断,这两个指针之间不能相互赋值。
实际上和const int **相兼容的类型是const int**const,所以下面代码是合法的:
const int * *const const_p_to_const = &p_to_const;
/*定义一个指向有const限定符的int类型的指针的常指针,它必需在定义时初始化,程序中不能再对它赋值。由于既不能修改指针的值也不能通过指针改变所指对象的值,所以在实际中,这种指针的用途并不广*/
const int **cpp;
cpp = const_p_to_const;
左操作数cpp所指向的类型是const int*,右操作数const_p_to_const指向类型也为const int*,满足指针赋值条件:左边指针所指向的类型具有右边指针所指向类型的全部限定符,只不过const_p_to_const是一个const指针,不能被再赋值,所以反过来是不能进行赋值的。还要注意被const限定的对象只能并且必需在声明时初始化。}
4.C语言中的值传递
在第3将中提到过C语言只提供函数参数的传值调用机制,即函数调用时,拷贝出一个实参的副本并把这个副本赋值给形参,从此实参与形参是各不相干的,形参在函数中的改变不会影响实参。我在前面说过C语言中所有非数组形式的数据实参(包括指针)均以传值形式调用,这并不与C语言只提供传值调用机制矛盾,对于数组形参会被转换为指向数组首元素的指针,当我们用数组名作为实参时,实际进行的也是值传递。请看程序:
#include
void pass_by_value(char parameter[ ])
{
printf("形参的值: %p\n", parameter);
printf("形参的地址: %p\n", ¶meter);
printf("%s\n", parameter);
}
int main( )
{
char argument[100] = "C语言只有传值调用机制!";
printf("实参的值: %p\n", argument);
pass_by_value(argument);
return 0;
}
在我机器上的输出结果为:实参的值: 0022FF00
形参的值: 0022FF00
形参的地址: 0022FED0
C语言只有传值调用机制!
当执行pass_by_value(argument);时,实参数组名argument被转换为指向数组第一个元素的指针,这个指针的值为(void *)0022FF00,然后把这个值拷贝一份赋给形式参数parameter,形参parameter虽然被声明为字符数组,但是会被转换为一个指针,它是创建在栈上的一个独立对象(它有自己独立的地址)并接收实参值的那份拷贝。从而我们看到了实参与形参具有相同的值,并且形参有一个独立的地址。再来看一个简单的例子:
#include
void pointer_plus(char *p)
{
p += 3;
}
int main( )
{
char *a = "abcd";
pointer_plus(a);
printf("%c\n", *a);
return 0;
}
如果哪位朋友认为输出是d,那么你还是没有搞清楚值传递的概念,此程序中将a拷贝一份赋给p,从此a和p就没有关系了,在函数pointer_plus中增加p的值实际上增加的是a的那份拷贝的值,根本不会影响到a,在主函数中a仍旧指向字符串的第一个字符,因此输出为a。如果想让pointer_plus改变a所指向的对象,采用二级指针即可,程序如下:
#include
void pointer_plus(char **p)
{
*p += 3;
}
int main( )
{
char *a = "abcd";
pointer_plus(&a);
printf("%c\n", *a);
return 0;
}
5.垂悬指针(Dangling pointer)
垂悬指针是我们在使用指针时经常出现的,所谓垂悬指针就是指向了不确定的内存区域的指针,通常对这种指针进行操作会使程序发生不可预知的错误,因此我们应该避免在程序中出现垂悬指针,一些好的编程习惯可以帮助我们减少这类事件的发生。
造成垂悬指针的原因通常分为三种,对此我们一个一个地进行讨论。
第一种:在声明一个指针时没有对其初始化。在C语言中不会对所声明的自动变量进行初始化,所以这个指针的默认值将是随机产生的,很可能指向受系统保护的内存,此时如果对指针进行解引用,会引发运行时错误。解决方法是在声明指针时将其初始化为NULL或零指针常量。大家应该养成习惯为每个新创建的对象进行初始化,此时所做的些许工作会为你减少很多烦恼。
第二种:指向动态分配的内存的指针在被free后,没有进行重新赋值就再次使用。就像下面的代码:
int *p = (int *)malloc(4);
*p = 10;
printf("%d\n", *p);
free(p);
……
……
printf("%d\n", *p);
这就可能会引发错误,首先我们声明了一个p并指向动态分配的一块内存空间,然后通过p对此空间赋值,再通过free( )函数把p所指向的那段内存释放掉。注意free函数的作用是通过指针p把p所指向的内存空间释放掉,并没有把p释放掉,所谓释放掉就是将这块内存中的对象销毁,并把这块内存交还给系统留作他用。指针p中的值仍是那块内存的首地址,倘若此时这块内存又被指派用于存储其他的值,那么对p进行解引用就可以访问这个当前值,但如果这块内存的状态是不确定的,也许是受保护的,也许不保存任何对象,这时如果对p解引用则可能出现运行时错误,并且这个错误检测起来非常困难。所以为了安全起见,在free一个指针后,将这个指针设置为NULL或零指针常量。虽然对空指针解引用是非法的,但如果我们不小心对空指针进行了解引用,所出现的错误在调试时比解引用一个指向未知物的指针所引发的错误要方便得多,因为这个错误是可预料的。
第三种:返回了一个指向局部变量的指针。这种造成垂悬指针的原因和第二种相似,都是造成一个指向曾经存在的对象的指针,但该对象已经不再存在了。不同的是造成这个对象不复存在的原因。在第二种原因中造成这个对象不复存在的原因是内存被手动释放掉了,而在第三种原因中是因为指针指向的是一个函数中的局部变量,在函数结束后,局部变量被自动释放掉了(无需程序员去手动释放)。如下面的程序:
#include
#include
int *return_pointer()
{
int i=3;
int *p =&i;
return p;
}
int main()
{
int *rp = return_pointer();
printf("%d\n", *rp);
return 0;
}
在return_pointer函数中创建了一个指针p指向了函数内的变量i (在函数内创建的变量叫做局部变量),并且将这个指针作为返回值。在主函数中有一个指针接收return_pointer的返回值,然后对其解引用并输出。此时的输出可能是3,也可能是0,也可能是其他值。本质原因就在于我们返回了一个指向局部变量的指针,这个局部变量在函数结束后会被编译器销毁,销毁的时间由编译器来决定,这样的话p就有可能指向不保存任何对象的内存,也可能这段内存中是一个随机值,总之,这块内存是不确定的,p返回的是一个无效的地址。