一、基础知识介绍
计算机中所有数据都存放在内存中,不同类型的数据占用的字节数不一样,例如int占用4个字节,char占用1个字节。为了正确的访问这些数据,每个字节都需要独一无二的编码,下图是4G内存中每个字节的编号,以十六进制表示:
我们将内存中字节的编号称为地址(address)或指针(pointer)。地址从0开始依次增加,对于32位环境,程序能够使用的内存为4GB,最小的地址为0,最大的地址为0XFFFFFFFF。
C语言用变量来存储数据,用函数来定义一段可以重复使用的代码,它们最终都要放到内存中才能供CPU使用。
数据和代码都以二进制的形式存储在内存中,计算机无法从格式上区分某块内存到底存储的是数据还是代码。当程序被加载到内存后,操作系统会给不同的内存块指定不同的权限,拥有读取和执行权限的内存块就是代码,而拥有读取和写入权限(也可能只有读取权限)的内存块就是数据。
CPU只能通过地址来取得内存中的代码和数据,程序在执行过程中会告知CPU要执行的代码以及要读写的数据的地址。如果程序不小心出错,在CPU要写入数据时给它一个代码区域的地址,就会发生内存访问错误。这种内存访问错误会被硬件和操作系统拦截,强制程序崩溃。
CPU访问内存时需要的是地址,而不是变量名和函数名。 变量名和函数名只是地址的一种助记符,当源文件被编译和链接成可执行程序后,它们都会被替换成地址。编译和链接的一项重要任务就是找到这些名称所对应的地址。
但变量名和函数名为我们提供了方便,让我们在编写代码的过程中可以使用易于阅读和理解的英文字符串,不用直接面对二进制地址。需要注意的是,虽然变量名、函数名、字符串名和数组名在本质上是一样的,它们都是地址的助记符,但在编写代码的过程中,我们认为变量名表示的是数据本身,而函数名、字符串名和数组名表示的是代码块或数据块的首地址。
二、指针变量的定义和使用
数据在内存中的地址也称为指针,如果一个变量存储了一份数据的指针,我们就称它为指针变量。 指针变量的值就是某份数据的地址,这样的一份数据可以是数组、字符串、函数,也可以是另外的一个普通变量或指针变量。
现在假设有一个char类型的变量c,它存储了字符’K’(ASCII码为十进制数 75),并占用了地址为 0X11A 的内存。另外有一个指针变量 p,它的值为 0X11A,正好等于变量 c 的地址,这种情况我们就称 p 指向了 c,或者说 p 是指向变量 c 的指针。
2.1 指针变量的定义
定义指针变量与定义普通变量非常类似,不过要在变量名前面加星号*,格式为:
datatype *name;
或者
datatype *name = value;
*表示这是一个指针变量,datatype表示该指针变量所指向的数据的类型,下面是例子:
//p1是一个指向int类型数据的指针变量,且未赋值
int *p1;
//对p1进行了赋值
int a=100;
int *p1=&a;
//和普通变量一样,指针变量也可以被多此写入,只要我们想,随时都能够改变指针变量的值
//注意在定义p1和p2的时候必须带*。而给p1、p2赋值时,不需要加星号,后面可以像使用普通变量一样来使用指针变量。
float a=99.5,b=10.6;
char c='@',d='#';
float *p1=&a;
char *p2=&c;
p1=&b;
p2=&d;
//指针变量也可以连续定义
int *a, *b, *c; //a、b、c 的类型都是 int*
//注意每个变量前都要加星号,如果写成下面的形式,那么只有a是指针变量,b、c都是类型为in的普通变量
int *a, b, c;
2.2 通过指针变量取得数据
格式为:
*pointer;
这里*
被称为指针运算符,用来取得某个地址上的数据,有如下的例子:
#include <stdio.h>
int main(){
int a = 15;
int *p = &a;
printf("%d, %d\n", a, *p); //两种方式都可以输出a的值
return 0;
}
输出结果:
15, 15
在上面说过,CPU读写数据必须要知道数据在内存中的地址,普通变量和指针变量都是地址的助记符,虽然通过p和a取到的数据一样,但它们的运行过程稍有不同:a只需要一次运算就能够取得数据,而p要经过两次运算。
假设变量a、p的地址分别为0x1000、0xF0A0,它们的指向关系如下图所示:
程序被编译和链接后,a、p被替换成相应的地址。使用*P的话,要先通过地址0XF0A0取得变量p本身的值,这个值是变量a的地址,然后再通过这个值取得变量a的数据,前后共有两次运算,而使用a的话,可以通过地址0X1000直接取得它的数据,只需要一步运算。所以使用指针是间接获取数据,使用变量名是直接获取数据,前者比后者的代价要高。
指针除了可以获取内存上的数据,也可以修改内存上的数据,例如:
#include <stdio.h>
int main(){
int a = 15, b = 99, c = 222;
int *p = &a; //定义指针变量
*p = b; //通过指针变量修改内存上的数据
c = *p; //通过指针变量获取内存上的数据
printf("%d, %d, %d, %d\n", a, b, c, *p);
return 0;
}
运行结果:
99, 99, 99, 99
注意给指针变量本身赋地址值时不能加*,例如上面的语句:
int *p;
p = &a;
*p = 100;
第二行代码中的p前面就不能加*。
指针变量也可以出现在普通变量能出现的任何表达式中,例如:
int x, y, *px = &x, *py = &y;
y = *px + 5; //表示把x的内容加5并赋给y,*px+5相当于(*px)+5
y = ++*px; //px的内容加上1之后赋给y,++*px相当于++(*px)
y = *px++; //相当于y=(*px)++
py = px; //把一个指针的值赋给另一个指针
三、C语言指针变量的运算(加法、减法和比较运算)
指针变量保存的是地址,而地址本质上是一个整数,所以指针变量可以进行部分运算,例如加法、减法、比较等,以下是代码例子:
#include <stdio.h>
int main(){
int a = 10, *pa = &a, *paa = &a;
double b = 99.9, *pb = &b;
char c = '@', *pc = &c;
//最初的值
printf("&a=%#X, &b=%#X, &c=%#X\n", &a, &b, &c);
printf("pa=%#X, pb=%#X, pc=%#X\n", pa, pb, pc);
//加法运算
pa++; pb++; pc++;
printf("pa=%#X, pb=%#X, pc=%#X\n", pa, pb, pc);
//减法运算
pa -= 2; pb -= 2; pc -= 2;
printf("pa=%#X, pb=%#X, pc=%#X\n", pa, pb, pc);
//比较运算
if(pa == paa){
printf("%d\n", *paa);
}else{
printf("%d\n", *pa);
}
return 0;
}
运行结果:
&a=0X28FF44, &b=0X28FF30, &c=0X28FF2B
pa=0X28FF44, pb=0X28FF30, pc=0X28FF2B
pa=0X28FF48, pb=0X28FF38, pc=0X28FF2C
pa=0X28FF40, pb=0X28FF28, pc=0X28FF2A
2686784
从运算结果可以看出:pa、pb、pc每次加一,它们的地址分别增加4、8、1,正好是int、double、char类型的长度,减2时,地址分别减少8、1、2,正好是int、double、char类型长度的2倍。
因为数组中的所有元素在内存中是连续排列的,如果一个指针指向了数组中的某个元素,那么加1就表示指向下一个元素,减1就表示指向上一个元素,这样指针的加减运算就具有了现实的意义。
不过C语言并没有规定变量的存储方式,如果连续定义多个变量,它们有可能是挨着的,也有可能是分散的,这取决于变量的类型、编译器的实现以及具体的编译模式,所以对于指向普通变量的指针,我们往往不进行加减运算,虽然编译器不会报错,但这样做没有意义,因为不知道它后面指向的是什么数据。
指针变量除了可以参与加减运算,还可以参与比较运算。当对指针变量进行比较运算时,比较的是指针变量本身的值,也就是数据的地址。如果地址相等,那么两个指针就指向同一份数据,否则就指向不同的数据。
四、数组指针
数组中的所有元素在内存中是连续排列的,整个数组占用的是一块内存。下面的例子演示了如果以指针的方式遍历数组元素:
#include <stdio.h>
int main(){
int arr[] = { 99, 15, 100, 888, 252 };
int len = sizeof(arr) / sizeof(int); //求数组长度,注意sizeof(arr)会获得整个数组所占用的字节数
int i;
for(i=0; i<len; i++){
printf("%d ", *(arr+i) ); //*(arr+i)等价于arr[i]
}
printf("\n");
return 0;
}
运行结果:
99 15 100 888 252
我们也可以定义一个指向数组的指针,例如:
int arr[] = { 99, 15, 100, 888, 252 };
int *p = arr;
arr 本身就是一个指针,可以直接赋值给指针变量 p。arr 是数组第 0 个元素的地址,所以int *p = arr;也可以写作int *p = &arr[0];。也就是说,arr、p、&arr[0] 这三种写法都是等价的,它们都指向数组第 0 个元素,或者说指向数组的开头。(注意arr本身就是一个指针的这种表述并不准确,严格来说应该是“arr倍转化成了一个指针”)
如果一个指针指向了数组,我们就称它为数组指针。数组指针指向的是数组中的一个具体元素,而不是整个数组,所以数组指针的类型和数组元素的类型有关,上面的例子中,p指向的数组是int类型,所以p的类型也必须为int*。
引入数组指针后,我们就有两种方案来访问数组元素了,一种是使用下标,另外一种是使用指针:
- 使用下标:就是采用arr[i]的形式访问数组元素。如果p是指向数组arr的指针,那么也可以使用p[i]来访问数组元素,它等价于arr[i]
- 使用指针:也就是使用*(p+i)的形式访问数组元素。另外数组本身也是指针,也可以使用*(arr+i)来访问数组元素,它等价于*(p+i)
不管是数组名还是数组指针,都可以使用上面的两种方式来访问数组元素。不同的是,数组名是常量,它的值不能改变,而数组指针是变量(除非特别指明它是常量),它的值可以任意改变。也就是说,数组名只能指向数组的开头,而数组指针可以先指向数组开头,再指向其他元素。
可以利用自增运算符来遍历数组元素:
#include <stdio.h>
int main(){
int arr[] = { 99, 15, 100, 888, 252 };
int i, *p = arr, len = sizeof(arr) / sizeof(int);
for(i=0; i<len; i++){
printf("%d ", *p++ );
}
printf("\n");
return 0;
}
运行结果:
99 15 100 888 252
第8行代码中,*p++ 应该理解为 *(p++),每次循环都会改变 p 的值(p++ 使得 p 自身的值增加),以使 p 指向下一个数组元素。该语句不能写为 *arr++,因为 arr 是常量,而 arr++ 会改变它的值,这显然是错误的。
关于数组指针的一些特殊语法情况:
假设 p 是指向数组 arr 中第 n 个元素的指针,那么 *p++、*++p、(*p)++ 分别是什么意思呢?
- *p++ 等价于 *(p++),表示先取得第 n 个元素的值,再将 p 指向下一个元素,上面已经进行了详细讲解。
- *++p 等价于 *(++p),会先进行 ++p 运算,使得 p 的值增加,指向下一个元素,整体上相当于 *(p+1),所以会获得第 n+1 个数组元素的值。
- (*p)++ 就非常简单了,会先取得第 n 个元素的值,再对该元素的值加 1。假设 p 指向第 0 个元素,并且第 0 个元素的值为 99,执行完该语句后,第 0 个元素的值就会变为 100。
五、指针数组和数组指针
在许多C程序中,指针常被用于引用数组,或者作为数组的元素。指向数组的指针常被称为数组指针,而具有指针类型元素的数组则被称为指针数组。
5.1 数组指针
要声明指向数组类型的指针,必须使用括号,如下所示:
int (* arrPtr)[10] = NULL; // 一个指针,它指向一个有10个int元素的数组
如果没有括号:
int *arrPtr[10];
这表示arrPtr是一个具有10个int类型指针的数组。
在最开始举的例子中,指向有10个int元素的数组的指针会被初始化为NULL。然而,如果把合适数组的地址分配给它,那么表达式*arrPtr会获得数组,并且(*arrPtr)[i] 会获得索引值为 i 的数组元素。根据下标运算符的规则,表达式(*arrPtr)[i] 等同于 *((*arrPtr)+i)。因此,**arrPtr 获得数组的第一个元素,其索引值为 0。
为了展示数组指针arrPtr的几个运算,下例使用它来定位一个二维数组的某些元素,也就是矩阵内的某些行:
int (* arrPtr)[10] = NULL;
int matrix[3][10]; // 3行,10列的数组,其中数组名称是一个指向第一个元素的指针,也就是第一行的指针
arrPtr = matrix; // 使得arrPtr指向矩阵的第一行
(*arrPtr)[0] = 5; // 将5赋值给第一行的第一个元素
arrPtr[2][9] = 6; // 将6赋值给最后一行的最后一个元素
++arrPtr; // 将指针移动到下一行
(*arrPtr)[0] = 7; // 将7赋值给第二行的第一个元素
在初始化赋值后,arrPtr指向矩阵的第一行,在这种情况下,使用arrPtr获取数组元素的方式与使用matrix完全一样。例如,赋值运算(*arrPtr)[0]=5 等效于 arrPtr[0][0]=5 和 matrix[0][0]=5。
然而,与数组名称 matrix 不同的是,指针名称 arrPtr 并不代表一个常量地址,如运算 ++arrPtr 所示,它进行了自增运算。这个自增运算会造成存储在数组指针的地址增加一个数组空间大小,在本例中,即增加矩阵一行的空间大小,也就是 10 乘以 int 元素在内存中所占字节数量。
如果想把一个多维数组传入函数,则必须声明对应的函数参数为数组指针。最后需要注意的是,如果a是一个具有10个int类型元素的数组,那么无法使用下面的方式对前面例子中的指针arrPtr赋值:
arrPtr = a; // 错误:指针类型不匹配
错误的原因是,数组名字,例如上文的a,会被隐式地转换为指针,指向数组第一个元素,而不是指向整个数组,这也就是说指向int的指针没有被隐式地转换为指向int数组的指针。本例中的赋值操作需要显式的类型转换,在类型转换运算符中明确指定目标类型是什么:
arrPtr = (int (*)[10])a; // 合法
5.2 指针数组
指针数组(也就是元素为指针类型的数组)常常作为二维数组的一种便捷替代方式。一般情况下,这种数组中的指针会指向动态分配的内存区域。
例如,假如用二维数组处理一些字符串,该数组行空间大小必须足以存储下可能出现的最长字符串:
#define ARRAY_LEN 100
#define STRLEN_MAX 256
char myStrings[ARRAY_LEN][STRLEN_MAX] =
{ // 墨菲定律的几条推论:
“会出错的事,总会出错。”
“世上没有绝对正确的事情。”
“每个解决办法都会衍生出新的问题。”
};
然而,这种方式会造成内存浪费,25600字节中只有一小部分被实际使用到。一方面,短字符串会让大部分的行是空的;另一方面,有些行根本没有用到,但却得为它预留内存。
一个简单的解决方案是,使用指针数组,让指针指向对象(在此处的对象就是字符串),然后只给实际存在的对象分配内存(未用到的数组元素则是空指针)。
#define ARRAY_LEN 100
char *myStrPtr[ARRAY_LEN] = // char指针的数组
{ // 墨菲定律的几条推论:
“会出错的事,总会出错。”
“世上没有绝对正确的事情。”
“每个解决办法都会衍生出新的问题。”
};
下图展示了利用指针数组的情况下对象在内存中的存储情况:
尚未使用的指针可以在运行时指向另一个字符串。所需的存储空间可以利用这种常见方法来动态地保存。当不再需要该内存时,可以释放。
六、无类型指针
在C语言中,void被翻译为“无类型”,相应的void*为“无类型指针”。
6.1 void的作用
对函数返回的限定,对函数参数的限定
一般我们常见的就是这两种情况:
- 当函数不需要返回值时,必须使用void限定,这就是我们所说的第一种情况。例如:void func(int a,char *b)。
- 当函数不允许接收参数时,必须使用void限定,这就是我们说的第二种情况,例如:int func(void)。
6.2 void指针的使用规则
void指针可以指向任意类型的数据,就是说可以用任意类型的指针对void指针赋值。
int *a;
void *p;
p=a;
如果要将void指针p赋给其他类型的指针,则需要强制类型转换,对于上面的例子,这就是:a=(int *)p。
在内存的分配中我们可以见到void指针的使用:内存分配函数malloc函数返回的指针就是void*类型,用户在使用这个指针的时候,要进行强制类型转换,也就是显式的说明该指针指向的内存中是存放什么类型的数据。(int *)malloc(1024)表示强制规定malloc返回的void*指针指向的内存中存放的是一个个的int型数据。
在 ANSI C 标准中,不允许对 void 指针进行一些算术运算如 p++ 或 p+=1 等,因为既然 void 是无类型,那么每次算术运算我们就不知道该操作几个字节,例如 char 型操作 sizeof(char) 字节,而 int 则要操作 sizeof(int) 字节。而在 GNU 中则允许,因为在默认情况下,GNU 认为 void * 和 char * 一样,既然是确定的,当然可以进行一些算术操作,在这里sizeof(*p)==sizeof(char)。
直接定义void变量是错误的,例如如下的例子:
void a;
这行语句编译时会出错,提示
illegal use of type 'void'
如果指针p1和p2的类型相同,我们可以直接在p1和p2间相互赋值;如果p1和p2指向不同的数据类型,则必须使用强制类型转换运算符把赋值运算符右边的指针类型转换为左边指针的类型。
float *p1;
int *p2;
p1 = p2;
//其中p1 = p2语句会编译出错,
//提示“'=' : cannot convert from 'int *' to 'float *'”,必须改为:
p1 = (float *)p2;
而void*则不同,任何类型的指针都可以直接赋值给它,无需进行强制类型转换:
void *p1;
int *p2;
p1 = p2;
但这并不意味着,void * 也可以无需强制类型转换地赋给其它类型的指针。因为"无类型"可以包容"有类型",而"有类型"则不能包容"无类型"。
七、函数指针
7.1 什么是函数指针
如果在程序中定义了一个函数,那么在编译时系统就会为这个函数代码分配一段存储空间,这段存储空间的首地址称为这个函数的地址。而且函数名表示的就是这个地址。我们可以定义一个指针变量来存放这个地址,这个指针变量就叫做函数指针变量,简称函数指针。
指向函数的指针变量同我们之前讲的指向变量的指针变量的定义方式是不同的,例如:
int(*p)(int, int);
这个语句就定义了一个指向函数的指针变量p。首先它是一个指针变量,所以要有一个*,即(*p);其次前面的int表示这个指针变量可以指向返回类型为int型的函数;后面括号中的两个int表示这个指针变量可以指向有两个参数且都是int型的函数。所以合起来这个语句的意思就是:定义了一个指针变量p,该指针变量可以指向返回类型为int型,且有两个整型参数的函数。p的类型为 int(*)(int,int)。
总结下,指针变量的定义方式为:
函数返回值类型 (* 指针变量名) (函数参数列表);
“函数返回值类型”表示该指针变量可以指向具有什么返回值类型的函数;“函数的参数列表”表示该指针变量可以指向具有什么参数列表的函数。这个参数列表中只需要写函数的参数类型即可。
我们看到,函数指针的定义就是将“函数声明”中的“函数名”改成“(*指针变量名)”。但是这里需要注意的是,“(*指针变量名)”两端的括号不能省略,括号改变了运算符的优先级。如果省略了括号,就不是定义函数指针而是一个函数声明了,即声明了一个返回值类型为指针型的函数。
需要注意,指向函数的指针变量没有++和- -运算。
7.2 如何用函数指针调用函数
以一个代码例子说明:
int Func(int x); /*声明一个函数*/
int (*p) (int x); /*定义一个函数指针*/
p = Func; /*将Func函数的首地址赋给指针变量p*/
赋值时函数Func不带括号,也不带参数。由于函数名Func代表函数的首地址,因此经过赋值以后,指针变量p就指向Func()代码的首地址了。
下面以一个程序来说明指函数指针怎么使用:
#include<stdio.h>
int Max(int,int);
int main(void){
int(*p)(int,int);
int a=20,b=10,c=0;
p=Max;
c=(*p)(a,b);
printf("a=%d\nb=%d\nmax=%d\n",a,b,c);
return 0;
}
int Max(int x,int y){
return x>y?x:y;
}
最后输出结果为:
a=20
b=10
max=20