在上篇内容中,我们学习了关于指针变量、野指针和部分指针运算的知识,对于指针有了一个初步的了解。本篇文章中将延续上文展开剩余篇幅。
1.指针的关系运算
指针的关系运算实际上就是指针比较大小(地址比较大小)
当我们想要打印一个数组的内容的时候,通常会这么做
#include <stdio.h>
int main()
{
int arr[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
int sz = sizeof(arr) / sizeof(arr[0]);
for (int i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
但是当我们学了指针的关系运算之后,我们就可以将指针的比较作为循环条件来打印数组
#include <stdio.h>
int main()
{
int arr[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
int sz = sizeof(arr) / sizeof(arr[0]);
int *p = &arr[0];
while (p < arr + sz)
{
printf("%d ", *p);
p++;
}
return 0;
}
2.const修饰指针
2.1 const修饰变量
我们知道,变量是可以被修改的。但是有时候在程序中我们并不希望创建的变量被修改,此时就可以用const修饰这个变量

上图中,变量x没有被const修饰,所以可以后续进行修改数值;变量y被const修饰后,再对它进行修改就会报错。
虽然我们无法修改被const修改的变量y,但是const仅仅是在语法上做了限制,y的本质还是变量,所以习惯上,我们称呼y为常变量。
但是尽管const修饰了变量y,我们还是可以通过指针使用y的地址去修改y的数值

2.2 const修饰指针变量
接上文,如果我们要想避免别人通过地址来修改被const修饰的变量的数值,就要用const将指针变量也修饰了
此时*p被const修饰后,再想通过地址修改变量y的值就会报错
另外的,const可以放在 * 的前面,也可以放在 * 的后面
1.const int *p
2.int const *p
3.int * const p
1和2中,const都在 * 的左边,此时修饰的是*p,也就是此时我们不能使用*p修改其指向空间的内容,但是我们仍然可以改变*p指向的空间

3中const在 * 的右边,此时修饰的是p,也就是此时我们不能改变*p指向的空间了,但是仍然可以使用*p修改其指向空间的内容

有点绕,但是不难理解。
3.assert断言
C语言中有宏assert(),被称为断言。我们可以在括号中输入一个表达式作为参数,若表达式的结果为真,程序继续执行,结果为假则报错并终止运行。
例如在上面const的讲解中,假设const只修饰了变量y,我们希望避免误操作导致使用了地址修改其数值,此时就可以使用assert()宏,使用之前记得包含<assert.h>头文件。
我们将y初始化为0并且使用const修改后,不希望后续操作中修改其数值,就使用assert(y==0)来验证变量y的数值是否被修改,此时再使用地址来修改其数值就会报错
assert()的使用对程序员是十分友好的,它不仅能给出出错的行号,同时当我们已经确认程序无误后,不需要再做断言,我们就可以在头文件#include <assert.h>前面定义一个宏NDEBUG
#define NDEBUG
#include <assert.h>
4.传值调用和传址调用
先举个例子,我们写一个Add函数实现整型相加
#include <stdio.h>
int Add(int x, int y)
{
return x + y;
}
int main()
{
int a = 2;
int b = 3;
int ret = Add(a, b);
return 0;
}
此处,我们将a和b的值传到了Add函数中,此时为传值调用,Add的形参和实参占用的是不一样的空间
在一些情况中,我们使用传值调用就可以解决问题,但是当我们在解决一些交换变量内容之类的问题的时候,传值调用就没有办法得到我们想要的效果,例如:
这是因为,传值调用中,实参传递给形参的时候,形参会创建一块临时空间来存放实参的值,所以此时形参和实参并不位于同一块空间,对形参的修改也影响不到实参
要解决这个问题,我们只需要把地址作为参数传入函数中
就顺利实现了我们想要的效果,这种方式就叫传址调用。
如果我们只需要使用变量值来进行计算,可以使用传值调用;但是当我们要在函数中修改实参的内容时,就要使用传址调用了
5.数组名和指针访问数组
5.1 数组名的理解
实际上,数组名就是数组首元素的地址。验证如下:
#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 = %p\n", arr);
return 0;
}
我们创建一个数组,然后分别取arr和&arr[0]并打印地址,接着我们会发现二者完全一样

但是例外的,当我们运行这段代码时
#include <stdio.h>
int main()
{
int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
printf("%d",sizeof(arr));
return 0;
}
输出的结果是40,也就是整个数组的大小,但是arr不是首元素地址吗?输出的应该是4或8才对
事实上数组名的理解有两个例外:
- sizeof内单独存放数组名时,此时的数组名表示整个数组,而不是首元素的地址,所以计算的是整个数组的大小
- 当我们&数组名的时候(&arr),此时取出的不是首元素地址,而是整个数组的地址,+1的话也是跳过整个数组而不是跳过一个元素
除此之外,其他任何地方在使用数组名的时候都表示首元素地址
5.2 指针访问数组
在学习指针之前,我们访问数组的内容通常是这样的
#include <stdio.h>
int main()
{
int arr[10] = { 0,1,2,3,4,5,6,7,8,9 };
int sz = sizeof(arr) / sizeof(arr[0]);
for (int i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
学习指针之后,我们就可以用这种方式
#include <stdio.h>
int main()
{
int arr[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
int sz = sizeof(arr) / sizeof(arr[0]);
int *p = arr;
for (int i = 0; i < sz; i++)
{
printf("%d ", *(p + i));
}
return 0;
}
我们会发现,现在arr和p中存放的都是首元素的地址,所以arr和p是等价的,那么我们是否可以把arr[i]换成p[i]呢

完全没问题。而且我们也可以将*(p+i)替换成*(arr+i)

也就是说,arr[i]和*(arr+i)其实是等价的,按照交换律
* ( arr + i ) == * ( i + arr )
∴ arr [ i ] == i [ arr ]

这种方法虽然可以,但是并不推荐这么做。平时我们还是最好按照习惯去写
6.一维数组作为函数参数
我们知道,数组也可以作为参数传递给函数。但是实际上传参的时候并不是传递一整个数组,而是传递数组的首元素地址。
例如我们分别在函数外和函数内分别求一个数组的元素个数

因为只是把数组的首元素地址传入了函数中,所以无法得到正确的数组元素个数。另外,函数的形参既可以写成上图中数组的形式,也可以写成指针的形式
7.二级指针
我们知道,指针变量内部存放了变量的地址,但是指针变量本身也是个变量,也有自己的地址,那么:指针变量的地址该存到哪里呢?
这里就引入二级指针的概念了
#include <stdio.h>
int main()
{
int a = 10;
int *pa = &a;
int **ppa = &pa;
return 0;
}
二级指针的一个显著标志就是有两颗 *,后一个 * 说明ppa是指针变量,前一个 * 和 int 组合说明ppa指向的类型是 int * 类型的变量。

(地址随便编的)
对二级指针解引用就像剥洋葱一样,一层解完解下一层
*ppa = pa
*pa = a
8.指针数组和数组指针
8.1 指针数组
学习指针数组的时候很多人都容易混淆:指针数组是数组还是指针?
实际上很容易区分,就像整型数组是存放整型的数组、字符数组是存放字符的数组一样:指针数组就是存放指针的数组。
指针数组的每个元素都是一个指针,也就是一个地址,分别指向不同的区域。
这么说来,前面提到数组名就是数组首元素地址,那么我们是否能使用指针数组模拟二维数组呢?
8.2 指针数组模拟二维数组
#include <stdio.h>
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("\n");
}
return 0;
}
上面这段代码中,arr是一个指针数组,操作符[ ]的优先级比 * 高,所以arr先和[ ]结合,代表arr是一个数组,arr前面的int *说明了数组内的元素类型

虽然我们可以使用指针数组模拟二维数组,但是二维数组中的元素都是连续的,而指针数组中的元素并不连续。
8.3 数组指针变量
同上,数组指针变量就是指向数组的指针变量,存放了数组的地址,实质上是一个指针变量
那么问题来了,你能认得出哪个是指针数组哪个是数组指针吗
int *p [10]
int (*p)[10]
上面刚提到,操作符[ ] 的优先级比 * 高,要创建一个数组指针变量,就要用括号将*和指针变量名括起来,变量名先和*结合说明是一个指针变量,否则就会先和[ ]结合变成一个数组,所以第一个是指针数组,第二个是数组指针变量
现在我们知道了数组指针变量是用来存放数组的地址的,现在试试使用&操作符将数组的地址存到数组指针中吧
数组指针的类型很好区分,我们去掉指针变量名,剩下的就是数组指针的类型:
type (*p ) [ ]
<type>是数组指针指向的数组的元素类型,方括号中的数字就是数组指针指向的数组的元素个数
9.二维数组传参的本质
前面我们已经使用指针数组模拟过二维数组了,实际上,二维数组传参的本质和其十分相似。
二维数组,可以看作每个元素都是一个一维数组的数组,前面我们在学习中也知道了数组名就是数组的首元素地址。所以二维数组传参的时候实际上就是传递了首元素的地址——也就是第一个一维数组的地址。
那么我们用二维数组名加减整数,就可以访问其内部的一维数组;再对其内部一维数组的地址加减整数,就可以访问一维数组中的每一个元素。
通过这两层关系,我们就可以实现访问二维数组的每一个元素。
10.字符指针变量
我们知道指针变量的类型有int *,char *等等,而类型为char *的字符指针变量也有一些知识需要我们了解
平时我们可能很少使用到char *类型的指针变量,顶多可能偶尔会写到这样的代码
#include <stdio.h>
int main()
{
char ch = 'w';
char *pc = &ch;
return 0;
}
很简单,很表层,但是字符指针变量并不仅限于这点功能
还有一种使用方法如下:
#include <stdio.h>
int main()
{
char *pc = "hello";
printf("%s\n", pc);
return 0;
}
可能有些同学会以为“hello”这一整个字符串都被放在字符指针中了,实际上,当我们想将一个字符串存到字符指针中时,其实只是把字符串首字符的地址放在了这个字符指针中
也就是说,pc此时指向的是h的位置
《剑指offer》中有一道和字符指针、字符串相关的笔试题,通过这道题我们再对字符指针有一个深入的了解
#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;
}
运行这段代码,输出如下

当我们使用相同的字符串去初始化两个数组的时候,这两个数组会开辟出不同的空间;而当几个不同的指针指向同一个字符串的时候,实际上这些指针指向了同一块内存。
11.函数指针变量
11.1 函数指针变量的理解
根据之前学习不同指针变量的经验,我们不难理解:函数指针变量就是存放函数的地址的指针变量
难道函数也有地址吗?我们可以测试一下
#include <stdio.h>
void test()
{
printf("hello");
}
int main()
{
printf("%p\n", test);
printf("%p\n", &test);
return 0;
}
随便创建一个函数,我们尝试打印它的地址,输出结果如下
确实打印出来了地址,而且我们发现直接用函数名和&函数名都可以获得函数的地址
说明:函数名就是函数的地址
知道了怎么取出函数的地址,我们就要知道怎么存放函数地址。函数指针变量的指针类型长得其实和数组指针非常相似:
type (*p ) ( )
其中,<type>是函数指针指向的函数的返回类型,*后面跟着函数指针变量名,第二个括号内填入函数指针指向的函数的不同参数类型
例如:
#include <stdio.h>
void test()
{
printf("hello");
}
int Add(int x,int y)
{
return x + y;
}
int main()
{
void (*p1)() = test;
int (*p2)(int, int) = Add; //第二个括号中可以只写类型不写参数名
return 0;
}
11.2 函数指针变量的使用
我们知道,平时调用函数的时候一般只会写函数名,而函数名也是函数的地址,我们将函数地址存到指针变量中后,指针变量名是否就和函数名等价了呢?
#include <stdio.h>
int Add(int x,int y)
{
return x + y;
}
int main()
{
int (*p)(int, int) = Add;
printf("%d\n", (*p)(2, 3));
printf("%d\n", p(3, 5));
return 0;
}
事实上,不管是否对函数指针解引用,我们都可以调用函数指针指向的函数
输出结果:

12.typedef关键字
typedef 可以帮助我们将一些复杂的类型重命名
例如我觉得 unsigned int 这个类型太长了,写代码不方便,能不能改简单一点呢?
我们只需要:
typedef unsigned int uint;
上面,我们就使用了 typedef 将 unsigned int 重命名为了 uint ,在后续的代码中我们只需要使用 uint 就可以实现 unsigned int 的功能了
当然,指针变量也能修改
typedef int* pty_t;
但是对于数组指针和函数指针,修改的方式就有所不同了,我们要在第一个括号内部修改新类型名
typedef int(*parr_t)[10];
typedef void(*pfun_t)(int,int);
13.函数指针数组和转移表
13.1 函数指针数组
存放函数的地址的数组就是函数指针数组,我们已经学习了指针数组和函数指针,那么你知道函数指针数组该怎么定义吗?
type (*parr [ ]) ( )
其中,parr先和 [ ] 结合,说明是一个数组,其内部元素类型是 type(* )( )
13.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("****** 0:exit ******\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");
break;
default:
printf("选择错误\n");
break;
}
} while (input);
return 0;
}
会发现,其中有很多段代码是重复的,太长了且太冗余
当我们使用函数指针数组后,就可以省去switch中的大段重复代码
#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}; // 转移表
do
{
printf("*************************\n");
printf("****** 1:add 2:sub ******\n");
printf("****** 3:mul 4:div ******\n");
printf("****** 0:exit ******\n");
printf("*************************\n");
printf("请选择:");
scanf("%d", &input);
if ((input <= 4 && input >= 1))
{
printf("输⼊操作数:");
scanf("%d %d", &x, &y);
ret = (*p[input])(x, y);
printf("ret = %d\n", ret);
}
else if (input == 0)
{
printf("退出计算器\n");
}
else
{
printf("输⼊错误\n");
}
} while (input);
return 0;
}
上面这段代码中的函数指针数组就是转移表。转移表是一种数据结构,主要用于存储预先计算的结果,以便在需要时进行快速查找。
14.回调函数
回调函数就是通过函数指针调用的函数
例如上面的实现计算机功能中有这么一段代码
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");
break;
default:
printf("选择错误\n");
break;
}
我们可以写一个函数来避免重复的代码,将上面的add、sub、mul、div函数作为参数传入
void calc(int (*pf)(int, int))
{
int ret = 0;
int x, y;
printf("输⼊操作数:");
scanf("%d %d", &x, &y);
ret = pf(x, y); //通过指针调用函数
printf("ret = %d\n", ret);
}
此时pf被用来调用其指向的函数,被调用的函数就是回调函数
优化后的代码:
switch (input)
{
case 1:
calc(add);
break;
case 2:
calc(sub);
break;
case 3:
calc(mul);
break;
case 4:
calc(div);
break;
case 0:
printf("退出程序\n");
break;
default:
printf("选择错误\n");
break;
}
明显更加简洁,避免了代码的重复
完.
3009

被折叠的 条评论
为什么被折叠?



