【C语言总结】C语言指针

指针

1.内存和地址

我们可以把计算机的内存看成是一条长街上的一排房屋,每座房子都可以容纳数据,并且通过一个房号来标识。

计算机的内存由数以万计的位(bit)组成,每个位可以容纳0或1,由于一个位所能表示的范围太有限,所以通常许多位合成一组作为一个单位,这样就可以存储范围较大的值。
这里写图片描述

这幅图栈是了现实机器中一些内存位置,这些位置的每一个都被称为字节(byte),每个字节都包含了存储一个字符所需要的位数。

为了存储更大的值,我们把两个或多个字节合在一起组成一个更大的内存单位,例如许多机器上以字为单位存储整数,每个字一般由2个到4个字节组成。

注意,有时候一个字包含了四个字节,它仍然只有一个地址,至于是左边那个字节的地址还是右边那个字节的地址,不同的机器有不同的规定,但是它一定只有一个地址表示该字。

另一个需要注意的硬件事项是内存对齐。在要求边界对齐的机器上,整型值的存储的起始位置只能是某些特定的字节,通常是2或4的倍数。

所以,1.内存中的每个位置都有一个独一无二的地址标识
2.内存中的每个位置都包含一个值
地址与内容

这里有一个例子,下图显示了内存中五个字的内容:
这里写图片描述

这里显示了五个整数,如果你记住了地址,可以通过地址取得这个值,但是显然太麻烦而且不现实,所以高级语言的特性之一就是通过名字而不是地址来访问内存的位置,下面这张图使用名字来替代地址:
这里写图片描述

这些名字就是我们所陈的变量,注意,名字与内存之间的位置并不是硬件提供的,它是由编译器为我们实现的,硬件仍然通过地址访问内存位置。

2.值和类型

看一下上面的五个数,头两个位置存储的数是个整数,第三个十个非常大的整数,第四个和第五个也是两个整数,下面是这些变量的声明:

int a = 112,b = -1;
float c = 3.14;
int* d = &a;
int* e = &c;

在这些声明中,变量a、b确实用于存储整型值,但是c所存储的是浮点值,却存储的是个整数,因为在内存中,存储的值是一系列0和1的组合,它可以被解释为整数,也可以被解释为浮点数,这取决于他们被使用的方式。如果使用的是整型算数指令,这个值就被解释为整数,如果使用的是浮点型指令,他就是个浮点数。

所以,不能简单的通过检查一个值来判断它的类型。

3.指针变量的内容

这里写图片描述

d和e的内容是地址而不是整型或浮点型数值,d的内容与a的存储地址一致,而e的内容和c的存储地址一样,这也正是对这两个指针进行初始化所期望的结果。
看一下几个声明:

int a = 102, b = -1;
int c =  3.14;
int *d = &a;
int *e = &c;

a的值是102,b的值是-1,c的值是3.14,d的值是100,e的值是108,需要注意的是,变量的值就是分配给该变量的内存位置所存储的数值,即使是指针变量也不例外,那么我们如何通过指针变量获得指针所指向的内容呢?

4.间接访问操作符

通过一个指针访问它所指向的地址的过程称之为间接访问或解引用指针。用于执行间接访问操作的是单目操作符*

5.未初始化和非法的指针

int* a;
*a = 12;

这个代码是先声明了一个指针变量a,然后把一个整型数12存储在a所指向的内存位置

警告:a究竟指向哪里呢?我们声明了这个变量,但是未对它进行初始化,所以我们没有办法预测12这个值将会存储于什么地方,声明一个指向整型的指针都不会“创建”用于存储整型值的内存空间。

所以,如果程序执行这个操作,如果运气好,这个地址是个非法地址,所以赋值语句将会出错,在Unix系统上,这个错误成为段违例或内存错误,在一个windows的系统上,对未初始化或非法指针进行间接访问的访问操作是一般保护性异常的根源之一。

对于那些要求整数必须存储于特定边界的机器而言,如果这种类型的数据在内存中的存储地址处在错误的边界上,那么对这个地址访问将会产生一个错误。在Unix上称为总线错误。

所以,对指针进行间接访问之前,必须确保已经被初始化。

6.NULL指针

标准定义了NULL指针,它表示不指向任何东西,要使一个指针变量变成NULL值,只需要给它赋个零值,之所以使0是因为这是个源代码约定,机器内部而言,NULL指针的实际值可能不同,在这种情况下,编译器将负责零值和内部值之间的翻译转化。

NULL指针使非常有用的,它给了我们一种方法,它表示特定的指针目前并未指向任何东西。

有一个小技巧:一个用于在某个数组中查找某个特定值的函数可能返回一个指向查找到的数组元素的指针,如果该数组不包含指定的值,函数就返回一个NULL指针。
尽管这个技巧在C里很常用,但是违背了软件工程的原则:用一个单一的值表示两种不同的意思是非常为危险的事,因为很可能无法弄清哪个才是它的真正用意。

对指针进行解引用可以获得它所指向的值,但是从定义上看,NULL指针并未指向任何东西,所以对一个NULL指针进行解引用操作是违法的。

7.指针、间接访问和左值

设计指针的表达式能不能作为左值?通过对优先级表格查询可以得知,间接访问操作符所需要的操作数是个右值,但这个操作符所产生的结果是个左值。
给定下面的声明:

int a;
int* b = &a;

这里写图片描述

指针变量可以作为左值,并不是因为它们是指针,而是因为它们是变量。对指针变量进行间接访问表示我们应该访问指针所指向的位置,间接访问指定了一个特定的内存位置,这样我们可以把间接访问表达式作为左值使用,例如:

*b = 10 - *b;
b = 10 - *d;

第一条语句包含了两个间接访问操作,右边的间接访问作为右值使用,所以它的值是b所指向的位置所存储的值,左边的间接访问作为左值使用,所以b所指向的位置(a)把赋值符右边的表达式的计算结果作为它的新值。

第二条语句是非法的,因为它表示把一个整型值(10 - *b)存储于一个指针变量中,这种情况编译器会判断错误所在。

8.指针、间接访问和变量

例子:

*&a = 25;

&操作符产生变量a的地址,它是一个指针常量(注意,我们并不知道这个指针常量的具体值),接着,操作符*访问其操作数表示的地址,因为是右值,所以表达式就是把25存入变量a中。

9.指针常量

如果假定变量a存储于位置100,下面这条语句:

*100 = 25;

看上去像是把25存入变量a中,但是这是非法的,因为字面值100的类型是整型,而间接访问操作只能作用于指针类型表达式,如果想把25存入位置100,必须使用强制类型转换:

*(int*)100 = 25;

强制类型转换把值100从“整型”转换为“指向整型的指针”,这样对它进行间接访问操作就是对的。但是使用这个方法的可能性几乎为0,因为我们通常无法知道编译器把某个特定的值存储于哪个位置。

10.指针的指针

考虑下面的代码:

int a = 12;
int* b = &a;
int** c = &b;

他们在内存中模样大致如下:
这里写图片描述
变量b是一个“指向整型的指针”,所以任何指向b的类型必须是指向“指向整型的指针”的指针,通俗的说就是指针的指针

表达式**c的类型是int。

11.指针表达式

首先来看一些声明:

char ch = 'a';
char *cp = &ch;

它们的初始化如下:
这里写图片描述

图中还显示了ch后面的内存位置,由于我们并不知道其中储存的内容所以用问号代替。

表达式 ch :

当它作为右值使用时,表达式的值为’a’,如下图:
这里写图片描述

圆圈提示变量ch的值就是表达式的值,但是当这个表达式作为左值使用时,它表示是这个内存的地址而不是地址所包含的值,如下图:
这里写图片描述

此时该位置用粗方框标记,提示这个位置就是表达式的结果。

表达式 &ch:
这里写图片描述

作为右值,这个表达式的值是变量ch的地址。但是它不能左左值,因为当表达式进行求值的时候,它的结果存储与计算机的某个地方,但是我们并不知道它位于何处,所以它不是一个合法的左值

表达式cp:
这里写图片描述

它的右值是cp的值,左值就是cp所处的内存位置。

表达式&cp:
这里写图片描述

这个结果的和&ch类似,不过它的类型是指向字符的指针的指针,是个右值但是不是个合法的左值。

表达式*cp:
这里写图片描述

加入间接访问操作符,结果很显然。

表达式 *cp + 1 :
这里写图片描述

*的优先级高于+,所以首先执行间接访问操作,我们可以得到它的值,我们取得这个值(a)的一份拷贝并把它加1,表达式的最终结果未字符’b’。

这个表达式最终结果的存储位置并未清晰定义,所以它不是一个合法的左值。

表达式*(cp + 1):
这里写图片描述

把1和cp所存储的地址相加,然后结果 是图中的那个虚线椭圆所示的指针,接下来间接访问操作随着箭头访问ch之后的内存位置。

这个表达式的右值就是这个位置的值,而它的左值是这个位置本身。

尽管cp + 1本身并不是左值,但是*(cp + 1) 就可以作用左值使用,间接访问操作符是少数几个其结果为左值的操作符之一。注:尽管能作为表达式左值,但是我们不知道该表达式原先所存储的位置内容是什么,所以该表达式作为左值是非法的。

表达式 ++cp :
这里写图片描述

在这个表达式中,先增加了指针变量cp的值,(省略了加法),表达式的结果是增值后的指针的一份拷贝,因为前缀++先增加它的操作数然后再返回这个结果,这份拷贝的存储位置并未清晰定义,所以不是一个合法的左值。

表达式cp++ :
这里写图片描述

后缀++操作符先返回cp的值然后再增加cp的值,这样表达式的值就是cp原来值的一份拷贝,这个表达式也不是一个合法的左值。

表达式 *++cp :
这里写图片描述

间接访问操作符用于增值后的指针的拷贝,所以它的右值是ch后面那个内存地址的值,而左值就是那个位置本身。

表达式*cp++ :
这里写图片描述

后缀++的左值和右值分别是变量ch的内存位置和ch的值本身。

注意:优先级表格显示后缀++的操作符的优先级高于*操作符!

这个表达式涉及三个步骤:

1.++操作符产生cp的一份拷贝;

2.然后++操作符增加cp的值;

3.然后在cp的拷贝值上执行间接访问操作。

表达式 ++*cp :
这里写图片描述

这个表达式中,这两个操作符的结合性是从右向左,所以首先执行的是间接访问操作,然后cp所指向的位置增加1,表达式结果是这个增值后的值的一份拷贝。

表达式 (*cp)++:
这里写图片描述

这个同上一个基本一样,但是这个表达式的结果值是ch增值前的原值。

表达式 ++*++cp :
这里写图片描述

这个表达式并不复杂,共有三个操作符,先看*++cp, 我们要做的就是增加它的结果值。这两个操作符的结合性是从右向左,首先执行++cp,cp下面的虚线椭圆表示第一个中间结果,然后对这个结果进行间接访问,它使我们访问ch后面的那个内存位置。第二个中间结果用虚线方框表示,下一个操作符把它当作左值使用,对这个数进行++操作,然后产生一份拷贝。

表达式++*cp++ :
这里写图片描述

这个表达式和前一个表达式的区别在于这次第一个++操作符使后缀形式而不是前缀形式,由于优先级较高所以先执行它,间接访问操作访问的使cp所指向的位置而不是cp所指向后面的那个位置。

12.指针运算

指针可以执行某些运算,但是并非所有的运算都是合法的。

指针加上一个整数的结果是另一个指针,但是,它指向哪里,如果把一个字符指针加1,运算结果产生的指针指向内存中的下一个字符,float占据的内存空间不止一个,对float指针加1,指针将跳过一个合适的大小,这个合适的大小就是指针所指向类型的大小。
这里写图片描述

12.1算数运算

C的指针算数运算只限于两种形式,
第一种形式是:
指针 +- 整数

标准定义这种形式只能用于指向数组中某个元素的指针,并且这类表达式的结果类型也是指针这种形式也是用于使用malloc函数动态分配获得的内存。

第二种形式是:
指针 - 指针

只有当两个指针都指向同一个数组中的元素时,才允许从一个指针减去另一个指针,两个指针相减的结果的类型是ptrdiff_t,它是一种有符号整数类型,减法运算的值是两个指针在内存中的距离(以数组元素的长度为单位)。
12.2关系运算

对指针执行关系运算也是有限制的,用下列操作符大多数情况是可以的:

< <= > >=

不管前提是它们都是指向同一个数组中的元素。

注:标准允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针进行比较,但是不允许指向数组的第一个元素之前的那个内存位置的指针进行比较

一些警告:
1.错误的对一个未初始化的指针进行解引用

2.错误的对一个NLULL指针进行解引用

3.向函数错误的传递NULL指针

4.未检测到指针表达式的错误,从而导致不可预料的结果

5.对一个指针进行减法错误,使它非法的指向了数组第一个元素的前面的内存位置

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值