深入理解C/C++指针

1.指针理解

首先我们看一段程序,如下:

cpp 复制代码
#include <stdio.h>

int main(void)
{
	char ch;
	char* pc = &ch;
	*pc = 'H';
	printf("%c", ch);
	return 0;
}

我们可以看到这是指针最基本的用法,那么我们现在来深入理解一下这段程序,char ch;编译器在执行了这句话后就会在内存中开辟一段内存,而对于该段内存就会有一个地址,这个地址就像是这段内存空间的一个门牌号用来帮助编译器在后面我们要用到该段程序时知道该空间。那么对于char就是类型,通过这个类型编译器可以知道要开辟的空间的大小即1个字节大小。

那么我们现在来思考一下ch是什么呢?ch是一个变量名对吧,但是编译器怎么通过这个变量名就找到了那块空间呢,我们前面讲到地址才是编译器找到内存空间的标识,那么我们来好好理解一些这个变量ch,变量的本质是标识符与内存地址的绑定,它代表了一个可以存储数据的内存空间,在编译时变量名会被编译器自动的与内存地址映射起来,就可以找到对应的空间了。这也就是c/c++这些高级语言的一个表现,变量名给我们使用的人看,但是底层编译会自动转化为地址。

所以接下来的char*pc=&ch,就好理解了,这也是语言的通行,就是开辟了一个char*类型内存大小的空间,pc就是这块空间的一个标识符,然后这块空间存放的就是ch这变量名绑定的那块空间的地址。所有通过pc我们并不能直接找到ch变量名的那块空间,故我们又要引用到了一个操作符*来通过该操作符,让其解引用然后就指向到了ch的那块空间,复制后ch空间下的值也被修改了。

我们可以通过下图来更好的理解:

2.指针类型理解

我们看下面这段程序:

cpp 复制代码
#include <stdio.h>

int main(void)
{
	int num = 512;
	char* p = &num;
	printf("%d", *p);
	return 0;
}

num是int类型,但是我们的指针是char*类型,运行后我们看输出结果如下:

可见所得是0,这是为什么呢?我们知道int类型开辟的空间是4个字节,但是char类型开辟的是1个字节,所以char*类型的指针指向的虽然是num空间,但是却只指向一个字节空间,我们看其中的空间分布图:

所以输出的就是0了。

我们再看下面这段程序:

cpp 复制代码
#include <stdio.h>

int main(void)
{
	int num = 512;
	char* p = &num;
	*p += 1;
	printf("%d", num);
	return 0;
}

输出结果如下:

可见用char*类型的指针来进值得操作时,也只会对它所指向得那块空间有影响。我们可以看看内存分布如下:

所以指针类型其实本质就是指向那块空间的大小。但是要注意的是在C++中上面这段程序会报错因为c++的语法更加严格了。

3.指针运算理解

我们看下面这段程序:

cpp 复制代码
#include <stdio.h>

int main(void)
{
	int arr[4] = { 10,20,30,40 };
	int* p1 = &arr[3];
	int* p2 = &arr[0];
	printf("%d", p1 - p2);
	printf("%d", (unsigned int)p1 - (unsigned int)p2);
	return 0;
}

看结果:

我们可以看到两个运算得到的结果不一样了,首先一个直接对p1和p2指针进行减法运算,得到的是两个指针之间的元素个数,这个元素个数所说的其实就是你指定的指针类型的这样字的空间有几块,比如我的指针类型是int*所以两者相减的其实就是这两个指针间有几块4字节大小空间,我们可以验证一下如下我们将程序修改为下面的程序:

cpp 复制代码
#include <stdio.h>

int main(void)
{
	int arr[4] = { 10,20,30,40 };
	//int* p1 = &arr[3];
	//int* p2 = &arr[0];
	char* p1 = &arr[3];
	char* p2 = &arr[0];
	printf("%d\n", p1 - p2);
	printf("%d\n", (unsigned int)p1 - (unsigned int)p2);
	return 0;
}

结果如下:

也就验证了我上面所说的观点。

接着我们来理解一下(unsigned int)p1 - (unsigned int)p2这句程序,该句程序就是将地址强转为了无符号整形然后进行减法运算,所以得到的就是整形减整形的值。对其的强转其实就是改变了编译器在访问时的访问类型。为什么上面两者的结果会这样子呢,我们来看看其中的汇编会更加清楚如下:

我们注意到其中一句汇编,sar eax,2可以在我们地址相减后得到的eax值,在执行的时地址减法运算时,eax右移了2位即除以了4,而在将地址转化为整数后再相减就没有做此处理,这也说明了其二者值不一样的原因。

4.指针和数组名的关系

我们知道了指针和数组的关系,如可以通过指针来操作数组,数组每个元素取出地址也可以赋值给指针操作,特别的我们知道数组名的地址和数组首元素地址一样,我们可以通通过指针来遍历数组也可以通过数组下标来遍历数组。下面我们来深入理解一下,数组名和指针的关系,看下面这段程序:

cpp 复制代码
#include <stdio.h>

int main(void)
{
	int arr[4] = { 10,20,30,40 };
	int* p1 = arr;
	printf("%d\n", *arr);
	printf("%d\n", *p1);
	return 0;
}

运行后结果如下:

可见两者的输出结果都是10,但是这两者是一样的嘛,我们再看下面这段程序:

cpp 复制代码
#include <stdio.h>

int main(void)
{
	int arr[4] = { 10,20,30,40 };
	int* p1 = arr;
	printf("%d\n", arr);
	printf("%d\n", *p1);
	int a = 100;
	arr = &a;
	p1 = &a;
	return 0;
}

运行后如下:

可见p1指针可以改变指向而arr却不能改变指向。再看下面这段程序:

cpp 复制代码
#include <stdio.h>

int main(void)
{
	int arr[4] = { 10,20,30,40 };
	int* p1 = arr;
	printf("%d\n", sizeof(arr));
	printf("%d\n", sizeof(p1));
	return 0;
}

运行后:

可见数组名和指针有着相似性但是又不是一样的,接下来我们就来深入理解一下这两者。首先看下图:

可见p1的类型为int*但是arr的类型为int4,p1指向的是单个的int但是arr指向的是4个int也就是整个数组,编译器在识别p1时就只会识别到一个int,而在识别arr时识别的时整个数组,这是在早年c语言创建时为了操作数组的高效性所定义的。

5.指针与const的应用

第一种就是我们不加const,我们称为自由指针可以更换指针的指向也可以更改指针指向内容的值。如下:

cpp 复制代码
#include <stdio.h>

int main(void)
{
	int a = 100;
	int arr[4] = { 10,20,30,40 };
	int* p1 = arr;
	p1 = &a;
	*p1 = 10;
	
	return 0;
}

第二种就是在指针类型前面加上const,该指针可以改变指向,但是不能改变指向内容的值。如下:

第三种就是在指针变量前加上const ,该指针不可以改变指针的指向,但是可以改变指向的内容的值如下:

第四种在指针类型和指针变量之前都加上const,该指针既不可以改变指向,也不可以改变指向内容的值,如下:

指针与const的联动常用于在给程序员定义接口时使用,防止实现接口的程序员对不需要进行修改的值不小心进行了修改,导致出现意外错误。

6.函数参数传递二维数组

对于二维数组作为参数进行传递有下面几种常用方法:

第一种:

cpp 复制代码
#include <stdio.h>

void add(int array[][4] , int n)
{
	int sum = 0;
	for (int i = 0; i < n; i++)
	{
		for (int j = 0; j < 4; j++)
		{
			sum += array[i][j];
		}
	}
	printf("sum=%d", sum);
}

int main(void)
{
	int a = 100;
	int arr[2][4] = { 10,20,30,40,
					  50,60,70,80};
	add(arr, 2);
	return 0;
}

这种写法比较明确,可以很清楚的看出传入该函数的参数是一个二维数组。在编译时会将该参数理解为int (*array)4。

第二种:

cpp 复制代码
#include <stdio.h>

void add(int (*array)[4] , int n)
{
	int sum = 0;
	for (int i = 0; i < n; i++)
	{
		for (int j = 0; j < 4; j++)
		{
			sum += array[i][j];
		}
	}
	printf("sum=%d", sum);
}

int main(void)
{
	int a = 100;
	int arr[2][4] = { 10,20,30,40,
					  50,60,70,80};
	add(arr, 2);
	return 0;
}

这种写法也比较明确,而且可以明确的看出该array就是一个二维数组的指针。

第三种:

cpp 复制代码
#include <stdio.h>

void add(int *array, int n , int m)
{
	int sum = 0;
	for (int i = 0; i < n; i++)
	{
		for (int j = 0; j < m; j++)
		{
			sum += array[i*m+j];
		}
	}
	printf("sum=%d", sum);
}

int main(void)
{
	int a = 100;
	int arr[2][4] = { 10,20,30,40,
					  50,60,70,80};
	add(arr, 2,4);
	return 0;
}

当使用第三种方式来对二维数组进行参数传递时,array就是一个指向数组首元素的地址,没有了二维数组的属性,所以不能使用上面两种通过arrayij的形式来访问二维数组了,所以我们可以通过上面的一维数组的访问形式来访问,其实也就是指针形式来访问,以为指针也可以通过数组下标形式来进行访问。

7.函数指针

函数指针是一个指向函数入口地址的指针,通过该指针可以实现对函数的调用。那么该函数指针的声明类型应该如何写呢?如下:<数据类型> (*<函数指针名称>)(<参数说明列表>)。其中数据类型就是函数的返回值类型,函数指针名称就是你相想要给该指针取得名称,参数说明列表就是你函数得形参列表。程序如下:

cpp 复制代码
#include <stdio.h>

int add(int a, int b)
{
	return a + b;
}

int main(void)
{
	int m = 10;
	int n = 20;
	int (*p)(int,int) = NULL;
	p = add;
	printf("%d", (*p)(m, n));
	return 0;
}
相关推荐
乌暮8 分钟前
从 `println` 到生产级日志:Spring Boot 日志体系完全指南
java·开发语言·jvm·spring boot·后端·学习·面试
碧海银沙音频科技研究院15 分钟前
面向长尾音频分类的知识蒸馏研究
人工智能·算法·音视频
脉动数据行情20 分钟前
Java OkHttp 对接国际期货批量行情 SpringBoot 定时落库实践
java·spring boot·okhttp
星夜离尘5622 分钟前
MATLAB 定点量化误差实验 半个 LSB 的界限为什么挡不住削顶失真
开发语言·matlab
巴巴媛66623 分钟前
【Debug1】程序死机,while(1)循环,怎么办?
c语言·笔记·stm32·嵌入式硬件·bug
TDTC_XQ23 分钟前
模板类的写法
linux·c++
Lxx_Zw24 分钟前
PID 用顺了,然后呢:增强手术与前馈
c语言·stm32·单片机·嵌入式硬件
Wang's Blog27 分钟前
Java 项目实战: 交友项目 - 百度人脸识别:介绍
java·开发语言·交友
障碍的枫子34 分钟前
我的第一个Agent
开发语言·python·langchain
Pniubi44 分钟前
区分agent中长期记忆和偏好记忆
开发语言·ai·智能体