使用指针时的“陷阱”

“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.charca[ ] = "abcdefg";

2.char*cp = "abcdefg";

3.ca[0]= 'b';

4.printf("%s\n", ca );

5.cp[0]= 'b';

6.printf("%s\n", cp );

此程序第3行修改的不是只读数据区中的字符串常量,而是由字符串常量复制而来的存在于栈中的字符数组ca的一个元素。但第5行却修改了用于初始化字符指针的位于只读数据区的字符串常量,所以会发生运行时错误。大家不要认为所有的字符串常量都存储在不同的地址,标准C允许编译器为两个包含相同字符的字符串常量使用相同的存储地址,而且现实中大多数厂商的编译器也都是这么做的。来看下面的程序:

charstr1[] = "abc";

charstr2[] = "abc";

char*str3 = "abc";

char*str4 = "abc";

printf("%d\n", str1 == str2 );

printf("%d\n",str3 == str4 );

输出的结果是:0 1

str1,str2是两个不同的字符数组,分别被初始化为"abc",它们在栈中有各自的空间;而str3,str4是两个字符指针分别被初始化为包含相同字符的字符串常量,它们指向相同的区域。

2.strlen( )和sizeof

请看下面程序:

char a[1000];

printf("%d\n",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是一个指向constint型变量的指针,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 ** 和constint **都是没有限定符的指针类型,它们所指向的类型是不一样的(int **指向int *,而constint **指向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 <stdio.h>void pass_by_value(char parameter[])
{
printf("形参的值:   %p\n",parameter);
printf("形参的地址: %p\n", ¶meter);
printf("%s\n",parameter);
}int main( )
{charargument[100] = "C语言只有传值调用机制!";printf("实参的值:   %p\n",argument);pass_by_value(argument);return0;
}

在我机器上的输出结果为:实参的值:  0022FF00

形参的值:   0022FF00

形参的地址: 0022FED0

C语言只有传值调用机制!

当执行pass_by_value(argument);时,实参数组名argument被转换为指向数组第一个元素的指针,这个指针的值为(void *)0022FF00,然后把这个值拷贝一份赋给形式参数parameter,形参parameter虽然被声明为字符数组,但是会被转换为一个指针,它是创建在栈上的一个独立对象(它有自己独立的地址)并接收实参值的那份拷贝。从而我们看到了实参与形参具有相同的值,并且形参有一个独立的地址。再来看一个简单的例子:

#include <stdio.h>void pointer_plus(char *p)
{p+= 3;
}int main( )
{char*a = "abcd";pointer_plus(a);printf("%c\n", *a);return0;
}

如果哪位朋友认为输出是d,那么你还是没有搞清楚值传递的概念,此程序中将a拷贝一份赋给p,从此a和p就没有关系了,在函数pointer_plus中增加p的值实际上增加的是a的那份拷贝的值,根本不会影响到a,在主函数中a仍旧指向字符串的第一个字符,因此输出为a。如果想让pointer_plus改变a所指向的对象,采用二级指针即可,程序如下:

#include <stdio.h>void pointer_plus(char **p)
{*p += 3;
}int main( )
{char*a = "abcd";pointer_plus(&a);printf("%c\n", *a);return0;
}

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<stdio.h>
#include<stdlib.h>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返回的是一个无效的地址。

使用指针时的“陷阱”相关推荐

  1. 结构指针的坑之(定义结构体指针时并没有分配存储空间,所以要用malloc()申请空间)

    这段代码会报错,核心转储 #include<stdio.h> #include <string.h>struct role // 定义一个结构体 { // char name[ ...

  2. C语言零碎知识点之定义指针时星号靠近类型名还是变量名

    我们在C语言中定义指针时,星号到底是靠近类型名还是变量名有如下几种情况,并且都能够通过编译: int * a;// 星号放在类型名和变量名之间 int* a;// 星号靠近类型名 int *a;// ...

  3. c语言file和fp未声明标识符,c语言定义文件指针时(FILE *fp)一定要写*fp吗、可以换成别的字母不...

    c语言定义文件指针时(FILE *fp)一定要写*fp吗.可以换成别的字母不 关注:146  答案:4  mip版 解决时间 2021-01-29 07:27 提问者祗剩寂寞 2021-01-28 1 ...

  4. 使用 C++11 智能指针时要避开的 10 大错误

    http://blog.jobbole.com/104666/ 我很喜欢新的C++11的智能指针.在很多时候,对很多讨厌自己管理内存的人来说是天赐的礼物.在我看来,C++11的智能指针能使得C++新手 ...

  5. C与指针、C陷阱与缺陷

    遗留问题: P15理解函数声明 P39指针与数组 1.单/多字符符号-->贪心法 a+++++b? [先了解a++与++a的实现原理] //首先对于i++的实现是:(此时返回的是临时对象)int ...

  6. C++11 智能指针时要避开的 10 大错误

    我很喜欢C++11全新的智能指针,对于那些苦恼于自己管理内存的开发者来说,这是天大的好事.但是,在广泛使用C++11智能指针的两年里,因为使用方法不当而导致程序的效率下降甚至直接crash掉的事情发生 ...

  7. 处理WinForm多线程程序时的陷阱(摘自网络)

    与所有的UI开发平台一样,.NET下线程开发图形界面同样要遵循一个基本原则:就是对UI对象的操作一定要在产生该UI对象的线程里进行(该线程称作UI线程),因为大部分UI对象都不是线程安全的. 在.NE ...

  8. PHP中Foreach在引用时的陷阱

    这个陷阱在PHP里存在很久了,先看代码: <?php $array = array(1, 2, 3); foreach ($array as &$v) {} foreach ($arra ...

  9. java week of year_JDK Calendar类获取WEEK_OF_YEAR时的陷阱

    /*Calendar类是Java中使用的较为频繁的类,这个类中有很多静态方法非常实用,免去了我们自己手动计算日期的过程.Calendar.get(Calendar.WEEK_OF_YEAR) 用于获取 ...

最新文章

  1. iOS WebviewJavascriptBridge 源码研读笔记
  2. 用gdb调试mpi程序的一些心得
  3. 空气输入法!浙大最新研究:空中动动手指,就能给智能手表输入文本
  4. mongodb 设置远程可以访问
  5. 新闻通稿 | 2021年世界互联网大会乌镇峰会网络法治分论坛圆满举行
  6. 软路试--就像一棵树活着
  7. 快速径向对称 只检测暗对称 代码(2)
  8. UA MATH564 概率论 标准二元正态分布的性质
  9. 安装php时,make步骤报错make: *** [ext/gd/gd.lo] Error 1
  10. 【职场】从算法工程师到主管的转变,需要改变的是什么
  11. Altium designer中设置PCB网络闭合(不闭合)
  12. 仓鼠体重年龄对照表_一文带你走进仓鼠世界:仓鼠种类之叙利亚仓鼠(上)
  13. php最后最后再执行,php最后几个函数
  14. 【LeetCode笔记】112 113. 路径总和 I II(Java、递归、DFS)
  15. cuSPARSE库:(一)函数的异步执行
  16. 洛谷 P3373 【模板】线段树 2 解题报告
  17. 前端组件化思想与实践
  18. iptv服务器维护中,iptv升级服务器地址
  19. Laravel——微信授权登陆
  20. 贪心算法基础之活动时间安排(一)安排 51nod 贪心教程

热门文章

  1. Vue 单页面应用 把公共组件放在 app.vue 但是我希望某个页面没有这些公共组件怎么办???(比如登陆页面)
  2. html表格隐藏1行,js控制隐藏或显示table的某一行
  3. [Java] 蓝桥杯ALGO-122 算法训练 未名湖边的烦恼
  4. [Java] 蓝桥杯ALGO-143 算法训练 字符串变换
  5. [Python] L1-052 2018我们要赢-PAT团体程序设计天梯赛GPLT
  6. 蓝桥杯 ADV-170算法提高 数字黑洞
  7. 蓝桥杯 ADV-134算法提高 校门外的树
  8. Java程序员需要注意的五大Docker误区
  9. 手把手教你用Spring Cloud和Docker构建微服务
  10. reduce个数究竟和哪些因素有关