c语言 有关各类指针的理解

此篇文章为本人自主学习,根据自身理解,整理的学习笔记(若侵权,请联系本人删文)

各类指针理解

目录

各类指针理解

巧记

以数组指针以及指针数组作为案列

        数组指针 (*p)[n]

        指针数组 *p[n]

以函数指针以及指针函数作为案列

        1.指针函数

        2.函数指针

以指针常量以及常量指针作为案列

1、指针常量——指针类型的常量(int *const p)

2、常量指针——指向“常量”的指针(const int *p, int const *p)

引用文献:数组指针和指针数组_mick_hu的博客-CSDN博客

(3条消息) 指针函数和函数指针函数指针和指针函数惟肖肖肖的博客-CSDN博客

(3条消息) 指针常量和常量指针_qq_36132127的博客-CSDN博客

巧记

1.数组指针--------指向数组的指针(可以当二维数组使用 int (*p)[N])

2.指针数组--------装着指针的数组(就是数组里面装的元素是指针即地址 int*p[N])

3.函数指针--------指向(调用)函数的指针

(就是通过指针调用函数的地址,从而达到调用函数 int(*p)(int,int) 可以调用形如 int get_sum(int a,int b)的函数)

4.指针函数--------返回指针的函数

(就是函数的返回值是一个指针 int *get_sum(int a,int b).....int *sum......return sum;)

5.常量指针--------指向“常量”的指针(指针指向的值不能改变 const int *p )

6.指针常量--------指针类型的常量(p地址不能改变 int *const p)

以数组指针以及指针数组作为案列

参考自:数组指针和指针数组_mick_hu的博客-CSDN博客

首先,理解一下数组指针和指针数组这两个名词:

“数组指针”和“指针数组”,只要在名词中间加上“的”字,就知道中心了——

数组的指针:是一个指针,什么样的指针呢?指向数组的指针。

指针的数组:是一个数组,什么样的数组呢?装着指针的数组。

然后,需要明确一个优先级顺序:()>[]>*,所以:

(*p)[n]:根据优先级,先看括号内,则p是一个指针,这个指针指向一个一维数组,数组长度为n,这是“数组的指针”,即数组指针;

p[n]:根据优先级,先看[],则p是一个数组,再结合,这个数组的元素是指针类型,共n个元素,这是“指针的数组”,即指针数组。

根据上面两个分析,可以看出,p是什么,则词组的中心词就是什么,即数组“指针”和指针“数组”。

int *p1[5];
​
​
​
int (*p2)[5];

首先,对于语句“intp1[5]”,因为“[]”的优先级要比“”要高,所以 p1 先与“[]”结合,构成一个数组的定义,数组名为 p1,而“int*”修饰的是数组的内容,即数组的每个元素。也就是说,该数组包含 5 个指向 int 类型数据的指针,如图 1 所示,因此,它是一个指针数组。

img 图1

其次,对于语句“int(p2)[5]”,“()”的优先级比“[]”高,“”号和 p2 构成一个指针的定义,指针变量名为 p2,而 int 修饰的是数组的内容,即数组的每个元素。也就是说,p2 是一个指针,它指向一个包含 5 个 int 类型数据的数组,如图 2 所示。很显然,它是一个数组指针,数组在这里并没有名字,是个匿名数组。

img

图2

由此可见,对指针数组来说,首先它是一个数组,数组的元素都是指针,也就是说该数组存储的是指针,数组占多少个字节由数组本身决定;而对数组指针来说,首先它是一个指针,它指向一个数组,也就是说它是指向数组的指针,在 32 位系统下永远占 4 字节,至于它指向的数组占多少字节,这个不能够确定,要看具体情况。

数组指针 (*p)[n]

数组指针:是指针——指向数组的指针。

看下面的例子进行理解

#include "stdafx.h"
 
 
int main()
{
    //一维数组
    int a[5] = { 1, 2, 3, 4, 5 };
    //步长为5的数组指针,即数组里有5个元素
    int (*p)[5];
    //把数组a的地址赋给p,则p为数组a的地址,则*p表示数组a本身
    p = &a;
 
    //%p输出地址, %d输出十进制
    //\n回车
    //在C中,在几乎所有使用数组的表达式中,数组名的值是个指针常量,也就是数组第一个元素的地址,它的类型取决于数组元素的类型。
    printf("%p\n", a); //输出数组名,一般用数组的首元素地址来标识一个数组,则输出数组首元素地址
    printf("%p\n", p); //根据上面,p为数组a的地址,输出数组a的地址
    printf("%p\n", *p); //*p表示数组a本身,一般用数组的首元素地址来标识一个数组
    printf("%p\n", &a[0]); //a[0]的地址
    printf("%p\n", &a[1]); //a[1]的地址
    printf("%p\n", p[0]); //数组首元素的地址
    printf("%d\n", **p); //*p为数组a本身,即为数组a首元素地址,则*(*p)为值,当*p为数组首元素地址时,**p表示首元素的值1
    printf("%d\n", *p[0]); //根据优先级,p[0] 表示首元素地址,则*p[0]表示首元素本身,即首元素的值1
    printf("%d\n", *p[1]); //为一个绝对值很大的负数,不表示a[1]...表示什么我还不知道
 
    
 
    //将二维数组赋给指针
    int b[3][4];
    int(*pp)[4]; //定义一个数组指针,指向含4个元素的一维数组
    pp = b; //将该二维数组的首地址赋给pp,也就是b[0]或&b[0],二维数组中pp=b和pp=&b[0]是等价的
    pp++; //pp=pp+1,该语句执行过后pp的指向从行b[0][]变为了行b[1][],pp=&b[1]
 
    int k;
    scanf_s("%d", &k);
 
    return 0;
}
 
 
 

根据上面二维数组可以得出,数组指针也称指向一维数组的指针,所以数组指针也称行指针。

指针数组 *p[n]

指针数组:是数组——装着指针的数组。

看下面的例子进行理解:

#include "stdafx.h"
​
int main()
​
{
    int a = 1;
    int b = 2;
    int *p[2];
    p[0] = &a;
    p[1] = &b;
​
    printf("%p\n", p[0]); //a的地址
​
    printf("%p\n", &a); //a的地址
​
    printf("%p\n", p[1]); //b的地址
​
    printf("%p\n", &b); //b的地址
​
    printf("%d\n", *p[0]); //p[0]表示a的地址,则*p[0]表示a的值
​
    printf("%d\n", *p[1]); //p[1]表示b的地址,则*p[1]表示b的值
​
    //将二维数组赋给指针数组
​
    int *pp[3]; //一个一维数组内存放着三个指针变量,分别是p[0]、p[1]、p[2],所以要分别赋值
​
​
    int c[3][4];
​
    for (int i = 0; i<3; i++)
        pp[i] = c[i];
    int k;
​
    scanf_s("%d", &k);
​
    return 0;
​
​
​
}
​
​
​
 

最后,从上文来看:

数组指针是一个指针变量,占有内存中一个指针的存储空间;

指针数组是多个指针变量,以数组的形式存储在内存中,占有多个指针的存储空间。

了解指针数组和数组指针二者之间的区别之后,继续来看下面的示例代码:

int arr[5]={1,2,3,4,5};
​
​
​
int (*p1)[5] = &arr;
​
​
​
/*下面是错误的*/
​
​
​
int (*p2)[5] = arr;

不难看出,在上面的示例代码中,&arr 是指整个数组的首地址,而 arr 是指数组首元素的首地址,虽然所表示的意义不同,但二者之间的值却是相同的。那么问题出来了,既然值是相同的,为什么语句“int(p1)[5]=&arr”是正确的,而语句“int(p2)[5]=arr”却在有些编译器下运行时会提示错误信息呢(如在 Microsoft Visual Studio 2010 中提示的错误信息为“a value of type"int"cannot be used to initialize an entity of type"int()[5]"”)?

其实原因很简单,在 C 语言中,赋值符号“=”号两边的数据类型必须是相同的,如果不同,则需要显示或隐式类型转换。在这里,p1 和 p2 都是数组指针,指向的是整个数组。p1 这个定义的“=”号两边的数据类型完全一致,而 p2 这个定义的“=”号两边的数据类型就不一致了(左边的类型是指向整个数组的指针,而右边的数据类型是指向单个字符的指针),因此会提示错误信息。

以函数指针以及指针函数作为案列

参考自:(3条消息) 指针函数和函数指针函数指针和指针函数惟肖肖肖的博客-CSDN博客

1.指针函数

(1)本质是一个函数,不过它的返回值是一个指针。其声明的形式:类型名 *函数名(函数参数列表);

int * pfun(int, int);

由于“*”的优先级低于“()”的优先级,因而pfun首先和后面的“()”结合,也就意味着,pfun是一个函数;

接着再和前面的“*”结合,说明这个函数的返回值是一个指针。由于前面还有一个int,也就是说,pfun是一个返回值为整型指针的函数。

(2)返回类型可以是任何基本类型和复合类型。返回指针的函数的用途十分广泛。事实上,每一个函数,即使它不带有返回某种类型的指针,它本身都有一个入口地址,该地址相当于一个指针。比如函数返回一个整型值,实际上也相当于返回一个指针变量的值,不过这时的变量是函数本身而已,而整个函数相当于一个“变量”。

(3)void型指针

    void指针是一种不明确类型的指针,任何指针都可转换为void指针。
​
   指针有两个非常重要的信息:

指针的值(指针目标对象的内存首地址) 指针指向对象的类型 注意点:void指针只保存了 指针的值 并没有记录 指针指向对象的类型。因此在用到对void指针解引时,需要先把void指针转换成原本的数据类型。

int n = 500; //定义一个int变量
​
int * p = &n; //定义int类型指针
​
void * pv = p; //定义void指针,只保存了p的值(即n的内存首地址)
​
//错误的写法
​
printf("%d\n", *pv); //这里会报错,因pv指针没有明确数据类型,因此也不知道需要取多少字节的数据
​
//正确写法
​
printf("%d\n", *( (int*)pv ) ); //先把pv指针转为int类型指针,再对其解引

(4)举例:

//打印第m个学生的成绩
#include 
 float *find(float(*pionter)[4],int n);//函数声明
 int main(void)
 {
     static float score[][4]={{60,70,80,90},{56,89,34,45},{34,23,56,45}};//3个学生的成绩
     float *p;                                 //定义float类型的指针
     int i,m;
     printf("Enter the number to be found:");
     scanf("%d",&m);
     printf("the score of NO.%d are:\n",m);
     p=find(score,m-1);                       //将find函数返回的地址给p
     for(i=0;i<4;i++)
         printf("%5.2f\t",*(p+i));            //打印(score+n)行i列数据
​
     return 0;
​
 }
​
float *find(float(*pionter)[4],int n)/*定义指针函数*/
 {
     float *pt;                             //定义float类型的指针
     pt=*(pionter+n);                       //将(pionter+n)行的首地址给pt
     return(pt);                            //返回float类型的地址
 }

共有三个学生的成绩,函数find()被定义为指针函数,其形参pointer是指针指向包含4个元素的一维数组的指针变量(即简称数组指针)。

补充:为什么*(point+n)是表示地址?而不是取值。

    *(point+1)单独使用时表示的是第 1行数据,放在表达式中会被转换为第 1 行数据的首地址,也就是第 1 行第 0 个元素的地址。因为使用整行数据没有实际的含义,编译器遇到这种情况都会转换为指向该行第 0 个元素的指针;就像一维数组的名字,在定义时或者和 sizeof、& 一起使用时才表示整个数组,出现在表达式中就会被转换为指向数组第 0 个元素的指针。

main()函数中调用find()函数,将score数组的首地址传给pointer。

2.函数指针

(1)函数指针是指向函数的指针变量。 因此“函数指针”本身首先应是指针变量,只不过该指针变量指向函数。

C在编译时,每一个函数都有一个入口地址,该入口地址就是函数指针所指向的地址。有了指向函数的指针变量后,可用该指针变量调用函数,就如同用指针变量可引用其他类型变量一样,在这些概念上是大体一致的。函数指针有两个用途:调用函数和做函数的参数。

(2)函数指针的声明方法为:

返回值类型 ( * 指针变量名) ([形参列表]);

注1:“返回值类型”说明函数的返回类型,“(指针变量名 )”中的括号不能省,括号改变了运算符的优先级。若省略整体则成为一个函数说明,说明了一个返回的数据类型是指针的函数,后面的“形参列表”表示指针变量指向的函数所带的参数列表。例如:

int func(int x); /* 声明一个函数 */

int (*f) (int x); /* 声明一个函数指针,是一个指向返回值为int的函数的指针 */

f=func; /* 将func函数的首地址赋给指针f */

或者使用下面的方法将函数地址赋给函数指针:

f = &func;

赋值时函数func不带括号,也不带参数,由于func代表函数的首地址,因此经过赋值以后,指针f就指向函数func(x)的代码的首地址。

(3)举例:

#include
int max(int x,int y){return (x>y? x:y);}  //返回两个数的最大值
int main()
{
    int (*ptr)(int, int);                //定义指向函数的指针变量
    int a, b, c;
    ptr = max;                           //将max函数的地址给ptr
    scanf("%d%d", &a, &b);
    c = (*ptr)(a,b);                     //ptr和max函数地址相同,相当于调用max函数
    printf("a=%d, b=%d, max=%d", a, b, c);
    return 0;
}

ptr是指向函数的指针变量,所以可把函数max赋给ptr,作为ptr的值,即把max函数的入口地址赋给ptr,以后就可以用ptr来调用该函数,实际上ptr和max都指向同一个入口地址。不同就是ptr是一个指针变量,不像函数名称那样是死的,它可以指向任何函数,就看你想怎么做了。在程序中把哪个函数的地址赋给它,它就指向哪个函数。而后用指针变量调用它,因此可以先后指向不同的函数。不过注意,指向函数的指针变量没有++和--运算,用时要小心。

(4)补充

函数指针可作为参数 函数指针可作为返回值

#include 

int add(int num1,int num2)
{
	return num1+num2;
}

int sub(int num1,int num2)
{
	return num1-num2;
}

int fun(int (*fp)(int,int),int num1,int num2)  //函数指针做参数
{
	return (*fp)(num1,num2);
}


int (*select(char c))(int,int)           //函数指针作为返回值
{

	switch(c)
	{
		case '+': return add;
		case '-': return sub;
	}

}
int main()
{
	

	int num1,op,num2;
	int (*fp)(int,int);
	 
	printf("请输入一个表达式,比如(1+3):\n");
	scanf("%d%c%d",&num1,&op,&num2);
	 
	fp=select(op);  //返回的函数指针赋予fp
	 
	printf("%d%c%d=%d\n",num1,op,num2,fun(fp,num1,num2));
	 
	/*
	printf("3+5=%d\n",fun(add,3,5));  函数名就是函数的首地址
	printf("3-5=%d\n",fun(sub,3,5));
	*/
	 
	return 0;
	}


int (*add)(int, int);定义了一个函数指针add,用于指向返回值为int,并且有两个int参数的函数

int (*select(char c))(int,int) 分解如下:

select与后面的(char c)结合,说明是一个函数,即select(char c) 在和结合,说明select函数的返回值是一个指针,即(select(char c)) 在和后面的(int,int)结合,说明select函数返回的指针指向函数,不是指向int类型,即int ((*select(char c)))(int,int) 总结:返回值为select函数指针,该返回值指向一个 返回值为int,两个参数为int类型的函数 (5)使用typedef关键字

int (*PF)(int *, int); PF是一个函数指针变量,用于指向返回值为int,一个int类型参数的函数。

当使用typedef声明后,则PF就成为了一个函数指针类型,即 typedef int (*PF)(int *, int); 这样就定义了返回值的类型。

再用PF作为返回值来声明函数:PF func(int); // func(int)就是一个返回值为函数指针,一个int类型参数的函数

再用PF来声明:PF phead; //phead就是一个函数指针

(6)地址直接操作函数和指针

if(((vu32)(0X20001000+4))&0xFF000000)==0x08000000)

其中:

(vu32)(0X20001000+4))== ((__IO uint32_t)(0X20001000+4))==((volatile unsigned int)(0X20001000+4))

((vu32)(0X20001000+4)) 通过内存寻址访问地址为(0x20001000 + 4)中的值

(0X20001000+4)只是一个常量;

(volatile unsigned int*)(0X20001000+4) 将0x20001000 + 4这个常量强制转化成volatile unsigned int类型的指针;

((volatile unsigned int)(0X20001000+4)) 相对于取0x20001000 + 4地址处的值。

为什么要将0x20001000 + 4这个常量强制转化成volatile unsigned int类型的指针呢?

   假设定义*P , 取地址符 &P 得到P地址,这个地址是随机的系统分配空闲地址。我们不能直接给P赋地址值,因为那样是不合法的,我们只能给指针赋值为NULL。

但是现在必须让指针指向一个已知地址(0X20001000+4),必须转换类型,在地址前面加上指针转换的类型我们这里用的volatile unsigned int*,如果不转换,不成功。

假如使用一个32位处理器,要对一个32位的内存地址进行访问,可以这样定义:

#define RAM_ADDR     (*(volatile unsigned long *)0x01234567)
然后就可以用C语言对这个内存地址进行读写操作了。
读:tmp = RAM_ADDR;
写:RAM_ADDR = 0x55;

这里使用 volatile关键字的好处就是:

1.volatile是一个类型修饰符(typespecifier);

2.volatile关键字声明的变量,编译器对访问该变量的代码就不再进行优化;

3.volatile 关键字声明的变量,对变量的存取不能缓存到寄存器,每次使用时需要在内存中重新存取。

(7)使用typedef定义函数指针

typedef的意义 typedef int a[10]; // a 类型是 int[10];(存放int型数据的数组) a arr; // 定义一个数组:int arr[3];

typedef void (*p)(void); //p 类型是void ( * )void p A; //是指void(A)(void); 语法上typedef属于存储类声明说明符。 a[10]不是int的别名,(p)(void)不是void的别名。 上面的语句把a声明为具有10个int元素的数组的类型别名,p是一种函数指针的类型别名。

函数指针对象赋值用法 两种用法

typedef void (*IapFun)(void);  //定义函数指针
void func(void);               //定义函数
IapFun fun  = func;            //为函数指针对象赋值
fun();                         //这里的fun()其实就相当于跳转到了func()里
typedef void (* IapFun)(void);          //定义函数指针
IapFun jump2app;                         //定义函数指针对象
jump2app=(IapFun) * (vu32*)(appxaddr+4); //为函数指针对象赋值 appxaddr为函数指针地址,例如0x08000000
jump2app();                              //调用函数

以指针常量以及常量指针作为案列

参考自:(3条消息) 指针常量和常量指针_qq_36132127的博客-CSDN博客

1、指针常量——指针类型的常量(int *const p)

本质上一个常量,指针用来说明常量的类型,表示该常量是一个指针类型的常量。在指针常量中,指针自身的值是一个常量,不可改变,始终指向同一个地址。在定义的同时必须初始化。用法如下:

int a = 10, b = 20;
int * const p = &a;

*p = 30; // p指向的地址是一定的,但其内容可以修改

2、常量指针——指向“常量”的指针(const int *p, int const *p)

常量指针本质上是一个指针,常量表示指针指向的内容,说明该指针指向一个“常量”。在常量指针中,指针指向的内容是不可改变的,指针看起来好像指向了一个常量。用法如下:

int a = 10, b = 20;
const int *p = &a;

p = &b; // 指针可以指向其他地址,但是内容不可以改变 3、例题 (1)

int main() {
    int m = 10;
    const int n = 20; // 必须在定义的同时初始化
const int *ptr1 = &m; // 指针指向的内容不可改变
int * const ptr2 = &m; // 指针不可以指向其他的地方
 
ptr1 = &n; // 正确
ptr2 = &n; // 错误,ptr2不能指向其他地方
 
*ptr1 = 3; // 错误,ptr1不能改变指针内容
*ptr2 = 4; // 正确
 
int *ptr3 = &n; // 错误,常量地址不能初始化普通指针吗,常量地址只能赋值给常量指针
const int * ptr4 = &n; // 正确,常量地址初始化常量指针
 
int * const ptr5; // 错误,指针常量定义时必须初始化
ptr5 = &m; // 错误,指针常量不能在定义后赋值
 
const int * const ptr6 = &m; // 指向“常量”的指针常量,具有常量指针和指针常量的特点,指针内容不能改变,也不能指向其他地方,定义同时要进行初始化
*ptr6 = 5; // 错误,不能改变指针内容
ptr6 = &n; // 错误,不能指向其他地方
 
const int * ptr7; // 正确
ptr7 = &m; // 正确
 
int * const ptr8 = &n;
*ptr8 = 8;
 
return 0;
}

(2)判断下面程序对错,并说明理由

int main()
{
    char * const str = "apple";
    * str = "orange";
    cout << str << endl;
    getchar();
}

错误

"apple"是字符串常量放在常量区,str指向"apple",那么str指向的是字符串常量"apple"的首地址,也就是字符a的地址,因此str指向字符a,str就等于字符a,对str的修改就是对字符串首字符a的修改,但"apple"是一个字符串常量,常量的值不可修改。

根据字符串赋值规则,可以修改整个字符串,方法是对指向字符串的指针str进行赋值,如下:

str = "orange"; 但依旧是错误的,在该赋值语句中,系统会在常量区一块新的空间写入字符串"orange"并返回其首地址,此时str由指向字符串常量"apple"的首地址变为指向字符串常量"orange"的首地址,str指向的地址发生了变化,但str是指针常量不能被修改,所以错误。

如果想要程序编译通过,就不能将str声明为指针常量,否则str在初始化之后就无法修改。因此将const修饰符去掉,并修改字符串赋值语句,修改后程序如下:

int main()
{
    char * str = "apple";
    str = "orange";
    cout << str << endl;
    getchar();
}

你可能感兴趣的:(c语言)