深入理解C语言的指针(一)

深入理解指针

指针学习是C语言中十分重要的一个环节,利用指针,学会了指针的知识后,我们会对系统的内存有更为清晰的认识,也对更为底层的程序,这样就是C语言经久不衰的原因。

地址

了解指针之前我们要了解地址

内存被划分为以字节为单位的空间
每一个字节都有一个编号,这个编号就是这块空间的地址。

地址要被存起来,需要一个空间,这个空间 就是指针变量

注意:

  1. 指针就是一块变量,用来存放地址,地址唯一标识一块内存空间

  2. 指针的大小是固定的4/8字节

  3. 指针是有类型的,指针的类型决定了指针的±步长,指针解引用操作的时候的权限

    一个整形指针+1:跨过四个字节;一个字符型指针+1,跨国一个字节

  4. 指针的运算

硬件:

CPU 32位虚拟地址空间—>32bit的地址

​ 64位虚拟地址空间—>64bit的地址

接下来我们来了解各种指针:

字符指针

常见的字符指针有下面两种情况:

int main()
{
	//情况1:
	char ch = 'a';
	char* cp = &ch;

	//情况2:
	char* cp2 = "abcdef";

	return 0;
}

情况1是我们常见的字符变量与指针指针,我们可以通过对指针的解引用来改变字符变量的值

情况2 比较特殊,字符串被储存在内存中的只读数据区(我们称为常量字符串),无法被更改

在这里插入图片描述

如果我们仍然去更改字符串的数据就会报错:
在这里插入图片描述

为了避免出现这种情况,我们可以在创建指向字符串的指针变量前面加上const去限定该变量指向的内容不能再被修改:

const char *p = "hello";

指向常量字符串的指针与指向字符数组的指针的区别

#include<stdio.h>
int main()
{
	char str1[] = "abcdef";
	char str2[] = "abcdef";

	const char* str3 = "abcdef";
	const char* str4 = "abcdef";

	if (str1 == str2)
	{
		printf("str1==str2n");
	}
	else
	{
		printf("str1!=str2n");
	}

	if (str3 == str4)
	{
		printf("str3 == str4n");
	}
	else
	{
		printf("str3 != str4n");
	}

	return 0;
}

上面这个代码块中创建了两个字符数组,并且两个数组的内容是一样的,然后创建了两个字符指针指向了相同的常量字符串。
然后需要我们判断str1str2是否相等、str3str4是否相等。

在这里插入图片描述

分析:

str1 与 str2是两个数组,数组名代表首元素的地址,数组中存储的内容是相同的,但是这两个数组在内存中的位置是不同的,我们也可以随时更改数组的内容。

str3 与 str4是两个指向常量字符串的地址,常量字符串存储在内存中的只读数据区,常量字符串不能被更改,所以同一个常量字符串只会有一个地址(提高了内存利用率),因此str3 与str4 存储的地址是相同的。

指针数组

指针数组是一个存放指针的数组

#include<stdio.h>
int main()
{
    char *arr[] = {"abcdef","qwer","hello"};
    int i = 0;
    int sz = sizeof(arr)/sizeof(arr[0]);
    for(i = 0; i <sz; i++)
    {
        printf("%sn",arr[i]);
    }
    return 0;
}

数组arr就是一个指针数组,数组的每一个元素都是一个指针,并且在这个arr数组中的每一个指针元素所指向的都是常量字符串。在我们需要访问某一个字符串的时候,我们 只需要放为对应下标的数组。

数组指针

数组指针的定义:

数组指针是数组还是指针?是指针

字符指针-----指向指针的指针

char ch = 'q';
char *cp = &ch;

整形指针-----指向整形的指针

int num = 0;
int *P = &num;

以此类推:

数组指针-----指向数组的指针

这里有两个指针,分别是什么类型?

int *p1[10];
int (*p2)[10];

在这里插入图片描述

“&数组名”和“数组名”

对于下面的数组:

int arr[10];

我们思考一下:
arr&arr到底是什么?
在前面的学习中,我们知道数组名就是数组首元素的地址。

那么取地址数组名又是什么呢?
我们来看这样一段代码:分别打印了数组名的地址,数组首元素的地址,取地址数组名的地址

#include<stdio.h>
int main()
{
	int arr[10] = { 0 };

	printf("arr = %pn", arr);
	printf("arr[0] = %pn", &arr[0]);
	printf("&arr = %pn", &arr);
	return 0;
}

打印出来的结果是相同的:
在这里插入图片描述

我们知道前面两个会打印出来相等的地址(前面我们说过数组名绝大多时候就是首元素的地址),但是我们看到在打印取地址数组名的时候也打印出来了相同的结果?是因为取地址数组名其实就等于数组名吗?结果是否定的。

我们利用下面这个代码段来看出取地址数组名和数组名的区别

#include<stdio.h>
int main()
{
	int arr[10] = { 0 };

	printf(" %pn", arr);
	printf(" %pn", arr+1);


	printf(" %pn", &arr[0]);
	printf(" %pn", &arr[0]+1);

	printf(" %pn", &arr);
	printf(" %pn", &arr+1);
	return 0;
}

在这里插入图片描述

在这里插入图片描述

思考:为什么会跨国40个字节呢?
我们知道这个数组是整形数组,包含十个元素,该数组的大小就是40个字节。所以&arr+1相当于跳过了这一个数组,所以&arr其实是整个数组的地址,我们利用其他的数据类型来解释:

在这里插入图片描述

所以&数组名其实是整个数组的地址

那我们怎么存放数组的地址呢?使用数组指针

int (*p)[10] = &arr;
p的类型是:int(*)[10]:表示一个指向整形数组的指针,该数组有十个元素

根据上面的代码我们发现:

其实取地址数组名和数组名虽然说值是一样的,但是他们的意义是不同的

实际上:&arr表示的数组的地址,而arr表示的是数组首元素的地址

本例中的&arr的类型是int(*)[10],是一种数组指针类型

数组的地址+1,会跳过整个数组的大小,所以&arr+1&arr的差值是40

数组指针的使用

使用数组指针时应该知道的是:解引用数组指针会得到什么?

在这里插入图片描述

同时我们知道arr就是数组首元素的地址,所以我们对数组指针解引用后得到的其实是数组首元素的地址

我们利用这一点来完成一个打印数组的程序

打印一维数组


#include<stdio.h>

void print(int(*p)[10],int n)
{
	int i = 0;
	for (i = 0; i < n; i++)
	{
		printf("%d ", *(*p + i));
	}
}

int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	print(&arr,10);

	return 0;
}

解释为什么printf函数的参数设置为*(*p+i):

前面我们知道:*p 等于 arr---->*p等于数组首元素的地址,(*p+i)表示的数组的第几个元素的地址,再解引用就得到数组中第某个元素的值。

结果:

在这里插入图片描述

打印二维数组

#include<stdio.h>

void print(int(*p)[5],int r,int c)
{
	int i = 0;
	int j = 0;
	for (i = 0; i < r; i++)
	{
		for (j = 0; j < c; j++)
		{
			printf("%d ",*(*(p+i)+j));
		}
		printf("n");
	}
}

int main()
{
	int arr[3][5] = { {1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7} };
	print(arr, 3, 5);

	return 0;
}

在这里插入图片描述

解释为什么printf函数的参数设置为:*(*(p+i)+j):
函数传参的时候传递的参数是arrarr是首行的地址,
首行是一个包含五个元素的数组,所以我们用int(*p)[5] = arr;
此时p等于首行的地址,(p + i)是第i行的地址
*(p+i)表示的时候第i行的首元素的地址)
(*(p+i)+j)表示的是第i行的第j个元素的地址
*(*(p+i)+j))表示的是第i行第j个元素的值

在这里插入图片描述

写代码的时候难免要把数组或者指针传给参数,那么此时函数的参数应该如何设计呢?

一维数组传参

一维数组传参有两种情况:

情况1:实参为普通一维数组—>有三种形参满足传参要求

//1、利用数组来作为形参
void test(int arr[10])
{
}
//2、仍然是一个数组,他和第一个情况的区别是没有指明数组的元素个数,但是这里也是满足条件的。
//原因:当我们利用一个数组作为形参的时候,不管有没有明确形参中数组的元素个数,
//		内存中都不会为了这个形参去创建一个数组。
void test(int arr[])
{
}
//3、利用指针来作为形参
void test(int *arr)
{
}
//其实三种方式本质上是相同的,都是将数组首元素的地址接收到,然后利用首元素的地址去访问其他元素的。

int main()
{
    int arr[10];
    test(arr);
    
    return 0;
}

情况2: 实参为指针数组---->有两种形参满组传参要求

void test2(int*arr[20])
{
}

void test2(int **arr)
{
}

int main()
{
    int *arr[20];
    test2(arr);
    return 0;
}

分析

情况1:直接用一个和实参相同的数组来作为形参,肯定是可以的

情况2:利用了一个二级指针来作为形参,二级指针存放的是指针的地址,恰好arr是一个指针数组(里面的每一个元素都是一个指针),所以可以使用二级指针来作为该函数的形参。

二维数组传参

当二维数组作为一个函数的实参时,形参可以是怎样的?

int main()
{
    int arr[3][5] = {0};
    test(arr);
    return 0;
}

下面有几种形参的情况,判断哪些是可行的。

//1.
void test(int arr[3][5])//可以
{}
//2.
void test(int arr[][])//不可以
{}
//3.
void test(int arr[][5])//可以
{}

注意:二维数组传参,形参如果为数组的话,那么二维数组的列不能省略

//4.
void test(int *arr)//不可以
{}
//5.
void test(int*arr[5])//不可以
{}
//6.
void test(int (*arr)[5])//可以
{}
//7.
void test(int **arr)//不可以
{}

解释:

4.形参是int *arr,该形参是一个指针,指向一个整形数据,但还是我们的实参是一个二维数组,类型不匹配,如果实参是一维数组就满足条件。

5.该形参是一个指针数组,但是实参是二维数组,指针数组是存放指针的,两者类型不匹配。

6.该形参是一个数组指针,指向一个元素个数为5的数组,恰好实参是二维数组,并且第二维数组的个数为5,满足条件。

7.二级指针应该是用来储存一级指针的地址的,但是实参是一个二维数组,数组类型和指针类型不匹配。

一级指针传参

  • 当一级指针为实参时:我们最好采用指针的形式去接收(而不是数组)

  • 当一级指针为形参时,我们的实参可以是什么?

void test(int *p)
{
    
}

int main()
{
    int a = 0;
    int *pa = &a;
    int arr[10];
    
    test(&a);
    test(pa);
    test(arr);
    return 0;
}

由上面的代码段可知:
当形参为一级指针时,我们有三种数据可以作为实参(前提是类型匹配):

  1. 变量的地址
  2. 一级指针
  3. 一维数组名

二级指针传参

  • 二级指针为实参时,用二级指针接收就行

  • 当二级指针为函数的形参时,函数的实参可以是什么?

void test(char **p)
{
    
}
int main()
{
    char ch = 'w';
    char*p = &ch;
    char **pp = &p; 
	char *arr[5];
    
    test(&p);
    test(pp);
    test(arr);
    
    return 0;
}

函数指针

什么是函数指针

函数指针就是指向函数的指针

#include<stdio.h>
int add(int x,int y)
{
	return x + y;
}

int main()
{
	printf("%pn", &add);
	printf("%pn", add);
	return 0;
}

函数也有地址,函数的地址就是内存中储存这个函数的位置
在这里插入图片描述

注意:取地址函数名和函数名都代表了函数的地址,两者没有区别

如果我们想要把函数的地址储存起来,我们就需要函数指针。
下面是函数指针的创建方式:

#include<stdio.h>
int add(int x,int y)
{
	return x + y;
}

int main()
{
	int (*pf)(int,int) = &add;
	return 0;
}

在这里插入图片描述

举一个新的例子:

如果存在这样一个函数:

void test(char*arr);

那么我们创建的函数指针就是这样的:

 void (*pf)(char*)= &test;

怎样使用函数指针

#include<stdio.h>
int add(int x, int y)
{
	return x + y;
}

int main()
{
	int (*pf)(int, int) = &add;
	int ret = (*pf)(3, 5);
	printf("%dn", ret);
	return 0;
}

在这里插入图片描述

思考:由于前面我们说的对于一个函数add来说:&addadd是相等的,所以我们是否可以让函数指针这样来存储函数的地址?

int (*pf)(int,int) = add;

答案是肯定的,不仅如此,我们再用解引用指针来调用函数的时候还可以采取省略*.


#include<stdio.h>
int add(int x, int y)
{
	return x + y;
}

int main()
{
	int (*pf)(int, int) = add;//存储地址的时候没有用&符号
	int ret = pf(3, 5);//利用指针调用函数的时候没有用*符号
	printf("%dn", ret);
	return 0;
}

可以看到结果是相同的:

在这里插入图片描述

得到结论(四者的关系):

对于这样的函数以及函数指针:

int add(int x, int y)int (*pf)(int, int) = add;

存在:

&add = add = *pf = pf

函数指针数组

函数指针是什么

存放函数指针的数组,每一个元素都是一个函数指针

假设存在这样一个函数指针是:

int (*pf)(int, int) = add;

那么如果我们需要用一个数组来存放该指针,我们的数组类型就应该和函数指针的类型是相同的:

int (*pf[10])(int,int);

这就是一个函数指针数组,它的类型是int(*)(int,int),它可以存放十个这种类型的指针。

函数指针的使用

函数指针有什么用处呢?

这里我们举一个计算机的例子:

#include<stdio.h>

int Add(int x, int y)
{
	return x + y;
}

int Sub(int x, int y)
{
	return x - y;
}

int Mul(int x, int y)
{
	return x * y;
}

int Div(int x, int y)
{
	return x / y;
}
void menu()
{
	printf("********************n");
	printf("*** 1.add  2.sub ***n");
	printf("*** 3.mul  4.div ***n");
	printf("*** 0.exit       ***n");
	printf("********************n");
}
int main()
{
	int option = 0;
	int x = 8;
	int y = 4;
	printf("请输入x,y:");
	scanf("%d %d", &x, &y);
	menu();
	printf("请选择:>");
	scanf("%d", &option);
	switch (option)
	{
	case 0:
		printf("退出");
		break;
	case 1:
		printf("%dn",Add(x, y));
		break;
	case 2:
		printf("%dn", Sub(x, y));
		break;
	case 3:
		printf("%dn", Mul(x, y));
		break;
	case 4:
		printf("%dn", Div(x, y));
		break;
	default:
		printf("输入错误");
		break;
	}

	return 0;
}

如果我们利用函数指针数组来实现这一个代码:

#include<stdio.h>

int Add(int x, int y)
{
	return x + y;
}

int Sub(int x, int y)
{
	return x - y;
}

int Mul(int x, int y)
{
	return x * y;
}

int Div(int x, int y)
{
	return x / y;
}
void menu()
{
	printf("********************n");
	printf("*** 1.add  2.sub ***n");
	printf("*** 3.mul  4.div ***n");
	printf("*** 0.exit       ***n");
	printf("********************n");
}
int main()
{
	int (*pfarr[4])(int, int) = { Add,Sub,Mul,Div };
	int option = 0;
	int x = 8;
	int y = 4;
	printf("请输入x,y:");
	scanf("%d %d", &x, &y);
	menu();
	printf("请选择:>");
	scanf("%d", &option);
	if (option == 0)
	{
		printf("退出");
	}
	else if (option > 0 && option < 5)
	{
		printf("%d", pfarr[option](x, y));
	}
	else
	{
		printf("输入错误");
	}
	

	return 0;
}

我们利用一个函数指针数组将多个函数指针存在一个数组中,调用的时候更方便,只需要更改函数指针数组中的下标就可以实现对不同函数的调用(所以函数指针数组也被称为转义表)。

但是应该注意:在利用函数指针数组储存函数指针时,应该就确定所储存的每一个函数指针的类型完全相同(包括返回值和参数)。

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇
下一篇>>