C语言指针的理解一:指针是什么

  • Post author:
  • Post category:其他




1.C语言中的指针是什么



1.1 指针变量和普通变量的区别

首先必须非常明确:指针完整的名字应该叫指针变量,简称为指针。指针的实质就是个变量,从内存和数据的角度来说,它跟普通变量没有任何本质区别。

来看下面一段代码:

#include<stdio.h>

int main(int argc,char**argv)
{
	int a=5;
	int* p=&a;
	printf("a = %d,p = %p.\n",a,p);
	return 0;
}

运行结果:

a = 5,p = 0x7ffe97237f6c.

以64位机器为例,来分析一下对于上面的代码编译器会做哪些工作:

  • 对于

    int a=5;

    ,编译器会在内存中找一个4个字节的内存空间,然后把这个空间和符号

    a

    绑定起来,之后操作

    a

    就等于操作这个内存空间,再将数值5以二进制补码的形式存放到这片空间当中去。
  • 同样的对于

    int* p=&a;

    ,编译器会在内存中找一个8个字节的空间,把这段空间和符号

    p

    绑定起来,再将变量

    a

    的地址存放到这片空间中去。

通过上面的分析可以知道,其实不管是

int a

还是

int* p

,编译器做的事情实际上是没有区别的:

都是在内存中分配一段空间,将这个空间和一个符号绑定,然后往该空间内赋上指定的数据

指针变量和其他非指针普通变量的区别是:指针变量内部存储的数据应该是另一个普通变量(包括指针变量)的地址。就好像我们买了2个桶

a



p



a

用来放水,

p

用来放盐,本质上

a



p

是没有区别的,它们都是桶,都可以用来存放东西,区别就是各自应当放入的东西是不一样的。



1.2 为什么需要指针

指针的出现是为了实现间接访问。在汇编语言中也有间接访问,这其实就是在计算机组成原理课程中CPU寻址方式中的间接寻址。CPU的间接寻址是CPU设计时决定的,这也决定了汇编语言必须能够实现间接寻址,从而又决定了汇编之上的C语言也必须实现间接寻址。

尽管高级语言如

Java



C#

等没有指针,但是语言本身封装实现了间接访问。



1.3 指针使用的标准方式

指针使用的三部曲:

  • 1.定义指针变量。
  • 2.关联指针变量,其实就是给指针变量赋值,让它指向另一个变量,没有关联之前,这个指针不能被解引用。
  • 3.对指针变量解引用,通过指针访问它指向的变量。

举个栗子:

#include<stdio.h>

int main(int argc,char**argv)
{
	int a=5;
	
	//1.定义指针变量
	int* p;
	
	//2.关联指针变量,将a的地址赋值给p,让p指向a
	p=&a;

	//3.对指针变量解引用
	printf("*p = %d.\n",*p);
	return 0;
}

单纯的使用

int * p;

定义一个指针变量

p

而不通过赋值符号初始化时,因为

p

是局部变量,所以也遵循

C

语言局部变量的一般规律:定义局部变量并且未初始化则值是随机的,所以此时

p

变量中存储的是一个随机的数字。此时如果解引用

p

,则相当于我们访问了这个随机数字为地址的内存空间。那这个空间到底能不能访问不知道,也许行也许不行,所以如果直接定义指针变量未绑定有效地址就去解引用几乎一定会导致错误。

因此定义一个指针变量,不经绑定有效地址就去解引用,就好像拿一个上了镗的枪随意转了几圈然后开了一枪。指针绑定的意义就在于:让指针指向一个可以访问并且应该访问的地方,就好象拿着枪瞄准目标的过程一样;指针的解引用是为了间接访问目标变量,就好象开枪是为了打中目标一样。



2.指针带来的一些符号的理解



2.1 间接访问操作符 *



C

语言中

*

可以表示乘号,也可以表示与指针相关的操作符。这两个用法是毫无关联的,只是恰好用了同一个符号而已。

星号在用于指针相关功能的时候有2种用法:

  • 第一种是指针定义时,

    *

    结合前面的类型(如

    int



    char

    )用于表明要定义的指针的类型。
  • 第二种功能是指针解引用,例如对于指针变量

    p

    解引用时,

    *p

    表示

    p

    指向的变量本身。



2.2 取地址符&

取地址符使用时直接加在一个变量的前面,然后取地址符和变量加起来构成一个新的符号,这个符号表示这个变量的地址,可以作为右值赋给指针变量。例如

&a

得到的就是变量

a

的地址。



2.3 指针定义并初始化与指针定义然后赋值的语法区别

指针变量定义时可以初始化,指针的初始化其实就是给指针变量初值,跟普通变量的初始化没有任何本质区别。

指针变量定义同时初始化的语法是:

int a = 32; 
int *p = &a;

指针变量定义时不初始化之后再赋值的语法是:

int a = 32; 
int *p; 	
p = &a;	//正确
*p = &a;//错误,*p表示p指向的变量



2.4 左值与右值

放在赋值运算符

=

左边的就叫左值,右边的就叫右值。所以赋值操作其实就是:

左值 = 右值;

当一个变量做左值时,编译器认为这个变量符号的真实含义是这个变量所对应的那个内存空间。当一个变量做右值时,编译器认为这个变量符号的真实含义是这个变量的值,也就是这个变量所对应的内存空间中存储的那个数。

左值与右值的区别,就好象现实生活中”家”这个字的含义。比如说”我回家了”,这里面的家指的是你家的房子,类似于左值;但是说”家比事业重要”,这时候的家指的是家人,就是住在家所对应的那个房子里面的人,类似于右值。



3.野指针问题



3.1 什么是野指针

野指针就是指针指向的位置是不可知的、随机的、不正确的、没有明确限制的。野指针很可能触发运行时段错误(

Sgmentation fault

),因为指针变量在定义时如果未初始化,它的值是随机的。指针变量的值其实就是别的变量(指针所指向的那个变量)的地址,野指针意味着这个指针指向了一个地址是不确定的变量,这时候去解引用就是去访问这个地址不确定的变量,所以结果是不可知的,很可能会导致错误。

野指针因为指向地址是不可预知的,所以有3种情况:

  • 第一种是指向普通程序不可访问的地址,操作系统不允许访问的敏感地址,比如内核空间。结果是触发段错误,这种算是最好的情况了,因为会报错,可以及时发现并解决。
  • 第二种是指向一个可用的但是没什么特别意义的空间,比如曾经使用过但是已经不用的栈空间或堆空间,这时候程序运行不会出错,也不会对当前程序造成损害,但是这会掩盖程序错误,让程序员以为程序没问题,其实是有问题的。
  • 第三种情况就是野指针恰好指向了一个可用的空间,而且这个空间其实正在本程序中正在被使用,比如程序的一个变量

    a

    ,那么野指针的解引用就会刚好修改这个变量

    x

    的值,导致这个变量莫名其妙的被改变,程序出现离奇的错误,一般最终都会导致程序崩溃,或者数据被损害,这种危害是最大的。

指针变量如果是局部变量,则分配在栈上,本身遵从栈的规律:反复使用,使用完不擦除,所以是脏的,本次在栈上分配到的变量的默认值是上次这个栈空间被使用时余留下来的值,就决定了栈的使用多少会影响这个默认值。因此野指针的值是有一定规律不是完全随机,但是这个值的规律对我们没意义,因为不管落在上面野指针3种情况的哪一种,都不是我们想看到的。



3.2 如何避免野指针

野指针的错误来源就是指针定义了以后没有初始化,也没有赋值,总之就是指针没有明确的指向一个可用的内存空间就去解引用。

知道了野指针产生的原因,避免方法就出来了:在指针的解引用之前,一定确保指针指向一个绝对可用的空间。常规的4点做法是:

  • 1.定义指针时,同时初始化为

    NULL

  • 2.在指针解引用之前,先去判断这个指针是不是

    NULL

  • 3.指针使用完之后,将其赋值为

    NULL

  • 4.在指针使用之前,将其赋值绑定给一个可用地址空间。

野指针的防治方案4点绝对可行,但是略显麻烦,很多人懒得这么做。在中小型程序中,自己水平可以把握的情况下,不必严格参照这个标准。但是在大型程序,或者自己水平感觉不好把握时,建议严格参照这个方案。



3.3 NULL是什么


NULL



C/C++

中定义为:

#ifdef _cplusplus			// 定义这个符号就表示当前是C++环境
#define NULL 0				// 在C++中NULL就是0
#else
#define NULL (void *)0		// 在C中NULL是强制类型转换为void *的0
#endif



C

语言中,

int *p;

之后可以

p = (int *)0;

但是不可以

p = 0;

因为类型不相同。

所以

NULL

的实质其实就是0,只不过这个0代表的是地址,而不是一个整数,然后我们给指针赋初值为

NULL

,其实就是让指针指向0地址处。指向0地址处有2个原因:

  • 第一层原因是:0地址处被认为是一个特殊地址,指针指向这里就被认为指针没有被初始化,就表示是野指针。
  • 第二层原因是:0地址在一般的操作系统中都是不可被访问的,如果

    C

    语言程序员不按规矩(不检查是否等于

    NULL

    就去解引用)写代码直接去解引用就会触发段错误,程序员就可以发现问题然后去解决,这样的结果已经是最好的结果了。

一般在判断指针是否野指针时,都写成

if (NULL != p)

而不是写成

if (p != NULL)

原因是:如果

NULL

写在后面,当中间是

==

号的时候,有时候容易写错写成了

=

,这时候其实程序已经错误,但是编译器不会报错。这个错误(对新手)很难检查出来。但是如果习惯了把

NULL

写在前面,当错误的把

==

写成了

=

时,编译器会报错,程序员就可以发现这个错误。



4.const与指针的合用



4.1 const修饰指针的4种形式


const

关键字在

C

语言中用来修饰变量,表示这个变量是常量。指针变量也是变量,所以

const

关键字自然也可以用来修饰指针。


const

修饰指针常见的有4种形式,区分清楚这4种即可全部理解

const

和指针。以

int

类型指针为例:

  • 第一种:

    const int *p;

    ,表示

    p

    本身不是

    const



    *p



    const

  • 第二种:

    int const *p;

    ,表示

    p

    本身不是

    const



    *p



    const

  • 第三种:

    int * const p;

    ,表示

    p

    本身是

    const



    *p

    不是

    const

  • 第四种:

    const int * const p;

    ,表示

    p

    本身是

    const



    *p

    也是

    const

关于指针变量的理解,主要涉及到2个变量:第一个是指针变量

p

本身,第二个是

p

指向的那个变量,也就是

*p

。一个

const

关键字只能修饰一个变量,所以弄清楚这4个表达式的关键就是搞清楚

const

放在某个位置是修饰谁的。



4.2 const修饰的变量真的不能改吗



gcc

环境下,

const

修饰的变量其实是可以改的。在某些单片机环境下,

const

修饰的变量是不可以改的。

const

修饰的变量到底能不能真的被修改,取决于具体的环境,

C

语言本身并没有完全严格一致的要求。



gcc

中,

const

是通过编译器在编译的时候执行检查来确保实现的(也就是说

const

类型的变量不能改是编译错误,不是运行时错误。)所以我们只要想办法骗过编译器,就可以修改

const

定义的常量,而运行时不会报错。更深入一层的原因,是因为

gcc



const

类型的常量也放在了

data

段,其实和普通的全局变量放在

data

段是一样实现的,只是通过编译器认定这个变量是

const

的,运行时并没有标记

const

标志,所以只要骗过编译器就可以修改了。



4.3 const究竟应该怎么用


const

是在编译器中实现的,在代码编译时检查,因此运行时并非不能骗过。所以在

C

语言中使用

const

就好象是一种道德约束而非法律约束,所以使用

const

时更多是传递一种信息,就是告诉编译器,也告诉读程序的人,这个变量是不应该也不必被修改的。



版权声明:本文为PecoHe原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。