🏠个人主页:泡泡牛奶
🌵系列专栏:C语言从入门到入土
本期将会带大家来认识什么是指针、指针的类型有哪些、指针运算、一级指针、二级指针、指针数组、数组指针、函数指针、函数指针数组等等相关知识,真正从0开始认识指针到指针的各种操作,让你在日常刷题、准备考试也能得心应手(≧∀≦)ゞ
赶快让我们看看今天的内容吧( ̄︶ ̄)>[GO!]
要理解指针, 我们首先要了解2个要点:
指针是内存中一个最小单元的编号,也就是地址
平时所说的指针,指的是指针变量,即用来存放内存地址的变量
内存
指针变量
我们可以通过
& (取地址操作符)
取出变量的内存起始地址,将地址存放到一个变量之中,这个变量就是我们所称的指针变量
#include
int main()
{
int a = 10;//在内存中申请一块空间
int* p = &a;//用 变量p 存放a的地址
return 0;
}
类比:
- 存放整型的变量,叫整型变量
- 存放地址的变量,叫指针变量
注意:
一个地址占用一个字节,而指针访问的字节数量由指针的类型来决定
对于32位机器,假设有32根地址线,那么每根地址线在寻址的时候产生的高电平(高电压 )和 低电平(低电压)就是 (1 或者 0)
那么32根地址线产生的地址就会是:
0000 0000 0000 0000 0000 0000 0000 0000
0000 0000 0000 0000 0000 0000 0000 0001
…
1111 1111 1111 1111 1111 1111 1111 1111
这里就有
2
32
2^{32}
232 个地址,按每个地址是一个字节来算
2
32
B
y
t
e
=
2
32
1024
k
B
=
2
32
1024
∗
1024
M
B
=
2
32
1024
∗
1024
∗
1024
G
B
=
4
G
B
2^{32} Byte = \frac{2^{32}}{1024} kB= \frac{2^{32}}{1024*1024} MB = \frac{2^{32}}{1024*1024*1024} GB = 4GB
232Byte=1024232kB=1024∗1024232MB=1024∗1024∗1024232GB=4GB
32位机器下有4G的闲置空间进行编址
同理,如果是64位的机器,就有 2 64 2^{64} 264 根地址线,到底有多大,不妨自己进行计算一下( •̀ ω •́ )✧
通过上面的介绍,我们可以明白:
总结:
我们知道,变量都是有不同类型的,例如整型、浮点型等。那指针有没有类型呢?
准确的说是有的,但是指针的类型并不会改变指针的大小。
有这样一段代码:
int num = 10;
p = #
如果我们想要将 &num
(num的地址) 保存到 p 中,已知 p是一个指针变量,那么它的类型就应该是 int*
类型。
char* pc = NULL;//字符型
short* ps = NULL;//短整型
int* pi = NULL;//整型
long* pl = NULL;//长整型
float* pf = NULL;//单精度浮点型
double* pd = NULL;//双精度浮点型
void* pv = NULL;//泛型指针
注意:
有些人可能会有一个疑问,
*
到底该靠左边写呢?还是右边写呢?
int* pi
可以很容易理解pi
是int*
类型的,但是这样写也相对会带来一个风险,当出现int* p1, p2;
这样多个命名的写法时,有些人会认为p2
也是int*
类型的,但这却是错的,p2
实际上的类型是int
类型。int *p1, *p2
这样的写法解决了上面的风险,但却相较于*
靠左边写比较难看出来类型总之,只要能分的清楚
*
在左边 和 在右边 的 优势和劣势,怎么写都行φ(゜▽゜*)♪
通过上面可以看到,指针的定义方式是: type + *
。
其实:
char*
类型的指针是为了存放 char
类型变量的地址
int*
类型的指针是为了存放 int
类型变量的地址
1. 进行解引用操作时,访问的字节数量
#include
int main()
{
int n = 0x11223344;
char *pc = (char*)&n;
int *pi = &n;
*pc = 0;
*pi = 0;
return 0;
}
可以看到 char
类型进行解引用操作时访问的是1个字节,而 int
类型进行解引用操作访问的是4个字节
2. 决定指针 向前 或 向后 走一步所跨过的字节 (步长)
#include
int main()
{
int arr[5] = { 0 };
char* pc = (char*)arr;
int* pi = arr;
*pi = 0x11223344;
*pc = 0;
*(pi + 1) = 0x11223344;
*(pc + 1) = 0;
return 0;
}
通过上面的例子,我们可以看到 char
类型是跳过1个字节, 而 int
类型跳过4个字节
概念: 野指针就是指针指向的位置是 不可知的(随机的、不正确的、没有明确限制的)
1. 指针未初始化
#include
int main()
{
int* p;//局部指针变量未初始化,默认随机值
*p = 20;
return 0;
}
2. 指针越界访问
#include
int main()
{
int arr[5] = {0};
int *pi = arr;
int i = 0;
for (i=0; i<=5; ++i)
{
//当指针指向数组规定的范围时,p就是野指针
*(pi++) = i;
}
return 0;
}
3. 指针所指向的空间被释放
#include
#include
int main()
{
int* nums = (int*)malloc(sizeof(int)*5);
if (!nums)
{
return -1;
}
for (int i = 0; i<5; ++i)
{
nums[i] = i;
}
free(nums);
for (int i = 0; i<5; ++i)
{
nums[i] = i;
}
return 0;
}
上面可以看到,被 malloc
开辟的空间被释放后,指针nums
就变为野指针,再去访问这块空间就会被当成随机访问
用于表示指针 向前 或 向后 走的步数
int arr[5];
int *pi = arr+3;
*(pi+1) = 4;
*(pi-1) = 3;
用于表示 前后两个指针 之间的 元素个数
int my_strlen(char* str)
{
char* p = s;
while (*p != '\0')
{
++p;
}
return p-s;
}
C语言标准规定:
允许 指向数组元素的指针 与 指向数组 最后一个元素后面 的那个内存位置的指针比较,但是不允许与 指向第一个元素之前 的那个内存位置的指针进行比较。
安全写法:
for (vp = &a[N]; vp > &a[0]; )
{
*--vp = 0;
}
危险写法:
for (vp = &a[N-1]; vp >= &a[0]; vp--)
{
*vp = 0;
}
注意: 虽然这样的写法再绝大部分编译器上使可以顺利完成任务的,但我们还是应该尽量避免这样的写法,因为标准规定不保证它可行。
请看下面例子:
#include
int main()
{
int arr[5] = { 0,1,2,3,4 };
printf("%p\n", arr);
printf("%p\n", &arr[0]);
return 0;
}
运行结果:
可以看到数组名和数组首元素地址是一样的。
结论: 数组名 表示的是 数组首元素地址。
注意:以下2种情况除外
sizeof(arr)
表示整个数组的(字节)大小&arr
表示整个数组,&arr + 1
跳过整个数组
那么,现在我们知道数组名可以表示数组的首元素地址,那么我们是否可以将数组首元素地址存放到一个指针中,利用指针来访问数组呢?
答案当然是可以的:
#include
int main()
{
int arr[] = { 1,1,4,5,1,4,1,9,1,9,8,1,0 };
int* p = arr;
int sz = sizeof(arr)/sizeof(arr[0]);
for (int i = 0; i<sz; ++i)
{
printf("%d ", *(p+i));
}
return 0;
}
一般使用:
int main()
{
char ch = 'w';
char* pc = &ch;
*pc = 'w';
return 0;
}
还有一种使用方法如下:
int main()
{
const char* pstr = "hello world!";//常量字符串
printf("%s\n", pstr);
return 0;
}
指针指向字符串可以类比数组,同样是指向首元素地址。
那么,有这样一个问题:
#include
int main()
{
char str1[] = "hello world!";
char str2[] = "hello world!";
char *str3 = "hello world.";
char *str4 = "hello world.";
if (str1 == str2)
printf("str1 和 str2 相同\n");
else
printf("str1 和 str2 不相同\n");
if (str3 == str4)
printf("str3 和 str4 相同\n");
else
printf("str3 和 str4 不相同\n");
return 0;
}
最终输出结果为:
原因:
str1
和str2
分别是两个不同的数组,即使数组内容相同,所占用的空间依然不同;str3
和str4
指向的是同一个相同的常量字符串,C/C++会把相同的常量字符串存放在一个单独的空间,当存在几个指针指向同一个字符串的时候,它们实际会指向同一块内存。
对比:
#include
int main()
{
char str1[] = "hello world!";
char str2[] = "hello world!";
char* str3 = "hello world.";
char* str4 = "hello world.";
char* str5 = "hello world!";
printf("%p\n", str1);
printf("%p\n", str2);
printf("%p\n", str3);
printf("%p\n", str4);
printf("%p\n", str5);
return 0;
}
str3
和 str4
指向的常量字符串相同,所以str3 == str4
,而str5
所指向的常量字符串不相同,故 str3 != str5
泛型指针,顾名思义,指针具有泛用性,什么类型的指针都可以接收,而这通常也用于函数中进行使用。
泛型指针特点:
- 可以 接收任何类型 的指针
- 泛型指针访问的字节数是0
注意: void*
只能接收,不能 指向数据(不能通过void*
访问数据)
**思考:**那么这样的指针到底有什么用呢🤔?
例如:
int fun(void* p, void* q)
{
return *((int*)p) - *((int*)q);
}
如果想知道更多,请接着往下看哦( •̀ ω •́ )✧
我们知道,指针变量也是变量,既然是变量就有地址,那么指针变量的地址存放在哪里?
二级指针
可用于存放一级指针的地址
对于二级指针的运算有:
*pi
通过对 pi
中地址进行解引用,这样找到的是i
,*pi
其实访问 的就是 i
**ppi
先通过 *ppi
找到 pi
内存放的值, 然后再通过 *pi
找到 i
内存放的值
int i = 10;
int *pi = &i;//pa 等价于 &a
int **ppi = π
**ppi = 30;
//等价于 *pa = 30;
//等价于 a = 30;
**Q:**指针数组到底是 指针 还是 数组 🤔?
**A:**是 数组
类比:
int arr1[5]
用于连续存放一组整型数据char arr2[5]
用于连续存放一组字符型数据那么指针数组是什么样的?
int* arr3[5];
解释:arr3
首先与[]
结合,说明arr3
是一个数组,数组里面存放了5个元素,每个元素是int*
类型
按操作符的优先级来说,arr3
首先会与[]
结合,再看到*
#include
int main()
{
int arr1[5] = { 1,2,3,4,5 };
int arr2[5] = { 2,3,4,5,6 };
int arr3[5] = { 3,4,5,6,7 };
int* arr[3] = { arr1,arr2,arr3 };
for (int i = 0; i < 3; ++i)
{
for (int j = 0; j < 5; ++j)
{
//写法一
printf("%d ", *(arr[i] + j));
//写法二
//printf("%d ", arr[i][j]);
}
printf("\n");
}
return 0;
}
我们知道,数组名代表其元素的首元素地址,通过arr[i]
访问到具体哪个地址,再通过地址访问到数组元素
总结:
什么是指针数组🤔?
存放指针的数组,就是指针数组。
指针是数组是数组,那么数组指针就是指针。
通过上面类比,我们可以思考思考:
什么是 数组指针🤔?
如同上面所说,按优先级来看,如果写成int* arr[5]
又是指针数组,那么该如何表示呢?
类比:
整型指针:int* pi = &a
( 假设定义了 int a = 0
)
浮点型指针:float* pf = &b
( 假设定义了 float b = 0.0f
)
int (*p)[10];
解析: p
先与*
结合,说明p
是一个指针变量,然后指向一个大小为10的整型数组。所以p
是一个指针,指向一个数组,脚数组指针。
使用实例:
#include
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int(*pa)[10] = arr;
//error
//arr是首元素地址,为int*类型
int(*pa)[10] = &arr;
//指向整个数组
for (int i = 0; i<10; ++i)
{
printf("%d ",(*pa)[i] );
}
return 0;
}
小结:
Q:什么是 数组指针🤔?
A:数组指针就是一个指向 (整个)数组 的指针。
注意:
指针的类型需要与数组的类型相同,以上面为例,
arr
的类型是int[10]
,那么(*pa)
的类型也应该是int[10]
现在我们已经对 指针数组 和 数组指针 有了一定的了解了,那么就来试试下面几个例子吧φ(゜▽゜*)♪
int *parr1[10];
int (*parr2)[10];
int (*parr3[10])[5];
int *parr1[10]
按优先级结合顺序来说,parr1
首先与[]
结合,确定是数组,是什么数组?向外看,类型是int*
类型,是 一个存放int*
类型的 指针数组
int (*parr2)[10]
parr2
首先与*
结合,是指针,是什么指针?向外看类型为int [10]
,是一个指向有10个元素的整型数组的 数组指针。
int (*parr3[10])[5]
parr3
首先与[]
结合,说明()
内部是数组,去除parr3[10]
后剩下int (*)[5]
,可以看出这是一个数组指针,那么组合在一起就是一个数组指针数组
在写代码的时候难免要将数组或者指针传给数组,那么函数的参数该如何设计呢?
我们知道,数组名就是首元素的地址,而指针又是可以接收数组的存在,那么我们是否可以这样写?
void test1(int arr[])// 1
{}
void test2(int arr[10])// 2
{}
void test3(int* arr)// 3
{}
void test4(int* arr[10])// 4
{}
void test5(int** arr)// 5
{}
void test1(int arr[])
void test2(int arr[10])
void test3(int* arr)
#include
//假设定义了以上代码
int main()
{
int arr1[20] = {0};
test1(arr1);
test2(arr1);
test3(arr1);
int *arr2[10];
test4(arr2);
test5(arr2);
return 0;
}
1、2、3的调用方式都相同,虽然void test2(int arr[10])
中参数为int arr[10]
但是还是能访问到20个元素,因为数组在内存中是连续存放
void test1(int arr[3][5])//有具体数值
{}
void test2(int arr[][5])//省略行
{}
//error
//不能都省略
void test3(int arr[][])
{}
总结:
二维数组传参,函数参数的设计只能省略 第一个[]
的数字。对于一个二维数组而言,可以不知道有多少行,但是必须知道一行有对少个元素,这样才能方便运算。
在数组一章中 🚀点此传送门[数组] ,我们知道二维数组在内存中存储实际上与一维数组相同,都是由低地址向高地址连续存储,那么我们是否也是可以将二维数组以一维数组的方式进行传参呢🤔?答案是可以的。
void test4(int* arr)
{}
结合指针数组,也可以写成下面这样
#include
#include
void test5(int** arr, int row, int col)
{
int i = 0;
for (i = 0; i < row; ++i)
{
int j = 0;
for (j = 0; j < col; ++j)
{
arr[i][j] = i + j;
printf("%d ", arr[i][j]);
}
printf("\n");
}
}
void test6(int* arr[5], int row, int col)
{}
int main()
{
int i = 0;
int* arr[5];
//构建二维数组
for (i=0; i<5; ++i)
{
int* tmp = (int*)malloc(sizeof(int)*10);
assert(tmp);
arr[i] = tmp;
}
//传参
test5(arr, 5, 10);
test6(arr, 5, 10);
return 0;
}
运行结果:
而这样的传参方法多可能会在OJ(例如:力扣)上遇到 ( •̀ ω •́ )✧暗示
#incldue <stdio.h>
void test(int *p, int sz)
{
int i = 0;
for (i=0; i<sz; ++i)
{
*(p+i) = i;
printf("%d ", *(p+i));
}
}
int main()
{
int arr[10] = {0};
int *p = arr;
int sz = sizeof(arr)/sizeof(arr[10]);
//一集指针p,传给函数
test(arr, sz);
return 0;
}
思考🤔:
当函数参数为一级指针时,函数可以传什么值?
- 传数组
主要用于对数组进行操作
- 传单个数值
用于修改某个数的值 (在之后的学习中,相信大家会遇到很多这样的情况☺️)
#include
void test(int **ptr)
{
printf("%d\n", **ptr);
}
int main()
{
int n = 10;
int *p = &n;
int **pp = &p;
test(pp);
test(&p);
return 0;
}
二级指针接收一集指针地址,传参的时候需要参数为一级指针地址
首先我们来看一下下面一段代码:
#include
void test()
{
printf("hehe\n");
}
int main()
{
printf("%p\n", test);
printf("%p\n", &test);
return 0;
}
输出结果为:
输出的是两个相同的地址,而这两个就是test
函数的地址,那么要怎样将函数的地址存起来呢?
//假设有以下函数定义
void test(int i)
{
printf("haha\n");
}
//那么函数指针应该为
void (*pfun)(int) = test;
pfun
首先与*
结合,说明pfun
是指针,其指向的是一个函数,指向的函数参数为int
,返回类型为void
看两段有意思的代码:
//代码1
(*(void (*)())0)();
//代码2
void (*signal(int , void(*)(int)))(int);
(* (void (*)()) 0 )()
对代码稍微进行简化一下
typedef void (*type_t)();
//函数指针在进行调换是要将名字放在()里
//理解上:效果等同于
//typedef void (*)() type_t;
//简化后
(* (type_t) 0 )()
可以发现,0被强制类型转换成了函数指针类型,再进行函数调用。
void (* signal( int , void(*)(int) ) )(int)
我们按照同样的方法对大妈进行简化
typedef void(*type_t)(int);
//简化后
type_t signal(int, type_t);
可以轻易看出:
signal
是一个函数声明,一个参数是int
,另一个参数是函数指针,参数为int
,返回类型为void
signal
的返回类型也是一个函数函数指针,该指针指向一个函数参数为int
,返回类型为void
数组是一个存放相同类型数据的储存空间,类比指针数组:
int* arr[10];
//arr先与[]结合,每个元素是int*
仿照指针数组
首先名字要与[]
先结合,再写出类型int (*)() parr[10]
再根据函数指针的语法规定,将数组放入()
内部,于是就出现了下面这样
int (* parr[10] )();
那么对于这样的函数指针数组,它的用途是什么呢?
答:转移表
可以将 函数 参数相同、返回类型相同 的一类函数用一个数组存起来,最后可以通过数组的下标来调用函数。
而最简单的实例就是:计算器
对比指向 指针数组 的指针:
指针指向一个数组,数组每个元素都是指针(指路✨->二维数组传参)
int* arr[10];
int **p = &arr;
那么我们接下来看看指向函数指针数组的指针要怎么定义吧😎。
void test(const char* str)
{
printf("%s\n", str);
}
int main()
{
//函数指针
void (*pfun)(const char*) = test;
//将函数指针存入数组
void (*pfarr[5])(const char*);
pfarr[0] = test;
//指向函数指针数组pfarr的指针ppfarr
void (* (*ppfarr[5]) )(const char*) = &pfarr;
return 0;
}
回调函数就是通过函数指针调用的函数。将函数的地址作为参数传递给另一个函数, 当这个函数指针用来调用其所指向的函数时,我们就说这是回调函数。
而qsort
这个函数就是利用了回调函数,下面为大家简单介绍一下这个函数吧😎
base - 需要排序的数组
num - 数组元素个数
size - 每个元素的大小
compare - 比较两个数的函数
#include
//回调函数
int cmp(const void * p1, const void * p2)
{
return (*( int *)p1 - *(int *) p2);
}
int main()
{
int arr[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };
int sz = sizeof(arr) / sizeof(arr[0]);
int i = 0;
qsort(arr, sz, sizeof(int), cmp);
//打印
for (i = 0; i< sizeof(arr) / sizeof(arr[0]); i++)
{
printf( "%d ", arr[i]);
}
printf("\n");
return 0;
}
现在我们已经基本了解 什么是回调函数 与 qsort的基本使用方法 ,那么能不能试着仿照 qsort
写一个冒泡排序呢?
void _swap(void *p1, void * p2, int size)
{
int i = 0;
for (i = 0; i< size; i++)
{
char tmp = *((char *)p1 + i);
*(( char *)p1 + i) = *((char *) p2 + i);
*(( char *)p2 + i) = tmp;
}
}
void BubbleSort(void* base, size_t num, size_t size,
int (*cmp)(const void*,const void*))
{
int i = 0;
int j = 0;
for (i=0; i<num-1; ++i)
{
int flag = 1;
for (j=0; j<num-i-1; ++j)
{
if ( cmp( (char*)base + j*size,
(char*)base + (j+1)*size ) > 0)
{
_swap((char*)base + j*size,
(char*)base + (j+1)*size,
size );
flag = 0;
}
}
if (flag)
{
break;
}
}
}
注意:
base需要强制类型转换成char*
,因为void*
的访问的是0字节,传入具体参数需要具体参数,至于转成char*
是因为,char*
每次跳过一个字节,方便访问地址
如果有其它想要了解的函数可以考虑去cplusplus官网看看,连接我就放在这里啦(≧∀≦)ゞ
好啦ヾ(▽*))) 本期的内容就到这里,如果觉得对你有帮助的话,还不忘三连支持一下,谢谢ο(=•ω<=)ρ⌒☆
这一次真的是爆肝了(;´д`)ゞ超长篇的大总结,如果对你有帮助的话,还不忘动动手指给予我一点点支持吧o(TヘTo)