【C语言步行梯】一级指针、二级指针、指针数组等 | 指针详谈

🎯每日努力一点点,技术进步看得见

🏠专栏介绍:【C语言步行梯】专栏用于介绍C语言相关内容,每篇文章将通过图片+代码片段+网络相关题目的方式编写,欢迎订阅~~

文章目录


什么是指针?

在我们的日常生活中,酒店为了方便找到某个房间,会给每个房间都进行编号。

计算机中,一个4GB的内存包含的字节数多如牛毛,为了方便查找到每个字节所在的位置。我们就需要给内存的每个位置编个号,这个编号就称为内存地址。

为了保存这个编号,我们就需要使用一个变量来存储它,也就是指针变量。这里要注意的是:指针=内存地址=编号。而我们日常说的定义一个"指针",这里的指针指的是指针变量,这里的"指针"并不是内存地址的意思。

在计算机科学中,指针是编程语言中的一个对象,利用地址,它的值直接指向存在电脑存储器中另一个地方的值。由于通过地址能找到所需的变量单元,可以说,地址指向该变量单元。因此,将地址形象化的称为"指针"。意思是通过它能找到以它为地址的内存单元。

例如:我们定义指针变量p,它存储值是0x00000001。这时使用p就能找到0x00000001这个内存单元。

指针变量用于存放内存单元的地址(编号),存放在指针变量中的值都被当成地址处理。下面来看一下如何定义一个指针变量↓↓↓

c 复制代码
#include <stdio.h>
int main()
{
	int a = 10;//在内存中开辟空间
	int* p = &a;//在内存中开辟一个指针变量p,用它保存a的地址。
	return 0;
}

指针的大小

我们的计算机分为32位机器和64位机器。32位机器表示它有32根地址线(每个地址线可以产生0或者1),在为内存编制地址时使用了32个比特位,可以表示0到 2 32 − 1 2^{32}-1 232−1的地址。同理,64位机器表示它有64根地址线,在为内存编制地址时使用了64个比特位,可以表示从0到 2 64 − 1 2^{64}-1 264−1的地址。

每8个比特位是一个字节。在32位的机器上,地址是32个0或者1组成二进制序列,那地址就得用4个字节(32÷8=4)的空间来存储,所以一个指针变量的大小就应该是4个字节 。那如果在64位机器上,如果有64个地址线(64÷8=8),那一个指针变量的大小是8个字节,才能存放一个地址。

总结

指针是用来存放地址的,地址是唯一标示一块地址空间的。

指针的大小在32位平台是4个字节,在64位平台是8个字节。

指针类型

变量有不同的类型,整形,浮点型等。那指针有没有类型呢? 准确的说:有的。

指针的定义方式是: type_name + * 。 其实: char* 类型的指针是为了存放 char 类型变

量的地址。 short* 类型的指针是为了存放 short 类型变量的地址。 int* 类型的指针是为了存放

int 类型变量的地址。

c 复制代码
#include <stdio.h>
int main()
{
	int a = 10;
	int* pa = &a;
	short b = 20;
	short* pb = &b;
	char c = 'a';
	char* pc = &c;
	return 0;
}

那指针类型的意义是什么?

c 复制代码
#include <stdio.h>
int main()
{
	int a = 10;
	int* pa = &a;
	printf("%p\n", pa);
	printf("%p\n", pa + 1);
	char c = 'c';
	char* pc = &c;
	printf("%p\n", pc);
	printf("%p\n", pc + 1);
}

我们知道,int类型占4个字节,char类型占1个字节。对于int类型的指针变量,如果+1后,存储的地址将向后移动4个字节;对于char类型的指针变量,如果+1后,存储的地址将向后移动1个字节。

所以,指针的类型决定了指针向前或者向后走一步有多大(距离)。

指针变量保存了某个变量的地址,那么,怎么使用指针变量间接访问指针变量所指向的变量呢?那就需要使用解引用操作符*(星号)。在指针变量的前面加上解引用操作符,就能访问指针变量所指向的变量。↓↓↓

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

int main()
{
	int a = 10;
	int* pa = &a;
	printf("a = %d\n", *pa);
	*pa = 66;
	printf("a = %d\n", a);//a的值被pa间接修改
}

对于解引用操作符来说,它是根据指针变量的类型来确定一次性需要读取(操作)多少个字节的空间。对于char类型,解引用后需要操作1字节的空间;对于int类型,解引用后需要操作4字节的空间。

总结

指针的类型决定了,对指针解引用的时候有多大的权限(能操作几个字节)。

野指针

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

野指针的几种情况

  1. 指针变量未初始化
c 复制代码
#include <stdio.h>
int main()
{
	int* p;
	printf("%d\n", *p);
	return 0;
}

★ps:指针变量没有初始化时,指针变量中保存的地址是不确定的。这时对该指针变量执行解引用操作可能引起程序崩溃。因为指针变量中的随机地址可能是其他程序所占有的地址,还有可能是其他情况。(等同于:不是我的东西,我没有权力操作)

  1. 指针越界访问
c 复制代码
#include <stdio.h>
int main()
{
	int arr[10] = {1,2,3,4,5,6,7,8,9,10};
	int* p = arr;
	for(int i = 0; i < 100; i++)
	{
		printf("%d\n", *p);
		p++;
	}
	return 0;
}

p一开始指向数组的首地址,解引用后可以访问到第一个元素1,p++后,会向后跳转4个字节,也就是指向第二个元素的地址...一直执行++操作,直到p指向arr范围以外的地址时,由于后序地址不知道属于谁,随便访问并不安全,甚至会导致程序崩溃。

3.指针指向的空间已经被释放

c 复制代码
#include <stdio.h>
#include <stdlib.h>
int main()
{
	int* p = (int*)malloc(sizeof(int));//申请了一个int类型的空间
	free(p);//释放一个int类型的空间
	*p = 0;//error
	return 0;
}

在释放了空间后,操作系统可能会将它分配给其他程序,此时访问并修改它,可能导致程序崩溃。

那如何规避野指针呢?

  1. 指针初始化
c 复制代码
#include <stdio.h>
int main()
{
	int* p = NULL;//指针初始化
	int a;
	p = &a;
	return 0;
}
  1. 小心指针越界
  2. 指针指向空间释放即使置NULL
c 复制代码
#include <stdio.h>
#include <stdlib.h>
int main()
{
	int* p = (int*)malloc(sizeof(int));//申请空间
	free(p);//释放空间
	p = NULL;//置为NULL
}
  1. 指针使用之前检查有效性
c 复制代码
#include <stdio.h>
int main()
{
	int* p = (int*)malloc(sizeof(int));//申请空间有可能失败
	if(p == NULL)
	{
		printf("申请失败\n");
	}
	else
	{
		printf("申请成功可以使用\n");
		*p = 100;
		printf("%d\n", *p);
	}
	return 0;
}

指针运算

指针+(-)整数

例如:对于整型指针p,对它+4。由于整型占4个字节的空间,对p+4,则p向后移动4个整型空间,即16字节。

c 复制代码
#include <stdio.h>
int main()
{
	int arr[10] = {1,2,3,4,5,6,7,8,9,10};
	int* p = arr;
	printf("%p\n", p);
	printf("%p\n", p + 4);
	return 0;
}

例如:对于字符指针c,对它+4。由于字符型占1个字节的空间,对c+4,则c向后移动4个字符型空间,即4字节。

c 复制代码
#include <stdio.h>
int main()
{
	char arr[] = {'a', 'b', 'c', 'd', 'e', 'f'};
	int* c = arr;
	printf("%p\n", c);
	printf("%p\n", c + 4);
	return 0;
}

指针-指针

一个指针变量减另一个指针变量,可以计算出两个指针变量之间的元素个数。

c 复制代码
#include <stdio.h>
int main()
{
	int arr[5] = {1,2,3,4,5};
	int* p = arr;
	int* t = &arr[4];
	printf("%d\n", t - p);//结果是4
	return 0;
}

指针的关系运算

指针变量的比较,就是地址进行大小比较。下面代码中定义了一个包含5个元素的数组,数组是从低地址向高地址存储的。因此,使用下标为0的元素的地址与下标为4的元素的地址进行比较时,p1必然小于p2,故下面代码执行结果为I am Jammingpro

c 复制代码
#include <stdio.h>
int main()
{
	int arr[5] = {1,2,3,4,5};//数组元素从小地址向大地址存储
	int* p1 = &arr[0];
	int* p2 = &arr[4];
	if(p1 < p2)
	{
		printf("I am Jammingpro\n");
	}
	return 0;
}

★ps:允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。

指针和数组

数组名是什么?它是不是和指针有什么关系呢?我们来看一段代码↓↓↓

c 复制代码
#include <stdio.h>
int main()
{
	int arr[3] = {1,2,3};
	printf("arr = %p\n", arr);
	printf("&arr[0] = %p\n", &arr[0]);
	return 0;
}

由上面代码的执行结果可以知道,数组名就是数组首元素的地址。我们可以将数组名理解成指向首元素的指针,但两者还是有区别的(例如:sizeof(arr)的大小是整个数组大大小,而不是指针的大小,说明指针和数组名不等划等号)。

我们可以使用(数组名+数组下标),通过对它解引用,就可以访问数组第i号下标的元素。

c 复制代码
#include <stdio.h>
int main()
{
	int arr[3] = {1,2,3};
	int* p = arr;//存储数组首元素的地址
	for(int i = 0; i < 3; i++)
	{
		printf("arr[%d] = %d, *(p + %d) = %d\n", i, arr[i], i, *(p + i));
		printf("&arr[%d] = %d, (p + %d) = %d\n", i, &arr[i], i, (p + i));
	}
	return 0;
}

★ps:这也就说明了,为什么数组是从下标0开始的。因为arr[i]的本质就是*(arr+i)。

二级指针

上面我们讨论都是用一个指针变量保存一个变量的地址。如果我们用指针变量pc指向字符变量c,再使用指针变量ppc指向指针变量pc的地址。像这种:使用一个指针变量保存另一个指针变量的地址的情形,称为二级指针。

c 复制代码
#include <stdio.h>
int main()
{
	char c = 0;
	char* pc = &c;
	char** ppc = &pc;
	printf("c = %c\n", c);
	printf("*pc = %c\n", *pc);
	printf("**ppc = %c\n", **ppc);
	return 0;
}

上面我们介绍了解引用的操作,对于pc指针来说,对它进行解引用后就可以操作字符变量c。对于ppc指针两说,对它进行一次解引用操作后就可以操作pc指针;如果对它进行两次解引用操作就可以操作字符变量c。

指针数组

听到这个名字,让人有点疑惑。指针数组到底是指针还是数组?其实,指针数组就是一个用来保存指针的数组。

假设我们需要用一个数组保存3个整型数组,由于数组的地址是int*类型的,我们可以定义一个int* arr[3]来存储这3个整型数组的地址。

下面演示如何使用该arr指针数组,访问其保存的各个数组的元素。↓↓↓

c 复制代码
#include <stdio.h>
int main()
{
	int* arr[3] = {0};
	int numArr1[5] = {1,2,3,4,5,6};
	int numArr2[5] = {1,2,3,4,5,6};
	int numArr3[5] = {1,2,3,4,5,6};
	arr[0] = numArr1;
	arr[1] = numArr2;
	arr[2] = numArr3;
	for(int i = 0; i < 3; i++)
	{
		for(int j = 0; j < 5; j++)
		{
			printf("%d ", *((*arr[i]) + j));
		}
		printf("\n");
	}
	return 0;
}

★ps:*arr[i]获取的是第i个数组的地址,*((*arr[i]) + j)获取的是第i个数组第j个元素。

🚩这篇文章结束了~~

如果文章中出现了错误,欢迎私信或留言。(๑•̀ㅂ•́)و✧

有任何疑问请评论或私信哦~~o( ̄▽ ̄)ブ

相关推荐
霁月风23 分钟前
设计模式——适配器模式
c++·适配器模式
萧鼎34 分钟前
Python并发编程库:Asyncio的异步编程实战
开发语言·数据库·python·异步
学地理的小胖砸35 分钟前
【一些关于Python的信息和帮助】
开发语言·python
疯一样的码农35 分钟前
Python 继承、多态、封装、抽象
开发语言·python
^velpro^36 分钟前
数据库连接池的创建
java·开发语言·数据库
秋の花44 分钟前
【JAVA基础】Java集合基础
java·开发语言·windows
jrrz08281 小时前
LeetCode 热题100(七)【链表】(1)
数据结构·c++·算法·leetcode·链表
小松学前端1 小时前
第六章 7.0 LinkList
java·开发语言·网络
可峰科技1 小时前
斗破QT编程入门系列之二:认识Qt:编写一个HelloWorld程序(四星斗师)
开发语言·qt
咖啡里的茶i1 小时前
Vehicle友元Date多态Sedan和Truck
c++