C语言n番战--指针(五)

嵌入式之路,贵在日常点滴

                                                                ---阿杰在线送代码

参考笔友老朱

目录

一、地址的引入

地址的概念

地址长啥样? 

地址与内存单元中的数据是两个完全不同的概念:

变量地址:

二、指针 

三、指针变量 

变量的访问方式

指针变量的概念 

指针变量的定义

指针变量的初始化方法

不合法的初始化:

注意点: 

四、访问指针所指向的存储空间 

&的意思 

*指针中的两种意思 

五、 指针也可以作为数组传入的形式参数

定义一个指针变量指向数组

指针增量和数组的关系

指针增量,记到回到数组首地址 

指针和数组名的见怪不怪 

数组名和指针区别

六、指针和二维数组(更多的是面试使用)

思考 

通过编程来验证一下

树立认知 

小总结 

七、指针分类型

整型指针,字符指针

数组指针

补:数组元素首地址和数组首地址

arr、arr[0]、&arr[0]和&arr区别

函数指针-----指向函数的指针

指针数组

指针函数 

八、二级指针

一级指针与二级指针关系示例 

二级指针的应用 

结论 

二级指针和二维数组的避坑指南 

一、地址的引入

什么是地址

生活中的地址:

内存地址:

地址的概念

  • 地址是一个十六进制表示的整数,用来映射一块空间,是系统用来查找数据位置的依据。地址标识了存储单元空间,而字节就是最小的存储单位
  • 按字节的编址方式:每一个字节都有一个唯一的地址。例如:一个int型的变量是4个字节,就会对应4个地址,我们只需要取到这个变量的首地址就能得到完整的int型数据

地址长啥样? 

用一个例子感受变量存放的地址: 

#include <stdio.h>

int main()
{
	//什么是整型变量,存放整型数的变量
	//什么是字符变量,存放字符型数据的变量
	//什么是指针变量,存放指针的变量
	//什么是指针变量,存放地址的变量
	
	int a = 10;
	int *p;
    //这里的*是一个标识符,告诉系统我是一个指针变量,
    是用来保存别人地址的,和下方的运算服不同
	p = &a;
	
	printf("变量名访问a:%d\n",a);
	printf("a的地址是:0x%p\n",&a);
	printf("地址访问a:%d\n",*(&a));//取值运算符,他把后面跟的内存地址中的数据”取出来“
	printf("指针变量的方式访问a:%d\n",*p);
	return 0;
}

运行结果:

变量名访问a:10
a的地址是:0x000000000061FE14
地址访问a:10
指针变量的方式访问a:10

地址与内存单元中的数据是两个完全不同的概念:

  • 地址如同房间编号, 根据这个编号我们可以找到对应的房间
  • 内存单元如同房间, 房间是专门用于存储数据的

变量地址:

系统分配给"变量"的"内存单元"的起始地址

int num = 6; // 占用4个字节
//那么变量num的地址为: 0ff06
char c = 'a'; // 占用1个字节
//那么变量c的地址为:0ff0a

二、指针 

  • 在计算机中所有数据都存储在内存单元中,而每个内存单元都有一个对应的地址, 只要通过这个地址就能找到对应单元中存储的数据.
  • 由于通过地址能找到所需的变量单元,所以我们说该地址指向了该变量单元。将地址形象化的称为“指针”
  • 内存单元的指针(地址)和内存单元的内容是两个不同的概念。

 

三、指针变量 

变量的访问方式

1、变量名

2、地址

#include <stdio.h>

int main()
{
	int a = 10;
	int *p;
	p = &a;
	
	printf("变量名访问a:%d\n",a);
	printf("地址访问a:%d\n",*(&a));//取值运算符,他把后面跟的内存地址中的数据”取出来“
	printf("指针变量的方式访问a:%d\n",*p);
	return 0;
}

运行结果:

变量名访问a:10
地址访问a:10
指针变量的方式访问a:10

指针变量的概念 

在C语言中,允许用一个变量来存放其它变量的地址, 这种专门用于存储其它变量地址的变量, 我们称之为指针变量。 

正如char型变量存放字符,int型变量存放整型数一样,指针变量存放的是地址,没有什么难理解的。 

指针变量的定义

指针变量的定义包括两个内容:

  • 指针类型说明,即定义变量为一个指针变量;
  • 指针变量名;

示例: 

char *p; // 一个用于指向字符型变量的指针

int *q; // 一个用于指向整型变量的指针
  • 类型说明符表示本指针变量所指向的变量的数据类型
  • 其中,*表示这是一个指针变量
  • 变量名即为定义的指针变量名

指针变量的初始化方法

指针变量初始化的方法有两种:定义的同时进行初始化先定义后初始化

  • 定义的同时进行初始化
int a = 5;
int *p = &a;
  • 先定义后初始化
int a = 5;
int *p;
p=&a;
  • 把指针初始化为NULL(c语言null的意思和0的值是一样的,用于指针和对象。)
int *p=NULL;

不合法的初始化:

  • 指针变量只能存储地址, 不能存储其它类型
int *p;
p =  250; // 错误写法
  • 给指针变量赋值时,指针变量前不能再加“*”
int *p;
*p=&a; //错误写法

注意点: 

  • 多个指针变量可以指向同一个地址

  • 指针的指向是可以改变的
int a = 5;
int b = 10;

int *p = &a;
p = &b; // 修改指针指向
  • 指针没有初始化里面是一个垃圾值,这时候我们这是一个野指针

        野指针可能会导致程序崩溃

        野指针访问你不该访问数据

        所以指针必须初始化才可以访问其所指向存储区域

 

四、访问指针所指向的存储空间 

&的意思 

  • C语言中提供了地址运算符&来表示变量的地址。其一般形式为:
    • &变量名;
    • &取地址,取变量名所代表的变量的内存地址
#include <stdio.h>

int main()
{
	int juHuaTai = 10;
    int meiGuiTai = 9;
    
    printf("ju=%d\n",juHuaTai);
    printf("mei=%d\n",meiGuiTai);
    
    printf("ju的地址是:%p\n",&juHuaTai);
    //&取地址,取变量名所代表的变量的内存地址
    printf("mei的地址是:%p\n",&meiGuiTai);

	return 0;
}

运行结果:

ju=10
mei=9
ju的地址是:0060FEFC
mei的地址是:0060FEF8

*指针中的两种意思 

  • C语言中提供了*来定义指针变量访问指针变量指向的内存存储空间
    • 在定义变量的时候 * 是一个类型说明符,说明定义的这个变量是一个指针变量
int *p=NULL; // 定义指针变量
  • 在不是定义变量的时候 *是一个操作符,代表访问指针所指向存储空间

*运算符的功能是:取出内存地址中数据的值(取内容)

int a = 5;
int *p = &a;
printf("通过地址来获取a=%d", *p);   // 访问指针变量

结果: 

通过地址来获取a=5

五、 指针也可以作为数组传入的形式参数

数组作为子函数的形式参数小节中,子函数的形式参数我们用 int array[ ]来定义。

学了指针之后,我们也可以用 int *array来定义.这是因为前者的本质上传入的是数组的首地址,而指针也一样,需要传入数组的首地址。

如下:

#include <stdio.h>

int arraySum(int *array,int num)//数组形参,仅仅传递数组的首地址,代表不了个数。
{
        int sum;
        int i;
        for(i=0;i<num;i++){
                sum+=array[i];//指针当作数组名,下标法访问
        }
        return sum;

}

int main()
{
        int sum;
        int array[5]={0,1,2,3,4};

//      sum=arraySum(array,sizeof(array)/sizeof(array[0]));  //传递数组名
        sum=arraySum(&array[0],sizeof(array)/sizeof(array[0]));
                                //或者传递首元素的地址(&)
                                //sizeof里面只能传入数组名                                                  
                    
        printf("sum=%d\n",sum);

        return 0;
}

一个例子是不是看的不够过瘾,好的,再来一个例子

数组初始化,偏历

#include "stdio.h"

/*
name:initArry
params:parry,size
returnValue:none
*/
void initArry(int *parry,int size)
{
	int i;
	for(i=0;i<size;i++)
	{
		printf("请输入第%i个元素的数据:\n",i+1);
		scanf("%d",parry++);
	}	
}

/*
name:printArry
params:parry,size
returnValue:none
*/
void printArry(int *parry,int size)
{
	int i;
	for(i=0;i<size;i++)
	{
		printf("%d ",*parry++);
	}	
}

int main()
{
	int arry[5];
	int size = sizeof(arry)/sizeof(arry[0]);
	
	initArry(arry,size);//实际参数,数组的首地址: 名,首个元素的地址
	printArry(arry,size);

	return 0;
}

内存空间分布

地址传递,在调用函数中改变数组元素的值,主函数的数组元素值也随之改变 

  • 定义一个指针变量指向数组

(数组的地址是连续的,只需知道第一个元素的地址,通过指针增量即可访问到其他元素)

        指向数组首元素的地址

        等于数组名:指向数组起始位置 

#include <stdio.h>

int main()
{
	
	int arr[3] = {1,2,3};
	int *p;

	//p = &arr[0];//数组的首地址就是首个元素的地址
	p = arr;//数组名就是数组的首地址

    printf("首元素的是:%d\n",*p);

    return 0;
}
  • 指针增量和数组的关系

        *(p+1):+1不代表地址+1,而是代表地址偏移了一个类型的字节数 

  

#include <stdio.h>

int main()
{
	
	int arr[3] = {1,2,3};
	int *p;

	//p = &arr[0];//数组的首地址就是首个元素的地址
	p = arr;//数组名就是数组的首地址
	
	for(int i = 0; i<3; i++)
	{
		printf("address:0x%p,%d ",(p+i),*(p+i));
	}
	
	/*
    //+1不代表地址+1,而是代表地址偏移了一个类型的字节数
	printf("0元素是:%d\n",*(p+0));
	printf("1元素是:%d\n",*(p+1));
	printf("2元素是:%d\n",*(p+2));
	*/
	return 0;
}
  • 指针增量,记到回到数组首地址 

#include <stdio.h>

int main()
{
	
	int arr[3] = {1,2,3};
	int *p;

	//p = &arr[0];//数组的首地址就是首个元素的地址
	p = arr;//数组名就是数组的首地址
	
	for(int i = 0; i<3; i++)
	{
		printf("%d ",*p);
		p++;
	}
	
	p = arr;//指针回调,上一次循环 指针偏移了,再次遍历需要指向初始值
	
	for(int i = 0; i<3; i++)
    {
		printf("%d ",*p++);
	}

	return 0;
}
  • 指针和数组名的见怪不怪 

        指针当作数组名,下标法访问 

#include <stdio.h>

int main()
{
	int arr[3] = {1,2,3};
	int *p = arr;
	
	for(int i = 0; i<3; i++){
		printf("%d ",p[i]);
	}
	
	return 0;
}

运行结果: 

1 2 3 

        数组名拿来加

#include <stdio.h>

int main()
{
	int arr[3] = {1,2,3};
	int *p = arr;
	
	for(int i = 0; i<3; i++)
    {
		printf("%d ",*(arr+i));
	}
	
	return 0;
}

运行结果: 

1 2 3 

数组名和指针区别

数组名是个指针常量,不能拿来自加(*arr++) //会报错

只有指针变量才可以自加 

#include <stdio.h>

int main()
{
	int arr[3] = {1,2,3};
	int *p = arr;
	
	
	for(int i = 0; i<3; i++)
    {
		printf("%d ",*arr++);//编译不过,指针常量
	}
	putchar('\n');

	return 0;
}

sizeof的时候 

#include <stdio.h>

int main()
{
	int arr[3] = {1,2,3};
	int *p = arr;
	
	printf("sizeof arr is %d\n",sizeof(arr));//3个元素*4个字节=12个字节
	printf("sizeof arr is %d\n",sizeof(p));//os:操作系统 用 (指针变量)8个字节表示一个地址
	printf("sizeof int is %d\n",sizeof(int));//4个字节
	printf("sizeof pointer is %d\n",sizeof(int*));//os 用8个字节表示一个地址
	printf("sizeof pointer is %d\n",sizeof(char *));//os 用8个字节表示一个地址

    return 0;
}

将数组中的n个元素按逆序存放

 思路:

#include "stdio.h"

void initArray(int *parray,int len)
{
	int i;
	for(i=0;i<len;i++)
	{
		printf("请输入第%d个数的值:",i+1);
		scanf("%d",parray++);
	}
}

void printArray(int *parray,int len)
{
	int i;
	for(i=0;i<len;i++)
	{
		printf("%d ",*parray++);
	}
}

void revangeArray(int *parray,int len)
{
	int i,j;
	int temp;
	
	for(i=0;i<len/2;i++)
	{
		j = len-1-i;
		temp = *(parray+i);
		*(parray+i) = *(parray+j);
		*(parray+j) = temp;
		/*
			temp = parray[i];
			parray[i] = parray[j];
			parray[j] = temp;
		*/
	}
	
}

int main()
{
	int array[5];
	int len = sizeof(array)/sizeof(array[0]);
	
	initArray(array,len);
	printArray(array,len);
	printf("\n");
	revangeArray(array,len);
	printArray(array,len);

	return 0;
}

六、指针和二维数组(更多的是面试使用)

二维数组中老陈特有说法:父子数组
为了研究清楚地址的概念,把二维回归到一维数组 

牢记: 数组名 = 地址

二维数组本质还是数组,不同点是数组元素还是个数组(子数组) 

父数组(行地址) =  a       (a : 父数组名即行地址)

子数组(列地址) =  a[0]、a[1]、a[2]    (a[0]、a[1]、a[2]:子数组名即列地址) 

                              =  *a / *(a+0)、*(a+1)、*(a+2)

这里的a[0]、a[1]、a[2]是列地址(列数组名),即数组的成员是另外一个数组的数组名(首地址)

二维数组中a[0]元素 相当于一维数组a的数组名不是一个数值是一个数组(地址)

首地址 = 数组名 = 首个元素的地址

即 首地址 = a[0](列数组名) = &a[0][0](列数组首个元素的地址)

a[0]代表第0行第0列地址

思考 

a+1偏移:16个字节 4*4

a[0]+1偏移:4个字节 1*4

通过编程来验证一下

a[0]:第0行第0列的地址         --- *(a+0)

a[0]+1:第0行第1列的地址     --- *(a+0)+1

a[1]:第1行第0列的地址         --- *(a+1)

a[1]+1:第1行第1列的地址     --- *(a+1)+1

a[0]+1:第0行第1列的地址     --- *(a+0)+1

也可以说是第0个子数组的第1个元素的地址

而第0个子数组的第1个元素表示方式是a[0][1],不要乱

#include "stdio.h"

int main()
{
	int arr[3][4] = {
   
   {11,22,33,44},{12,13,14,15},{41,51,61,71}};
	
	printf("arr是父地址:%p,偏移1后是%p\n",arr,arr+1);
	printf("arr[0]是子数组地址:%p,偏移1后是%p\n",arr[0],arr[0]+1);
	printf("arr[0]是子数组地址:%p,偏移1后是%p\n",*(arr+0),*(arr+0)+1);//为啥可以这样表示
	//你想想二维数组代表什么,数组里面还有数组,在父数组名取内容后还是数组,数组名又代表地址
	printf("arr[1]是子数组地址:%p,偏移1后是%p\n",arr[1],arr[1]+1);
	printf("arr[1]是子数组地址:%p,偏移1后是%p\n",*(arr+1),*(arr+1)+1);//为啥可以这样表示
	
	return 0;
}

运行结果

arr是父地址:000000000061FDF0,偏移1后是000000000061FE00
arr[0]是子数组地址:000000000061FDF0,偏移1后是000000000061FDF4
arr[0]是子数组地址:000000000061FDF0,偏移1后是000000000061FDF4
arr[1]是子数组地址:000000000061FE00,偏移1后是000000000061FE04
arr[1]是子数组地址:000000000061FE00,偏移1后是000000000061FE04

树立认知 

提示

进阶 

有意思的思考

阿杰在线理解,如果错误,请指出。

首先我们来说说 a+1, a 是二维数组名(行地址 -》 第0行第0列地址),这个不难理解吧,a+1 表示地址往后偏移一个子数组(到下一行)不难理解吧。即a + 1 表示第1行第0列的地址。

然后我们来说说 *(a+1)

(补充二维数组:数组的成员是另外一个数组的首地址;只有这个点你理解了,后边就容易了)

a 是二维数组名(父数组-》行地址),a+1表示整个子数组往后移一个子数组,*(a+1)取内容后,由于是二维数组,数组里面成员还是数组,数组名代表首地址,所以*(a+1)取内容取到的还是一个数组即地址。即 *(a+1) 表示第一行第0列的地址。

不难得出结论:a+1和*(a+1)都是同一个地址值

小总结 

(这个嵌入式工程师的笔试题会考)

#include "stdio.h"

int main()
{
	int arr[3][4] = {
   
   {11,22,33,44},{12,13,14,15},{41,51,61,71}};
	int i;
	int j;
	
	for(i=0;i<3;i++)
	{
		for(j=0;j<4;j++)
		{
			printf("地址:0x%p,data:%d\n",&arr[i][j],arr[i][j]);
			printf("地址:0x%p,data:%d\n",arr[i]+j,*(arr[i]+j));
			printf("地址:0x%p,data:%d\n",*(arr+i)+j,*(*(arr+i)+j));
			printf("===========================\n");
		}		
	}
	
	
	return 0;
}

运行结果 

地址:0x000000000061FDE0,data:11
地址:0x000000000061FDE0,data:11
地址:0x000000000061FDE0,data:11
===========================
地址:0x000000000061FDE4,data:22
地址:0x000000000061FDE4,data:22
地址:0x000000000061FDE4,data:22
===========================
地址:0x000000000061FDE8,data:33
地址:0x000000000061FDE8,data:33
地址:0x000000000061FDE8,data:33
===========================
地址:0x000000000061FDEC,data:44
地址:0x000000000061FDEC,data:44
地址:0x000000000061FDEC,data:44
===========================
地址:0x000000000061FDF0,data:12
地址:0x000000000061FDF0,data:12
地址:0x000000000061FDF0,data:12
===========================
地址:0x000000000061FDF4,data:13
地址:0x000000000061FDF4,data:13
地址:0x000000000061FDF4,data:13
===========================
地址:0x000000000061FDF8,data:14
地址:0x000000000061FDF8,data:14
地址:0x000000000061FDF8,data:14
===========================
地址:0x000000000061FDFC,data:15
地址:0x000000000061FDFC,data:15
地址:0x000000000061FDFC,data:15
===========================
地址:0x000000000061FE00,data:41
地址:0x000000000061FE00,data:41
地址:0x000000000061FE00,data:41
===========================
地址:0x000000000061FE04,data:51
地址:0x000000000061FE04,data:51
地址:0x000000000061FE04,data:51
===========================
地址:0x000000000061FE08,data:61
地址:0x000000000061FE08,data:61
地址:0x000000000061FE08,data:61
===========================
地址:0x000000000061FE0C,data:71
地址:0x000000000061FE0C,data:71
地址:0x000000000061FE0C,data:71
===========================

七、指针分类型

整型指针,字符指针

#include <stdio.h>

int main()
{
    int a = 5;
    char b = 'A';

    int *pa = &a;//存放整型数据的指针叫整型指针
    char *pb = &b;//存放字符型数据的指针叫字符型指针

    printf("int型指针 pa 的地址是%p,指针偏移(++pa)的地址是:%p\n",pa,++pa);
    printf("char型指针 pb 的地址是%p,指针偏移(++pb)的地址是:%p\n",pb,++pb);


    return 0;
}

运行结果:

int型指针 pa 的地址是000000000061FE10,指针偏移(++pa)的地址是:000000000061FE10
char型指针 pb 的地址是000000000061FE0C,指针偏移(++pb)的地是:000000000061FE0C 

咦,有鬼!指针看不出偏移变化

这其实关系到了printf的出栈入栈问题,以后展开讲

#include <stdio.h>

int main()
{
    int a = 5;
    char b = 'A';

    int *pa = &a;//存放整型数的指针叫整型指针
    char *pb = &b;//存放字符型数的指针叫字符型指针

    printf("int 型指针pa的地址是%p\n",pa);
    printf("int 型指针偏移(++pa)后的地址是:%p\n\n",++pa);

    printf("char 型指针pb的地址是%p\n",pb);
    printf("char 型指针偏移(++pb)后的地址是:%p\n",++pb);

    return 0;
}

运行结果:可以看到指针类型不同,其每次偏移的地址量也不同。

int 型指针pa的地址是000000000061FE0C
int 型指针偏移(++pa)后的地址是:000000000061FE10

char 型指针pb的地址是000000000061FE0B
char 型指针偏移(++pb)后的地址是:000000000061FE0C

数组指针

顾名思义,就是指向数组的指针。 

我们是这样定义它的:int(* p)[n] = { }; (n为要定义的个数) 

按照优先级运算

  • ()与[ ] 优先级相同,根据结合律,就从左向右运算。
  • ()里是*p,先定义了指针,所以p是个指针,然后后面是[ ],才是数组,即数组指针它指向了含有n个int类型的数组。 

数组指针

如上图所示,假设定义 int (*p2)[5] = { 0, 0, 0, 0, 0}; 那么p2就指向了这个包含5个int类型的数组了。
( !!!注意:指向数组首地址指向数组首元素的地址两码事p2在内存中指向的是这个数组的首地址,是和数组有关联的,而绝不仅仅是指向数组首元素的地址。)

就比如:


int *p1 = b;
int a[5] = {1, 2, 3, 4, 5};
int *p2 = a;

int (*p3)[5] = a;


指针p1和p2就单纯的指向了变量,p1指向了变量b的地址,p2指向的是a数组首元素的地址,而不是指向这个数组,它与整个的数组是无关的。

p3指向的是a数组的首地址,与整个的数组是有关的。

p2和p3在现象最明显的体现是地址的偏移

下面通过一个二维数组的例子来体现。 

数组指针才是真正等同于二维数组名 

#include <stdio.h>

int main()
{
	//++arr 偏移一个行元素
    int arr[3][4] = {
   
   {11,22,33,44},{12,13,15,16},{22,66,77,88}};
	int i,j;
	int *p;//p++

	p = arr; 
	printf("p=%p\n",p);
	printf("++p=%p\n",++p);//++p 偏移一个整形字节
	
	//能不能定义一个指针,让指针偏移的时候,也偏移对应大小的数组?
	//数组指针,定义一个指针,指向一个数组!
	//数组指针才是真正等同于二维数组名

	int (*p2)[4];
	p2 = arr;
	printf("p2=%p\n",p2);
	printf("++p2=%p\n",++p2);//++p2 偏移一个行元素
	
	return 0;
}

p=000000000061FDE0
++p=000000000061FDE4
p2=000000000061FDE0
++p2=000000000061FDF0

编译会有下面这个警告

shuzuzhizhen.c: In function 'main':
shuzuzhizhen.c:10:4: warning: assignment to 'int *' from incompatible pointer type 'int (*)[4]' [-Wincompatible-pointer-types]
  p = arr;

为什么呢?

因为p面向的是单个整型数据的地址

而arr面向的是二维数组行的地址

即p++和arr++是两码事,偏移的量都不一样,所以有警告是正常的

看到这里是不是还是概念模糊, 没关系 我们换个说法

这种方式是定义指针来指向整个数组,定义方式如下

int a[3][4]={
   
   {1,2,3,4},{5,6,7,8},{9,10,11,12}};
int (*p)[4]=a;

 那么这里的p是什么意思呢?如果看不懂(*p)[4],那就把*p用b去替换掉,就成了b[4],这里的b就很明显了,就是一个有4个元素的数组的首地址,那么这里的p的含义也就不难得出了:指向一个有四个元素的数组的首地址,尤其需要注意的是,这里p是指向首地址,并非就是首地址,*p才是首地址,也就是指向第一个元素,因此这里的p也就是指向指针的指针。在int (*p)[4]=a中,p就是指向了第一个含四个元素的数组的首地址,也就是p指向a的首地址也就是a[0],*p指向a[0]的首地址也就是a[0][0],要访问a[0][0],就是*(*p)了;

既然p是指向一个有四个元素的数组首地址的指针,那么p+i呢?*p+i呢?要分析清楚这个问题,我们还是参考b[4],前面说过,这里的p是指向首地址的指针,因此p+i就对应了&b+i,因此p+i就指向了&b+i所在的位置;*p是b的首地址,指向b的第一个元素,*p+i就对应了b+i,因此*p+i就指向了b+i所在的位置

p+i偏移i个b数组大小的地址,也就是指向二维数组的第i行

*p+i指向当前有四个元素的数组的第i个元素

好了,那么我们要是想访问数组中的a[i][j]怎么办呢?前面说过,要访问数组中的元素,必须先得到其地址,对于a[i][j],它是a数组中第i行的子数组中的第j个元素,综合上面分析的,p+i即指向了数组a的第i行,*(p+i)就是该行的首地址,也就是指向了a[i][0],而对于a[i]这个一维数组的第j个元素当然就是a[i]+j,也就是*(p+i)+j了,因此,a[i][j]的地址就是*(p+i)+j,a[i][j]=*(*(p+i)+j)

#include <stdio.h>

int main()
{
	int arr[3][4] = {
   
   {11,22,33,44},{12,13,15,16},{22,66,77,88}};//arr+
	int i,j;
	
	int (*p2)[4];
	p2 = arr;

	for(i=0;i<3;i++){
		for(j=0;j<4;j++){
			//printf("%d\n",*(*(arr+i)+j));
			printf("%d\n",*(*(p2+i)+j));
		}		
	}
}

运行结果

11
22
33
44
12
13
15
16
22
66
77
88

输出二维数组任意行列的数

#include  "stdio.h"

void tipsIputhanglie(int *phang,int *plie)
{
	printf("输入行列值:\n");
	scanf("%d%d",phang,plie);
}

int getData(int (*p)[4],int hang,int lie)
{
	int data;
	
	data =  *(*(p+hang)+lie);
	
	return	data;
}

int main()
{
	int arr[3][4] = {
   
   {11,22,33,44},{12,13,14,15},{16,17,18,19}};
	int ihang,ilie;
	int data;
	
	//1、提示用户输入行列值
	tipsIputhanglie(&ihang,&ilie);
	//2、找出对应行列值的数
	data = getData(arr,ihang,ilie);
	//3、打印出来
	printf("%d行%d列的值是%d\n",ihang,ilie,data);
	
	return 0;
}

运行结果:

输入行列值:
2
1
2行1列的值是17 

补:数组元素首地址和数组首地址

先此前,先了解一下数组名不同用处的含义

1、数组首元素的地址;

2、 sizeof(数组名)- 数组名表示整个数组,sizeof(数组名)计算的是整个数组的大小,单位是字节

3、&数组名,数组名代表整个数组,&数组名取的是整个数组的地址 即 数组首地址

#include  "stdio.h"

int main()
{
	int arr[4] = {0};
	
    //数组元素首地址偏移一个单位偏移的是数组一个数组成员的字节大小
	printf("数组元素首地址 :0x%p\n",arr);	//数组元素首地址  
	printf("数组元素首地址+1 :0x%p\n",arr+1);	//数组元素首地址 		
	
    //数组首地址偏移一个单位偏移的是一整个数组的字节大小
	printf("数组首地址 		:0x%p\n",&arr);	//数组首地址
	printf("数组首地址+1 		:0x%p\n",&arr+1);	//数组首地址 
	
	return 0;
}

运行结果 :

数组元素首地址 :0x000000000061FE10
数组元素首地址+1 :0x000000000061FE14
数组首地址              :0x000000000061FE10
数组首地址+1            :0x000000000061FE20

arr、arr[0]、&arr[0]和&arr区别

arr:数组名,同时也表示数组第一个元素的首字节地址,是一个常量值。

&arr[0]:等价于arr,是一个地址常量,只能作为右值。

arr[0]:数组一个元素,可以进行读写操作。

&arr:数组首地址,其值与arr相等,但含义完全不同。&arr,是将整个数组作为一个新的数组类型,&arr就是这个类型对应内存空间的首地址。

函数指针-----指向函数的指针

前面提过:数组名就是地址。

函数也一样,函数名就是地址

函数具有可赋值给指针的物理内存地址,一个函数的函数名就是一个指针,它指向函数的代码。一个函数的地址是该函数的进入点,也是调用函数的地址函数的调用可以通过函数名,也可以通过指向函数的指针来调用。函数指针还允许将函数作为变元传递给其他函数。 

不带括号和变量列表的函数名,这可以表示函数的地址,正如不带下标的数组名可以表示数组的首地址。

定义形式:

        类型 (*指针变量名)(参数列表);

例如:

        int (*p)(int i,int j);

    p是一个指针,它指向一个函数,该函数有2个整形参数,返回类型为int。p首先和*结合,表明p是一个指针。然后再与()结合,表明它指向的是一个函数。指向函数的指针也称为函数指针

函数调用概念和变量一样

#include "stdio.h"

void printWelcome()
{
	puts("程序启动,欢迎使用\n");
}

int main()
{	
	int a = 10;
	int *pa;//定义
	pa = &a;//赋值
	printf("%d\n",*pa);//调用
	
	void (*p)();//定义一个函数指针变量
	p = printWelcome;//指向函数即赋值
	(*p)();//间接调用
	printWelcome();//直接调用

	return 0;
}

运行结果:

10
程序启动,欢迎使用

程序启动,欢迎使用

直接访问:变量名(函数名)

间接访问: 指针(函数指针)

#include "stdio.h"

void printWelcome()
{
	puts("程序启动,欢迎使用\n");
}

int inData(int data)
{
	return ++data;
}

int main()
{	
	//注意:函数指针变量的型要和调用的函数一样,即返回类型、形参要一样
	void (*p)();//定义一个函数指针变量
	int (*p2)(int data);
	
	p = printWelcome;//赋值 指向函数
	p2 = inData;
	
	(*p)();//调用
	printf("p2测试:%d\n",(*p2)(10));

	return 0;
}

运行结果:

程序启动,欢迎使用

p2测试:11

#include "stdio.h"
#include <stdlib.h>

int getMax(int data1,int data2)
{
	return data1>data2 ? data1:data2;
}

int getMin(int data1,int data2)
{
	return data1<data2 ? data1:data2;
}

int getSum(int data1,int data2)
{
	return data1+data2;
}

int dataHandler(int data1,int data2,int (*pfunc)(int ,int ))
{
	int ret;
	ret = (*pfunc)(data1,data2);//调用
	
	return ret;
}

int main()
{
	int a = 10;
	int b = 20;
	int cmd;
	int ret;
	
	int (*pfunc)(int,int);//定义
	
	printf("请输入1(取大值),2(取小值),或者3(求和)\n");
	scanf("%d",&cmd);
	switch(cmd)
	{
		case 1:
			pfunc = getMax;//赋值
		break;
		case 2:
			pfunc = getMin;
		break;
		case 3:
			pfunc = getSum;
		break;
		default:
			printf("输入错误!");
			exit(-1);//程序异常退出,不加这个会引起段错误
		break;
	}
	ret = dataHandler(a,b,pfunc);
	printf("ret = %d\n",ret);
	
	return 0;
}

int dataHandler(int data1,int data2,int (*pfunc)(int ,int ))
{
    int ret;
    ret = (*pfunc)(data1,data2);//调用
    
    return ret;
}

int (*pfunc)(int,int);//定义

有人读到这里,不知道会不会有疑惑,就是为什么这里的形参和主函数的函数指针变量定义里面的形参,变量名可以省略。

形参强调的是类型,后面的变量名在函数体中没有用到可以省略不写,定义也一样。用到的时候就需要写进来,如data1,data2

指针数组

定义,注意和数组指针的区别,面试的笔试题会考  

一个数组,若其元素均为指针类型数据,称为指针数组,也就是说,指针数组中的每一个元素都存放一个地址,相当于一个指针变量。下面定义一个指针数组:

int *p[4];

由于[]比*优先级高,因此p先与[4]结合,形成p[4]形式,这显然是数组形式,表示p数组有4个元素。然后再与p前面的“*”结合“*”表示此数组是指针类型的,每个数组元素(相当于一个指针变量)都可指向一个整型变量。

注意不要写成
int(*p)[4]; //这是指向一维数组的指针变量 

定义一维指针数组的一般形式为

类型名 * 数组名[数组长度];
类型名中应包括符号“*”,如“int*”表示是指向整型数据的指针类型。

#include "stdio.h"

int main()
{
	int a = 10,b = 20,c = 30,d = 40;
	
	int *p[4] = {&a,&b,&c,&d};
	for(int i=0;i<4;i++)
	{
		printf("%d ",*p[i]);
	}
	
	return 0;
}	

运行结果:

 10 20 30 40

指针数组 是数组,数组的每一项都是一个指针变量

#include "stdio.h"
#include <stdlib.h>

int getMax(int data1,int data2)
{
	return data1>data2 ? data1:data2;
}

int getMin(int data1,int data2)
{
	return data1<data2 ? data1:data2;
}

int getSum(int data1,int data2)
{
	return data1+data2;
}

int main()
{
	int a = 10;
	int b = 20;
	int ret;
	
	int (*pfunc[3])(int,int) = {getMax,getMin,getSum};//函数指针数组
	
	for(int i=0;i<3;i++)
	{
		ret = (*pfunc[i])(a,b);
		printf("ret = %d\n",ret);
	}
	
	
	return 0;
}

运行结果:

ret = 20
ret = 10
ret = 30

定义指针来指向二维数组的某一行,定义方式如下: 

对于有r行的二维数组,就需要定义r个指针指向每一行,这样其实就可以定义一个装有r个指针的数组,其中每一个指针分别指向二维数组的每一行,定义方式如下: 


int a[3][4]={
    
    {1,2,3,4},{5,6,7,8},{9,10,11,12}};
int *p[3];
 
for(int i=0;i<3;i++)p[i]=a[i];

在这种定义方式下,p[i]即指向数组的第i行,也就是a[i][0],知道了a[i][0]的地址,要想访问a[i][j],地址即是a[i][0]+4*j,这里由于p[i]是int型的指针变量,因此刚好就等于p[i]+j,

a[i][j]=*(p[i]+j)。这就是一种定义指向二维数组的指针的方式,这里的p是以指针作为元素的数组,也就是指针数组。

       值得注意的是,这种方法必须知道二维数组的行数

指针函数 

一个函数可以返回一个整型值、字符值、实型值等,也可以返回指针型的数据,即地址。其概念与以前类似,只是返回的值的类型是指针类型而已。 

例如“int * a(int x,int y);"

a是函数名,调用它以后能得到一个int*型(指向整型数据)的指针,即整型数据的地址。x和y是函数a的形参,为整型。

#include "stdio.h"

int *getPosperson(int pos,int (*pstu)[4])//函数指针,返回指针的函数
{
	int *p;
	p = (int *)(pstu+pos);//(int *)强行转换类型
	return p;
}

int main()
{
	int scores[3][4] = {
		{55,66,77,88},
		{66,55,77,88},
		{42,52,63,44}
	};
	int *ppos;
	int pos;
	printf("请输入你需要看的学生号数:0,1,2\n");
	scanf("%d",&pos);
	
	ppos = getPosperson(pos,scores);
	for(int i=0;i<4;i++)
	{
		printf("%d ",*ppos++);
	}
	
	return 0;
}

运行结果

请输入你需要看的学生号数:0,1,2
2
42 52 63 44

有没有人看到这里很迷惑,为什么pstu+pos需要强行转换。

int *getPosperson(int pos,int (*pstu)[4])//函数指针,返回指针的函数
{
    int *p;
    p = (int *)(pstu+pos);//(int *)强行转换类型
    return p;

不转换试试编译,看看结果 -》有警告

zhizhenhaishu.c: In function 'getPosperson':
zhizhenhaishu.c:6:4: warning: assignment to 'int *' from incompatible pointer type 'int (*)[4]' [-Wincompatible-pointer-types]
  p = (pstu+pos);//(int *)强行转换类型 

这是为什么呢,举个例子

  1. int a[3][4]={ {1,2,3,4},{5,6,7,8},{9,10,11,12}};

  2. int *p=a[0];

为什么这里的指针变量p是指向的a[0]呢?而不能让p指向a呢?

       实际上,前面说过,二维数组实际上是一维数组中每个元素都为一个一维数组,那么此时定义了a[3][4],a代表什么呢?一定要知道,不管数组a是几维的,它实际上都是一维的a一定是数组第一个元素的地址,这里的第一个元素是指的将n维数组看做一维数组后的第一个元素。因此,在a[3][4]中,将a看做一维数组后它的每一个元素实际上是每一行,因此如果让指针变量p指向a,就相当于让p指向第0行,而这里的第0行并不是整形变量,因此不能让p指向a

       而如果让p指向a[0],实际上就相当于指向a[0]的第一个元素,也就是a[0][0]了,此时的a[0][0]是整形变量,因此让p指向a[0]是可以的,当然,也可以让p指向a[1]、a[2].....如果让p指向第i行,那么此时的p+j就相当于是一维数组中第j个元素的地址了,即p+j就指向了a[i][j]。

八、二级指针

概念:提到指针,我们都知道指针是用来存储一个变量的地址。所以,当我们定义了一个指向指针的指针的时候(pointer to pointer),我们也称之为二级指针,那针对于这个二级指针来说,第一级指针存放的是指向的变量的地址,第二级指针存放的是第一级指针的地址。可以用下面这张图表示他们之间的关系。

上图所表达的意思也就是,一级指针变量 ptr1 存放的是 var 变量的地址,二级指针变量 ptr2 存放的是一级指针变量的地址。这也就是关于二级指针的相关概念。 

一级指针与二级指针关系示例 

#include <stdio.h>

int main(void)
{
	int a = 10;
	int *p = &a;
	int **q = &p;
	
	printf("a   = %d\n",a);
	printf("&a  = %p\n",&a);
	printf("p   = %p\n",p);
	printf("&p  = %p\n",&p);
	printf("*p  = %d\n",*p);
	printf("q   = %p\n",q);
	printf("&q  = %p\n",&q);
	printf("*q  = %p\n",*q);
	printf("**q = %d\n",**q);
}

运行的结果:
 

结果也很明显了,一级指针变量 p 存放的是变量 a 的地址,二级指针变量 q 存放的是一级指针变量 p 的地址,所以根据以上结果也能得出下面的等式: 

整型变量a,一级指针变量 p,二级指针变量 q

q本身是p的地址;p本身是a的地址;a本身是数据10

q = &p;

*q = p = &a;

**q = *p = a; 

在了解了上述一级指针和二级指针的一个关系之后,我们再来看另外一个例子:
现在有如下代码:

int main(void)
{
	int **ipp;
	int i = 5,j = 6,k = 7;
	int *ip1 = &i,*ip2 = &j;	
}

如果这个时候,我们加了这么一句代码:

ipp = &ip1;

那么上述所涉及到的数据之间的关系是这样的:

变量关系图 

根据上面这个图我们也可以知道,对于 ipp 的两次解引用的结果是 i 的值,也就是说 **ipp = 5 :
我想对于这个的理解并不困难,如果我继续在这个基础上添加代码,注意,是在上条代码的基础上添加如下代码: 

*ipp = ip2;

 在这条代码的作用下,数据关系图就发生了改变,改变如下所示:

对于上述的变化来说,我们增加的代码改变的是 *ipp 的值,也就是说 ipp 的值是不会发生改变的,既然 ipp 的值不会发生改变,那么 ipp 指向 ip1 的关系不会发生改变,我们增加的代码改变了 *ipp 的值,那么也就是说改变了一级指针指向的值,而 ip2 是指向 j 的,所以也就有了上述的变化
紧接着我们继续在第一条增加的代码的基础上重新增加一条代码,增加的代码如下:

*ipp = &k;

那么这个时候所对应的数据关系图如下图所示:

这个原理和刚才的一样,不在这里赘述了。

二级指针的应用 

那再讲述了上述的基本概念之后,我们知道二级指针变量是用于存放一级指针变量的地址的,那么在具体的实际应用中,又在什么地方可以用到二级指针呢?下面来看一个 C 语言函数传址调用的例子。
我们在刚学习指针的时候,都会碰到如下这样一个例子: 

void swap(int *a,int *b)
{
	int temp;
	temp = *a;
	*a = *b;
	*b = temp;
}

之所以在定义函数时,把函数的形参定义为指针,而非如下这样的形式: 

void swap(int a,int b);

是因为C 语言在进行函数调用的时候,是将实参的值复制一份,并将其副本传递到函数调用里,如果形参定义的不是指针,那么在函数内部改变数值,不会对实参本来的值发生改变。而将形参定义成了指针的话,那么传到函数里面的值虽然是实参地址的一个副本,但是地址里存的值发生了改变,也就导致实参本来的值也发生了改变
有了上述分析的基础上,我们知道,如果要在一个函数内改变一个数的值,那么就需要将形参定义为指针。同样的,如果我们要在一个函数内(即调用函数)改变一个指针的值(即主函数中指针的值),我们就需要将形参定义了二级指针,下面来看这样一个例子:
 

#include "stdio.h"

void getPosperson(int pos,int (*pstu)[4],int **ppos)
{
	*ppos = (int *)(pstu+pos);//(int *)强行转换类型
}

int main()
{
	int scores[3][4] = {
		{55,66,77,88},
		{66,55,77,88},
		{42,52,63,44}
	};
	int *ppos;
	int pos;
	printf("请输入你需要看的学生号数:0,1,2\n");
	scanf("%d",&pos);
	
	getPosperson(pos,scores,&ppos);
	for(int i=0;i<4;i++)
	{
		printf("%d ",*ppos++);
	}
	
	return 0;
}

运行结果:

请输入你需要看的学生号数:0,1,2
1
66 55 77 88

结论 

上述就是关于二级指针的相关内容,总体来说,二级指针也是指针,用指针的思想来处理这个问题就好,区别只是在于一级指针是用于存放普通变量的地址的,二级指针是用于存放指针变量的地址的。另外需要注意的就是 C 语言在进行函数调用时,实参的传递采用的是实参值的一份拷贝。如果要在函数内部改变变量的值,就要传入指针,如果要在函数内部改变指针的值,就需要传入二级指针。

二级指针和二维数组的避坑指南 

啥都不说,先上个例子

#include "stdio.h"


int main()
{
	int scores[3][4] = {
		{55,66,77,88},
		{66,55,77,88},
		{42,52,63,44}
	};
	int **p = scores;//会出问题

	printf("scores:%p\n",scores);
	printf("p:%p\n",p);
	printf("*scores:%p\n",*scores);//列地址
	printf("*p:%p\n",*p);//*p是一个野指针,不是我们认为的会变成列地址
	
	
	return 0;
}

运行结果

scores:000000000061FDE0
p:000000000061FDE0
*scores:000000000061FDE0
*p:0000004200000037 

是不是跟你预想的编译结果不一样,是不是认为

printf("*p:%p\n",*p);

会打印一个列地址,但并不然。

原因是什么:二级指针不能简单粗暴指向二维数组,这跟我们认为的二维数组的数组名不过是一个二级指针相违背。

那二维数组到底是什么

二维数组名不是二级指针,而是一个数组指针

如果非要让二级指针指向二维数组,需要通过一个桥梁,就是数组指针。

实际应用开发我们很少用二级指针取指向二维数组,这里只是补充认知 

#include "stdio.h"


int main()
{
	int scores[3][4] = {
		{55,66,77,88},
		{66,55,77,88},
		{42,52,63,44}
	};
	int (*p)[4] = scores;//数组指针
	int **p2 = &p;//能用但是会有警告
	
	**p2 = 100;
	printf("%d\n",scores[0][0]);
	
	return 0;
}

运行结果

100

猜你喜欢

转载自blog.csdn.net/weixin_50546241/article/details/124558982