指针的实质
在计算机科学中,指针(Pointer)有两种含义,一是作为数据类型,二是作为实体。
指针作为实体,是一个用来保存一个内存地址的计算机语言中的变量。指针一般出现在比较底层的程序设计语言中,如C语言。高层的语言如Java一般避免用指针,而是引用。
指针作为数据类型,可以从一个函数类型、一个对象类型或者一个不完备类型中导出。从中导出的数据类型称之为被引用类型(referenced type)。指针类型描述了一种对象,其
值为对被引用类型的实体的引用。
指针实质是一个整数
计算机中的内存都是编址的,每个地址都有一个符号,就像家庭地址或者IP地址一样。在C语言的多数实现中,指针值等同于一个无符号整数(unsigned int,因不致歧义,下
简称“整数”),它是一个以当前系统寻址范围为取值范围的整数。声明一个无符号整数并使它的值等于对象的地址值,实质上也能使之有指针的作用。
32位系统的寻址能力(地址空间)是4GB(0~232-1),二进制表示长度为32比特,也就是4B。不难验证,在32位系统的大多数实现里,int类型也正好是4B(32-bit)长度,可
以取遍上述范围。同理,64位系统取值范围为0~264-1,int类型长度为8B。
例证就是程序1得到的答案和程序2的答案一致。
| 程序1 | 程序2 |
|---|---|
#include <stdio.h>
int main()
{
char *pT;
char t='h';
pT=&t;
putchar(*pT);
}
|
#include <stdio.h>
int main()
{
char *pT;
char t='h';
pT=(char *)1245048;
putchar(*pT);
}
|
指针和整数的区别
既然指针的实质是一个整数,为何不用unsigned int直接声明,或者统一用int *声明,而要用不同的类型后面加上一个“*”表示呢?char *声明过的类型,一次访问1个
sizeof(char)长度,double *声明过的类型,一次访问1个sizeof(double)长度。也正因此,程序2第6行加上“(char *)”是因为毕竟unsigned int和char *不是一回事,
需要强制转换,否则会有个警告。
在汇编里,没有数据类型这一概念,整数类型和指针就是一回事了。不论是整数还是指针,执行自增的时候,都是将原值加1。如果上文声明char *pT;,汇编语言中pT自增
(INC)之后值为1245049,可是C语言中pT++之后pT值为1245049。如果32位系统中,上文声明int *pT;,汇编语言中pT自增之后值为1245049,可是C语言中pT++之后
pT值为1245052。
为什么DOS下面的Turbo C,和Windows下的VC的int类型自增时的步进不一样长?因为DOS是16位的,Windows x86是32位的,int类型长度取决于编译器的位长。可以预
见,在Windows x64中编译,上文声明int *pT;,在执行pT++之后pT值为1245056。
指针的空间分配
基本数据类型
在程序编译或者运行时,系统[开辟了一张表。每遇到一次声明语句(包括变量的声明、函数的声明和传入参数的声明等等)都会开辟一个内存空间,并在表中增加一行纪录,记载
一些对应关系。以32位操作系统下为例:
| 声明 | 序号 | 变量名 | 内存地址 | 访问长度 | 值 |
|---|---|---|---|---|---|
| int nP; | 1 | nP | 2000 | 4B | 0xCCCCCCCC[8] |
| char myChar; | 2 | myChar | 2004 | 1B | 0xCC |
| int * myPointer; | 3 | myPointer | 2005 | 4B | 0xCCCCCCCC |
| char * myPointer2; | 4 | myPointer2 | 2008 | 4B | 0xCCCCCCCC |
高级数据类型
那么,复杂的结构怎么分配空间呢?C语言的结构体(汇编语言对应为Record类型)按顺序分配空间。
int a[20];
|
typedef struct st
{
double val;
char c;
struct st *next;
} pst;
|
pst pT[10];
|
在32位系统下,内存里面做如下分配:(单位:H,16进制)
| 虚拟地址 | 2000 | 2001 | 2002 | 2003 | 2004 | 2005 | 2006 | … | 204C | 204D | 204E | 204F |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 变量 | a[0] | a[1] | … | a[19] |
| 虚拟地址 | 2050 | 2051 | … | 2057 | 2058 | 2059 | 205A | 205B | 205C | 205D | 205E | 205F |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 变量 | pst.val | … | pst.c | pst.next | 无效 | 无效 | 无效 |
这就说明了为什么sizeof(pst)=16而不是8。涉及到内存对齐的问题 编译器把结构体的大小规定为结构体成员中大小最大的那个类型的整数倍。至于pT的存储,可以推得总长为
160。如果执行pT++,答案不是自增,也不是160。因为数组声明时,pT是常量,不能加减。
指针连接的数据类型
所以,我们就可以声明:
typedef struct BiTree
{
int value;
struct BiTree *LeftChild;
struct BiTree *RightChild;
} BTree;
用一个整数,代表一棵树的结点。把它赋给某个结点的LeftChild/RightChild值,就形成了上下级关系。只要无法找到一个路径,使得A->LC/RC->LC/RC…->LC/RC==A(A泛指某一结点),这就构成了一棵二叉树。反之就成了图。
使用指针的目的
简化代码
如果没有指针,我们很难用一个统一的模式去A的定位并修改一棵树的结点。例如:不用指针要修改A的左子树的左子树的右子结点,只有“A.LC.LC.RC=…”一种表达方式,不能
通过赋值而简化。
参数传递
C中函数调用是按值传递的,传入参数在子函数中只是一个初值相等的副本,无法对传入参数作任何改动。但实际编程中,经常要改动传入参数的值。这一点我们可以用一个小技
巧,即传入参数的地址而不是原参数本身,当对传入参数(地址)取“*”运算时,就可以直接在内存中修改,从而改动原想作为传入参数的参数值。
- 传参考 (Pass by Reference)
#include <stdio.h>
void inc(int *val)
{
(*val)++;
}
int main()
{
int a=3;
inc(&a);
printf("%d", a);
return 0;
}
在执行inc(&a);时,系统在内存分配表里增加了一行“val@inc”,其地址为新地址,值为&a。操作*val,即是在操作a了。
- 传值法 (Pass by Value)
以下例子中,main()内的变量从来没有改变,改变的只是sw()内的变量。
#include <iostream>
using namespace std;
void sw(int x, int y) {
int Temp;
Temp = x;
x = y;
y = Temp;
}
int main() {
int a=1;
int b=2;
cout << a << b << endl;
sw(a,b);
cout << a << b << endl;
return 0;
}
// Output:
// 12
// 12
sw()运行完毕后,其内容会自动删除。
| int a | int b | int x | int y |
|---|---|---|---|
| 1 | 2 | - | - |
| 1 | 2 | 1 | 2 |
| 1 | 2 | 2 | 1 |
| 1 | 2 | - | - |
指针的运算和声明
取地址和取值运算
*p(取值)操作是这样一种运算,返回p的值作为地址之内存空间的取值。&p(取址)则是这样一种运算,返回其操作数p的地址。显然可以用赋值语句对内存地址赋值。
我们假设有这么一段内存地址空间,他们取值如下:(单位:H,16进制)
| 地址 | 0000 | … | 2000 | 2001 | 2002 | 2003 | 2004 | … | 3000 | 3001 | 3002 | 3003 | … |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 取值 | ???? | … | 01 | 30 | 00 | 30 | 00 | … | 00 | 20 | 03 | 9A | … |
然后,执行这么一段代码“int *p;”,假设开辟空间时p被分配3001H、3002H两个位置。则p为2003H,*p为3000H。
**p的值为多少?
**p=*(*(p))=*(*(2003H))=*(3000H)=0020H。
那么&&p、*(&p)和&(*p)又等于多少?
&&p=&(&(p))=&(3001H),此时出错了,3001H是个常数怎么可能有地址呢?
*&p=*(&(p))=*(3001H)=2003H,也就是*&p=p。
&&p=&(&(p))=&(3000H),此时出错了,3000H是个常数怎么可能有地址呢?
指针和引用的声明注记
我们再看看另类的*和&。这里有两个地方要注意:
(1)在程序声明变量的时候的*,只是表明“它是一个整数,这个整数指向某个内存地址,一次访问sizeof(type)长度”。这点不要和*操作符混淆;
(2)在C++程序声明变量的时候的&,只是表明“它是一个引用,这个引用声明时不开辟新空间,它在内存分配表加入新的一行,该行内存地址等于和调用时传入的对应参数
内存地址”。这一点不要与“*”声明符和“&”操作符混淆。
指针的复杂形式
双重指针(指向指针的指针)
双重指针是指向指针的指针,它是一个整数,这个整数指向某个内存地址,该地址的值是一个整数,指向给另一个内存地址(通常异于前者,但不排除二者相等)。综合前述
的BTree定义,对于一棵树,我们通常用它的根结点地址来表示这棵树。正所谓“擒贼先擒王”,找到了树的根,其每个结点都可以找到。
但是有时候我们需要对树进行删除结点,增加节点操作,往往考虑到删除根结点,增加的结点取代原来的根结点作为新根结点的情况。为了修改根结点这个“整数”,我们需要退
一步,使用这个“整数”的内存地址,也就是指向这个“整数”的指针。在声明时,我们用2个*号,声明指向指针的指针。它的意思是“它是一个整数,这个整数指向某个内存地
址,一次访问sizeof(unsigned int)长度,其指向的内存地址所存储的值是一个整数,那个整数值指向某个内存地址,一次访问sizeof(BTree)长度。”详见数据结构有关
“树”的程序代码。
指针数组
指针数组:就是一个数组,数组的各个元素都是指针值。
数组指针
数组名出现在表达式中时,很多情况下(除了数组名作为sizeof的操作数或者作为取地址&元素符的操作数)会被隐式转换为指向数组的首个元素的指针右值表达式。
当数组名作为取地址&元素符的操作数,则表达式的值为指向整个数组的指针右值表达式。
例子:
char s[]="hello";
int main() {
char (*p1)[6]=&s; //OK!
char (*p2)[6]=s; //compile error: cannot convert 'char*' to 'char (*)[6]'
char *p3=&s;//compile error: cannot convert 'char (*)[6]' to 'char*'
}
根据上述C语言标准中的规定,表达式 &s 的值的类型是char (*)[6],即指向整个数组的指针;而表达式 s 则被隐式转换为指向数组首元素的指针值,即 char* 类型。同理,表达式 s[4] ,等效于表达式 *(s+4)。
指向函数的指针
函数指针
指向函数的指针:不同于指向数据类型的指针,函数指针指向一段可执行的代码。很多人都说C语言是一种面向过程的语言,因为它最多只有结构体的定义,而没有类的概念。根据本段所述,我们可以认为C语言能成为面向对象的语言,只是表述比较麻烦而已。事实上很多开源程序都使用这种方式组织他们的代码。
#include <stdio.h>
void inc(int *val)
{
(*val)++;
}
int main(void)
{
void (*fun)(int *);
int a=3;
fun=inc;
(*fun)(&a);
printf("%d", a);
return 0;
}
指针的进化与取代
由于指针太活跃,因此导致它几乎能不受限制的在各种存储器地址间活动,所以一旦有任何重复、重叠、溢出的情形发生时,电脑便直接死机,这成为指针功能上的最大缺憾。因
此在新的网络编程语言的开发上,新的语言如java、c#等语种已经取消了指针的无限制使用形式。 C#允许指针的有限功能的使用,指针和运算指针在一个操作的环境中是存在潜
在的非安全性的,因为他们的使用可以避开对象的一些严格访问规则。C#中使用指针的代码段或者方法的地址要用unsafe关键字进行标记,这样,这些代码的用户就会知道这个代
码相比其他的代码而言是不具有安全性的。编译器需要unsafe关键字时将使用此代码的程序转换成是允许被编译的。一般来说,不安全代码的使用可能是为了非托管的API(应用
程序编程接口)的更好互用,或者是为了(存在内在不安全性的)系统调用,也有可能是出于提高性能等方面的原因。而Java中不允许指针或者算术指针的使用。
posted on 2014-11-15 09:29 海角天涯TOwang 阅读(284) 评论(0) 收藏 举报
浙公网安备 33010602011771号