您的位置:首页 > 编程语言 > C语言/C++

C语言指针详解(1)-概述

2015-05-16 13:07 155 查看
指针是C语言的精华,用指针可以写出更加快速高效的代码,因为指针更加接近硬件,指针附带的开销一般不像别的操作符那么大,而且应用起来也更加灵活。

(1)理解指针

为了形象的理解指针的含义,我们举个例子:我们写代码时会定义变量,假如定义 int a ;初始化a = 4;这里a是一个代号就和名字一样,4就是存在这个代号里的变量,计算机里面变量都是存放在内存里的,假设内存是以字节为单位并且有地址的,我们要说的指针就是这个地址,int是整型变量,一般分配4个字节,即给a这个代号的整型变量分配4个字节,来存放4这个数。就像在仓库一样,假如每个仓库都有一个仓库号,就和计算机的内存地址一样,每个仓库都有自己的名字,就和变量a的名字一样,而且仓库里面都存放这不同的东西,就和4这个数一样,例如大米粮仓这个代号的仓库存放了3吨大米,这个仓库的编号是6号仓库,那么我们就可以和计算机存放数据类比起来。我们要索引这3吨大米的时候,可以说大米粮仓,或者也可以先去找6号仓库,然后六号仓库里面放的就是这3吨大米,这个编号6就是地址。很明显如果所有仓库都是按好排列的,那么我们根据编号来找速度更快,效率更高,但是我们并不知道6号仓库里是什么,所以记录的时候要写大米粮仓,这和计算机存储数据是一个原理。

经过举例子我们大概理解了计算机地址的原理,下面我们详细谈一谈指针:指针就是存储单元的地址,要和存储单元的内容分离开,内容是存储单元里面实际有的东西,而地址是找到存储单元的编号。如图所以,假设程序定义3个整型变量i,j,k,在程序编译时,系统可能分配地址为2000~2003的4个字节给变量i=3,2004~2007的4个字节给变量j=6,2008~2011的4个字节给变量k=i+j(注意不同的编译系统在不同次的编译中,根据自身内存状况,分配给变量的存储单元的地址是不一样的)。当执行语句k=i+j时,就从2000~2003字节取出i的值3,再从2004~2007字节取出j的值6;然后将他们相加后再将和9送到k所占内存2008~2011字节单元中。这种变量名访问的方式叫做“直接访问”。



(2)声明指针

通过在数据类型后面跟星号(*),再加上指针变量的名字可以声明指针,下面声明一个整数和一个整数指针:

int num;
int *pi;


(注:星号两边的空白符无关紧要,左右有或没有都可以)

我喜欢用int* pi命名,因为这样看起来更像这个类型是int*,而变量名为pi,方便理解,星号是一个重载过的符号,因为它也可以用在乘法和解引指针上。下图说明了内存分配是什么样的,在此不再赘述。这三个内存单元是未初始化的。





指向未初始化的内存的指针可能会产生问题,如果解引这种指针,指针的内容可能并不是一个合法的地址,就算是合法地址那个地址也可能包含不合法数据,程序没有权限访问不合法地址。说明几点:(1)pi的内容最终应该赋值为一个整型变量的地址。(2)这些变量未初始化,所以包含的是垃圾数据(垃圾数据即内存中可能包含任何数据,因为内存刚分配的时候是不会被清理的,之前的内容可能是任何东西)。(3)指针的实现中没有内部信息表明自己指向的是什么类型的数据或者内容是否合法。(4)指针有类型,如果不正确使用,编译器会频繁报错。

(3)地址操作符/初始化指针

地址操作符&会返回操作数的地址,我们可以用这个操作符来初始化指针pi,如num = 0;pi = #num变量设置为0,pi设置为指向num的地址。 如上图右侧所示。也可以在声明变量pi的时候同时把它初始化为num地址,即:int num = 0;int* pi = #

(4)解引指针

简介引用操作符 * 返回指针变量指向的值,一般称为解引指针。下面声明和初始化num和pi:int num = 5;int* pi = #下面语句就用解引操作符来显示5,也就是num的值:printf(”%p\n”,*pi);//输出5。我们也可以把解引操作符的结果作为左值赋值:*pi = 200,则num此时就会输出200,这就是通过指针改变变量值。

(5)null的概念

null很有趣,但是又经常被弄混,一般有以下几种:

①null概念;

②null 指针常量;

③NULL宏;

④ASCII字符NULL;

⑤null字符串;

⑥null语句。

null概念是通过null指针常量来支持的一种抽象。这个常量可能是也可能不是常量0。null是指指针包含一个特殊值,它没有指向任何内存区域,两个null指针总是相等的,而且每种指针类型都可以有对应的null指针类型。NULL宏是强制类型转换为void指针的整数常量0:#define NULL ((void *)0) ,NULL被赋值给指针就意味着不指向任何东西(注意此处可以将NULL赋值给任意类型的指针)。

上一段说null指针常量可以也可以不是常量0,如果编译器使用一个非0的位串来表示null,那么编译器就有责任在指针上下文中把NULL或者0当作null指针,实际null内部表示由实现定义,使用NULL或0实在语言层面表示null指针的符号。所以我们可以给指针赋0,但是不能赋任何其他的整数值。pi = 0和pi = NULL都可以,但是哪一种更好那,哪一种都可以。但是NULL不应该用在指针之外的上下文中,有时候可能有用,但是不能这么用,如果代替ASCII字符NUL的话肯定又问题。而0的含义会随着上下文的变化而变化,有时候可能是整数,有时候是null指针,例如:int num = 0;int* pi = 0(这里0表示null的指针NULL);pi = #*pi = 0(这里的0表示整数0);我们只是习惯了操作符重载,其实0也被重载了。这是操作数重载。

ASCII字符NUL表示全0的字节。然而这和null指针不一样。C字符串表示以0结尾的字符序列。null字符串表示空字符串,不包含任何字符。null语句就是只有一个分号(;)的语句。

注意:null指针和未初始化的指针不同。未初始化的指针可能包含任何值(即垃圾数据),而包含NULL的指针则不会引用内存中任何地址。

(6)void指针

void指针是通用指针,用来存放任何数据类型的引用,例如:void* pv;任何指针都可以被赋给void指针,它可以转换会原来的指针类型,这样的话指针的值和原指针的值是相等的。例如:

int num;
int* pi = #
printf("Value of pi: %p\n",pi);
void* pv = pi;
pi = (int*)pv;
printf("Value of pi: %p\n",pi);


运行结果是一样的。void指针只做数据指针,而不能做函数指针。

(7)全局和静态指针

指针被声明为全局或者静态时,就会在程序启动时被初始化成NULL。而且被分配到堆内存。

(8)指针的长度和类型

如果考虑应用程序的兼容性和可移植性,指针长度就是一个问题。在大部分现在平台上,指针长度通常是一样的,与指针类型无关,char指针和结构体指针长度相同。函数指针可能与数据指针长度不同。指针长度取决于所使用的机器和编译器,比如现在的windows平台上,指针是32位或者64位长,这就是平时所说的操作系统是32位的还是64位的,着涉及到内存寻址,32位系统的最大寻址空间是2 的32次方= 4294967296(bit)(位)= 4(GB)左右,而64位系统的最大寻址空间的寻址空间则达到了2的64次方= 4294967296(bit)的32次方=数值大于1亿GB,换言之,就是说32位系统的处理器最大只支持到4GB内存,而64位系统最大支持的内存高达亿位数,但是我们平时见到的64位系统一般支持到128GB,因为现在的64位非真正的64位,只是在32位上面的改装,所以并没有支持到上亿GB。

①内存模型

64位机器的出现导致不同数据类型分配的内存在长度上的差异变得明显,不同机器和编译器在给C的基本数据类型分配空间时有不同的做法。一种操作系统往往支持多种内存模型,让编译器来选择控制。

②指针相关的预定义类型

使用指针时经常会用到一下四中预定义类型:size_t(用于安全地表示长度),ptrdiff_t(用于处理指针算数运算),intptr_t和uintptr_t(用于存储指针地址)。

理解size_t:size_t类型表示C中任何对象所能达到的最大长度。它是无符号整数,它的目的时提供一种可移植的方法来声明与系统中可寻址的内存区域一致的长度。size_t用作sizeof操作符的返回值类型,同时也是很多函数的参数类型,包括malloc和strlen。size_t的声明是实现相关的,比如在stdio.h中

#ifndef _SIZE_T
#define _SIZE_T
typedef unsigned int size_t;
#endif


实际的长度取决于实现,通常来说32位系统上长度时32位,64位系统上是64位。

sizeof操作符使用:sizeof操作符可用来判断指针长度,如printf(“Size of char: %d\n”,sizeof(char));//输出Size of *char:4,函数指针的长度是可变的,对于给定的操作系统和编译器组合,它是固定的,很多编译器支持创建32位和64位的应用程序,所以同一个程序使用不同的编译会使用不同的指针长度。

intptr_t和uintptr_t:intptr_t和uintptr_t类型用来存放指针地址,它们提供了一种可移植且安全的方法声明指针,而且和系统中使用的指针长度相同,对于把指针转换成整数形式来说很有用,uintptr_t是intptr_t的无符号版本,对于大部分操作,用intptr_t比较好,uintptr_t不像intptr_t那样灵活。下面介绍如何使用:

int num;
intptr_t* pi = #


如果像下面这样把整数地址赋值给uintptr_t类型指针,我们会得到一个语句错误

uintptr_t* pu = #


错误是error:invalid conversion from ‘int*’ to ‘uintptr_t* {aka unsigned int*}’ [-fpermissive],不过可以强制转化,

intptr_t* pi = #
uintptr_t* pu = (uintptr_t*)#


当可移植性和安全性变得重要时,就应该使用这些类型。避免把指针转化成整数,如果指针是64位的,整数只有4个字节时就会丢失信息。

(9)指针操作符

1.给指针加上整数:

给指针加上一个整数实际上加的就是这个整数和指针数据类型对应字节数的乘积。

int vector[] = {28,41,7};
int *pi = vector;        //pi:100
printf("%d\n",*pi);      //显示28
pi += 1;                 //pi:104
printf("%d\n",*pi);      //显示41
pi += 1;                 //pi:108
printf("%d\n",*pi);      //显示7


指针减法和加法原理一样,不再赘述。

2.void指针和加法

作为扩展,大部分编译器都允许给void指针做算术加法,这里假设void指针长度为4个字节。不过试图给void指针加1可能导致语法错误。

int num = 5;

void* pi = #

printf(“%p\n”,pv);

pv +=1; //语法警告

warning: pointer of type ‘void*’ used in arithmetic [-Wpointerarith]

这不是标准C允许的,所以编译器发出了警告,不过,pv包含的地址增加了4个字节。

3.指针相减和比较

一般来说,两个指针相减和比较是没什么意义的,不过可以比较一个数组中的元素的顺序。

注:上文内容很多引自图灵教育的《深入理解C指针》一书,本人不享有任何版权。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: