C语言中关于指针的易混淆、难理解的知识点

指针

指针基础知识

指针,就是内存中的一个地址。
计算机内存中的每个位置都由一个地址标识。通常,临近的内存位置合成一组,就允许存储更大范围的值。可以用指针指向这个范围的首地址,从而实现对数据的存储、读写、调用等。

int i=0;//在32位计算机中,i占用4个字节
int *p=&i;//指针p指向整型变量i的首地址
int a[10]={0};//数组也可以看成是一个指针,a为10个整型数据所占用内存区域的首地址,sizeof(a)等于10*4

指针的值并非它所指向的内存位置的存储的值,就像上例中,指针p的值,并不是i的值0,而是内存中的一个地址,如0x12。我们必须使用间接访问(*)来获取指针所指向位置存储的值。如,对一个“指向整型的指针”施加间接访问的操作结果将是一个整型值。
声明一个指针变量并不会自动分配任何内存。在对指针执行间接访问前,指针必须要进行初始化:或者使它指向现有的内存,或者给他分配动态内存。对未初始化的指针变量执行间接访问操作是非法的,而且这种错误常常难以检测。其结果常常是一个不相关的值被修改。
NULL指针就是不指向任何东西的指针。它可以赋给一个指针,用于表示那个指针并不指向任何值。对NULL指针执行间接访问操作的后果因编译器而异,两个常见的后果分别是返回内存位置置零的值以及终止程序。
指针变量可以作为左值使用。对指针执行间接访问操作所产生的值也是个左值,因为这种表达式标识了一个特定的内存位置。如下例所示:

int i =0;
int *p=NULL;
p=&i;
*p=i;
指针运算

可以把一个整型值加到一个指针上,也可以从一个指针减去一个整型值。不过这些运算只有作用于数组,其结果才是可预测的,因为数组存储的数据都是同一类型的。而对于非数组,其地址后面内存位置存储什么类型数据,是不确定的。如下例:

int a=1;
int *p1=&a;
int *p2=p1+3;//现在p2指向的地址为p1地址加上sizeof(int)*3

对于数组,进行加减比较好理解,但注意不要越界。

指针类型转换

C语言允许强制对指针指向的类型进行转换。如double db=12.3;int *p = (int *)&db;但在进行强制类型转换后,指针指向的位置还是原有数据类型的位置,这个时候重新给指针进行运算或者赋值操作,有可能会引发问题。
如:
char s='a';int *ptr=(int *)&s;*ptr=1298;
s原本只占用一个字节,但转换成int型指针后,将占用4个字节(32位系统),而对*ptr赋值,会占用掉s所占用内存后的3个字节,而这3个字节中可能会存储比较重要的数据,从而会对整个程序造成影响。
在指针的强制类型转换:ptr1=(TYPE *)ptr2 中,如果sizeof(ptr2的类型)大于sizeof(ptr1 的类型),那么在使用指针ptr1 来访问ptr2所指向的存储区时是安全的。如果sizeof(ptr2 的类型) 小于sizeof(ptr1 的类型),那么在使用指针ptr1 来访问ptr2 所指向的存储区时是不安全的。

高级指针话题

指向指针的指针
int i;
int *pi;
int **pi;

在上面代码中,pi是一个指针,其值是一个内存地址,而ppi是一个指向指针的指针,首先,其还是一个指针,其值仍然是一个内存地址,不过,ppi的内存位置,存储的地址是指向一个指针的。
因此,可以有以下操作:

ppi=π//通过取地址符,获取pi的地址,并将其赋给ppi

而*ppi,也是一个内存地址,不过此时*ppi是一个指针,其内存地址指向一个int型变量。所以有:

*ppi=&i;

所以,以下语句具有相同的效果:

i=1;
*pi=1;
**ppi=1;

对于多层间接访问,最好只有在确实需要时,才使用。

指针与数组

1)一维数组与指针
数组是存储某种固定类型,具有一定的存储空间的结构类型。由于数组指向的存储空间是连续的,所以,数组的起始存储位置,也就是数组名,可以看成是一个指针。

int a[10]={1,2,3,4,5};
int b;
b=a[0];//相当于b=*a
b=a[3];//相当于b=*(a+3),在此处想想前面讲述的指针的运算操作

关于数组,在此补充一下:数组名是一个指向某种类型的常量指针(指向不能更改,在分配内存时,其指向已确定为数组第一个元素的地址,所以不允许数组之间进行赋值操作)。
对于数组名,有两个例外:sizeof返回整个数组所占用的字节而不是一直指针所占用的字节;单目操作符&返回一个指向数组的指针而不是一个指向数组第一个元素的指针的指针。
2)多维数组与指针
下面以二维数组为例进行解释。
我们知道,一个二维数组在计算机中存储时,是按照先行后列的顺序依次存储的,当把每一行看作一个整体,即视为一个大的数组元素时,这个存储的二维数组也就变成了一个一维数组了。而每个大数组元素对应二维数组的一行,我们就称之为行数组元素,显然每个行数组元素都是一个一维数组。

int matrix[M][N] = {0};
int *p = matrix[0];

可知,目前p指向二维数组matrix的第一行,则p+j将指向matrix[0]数组中的元素matrix[0][j]。
由于matrix[0]、matrix[1]┅matrix[M-1]等各个行数组依次连续存储,则对于matrix数组中的任一元素matrix[i][j],指针的一般形式为:p+i*N+j
元素matrix[i][j]相应的指针表示为:*( p+i*N+j)
同样,matrix[i][j]也可使用指针下标法表示为:p[i*N+j]

int matrix[3][4] = {0};
int *p = matrix[0];
//则matrix[1][2]对应的指针为:p+1*4+2;元素matrix[1][2]也就可以表示
//为:*( p+1*4+2);用下标表示法,matrix[1][2]表示为:p[1*4+2]

特别注意,对于二维数组matrix,matrix与matrix[0]的区别。虽然二者都是数组首地址,但是其指向的对象不同。matrix[0]是一维数组的名字,其表示的是数组的第一行,它指向的是matrix[0]数组的首元素,对其进行“*”运算,得到的是一个数组元素值,即matrix[0]数组首元素值,因此,*matrix[0]与matrix[0][0]是同一个值;而matrix是二维数组的名,它指向的是它所属数组的首元素,它的每一个元素都是一个行数组,因此,它的指针移动单位是“行”,所以matrix+i指向的是第i个行数组,即指向matrix[i]。对matrix进行“*”运算,得到的是一维数组matrix[0]的首地址,即*matrix与matrix[0]是同一个值。当用int *p;定义指针p时,p的指向是一个int型数据,而不是一个地址,因此,用matrix[0]对p赋值是正确的,而用matrix对p赋值是错误的(可参看数组指针部分)。

int a[M][N] = {0};
//对于数组a,其a[0]数组由a指向,a[1]数组则由a+1指向,a[2]数组由a+2指向。
//则*a与a[0]等价、*(a+1)与a[1]等价、┅,即对于a[i]数组,由*(a+i)指向。
//由此,对于数组元素a[i][j],用数组名a的表示形式为:*(*(a+i)+j)
//指向该元素的指针为:*(a+i)+j

3)数组指针
数组指针,也称为指向数组的指针。首先它是一个指针,其指向一个数组。

int (*f)[10];//一个数组指针

由于[]的优先级大于*,所以将*f用括号括起来,表示其是一个整体。*f是一个指针,其指向一个数组。
其应用场景是:

int matrix[3][10];
int (*p)[10] = matrix;

4)指针数组
指针数组,首先是一个数组,数组中的元素是一个个指针。

int *p[10];

由于[]的优先级高于*,所以p首先执行下标引用,因此p是一个数组,数组中元素的类型是指针。

指针与函数

1)返回值为指针的函数

int *f();

函数调用操作符( )的优先级要大于间接访问操作符。所以,f是一个函数,其返回类型是一个指向整型的指针。
2)函数指针

int (*f)();

f是一个函数指针,它所指向的函数返回一个整型值。
程序中的每个函数都位于内存中的某个位置,所以存在指向那个位置的指针也是完全可能的。
3)函数指针数组

int (*f[])();

f是一个数组,数组内的元素是一个个指针。上面表达式表示的是一个返回为整型的函数指针数组。

函数指针

函数指针常用来做回调函数以及转移表。
简单声明一个函数指针并不意味着它马上就可以使用。和其他指针一样,对函数指针执行间接访问之前必须初始化为指向某个函数。下面的代码段说明了一种初始化函数指针的方法。

int f(int);
int (*pf)(int) = &f;

第二个声明创建了函数指针pf,并把它初始化为指向函数f。初始化表达式中的&操作符是可选的,因为函数名在使用时总是由编译器把他转化为指针。&操作符只是显式地说明了编译器将隐式执行的任务。
函数指针的初始化也可以通过一条赋值语句来完成。如pf=f;pf=&f;在函数指针初始化之前具有f的原型很重要,否则编译器将无法检查f的类型是否与pf所指向的类型一致。
三种方式调用函数:

int ans;
ans = f(25);
ans = (*pf)(25);
ans = pf(25);

三者是等价的。
1)回调函数
函数指针变量可以作为某个函数的参数来使用的,回调函数就是一个通过函数指针调用的函数。
简单讲:回调函数是由别人的函数执行时调用你实现的函数。
以下是自知乎作者常溪玲的解说:

你到一个商店买东西,刚好你要的东西没有货,于是你在店员那里留下了你的电话,过了几天店里有货了,店员就打了你的电话,
然后你接到电话后就到店里去取了货。在这个例子里,你的电话号码就叫回调函数,你把电话留给店员就叫登记回调函数,
店里后来有货了叫做触发了回调关联的事件,店员给你打电话叫做调用回调函数,你到店里去取货叫做响应回调事件。
#include <stdlib.h>  
#include <stdio.h>

// 回调函数
void populate_array(int *array, size_t arraySize, int (*getNextValue)(void))
{
    for (size_t i=0; i<arraySize; i++)
        array[i] = getNextValue();
}

// 获取随机值
int getNextRandomValue(void)
{
    return rand();
}

int main(void)
{
    int myarray[10];
    populate_array(myarray, 10, getNextRandomValue);
    for(int i = 0; i < 10; i++) {
        printf("%d ", myarray[i]);
    }
    printf("\n");
    return 0;
}

实例中 populate_array 函数定义了三个参数,其中第三个参数是函数的指针,通过该函数来设置数组的值。
实例中我们定义了回调函数 getNextRandomValue,它返回一个随机值,它作为一个函数指针传递给 populate_array 函数。populate_array 将调用 10 次回调函数,并将回调函数的返回值赋值给数组。
2)转移表
通常情况下,我们会用if-else或者switch-case来描述一些不同情况下的处理方式,但是,若某种逻辑下需要描述的状态值特别多,if-else或者switch-case就会显得代码量庞大且不易维护,这个时候就可以用函数指针(转移表)来解决。
常见的例子是计算器处理逻辑,若用switch-case来编写代码:

switch(operation)  
{  
      case ADD:  
              result=add(a,b);break;  
      case SUB:  
              result=sub(a,b);break;  
      case MUL:  
              result=mul(a,b);break;  
      case DIV:  
              result=div(a,b);break;  
      .....  
}  

如果这个计算器要实现的功能很多,那么将有很多这样的语句,可维护性很差。如果我们将具体的数值操作与选择操作的代码分开将会提高代码的可读性。
这种情况下,我们需要建立一个“转移表”。在建立转移表之间需要对涉及到的函数提前声明,然后建立转移表,对于上面的可以这么修改:

double add(double,double);  
double sub(double,double);  
double mul(double,double);  
double div(double,double);  
......  

那么建立的转移表如下:

double (*operation_fun[])(double,double)={add,sub,mul,div,......};  

可见,用一个函数指针数组,来包括多个函数,从而实现转移表的建立。
在调用的时候可以这样操作:

double result;  
result=operation_fun[operation](a,b);  

在实际使用中,只需将operation和计算方式一一对应,就可以实现对各个函数的调用。

参考:
1.《C和指针》
2.http://blog.csdn.net/soonfly/article/details/51131141
3.https://blog.csdn.net/iu_81/article/details/1782642

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值