C语言指针进阶(下)

Posted 长月.

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C语言指针进阶(下)相关的知识,希望对你有一定的参考价值。

提示: 本篇深度剖析数组和指针的知识点,并且列举多种例子来说明sizeof和strlen的用法
接下来一起来学习吧👻👻

文章目录


前言

九、数组和指针

数组–>能够存放一组相同类型的元素,数组的大小取决于数组的元素个数和元素类型
指针–>就是地址或者也说是指针变量,大小是4/8个字节
二者之间的关系:
(1)数组是数组,指针是指针,二者不等价
(2)数组名是数组首元素的地址,这个地址可以存放在指针变量中
(3)我们可以用指针来遍历数组
(4)数组名
大多数情况下,数组名是数组首元素的地址.
但是有两个例外:
一是sizeof(数组名)–>数组名表示整个数组,计算的是整个数组的大小
二是&数组名–>数组名表示整个数组,取出的是数组的大小

1.透彻理解整型数组

以下内容的重点是区分下面三种写法的区别是什么以及sizeof和strlen在这三种写法中的使用

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main() 
	int a[] =  1,2,3,4 ;
	printf("%d\\n", sizeof(a));//16
	//一个整型是四个字节,sizeof(数组名)就是例外情况之一,计算的是数组总大小,单位是字节,所以是16

	printf("%d\\n", sizeof(a + 0));//4
	//注意X86环境32位,X64环境64位,目前是X86环境
	//sizeof(a+0)不是把数组名单独放在siaeof内部,所以这里的a表示数组名首元素的地址
	//a+0其实是数组首元素的地址
	//首元素的地址+0还是首元素的地址,大小是首元素地址的大小,4(X86环境)/8(X64环境)个字节

	printf("%d\\n", sizeof(*a));//4
	//不是数组名单独放在sizeof内部,所以这里的a表示数组首元素的地址,a等价于&a[0]
	//*a等价于*&a[0]等价于a[0]
	//首元素的大小也就是一个整型的大小4

	printf("%d\\n", sizeof(a + 1));//4/8
	//不是数组名单独放在sizeof内部,所以这里的a表示数组首元素的地址,a的类型是int*
	//a+1  跳过一个整型,是第二个元素的地址.是地址的话就是4/8个字节


	printf("%d\\n", sizeof(a[1]));//4
	//数组下标为1的元素的大小,数组每个元素都是整型,大小为4

	printf("%d\\n", sizeof(&a));//4/8
	//&a 取出的是整个数组的地址,是地址就是4个或者8个字节,即使它是整个数组的地址也仅仅是个地址!!!
	//&a 要是想存起来,需要放到一个数组指针里面
	//int (*pa)[4]=&a;  这里&a的类型是int (*)[4]

	printf("%d\\n", sizeof(*&a));//16
	//&a 拿到整个数组
	//*&a  解引用整个数组
	//整个数组的大小是4*4=16字节
	//事实上,*和&可以相互抵消,在这里sizeof(*&a)等价于sizeof(a),也就是整个数组的大小16

	printf("%d\\n", sizeof(&a + 1));//4/8
	//&a  拿到整个数组的地址,它的类型是数组指针int (*)[4]
	//&a+1  数组指针+1指的是地址跳过一个数组的,但是其实它还是指向内存里面的,是地址就是4/8个字节

	printf("%d\\n", sizeof(&a[0]));//4/8
	//取出首元素的地址 4/8个字节

	printf("%d\\n", sizeof(&a[0] + 1));//4/8
	//第二个元素的地址

图解:

2.透彻理解字符数组(区分sizeof和strlen)

重点:区分sizeof和strlen

sizeof()👻👻👻
1.sizeof计算的是占用内存空间的大小,单位是字节,不关注内存中到底存放的是什么
2.注意sizeof不是函数,是操作符
strlen()👻👻👻
1.strlen是函数
2.strlen是针对字符串的,求的是字符串的长度,本质上统计的是\\0之前出现的字符个数

(1)sizeof和strlen在字符数组中的应用

定义一个字符数组
char arr[] = ‘a’,‘b’,‘c’,‘d’,‘e’,‘f’ ;

sizeof用法:
printf("%d\\n", sizeof(arr));//6

sizeof(数组名) 数组名表示整个数组的地址
sizeof计算的是占用内存空间的大小,单位是字节,不关注内存中到底存放的是什么
sizeof(arr) 计算整个数组的大小,6个字符型元素总大小为6字节

printf("%d\\n", sizeof(arr + 0));//4/8

数组名没有单独放在sizeof内部,这里是首元素的地址
arr+0 还是数组首元素的地址
不管是什么地址,只要是地址就是4/8个字节

printf("%d\\n", sizeof(*arr));//1

这里的arr是首元素地址
arr是首元素,计算的是首元素的大小
数组名的类型是char
即数组里面每个元素的类型都是char*
这里的arr是首元素的地址,解引用,也就是char*类型的解引用,访问一个字节

printf("%d\\n", sizeof(arr[1]));//1

arr[1]是数组的第二个元素,一个字符它的大小为1个字节

printf("%d\\n", sizeof(&arr));//4/8

&arr取出的是数组的地址
数组的地址也是地址,为4/8个字节

printf("%d\\n", sizeof(&arr + 1));//4/8

&arr 数组的地址 +1 代表跳过一个数组后的地址
但是它还是一个地址,地址的长度为4/8个字节

printf("%d\\n", sizeof(&arr[0] + 1));//4/8

&arr[0] 下边为0的元素的地址 +1 跳过这个元素的地址
实质上是第二个元素的地址,但还是地址,为4/8个

strlen的用法
printf("%d\\n", strlen(arr));//随机值

arr指向首元素的地址
strlen是针对字符串的,求的是字符串的长度,本质上统计的是\\0之前出现的字符个数
\\0之前是多少个字符,长度就是多少,strlen会一直往后数,直到遇到\\0停止
但是不知道会什么时候遇到\\0,所以这个长度是个随机值

printf("%d\\n", strlen(arr + 0));//随机值

arr指向首元素的地址 +0 还是指向首元素的地址
传进来首元素的地址,strlen会一直往后数,直到遇到\\0停止
但是不知道会什么时候遇到\\0,所以这个长度是个随机值

printf("%d\\n", strlen(*arr));//非法访问

在之前的学习中,模拟实现strlen功能的函数是这么写的:
my_strlen(const char* str)
注意传给strlen的是地址,arr指向首元素的地址,解引用得到首元素’a’,所以传进去的是字符a,而字符a的本质是97
把97传给strlen,strlen就把97当成一个地址,所以strlen就从97这个地址编号往后数,97作为地址编号找到的内存空间,不是给我们分配的内存空间,97是我们随便传入的一个地址,就要去访问它吗?这样是不行的
所以这里会形成非法访问的

printf("%d\\n", strlen(arr[1]));//非法访问

arr[1]是字符b,这里会把b的ASCII值当成地址,b–98,同上会形成非法访问

printf("%d\\n", strlen(&arr));//随机值

&arr是数组的地址,但传给strlen之后还是从数组的起始位置开始往后找\\0,它数到的也是随机值

printf("%d\\n", strlen(&arr + 1));//随机值-6

&arr+1 跳过整个数组,然后往后数找\\0,也不知道什么时候能遇到\\0,结果也是个随机值
但是这个随机值一定比从数组原始位置开始往后数时得到的随机值小6,因为开始的位置不同

printf("%d\\n", strlen(&arr[0] + 1));//随机值-1

是从第二个元素的地址开始往后数的,往后数找\\0,也不知道什么时候能遇到\\0,结果也是个随机值
但是这个随机值一定比从数组原始位置开始往后数时得到的随机值小1,因为开始的位置不同

(2)sizeof和strlen在用字符串初始化数组中的应用

用字符串初始化数组:
char arr[] = “abcdef”;

[0] a
[1] b
[2] c
[3] d
[4] e
[5] f
[6] \\0
在数组里面实际上放了7个字符

sizeof用法
printf("%d\\n", sizeof(arr));//7

arr单独放在sizeof内部,sizeof计算的是整个数组的大小,关心的数组的大小,不关心里面是不是\\0
所以它的大小是7

printf("%d\\n", sizeof(arr + 0));//1

arr是首元素的地址,首元素的地址+0,还是首元素的地址,是地址就是4个或者8个字节

printf("%d\\n", sizeof(*arr));//1

arr是首元素的地址,arr 是数组首元素,每个元素都是char类型,每个元素的大小都是一个字节
补充:首元素可以怎么表示?
arr[0]或者
arr或者*(arr+0)
计算数组的大小时,可以写成:
int sz=sizeof(arr)/sizeof(arr[0];
int sz=sizeof(arr)/sizeof(*arr);

printf("%d\\n", sizeof(arr[1]));//1

arr[1]就是第二个元素的大小
数组每个元素都是char类型,每个元素的大小都是一个字节

printf("%d\\n", sizeof(&arr));//4/8

&arr取出的是整个数组的地址,是地址,它的长度就是4或者8个字节

printf("%d\\n", sizeof(&arr + 1));//4/8

&arr取出的是整个数组的地址 +1 跳过整个数组之后的地址
是地址就是4/8个字节
注意:与之前I.里面跳过整个数组的地址,在跳过的内容上有所区别,之前跳过整个数组的时候数组里面没有\\0.这次跳过的数组里面包含\\0

printf("%d\\n", sizeof(&arr[0] + 1));//4/8

&arr[0] 取到的是第一个元素的地址,+1 是跳过第一个元素后的地址
是地址,就是4/8个字节

strlen用法
printf("%d\\n", strlen(arr));//6

arr是首元素的地址,strlen从首元素的地址开始往后数找\\0,\\0前面有6个元素,长度为6

printf("%d\\n", strlen(arr + 0));//6

arr数组名表示首元素的地址 +0 还是首元素的地址
strlen从首元素的地址开始往后数找\\0,\\0前面有6个元素,长度为6

printf("%d\\n", strlen(*arr));//非法访问

*arr 是第一个元素a的值传给strlen,当成地址去访问会造成非法访问

printf("%d\\n", strlen(&arr));//6

&arr 整个数组的地址
&arr的类型是数组指针char(*)[7]
而实际上strlen应该穿进去的是const char 类型的指针(地址),在这里的话非要传的类型是数组指针,这时就会把它强制转换成const char类型的
在这里编译的时候会报警告,但是不影响使用
strlen数的时候还是从起始的位置开始数,直到遇到\\0,长度应该为6

printf("%d\\n", strlen(&arr[0] + 1));

&arr[0] 首元素的地址 +1 跳过首元素的地址 往后数直到遇到\\0.可以数到5个

注意:
strlen()进去的应该是地址,不应该是元素,如果传进去元素,会把元素的值当成地址进行访问,造成非法访问

(3)当把常量字符串的首地址放进指针变量,sizeof和strlen的用法

char* p = “abcdef”;

sizeof:
printf("%d\\n", sizeof(p));//4/8

这里算的是一个指针变量的大小,既然是指针变量,指针变量是用来存放地址的,所以指针变量的大小是4或者8

printf("%d\\n", sizeof(p + 1));//4/8

p里面是常量字符串的首地址也就是a的地址,p+1 p的类型是char*,char*类型的+1跳过一个字符,实际上也就是字符b的地址
是地址,就是4个或者8个字节

printf("%d\\n", sizeof(*p));//1

p存放的是常量字符串的首地址也就是a的地址,解引用访问一个字符,一个字符的大小就是1

printf("%d\\n", sizeof(p[0]));//1

p[0]从数组的访问形式可以转成指针
p[0]可以写成*(p+0),也就是*p
p存放的是常量字符串的首地址也就是a的地址,解引用访问一个字符,一个字符的大小就是1

printf("%d\\n", sizeof(&p));//4/8

&p 拿到的是地址,是地址长度就是4/8个字节
不过要清楚的是&p是指针变量p的地址,而不是常量字符串的地址
&p是个二级指针

printf("%d\\n", sizeof(&p + 1));//4/8

p类型char*
&p类型char**
char* p;//字符指针跳过一个字符
char* pp=&p; //(第二个说明pp是指针,第一个说明pp里面指向的是char类型的数据)pp指向的是char的数据,+1跳过一个char的数据
但是还是地址,所以是四个或者八个字节

printf("%d\\n", sizeof(&p[0] + 1));//4/8

&p[0]取出a的地址,+1跳过a的地址,是字符b的地址
但还是地址,是地址就是四个或者八个字节

strlen的用法
printf("%d\\n", strlen(p + 1));//5

p+1 跳过a的地址,里面放的是b的地址,从b开始往后数,字符串末尾是\\0,所以长度是5

printf("%d\\n", strlen(*p));//非法访问

*p是a,传过去但是strlen需要的是地址,那就把a的ASCII码值当做地址,形成了非法访问

printf("%d\\n", strlen(p[0]));//非法访问

p[0]和p是一样的,因为p[0]等价于(p+0),都是a,传过去但是strlen需要的是地址,那就把a的ASCII码值当做地址,形成了非法访问

printf("%d\\n", strlen(&p));//随机值x

注意&p拿出来的是哪里的地址,
拿出p的地址然后往后数,p里面存的地址是什么以及后面什么时候遇到\\0这完全不可知
所以这里只能是随机值

printf("%d\\n", strlen(&p + 1));//随机值y

&p+1 跳过p的地址往后数,什么时候遇到\\0,这就更无法预测了,所以也是随机值
但是这个随机值y和上面的随机值x毫无关系,完全取决于这个内存块里面放的是什么

printf("%d\\n", strlen(&p[0] + 1));//5

&p[0]+1 是b的地址 ,从b的地址往后数,遇到\\0,所以长度是5

3.透彻理解二维数组

创建一个二维数组
int a[3] [4] = 0 ;

二维数组:
二维数组的首元素是第零行

printf("%d\\n", sizeof(a));//48

a这个二维数组的数组名单独放在sizeof内部,计算整个数组的大小
这个二维数组三行四列,每个元素是整型,一个整型四个字节,434=48字节

printf("%d\\n", sizeof(a[0][0]));//4

第一行第一个元素,大小是四个字节

printf("%d\\n", sizeof(a[0]));//16

a[0]是第零行的数组名,这时数组名单独放在sizeof内部,计算的是数组的大小,单位是字节
第零行是四个整型,4*4=16字节

printf("%d\\n", sizeof(a[0] + 1));//4/8

arr[0]数组名不是单独放在sizeof内部,a[0]表示表示首元素的地址,即第一行第一个元素的地址也就是a[0][0]的地址
+1 第一行第二个元素的地址即a[0][1]的地址
是地址就是4/8个字节

printf("%d\\n", sizeof(*(a[0] + 1)));//4

*(a[0]+1) 也就是解引用第一行第二个元素的地址,得到a[0][1] 大小是4个字节

printf("%d\\n", sizeof(a + 1));//4/8

a作为二维数组的数组名,并非单独放在sizeof内部,所以表示首元素的地址
二维数组的首元素是第一行,这里的a是第一行的地址,第一行的地址是数组指针int ()[4]
当+1的时候,跳过一行,
a+1 是第二行的地址,注意不是第二行首元素的地址,因为a的类型是数组指针int (
)[4],+1之后还是这个类型,是指向第二行的
要分清,第二行的地址是数组指针,第二行首元素的地址是整型指针

printf("%d\\n", sizeof(*(a + 1)));//16

解读方式1:这里a+1拿到的是第二行的地址,第二行的地址解引用相当于对第二行的数组指针解引用也就是访问一个数组,访问第二行的大小,44=16字节
解读方式2:
(a+1)从语法上讲等价于a[1],a[1]是第二行的数组名,sizeof(a[1])数组名单独放到sizeof内部,求出来的是第二行的大小,4*4=16

printf("%d\\n", sizeof(&a[0] + 1));//4/8

&a[0] 数组名取地址,取出的是第一行的地址
+1 第二行的地址
是地址就是4/8个字节

printf("%d\\n", sizeof(*(&a[0] + 1)));//16

上一个代码已解释&a[0] + 1是第二行的地址,解引用就是第二行,第二行的大小4*4=16
*(&a[0] + 1))等价于a[1]1

printf("%d\\n", sizeof(*a));//16

a在这里是首元素的地址,二维数组的首元素地址就是第一行的地址,解引用就是第一行,第一行的大小是4*4=16
*a 就是第一行
*a 等价于 *(a+0)等价于a[0]

printf("%d\\n", sizeof(a[3]));//16

这个数字总共只有三行,a[3]是第四行了,a[3]和a[2],a[1],a[0]同样的道理是行的数组名,如果有第四行的话,它的大小一定是16字节,因为每一行都是16字节
这里存在越界吗?
不存在.因为sizeof内部的表达式不会真的去访问或者计算
这里就根本不会去访问第四行,只要根据它的类型属性确定了,假设存在第四行的话,和之前的a[2],a[1],a[0比较一推断,就知道是相同的类型,不会形成越界访问

补充说明

int a = 5;
short s = 11;
printf("%d\\n", sizeof(s = a + 2));//2
//这里按说a是整型,+2之后还是整型,非要把四个字节的整型数据放到2个字节的short变量里面,short变量不过也可以放下7,因为发生了截断,把低位的7留下,高位的0截断丢了,也可以计算出7
//在计算表达式的值所占的空间大小是几个字节,把结果放到short类型的变量里面,结果是short类型的,sizeof(short类型的数据)理论上应该是两个字节,是2
//s = a + 2值属性是7,类型属性是short

printf("%d\\n", s);//11
//这里运行的结果是11而不是计算之后的7
//说明sizeof内部的表达式不会真的计算,
//原因一:因为sizeof通过类型就可以知道表达式的大小是2,根本不需要计算得出2
//原因二:代码test.c---->编译--->链接--->test.exe
//如果表达式s=a+2要运算的话必须生成可执行程序test.exe才能运算,遗憾的是像sizeof这样的在编译期间就已经处理了,编译期间根据类型属性就已经确定是2了,就不会把表达式计算了

int a = 10;
a + 3;
//像a+3这样的式子有值属性,类型属性
//值属性是13
//类型属性是int

printf("%d\\n", sizeof(*a + 1));//4/8

a是第一行的地址,第一行的地址解引用,*a等价于a[0]
第一行的数组名a[0]表示首元素的地址&a[0][0],+1就是&a[0][0]+1=&a[0][1]
也就是第一行第二个元素的地址,是地址的话长度就是4/8个字节

注意点

1.数组名单独放到sizeof内部,这里的数组名表示整个数组,计算的是整个数组的大小
&数组名,这里的数组名表示整个数组,取出的是整个数组的地址
除上面两个例外情况,所有的数组名都表示首元素的地址
2.二维数组首元素的地址是第一行的地址,
✨sizeof(a[0]) 计算的是这一行的大小
✨sizeof(a[0]+1) 某一行的数组名并没有单独放到sizeof内部的时候,数组名表示首元素的地址指的是这一行的首元素的地址
简单来说就是降级处理:
当二维数组的数组名没有单独放在sizeof内部—>表示的是二维数组首元素的地址(是第一行的地址),如果某一行的数组名并没有单独放到sizeof内部的时候—>数组名表示首元素的地址指的是这一行的首元素的地址
3.sizeof只关注类型,不会计算里面的表达式


总结

指针进阶(下)的内容就到这里啦,创作不易😭如果对友友们有帮助的话,记得点赞收藏博客🥰🥰🥰,关注后续的指针进阶笔试题详解题目篇哦~🐾

C语言进阶笔记深入了解进阶指针

目录

前言

指针进阶

字符指针

指向常量字符串的指针

指针数组

指针数组打印数组内容

数组指针

对数组指针的理解

&数组名和数组名

数组指针的使用

数组参数、指针参数

一维数组传参

二维数组传参

一级指针传参

二级指针传参

函数指针

阅读两段有趣的代码

代码1

代码2

函数指针数组

函数指针数组的用途:转移表(通过下标调用函数)

指向函数指针数组的指针

回调函数

qsort函数来排序整型数组

qsort函数排序结构体

模拟qsort函数


前言

指针进阶

我们在初级指针章节已经接触过了指针,我们知道了指针的概念:

1. 指针就是个变量,用来存放地址,地址唯一标识一块内存空间。

2. 指针的大小是固定的4/8个字节(32位平台/64位平台)。

3. 指针是有类型,指针的类型决定了指针的+-整数的步长,指针解引用操作的时候的权限。

4. 指针的运算。

在这里我们将继续探索指针的奥秘,掌握高级指针初识以及运用计算,本篇内容将详细介绍各种指针,内容较多,干货满满💪,相信大家会收获很多!!话不多说,进入正题:

字符指针

在指针的类型中我们知道有一种指针类型为字符指针 char* ,

一般使用:

int main()

    char ch = 'w';
    char *pc = &ch;
    *pc = 'w';
    return 0;

还有另外一种使用方式🙂:

int main()

    char* pstr = "hello bit.";//常量字符串
    char arr[]="hello bit";
    printf("%s",arr);//数组名表示字符串首地址,即“h”的地址,打印为hello bit;
    printf("%s\\n", pstr);//pstr存放首字符“h”的地址。打印为hello bit;
    return 0;

代码 char* pstr = "hello bit."; 大家特别容易以为是把字符串 hello bit 放到字符指针 pstr 里 了,但是本质上是把字符串 hello bit. 首字符的地址放到了pstr中。就像这样:

上面代码的意思是把一个常量字符串的首字符 h 的地址存放到指针变量 pstr 中。

了解了这个之后我们来看看一道题🙃:

指向常量字符串的指针

#include <stdio.h>
int main()

    char str1[] = "hello bit.";
    char str2[] = "hello bit.";
    char* str3 = "hello bit.";//常量字符串,不能改,那么两个字符肯定存在一个地址中,不过是用不同的指针指向首字符
    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)//字符串指针存放的是首字符的首地址,都指向'h'的地址
        printf("str3 and str4 are same\\n");
    else
        printf("str3 and str4 are not same\\n");

    return 0;

解析:

这里str3和str4指向的是一个同一个常量字符串。C/C++会把常量字符串存储到单独的一个内存区域, 当几个指针。指向同一个字符串的时候,他们实际会指向同一块内存。但是用相同的常量字符串去初始 化不同的数组的时候就会开辟出不同的内存块。所以str1和str2不同,str3和str4不同。

指针数组

在初阶指针中我们也提到了指针数组,指针数组是一个存放指针的数组。

int* arr1[10]; //整形指针的数组
char *arr2[4]; //一级字符指针的数组
char **arr3[5];//二级字符指针的数组

指针数组打印数组内容

#include<stdio.h>
int main()

    int a[5] =  1,2,3,4,5 ;
    int b[5] =  2,3,4,5,6 ;
    int c[5] =  3,4,5,6,7;
    int* arr[3] =  a,b,c ;//数组名表示首地址
    int i = 0;
    for (i = 0; i < 3; i++)//指的是三个一维数组
    
        int j = 0;
        for (j = 0; j < 5; j++)//每个一维数组的元素
        
            printf("%d ", *(arr[i] + j));//相当于arr[i][j];
        
        printf("\\n");
          
           
    

用图形来描述就是这样的:

好了,对于指针数组就是这样,比较简单,下面介绍一个比较重要的知识👇:

数组指针

数组指针是指针,我们已经熟悉:

整形指针: int * pint; 能够指向整形数据的指针。

浮点型指针: float * pf; 能够 指向浮点型数据的指针。

那数组指针应该是:能够指向数组的指针

其基本的结构就是:int (*p)[10];

下面我对于数组指针做出了一个解释🤏:

对数组指针的理解

p先和*结合,说明p是一个指针变量。
然后指着指向的是一个大小为10个整型的数组。

所以p是一个指针,指向一个数组,叫数组指针。

//这里要注意:[]的优先级要高于*号的,所以必须加上()来保证p先和*结合

如果我们要用一个指针来存放一个数组的地址,那么这个指针应该是数组指针指向整个数组的地址:

int arr[10]=1,2,3,4,5;
int (*parr)=&arr;//取出的是整个数组的地址,int是元素类型,(*parr)是数组指针

&数组名和数组名

对于下面的数组:

int arr[10];

arr和&arr分别是什么:
我们知道arr是数组名,数组名表示首元素的地址,那么&数组名是什么,我们可以通过代码来一步步深入研究:
 

#include <stdio.h>
int main()

    int arr[10] = 0;
    printf("%p\\n", arr);
    printf("%p\\n", &arr);
    return 0;

虽然两个地址一样,但是意义决然不同,我们还可以看这么一段代码:

#include <stdio.h>
int main()

 int arr[10] =  0 ;
 printf("arr = %p\\n", arr);
 printf("&arr= %p\\n", &arr);
 printf("arr+1 = %p\\n", arr+1);
 printf("&arr+1= %p\\n", &arr+1);
 return 0;

或者通过指针来访问:

#include<stdio.h>
int main()

	int arr[10] =  0 ;
	int* p1 = arr;//首元素地址
	int(*p2)[10] = &arr;//整个数组的地址
	printf("%p\\n", p1);
	printf("%p\\n", p1+1);//跳过一个整形,四个字节
	printf("%p\\n", p2);
	printf("%p\\n", p2+1);//跳过一个整形数组,40个字节


 

通过以上代码和运行结果可以知道:
其实&arr和arr,虽然值是一样的,但是意义应该不一样的。

&arr 表示的是数组的地址,而不是数组首元素的地址。 数组的地址+1,跳过整个数组的大小。

但是我们要记住有两个例外:

1.sizeof(数组名),数组名表示整个数组,计算的是整个数组的大小。

2.&数组名,数组名表示整个数组,取出的是整个数组的地址。

⚠️:这两个例外对于关于指针的解题很重要,所以我们一定要熟记。

数组指针的使用

既然数组指针指向的是数组,那数组指针中存放的应该是数组的地址。

数组指针的使用对于二维数组是比较多的,对于一维数组则比较少,我们先来看看他对于一维数组的使用:
 

#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>
int main()
  
    int arr[10]=1,2,3,4,5,6,7,8,9,10;
    int (*pa)[10]=&arr;
    int i=0;
    for(i=0;i<10;i++)
    
        printf("%d\\n",*(*pa)+i);
    
    return 0;

    

但是我们一般不会这样写,这样写看着别扭,可读性不高,而是这样用于二维数组:
 

#include<stdio.h>
void print(int(*p)[5], int r, int c)

	int i = 0;
	int j = 0;
	for (i = 0; i < r; i++)
	
		for (j = 0; j < c; j++)
		
			printf("%d ", *(*(p + i) + j));//p指向第一个一维数组的地址,p+i依次指向第二个第三个数组
										   //*(p+i)表示一维数组名,数组名表示首元素地址,所以*(p+i)表示每一行的首元素地址
										   //*(*(p+i)+j)表示每一行的元素
		
		printf("\\n");
	

int main()

	int arr[3][5] =  1,2,3,4,5,2,3,4,5,6,3,4,5,6,7 ;
	print(arr, 3, 5);//二维数组名表示第一行一维数组的地址;
	return 0;

对数组指针有了了解之后,我们可以来判断一下下面代码的意思:

int arr[5];
int *parr1[10];
int (*parr2)[10];
int (*parr3[10])[5];

▶️解析:
int arr[5]:整型数组

int *parr1[10]:整型指针数组

int (*parr2)[10]:数组指针

int (*parr3[10])[5]:parr3是一个存储指针的数组,该数组能够存放10个数组指针,每个数组指针能够指向一个数组,数组有5个元素,每个元素是int型;


数组参数、指针参数

在写代码的时候难免要把数组或者指针传给函数,那函数的参数该如何设计呢?

一维数组传参

#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);//数组名表示第一个指针的地址

二维数组传参

void test(int arr[3][5])//二维数组,ok

void test(int arr[][])//省略了列

void test(int arr[][5])//ok

void test(int *arr)//数组名表示首元素地址,二维数组数组名表示一维数组的地址

void test(int* arr[5])//指针数组

void test(int (*arr)[5])//数组指针,存放数组的地址,ok
void test(int **arr)//二级指针,表示指针的地址

int main()

 int arr[3][5] = 0;
 test(arr);//传的是一维数组的地址

✔️总结

二维数组传参,函数形参的设计只能省略第一个[]的数字。 //因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素,这样才方便运算。

一级指针传参

示例1️⃣:

#include <stdio.h>
void print(int *p, int sz)

 int i = 0;
 for(i=0; i<sz; i++)
 
 printf("%d\\n", *(p+i));//打印1到9
 

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;

示例2️⃣:

void test(char* p)

int main()

    char ch='w';
    char* str=&ch;
    test(&ch);//将ch的地址传参给一级指针
    test(str);//存放ch地址的指针

二级指针传参

示例1️⃣:

#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;

示例2️⃣:

void test(char **p)

 

int main()

 char c = 'b';
 char*pc = &c;
 char**ppc = &pc;
 char* arr[10];//存放一级指针的数组
 test(&pc);//传一级指针地址,ok
 test(ppc);//传二级指针,ok
 test(arr);//传存放一级指针的数组,数组名表示首元素地址,即第一个指针地址,所以用二级指针接收,ok

总结

  1. 如果一个函数的参数是一级指针,那么他可以接收 普通数组,变量地址,和一级指针;

  2. 如果一个函数的参数是二级指针,那么他可以接收 二级指针,一级指针地址,和 一级指针数组 ;

函数指针

我们已经学过存放普通变量地址的指针,也学过存放数组地址的指针,那么肯定有存放函数地址的指针。

指向整型变量的指针:

int a=10;
int* pa=&a;

指向字符变量的指针:

char ch='w';
char* pc=&ch;

指向数组的指针:

int arr[10]=0;
int(*parr)=&arr;

那么指向函数的指针应该是:

void test(char* str)

void (*pt)(char*)=&test;

对于函数指针,我们要能够正确判断:

函数指针的判断方法

就拿void (*pt)(char*)=&test来说,void指的是函数的返回类型,(*pt)表示指针,注意要有括号才能保证pt和*号结合,表示他是指针,而(char*)则是函数的参数类型。

了解了函数指针的结构我们是否可以用函数指针来代替函数名,也就是说通过函数指针来调用函数呢

首先我们来看这样一段代码:
 

#include<stdio.h>
int Add(int x, int y)

	return x + y;

int main()

	system("color f1");
	printf("%p\\n", &Add);
	printf("%p\\n", Add);

再看运行结果发现🙄:

输出的是竟然是两个一样的地址,这两个地址是 test 函数的地址。那函数的地址要想保存起来,怎么保存?

当然是用存放函数地址的函数指针保存,那么下面的哪个有能力存放函数的地址:

1.int(*pt1)(int,int)=&Add;

2.int* pt2(int,int)=&Add;

首先,能给存储地址,就要求pt1和pt2是指针,显然在表达式1中*号先和pt1结合,表示pt1是指针,后面括号中的两个int表示两个int类型的参数。*pt1前面的int表示函数的返回类型是int类型。

而pt2则不能达到这样的效果。

下面我们来看看一段这样的代码,将会发现很多不知道的秘密😲:

我们发现这三个打印出来的是一样的,那么为什么呢?
首先既然&Add和Add是一样的,那么int(*pf)(int,int)=&Add就可以写成int(*pf)(int,int)=Add;

对指针pf解引用的到Add,所以Add(3,5)和(*pf)(3,5)是等价的;

pf能存放Add的地址,所以Add(3,5)可以写成pf(3,5);

阅读两段有趣的代码

代码1

(*(void (*)())0)();

解读📖:

  1. void (*)():函数指针类型;
  2. (void(*)())0:对0进行强制转换,转换为指向函数的指针,解释为函数地址;
  3. *(void(*)())0:对0处的地址进行解引用得到函数;
  4. (*(void(*)())0)():用指针调用函数的模型,所以这是调用0地址处的函数;

代码2

void (*signal(int , void(*)(int)))(int);

解读📖:

  1. signal后面有(),signal没有和*号结合,而是和()结合,说明signal是函数;
  2. signal函数的第一个参数为int,第二个参数是函数指针类型,该函数指针指向一个参数为int,返回类型是void的函数;
  3. signal函数的返回类型也是一个函数指针类型,该函数指针指向一个参数为int,返回类型是void的函数;

整体来看这是一次函数声明,但是这样太复杂了,可以简化:

void(*)(int)signal(int,void(*)(int));

这样就很容易理解,void(*)(int)是函数返回类型,signal是函数名,(int,void(*)(int)是函数参数;

但是这样会报错,在vs编译器中会出现语法错误,不符合规定,所以会把函数的返回类型拆开到函数参数的后面,所以我们可以采用“typedef”对类型进行重定义后可以简化;

因为题中有两个void(*) (int),所以可以用typedef进行重定义:

typedef  void(*pfun_t)(int);//本来是typedef void(*)(int) pfun_t这样写的,但是不符合语法规定;

那么总结起来这个代码可以写成:

typedef void(*pfun_t)(int);
pfun_t signal(int,pfun_t);//pfun_t指的是void(*)(int);

函数指针数组

数组是一个存放相同类型数据的存储空间,那我们已经学习了指针数组, 比如:

int *arr[10];
//数组的每个元素是int*

那要把函数的地址存到一个数组中,那这个数组就叫函数指针数组,那函数指针的数组如何定义呢?

int (*parr1[10])();

parr1 先和 [] 结合,说明parr1是数组,数组的内容是 是 int (*)() 类型的函数指针。

那么函数指针数组有什么作用呢?

函数指针数组的用途:转移表(通过下标调用函数)

见代码:

#include<stdio.h>
int Add(int x, int y)

	return x + y;

int Sub(int x, int y)

	return x - y;

int main()

	int(*pf1)(int, int) = Add;
	int(*pf2)(int, int) = Sub;
	int(*pfArr[2])(int, int) =  Add,Sub ;//int(*)(int,int)就是一个函数指针类型,pfArr[2]就是一个函数指针数组;

我们可以用函数指针数组来实现一个简单的计算器,首先来看看没有我们之前写的普通版的计算器代码:

#include <stdio.h>
int add(int a, int b)

 return a + b;

int sub(int a, int b)

 return a - b;

int mul(int a, int b)

 return a*b;

int div(int a, int b)

 return a / b;

int main()

 int x, y;
 int input = 1;
    int ret = 0;
    do
   
        printf( "*************************\\n" );
        printf( " 1:add           2:sub \\n" );
        printf( " 3:mul           4:div \\n" );
        printf( "*************************\\n" );
        printf( "请选择:" );
        scanf( "%d", &input);
        switch (input)
       
        case 1:
              printf( "输入操作数:" );
              scanf( "%d %d", &x, &y);
              ret = add(x, y);
              printf( "ret = %d\\n", ret);
              break;
        case 2:
              printf( "输入操作数:" );
              scanf( "%d %d", &x, &y);
              ret = sub(x, y);
              printf( "ret = %d\\n", ret);
              break;
        case 3:
              printf( "输入操作数:" );
              scanf( "%d %d", &x, &y);
              ret = mul(x, y);
              printf( "ret = %d\\n", ret);
              break;
        case 4:
              printf( "输入操作数:" );
              scanf( "%d %d", &x, &y);
              ret = div(x, y);
              printf( "ret = %d\\n", ret);
              break;
        case 0:
                printf("退出程序\\n");
 breark;
        default:
              printf( "选择错误\\n" );
              break;
       
  while (input);
    
    return 0;

这样的话我们直接从case语句看不能一眼的看出是什么函数,而且代码量比较大,所以我们可以通过函数指针数组进行优化:

#include <stdio.h>
int add(int a, int b)

           return a + b;

int sub(int a, int b)

           return a - b;

int mul(int a, int b)

           return a*b;

int div(int a, int b)

           return a / b;

int main()

     int x, y;
     int input = 1;
     int ret = 0;
     int(*p[5])(int x, int y) =  0, add, sub, mul, div ; //转移表
     while (input)
     
          printf( "*************************\\n" );
          printf( " 1:add           2:sub \\n" );
          printf( " 3:mul           4:div \\n" );
          printf( "*************************\\n" );
          printf( "请选择:" );
      scanf( "%d", &input);
          if ((input <= 4 && input >= 1))
         
          printf( "输入操作数:" );
              scanf( "%d %d", &x, &y);
              ret = (*p[input])(x, y);//通过下标来调用函数
         
          else
               printf( "输入有误\\n" );
          printf( "ret = %d\\n", ret);
     
      return 0;

这个函数指针数组代替了switch case语句,让代码变得那个更加简单。


我们已经知道对于整型数组有指向整型数组的指针:

int arr[5];
int(*p1)[5]=&arr;

对于整型指针数组有指向整型指针数组的指针:

int *arr[5];
int*(*p2)[5]=&arr;

那么指向函数指针数组的指针应该怎么表示呢?

指向函数指针数组的指针

指向函数指针数组的指针是一个指针,指针指向一个数组 ,数组的元素都是函数指针。

int (*p)(int,int);//函数指针;
int (*p2[4](int,int);//函数指针数组;
int (*(*p3)[4])(int,int)=&p2;//p3就是指向函数指针数组的指针,存放函数指针数组的地址;

那么指向函数数组指针的定义应该是这样的:
 

void test(const char* str)

 printf("%s\\n", str);

int main()

 //函数指针pfun
 void (*pfun)(const char*) = test;

 //函数指针的数组pfunArr
 void (*pfunArr[5])(const char* str);
 pfunArr[0] = test;

 //指向函数指针数组pfunArr的指针ppfunArr
 void (*(*ppfunArr)[10])(const char*) = &pfunArr;
 return 0;

回调函数

回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一 个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该 函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或 条件进行响应。

qsort函数就是一个回调函数,这是一个快速排序函数,默认排序为升序,定义在头文件stdlib.h中,能排序各种类型的数据,而我们之前使用的冒泡排序函数则比较单一,在整个程序中只能排序同一种函数。

要想知道qsort函数的优点我们先来回顾一下冒泡排序函数的应用:
 

#include<stdio.h>
void print_arr(int arr[], int sz)

	int i = 0;
	for (i = 0; i < sz; i++)
	
		printf("%d ", arr[i]);
	

void bubble_sort(int arr[],int  sz)

	int i = 0;
	for (i = 0; i < sz - 1; i++)
	
		int j = 0;
		for (j = 0; j < sz - 1 - i; j++)
		
	
			if (arr[j] > arr[j + 1])
			
				int tmp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = tmp;
			
		
	

int main()

	int arr[10] =  9,8,7,6,5,4,3,2,1,0 ;
	int sz = sizeof(arr) / sizeof(arr[0]);
	print_arr(arr, sz);
	bubble_sort(arr, sz);
	print_arr(arr, sz);
	return 0;

这里采用qsort函数的话会更加简单,那么我们来了解一下qsort函数🔍:

这里的void都是不确定的类型,使用者可以根据实际情况来设置类型;

qsort函数来排序整型数组

#include <stdio.h>
#include<stdlib.h>
//qosrt函数的使用者得实现一个比较函数
int cmp_int(const void* p1, const void* p2)//由于要比较元素的类型不确定,所以要用不可修改的void的地址;

	return (*(int*)p1 - *(int*)p2);//强制转换为int*型,前者大于后者则返回大于0的数,前者小于后者返回小于0的数;

int main()

	int arr[] =  1, 3, 5, 7, 9, 2, 4, 6, 8, 0 ;
	int i = 0;
	qsort(arr, sizeof(arr) / sizeof(arr[0]), sizeof(int), cmp_int);
	for (i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
	
		printf("%d ", arr[i]);
	
	printf("\\n");
	return 0;

qsort函数排序结构体

#include<stdlib.h>
#include<stdio.h>
#include<string.h>
struct stu

	char name[20];
	int age;
;
int sort_by_age(const void* e1, const void* e2)

	return ((struct stu*)e1)->age - ((struct stu*)e2)->age;//强制转换为需要比较的两个结构体类型元素

int sort_by_name(const void* e1, const void* e2)

	return strcmp(((struct stu*)e1)->name, ((struct stu*)e2)->name);//strcmp函数比较字符串,返回的是字符的ASCII码值;

void test1()

	struct stu s[] =  "zhangsan",30,"lisi",34,"wangwu",28 ;
	int sz = sizeof(s) / sizeof(s[0]);
	//按年龄排序
	qsort(s, sz, sizeof(s[0]), sort_by_age);
	//按姓名排序
	qsort(s, sz, sizeof(s[0]), sort_by_name);
	
	

int main()

	
	test1();

当没有进行排序时:

当按年龄排序时:

当按姓名排序时:

这就是关于qsort函数对不同类型的排序,需要注意的是qsort函数默认排的序为升序,当需要降序排序时只需要交换比较函数语句里面两个成员的位置即可;


掌握了qsort函数的使用之后,我们需要模拟qsort函数的运用;

模拟qsort函数

/* qsort函数,头文件,<stdlib.h>
* 语法结构:void qort(void* base,size_t num,size_t size,int (*compare)(const void*,const void*))
* void* base存放的是待排序数据的第一个对象的地址
* size_t num指的是待排序对象的元素个数,传参时可以用sizeof求得
* size_t size指的是每个元素个数的字节大小
* int (*compare)(const void*,const void*))指的是比较两个元素的函数指针,存放比较函数的地址
* 这里的void*是指不确定的指针类型,便于程序员自己设计指针类型
*/
#include<stdio.h>
void print_arr(int arr[],int sz)//打印函数

	int i = 0;
	for (i = 0; i < sz; i++)
	
		printf("%d ", arr[i]);
    
	printf("\\n");


int cmp_int(const void* e1, const void* e2)//比较函数

	return *(int*)e1 - *(int*)e2;//比较函数,由于不知道要比较的是什么类型,所以可以强制转换为自己需要转换的类型
								//qsort规定,当结果大于0时返回大于0的数,小于0时返回小于0的数												
								//至于函数内部怎么根据这个来排序,怎么把比较元素的地址传给e1,e2的是库函数规定的

void Swap(char* buf1,char* buf2,int width)//当满足条件时交换函数

	int i = 0;
	for (i = 0; i < width; i++)
	
		char tmp =* buf1;
		*buf1 =* buf2;
		*buf2 = tmp;
		buf1++;
		buf2--;
	

//模拟的qsort函数
void my_qsort(void* base,int sz,int width,int(*cmp_int)(const void* e1,const void* e2))

	int i = 0;
	for (i = 0; i < sz; i++)
	
		int j = 0;
		for (j = 0; j < sz - 1 - i; j++)
		
			//base接收首元素地址的指针
			// j*width表示用下标或者说元素个数乘以一个元素的字节宽度就等于指针要跳过的大小
			//(char*)base + j * width和(char*)base + (j + 1) * width代表要比较的两个元素的地址
			//由于需要比较的元素类型多变,所以用char型指针+字节宽度就很通用
			if (cmp_int((char*)base + j * width, (char*)base + (j + 1) * width) > 0)
			
				Swap((char*)base + j * width, (char*)base + (j + 1) * width, width);
				//不知道交换什么类型就可以一个一个字节交换,所以要把字节宽度传参
			
		
	

int main()

	int arr[] =  9,8,7,6,5,4,3,2,1,0 ;
	int sz = sizeof(arr) / sizeof(arr[0]);
	print_arr(arr, sz);
	my_qsort(arr,sz,sizeof(arr[0]),cmp_int);//arr代表首元素地址
	print_arr(arr, sz);

 


好了,本篇内容就到这里,相信大家看完这篇文章,一定对指针有了更全面的了解。在这里我建议大家可以去像我一样去总结各种指针以及他的用法,这样我们一定会更上一层楼,一起加油啊💪💪,大家看完了,别忘了一键三连哦😉

以上是关于C语言指针进阶(下)的主要内容,如果未能解决你的问题,请参考以下文章

C 语言编程系列

C语言进阶之旅 (11.5)指针下 提升篇

C语言函数详解(入门到进阶)

C语言—指针进阶

C语言—指针进阶

C语言进阶学习笔记二指针的进阶(练习篇)