【C语言】深入浅出理解指针及内存与指针的关系(详细讲解+代码展示)

目录

概述

内存

内存含义

内存作用:

物理存储器和存储地址空间

物理存储器:实际存在的具体存储器芯片。

存储地址空间:对存储器编码的范围。

内存地址

 指针和指针变量

指针基础知识

指针变量的定义和使用

声明一个指针

初始化一个指针

指针大小

野指针和空指针

野指针

空指针 

万能指针void

 const修饰的指针变量

第一种 指向常量的指针

第二种 指针常量

指针和数组

数组名

指针操作数组元素

指针加减运算

指针数组

多级指针

指针和函数

函数形参改变实参的值

数组名做函数参数

指针做为函数的返回值

指针和字符串

字符指针

const修饰的指针变量

指针数组做为main函数的形参

总结


 

概述

指针,是C语言中的一个重要概念及其特点,也是掌握C语言比较困难的部分。指针也就是内存地址,指针变量是用来存放内存地址的变量,在同一CPU构架下,不同类型的指针变量所占用的存储单元长度是相同的,而存放数据的变量因数据的类型不同,所占用的储存空间长度也不同。有了指针以后,不仅可以对数据本身,也可以对存储数据的变量地址进行操作。

为了了解指针,我们首先需要讲解一下内存的概念。

内存

内存含义

        存储器:在计算机的组成中,用来存储程序和数据,辅助CPU进行运算处理的重要部分。

        内存:内部存贮器,暂存程序/数据——掉电丢失 SRAM、DRAM、DDR、DDR2、DDR3。

        外存:外部存储器,长时间保存程序/数据—掉电不丢ROM、ERRROM、FLASH(NAND、NOR)、硬盘、光盘。

【C语言】深入浅出理解指针及内存与指针的关系(详细讲解+代码展示)_第1张图片

内存是沟通CPU与硬盘的桥梁

内存作用:

  1. 暂存放CPU中的运算数据
  2. 暂存与硬盘等外部存储器交换的数据

【C语言】深入浅出理解指针及内存与指针的关系(详细讲解+代码展示)_第2张图片

物理存储器和存储地址空间

有关内存的两个概念:物理存储器存储地址空间

物理存储器:实际存在的具体存储器芯片。

  1. 主板上装插的内存条
  2. 显示卡上的显示RAM芯片
  3. 各种适配卡上的RAM芯片和ROM芯片

我们所有的数据都存在内存中,对每一个物理存储单元,分配一个单元,我们称之为编码,可以根据分配的号码找到相应的存储单元,也称为寻址 。

存储地址空间:对存储器编码的范围。

  1. 编码:对每个物理存储单元(一个字节)分配一个号码 。
  2. 寻址:可以根据分配的号码找到相应的存储单元,完成数据的读写 。

【C语言】深入浅出理解指针及内存与指针的关系(详细讲解+代码展示)_第3张图片

内存地址

在这里,我们将内存抽象成一个很大很大的一维字符数组。编码就是对内存的每一个字节去分配一个32位或64位的编号(位数与自己的处理器有关)。儿这个内存编号我们称其为内存地址。       内存中的每一个数据都会分配相应的地址,例如:char:占一个字节,分配一个地址 。int:占四个字节分配四个地址等。

【C语言】深入浅出理解指针及内存与指针的关系(详细讲解+代码展示)_第4张图片

 指针和指针变量

 内存区中的每一个字节都是有一个编号的,这个编号就是“地址”。如果我们在程序中定义了一个变量,在对这个程序进行编译或者运行的时候,系统会自动给这个变量分配内存单元,确定其地址。

我们将要学习的指针就是内存单元的编号,而指针变量就是存放地址的一个变量。我们平时通常会把指针变量称作指针,但是指针变量与指针的含义是完全不同的。

【C语言】深入浅出理解指针及内存与指针的关系(详细讲解+代码展示)_第5张图片

 从这张图中就可以看出,b_point是指针,其存放了变量b的内存地址,也就是指针变量。

指针基础知识

上面我们了解完了内存以及内存和指针关系之后,那么接下来我们就来开始指针的学习吧!

指针变量的定义和使用

首先我们不要将指针想象的那么特殊,指针就是是一种数据类型,而指针变量也是一种变量,指针变量只想谁,就把谁的地址赋值给指针变量。我们使用“ * ”操作符操作指针变量指向的内存空间。而我们平时使用指针,主要是因为使用指针往往可以生成更高效、更紧凑的代码。

声明一个指针

上面说到指针就是一个变量所以,指针的声明方式与一般的变量声明方式没太大区别:

int *p ;        // 声明一个 int 类型的指针 p
char *p ;      // 声明一个 char 类型的指针 p
int *arr[10]   // 声明一个指针数组,该数组有10个元素,其中每个元素都是一个指针。
int (*arr)[10] // 声明一个数组指针,该指针指向一个 int 类型的一维数组
int **p;       // 声明一个指针 p ,该指针指向一个 int 类型的指针,也就是我们的二级指针。

初始化一个指针

声明一个指针变量并不会自动分配任何内存。在对指针进行间接访问之前,指针必须进行初始化,或是使他指向现有的内存,或者是给他动态分配内存,否则我们并不知道指针指向哪儿,这将是一个很严重的问题,也是我们后面所提到的野指针。所以我们在定义指针后一定要进行初始化,而初始化操作具体如下

 方法1:使指针指向现有的内存 

int n = 1;
int *p = &n;  // 指针 p 被初始化,指向变量 x ,其中取地址符 & 用于产生操作数内存地址

方法2:动态分配内存给指针 

int *p ;
p = (int *)malloc(sizeof(int) * 10) ;    // malloc 函数用于动态分配内存
free(p) ;    // free 函数用于释放一块已经分配的内存,常与 malloc 函数一起使用。

 总结:

#include 

int main()
{
	int a = 0;
	char b = 100;
	printf("%p, %p\n", &a, &b); //输出a, b的地址

	//int * 代表是一种数据类型,int*指针类型,p才是变量名
	int *p;
	p = &a;//将a的地址赋值给变量p,p也是一个变量,值是一个内存地址编号
	printf("%d\n", *p);//p指向了a的地址,*p就是a的值

	char *p1 = &b;
	printf("%c\n", *p1);//*p1指向了b的地址,*p1就是b的值

	return 0;
}

附: &可以取得一个变量在内存中的地址。但是不能取寄存器变量,因为寄存器变量不在内存里,而在CPU里面,所以是没有地址的。

所以这里将上面所讲的部分总结为代码,也希望大家都可以去看懂并理解这些内容。

#include 

int main()
{
	int a = 0 ;
	int b = 11 ;
	int *p = &a ;

	*p = 100 ;
	printf("a = %d, *p = %d\n", a, *p) ;

	p = &b;
	*p = 22;
	printf("b = %d, *p = %d\n", b, *p) ;
}

 运行结果:

【C语言】深入浅出理解指针及内存与指针的关系(详细讲解+代码展示)_第6张图片

指针大小

测量指针大小时我们需要使用sizeof()。

sizeof()是一个关键字,它是一个编译时运算符,用于判断变量或数据类型的字节大小。

sizeof 运算符可用于获取类、结构、共用体和其他用户自定义数据类型的大小。其使用语法如下:

sizeof (data type)

其中,data type 是要计算大小的数据类型,其实包括类、结构、共用体以及其他用户自定义数据类型。 

并且sizeof()测的是指针变量指向存储地址的大小,在32位平台,所有的指针(地址)都是32位(4字节),同理在64位平台,所有的指针(地址)都是64位(8字节) 。

#include 

int main()
{
	int *p1;
	int **p2;
	char *p3;
	char **p4;
	
	printf("sizeof(p1) = %d\n", sizeof(p1));
	printf("sizeof(p2) = %d\n", sizeof(p2));
	printf("sizeof(p3) = %d\n", sizeof(p3));
	printf("sizeof(p4) = %d\n", sizeof(p4));
	printf("sizeof(double *) = %d\n", sizeof(double *));

}

运行结果: 

【C语言】深入浅出理解指针及内存与指针的关系(详细讲解+代码展示)_第7张图片

 其中我们可以看出,我们在定义了多级指针之后,通过输出发现,其内存大小是相同的,所以内存大小是不受指针级数的影响。

野指针和空指针

野指针

指针变量也是变量,是变量就可以任意赋值,但是我们不要越界即可(即32位为4字节,64位为8字节);不过,任意数值赋值给指针变量是毫无意义的,因为这样的指针就成了野指针,此指针指向的区域是未知(操作系统不允许操作此指针指向的内存区域)。所以,野指针不会直接引发错误,但是操作野指针指向的内存区域才会出问题。

int a = 100 ;
int *p ;

p = a; //把a的值赋值给指针变量p,p为野指针, ok,不会有问题,但没有意义

p = 0x1111111; //给指针变量p赋值,p为野指针, ok,不会有问题,但没有意义

*p = 1000;  //操作野指针指向未知区域,内存出问题,over

空指针 

但是,野指针和有效的指针变量保存的都是数值,为了标志此指针变量没有指向任何变量(空闲可用);所以在C语言中,可以把NULL赋值给此指针,这样也就标志此指针为空指针,没有任何指针。

	int *p = NULL ; //成功定义一个空指针

万能指针void

 void*又被我们成为万能指针,其原因为void *指针可以指向任意变量的内存空间。下面我们来参照代码看看:

void *p = NULL ;	//定义空指针 

	int a = 10 ;
	p = (void *)&a ; //指向变量时,最好转换为void *

	//使用指针变量指向的内存时,转换为int *
	*( (int *)p ) = 11;
	printf("a = %d\n", a);

 const修饰的指针变量

有时候我们希望定义这样一种变量,它的值不能被改变,并且在整个作用域中都保持固定。例如,在游戏中,官方在设定物品掉落率的时候,将其设定的一定是一个定值,并且不希望玩家通过其终端去进行修改,那这个时候就需要定义一种变量,它的值不可以被修改了。为了满足这一要求,可以使用const关键字对变量加以限定。

在这里const修饰指针分为两种情况:

第一种 指向常量的指针

const int * p1 = &a这种情况下等价于int const *p1 = &a;也就是我们对*进行修饰,其指针指向内存区域不能修改指针指向可以变 。

第二种 指针常量

int * const p2 = &a 修饰的是p2,也就是其指针指向不能变,但是指针指向的内存可以修改

所以我们在平时编辑程序时,指针作为函数参数,如果不想修改指针对应内存空间的值,需要使用const修饰指针数据类型。

	int a = 100;
	int b = 1000;

	// 指向常量的指针
	//修饰* 指针指向内存区域不能修改,指针指向可以变
	const int * p1 = &a ; //等价于int const *p1 = &a;
	//*p1 = 111; //错误 指针指向内存区域不能修改
	p1 = &b; //ok 指针指向可以变

	//指针常量
	//修饰p2,指针指向不能变,但指针指向的内存可以修改
	int * const p2 = &a;
	//p2 = &b;  //错误  针指向不能变
	*p2 = 200;  //ok 指针指向的内存可以修改

在上一串代码中,其指针指向内存区域也就是我们通常认识的值不可以被改变,但是指针的指向可以改变,也就是指针可以从指向a变为指向b;反之亦然。 

指针和数组

数组名

通过之前的博客可知数组名字是数组的首元素地址,但它是一个常量不可以被修改。

	int a[] = { 1, 2, 3, 4 };
	printf("%p\n", a) ;
	printf("%p\n", &a[0]) ;

	//a = 10; //err, 数组名只是常量,不能修改

如果数组知识没有掌握的话就建议你先看下这个:http://t.csdn.cn/qL5we

看完之后你就可以很好的掌握数组了。

指针操作数组元素

定义数组时,要给出数组名和数组长度,数组名可以认为是一个指针,它指向数组的第 0 个元素。在C语言中,我们将第 0 个元素的地址称为数组的首地址。因为我们数组名就是数组首地址,所以我们就可以用我们的指针来操作数组元素。

#include 

int  main()
{
	int a[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 } ;
	int i = 0 ;
	int n = sizeof(a) / sizeof(a[0]) ;	//找出数组长度 
	
	for (i = 0; i < n; i++)
	{
		//printf("%d, ", a[i]);	//数组表示 
		printf("%d, ", *(a+i));		//使用指针表示 
	}
	printf("\n");

	int *p = a; //定义一个指针变量保存a的地址
	for (i = 0; i < n; i++)
	{
		p[i] = i ;		//利用指针给数组赋值 
	}

	for (i = 0; i < n; i++)
	{
		printf("%d, ", *(p + i));	//利用指针输出 
	}
	printf("\n");


	return 0;
}

在上面的例子中我们不难看出,我们可以利用指针去操作我们的数组,将数组首地址赋给指针后,就可以用*去查找具体某一位的元素了。

*(a+i)这个表达式,a 是数组名,指向数组的第 0 个元素,表示数组首地址, a+i 指向数组的第 i 个元素,*(a+i) 表示取第 i 个元素的数据,它等价于 a[i]。 

a 本身就是一个指针,可以直接赋值给指针变量 p。a 是数组第 0 个元素的地址,所以int *p = a;也可以写作int *p = &a[0];。也就是说,a、p、&a[0] 这三种写法都是等价的,它们都指向数组第 0 个元素,或者说指向数组的开头。

当然如果一个指针指向了数组,那么我们就称它为数组指针(Array Pointer)。数组指针指向的是数组中的一个具体元素,而不是整个数组,所以数组指针的类型和数组元素的类型有关。

我们可以反过来想,对于p来说,p 并不知道它指向的是一个数组,p 只知道它指向的是一个整数,究竟如何使用 p 取决于程序员如何去进行操作了。

指针加减运算

在这里指针的加减运算并不是指的我们常规意义上的那种(1+1=2)类似于这种整数相加减的运算,在这里我们的计算是指内存的计算,也就是如果是一个int *,+1的结果是增加一个int的大小同理而言如果是一个char *,+1的结果是增加一个char大小。

#include 

int main()
{
	int a;
	int *p = &a;
	printf("%d\n", p);
	p += 2 ;	//移动了2个int
	printf("%d\n", p);

	char b = 0;
	char *p1 = &b;
	printf("%d\n", p1);
	p1 += 2 ;	//移动了2个char
	printf("%d\n", p1);

	return 0;
}

这也是为什么在上面我们使用指针的加减法可以移动数组下标的原因,因为数组每个元素内存大小均相等,所以我们的指针在进行加减时对对应加减其内存大小,这样也就可以对应其元素的移动了。 

指针减法也同样如此,下面给大家看个例子,如果这个例子可以看懂这部分知识你也就明白了。

#include 

int main()
{
	int a[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
	int i = 0;
	int n = sizeof(a) / sizeof(a[0]);

	int *p = a+n-1;
	for (i = 0; i < n; i++)
	{
		printf("%d, ", *p);
		p--;
	}
	printf("\n");

	return 0;
}

指针数组

指针数组,它也是数组,只是这个数组的每个元素都是指针类型。声明一个指针数组的方法如下:

int *p[10];    // 声明一个指针数组,该数组有10个元素,并且每个元素都是一个指向int类型的指针

在上述声明中,由于 [] 的优先级比 * 高,所以 p 先与 [] 结合,成为一个数组 p[];之后由 int * 指明这是一个 int 类型的指针数组,数组中的元素都是 int 类型的指针。数组的第 i 个元素是 *p[i],而 p[i] 是一个指针

所以我们来尝试一下指针数组的使用吧:

#include 

int main()
{
	//指针数组
	int *p[3];
	int a = 1;
	int b = 2;
	int c = 3;
	int i = 0;

	p[0] = &a;    //由于数组元素是指针,所以我们要传入地址
	p[1] = &b;
	p[2] = &c;

	for (i = 0; i < sizeof(p) / sizeof(p[0]); i++ )
	{
		printf("%d, ", *(p[i]));
	}
	printf("\n");
	
	return 0;
}

输出结果:

【C语言】深入浅出理解指针及内存与指针的关系(详细讲解+代码展示)_第8张图片

多级指针

多级指针,听名字大概也可以听得出来,这就是一个套娃指针;也就是就是指针的指针的指针...,实际上也没那么复杂。C语言中允许有多级指针存在,但是我们在实际的程序中一级指针最常用,其次是二级指针,到了三级指针以及三级以上的指针时,我们不会过多的去使用了。

定义一个二级指针

int **q;

我们可以将int**q 分为两部分来看,即为 int* 和 (*q),对于后面 (*q) 中的“*”表示 q 是一个指针变量,而前面的 int* 表示指针变量 q 只能存放 int* 型变量的地址。所有对于二级指针甚至多级指针,我们都可以把它拆成两部分。首先不管是多少级的指针变量,它都是一个指针变量,指针变量就是一个“*”,其余的“*”表示的是这个指针变量只能存放什么类型变量的地址。

就比如我们定义一个三级指针:

int ***p = &q ;

在这里我们按照上面的方法去进行逐步拆分,  p的基类型就是 int** 型。而 q 的基类型是 int* 型,所以 &q 的基类型是 int** 型。所以 r 有三个“*”才能指向 q 的地址。三个“*”表示三级指针,即指针的指针的指针。三级指针需要三个“*”才能指向最终的内存单元。

	int a = 10 ;
	int *p = &a ; //一级指针
	*p = 100 ; //*p就是a

	int **q = &p ;
	//*q就是p
	//**q就是a

	int ***t = &q;
	//*t就是q
	//**t就是p
	//***t就是a

看上面这串代码,指针变量的“基类型”用来指定该指针变量可以指向的变量的类型,即该指针变量只能存放什么类型变量的地址。所以 int*p 表示 p 指向的是 int 型变量,也就是说里面只能放int类型的变量地址。这时的p表示a的地址,而*p等于a ;

好的现在我们向下继续看,到了二级指针这里,在这里为什么我们在存放&p的时候要使用两个**呢?前面我们知道,*p是我们的int类型,p是表示的a的地址,我们在存放p时使用int*类型去存放,那么当我们存放&p的时候,就要使用int**去存放了。

那下面我们同理,由上面可知,存p应该使用int*类型,存&p应该使用int**类型,同时q也等价于&p,那么存q就要使用int**类型,所以题目中存&q就理所当然的要使用int***类型啦。那么我们反过来看,t存的是&q,那么*t就是q了;而**t也就是*q也就p了;***t也是*p也是我们一开始设定的变量a了。

这也就是我们的多级指针了,也是我们在学习C语言时最大的拦路虎之一了,加油,相信你可以的! 

指针和函数

为什么C语言中能够说形参的值改变,不影响实参的值,因为在程序运行的过程中,身为形参的静态局部变量m的值随着程序的运行在一直的改变,但是实参的m值却是一直的没有改变一直是,说明了,形参和实参同名的情况下,改变形参的值实参的值不变。

形参,顾名思义,形式上的参数,在函数定义中出现的参数可以看做是一个占位符,它没有数据,只能等到函数被调用时接收传递进来的数据;

实参,平常定义的变量,函数被调用时给出的参数包含了实实在在的数据,会被函数内部的代码使用 。

函数形参改变实参的值

在我们使用函数的时候,我们可能会向函数中传递数去进行操作,这时我们就要判断一下我们所传入的形参在经过函数运算之后,返回时是否可以成功改变其实参的值。

#include 

void swap1(int x, int y)
{
	int tmp;
	tmp = x;
	x = y;
	y = tmp;
	printf("x = %d, y = %d\n", x, y);
}

void swap2(int *x, int *y)
{
	int tmp;
	tmp = *x;
	*x = *y;
	*y = tmp;
}

int main()
{
	int a = 3;
	int b = 5;
	swap1(a, b); //值传递
	printf("a = %d, b = %d\n", a, b);

	a = 3;
	b = 5;
	swap2(&a, &b); //地址传递
	printf("a2 = %d, b2 = %d\n", a, b);

	return 0;
}

在上面的代码中我们提到了值传递和地址传递两个概念;值传递:实参将值传递给形参,形参值发生互换后的值不能回传给主调函数 ;地址传递:传递的是该元素地址或指针的值,而形参接收到的是地址,即指向实参的存储单元,形参和实参占用相同的存储单元,这种传递方式称为“参数的地址传递”。 

在题目中我们也是使用&号去获取该元素的地址,这也是地址传递的一种方式,我们只有使用地址传递时,才可以通过形参去改变实参的值,通过值传递是不行的。例子中的值传递,ab值就不会发生改变。

数组名做函数参数

当我们使用数组名作为函数参数时,这时函数的形参就会退化为指针;这并不难理解,因为我们使用数组作形参的时候,我们需要传入数组首地址,而对于传入的地址,我们才上文也提到了,可以通过指针的方式去使其指向相关的元素,所以这时其形参就会退化为指针了。

#include 

void print_Arrary(int *a, int n)       //退化为指针
{
	int i = 0;
	for (i = 0; i < n; i++)
	{
		printf("%d, ", a[i]);
	}
	printf("\n");
}

int main()
{
	int a[] = { 1, 2, 3, 4, 5 };
	int n = sizeof(a) / sizeof(a[0]);

	//数组名做函数参数
	print_Arrary(a, n); 
	return 0;
}

指针做为函数的返回值

当我们使用指针作为函数的返回值时,这时我们应该与我们在使用指针时去联想,例如:

int *p = &a ;

我们在使用指针的时候需要接受的是元素地址,那么同理可得,当我们使用指针做为函数的返回值时,我们应该用函数返回地址可以。

#include 

int a = 10;

int *getA()    //注意创建函数类型
{
	return &a;    //函数返回地址
}


int main()
{
	*( getA() ) = 110 ;    //使用指针接收
	printf("a = %d\n", a) ;

	return 0;
}

运行结果:

【C语言】深入浅出理解指针及内存与指针的关系(详细讲解+代码展示)_第9张图片

指针和字符串

字符指针

其实对于字符指针已经没有什么需要讲的了,因为我们在使用的时候与上文中提到的数组与指针的使用方法相同,只不过这里的数组时char类型的,我们在输入的时候应该注意字符所需要的单引号,其余我们的操作与指针对数组的操作是相同的。

#include 

int main()
{
	char str[] = "hello world";
	char *p = str;
	*p = 'm';
	p++;
	*p = 'i';
	printf("%s\n", str);

	p = "hello c++";
	printf("%s\n", p);

	char *q = "test";
	printf("%s\n", q);

	return 0;
}

相信通过指针与数组的学习你理解上面的代码并不困难,输出结果如下图: 

【C语言】深入浅出理解指针及内存与指针的关系(详细讲解+代码展示)_第10张图片

const修饰的指针变量

我们在上文中提到过const关键字可以固定其变量,也提到了const修饰指针变量的两种情况,那么这里就用一个例子来带大家巩固一下我们刚才讲解的知识吧。

#include 
#include 
#include 

int main(void)
{
	
	const int a = 10 ; //const修饰一个变量为只读
	
	char buf[] = "wijisjcmsiopjiofjcvs";

	const char *p = buf;
	// 等价于上面 char const *p1 = buf;
	//p[1] = '2'; //错误 
	p = "agdlsjaglkdsajgl"; //正确 

	char * const p2 = buf;
	p2[1] = '3';
	//p2 = "salkjgldsjaglk"; //错误 

	//p3为只读,指向不能变,指向的内存也不能变
	const char * const p3 = buf;

	return 0;
}

在这里我们来总结一下,当我们碰见const修饰指针的时候我们可以首先从左往右看,跳过类型,看修饰哪个字符,如果是*, 说明指针指向的内存不能改变,也就是*修饰什么,什么不能改变。

指针数组做为main函数的形参

main函数是操作系统调用的,它存在两个参数,即第一个参数标明argc数组的成员数量,argv数组的每个成员都是char *类型。其中,argv是命令行参数的字符串数组,而argc代表命令行参数的数量,程序名字本身算一个参数

int main(int argc, char *argv[]);

也就是因为main函数不能被其它函数调用, 不可能在程序内部取得实际值。所以main函数的参数值是从操作系统命令行上获得的。当我们要运行一个可执行文件时,在命令行键入文件名,再输入实际参数即可把这些实参传送到main的形参中去。

总结

好的,今天我们的知识也就分享到这里啦,也感谢大家的支持与关注,在这里也有一些问题向询问一下大家,就发起了一个投票,希望大家都可以参与一下。

好啦,感谢大家支持,如果对我的内容感兴趣的话,还请多多支持一下。

你可能感兴趣的:(C语言,c语言,c++,数据结构,青少年编程,编辑器)