指针的总结
1 关于指针的一些概念
1.1 什么是变量
变量提供了程序可以操作的有名字的存储区。C/C++中的每个变量都有特定的类型,该类型决定了变量的内存大小和布局、能够存储于该内存中的值以及可应用在该变量上的操作集。
也可以称有着完整的含义和功能的一块内存区域为变量或对象。
1.2 什么是指针
指针就是一种变量类型。它的大小依机器而定,在32位机上一般是4个字节。指针变量中存储的值是一个内存地址。我们约定指针变量指向的内存地址为某种类型的对象。如
int *p=&i;
p是int*类型的指针变量,即我们约定在p中存储的内存地址实际是一个int类型的对象(当然实际上p中存储的值可以是任意一个32位整数,也可以指向任意一个实际存在的对象)。
2 指针支持什么操作
2.1 左值和右值
C/C++中的表达式分为两种:
1) 左值:可以出现在赋值语句的左边或右边。左值表达式指向一个存在于内存中的对象,我们可以对这个对象进行读写操作。
2) 右值:只能出现在赋值的右边。右值表达式返回对象进行运算后的值。
简单的说,左值相当于地址值,右值相当于数据值。
变量是左值,常量和表达式是右值。因此以下操作合法:
int p=5,q=7;
p=q-4;
以下操作非法:
p+q=6;
4=p-3;
2.2 解引用操作
只有指针型变量支持解引用操作。如
int *p=&i;
*p=5;
解引用操作即:取指针指向的地址的内容。作为左值时,它表示实际操作的是指针指向的对象;作为右值时,它返回指针指向的对象的值。
如果在进行解引用操作时,被操作的指针中存储的地址为0(即NULL)或指向了一个非法的地址,如其它进程的内存空间中,程序会被强行关闭。这种现象就是内存访问越界。所以指针在使用前一般都需要进行初始化。
2.3 自增自减操作
int *p=&i;
p++;
--p;
指针变量的自增自减操作与正常变量类似,都是改变自身的值。与正常变量的区别是,指针变量的值的改变量与其约定的指向类型有关,为其指向类型占用的字节数。上面例子中p每次操作的改变量都是4。
2.4 与整数进行加减操作
int *p=&i;
int t=3;
p=p+4;
*(p-t)=5;
与自增自减操作类似,指针变量与整数进行加减操作时,其实际值的改变量为整数乘指针指向类型的字节数。
2.5 下标操作
int *p=&i[0];
p[3]=5;
指针的下标操作与2.4中提到的与整数的加减操作类似,p[3]就是*(p+3),即下标就是内存地址的偏移量。从这里可以看出,数组和指针其实是非常相关的概念。
2.6 指针间的减操作
int *p=&i[0],*q=&i[2];
ptrdiff_t d=p-q;
指向同一数组的指针支持相互的减操作,其差值为两个指针之间的元素的个数。其它的指针间的减操作,比如指向不同数组的指针间的,不同类型的指针间的减操作,C语言中并没有相关的规定,即这样的操作一般可以认为是非法的或者是无意义的。
指针相加的操作是无意义的。
2.7 赋值操作
指针变量可以接受其指向类型的变量的地址的赋值:
int i;
int *p=&i;
也可以接受其指向类型的数组的赋值:
int i[10];
int *p=i;
也可以接受同类型指针算术表达式的结果:
int *p,*q=&i;
p=q+5;
2.8 取地址操作
指针变量也是变量,因此也支持所有变量都支持的取地址操作:
int *p=&i;
int **q=&p;
取地址表达式只能做右值。
2.9 成员访问操作
如果指针指向的类型为结构体,可以用->操作符访问其中的成员。成员访问操作可以做左值。
3 指针与数组的关系
数组是内存中一段连续的地址空间,数组名表示这段空间的首地址。可以认为数组名是一个常量的地址值,因此它是不可以做左值的,但可以做右值。
在算术操作上,数组名与指针有着相似之处,可以做与整数的加减操作,下标操作,解引用操作,但不能做自增自减操作与赋值操作。
数组名不是指针,因为它不是变量,而是一个常量,其值等于数组首项的地址。在C/C++中我们可以改变一个变量中存储的值,但不能改变这个变量所在的内存地址。因为数组名是地址常量,所以我们可以把数组名赋值给同类型的指针,就相当于把一个整数常量赋值给int型变量一样。
在将数组名当作参数传递给函数时,函数需要用一个指针型变量来接收这个参数,此时即使将这个变量写成指针形式,它代表的也不再是数组,而是一个指针变量了。
将数组名当作sizeof的参数时,返回的是整个数组占用的内存空间字节数,而将指针当作sizeof的参数时,返回的是指针本身占用的内存空间数,一般为4。
4 传值方式和传引用方式进行函数调用的区别
从函数参数变量的角度来看,传值方式和传引用方式是一样的,都是把实参的值传递给形参。其区别就是传引用方式需要形参为指针类型的变量,而实参为一个对象的地址,这样我们就可以通过对形参指针的解引用操作来修改实参指向的对象的值了。
这种方式的便利性体现在:
1) 可以在函数中修改其它函数中定义的变量的值,方便实现很多传值方式无法实现的操作。
2) 传递速度快。如果需要传递一个占用空间很大的结构体变量的话,传值方式需要在传递时进行结构体的复制操作,而传引用方式只需要进行指针的复制操作。因为不管什么类型的指针都只占用4个字节,传递的效率可以很高。
而如果需要在函数中修改一个指针变量的值,就需要修改函数,使函数接受一个指向这种类型指针的指针参数,实际调用时传递要修改的指针变量的地址。例如下面的creat函数中有申请内存空间的功能,为了将指针指向新申请的空间,就需要传递指针变量的地址:
int *data;
creat(&data);
//…
void creat(int **p)
{//…}
我的总结就是:函数间传递方式其实只有传值方式,所谓的传引用方式只是利用了指针类型特有的解引用操作的性质。
5 常量指针与指针常量
指针中包括了指向常量的指针即常量指针,与指针类型的常量,即指针常量两种特殊的指针。
常量指针形如const int *p,我们约定p指向的对象为常量,即不能通过p的解引用操作去修改它所指向的对象的值,但p本身并非常量,仍可进行自增自减赋值等操作,可以改变p的指向。
常量指针可以接受非常量指针的赋值,但无法赋值给非常量指针。
指针常量形如int * const p,p本身为常量,不能进行自增自减赋值操作,即无法改变p的指向,但可通过p的解引用操作去修改它所指向的对象的值。
采用传引用方式进行函数参数传递时,如果不想改变参数的值(比如只是想加速结构体参数的传递时),可以将指针声明为常量指针。
函数在接受字符串参数时,一般都需要将参数设为const char*。
6 void*指针
void *为“无类型指针”,void *可以指向任何类型的数据。
void*类型的指针可以接受任意类型的指针的赋值,但在赋值给其它类型的指针时需要进行强制转换。在ANSI标准中void*指针不支持除赋值外的其它操作,但在GCC中void*在非解引用操作时被当作char*指针处理。
在编写通用类型的函数时,经常需要接受void*类型的参数,但在使用时需要转型为具体的类型。
7 指针的初始化与内存管理
指针变量与普通变量相同,在栈上分配的指针变量的值是不确定的。如果指针变量在定义后第一次操作是赋值操作的话,可以不需要进行初始化,其它情况下都需要进行初始化。
未进行初始化的指针可能指向:空地址(NULL),非法地址(其它进程的内存空间),无效地址(已经被释放的内存空间)等,对这样的指针进行解引用操作、下标操作与成员访问操作时,程序可能因内存访问错误而中止。
指针指向的对象如果是在栈上分配的,使用结束后不需要对指针进行free操作:
int i[10];
int *p=i;
//…
free(p);//error!
如果指针指向的对象是在堆上分配的,即malloc获得的地址,在使用结束后需要进行free操作,即malloc和free操作要配套使用,否则会导致内存泄露。
8 函数指针
函数指针是一种特殊的指针类型,它指向的是特定的函数类型,这种函数类型是通过函数的参数数量与类型和返回值的类型而确定的。
int (*fp)(int,int);//fp指向的函数必须有2个int型参数,返回int型返回值
函数指针可以接受函数名的赋值,它与函数名的关系相当于普通指针与数组名的关系。
函数指针支持赋值操作与函数调用操作(即括号),在通过函数指针调用函数时,与直接使用函数的名字的效果相同。
函数指针常用于回调操作中。
9 指针数组与数组指针
int *p[10]中的p就是指针数组,它有10个元素,其中每个元素都是一个指向int的指针。这里注意的是*操作符的优先级。
int (*p)[10]中的p是数组指针,它指向了一个有着10个int元素的数组。在函数传递二维数组时,可以用这种指针来简化操作。