1、地址和指针的概念
如果程序在定义中定义了一个变量,在对程序进行编译时,系统就会给这个变量分配内存单元。编译系统根据程序中定义的变量类型,分配一定长度的空间。例如,一般为整型变量分配2个字节,对单精度分配4个字节,对字符分配1个字节。内存区的每一个字节有一个编号,这就是"地址"。然而,存储在单元中的数据就是该单元的内容。在程序中一般是通过变量名来对内存单元进行存取操作的。其实程序经过编译后已经将变量名转换为变量地址,对变量值的存取都是通过地址进行的。这种按照变量地址存取变量值的方法称为"直接访问"。还可以采用另一种称之为"间接访问"的方式,即将一个变量的地址存放到另一个变量中,取值时先找到存放变量地址的变量,然后通过变量地址取出变量的值。如:i、j其中j存放了i的地址,i的值为3。此时,通过取出j中i的地址,再根据取出地址取出i的内容。所谓指向是通过地址来实现的,假设j=2000,然而i的地址也为2000,则j指向了i,通过j可以取出i变量中的内容。一个变量的地址称为该变量的"指针"。如果一个变量专门用来存放另一个变量的地址(即指针),则称为"指针变量"。指针变量的值是一个地址(指针)。指针是一个地址,而指针变量是存放地址的变量。
2、变量的指针和指向变量的指针变量
如前所述,变量的指针就是变量的地址。存放变量地址的变量是指针变量,它是用来指向另一个变量。在程序中为了表示指针变量和他所指向的变量之间的联系,使用‘*’符号表示指向。如果定义了指针变量i_pointer,那么(*i_pointer)是i_pointer所指向的变量。
i=3 <==> *i_pointer=2(表示将3赋值给指针变量i_pointer指向的变量)。
C语言中任何变量在使用前必须进行声明且明确指定变量的类型。指针变量也不列外,语法如下:基本类型 *指针变量名
如:
int i, j; int *pointer_1, *pointer_2;
定义了两个指向整型的指针变量,左端的int是在定义指针变量是必须指定的"基类型"。指针变量的基类型用来指定该指针变量可以指向的变量类型。如上面的pointer_1和pointer_2只能指向int型。
可以使用赋值语句使一个指针变量得到另一个变量的地址,从而使它指向一个该变量。如:
pointer_1 = &i; // 指向i pointer_2 = &j; // 指向j
在定义指针变量是要注意两点:
a、指针变量前面的‘*’表示该变量的类型为指针型变量。指针名称是pointer_1和pointer_2,而不是*pointer_1和*pointer_2。
b、在定义指针变量是必须指定基类型。
3、指针变量的引用
请牢记,指针变量中只能存放地址,不要将一个整数赋给一个指针变量。
&:取地址运算符
*:指针运算符(或称"间接访问"运算符),取其指向的内容。
如:
int a = 3; int *pointer_a = &a;
取出a变量的地址送pointer_a
*pointer_a取出pointer_a 指向单元的内容
【实例】通过指针变量访问整型变量
#include <stdio.h> void main() { int a, b; int *p1, *p2; a = 100; b = 200; p1 = &a; p2 = &b; printf("a=%d\tb=%d\n", a, b); printf("p1=%d\tp2=%d\n", *p1, *p2); }
&和*运算符:
a、&*p1的含义是什么? &和*运算符的优先级相同,但按照自右而左方向结合。因此先进行*p1的运算,它就是变量a,再执行&运算。因此,&*p1与&a相同。
b、*&a的含义是什么?先进性&a运算,得a的地址,再进行*运算,即&a所指向的变量。*&a和*p的效果一样。
c、(*p)++相当于a++。注意括号是必要的,如果没有括号,就成为了*p++,++和*的优先级别是一样的,而结合方向是自右而左,因此相当于*(p++)。由于++在p的右边,是"后加",因此先对p的原值进行*运算,得到a的值,然后是p的值改变,这样p不再指向a了。
【例】输入a和b两个整数,按先大后小顺序输出a和b
#include <stdio.h> void main() { int a, b; int *p1, *p2, *p; scanf("%d%d", &a, &b); p1 = &a; p2 = &b; if (a < b) { p = p1; p1 = p2; p2 = p; } printf("p1=%d\tp2=%d\n", *p1, *p2); }
4、指针变量作为函数参数
函数的参数不仅可以是整型、浮点型、字符型等数据类型,还可以是指针类型。它的作用是将一个变量的地址传送到另一个函数中。
【例】输入a和b两个整数,按先大后小顺序输出a和b,用函数实现。
#include <stdio.h> void main() { void swap(int *p1, int *p2); int a, b; int *p1, *p2; scanf("%d%d", &a, &b); p1 = &a; p2 = &b; if (a < b) { swap(p1, p2); // 或swap(&a, &b); } printf("p1=%d\tp2=%d\n", *p1, *p2); } void swap(int *p1, int *p2) { int temp; temp = *p1; *p1 = *p2; *p2 = temp; }
【例】输入a、b、c这3个整数,按大小顺序输出。
#include <stdio.h> void main() { void change(int *q1, int *q2, int *q3); int a, b, c; int *p1, *p2, *p3; scanf("%d%d%d", &a, &b, &c); p1 = &a; p2 = &b; p3 = &c; change(p1, p2, p3); printf("p1=%d\tp2=%d\tp3=%d\n", *p1, *p2, *p3); } void change(int *q1, int *q2, int *q3) { void swap(int *p1, int *p2); if (*q1 < *q2) swap(q1, q2); if (*q1 < *q3) swap(q1, q3); if (*q2 < *q3) swap(q2, q3); } void swap(int *p1, int *p2) { int temp; temp = *p1; *p1 = *p2; *p2 = temp; }
5、数组与指针
所谓数组元素的指针就是数组元素的地址。引用数组元素可以使用下标法,也可以使用指针法,即通过指向数组元素的指针找到所需的元素。使用指针法能使目标程序质量高(占内存少,运行速度快)。
定义一个指向数组元素的指针变量的方法,与前面一样。如:
int a[10];
int *p;
注意,如果数组为int型,则指针变量的基类型也应该为int型。如下:
p=&a[0];
C语言规定数组名代表数组中首元素的地址。因此,下面两个语句等价:
p = &a[0];
p = a;
注意数组名a不代表整个数组,上述"p=a;"的作用是"把a数组的首元素的地址赋给指针变量p",而不是"把数组a各元素的值赋给p"。
在定义指针变量时可以给变量赋初始值。如:
int *p = &a[0];
等效与下面两行:
int *p;
p = &a[0];
当然也可以写成:
int *p = a;
6、通过指针引用数组元素
假设p已定义为一个指向整型数据的指针变量,并已经给它赋了一个整型数组元素的地址,使它指向某一个数组元素。如下:
*p=1; 表示将1赋给p当前所指向数组元素。
按照C语言规定:如果指针变量p已经指向数组中的一个元素,则p+1指向同一个数组的下一个元素,而不是简单的加1。如果:数组元素是float型,每个元素占4个字节,则p+1意味着使p的值(是地址)加4个字节。p+1代表的地址实际上是p+1*d,d是数组元素所占字节数。
如果p的初始值为&a[0],则:
(1)p+i和a+i就是a[i]的地址,它们指向a数组的第i个元素。注意:a代表数组的首地址
(2)*(p+i)或*(a+i)是p+i或a+i所指向的数组元素,即a[i]。例如:*(p+5)或*(a+5)就是a[5]。即*(p+5)、*(a+5)、a[5]三者等价。实际上,在编译时,对数组元素a[i]就是按照*(a+i)处理的,即按照数组首元素的地址加上相对位移量得到要找的元素的地址,然后找出该单元中的内容。
(3)指向数组的指针变量也可以带小标。如:p[i]与*(p+i)等价
【例】输出数组中的元素
(1)、下标法
#include <stdio.h> void main() { int a[10]; int i; // 给数组赋值 for (i = 0; i < 10; i ++) { a[i] = i+10; } // 输出数组的值 for (i = 0; i < 10; i ++) { printf("a[%d] = %d\n", i, a[i]); } }
(2)、通过数组名计算数组元素地址
#include <stdio.h> void main() { int a[10]; int i; // 给数组赋值 for (i = 0; i < 10; i ++) { a[i] = i+10; } // 输出数组的值 for (i = 0; i < 10; i ++) { printf("a[%d] = %d\n", i, *(a+i)); } }
(3)、用指针变量指向数组元素
#include <stdio.h> void main() { int a[10]; int i, *p; // 给数组赋值 for (i = 0; i < 10; i ++) { a[i] = i+10; } // 输出数组的值 for (i = 0, p = a; p < (a+10); p ++, i ++) { printf("a[%d] = %d\n", i, *p); } }
使用指针变量指向数组元素时需要注意的问题:
a、可以通过改变指针变量的值指向不同的元素。如上使p++来指向不同的数组元素是合法的,但是使用数组名a++是不正确的,因为a是数组的首地址,是一个指针常量,在程序运行期间是固定的。
b、要注意指针变量的当前值。如下:
#include <stdio.h> void main() { int a[5]; int i, *p; p = a; // 给数组赋值 for (i = 0; i < 5; i++) { printf("a[%d] = ", i); scanf("%d", p++); } p = a; // 这句是必须的,因为上一个循环将p+9,此时需要将p从新指向数组首地址 // 输出数组的值 printf("\n输出数组a的值:\n"); for (i = 0; i < 5; i++, p++) { printf("a[%d] = %d\n", i, *p); } }
c、从上例可以看出,定义的数组包含10个元素,并用p指向某个数组元素。实际上指针变量p可以指向数组以后的内存单元。如引用了a[10],C编译器不认为非法,系统把它按*(a+10)处理,但是a+10的内存单元是不可知的,所以数据是未知的,这种错误很隐蔽,不以发现,要避免使用。
d、注意指针变量的运算。如果先使p指向数组a的首地址(p=a)。分析:
1> p++或p+=1。使p指向下一个元素,即a[1]。若执行*p,则取得下一个元素a[1]的值
2> *p++。由于*和++同优先级,结合方向为自右而左,因此等价*(p++)
3> *(p++)和*(++p)作用不同。前者是先取*p的值,然后使p加1。后者是先使p加1,再取*p。若p初值为a(即&a[0]),
则 *(p++)为a[0],则*(++p)为a[1]
4> (*p)++表示p所指向的元素值加1,如果p=a,则(*p)++相当于(a[0])++。注意:是元素值加1,不是指针加1
5> 如果p当前指向a数组中第i个元素,则:
*(p--)相当于a[i--],先对p进行*运算,再使p自减
*(++p)相当于a[++i],先使p自加,再作*运算
*(--p)相当于a[--i],先使p自减,再作*元算
7、用数组名作函数参数
当数组名作为函数参数,那么当形参中各元素的值发生变化,实参数组元素的值随之变化。前面介绍过,实参数组名代表该数组的首元素的地址,而形参是用来接收实参传递过来的数组首元素地址的。因此,形参应该是一个指针变量。实际上,C编译器都是将形参数组名作为指针变量来处理的。如:
f(int arr[],int n) 等价于 f(int *arr,int n)
该函数在被调用时,系统会建立一个指针变量arr,用来存放从主调函数传递过来的实参数组首元素的地址。可以使用sizeof(arr)测试该形参的长度。
需要说明
C语言调用函数时虚实结合的方法都是采用"值传递"方式,当用变量名作为函数参数时传递的是变量的值,当用数组名作为函数参数时,由于数组名代表数组首元素地址,因此传递的值是地址,所以要求形参为指针变量。
注意
实参数组名代表一个固定的地址,或者说是指针常量,但形参数组并不是一个固定的地址值,而是作为指针变量,在函数调用开始时,它的值等于实参数组首元素地址,在函数调用期间,它可以被再次赋值。
【例】将数组a中n个整数按相反顺序存放
#include <stdio.h> void main() { void inv(int x[], int n); int i, a[10]={3,7,9,11,0,6,7,5,4,2}; printf("The original array: \n"); for (i = 0; i < 10; i ++) { printf("%d, ", a[i]); } printf("\n"); inv(a, 10); printf("The array has been inverted: \n"); for (i = 0; i < 10; i ++) { printf("%d, ", a[i]); } printf("\n"); } void inv(int x[], int n) { int temp, i, j, m=(n-1)/2; for (i = 0; i <= m ; i ++) { j = n-1-i; temp = x[i]; x[i] = x[j]; x[j] = temp; } return; } void inv2(int *x, int n) { int *p, temp, *i, *j, m=(n-1)/2; i = x; j = x+n-1; p = x+m; for (; i <= p; i++, j--) { temp = *i; *i = *j; *j = temp; } }
如果想要在数组中改变此数组中的元素的值,实参和形参的对对应关系有一下4种情况:
a、形参和实参都是数组名
b、实参用数组名,形参用指针变量
c、实参和形参都用指针变量
d、实参是指针变量,形参为数组名
【例】用选择法对10个整数按由大到小顺序排序
#include <stdio.h> // 定义BOOL类型 #define BOOL int #define TRUE 1 #define FALSE 0 void main() { void sort1(int x[], int n); void sort2(int x[], int n); void sort3(int *x, int n); int *p, i, a[10]={3,7,9,11,0,6,7,5,4,2}; for (p = a, i = 0; i < 10; i ++) { printf("%d, ", *(p++)); } printf("\n"); sort3(a, 10); for (p = a, i = 0; i < 10; i ++) { printf("%d, ", *(p++)); } printf("\n"); } void sort1(int x[], int n) { int i, j, k, temp; for (i=0; i<n-1; i++) { k = i; for (j=i+1; j<n; j++) { if (x[j] > x[k]) k = j; printf("%d=%d,", i, k); } printf("\n"); if (k != i) { temp = x[i]; x[i] = x[k]; x[k] = temp; } } } void sort2(int x[], int n) { int i, j, temp; // 定义布尔变量 BOOL flag = TRUE; for (i=0; i<n-1; i++) { for (j=i+1; j<n; j++) { if (x[j]>x[i]) { temp = x[j]; x[j] = x[i]; x[i] = temp; flag = FALSE; } } if (flag) break; } } void sort3(int *x, int n) { int i, j, k, t; for (i=0; i<n-1; i++) { k = i; for (j=i+1; j<n; j++) if(*(x+j) > *(x+k)) k = j; if (k!=i) { t = *(x+i); *(x+i) = *(x+k); *(x+k) = t; } } }
8、多维数组与指针
用指针可以指向一维数组的元素,也可以指向二维数组元素。二维数组就是一维数组的数组。如:
int a[3][4] = {{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 0, 1, 2}}; // 定义了一个二维数组
a[0] = {1, 2, 3, 4}
a[1] = {5, 6, 7, 8}
a[2] = {9, 0, 1, 2} 从二维数组的角度来看,a代表二维数组首元素的地址,现在的首元素不是一个简单的整型元素,而是一个由4个整数所组成的一维数组,因此a代表的是首行(即第0行)的首地址。a+1代表第1行的首地址,如果数组首地址是2000,a+1为2008,因为第0行有四个整型,因此a+1的含义是a[1]的地址,即a+4*2=2008。a[0]、a[1]、a[2]既然是一维数组名,而C语言又规定数组名代表 数组首元素地址,因此a[0]代表一维数组a[0]中第0列元素的地址,即&a[0][0]。a[1]=&a[1][0], a[2]=&a[2][0]。怎样表示a[0][1]?a[0]+1。
a[0]+0 ==> a[0][0] &a[0][0]
a[0]+1 ==> a[0][1] &a[0][1]
a[0]+2 ==> a[0][2] &a[0][2]
a[0]+3 ==> a[0][3] &a[0][3]
如前所述,a[0]和*(a+0)等价、a[1]和*(a+1)等价、a[i]和*(a+i)等价。因此,a[0]+1和*(a+0)+1都是&a[0][1]、a[1][2]和*(a+1)+2的值都是&a[1][2]。请不要将*(a+1)+2写成*(a+1+2),后者变成了*(a+3)了,相当于a[3]。怎样获取a[0][1]的值呢?既然a[0]+1和*(a+0)+1是a[0][1]的地址,那么,*(a[0]+1)就是a[0][1]的值。同理,*(*(a+0)+1)或*(*a+1)也是a[0][1]的值。
*(a[i]+j)或*(*(a+i)+j)是a[i][j]的值。务必记住*(a+i)和a[i]是等价的。
【例】用指针变量输出二维数组元素的值。
#include <stdio.h> void main() { int a[3][4] = {1,3,5,7,9,2,4,6,8,10,12,14}; int *p; for (p=a[0]; p<a[0]+12; p++) { if ((p-a[0])%4 == 0) printf("\n"); printf("%4d", *p); } printf("\n"); }
以上是顺序输出二维数组的值,比较简单。现在我们要取出a[2][3]单元的元素值。因为a[2][3]对a[0][0]的相对位移量为2*4+3=8。即
a[i][j]的地址为&a[0][0]+i*m+j(i表示当前元素前面有几行、m表示每一行有多少列、j表示当前元素在当前行前面共计多少个元素)。
【例】输出二维数组任意一行任意一列元素的值
#include <stdio.h> void main() { int a[3][4] = {1,3,5,7,9,2,4,6,8,10,12,14}; int (*p)[4], i, j; p = a; scanf("i=%d,j=%d", &i, &j); printf("a[%d,%d]=%d\n", i, j, *(*(p+i)+j)); }
int (*p)[4];表示p是一个指针变量,它指向包含4个整数元素的一维数组。*p两侧的括号不可缺少,如果写成*p[4],由于方括号[]运算级别高,因此p先与[4]结合,p[4]是定义数组的形式,然后再与前面的*结合,*p[4]就是指针数组。此时的p指针变量只能指向一维数组的首地址,不能指向一维数组中的元素。