一. 什么是指针?
内存
在计算机管理内存时,会把内存划分成好几个内存单元,每个内存单元对应相应的编号(这个编号也可以理解为对应内存单元的地址),在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;
}
注:随着数组下标的增大,地址是由低到高
野指针
什么是野指针?
概念: 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
造成原因:
- 指针未初始化
代码举例
#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写的一些链表练习题,进行对比和相关讲解