指针是一种变量,也称指针变量,它的值不是整数、浮点数和字符,而是内存地址。指针的值就是变量的地址,而变量有拥有一个具体值。因此,可以理解为变量直接引用了一个值,指着间接地引用了一个值。一个存放变量地址的类型称为该变量的“指针”。
指针变量的大小?
以
32位
系统为例,每个字节(即一个内存单元)都拥有一个地址编号,地址范围为0x00000000~0xffffffff。当指针变量占4
个字节(即32bit)时,刚好能够表示所有地地址编号。
不管什么类型的指针,其大小只和系统编译器有关系。
2.指针的定义与使用
2.1 指针的定义
在C语言中,所有变量在使用前都需要声明。例如,声明一个指针变量的语句如下:
int *qPtr, q;
q是整型变量,表示要存放一个整型类型的值;qPtr是一个整形指针变量,表示要存放一个变量的地址,而这个变量是整数类型。qPtr叫做一个指向整型的指针。
在声明指针变量时,“*”只是一个指针类型标识符,指针变量的声明也可以写成 int* qPtr。
定义指针三步骤(来自传智播客):
- *与符号相结合代表是一个指针变量,比如*p;
- 要保存谁的地址,就写出它的声明语句,比如int a, int a[10];
- 用*p替换掉变量名称,即int a→int *p,int a[10]→int (*p)[10](数组指针);
指针变量可以在声明时赋值,也可以在声明后赋值。例如,在声明时为指针变量赋值的语句如下:
int q = 12;
int *qPtr = &q;
也可以在声明后为指针变量赋值:
int q = 12, *qPtr;
qPtr = &q;
2.2 指针的使用
指针变量主要通过取地址运算符&和指针运算符*来存取数据。例如,&a指的是变量a的地址(取址),*ptr表示ptr所指向的内存单元存放的内容(取值)。
#include<stdio.h>
int main(){
int q=12;
int *qptr;
qptr = &q;
printf("q的地址是:%p\nqptr中的内容是:%p\n", &q, qptr);
printf("q的值是:%d\n*qptr的值是:%d\n", q, *qptr);
// 运算符'&'和'*'是互逆的
printf("&*qptr=%p, *&qptr=%p\n因此有&*qptr=*&qptr\n", &*qptr, *&qptr);
return 0;
}
3.指针的宽度(步长)
#include<stdio.h>
int main(){
int num = 0x01020304;
char *p1 = (char *)#
short *p2 = (short *)#
int *p3 = #
printf("%#x\n", *p1);
printf("%#x\n", *p2);
printf("%#x\n", *p3);
return 0;
}
通过*取指针变量所指向那块内存空间内容时,取得内存的宽度和指针变量本身指向变量的类型有关。
题目:
int a[5] = {1, 2, 3,4 , 5};
int *ptr = (int *)(&a+1);
printf("%d,%d", *(a+1), *(ptr-1));
输出结果为:A
.2,5 B.2,4 C.1,5 D.1,4
分析:
&a+1:跨过的是整个数组的宽度
&a[0]+1:跨过的是数组内单个元素的宽度
所以&a+1指向的内存地址已经不属于数组了,然后int *ptr = (int *)(&a+1)将&a+1强转为int*型,所以ptr-1将后退一个4个字节即一个数组元素大小,即指向a[4]=5
4.野指针和空指针和万能指针
4.1 野指针
野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)。
#include<stdio.h>
int main(){
int *p ;
*p = 200;
printf("%d\n", *p);
return 0;
}
上述代码出现问题的原因:指针变量未初始化
任何指针变量刚被创建时不会自动成为 NULL 指针,它的缺省值是随机的。
所以,指针变量在创建的同时应当被初始化,要么将指针设置为 NULL ,要么让它指向合法的内存。
如果没有初始化,编译器会报错‘point’ may be uninitializedin the function。
4.2 空指针
#include<stdio.h>
int main(){
// 将0号地址编号0x00000000号内存赋予指针内部值,等价于赋予NULL
int *p = NULL;
*p = 200; // 因为p保存了0号内存的地址,这个地址为内存的起始地址,是不可使用的,非法
printf("%d\n", *p);
return 0;
}
NULL是C语言标准定义的一个值,这个值其实就是0,只不过为了使得看起来更加具有意义,才定义了这样的一个宏,中文的意思是空,表明不指向任何东西。
任何程序数据都不会存储在地址为0的内存块中,它是被操作系统预留的内存块。
空指针的作用:
如果指针使用完毕,需将指针赋予NULL;在使用指针前需判断指针是否为NULL
4.3 万能指针
即void *p
,可以保存任意的地址。
void p:不可遗定义void类型的变量,因为编译器不知道给该变量分配多大的内存空间;
void *p:可以定义void *变量,因为指针都是4个字节(32位系统)。
#include<stdio.h>
int main(){
int a = 10;
void *p = (void *)&a;
printf("%d\n", *p);
return 0;
}
程序将报错,无法编译。因为虽然p内存内确实存储的是变量a的地址,但是由于p指向void型变量,导致根据p指针内部存储地址去取相应位置值时不知道取多大的内存大小。
#include<stdio.h>
int main(){
int a = 10;
void *p = (void *)&a;
printf("%d\n", *(int *)p);
return 0;
}
*(int *)p:将指针p强转为int *型,此时根据p指针内部存储地址去取相应位置值时,将读取4个字节大小。
5.const修饰的指针变量
引子:const int a = 10;
const修饰变量a,表示不能再通过a修改a内存里面的内容。
5.1 指向常量的指针
const修饰*,表示不能通过该指针修改指针所指内存的数值,但是指针指向可以变。
5.2 指针常量
修饰p,指针指向不能变,指针指向的内存可以被修改。
注:const int * const p =&a;
表示p指针指向内存区域不能被修改,同时p的指向也不能被改变。
6.多级指针
#include<stdio.h>
int main(){
int a = 10;
int *p = &a;
int **q = &p;
// 通过q获取a的值
printf("%d\n", **q);
return 0;
}
7.指针数组与数组指针
7.1 指向数组元素的指针
例如定义一个整型数组和一个指针变量,语句如下。
int a[5]={10,20,30,40,50};
int *aPtr;
这里的a是一个数组,它包含了5个整型数据。变量名a就是数组a的首地址,它与&a[0]等价。如果令aPtr=&a[0]或者
aPtr=a,则aPtr也指向了数组a的首地址。
也可以在定义指针变量时直接赋值,如以下语句是等价的。
int *aPtr=&a[0];
int *aPtr;
aPtr =&a[0];
与整型、浮点型数据一样,指针也可以进行算术运算,但含义却不同。当一个指针加1(或减)1并不是指针值增加(或减少)1,而是使指针指向的位置向后(或向前)移动了一个位置,即加上(或减去)该整数与指针指向对象的大小的乘积。例如对于aPtr+=3,如果一个整数占用4个字节,则相加后aPtr=2000+4*3=2012(这里假设指针的初值是2000)。同样指针也可以进行自增(++)运算和自减(--)运算。
也可以用一个指针变量减去另一个指针变量。例如,指向数组元素的指针aPtr的地址是2008,另一个指向数组元素的指针bPtr的地址是2000,则a=aPtr-bPtr的运算结果就是把从aPtr到bPtr之间的元素个数赋给a,元素个数为(2008-2000)/4=2(假设整数占用4个字节)。
我们也可以通过指针来引用数组元素。例如以下语句。
*(aPtr+2);
如果aPtr是指向a[0],即数组a的首地址,则aPtr+2就是数组a[2]的地址,*(aPtr+2)就是30。
注意:指向数组的指针可以进行自增或自减运算,但是数组名则不能进行自增或自减运算,这是因为数组名是一个常量指针,它是一个常量,常量值是不能改变的。
#include<stdio.h>
int main(){
int a[5] = {10, 20, 30, 40, 50};
int *aPtr, i;
aPtr = &a[0];
for(i=0;i<5;i++){ //通过数组下标引用元素的方式输出数组元素
printf("a[%d]=%d\n", i, a[i]);
}
for(i=0;i<5;i++){ //通过数组名引用元素的方式是输出数组元素
printf("*(a+%d)=%d\n", i, *(a+i));
}
for(i=0;i<5;i++){ //通过指针变量下标引用元素的方式输出数组元素
printf("aPtr[%d]=%d\n", i, aPtr[i]);
}
for(aPtr=a, i=0; aPtr<a+5; aPtr++, i++){ //通过指针变量偏移的方式输出数组元素
printf("*(aPtr+%d)=%d\n", i, *aPtr);
}
return 0;
}
7.2 指针数组
定义:指针数组其实也是一个数组,只是数组中的元素是指针类型的数据。换句话说,指针数组中的每一个元素都是一个指针变量。
定义指针数组的方式如下:
int *p[4]
例1:使用指针数组保存字符串并将字符串打印输出。
#include<stdio.h>
int main(){
// 定义指针数组
const char *s[4] = {"ABC", "DEF", "GHI", "JKL"};
int n = 4;
int i;
const char *aPtr;
// 方法1:通过数组名输出字符串
for(i=0;i<n;i++){
printf("第%d个字符串:%s\n", i+1, s[i]);
}
// 方法2:通过指向数组的指针输出字符串
for(aPtr=s[0],i=0;i<n;aPtr=s[i]){
printf("第%d个字符串:%s\n", i+1, aPtr);
i++;
}
return 0;
}
运行结果图示:
注:常量与指针间的转换 warning: ISO C++ forbids converting a string constant to 'char*'
Q:为什么s[i]打印的是值而不是地址?
A:%s占位符的特点就是只要告诉他字符串的首地址,就可以读取整个字符串
例2:利用指针数组实现对一组变量的值按照从小到大排序,排序时交换变量的指针值。
/* 利用指针数组实现对一组变量的值按照从小到大排序,排序时交换变量的指针值。 */
#include<stdio.h>
int main(){
int a, b, c, d, e;
int *s[5] = {&a, &b, &c, &d, &e};
int i,j;
int n=5;
int *p;
// 用户输入5个数(大小任意)
printf("请输入5个任意正整数(空格分隔):\n");
scanf("%d%d%d%d%d", &a, &b, &c, &d, &e);
printf("排序前:\n");
for(i=0;i<n;i++){
printf("%d\n",*s[i]);
}
// 排序:冒泡排序
for(i=0;i<n-1;i++){ // 执行n-1趟
for(j=0;j<n-i-1;j++){ // 每一趟需要执行n-1-i次比较操作
if(*s[j] > *s[j+1]){
p = s[j];
s[j] = s[j+1];
s[j+1] = p;
}
}
}
printf("排序后:\n");
for(i=0;i<n;i++){
printf("%d\n",*s[i]);
}
return 0;
}
运行结果图示:
7.3 数组指针
定义:数组指针是指向数组的一个指针。如下定义:
int (*p)[4]
其中,p是指向一个拥有4个元素的数组的指针,数组中每个元素都为整型。与前面刚刚介绍过的指针数组做比较,这里定义的数组指针多了一对括号,*p两边的括号不可以省略。这里定义的p仅仅是一个指针,不过这个指针有点特殊,这个p指向的是包含4个元素的一维数组。
数组指针p与它指向的数组之间的关系可以用下图来表示。
如果有如下语句:
int a[3][4]={{1,2,3,4},{5,6,7,8},{9,10,11,12}};
p=a;
数组指针p与数组a中元素之间的关系如图所示。其中,(*p)[0]、(*p)[1]、(*p)[2]、(*p)[3]分别保存的是元素值为1、2、3、4的值。p、p+1和p+2分别指向二维数组的第一行、第二行和第三行,p+1表示将指针p移动到下一行。
*(p+1)+2表示数组a第1行第2列的元素的地址,即&a[1][2],*(*(p+1)+2)表示a[1][2]的值即7,其中1表示行,2表示列。
Q:为什么(*p)[0]、(*p)[1]、(*p)[2]、(*p)[3]分别保存的是元素值为1、2、3、4的值而不是它们的地址呢?
A:指针[i] == *(指针+i)
(*p)[0] == *(*p+0)→p本来表示的是第0行一整行的数据,出现在表达式中将自动转为指向a[0][0]的指针→*p+0还是指向a[0][0]的指针→*(*p+0)即对地址取值。
下面编程输出以上数组指针的值和数组的内容。
#include<stdio.h>
int main(){
int a[3][4] = {{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}};
int (*p)[4] = a; // 声明数组指针p(p是一个指向'内含4个整型元素的数组'的指针)
int row, col;
// 输出数组的内容
for(row=0;row<3;row++){
for(col=0;col<4;col++){
printf("a[%d][%d]=%-4d", row, col, *(*(p+row)+col));
}
printf("\n");
}
// 输出数组指针的值
for(row=0;row<3;row++, p++){
for(col=0;col<4;col++){
printf("(*p[%d])[%d]=%p\t", row, col, (*p+col));
}
printf("\n");
}
return 0;
}
运行结果图示:
注释:[] == *()→如,p[0] 等价于 *(p+0)
8.指针函数与函数指针
8.1 指针函数
指针函数是指函数的返回值是指针类型的函数。例如,以下是一个指针函数的声明:
float *func(int a, int b);
func是函数名,前面的'*'表明返回值的类型是指针类型,因为前面的类型标识符是float,所以返回的指针是指向浮点型的。
例:假设若干个学生的成绩存放在二维数组中,要求输入学生编号,利用指针函数实现其成绩的输出。
#include<stdio.h>
int *FindAddress(int (*ptrScore)[4], int index);
void Display(int *, int n);
int main(){
int score[3][4] = {{83, 78, 79, 88}, {71, 88, 92, 63}, {99, 92, 87, 80}};
int n = 4;
int row;
int *p;
while(1){
printf("请输入学生编号(1 or 2 or 3),输入0退出程序:\n");
scanf("%d", &row);
if(row == 0){
break;
}else if(row == 1 || row == 2 || row == 3){
printf("第%d名学生的各科成绩分别为:\n", row);
p = FindAddress(score, row-1);
Display(p,n);
}else{
printf("输入不合法,请重新输入!\n");
}
}
return 0;
}
int *FindAddress(int (*ptrScore)[4], int index){
/*查找某条学生成绩记录地址函数。通过传递的行地址找到要查找学生成绩所在行,并返回该行的首元素地址*/
int *ptr;
ptr = *(ptrScore+index);
return ptr;
}
void Display(int *ptr, int n){
/*输出学生成绩的实现函数。利用传递过来的指针输出每门课的成绩*/
int col;
for(col=0; col<n; col++){
printf("%4d", *(ptr+col));
}
printf("\n");
}
注:
p = FindAddress(score, row-1);
二维数组的数组名表示啥?若a是一维数组,则a指向的是第一个元素。
若a是二维数组,也可以将a看成一个一维数组,那么其元素是其行向量。则a指向的是第一个行向量。
8.2 函数指针
指针可以指向变量、数组,也可以指向函数,指向函数的指针就是函数指针。
1)函数指针的调用
例1:通过一个函数求两个数的乘积,并通过函数指针调用该函数。
#include<stdio.h>
int Mult(int a, int b);
int main(){
int a, b;
int (*func)(int, int);
printf("请输入2个数:\n");
scanf("%d%d", &a, &b);
/*方法1:函数名调用*/
printf("%d * %d = %d\n", a, b, Mult(a, b));
/*方法2:函数指针调用*/
func = &Mult; // 因为函数名本身就是地址,所以&可以省略
printf("%d * %d = %d\n", a, b, func(a, b));
return 0;
}
int Mult(int x, int y){
return x*y;
}
2)函数指针作为函数参数的使用
例2:利用函数指针作为函数参数,实现选择排序算法的升序排列和降序排列。
#include<stdio.h>
void SelectSort(int *, int, int (*)(int, int)); //选择排序,函数指针作为参数调用
int Ascending(int, int); // 是否进行升序排列
int Descending(int, int); // 是否进行降序排列
void swap(int *, int *);
void Display(int a[], int n);
int main(){
int a[10] = {13, 23, 11, 4, 9, 16, 22, 23, 9, 10};
printf("排序前数列:\n");
Display(a, 10);
printf("升序排列:\n");
SelectSort(a, 10, Ascending);
Display(a, 10);
printf("降序排列:\n");
SelectSort(a, 10, Descending);
Display(a, 10);
return 0;
}
void swap(int *a, int *b){
int temp = *a;
*a = *b;
*b = temp;
}
void Display(int a[], int n){
int i;
for(i=0;i<n;i++){
printf("%4d", a[i]);
}
printf("\n");
}
int Ascending(int a, int b){
if(a>b){
return 1;
}else{
return 0;
}
}
int Descending(int a, int b){
if(a<b){
return 1;
}else{
return 0;
}
}
void SelectSort(int *ptr, int n, int (*compare)(int, int)){
/*选择排序基本思想(升序):每一趟排序从n-i个元素中选取关键字最小的元素作为有序序列的第i个元素*/
int i, j, k;
// 将第i个元素与后面n-i个元素进行比较,将关键字最小的元素放在第i个位置
for(i=0;i<n;i++){
j=i; // 初始时,关键字最小的元素下标为i
for(k=j+1;k<n;k++){
if(compare(*(ptr+j), *(ptr+k))){
j=k;
}
}
if(j!=i){
swap(ptr+j, ptr+i);
}
}
}
其中,函数SelectSort(a,N,Ascending)中的参数Asscending是一个函数名,传递给函数定义void SelectSort(int *p,int n,int(*compare)(int,int))中的函数指针compare,这样指针就指向了Asscending。从而可以在执行语句(*compare)(a[j], a[j+1])时调用函数Ascending(int a,int b)判断是否需要交换数组中两个相邻的元素,然后调用swap(&a[j],&a[j+1])进行交换。
8.3 函数指针数组
假设有3个函数f1、f2和f3,可以把这3个函数作为数组元素存放在一个数组中,需要定义一个指向函数的指针数组指向这
三个函数,代码如下:
void (*f[3])(int)={f1,f2,f3};
f是包含3个指向函数指针元素的数组,f[0]、f[1]和f[2]分别指向函数f1、f2和f3。通过函数指针f调用函数的形式如下。
f[n](m); /*n和m都是正整数*/
例:声明一个指向函数的指针数组,并通过指针调用函数。
#include<stdio.h>
void f1(int n); /*函数f1声明*/
void f2(int n); /*函数f2声明*/
void f3(int n); /*函数f3声明*/
int main(){
void (*f[3])(int)={f1,f2,f3}; /*声明指向函数的指针数组*/
int flag;
printf("调用函数请输入1、2或者3,结束程序请输入0。\n");
scanf("%d",&flag);
while(flag){
if(flag==1||flag==2||flag==3){
f[flag-1](flag); /*通过函数指针调用数组中的函数*/
printf("请输入1、2或者3,输入0结束程序.\n");
scanf("%d",&flag);
}else{
printf("请输入一个合法的数(1~3),输入0结束程序.\n");
scanf("%d",&flag);
}
}
printf("程序结束.\n");
return 0;
}
void f1(int n) /*函数f1的定义*/
{
printf("函数f%d:调用第%d个函数!\n",n,n);
}
void f2(int n) /*函数f2的定义*/
{
printf("函数f%d:调用第%d个函数!\n",n,n);
}
void f3(int n) /*函数f3的定义*/
{
printf("函数f%d:调用第%d个函数!\n",n,n);
}
函数指针不能执行像f+1、f++、f--等运算。