指针相关介绍

一. 什么是指针?

内存

在计算机管理内存时,会把内存划分成好几个内存单元,每个内存单元对应相应的编号(这个编号也可以理解为对应内存单元的地址),在C语言中,地址也称为指针

注:一个内存单元占一个字节

编址

CPU访问内存中的某个字节空间,必须知道这个

字节空间在内存的什么位置,而因为内存中字节

很多,所以需要给内存进行编址

计算机中的编址,并不是把每个字节的地址记录

下来,而是通过硬件设计完成的。

硬件与硬件之间是互相独立的,进行通讯可以用“线”连起来,这里我们只谈论一种“线”—— 地址总线

以VS举例:

地址总线一般只有两种情况:32根(x86环境)[即32位机器]和64根(x64环境)[64位机器]

如:32位机器有32根地址总线,每根线只有两态,表示0,1【电脉冲有无】,那么一根线,就能表式2种含义,2根线就能表示4种含义,依次类推。32根地址线,就能表示2^32种含义,每一种含义都代表一个地址。

二. 指针变量和地址

认识取地址操作符&

在C语言中创建变量其实就是向内存申请空间(详解看函数栈帧的创建与销毁)

&可以得到一个变量的地址


代码举例

#include<stdio.h>
int main()
{
int a = 10;
 &a;//取出a的地址
 printf("%p\n", &a);//打印a的地址
 return 0;
}

运行两次的结果:

(内存是由编译器分配的,每次分配的地址不一定相同)

这里的&a取出的是a所占4个字节中地址较小的字节的地


指针变量和解引用操作符—— *

通过取地址操作符(&)拿到的地址是⼀个数值,比如:0x006FFD70,如果要存放地址,我们需要用到指针变量

指针变量使用

代码举例

#include <stdio.h>
int main()
{
 int a = 10;
 int* pa = &a;//取出a的地址并存储到指针变量pa中
 
 return 0;
}

对指针变量的拆解(还是上述例子)

* 说明pa是指针变量,int 是 pa 所指的对象的类型,int * 是这个指针变量的类型,同时 &a 的类型也是 int 类型

画图解释:


解引用操作符——*

顾名思义,解引用的作用是通过指针变量存放的地址来找到所指对象


代码举例

#include<stdio.h>
int main()
{
 int a = 100;
 int* pa = &a;
 *pa = 0;//这里的*才是解引用操作符
 printf("%d\n",a);
 printf("%d\n",*pa);
 return 0;
}

运行结果:

对上述代码图画和文字分析

由于是通过找到a的地址来修改值的,所以实质上可以理解成*pa = 0变成了 a = 0


指针变量的大小

指针变量的大小只跟地址总线有关

以VS为例:

指针只有4字节(x86环境)或8字节(x64环境)两种情况

x86: 32根地址总线,32个比特位;

x64:64根地址总线,64个比特位


代码举例

#include<stdio.h>
int main()
{
 printf("%zd\n", sizeof(char *));
 printf("%zd\n", sizeof(short *));
 printf("%zd\n", sizeof(int *));
 printf("%zd\n", sizeof(double *));
 return 0;
}

运行结果:


指针变量大小与其类型没有关系

指针变量每次访问空间大小

上面我们说了,指针变量的大小只与地址总线有关,那么定义那么多指针类型有什么用呢?

这里就跟指针变量访问空间有关联


代码举例

试想一下,两种情况有什么不同?

#include<stdio.h>
int main()
{
	int a = 10;
	int *pa = &a;
	*pa = 1001;
	return 0;
}
#include<stdio.h>
int main()
{
	int a = 10;
	char *pa = (char *)&a;//&a类型是int*,需要强制类型转换
	*pa = 1001;
	return 0;
}

文字和画图分析

第一个代码:(实际上a的地址是一样的,但是我运行了两次,才导致不一样)

这是 a 的地址

// 此时a是10(a的数值以16进制显示)

//当运行到这时,很明显,发现值改变了

第二个代码:

//运行到这一步,内存变化如下:

两次结果明显不一样


指针类型可以决定每次访问内存的大小,如:int * 可以一次访问4个字节,char *可以一次访问1个字节

指针+-整数运算


代码举例

#include <stdio.h>
int main()
{
 int n = 10;
 char *pc = (char*)&n;
 int *pi = &n;
 
 printf("%p\n", &n);
 printf("%p\n", pc);
 printf("%p\n", pc+1);
 printf("%p\n", pi);
 printf("%p\n", pi+1);
 return 0;
}

运行结果:

文字,画图分析

pc ,pi 存放 n 的地址,打印地址,三者结果都一样

pc 类型是 char *,每次访问1个字节,地址编号+1

pi 类型是 int *,每次访问4个字节,地址编号+4


void* 指针

在指针类型中有⼀种特殊的类型是 void* 类型的,可以理解为无具体类型的指针(或者叫泛型指针),这种类型的指针可以用来接受任意类型地址,而其它类型指针接受的地址需要相同类型,否则需要发生强制类型转换

但是也有局限性, void* 类型的指针不能直接进行指针的+-整数和解引用的运算


运用情形

#include<stdio.h>
int main()
{
	int a = 10;
	void* pa = &a;
	return 0;
}

如果是指针类型与接受的地址类型不同,会是怎样呢?


代码举例

#include<stdio.h>
int main()
{
	int a = 10;
	char* pa = &a;
	return 0;
}

以VS为例:

编译器会报出这样的警告:


如果void * 类型进行了指针的+-整数和解引用的运算呢?

代码举例

#include<stdio.h>
int main()
{
	int a = 10;
	void * pa = &a;
	pa + 1;
	*pa = 10;
	return 0;
}

以VS为例:

错误在于:

编译器会报出这样的警告:


const 修饰指针变量

const位置放置不同,修饰的对象也就不一样


代码举例

#include<stdio.h>
int main()
{
	int a = 10;
	const int* pa = &a;//const 修饰 *pa,代表不能通过解引用来修改pa所指向对象的内容
	*pa = 11;//错误写法;
	return 0;
}

编译报错:

#include<stdio.h>
int main()
{
	int a = 10;
	int* const  pa = &a;//const 修饰pa,代表pa存放的地址不可以更改
	pa++;//错误写法
	return 0;
}

编译报错:

#include<stdio.h>
int main()
{
	int a = 10;
	const int* const  pa = &a;
	*pa = 11;//写法错误
	pa++;//写法错误
	return 0;
}

编译报错:


综上:

const 在 * 左边,修饰指针变量所指向的对象的内容

const 在 * 右边,修饰指针变量存放的地址

修饰的对象都不可以通过指针变量来改变

指针运算

指针 +- 整数

代码举例

#include <stdio.h>
//指针+- 整数
int main()
{
 int arr[10] = {1,2,3,4,5,6,7,8,9,10};
 int *p = &arr[0];
 int i = 0;
 int sz = sizeof(arr)/sizeof(arr[0]);
 for(i=0; i<sz; i++)
 {
 printf("%d ", *(p+i));//p+i 这⾥就是指针+整数
 }
 return 0;
}

指针和数组很像,它们之间也存在某种转换:

p[i] = *(p + i)


指针 - 指针

代码举例和画图

//指针-指针
#include <stdio.h>
int my_strlen(char *s)
{
 char *p = s;
 while(*p != '\0' )
 p++;
 return p-s;//计算的是两个指针之间的元素个数
}
int main()
{
 printf("%d\n", my_strlen("abc"));
 return 0;
}

画图:

注:两指针相减,算出的可以是负数,真正意义上,相减的绝对值才是元素个数


指针的关系运算

代码举例

#include <stdio.h>
int main()
{
 int arr[10] = {1,2,3,4,5,6,7,8,9,10};
 int *p = &arr[0];
 int i = 0;
 int sz = sizeof(arr)/sizeof(arr[0]);
 while(p < arr+sz) //指针的⼤⼩⽐较
 {
 printf("%d ", *p);
 p++;
 }
 return 0;
}

注:随着数组下标的增大,地址是由低到高


野指针

什么是野指针?

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

造成原因:

  1. 指针未初始化

代码举例

#include <stdio.h>
int main()
{ 
 int *p;//局部变量指针未初始化,默认为随机值
 *p = 20;
 return 0;
}

编译报错:


2. 指针越界访问


代码举例

#include <stdio.h>
int main()
{
 int arr[10] = {0};
 int *p = &arr[0];
 int i = 0;
  for(i=0; i<=11; i++)
  {
 //当指针指向的范围超出数组arr的范围时,p就是野指针
 *(p++) = i;
  }
 return 0;
}

当越界还继续访问内容,编译会报错

3. 指针指向的空间是释放了的


代码举例

#include <stdio.h>
int* test()
{
 int n = 100;
 return &n;//出这个函数,n变量销毁,得到n的地址,如果访问就是强行访问
}
int main()
{
 int*p = test();
 printf("%d\n", *p);
 return 0;
}

如何避免野指针

  • 尽量指针初始化

如果不知道所指向的位置,可以暂时存放NULL

NULL 是C语言中定义的⼀个标识符常量,值是0,0也是地址,这个地址是无法使用的,读写该地址

会报错。


代码举例

#include <stdio.h>
int main()
{
 int*p1 = NULL;
 return 0;
}

  • 小心指针越界

指针越界,一般只能人为去操作,如果越界也不要解引用去访问内容

  • 指针变量不再使用时,及时置NULL,指针使用之前检查有效性

代码举例

int main()
{
 int arr[10] = {1,2,3,4,5,67,7,8,9,10};
 int *p = &arr[0];
 for(i=0; i<10; i++)
 {
 *(p++) = i;
 }
 //此时p已经越界了,p[10]
 p = NULL;//防止p往后访问
 //下次使⽤的时候,判断p不为NULL的时候再使⽤
 //...
 p = &arr[0];//重新让p获得地址
 if(p != NULL) //判断
 {
 //...
 }
 return 0;
}

  • 避免访问局部变量的地址

三. 数组名与指针变量

数组名含义

其实数组名本来就是地址,而且是数组首元素的地址


代码举例

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

运行结果:

只有两种情况下,数组名不是首元素的地址:

  • sizeof(arr)

这里算的是整个arr数组的内存大小

  • &arr

这里得到的是整个arr的数组的地址


代码举例(辨 arr , &arr)

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

运行结果:

画图

这里 + 1 跨越4个字节

(同上)

这里 + 1 跨越一整个数组(即 4 * 10 个字节)


使用指针访问数组

代码举例

#include <stdio.h>
int main()
{
 int arr[10] = {0};
 int i = 0;
 int sz = sizeof(arr)/sizeof(arr[0]);
 int* p = arr;
  for(i=0; i<sz; i++)
  {
  scanf("%d", p+i);
  //scanf("%d", arr+i);//也可以这样写
  }
  for(i=0; i<sz; i++)
  {
  printf("%d ", *(p+i)); 
  // *(p + i) = p[i] = arr[i] = i[arr] = i[p]
  }
     return 0;
 }

这里可以看成: p = arr

*(p + i) = p[i] = arr[i] = i[arr] = i[p] 这种转换的成立的


一维数组与数组


代码举例

void test(int arr[])//参数写成数组形式,本质上还是指针
{
  printf("%d\n", sizeof(arr));
}
void test(int* arr)//参数写成指针形式
{
  printf("%d\n", sizeof(arr));//计算⼀个指针变量的⼤⼩
}
 int main()
 {
  int arr[10] = {1,2,3,4,5,6,7,8,9,10};
  test(arr);
   return 0;
 }

两种形式的参数都可以接受数组的地址

四. 二级指针

指针变量也是变量,是变量就有地址,那指针变量的地址存放在哪里?


代码举例 + 画图

#include<stdio.h>
int main()
{
	int a = 10;
	int* pa = &a;
	int** ppa = &pa;
	return 0;
}

提问:

*ppa **ppa *pa 分别指什么?

*ppa = &a;

**ppa = a;

*pa = a;


五. 指针数组

指针数组 : 存放指针(即地址)的数组


代码举例

#include<stdio.h>
int main()
{
	int i = 0;
	int a = 10;
	int b = 2;
	int* pa[2] = { &a,&b };
	for (i = 0; i < 2; i++)
	{
		printf("%d ", *pa[i]); // pa[i] 拿到的是 类型为 int * 的数组元素下标为 i 的地址
	}
	return 0;
}

运行结果:

画图分析

int * pa[2] :

pa与[] 先结合 :

说明pa是数组,

[2] :

代表 数组有两个元素

int * :

说明每个元素都是int *类型

pa的类型是 : int *[2]


六. 指针数组与二维数组

指针数组可以用来模拟二维数组,但两者还是有区别的:

二维数组的空间是连续的,而指针数组存储的只有每一行的空间是连续的


画图区别

二维数组:

指针数组:


代码模拟指针数组 + 分析

#include<stdio.h>
void Print(int* arr[])
{
	int i = 0;
	int j = 0;
	for (i = 0; i < 3; i++)
	{
		for (j = 0; j < 5; j++)
		{
			printf("%d ", *(arr[i] + j));//arr[i]访问数组的每个元素,得到每一行首元素的地址
		}
		printf("\n");
	}
}
int main()
{
	int arr1[] = { 1,2,3,4,5 };
	int arr2[] = { 6,7,8,9,10 };
	int arr3[] = { 11,12,13,14,15 };
	int* arr[3] = { arr1,arr2,arr3 };
	Print(arr);
	return 0;
}

运行结果:

arr[i] 通过下标找到对应数组的存放的内容(这里存放的是arr1,arr2,arr3),由于拿到的是数组名,即对应的首元素地址,通过 + j ,可以拿到对应的那一行数组元素


七. 数组指针

数组指针是一种指针,里面存放数组的地址


代码举例

#include<stdio.h>
int main()
{
	int arr[] = { 1,2,3,4 };
	int(*pa)[4] = &arr;
	int i = 0;
	for (i = 0; i < 4; i++)
	{
		printf("%d ", *(*pa + i));
	}
	return 0;
}

运行结果:


画图 + 文字 分析

*先和pa 结合,代表 pa 是一种指针类型,再与[]结合,代表里面存放的是数组,4 代表数组里面 4 个元素,int 代表每个类型都是 int 类型,而 pa 和 &arr 的类型就是 int * [4]

画图:

pa 存放的是 &arr(整个数组的地址),*&arr 等同于 arr(数组名,即首元素的地址),*pa + i 拿到每个数组里面每个元素的地址,再解引用,得到数组里面的每个元素


八. 二维数组传参的实质

二维数组也是数组,它可以看作多个一维数组构成,且空间是连续的,当传参时,传的是数组首元素的地址(即第一个一维数组的地址)


代码举例 + 文字分析

void Print(int arr[2][3])
{
	int i = 0;
	int j = 0;
	for (i = 0; i < 2; i++)
	{
		for (j = 0; j < 3; j++)
		{
			printf("%d ", arr[i][j]);
		}
		printf("\n");
	}
}
int main()
{
	int arr[2][3] = { {0,1,2},{3,4,5} };
	Print(arr);
}

运行结果:

这个是最容易看懂的形参接收的方式

#include<stdio.h>
void Print(int(*pa)[3])
{
	int i = 0;
	int j = 0;
	for (i = 0; i < 2; i++)
	{
		for (j = 0; j < 3; j++)
		{
			printf("%d ", *(*(pa + i) + j));
		}
		printf("\n");
	}
}
int main()
{
	int arr[2][3] = { {0,1,2},{3,4,5} };
	Print(arr);
}

运行结果:

这里我们用了 数组指针 的方式来接收实参

pa 拿到的是第一个一维数组的地址

pa + i 得到的是每一个一维数组的地址

解引用得到的是每一个一维数组的首元素地址

*(pa + i) + j 遍历了每一个一维数组的每个元素的地址

最后解引用,得到每个元素

对比两者,我们可以这样转换:

*(*(pa + i) + j) = *(pa[i] + j) = pa[i][j]


九. 函数指针

实际上,函数自身也有地址,而函数指针是存放函数地址的一种指针变量


代码举例 + 文字举例

#include<stdio.h>
void test()
{
	;
}
int main()
{
	void (*pa)() = test;
	printf("%p\n", test);
	printf("%p\n", *test);
	printf("%p\n", pa);
	printf("%p\n", *pa);
}

运行结果;

我们发现实际上,test 和 *test 的地址是一样的,说明函数名就是自身地址,不管&还是*,结果都不变

*先和 pa 结合,说明 pa 是指针

()代表存放的是函数的地址,而里面什么都没有,说明这个函数不传参

void 代表的是这个函数的返回类型 是 void

pa 的类型是 void (*) ()


十. 函数指针数组

这是一种数组,里面存放的是函数的地址


代码举例 + 文字分析

#include<stdio.h>
int ADD(int x, int y)
{
	return x + y;
}
int SUB(int x, int y)
{
	return x - y;
}
int main()
{
	int (*pa[2])(int, int) = { ADD,SUB };
	printf("%d %d", pa[0](1, 2), pa[1](1, 2));
}

运行结果:

分析:

pa 和 [] 结合,说明它是数组

2 代表数组存放的元素有 2 个

然后与 * 结合,说明数组存放的是地址

再与()结合,说明数组存放的是函数的地址

(int , int )说明每个函数的两个参数都是 int 类型

最后的 int 代表函数的返回类型是 int 类型

pa[i] 通过下标找到对应的内容 (这里找到的是函数的地址)

pa[i](1,2)类似于函数调用,把参数 1, 2 传过去了


几天没更新了,每次写这样的长篇介绍知识点真的太难了,最近在学习链表相关的问题,又得抽空更新前面的指针相关知识,希望体谅更新速度,如果可能,我会整理最近leetcode写的一些链表练习题,进行对比和相关讲解

  • 12
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值