指针太难?手把手教你理解指针(指针?数组?)

目录

前言

一、指针的基本概念

1、内存

2、指针变量的大小

3、指针的类型及其运算

(1)类型

(2)运算

二、指针的进阶 

1.字符指针

2.指针数组

3.数组指针

(1)数组指针的定义

(2)&数组名VS数组名  

(3)数组指针的使用 


前言

 指针是语言基础中比较有难度的部分。但是只要能仔细理解,还是比较简单的。这篇文章,将从入门到进阶,手把手教会你理解指针。废话不多说,我们直入正题。

一、指针的基本概念

1、内存

指针是一个变量,用来存放地址,这个地址唯一标识一块内存空间,即通过一个指针只能找到唯一一个内存。那么想要深入理解指针,就要先理解计算机的内存结构。

内存是电脑上特别重要的存储器,计算机中程序的运行都是在内存中进行的 。内存被划分为一个个小小的内存单元,大小为一个字节。如果将每个内存都进行编号,那么我们就可以通过编号来找到这些内存单元,从而访问这些内存中存储的内容。
不太理解?来看一下下面这张图:

指针太难?手把手教你理解指针(指针?数组?)_第1张图片

 这样每个字节的内存都有其相应的编号。通过右边的编号(十六进制),即可找到左边的内存单元。而指针正是存储右边这些地址(编号)的变量。

现在我们通过一个简单的例子来使用指针,加深理解:

#include
int main()
{
	char c = 'a';//将字符a赋给字符变量c
	char* pc = &c;//定义一个指针pc,取出c的地址赋给pc
	*pc = 'b';//访问pc地址对应的内存单元,把这一内存单元中的内容修改为字符b
	printf("%c\n", c);//打印变量c的内容
	return 0;
}

打印的结果为b。我们发现,通过指针,我们可以越过字符变量,直接修改字符变量所在内存的内容。(把‘a’改成‘b’)

 各位,理解了吗?理解啦!好,我们进入下一概念。

2、指针变量的大小

指针是用来存放地址的,因此,指针变量的大小取决于地址的大小。而地址的大小取决于平台,有32位平台和64位平台等。什么意思呢?简单来说,就是有些电脑一个内存单元编号共由32个比特位(4字节)表示,另一些电脑一个内存单元编号由64个比特位(8字节)表示。所以,指针大小在32位平台是4个字节,在64位平台是8个字节。接下来我们用代码来验证一下。

int main()
{
	printf("%d\n", sizeof(char*));//存放char类型的指针的大小
	printf("%d\n", sizeof(short*));//存放short类型的指针的大小
	printf("%d\n", sizeof(int*));//存放int类型的指针的大小
	printf("%d\n", sizeof(double*));//存放double类型的指针的大小
	return 0;
}

在32位·平台上运行结果如下:

指针太难?手把手教你理解指针(指针?数组?)_第2张图片

在64位平台上运行结果如下:

指针太难?手把手教你理解指针(指针?数组?)_第3张图片

 由此可见,指针的大小只与平台有关,与内存存放的数据类型无关。指针大小在32位平台是4个字节,在64位平台是8个字节

3、指针的类型及其运算

(1)类型

上一个代码,大家已经看到了求不同类型的指针的大小。对,没错,不同类型的指针就是长这样:

char* pa;
int* pb;
short* pc;
double* pd;

只需在类型后面加上一个“*”即可。如何理解呢?我觉得可以这样理解,“*”是解引用操作符,即把后面的变量解引用之后就是前面的类型。比如我们定义一个char类型的变量,可以这样定义:

char a;

与上面的指针pa比较,可以发现,*pa就相当于a。这里我要提醒一下大家,这只是我自己的理解,你也可以有其他理解方式。

(2)运算

指针是可以进行加减和解引用运算的。但是类型会影响指针的这些运算。如何影响呢?我们用几行代码来演示一下:

int main()
{
	char arr[5] = { '1','2','3','4','\0'};//数组名表示首元素地址
	char* pc = arr;//把字符1的地址赋给pc
	printf("%c ", *pc);
	printf("%c ", *(pc + 2));
	return 0;
}

运行结果为1 3。可以发现,当pc存储‘1’的地址时,pc+2就是‘3’的地址,即加2之后指针指向跳过了2个字节,指向了第三个元素。那么,其他指针是不是也是跳过2个字节呢?我们来试试:

int main()
{
	int arr[5] = { 1,2,3,4,5};//数组名表示首元素地址
	int* pc = arr;//把整数1的地址赋给pc
	printf("%d ", *pc);
	printf("%d ", *(pc + 2));
	return 0;
}

运行结果还是1 3。原来如此,指针向后跳过了2*4个字节,指向了第三个元素,也就是两个整形的大小!到这里我们就能明白指针类型的意义了,某个类型的指针在加上整数时,会跳过相应大小的该类型的元素,而且,在指针解引用访问内存时,也访问与类型相应的大小。比如上面的字符指针跳过两个char元素的大小,访问时访问一个char(1字节)的大小的空间。整形指针跳过两个int元素的大小,访问时访问一个int(4字节)大小的空间。由此还可以得出另一个结论,一个指针加上元素个数等于跨过元素个数大小的另一个指针,那么两个指针相减结果也就是元素个数了,但是别忘了正负号哦。

指针太难?手把手教你理解指针(指针?数组?)_第4张图片

二、指针的进阶 

指针的概念理解了,现在我们深入探讨一下指针的使用和高级类型。

1.字符指针

在指针类型中我们知道有一种指针类型为字符指针char*

这种指针有两种使用方法,如下:


int main()
{
	char ch = 'w';
	char* pc = &ch;//赋值运算
	*pc = 'w';//解引用运算
	return 0;
}

这种方法与其他类型的指针一样,都是一般的使用方法。

还有一种使用方法如下:

int main()
{
	char* str = "hello";
	printf("%s\n", str);
	return 0;
}

这里是把一个字符串放到str指针变量里了吗?当然不是。因为一个字符串肯定要占用很多个内存单元,而指针只能指向一个内存单元。其实,这里本质上时把常量字符串“hello”的首字符的地址放进str中,在打印时,从第一个内存单元向后依次读取,最终遇到’\0‘结束,打印结果为hello。

现在,我们用一段代码来深入理解一下这种指针:

#include 
int main()
{
    char str1[] = "hello";
    char str2[] = "hello";
    const char *str3 = "hello";
    const char *str4 = "hello";
    if(str1 ==str2)
 printf("str1 and str2 are same\n");
    else
 printf("str1 and str2 are not same\n");
       
    if(str3 ==str4)
 printf("str3 and str4 are same\n");
    else
 printf("str3 and str4 are not same\n");
       
    return 0;
}

这个代码就是用来判断这几个指针是否相同的。那么运行结果是什么呢?

指针太难?手把手教你理解指针(指针?数组?)_第5张图片

 可以发现,这里str1和str2是不同的,而str3与str4是相同的。这是为什么呢?首先我们要理解一下常量字符串的概念。常量字符串是直接用""引用的字符串,这种字符串被视为常量,不能修改。

那么为什么数组表示的字符串和常量字符串的指针有所不同呢?看完下面这张图,你也许就懂了。

指针太难?手把手教你理解指针(指针?数组?)_第6张图片

 原来,数组创建后,由于数组是变量,创建str1和str2数组即创建两个不同的变量。而常量字符串被创建后,其位置是固定的,创建str3和str4两个指针是两个不同的变量,但是两个指针接收的是同一个常量字符串的首元素h的地址,所以str3和str4虽然是两个变量,但是存储的地址是相同的。可以类比一下整形的赋值。

int a = 5;//将常量5赋给变量a
int b = 5;//将常量5赋给变量b
//a和b相等
char* str1 = "abcde";//将常量字符串abcde的首元素地址赋给str1
char* str2 = "abcde";//将常量字符串abcde的首元素地址赋给str2
//str1和str2相等

这样是不是更好理解了呢?

2.指针数组

指针数组是数组,是用来存放指针的数组。与其他数组类比就很好理解,比如字符数组是“一堆”字符作为数组的内容,整形数组就是“一堆”整形作为数组内容,很明显,指针数组,就是“一堆”指针作为数组的内容。那么不同类型的指针数组是怎么定义的呢?我们来看看下面这些指针数组:

int* arr1[10];//整形指针的数组
char* arr2[4];//一级字符指针的数组
char** arr3[5];//二级字符指针的数组

其他类型的指针数组类似。其实还可以用我之前说过的理解方式,即可以理解为type(*arr)。例如,第一个arr1数组可以理解为,将后面的数组arr1的元素解引用(*arr1【i】),得到的结果就是前面的int类型。

3.数组指针

(1)数组指针的定义

数组指针是指针,还是数组?答案是指针。和前面其他类型指针类似,整形指针是指向整形元素所在空间的指针,字符指针是指向字符空间类型的指针,显然,数组指针就是指向一个数组所存放的空间的指针。数组指针与指针数组在形式上非常类似,需要仔细区分。

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

p1, p2分别是什么呢?

首先要确定一下,下标引用操作符“[]”是比间接访问操作符“*”优先级高的。

如果按照之前我所说的理解方式,可以理解为第一个变量p1与后面的[10]结合,是一个数组,这个数组的元素解引用后的(* p1[i])是一个int类型。也就是说,p1[10]是一个数组,这个数组每个元素是整形指针,指针解引用之后是整形。第二个变量可以理解为,p2解引用后的(*p2)与[10]结合,整体是一个数组,数组的元素是int类型。也就是说,p2是一个指针,这个指针解引用之后的(*p2)是一个数组名,(*p2)[10]是个数组,这个数组有十个元素,每个元素类型是int。

这样,就可以区分p1和p2了。p1是一个数组名,数组元素解引用之后是整形,即每个元素是指针,所以p1是指针数组(存放指针的数组,即数组元素可以解引用)。p2是一个指针,解引用之后是数组名,所以p2是数组指针(指向数组的指针,即指针解引用后是数组名)。

当然,也可以有其他理解方式,比如int*就是一种指针类型,p1[10]就是一个数组。

(2)&数组名VS数组名  

对于下面的数组:
int arr[10];

arr &arr 分别是啥?我们知道arr是数组名,数组名表示数组首元素的地址。 &arr(取地址数组名)是啥? 

我们先来看看arr的地址和&arr的地址有什么区别:

#include 
int main()
{
    int arr[10] = {0};
    printf("%p\n", arr);
    printf("%p\n", &arr);
    return 0;
}

指针太难?手把手教你理解指针(指针?数组?)_第7张图片

 震惊,运行结果居然一毛一样?!那他俩还有啥区别?

现在我们再让它们进行运算试一试:

#include 
int main()
{
 int arr[10] = { 0 };
 printf("arr = %p\n", arr);
 printf("&arr= %p\n", &arr);
 printf("arr+1 = %p\n", arr+1);
 printf("&arr+1= %p\n", &arr+1);
 return 0;
}

指针太难?手把手教你理解指针(指针?数组?)_第8张图片

 哇哦,原来两个东西加上1得到的结果不一样。arr数组名直接加1,跳过了四个字节的地址,也就是跳过了一个数组元素,而&arr取地址数组名加1,直接跳过了40个字节,也就是一整个数组都被跳过啦!

根据上面的代码我们发现,其实 &arr arr ,虽然值是一样的,但是意义应该不一样的。
实际上: &arr 表示的是 数组的地址 ,而不是数组首元素的地址。
本例中 &arr 的类型是: int(*)[10] ,是一种数组指针类型
数组的地址 +1 ,跳过整个数组的大小,所以 &arr+1 相对于 &arr 的差值是 40.
除此之外,数组名也不一定是首元素地址哦!
数组名绝大多数情况下是首元素地址,但有两个例外:
1.sizeof(数组名)。sizeof内部单独放一个数组名的时候,数组名表示整个数组,计算得到数组的总大小。
2.&arr。地址与数组名一样,但意义不一样。

(3)数组指针的使用 

首先我们来简单的使用一下数组指针:

#include 
int main()
{
    int arr[10] = {1,2,3,4,5,6,7,8,9,0};
    int (*p)[10] = &arr;//把数组arr的地址赋值给数组指针变量p
    return 0;
}

这里只是为了演示才写了这样的代码,但实际应用当中我们一般很少这样写代码。那么数组指针到底在那种情况下能用得上呢?我们知道,二维数组的数组名也表示首元素地址,但是这个地址表示的是二维数组的的第一行,也就是一个数组,所以如果在函数中对二维数组进行传址调用时,就不能用普通指针来接收,而必须用数组指针来接收(当然也可以在创建一个形式变参数接收,但是浪费空间)。如下:

#include 
void print_arr1(int arr[3][5], int row, int col)
{
	int i,j;
	for (i = 0; i < row; i++)
	{
		for (j = 0; j < col; j++)
		{
			printf("%d ", arr[i][j]);
		}
		printf("\n");
   }
}
void print_arr2(int(*arr)[5], int row, int col)
{
	int i,j = 0;
	for (i = 0; i < row; i++)
	{
		for (j = 0; j < col; j++)
		{
			printf("%d ", arr[i][j]);
		}
		printf("\n");
	}
}
int main()
{
	int arr[3][5] = { 1,2,3,4,5,6,7,8,9,10 };
	print_arr1(arr, 3, 5);
	//这里传递的arr,其实相当于第一行的地址,是一维数组的地址
	//可以数组指针来接收
	print_arr2(arr, 3, 5);
	return 0;
}

指针太难?手把手教你理解指针(指针?数组?)_第9张图片

 可以看到,两种运行结果是一样的。

学完这些是不是对指针有了更深的理解了呢?让我们看看下面这些代码来回顾一下吧!

int arr[5];
int *parr1[10];
int (*parr2)[10];
int (*parr3[10])[5];

这些东西都是什么意思呢?

第一个很容易,就是一个整形数组

第二个parr1,先与后面[10]结合,是一个数组,每个元素时int*类型,所以是指针数组。

的三个parr2先于*结合,是个指针,解引用之后是数组,数组每个元素是整形,所以是数组指针。

第四个parr3先与[10]结合,是个数组,共有十个元素,每个元素能解引用,所以是个存放指针的数组,但是这些指针解引用之后都是数组名,也就是说这个数组里的元素都是数组指针,所以是数组指针数组(存放数组指针的数组)。可以看下面这张图来理解:

指针太难?手把手教你理解指针(指针?数组?)_第10张图片

 大家理解了吗?

对于指针和数组的传参以及函数指针的问题,我会再发一篇文章解释。所以,文章的结尾,当然是:

To Be Continued...

你可能感兴趣的:(c语言基础知识,c语言)