建议:没有了解指针的人可以去学习一下指针(课本或视频),那基本都是普通的语法,先去学习指针的基本使用方法再来看这篇文章,不然你会接受不了🤭
目录
主要讲的内容:
1.字符指针
2.数组指针
3.指针数组
4.数组传参和指针传参
5.函数指针
6.函数指针数组
7.指向函数指针数组的指针
8.回调函数
9.指针和数组面试题的解析
指针的概念:
1. 指针就是个变量,用来存放地址,地址唯一标识一块内存空间。
2. 指针的大小是固定的4/8个字节(32位平台/64位平台)。
3. 指针是有类型,指针的类型决定了指针的+-整数的步长,指针解引用操作的时候的权限。
4. 指针的运算。
首先让基础的知识先放在前面
1.
大小端的问题感兴趣的话可以看下我的数据的存储进阶那一篇文章(对这篇不是很重要)
C语言——数据的存储进阶(如果位运算不了解先看文章的开篇链接)_逆风路上伴有谁的博客-优快云博客
2.
这里的==是等价,不要以为这是语法。
1.字符指针
在指针的类型中我们知道有一种指针类型为字符指针 char*;
常见用法1:
int main(void)
{
char ch = 'a';
char* pc = &ch;
printf("%c\n", ch);
*pc = 'b';
printf("%c\n", ch);
return 0;
}
常用方法2:
int main(void)
{
const char* pstr = "nice";
printf("%s\n", pstr);
return 0;
}
有人第一种用法很常见,但是第二种用法很少见过,不太理解为什么要加const,让我来细细道来。
首先我们来看一下不加const的后果
好吧,没报错,那么我们修改修改一下pstr指向的值看会不会报错。
为什么报错呢?
*p是取出字符串的n这个字符,如果改变意味着改变静态区的内容,而静态区存放的是常量和全局变量,以为这个字符串没有定义,所以说是常量,常量是不能修改的。所以会报错,但是可能又有人会问了,下面的这种常见情况是为什么。
我认为这是让静态区的nice\0拷贝到栈上的一块开辟的空间,ch数组的内容只是拷贝的内容,而自己在栈上的开辟的空间的内容是可以修改的。
所以pstr要用const来修饰,表示它指向的内容是不可被修改的,这样也不容易被误用。
常量不能修改,经常出现的错误(我是经常出现)常量不能作为左值:
这个就相当于
表达式必须是可被修改的左值的意思是左边应该是变量,所以以后出现这个问题的时候伙伴们应该知道怎么改错了吧。
来看一道面试题,尝试推测一下结果
#include <stdio.h>
int main()
{
char str1[] = "hello bit.";
char str2[] = "hello bit.";
const char* str3 = "hello bit.";
const char* str4 = "hello bit.";
if (str1 == str2)
printf("str1 and str2 are same\n");
else
printf("str1 and str2 are not same\n");
if (str3 == str4)
printf("str3 and str4 are same\n");
else
printf("str3 and str4 are not same\n");
return 0;
}
看一下结果
可能有人已经知道了,但是估计大多数人还是不理解,主要是第二个。
第一个是因为创建了两个数组,就意味着开辟了两块空间。比较两个字符数组的首元素地址,空间不一样,所以首元素地址也不相同。
第二个是因为在静态区开辟了一块空间,内容是hello bit.,因为静态区的大小是有限的且内容是不可改变的,所以计算机为了为了节省空间不会再开辟一块空间放置hello bit.。(重点)
那么有人可能会问了,怎么比较两个字符数组的是否相等呢?strcpy函数来比较(这个学校都讲吧)。
2. 指针数组
指针数组,它本质上还是数组,它每个元素的类型是指针。
int* arr1[10]; //整形指针的数组
char *arr2[4]; //一级字符指针的数组
char **arr3[5];//二级字符指针的数组
感觉只有一个点要讲,就是它的类型。
首先回忆一下,一个普通数组的类型是啥,这个我之前好像没有讲过。
例如:
那么上面三个指针数组的类型就是
int*[10]
char*[4]
char**[5]
这里[]里面的数组不要忘记
3. 数组指针
3.1 数组指针的定义:
根据上面那个指针数组的定义,数组指针就很明了,它其实就是指针,它存储地址对应那个数据
的类型是数组。
整形指针: int * pint; 能够指向整形数据的指针。
浮点型指针: float * pf; 能够指向浮点型数据的指针。
那数组指针应该是:能够指向数组的指针。
下面判断一下p1和p2分别是什么
int *p1[10];
int (*p2)[10];
//p1, p2分别是什么?
答案:p1是指针数组,p2是数组指针
原因是什么呢?这里需要了解[]和*那个优先级高,我直接说吧,[]比*的优先级高。
如果想了解操作符的优先级可以看下的操作符详解的那篇文章
所以我们知道p1先要和[]结合,那就是说p1先成了一个数组,那么它最终是个数组,它每个的类型就是前面的int*,p1的类型是int* []。
p2在()里面和*先结合,成了一个指针,那么它最终是个指针,它的指向的就是int [10]类型的数据,p2的类型是int (*) []。
3.2 &数组名VS数组名
int arr[10];
抛出问题:
arr 和 &arr 分别是啥?
我们知道arr是数组名,数组名表示数组首元素的地址。
那&arr数组名到底是啥?
我们看一段代码:
#include <stdio.h>
int main()
{
int arr[10] = {0};
printf("%p\n", arr);
printf("%p\n", &arr);
return 0;
}
看看结果
看着好像一样呀,没区别,再来改进一下例子。
int main()
{
int arr[10] = { 0 };
printf("%p\n", arr);
printf("%p\n", arr + 1);
printf("%p\n", &arr);
printf("%p\n", &arr + 1);
return 0;
}
这个看不懂的话看一下我之前发的数据的存储那一片文章(也花不了多长时间)
C语言——数据的存储进阶(如果位运算不了解先看文章的开篇链接)_逆风路上伴有谁的博客-优快云博客
这里可以看出arr + 1处的地址和&arr + 1处的地址不一样。让我来细细分析
感觉看完下面的这张图就可以解决这个疑惑了
arr是首元素的地址。
&arr是整个数组的地址,表现的是第一个元素的地址,实际上是整个数组的地址。&arr 的类型是: int(*)[10] ,是一种数组指针类型。数组的地址+1,跳过整个数组的大小,所以 &arr+1 相对于 &arr 的差值是40(单位是字节,数据在计算机中存储是以字节为单位的)。
3.3 数组指针的使用
看一个数组指针的例子
#include <stdio.h>
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,0};
int (*p)[10] = &arr;//把数组arr的地址赋值给数组指针变量p
//但是我们一般很少这样写代码
return 0;
}
#include <stdio.h>
void print_arr1(int arr[3][5], int row, int col)
{
int i = 0;
int j = 0;
for (i = 0; i < row; i++)
{
for (j = 0; j < col; j++)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
}
void print_arr2(int(*arr)[5], int row, int col)
{
int i = 0;
int j = 0;
for (i = 0; i < row; i++)
{
for (j = 0; j < col; j++)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
}
int main()
{
int arr[3][5] = { 1,2,3,4,5,6,7,8,9,10 };
print_arr1(arr, 3, 5);
//数组名arr,表示首元素的地址
//但是二维数组的首元素是二维数组的第一行
//所以这里传递的arr,其实相当于第一行的地址,是一维数组的地址
//可以数组指针来接收
print_arr2(arr, 3, 5);
return 0;
}
二维数组的传参传的是第一个元素的地址,而第一个元素的是一个一维数组,那么形参必须可以接收一维数组的地址。那么这个形参的类型就是一个数组指针。
观察下面四个类型分别是什么。
int arr[5];
int *parr1[10];
int (*parr2)[10];
int (*parr3[10])[5];
1是数组,类型是int[5]
2是指针数组,类型是int*[10],每个元素的类型是int*
3是数组指针,类型是int(*)[10]
4.是指针数组,类型是int(*[10])[5],每个元素的类型是int(*)[5]
4. 数组参数、指针参数
4.1 一维数组传参
#include <stdio.h>
void test(int arr[])//ok?
{}
void test(int arr[10])//ok?
{}
void test(int* arr)//ok?
{}
void test2(int* arr[20])//ok?
{}
void test2(int** arr)//ok?
{}
int main()
{
int arr[10] = { 0 };
int* arr2[20] = { 0 };
test(arr);
test2(arr2);
}
提醒:数组在传参的是时候传的是第一个元素的地址。
4.2 二维数组传参
void test(int arr[3][5])//ok?
{}
void test(int arr[][])//ok?
{}
void test(int arr[][5])//ok?
{}
//总结:二维数组传参,函数形参的设计只能省略第一个[]的数字。
//因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。
//这样才方便运算。
void test(int *arr)//ok?
{}
void test(int* arr[5])//ok?
{}
void test(int (*arr)[5])//ok?
{}
void test(int **arr)//ok?
{}
int main()
{
int arr[3][5] = {0};
test(arr);
}
第五个解释错误了,原因是传进去的是地址,那肯定是用地址来接收,即数组指针接收。(不想在图片里面改了)
4.3 一级指针传参
#include <stdio.h>
void print(int *p, int sz)
{
int i = 0;
for(i=0; i<sz; i++)
{
printf("%d\n", *(p+i));
}
}
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9};
int *p = arr;
int sz = sizeof(arr)/sizeof(arr[0]);
//一级指针p,传给函数
print(p, sz);
return 0;
}
思考:
当一个函数的参数部分为一级指针的时候,函数能接收什么参数?
4.4 二级指针传参
#include <stdio.h>
void test(int** ptr)
{
printf("num = %d\n", **ptr);
}
int main()
{
int n = 10;
int*p = &n;
int **pp = &p;
test(pp);
test(&p);
return 0;
}
思考:
当函数的参数为二级指针的时候,可以接收什么参数?
5. 函数指针
先看一段代码
#include <stdio.h>
void test()
{
printf("hehe\n");
}
int main()
{
printf("%p\n", test);
printf("%p\n", &test);
return 0;
}
发现和数组一样,函数的地址和函数指针的地址一样,其实这个才是真正的一样。函数名就可作为该函数的函数指针。
那么它的类型是什么呢?答案是:void (*) ()。返回类型 + (*) + (参数列表)
再举几个例子,
看到令人头痛的两道题
你们试一试可以做=看出来几道。
代码1:
先看一下(强制类型转换) 和*谁的优先级高(查看优先级表可访问我的操作符详解那篇文章)
但是都是右结合性(就是从右向左看),所以还是强制类型转换先执行。
代码2:
稍微有一点复杂,先讨论一个问题,一个函数是怎么定义的?
例如:
看懂这个,这个问题就好办了
这其实就是一个函数的定义。
6. 函数指针数组
顾名思义,函数指针数组就是一个数组,它的每一个元素是函数指针,因为函数名就是该函数的函数指针,所以可以说这个数组存放的每个元素是函数(相同类型)。
举个例子
ptest的类型是int (*[4]) (int, int)。
不要管这里的绿色曲线,这是因为我只是定义而没有实现函数,编译器报出的警告。
7. 指向函数指针数组的指针
实现把pptest变为指针那么就是(*pptest),然后指向的是函数指针数组,类型是int (*[4]) (int, int)。
8. 回调函数
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
简单来说就是这个函数需要调用其他函数帮助它完成工作,就像挑战杯比赛一样,一个战队完成一件事,但是这件事需要做PPT,做文档,找资料文献,有人封装代码,需要不同的人完成不同的事情。
qsort()函数就是一个典型的例子。它有四个参数。这里不讲了,直接看我的另一篇文章,有讲解。
9. 指针和数组笔试题解析
sizeof和strlen的区别
//1. strlen 和 sizeof没有什么关联
//2. srelen 是求字符串长度的-智能针对字符串求长度 - 库函数-使用得引头文件 '\0'之前的长度
//3. sizeof 计算变量,数组、类型的大小-单位是字节 - 操作符
还有重要的两个地方
一:数组名参与运算后会以指针的形式操作。
二:指针的大小是根据操作系统是几位来定的,32位是4个字节,64位是8个字节。也不能完全这个说,这也可以根据编译器,我的电脑是64位的(现在大部人的电脑都是64位吧),VS可以以X86(32位)平台运行,也可以以64位平台运行。
1.
//一维数组
int a[] = {1,2,3,4};
printf("%d\n",sizeof(a));
printf("%d\n",sizeof(a+0));
printf("%d\n",sizeof(*a));
printf("%d\n",sizeof(a+1));
printf("%d\n",sizeof(a[1]));
printf("%d\n",sizeof(&a));
printf("%d\n",sizeof(*&a));
printf("%d\n",sizeof(&a+1));
printf("%d\n",sizeof(&a[0]));
printf("%d\n",sizeof(&a[0]+1));
这两个报错别管它,我下载了个插件,注释的东西它都报错。
2.
//字符数组
char arr[] = {'a','b','c','d','e','f'};
printf("%d\n", sizeof(arr));
printf("%d\n", sizeof(arr+0));
printf("%d\n", sizeof(*arr));
printf("%d\n", sizeof(arr[1]));
printf("%d\n", sizeof(&arr));
printf("%d\n", sizeof(&arr+1));
printf("%d\n", sizeof(&arr[0]+1));
3.
printf("%d\n", strlen(arr));
printf("%d\n", strlen(arr+0));
printf("%d\n", strlen(*arr));
printf("%d\n", strlen(arr[1]));
printf("%d\n", strlen(&arr));
printf("%d\n", strlen(&arr+1));
printf("%d\n", strlen(&arr[0]+1));
遇到这种情况不要害怕,看到它提示的信息,访问冲突,其实就是非法访问,有些内存计算机是不允许计算机的使用者进行访问的例如0处的地址。还有有人关不掉这个界面,就很烦,看一下我的处理方法:
点击这个终止调试就行了,注意,这个红色的按钮不是左边那个火状红
4.
char arr[] = "abcdef";
printf("%d\n", sizeof(arr));
printf("%d\n", sizeof(arr+0));
printf("%d\n", sizeof(*arr));
printf("%d\n", sizeof(arr[1]));
printf("%d\n", sizeof(&arr));
printf("%d\n", sizeof(&arr+1));
printf("%d\n", sizeof(&arr[0]+1));
不要管报错,还是插件的问题。
5.
char arr[] = "abcdef";
printf("%d\n", strlen(arr));
printf("%d\n", strlen(arr+0));
printf("%d\n", strlen(*arr));
printf("%d\n", strlen(arr[1]));
printf("%d\n", strlen(&arr));
printf("%d\n", strlen(&arr+1));
printf("%d\n", strlen(&arr[0]+1));
6.
char *p = "abcdef";
printf("%d\n", sizeof(p));
printf("%d\n", sizeof(p+1));
printf("%d\n", sizeof(*p));
printf("%d\n", sizeof(p[0]));
printf("%d\n", sizeof(&p));
printf("%d\n", sizeof(&p+1));
printf("%d\n", sizeof(&p[0]+1));
报错还是插件的毛病
7.
char *p = "abcdef";
printf("%d\n", strlen(p));
printf("%d\n", strlen(p+1));
printf("%d\n", strlen(*p));
printf("%d\n", strlen(p[0]));
printf("%d\n", strlen(&p));
printf("%d\n", strlen(&p+1));
printf("%d\n", strlen(&p[0]+1));
8.
//二维数组
int a[3][4] = {0};
printf("%d\n",sizeof(a));
printf("%d\n",sizeof(a[0][0]));
printf("%d\n",sizeof(a[0]));
printf("%d\n",sizeof(a[0]+1));
printf("%d\n",sizeof(*(a[0]+1)));
printf("%d\n",sizeof(a+1));
printf("%d\n",sizeof(*(a+1)));
printf("%d\n",sizeof(&a[0]+1));
printf("%d\n",sizeof(*(&a[0]+1)));
printf("%d\n",sizeof(*a));
printf("%d\n",sizeof(a[3]));
总结:
数组名的意义:
1. sizeof(数组名),这里的数组名表示整个数组,计算的是整个数组的大小。
2. &数组名,这里的数组名表示整个数组,取出的是整个数组的地址。
3. 除此之外所有的数组名都表示首元素的地址。
10. 指针笔试题
笔试题1
int main()
{
int a[5] = { 1, 2, 3, 4, 5 };
int *ptr = (int *)(&a + 1);
printf( "%d,%d", *(a + 1), *(ptr - 1));
return 0;
}
//程序的结果是什么?
笔试题2
首先有一点很重要,地址其实就是一个16进制数字
//由于还没学习结构体,这里告知结构体的大小是20个字节
struct Test
{
int Num;
char *pcName;
short sDate;
char cha[2];
short sBa[4];
}*p;
//假设p 的值为0x100000。 如下表表达式的值分别为多少?
//已知,结构体Test类型的变量大小是20个字节
int main()
{
printf("%p\n", p + 0x1);
printf("%p\n", (unsigned long)p + 0x1);
printf("%p\n", (unsigned int*)p + 0x1);
return 0;
}
感觉不好看出来的是第二个,这是让地址地址强制类型转换为数字(unsigned long)。
好像有点尴尬,我说的是默认值,并不是真实值。但是这个结果是不是意味者一个指针没有指向它的地址是0处的地址?
尴尬了,我想错了。
又搞了一遍又出问题了
经过网友的解答,终于知道了。
首先说一点,全局变量的默认值是0,而局部变量是没有默认值的。
例如:
笔试题3
int main()
{
int a[4] = { 1, 2, 3, 4 };
int *ptr1 = (int *)(&a + 1);
int *ptr2 = (int *)((int)a + 1);
printf( "%x,%x", ptr1[-1], *ptr2);
return 0;
}
笔试题4
#include <stdio.h>
int main()
{
int a[3][2] = { (0, 1), (2, 3), (4, 5) };
int *p;
p = a[0];
printf( "%d", p[0]);
return 0;
}
笔试题5
int main()
{
int a[5][5];
int(*p)[4];
p = a;
printf( "%p,%d\n", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]);
return 0;
}
笔试题6
int main()
{
int aa[2][5] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
int* ptr1 = (int*)(&aa + 1);
int* ptr2 = (int*)(*(aa + 1));
printf("%d,%d", *(ptr1 - 1), *(ptr2 - 1));
return 0;
}
面试题7
int main()
{
int aa[2][5] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
int* ptr1 = (int*)(&aa + 1);
int* ptr2 = (int*)(*(aa + 1));
printf("%d,%d", *(ptr1 - 1), *(ptr2 - 1));
return 0;
}
a指向的那个w是w的地址这里不应该是w。但是思路没错。
这里提醒一点,a数组存的是三个字符串的首元素的地址。
面试题8
int main()
{
char* c[] = { "ENTER","NEW","POINT","FIRST" };
char** cp[] = { c + 3,c + 2,c + 1,c };
char*** cpp = cp;
printf("%s\n", **++cpp);
printf("%s\n", *-- * ++cpp + 3);
printf("%s\n", *cpp[-2] + 3);
printf("%s\n", cpp[-1][-1] + 1);
return 0;
}
++cpp后
解引用1次找到c+1处的地址,解引用2次找到字符P的地址。
意思是说打印的是POINT,此时cpp指向的位置已经发生改变
++cpp此时为
解引用1次找到c+1的地址。前置--找到c的地址(在c中观察),然后解引用找到E的地址,+3找到E的地址。打印ER
现在的情况是
[]比*的优先级高,不知道的可以看下我写的操作符详解的最后部分那个优先级表
cpp[-2]是c+3(没有从cpp的指向没有发生变化),解引用找到F的地址,+3找到S的地址,打印ST。
cpp[-1]是找到c+2(暂时的)
cpp[-1][-1]是找到字符N的地址,+1是找到E的地址,打印EW。
打印结果
最后要说的话
指针的知识确实难,我在写文章的是时候也有两道题写错了,仔细分析才弄对。要想真正理解指针,需要知道每一个变量意味着什么,最好在画一下图,这样更清晰,最后可以达到心中有图(当然我还没有达到这个水平)。
开始都感觉很难,但是如果慢慢理解,我们在刷题、做事情的时候用到指针真的感觉很爽(我自己感觉的)。