含义
从根本上看,指针是一个值为内存地址的变量(或数据对象)。指针变量的值是地址。
要创建指针变量,先要声明指针变量的类型
作用
1.实现复杂的数据结构,例如数组、链表、队列和堆栈等;
2.能方便地表示和处理字符串;
char s1[20]={'a','b','\0'},*sp1;
sp1=s1; //s1和sp1都代表字符串"ab"
3.实现动态存储分配;
对于程序中所包含的大存储量的数据对象,一般用预先定义指针变量来表示,当实际使用时才临时申请实际的存储空间,使用完毕立即释放。
指针变量所占的内存空间与所表示的数据对象的存储空间相比实在是微乎其微,因为它只是用来存放对应空间的首地址,而不是存放数据本身,所以可以节省大量的存储空间,提高内存空间的利用率。
4.在函数之间进行数据的双向传递;
将形参定义成指针类型,对应的实参必须是某个数据对象的地址或首地址,也即采用传地址的方式,这样就可以实现数据的双向传递了。
类型
指针类型属于非标准类型,其取值是所表示的数据对象的内存地址,所以其值域是内存地址集。
指针类型用来定义各种类型的指针变量。
<类型标识符>*
类型标识符,是指针类型的基类型,也即指针所指向的数据对象的类型。
*,作用在各个标识符上,表示该标识符所标识的变量是指针变量。
指针类型定义和指针变量声明是同时进行的。
int *a,b; //既定义了整型指针int*,又声明了整型指针变量a和整型变量b。
声明指针
类型 * 名称
指针实际上是一个新类型,不是整数类型。
指针变量的作用域
指针变量也有全局和局部之分:在函数外部声明的指针变量是全局的;在函数内部声明的指针变量是局部于该函数的。
指针变量的初始化
可以在定义指针变量的同时给其赋初值,指针的初值是某个数据对象的内存地址,也使得该指针指向对应的数据对象。
这一过程也称为建立指针。
int i=10;
int *iptr=&i; //在声明指针变量iptr的同时赋予初值,其初值是i的地址。
说明
1.标识符前面的"*"并不是名称的一部分,而表示该数据对象的类型为指针类型,也即声明该数据对象是指针类型数据对象。
2.指针变量可以和其它变量在同一语句中声明。
double d1,*d2;
3.指针变量只能存放相同基类型数据对象的内存地址,话句话说,一个指针变量在任何时候都只能指向同一基类型的数据对象。
char*c;
int i;
......
c=&i; //错误,因为c只能指向字符串
使用指针在函数间通信
一般而言,可以把变量相关的两类信息传递给函数。
1.function1(x)
传递的是x的值
2.function2(&x)
传递的是x的地址
如果要计算和处理值,那么使用第1种形式的函数调用;如果要在被调函数中改变主调函数的变量,则使用第2种形式的函数调用。
变量:名称、地址和值
通过前面的讨论发现,变量的名称、地址和变量的值之间关系密切。
编写程序时,可以认为变量有两个属性:名称和值(还有其他性质,如类型,暂不讨论)。计算机编译和加载程序后,认为变量也有两个属性:地址和值。地址就是变量在计算机内部的名称。
在许多语言中,地址都归计算机管,对程序员隐藏。然而在 C 中,可以通过&运算符访问地址,通过*运算符获得地址上的值。例如,&barn表示变量barn的地址,使用函数名即可获得变量的数值。例如,printf("%d\n",barn)打印barn的值,使用*运算符即可获得储存在地址上的值。如果pbarn=&barn;,那么*pbarn表示的是储存在&barn地址上的值。
简而言之,普通变量把值作为基本量,把地址作为通过&运算符获得的派生量,而指针变量把地址作为基本量,把值作为通过*运算符获得的派生量。
虽然打印地址可以满足读者好奇心,但是这并不是&运算符的主要用途。更重要的是使用&、*和指针可以操纵地址和地址上的内容,如swap3.c630程序(程序清单9.15)所示。
数组和指针
现在可以更清楚地定义指向int的指针、指向float的指针,以及指向其他 数据对象的指针。
指针的值是它所指向对象的地址。地址的表示方式依赖于计算机内部的 669 硬件。许多计算机(包括PC和Macintosh)都是按字节编址,意思是内存中 的每个字节都按顺序编号。这里,一个较大对象的地址(如double类型的变 量)通常是该对象第一个字节的地址。
在指针前面使用*运算符可以得到该指针所指向对象的值。
指针加1,指针的值递增它所指向类型的大小(以字节为单位)。
可以使用指针标识数组的 元素和获得元素的值。从本质上看,同一个对象有两种表示法。实际上,C 语言标准在描述数组表示法时确实借助了指针。
函数 、数组和指针
只有在函数原型或函数定义头中, 才可以用int ar[]
代替int * ar
。
int *ar
形式和int ar[]
形式都表示ar是一个指向int的指针。但是,int ar[]
只 能用于声明形式参数。第2种形式int ar[]
提醒读者指针ar指向的不仅仅 一个int类型值,还是一个int类型数组的元素。
声明数组形参
因为数组名是该数组首元素的地址,作为实际参数的数组名要求形式参 数是一个与之匹配的指针。只有在这种情况下,C才会把int ar[]
和int * ar
解 释成一样。也就是说,ar是指向int的指针。由于函数原型可以省略参数名, 所以下面4种原型都是等价的:
int sum(int *ar, int n);
int sum(int *, int);
int sum(int ar[], int n);
int sum(int [], int);
但是,在函数定义中不能省略参数名。下面两种形式的函数定义等价:
int sum(int *ar, int n)
{
// 其他代码已省略
}
int sum(int ar[], int n);
{
//其 他代码已省略
}
可以使用以上提到的任意一种函数原型和函数定义。
使用指针形参
函数要处理数组必须知道何时开始、何时结束。sum()函数使用一个指 针形参标识数组的开始,用一个整数形参表明待处理数组的元素个数(指针 形参也表明了数组中的数据类型)。但是这并不是给函数传递必备信息的唯 一方法。还有一种方法是传递两个指针,第1个指针指明数组的开始处(与 前面用法相同),第2个指针指明数组的结束处。
指针表示法和数组表示法
使用数组表示法,让函数处理数组的这一意图更加明显。
指针表示法更接近机器语言,因此一些编译器在编译时能生成效率更高的代码。
指针操作
赋值
可以把地址赋给指针。
解引号
* 运算符给出指针指向地址上存储的值
取址
和所有变量一样,指针变量也有自己的地址和值。对指针而言, &运算符给出指针本身的地址。
指针与整数相加
可以使用+运算符把指针与整数相加,或整数与指针 相加。无论哪种情况,整数都会和指针所指向类型的大小(以字节为单位) 相乘,然后把结果与初始地址相加。如果相加 的结果超出了初始指针指向的数组范围,计算结果则是未定义的。除非正好 超过数组末尾第一个位置,C保证该指针有效。
递增指针
递增指向数组元素的指针可以让该指针移动至数组的下一个元素。
指针减去一个整数
可以使用-运算符从一个指针中减去一个整数。指针必须是第1个运算对象,整数是第二个运算对象。该整数将乘以指针指向类型的大小(以字节为单位),然后用初始地址减去乘积。如果相减的结果超出了初始指针所指向数组的范围,计算结果则是未定义的。除非正好超过数组末尾第一个位置,C保证该指针有效。
递减指针
前缀和后缀的递增或递减运算符都可以使用。
指针求差
可以计算两个指针的差值。通常,求差的两个指针分别指向同一个数组的不同元素,通过计算求出两元素之间的距离。差值的单位与数组类型的单位相同。只要两个指针都指向相同的数组(或者其中一个指针指向数组后面的第1个地址),C都能保证相减运算有效。如果指向两个不同数组的指针进行求差运算,可能会得出一个值,或者导致运行时错误。
比较
使用关系运算符可以比较两个指针的值,前提是两个指针都指向相同类型的对象。
PS:这里的减法有两种。可以用一个指针减去另一个指针。
在递增或递减指针时还要注意一些问题。编译器不会检查指针是否仍指向数组元素。C只能保证指向数组任意元素的指针和指向数组后面的第一个位置的指针有效。但是,如果递增或递减一个指针后超出了这个范围,则是未定义的。另外,可以解引用指向数组任意元素的指针。但是,即使指针指向数组后面一个位置是有效的,也不能保证可以解引这样的越界指针。
解引用未初始化的指针
千万不要解引用未初始化的指针。
创建一个指针时,系统只分配了存储指针本身的内存,并未分配存储数据的内存。因此,在使用指针之前,必须先用已分配的地址初始化它。
指针的基本用法
1.在函数间传递信息。
2.用在处理数组的函数中。
传递值还是传递指针?
编写一个处理基本类型(如,int)的函数时,要选择是传递int类型的值还是传递指向int的指针。通常都是直接传递数值,只有程序需要在函数中改变该数值时,才会传递针。对于数组别无选择,必须传递指针,因为这样做效率高。如果一个函数按值传递数组,则必须分配足够的空间来储存原数组的副本,然后把原数组所有的数据拷贝至新的数组中。如果把数组的地址传递给函数,让函数直接处理原数组则效率要高。
传递地址会导致一些问题。C 通常都按值传递数据,因为这样做可以保证数据的完整性。如果函数使用的是原始数据的副本,就不会意外修改原始数据。但是,处理数组的函数通常都需要使用原始数据,因此这样的函数可以修改原数组。
空字符和空指针
从概念上看,两者完全不同。空字符(或'\0')是用于标记C字符串末尾的字符,其对应字符编码是0。由于其他字符的编码不可能是 0,所以不可能是字符串的一部分。
空指针(或NULL)有一个值,该值不会与任何数据的有效地址对应。通常,函数使用它返回一个有效地址表示某些特殊情况发生,例如遇到文件结尾或未能按预期执行。
空字符是整数类型,而空指针是指针类型。两者有时容易混淆的原因是:它们都可以用数值0来表示。但是,从概念上看,两者是不同类型的0。
另外,空字符是一个字符,占1字节;而空指针是一个地址,通常占4字节。
指针运算
指针的赋值
1.赋空值(NULL)
2.赋予某个变量的地址
3.将一个指针变量的值赋予另一指针变量
int grade,*p;
p=&grade;
指针的加减运算
运算符:+、-、++、--
一个指针量加上(或减去)一个整型量n,表示地址偏移了n个单位,具体向上或向下偏移多少字节,取决于其基类型。
例如:一个整型指针变量加上4等于原存放的地址值加上8字节;而一个双精度型指针加上4等于原存放的地址值加上32字节。
PS:参与运算的指针变量必须是已赋值的。
对数组名施加+、-运算:数组名的初值是数组的首地址,也即指向数组的第一个元素,数组名+i,表示指向数组的第i+1个元素。
a[i]与*(a+i)这两种表示法是等价的,都表示a数组的第i+1个元素。
指针变量的++、--运算:
++:原地址加上一个地址单位(基类型的实际字节数)
--:原地址减去一个地址单位(基类型的实际字节数)
int *iptr;
......
iptr++; //iptr=iptr+1向下移动两个字节
iptr--; //iptr=iptr-1向上移动两个字节
取地址运算
运算符:&
作用:获取数据对象的内存地址,如果是结构数据对象,则获取其内存首地址
间接访问
运算符:*
该运算符作用在指针变量上,表示访问指针变量所指向的数据对象。
作用
实现对指针所指向的数据对象的间接访问,包括引用和赋值等基本运算。
int a,b=2,c,*p;
......
p=&b;
scanf("%d",&a);
c=a+*p;
c=a+b
"*"的说明
1.作为运算符,表示乘法
2.作为类型标识符,用来定义指针类型(出现在数据定义部分)
3.作为指针运算符,表示间接访问
指针与数组
数组名用来存放数组的内存首地址,也是第一个数组元素的内存地址,因此数组名是一种特殊的指针变量。
数组名是指向数组元素的指针变量
对于数组a而言,数组名a和a中的各个元素的关系如下:a=&a[0];a+i=&a[i]。数组元素的下标描述可以用数组名指针的偏移来代替,所以可以用指针来间接访问数组元素。
*a=a[0],*(a+i)=a[i]
指针数组
元素的数据类型是指针的数组称为指针数组。
char *name[]={"Lin","Ding","Zhan"};
赋初值后,name数组的每一个元素的值并不是学生的姓名(字符串),而是对应字符串的首地址,每一个元素的值是一个字符指针,该指针指向对应的字符串。
指向指针的指针
对于指针数组而言,其数组名是指针,而且是指向指针的指针,称为二级指针。因此除了描述成指针数组之外,还可以描述成**类型。
char*name[]={"Lin","Ding","Zhan"};
char**pname;
......
pname=name;
执行pname=name后,pname可以表示指针数组name,也即:pname指向name[0],pname指向name[1]......以此类推。
cout<<(int)arr[i]
是输出字符指针(字符串地址);
cout<<arr[i]
是输出字符指针所指向的字符串。
指针与函数
函数与指针的关系:
1.函数的参数是指针类型数据,例如数组参数、指针变量参数......
2.函数返回值类型本身就是指针类型,这种函数称为指针函数
3.函数名本身就是指向函数入口地址的指针,因此可以声明一种指针数据用来存放函数名,这样的指针称为函数指针
函数的指针类型参数
形式
可以定义成指针变量或数组
作用
返回函数对指针的修改,实质上是返回函数对指针所指向的数据对象的修改,这样可以返回不止一个值,同时还可以节省大量的内存空间,因此具有很大的灵活性与实用性。
带有指针参数的函数的实现过程
1.在函数的参数表中定义指针类型参数void swap(int *x,int *y)
2.在函数调用时提供相应的变量或数组地址(传地址)swap(&a,&b);
3.函数的执行部分对指针形参进行间接访问
使用指针类型参数的副作用
指针类型参数的灵活性体现在它使函数可以访问本函数的局部空间(栈空间)以外的内存区域,这显然破坏了函数的黑盒特性,带来以下副作用:
1.可读性问题:对数据的间接访问比直接访问相对难以理解。
2.重用性问题:函数调用依赖于上层函数或整个外部内存空间环境,丧失其封装特性(黑盒特性),所以无法作为公共模块来使用。
3.调试的复杂性问题:跟踪错误的区域从函数的局部数据区扩大到整个内存空间,不但要跟踪变量
函数指针
程序运行时,每个函数都存放在代码区中,有一个入口地址,称为函数地址,函数名就表示该地址。
指向函数地址的指针称为函数指针,通过该指针可以调用相应的函数。、
作用:调用函数和做函数的参数
声明
类型标识符名(*指针变量名)(参数列表)
用函数指针调用函数
函数指针变量(参数列表)
(*函数指针变量名)(参数列表)
函数指针用作函数参数
当函数形参是函数指针时,对应的实参必须是函数名
char f2(int(*fp1(int)))
......
f2(f1)
实参是函数名,在函数调用时实现函数地址的传递,这样可以在被调函数体内,通过对函数指针形参的引用来调用另一函数,而且对应不同的实参值可以调用不同的函数。
函数指针和指针函数的区别
函数指针是指向函数的指针变量,即本质是一个指针变量;指针函数是指带指针的函数,即本质是一个函数。函数返回类型是某一类型的指针,格式为类型标识符*函数名(参数列表)
指针函数
该函数的返回值是一个地址值,函数返回值必须用同类型的指针变量来接受,也就是说,指针函数一定有"函数返回值",而且,在主调函数中,函数返回值必须赋给同类型的指针变量。
定义类型名*函数名(参数列表)
( )的优先级高于*,所以参数名先和参数列表结合,再和*结合。