初阶C语言中指针的理解

提示:本文主要讲解了C语言中初阶一级指针和二级指针的使用


前言

  1. 了解并熟练掌握,用指针来访问一块内存空间。
  2. 指针的基本使用,指针+1,-1的运算
  3. 理解指针和数组之间的关系
  4. 二级指针要多画图
  5. 指针数组和数组指针的区分

一、什么是指针?

  1. 指针表示的是内存中一个最小单元的编号,也就是地址。
  2. 平时口语中所说的指针,通常是指针变量,指针变量是用来存放内存地址的变量。
    总结:指针就是地址,是内存中的一个最小单元的编号。

那么我们来探讨计算机中内存是如何展示的?

1.1 计算机中的内存

【问】 什么是内存,什么是地址?计算机中的地址是如何产生的呢?内存的大小是多少?一个地址的大小是多大?
【答】

  • 内存(Memory)是计算机的重要部件,也称内存储器和主存储器,它用于暂时存放CPU中的运算数据,以及与硬盘等外部存储器交换的数据。(来自@百度百科 什么是内存)
  • 地址:是计算机中对内存的编号。一个地址是8个比特位, 占1个字节。
  • 在X86系统, 即32位操作系统中,计算机中共有32根地址线,根据电信号,每根地址线会产生高电平和低电平两种电信号。我们规定高电平为1,低电平为0;那么32跟地址线一共产生2的32次方种电信号,每一个电信号当作一个比特位(1bit),共32位。我们把32个比特位所组成的010101…信号,组成二进制序列,规定这个二进制序列为内存的地址。那么内存中一共有2^32次方个这样的地址(见下图)。地址就是指针,那么一共有2的32次方个指针。
  • 内存的大小:那么计算机最大能管理多大的内存地址呢?在32位操作系统种,共管理4GB的内存(如下图所示)。
  • 地址的大小:在X86系统,32位操作系统中,一个地址是由32个bit位组成的,1个字节等于8个bit位,8bit = 1Byte; 那么32个比特位一共是:32/8=4字节。 所以一个地址的大小是 4 字节。
    在X64系统,即64位的操作系统中, 计算机中一共有64根地址线,有64个bit位,那么64个比特位一共是:64/8 = 8字节。
    总结:指针就是地址,所以在32位操作系统中,地址(指针)的大小是4个字节,在64位操作系统中地址(指针)的大小是8个字节。

在这里插入图片描述

1.2 指针有什么作用?

指针表示的是内存中一块空间的地址, 使用 * 解引用操作符,可以拿到这块空间里的数据,使用指针可以访问并使用这块内存空间的数据。

1.3 指针变量

用来存储地址的变量称为指针变量。C语言中’&‘(取地址操作符),可以取出一个变量的地址,我们可以把地址存放到一个变量中,这个变量就叫做指针变量。

#include <stdio.h>
int main()
{
	int a = 10;
	int* p = &a; //这里的变量p就是指针变量
	//a变量占用4个字节的空间,这里是将a的4个字节的
	//第一个字节的地址存放在变量p中,
	//p就是一个之指针变量。
	*p = 20; //使用p指向空间的数据a, 将20赋值给a
	
	return 0;
}

我们知道了指针变量,那么,指针变量的的类型又是什么呢?

二、指针的类型

同C语言中的基本数据类型(char, short, int, float, double, long)一样,指针也有类型,那么指针的类型都有哪些呢?

char * p1;
short * p2;
int *p3;
float *p4;
double * p5;
long *p6;

我们可以看到,指针的类型是 type+'';
以 int * p3 = &a 为例:
这颗星号表示变量 p3 是一个指针变量,
而 int 表示的是p3指向的数据是int 类型。
p3的类型是int
*。
同理:char * p1;
星号表示p1是一个指针变量,char表示 p1指向的数据是char类型。
p1的类型是 char *。

2.1 指针的大小

在32位操作系统下:
在这里插入图片描述
在64位操作系统下:
在这里插入图片描述

我们可以看到在32位操作系统下,不管是什么类型的指针变量,他们都占4个字节。在64位操作系统下,不管是什么类型的指针变量,他们都占8个字节。为什么呢?

  • 因为指针变量存储的是地址,在32位操作系统下,地址是由32个0或1组合而成的二进制序列,共32位 ,占4个字节。地址占4个字节,那么用来存储地址的变量——指针,也需要4个字节的空间来存储这个地址,所以指针变量的大小为4个字节。
  • 我们知道不管是什么类型的变量,他们的地址都是由32个0或1的bit位组合而成,每个地址都是占4个字节,所以在32位操作系统下指针变量的大小始终是4个字节。
  • 同理,64位操作系统下,地址是由64个0或1的bit位组合而成,每个地址的大小是8个字节,所以指针变量的大小也是8个字节。

总结: 指针变量的大小根指针变量的类型无关,只跟当前所使用的操作系统有关。在X86系统下,指针变量的大小是4个字节,在X64系统下,指针变量的大小是8个字节。

2.3 指针类型产生的作用

指针类型的作用是:指针一步能跳过多少个字节。

  • 如果是int* 类型的指针变量,那个这个给指针变量+1,指针所指向的地址向后跳过4个字节。
  • 如果是char* 类型的指针变量,指针变量+1,指针跳过1个字节。
#include <stdio.h>

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

	char* pa = &a;
	int* pb = &b;

	printf("(char*)pa   = %p\n", pa);
	printf("(char*)pa+1 = %p\n", pa+1);
	printf("(int*)pb    = %p\n", pb);
	printf("(int*)pb+1  = %p\n", pb+1);

	return 0;
}

在这里插入图片描述

可以看到,char* 类型的指针变量+1,跳过1个字节;int* 类型的指针变量+1,跳过4个字节。

三、野指针

3.1 什么是野指针?

  • 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)

3.2 野指针形成的原因

  1. 指针未初始化
  2. 指针的越界访问
  3. 返回局部变量的地址
  4. 动态开辟的空间被释放,free()。
#include <stdio.h>

int* test()
{
	int a = 0;
	int* p = &a; //局部变量指针p指向 局部变量a
	//局部变量创建在栈区,出了作用域立即销毁
	return p; //返回了局部变量a的地址
}

int main()
{
	int* p;
	//指针变量p是野指针, 未初始化,指向的位置是随机值
	
	int* p2 = test();//局部变量的地址虽然记住了,
	//但是局部变量a已经销毁, 此时p2是野指针
	
	int arr[10] = {1,2,3,4,5,6,7,8,9,10};
	p = arr; //指针变量p指向数组首元素,此时p不是野指针
	//数组名表示数组首元素的地址
	int i = 0;
	for(i=0; i<=13; i++)
	{
		printf("%d ", *p);
		p++;//当p大于数组长度10时,指针p为野指针
	}
	
	return 0;
}

注意:野指针非常的危险,我们要尽量避免写出含有野指针的代码。

3.3 如何规避野指针的产生?

  1. 指针初始化
  2. 小心指针越界
  3. 指针指向空间释放即使置NULL
  4. 避免返回局部变量的地址
  5. 指针使用之前检查有效性

四、如何使用指针?

4.1 指针的基本使用

#include <stdio.h>

int main()
{
	int a = 10;
	int* p = &a; //& 取出a的地址,指针p指向变量a

	*p = 20;//使用 * 解引用操作符,拿到p指向的地址中的
	//数据,即变量a。*p <==> a
	//*p=20 等价于 a=20;
	printf("%d\n", *p);//输出:20
	
	return 0;
}

4.1指针的运算

4.1.1 指针加减整数

#include <stdio.h>

int main()
{
	int arr[10] = { 0,1,2,3,4,5,6,7,8,9 };
	int* p = &arr[0];	//定义一个指针变量p, 指针指向数组的第一个元素arr[0]

	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", *p);
		p++; //指针p加1,跳过一个sizeof(int) 大小的空间,指向数组第二个元素arr[1]
	}

	return 0;
}

在这里插入图片描述

4.1.2 指针减指针

  • 指针减指针表示的是两个指针之间的元素个数。
  • 前提:两个指针必须是同一类型,指针必须指向同一块连续的地址。
    • 例如:使用两个指针相减,求字符串的长度。
#include <stdio.h>
#include <string.h>

int main()
{
	char arr[] = "hello";
	//arr中存储的数据:h e l l o \0; 六个元素,大小是6个字节

	char* p = &arr[0]; //p指针指向数组的首元素
	char* q = &arr[0]; //q指针要指向数组最后一个元素\0,但不知道他的地址,
	//所以依次遍历找到\0
	//找到最后一个元素\0
	while (*q != '\0')
	{
		q++;
	}

	printf("%d\n", q-p);//两个指针之间的元素个数,也就是字符串的长度
	printf("%d\n", strlen(arr));

	return 0;
}

在这里插入图片描述

五、指针和数组

5.1 指针和数组的关系

在数组中,[]是访问数组元素的操作符,数组名是一个地址,是数组首元素的地址。指针就是地址,那么数组名和指针能不能交换使用呢?——答案是可以
数组的元素的表达方式有很多种;

arr[i]  <==> p[i] <==> i[p] <==> i[arr]
*(p+i) <==>*(arr+i) <==> arr[i] <==> p[i]

总结:数组元素的表达形式有很多种,要理解掌握[]操作符和*解引用操作符在指针和数组中的应用。

#include <stdio.h>

int main()
{
	int arr[] = { 1, 2, 3 };
	int* p = arr;//arr 是数组名,通常表示数组首元素的地址(&arr[0])
	//p指向数组的首元素

	int i = 0;
	for (i=0; i<3; i++)
	{
		//既然数组名是数组首元素的地址,并且 p 指向 arr,即 p==arr
		//那么可不可以用p来代表数组名呢?——答案是可以
		//printf("%d ", arr[i]);    //输出: 1 2 3 
		//printf("%d ", p[i]);      //输出: 1 2 3
		
		//我们知道[] 是获取数组元素的操作符, 
		//该操作符有两个参数,两个参数可以互换
		//printf("%d ", i[arr]);    //输出: 1 2 3
		//printf("%d ", i[p]);      //输出: 1 2 3
		
		//使用指针操作获取元素的值
		//p+i p是指向数组首元素的地址,p+1 跳过一个4个字节,指向数组的第二个元素
		//printf("%d ", *(p+i));    //输出: 1 2 3
		printf("%d ", *(arr+i));    //输出: 1 2 3
	}

	return 0;
}

5.2 sizeof(数组名)和&数组名

数组名是数组首元素的地址,但有两个特例:

    1. sizeof(数组名) ,数组名单独放在sizeof(), 里面时,计算的是整个数组的大小,而不是数组首元素的大小。
    1. &数组名, 取出的是整个数组的地址,而不是数组首元素的地址。
#include <stdio.h>

int main()
{
	int arr[10] = { 0,1,2,3,4,5,6,7,8,9 };
	int* p = arr; //数组名,代表数组首元素的地址,即 int* p = &arr[0];

	//sizeof(数组名)
	printf("sizeof(arr)    = %zd\n", sizeof(arr));
	printf("sizeof(arr[0]) = %zd\n", sizeof(arr[0]));

	//&数组名
	printf("\n&arr    = %p\n", &arr);
	printf("arr     = %p\n", arr);
	printf("&arr[0] = %p\n", &arr[0]);
	printf("p       = %p\n", p);
	
	printf("\n&arr+1    = %p\n", &arr+1);
	printf("arr+1     = %p\n", arr+1);
	printf("&arr[0]+1 = %p\n", &arr[0]+1);
	printf("p+1       = %p\n", p+1);

	return 0;
}

在这里插入图片描述

看输出结果:

  • 当数组名单独放在sizeof内部时,求出来的大小是40个字节,也就是整个数组的大小。
  • 整个数组的地址&arr,数组名(即数组首元素的地址)的地址arr,数组首元素的地址&arr[0],都是相同的,但表示的意义不同。
  • &arr+1, 表示的是取出整个数组的地址,+1,代表向后跳过一个数组的大小,即跳过sizeof(arr)=40个字节的大小,0x001EFA04 - 0x001EF9DC = 0x00000028= 40 Byte
  • arr+1, arr是数组名表示的是数组首元素的地址,+1, 跳过4个字节,0x001EF9E0-0x001EF9DC = 0x00000004 Byte

总结:sizeof(数组名),&数组名,此时数组名表示整个数组。其余情况数组名均表示数组首元素的地址。

六、二级指针

6.1 二级指针的介绍

二级指针是用来存储一级指针的地址, 二级指针指向一级指针。
用法如下:

#include <stdio.h>

int main()
{
	int a = 10;
	
	int* pa = &a; //pa是一级指针,指向变量a, pa的类型是:int*
	int* * ppa = &pa;//ppa 是二级指针, 指向指针变量pa,ppa的类型是:int* *

	**ppa = 20;
	printf("a = %d\n", a);//输出结果:a = 20

	return 0;
}

二级指针也有类型,int* *ppa = &a; 这里的ppa 是一个二级指针变量, 靠近ppa的第一颗星表示ppa是一个指针变量, int *表示ppa指向的数据的类型是int *类型。

二级指针的图解:
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

6.2 指针数组

我们知道,数组是一组相同数据类型的数据集合
存储字符的数组叫字符数组
存储整形的数组叫整形数组
存储浮点型的数组叫浮点型数组
那么存储指针的数组是什么数组呢?——指针数组
顾名思义,指针数组就是用来存放指针变量的数组。
数组中的每一个元素都是一个指针。
他的表示方式为:

int* arr[] = {p1, p2, p3};

其中,p1, p2, p3是指针变量,arr是数组名, int* 是数组的数据类型, 代表数组中存储的每一个元素的类型都是int* 类型。
总结:指针数组的本质是数组。指针数组,就是用来存放指针变量的数组,数组中每一个元素都是一个指针变量。

#include <stdio.h>

int main()
{
	int a = 1;
	int b = 2;
	int c = 3;
	int d = 4;
	//指针变量
	int* p1 = &a;
	int* p2 = &b;
	int* p3 = &c;
	int* p4 = &d;
	//指针数组--本质是数组,数组里面的每个元素都是指针
	int* arr[4] = { p1, p2, p3, p4 };

	int i = 0;
	for (i = 0; i < 4; i++)
	{
		printf("%d ", *arr[i]); 
		//数组中的每个元素
		//arr[1] == p1;
		//arr[2] == p2;
		//arr[3] == p3;
		//arr[4] == p4;
		//每个元素都是一个指针,对该指针解引用,得到,指针指向的内存空间的数据。
	}

	return 0;
}

6.3 数组指针

指向字符的指针叫字符指针,
指向整型的指针叫整型指针,
指向浮点型的指针叫浮点型指针,
那么指向数组的指针叫什么呢?——数组指针。
表达形式:

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

p是一个指向由3位元素构成的数组的指针。
数组指针的数据类型是:int ()[N], p是指针变量名, [N]是指针的指向是一个数组,数组有N个元素,( *)表示p是一个指针,int 表示p指向数组的元素类型是int类型。

总结:数组指针的本质是指针。这个指针指向一个数组。

#include <stdio.h>

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

	int (*p)[3] = arr;//数组指针p指向数组arr
	
	int i = 0;
	for(i=0; i<3; i++)
	{
		printf("%d ", (*p)[i]);
		//*p,对指针解引用,得到的是数组arr
		//*p 等价于 arr
		//(*p)[i] == arr[i]
		//也可以这样写
		//printf("%d ", *((*p)+i));
	}
	
	return 0;
}

总结

Ending: 理解并熟练掌握什么是指针?指针是如何指向内存的?要注意避免野指针所带来的危害。理解,数组名是数组的首地址, 但有两个特例,这个要熟记。理解二级指针的指向,解引用之后又代表什么?指针数组和数组指针容易混淆,多画图,多理解指针在内存中的指向,这样就会加深对指针的理解。

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值