指针的进阶

建议:没有了解指针的人可以去学习一下指针(课本或视频),那基本都是普通的语法,先去学习指针的基本使用方法再来看这篇文章,不然你会接受不了🤭

目录

首先让基础的知识先放在前面

1.字符指针

常见用法1:

常用方法2:

2. 指针数组

3. 数组指针

3.1 数组指针的定义:

3.2 &数组名VS数组名

3.3 数组指针的使用

4. 数组参数、指针参数

4.1 一维数组传参

4.2 二维数组传参

4.3 一级指针传参

 4.4 二级指针传参

 5. 函数指针

看到令人头痛的两道题

6. 函数指针数组

7. 指向函数指针数组的指针

8. 回调函数

9. 指针和数组笔试题解析

sizeof和strlen的区别

总结:

数组名的意义:

 10. 指针笔试题

笔试题1

 笔试题2

笔试题3

 笔试题4

 笔试题5

笔试题6

面试题7

 面试题8

最后要说的话


主要讲的内容:

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()函数就是一个典型的例子。它有四个参数。这里不讲了,直接看我的另一篇文章,有讲解。

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。

 打印结果

 

最后要说的话

指针的知识确实难,我在写文章的是时候也有两道题写错了,仔细分析才弄对。要想真正理解指针,需要知道每一个变量意味着什么,最好在画一下图,这样更清晰,最后可以达到心中有图(当然我还没有达到这个水平)。

开始都感觉很难,但是如果慢慢理解,我们在刷题、做事情的时候用到指针真的感觉很爽(我自己感觉的)。

 

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值