往期文章
- C语言:初识C语言
- C语言:分支语句和循环语句
- C语言:函数
- C语言:数组
- C语言:操作符详解
目录
- 往期文章
- 前言
- 1. 指针是什么
- 2. 指针和指针类型
- 3. 野指针
- 4. 指针运算
- 4.1 指针+-整数
- 4.2 指针-指针
- 4.3 指针的关系运算
- 5. 二级指针
- 6. 字符指针
- 7. 指针数组
- 8. 数组指针
- 9. 数组参数、指针参数
- 9.1 一维数组
- 9.2 二维数组
- 9.3 一级指针
- 9.4 二级指针
- 10. 函数指针
- 11. 函数指针数组
- 12. 指向函数指针数组的指针
- 13. 回调函数
- 后记
前言
大家好,文章已经更新了五篇了,今天我们来更新第六篇指针。遥记得当年学指针的痛苦,希望这次重新学习能够有别样收获。本博主之前也写过两篇关于指针的文章,也比较详细,不过无论排版还是内容上还是有一些遗憾,希望这篇博客可以弥补之前文章中的遗憾。也欢迎大家点击阅读之前博主关于指针的文章:安得指针千万间大庇天下指针具欢颜上和
安得指针千万间大庇天下地址具欢颜中
1. 指针是什么
在计算机科学中,指针(Pointer)是编程语言中的一个对象,利用地址,它的值直接指向
(points to)存在电脑存储器中另一个地方的值。由于通过地址能找到所需的变量单元,可以
说,地址指向该变量单元。因此,将地址形象化的称为“指针”。意思是通过它能找到以它为地址
的内存单元。
计算机是有32位和64位之分的。所谓32位就是有32位地址线,64位就是有64位地址线。每根地址线有正负电之分,我们可以对应为0和1来表示,也就是二进制来表示。
我们以32位的计算机为例,它的表示范围为00000000000000000000000000000000到11111111111111111111111111111111这之中的每一个编号都可以作为一个地址,也是一个字节。
指针变量是用来存放地址的。
#include <stdio.h>
int main()
{
int a = 10;//在内存中开辟一块空间
int *p = &a;//这里我们对变量a,取出它的地址,可以使用&操作符。
//将a的地址存放在p变量中,p就是一个之指针变量。
return 0;
}
指针指向的是首地址。
2. 指针和指针类型
虽然指针大小相同,但是指针类型仍然有意义。
定义指针的类型,目的是决定指针访问几个字节。
指针类型不仅可以决定指针访问几个字节,也可以决定指针加一后,跳过几个字节。
3. 野指针
野指针成因:
1.指针未初始化
2.指针越界访问
3.指针指向的空间释放
如何避免野指针
- 指针初始化
- 小心指针越界
- 指针指向空间释放即使置NULL
- 指针使用之前检查有效性
4. 指针运算
4.1 指针±整数
#include<stdio.h>
int main()
{
int arr[5] = { 0 };
int *p = arr;
int i = 0;
for (int i = 0; i < 5; i++)
{
*(p + i) = i;
}
for (int i = 0; i < 5; i++)
{
printf("%d ", *(p + i));
}
return 0;
}
#include<stdio.h>
int main()
{
int arr[5] = { 0 };
int *p = arr;
int i = 0;
for (int i = 0; i < 5; i++)
{
*p++ = i;
}
p = arr;
for (int i = 0; i < 5; i++)
{
printf("%d ", *(p + i));
}
return 0;
}
指针与整数的加减法,有时候可以产生新的地址,有时候可以改变指针指向的地址。
4.2 指针-指针
#include<stdio.h>
int main()
{
int arr[10] = { 0 };
printf("%d\n", &arr[9] - &arr[0]);
return 0;
}
指针-指针得到的值的绝对值是指针之间的元素个数。我们这里用的是绝对值,因为我们用arr0-arr9会得到同样的结果-9.
注意,指针减指针的前提是两个指针指向的是同一块连续的空间。
4.3 指针的关系运算
也就是说p是可以和R进行比较滴,但不可以和L进行比较。
5. 二级指针
指针变量也是变量,是变量就有地址,那指针变量的地址存放在哪里? 这就是 二级指针。
6. 字符指针
有一种指针类型为字符指针,我们通常这样写:
int main()
{
char ch = 'w';
char *pc = &ch;
*pc = 'w';
return 0;
}
但有些时候,我们会遇到这样的写法:
int main()
{
char* pstr = "hello.";//这里是把一个字符串放到pstr指针变量里了吗?
printf("%s\n", pstr);
return 0;
}
上面代码的意思是把一个常量字符串的首字符 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)
printf("str3 and str4 are same\n");
else
printf("str3 and str4 are not same\n");
return 0;
}
这段代码的执行结果是什么呢?
7. 指针数组
int* arr1[10]; //整形指针的数组
char *arr2[4]; //一级字符指针的数组
char **arr3[5];//二级字符指针的数组
存放指针的数组为指针数组。
#include<stdio.h>
int main()
{
int a=0;
int b=5;
int c=10;
int *arr[3] = { &a, &b, &c };//指针数组
return 0;
}
8. 数组指针
数组指针本质上是指针。能够指向数组的指针。
int (*p)[10];
//解释:p先和*结合,说明p是一个指针变量,然后指着指向的是一个大小为10个整型的数组。所以p是一个
指针,指向一个数组,叫数组指针。
//这里要注意:[]的优先级要高于*号的,所以必须加上()来保证p先和*结合
我们知道数组名是数组的首地址,那么&数组名又是什么呢?
#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;
}
注意:除了sizeof(数组名)和&数组名中数组名代表整个数组之外,其他时候数组名都代表数组首元素的地址。
数组指针的使用:
#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,其实相当于第一行的地址,是一维数组的地址
//可以数组指针来接收
printf("*****************************\n");
print_arr2(arr, 3, 5);
return 0;
}
我们再来复习一下数组指针和指针数组:
9. 数组参数、指针参数
在写代码的时候难免要把【数组】或者【指针】传给函数,那函数的参数该如何设计呢?
9.1 一维数组
对于一维数组,这些传参方式都是可以的:
#include <stdio.h>
void test(int arr[])
{}
void test(int arr[10])
{}
void test(int *arr)
{}
void test2(int *arr[20])
{}
void test2(int **arr)
{}
int main()
{
int arr[10] = { 0 };
int *arr2[20] = { 0 };
test(arr);
test2(arr2);
}
9.2 二维数组
对于二维数组,这些传参方式都是可以的:
#include<stdio.h>
void test(int arr[3][5])
{}
void test(int arr[][5])
{}
void test(int(*arr)[5])//ok?
{}
int main()
{
int arr[3][5] = { 0 };
test(arr);
}
总结:二维数组传参,函数形参的设计只能省略第一个[]的数字。因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。这样才方便运算。
9.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;
}
思考:当一个函数的参数部分为整型一级指针的时候,函数能接收什么参数?
- 一个整型参数的地址
- 一个整型指针
- 一个整型数组的数组名
9.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;
}
思考:当一个函数的参数部分为整型二级指针的时候,函数能接收什么参数?
- 取地址一级指针
- 二级指针
- 每个元素是int*的数组名
10. 函数指针
先来看一段代码:
#include <stdio.h>
void test()
{
printf("hehe\n");
}
int main()
{
printf("%p\n", test);
printf("%p\n", &test);
return 0;
}
输出的是两个地址,这两个地址是 test 函数的地址。函数指针,就是指向函数的指针。
函数指针的使用:
解读两个复杂一些的函数指针:
我们可以将代码2简化一下:
这样就好懂一些了。
11. 函数指针数组
要把函数的地址存到一个数组中,那这个数组就叫函数指针数组,那函数指针的数组如何定义呢?
int (*parr1[10]])();
parr1 先和 [] 结合,说明parr1是数组,数组的内容是什么呢? 是 int (*)() 类型的函数指针。
函数指针数组的用途:转移表。这个作用是怎么体现的呢?我们来举一个例子,加入我们要写一个计算器的程序,根据我们之前学过的知识,我们写出的代码是这样的:
#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");
break;
default:
printf("选择错误\n");
break;
}
} while (input);
return 0;
}
我们发现,这些加减乘除除了名字不一样,参数啊,返回类型啊都是一样的。我们却还要一个个去调用,有没有什么方法可以简化一下呢,我们看下面的代码:
#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;
}
这样我们的代码就得到了极大的简化。
12. 指向函数指针数组的指针
指向函数指针数组的指针是一个 指针 ,指针指向一个 数组 ,数组的元素都是 函数指针。
定义使用方法如下:
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;
}
13. 回调函数
回调函数就是一个通过函数指针调用的函数,如果你把函数指针作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数,回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另一方调用的,用于对该事件或条件进行响应。
举一个例子:
我们来看上面的那个程序里的test1,它的参数放的是test2,而test2函数里的参数是一个函数指针,在这个程序里,我们并没有直接调用test1,而是把test1的地址传递给了另一个函数test2,然后我们通过p反过来去调用了test1函数,我们称test1函数为回调函数。这个程序可以很好地演示回调函数,我们再来用计算器的程序来演示一下:
#include<stdio.h>
void menu()
{
printf("欢迎使用计算器>>\n");
printf("*******************************\n");
printf("***** 1.add *******\n");
printf("***** 2.sub *******\n");
printf("***** 3.mul *******\n");
printf("***** 4.div *******\n");
printf("***** 0.exit *******\n");
printf("*******************************\n");
}
int add(int x, int y)
{
return x + y;
}
int sub(int x, int y)
{
return x - y;
}
int mul(int x, int y)
{
return x*y;
}
int div(int x, int y)
{
return x / y;
}
void cal(int(*p)(int,int))
{
int a = 0;
int b = 0;
int ret = 0;
printf("请输入两个操作数>>\n");
scanf("%d %d", &a, &b);
ret = p(a, b);
printf("ret=%d\n", ret);
}
int main()
{
int input = 0;
do
{
menu();
printf("请选择>>\n");
scanf("%d", &input);
switch (input)
{
case 1:
cal(add);
break;
case 2:
cal(sub);
break;
case 3:
cal(mul);
break;
case 4:
cal(div);
break;
case 0:
printf("退出计算器>>\n");
break;
default:
printf("选择错误>>\n");
break;
}
} while (input);
return 0;
}
我们增加了cal函数,我们在程序中,将加减乘除的函数名作为cal的参数,从而避免了使用多次重复打相同的代码。
后记
好的,这篇万字长文到这里就结束啦,这绝对是一篇诚意满满的博客,希望对大家有所帮助。感谢大家的关注和支持,我们下一篇博客再见。