目录
一、什么是指针
1.内存
程序的运行需要储存信息,而信息储存在内存中,我们为了有效地使用内存,就需要将内存划分为一个个小的内存单元,每一个单元的大小是一个字节。(一个字节比较合理,这个内存单元太小也不好,太大也不好)为了能够有效地使用每个内存单元,我们给每一个单元都定了一个编号,这个编号就叫做这个内存单元的地址。假如内存是一幢楼房,就像楼中的门牌号,通过门牌号我们可以找到对应的房间。同样在计算机中使用这样的方式,我们可以轻松地找到对应的字节位置,而不需要一个一个去比对。
内存 | 编号(取十六进制数) |
1byte | 0x00000001(十进制:1) |
1byte | 0x00000002(十进制:2) |
1byte | 0x00000003(十进制:3) |
1byte | 0x00000004(十进制:4) |
1byte | 0x00000005(十进制:5) |
1byte | 0x00000006(十进制:6) |
1byte | ...... |
2.地址的生成
我们的电脑中都有硬件电路,用于生成地址的电线叫地址线。当电路中有电路通过时,会产生正负脉冲,从而表示0与1.此处我们以三十二位电脑为例,它在生成地址时32根地址线同时产生电信号表示1或0,当每一个地址线组合起来时就有了许许多多的不同的排列组合方式
我可以三十二个全为0:00000000000000000000000000000000——对应0
我也可以前三十一个全为0:00000000000000000000000000000001——对应1
......
这样的排序方式一共有2的32次方种,也就是说它有是4294967296种排序,内存中就一共有这么多个字节的空间。
但是这个数字不是很直观,我们先对它除以1024得到4194304个KB,再除1024得到4096个MB,再除以1024得到4GB,也就是说在早期的三十二位电脑内存中一共有4GB的内存空间。
3.数据的储存
这里我们打开VS2022,输入以下代码,可以通过调试找到a的地址
#include <stdio.h>
int main()
{
int a = 10;
&a;//取出num的地址,&为取地址符号
//这里的num共有4个字节,每个字节都有地址,但我们取出的是第一个字节的地址(较小的地址)
printf("%p\n", a);//打印地址,%p是以地址的形式打印
return 0;
}
具象化的话,就用以下内容表示:
内存 | 编号 |
1byte | 0xFFFFFFFF |
1byte | 0xFFFFFFFE |
1byte | ...... |
a | 0x0012FF47 |
0x0012FF46 | |
0x0012FF45 | |
0x0012FF44 | |
1byte | ...... |
1byte | 0x0000002 |
1byte | 0x0000001 |
我们实际上取出的只有0x0012FF47这个地址
我们在VS上的内存窗口上可以看到三行,包含以下内容:
地址 | 内存中的数据(补码) | 内存数据的文本解析 |
0x0012FF47 | a0 00 00 00(小端存储) | ????(内容不定,没什么用处) |
我们a的值为10,用二进制表示即为:0000 0000 0000 0000 0000 0000 0000 1010(二进制的数字表达最后一位表示2的0次方,倒数第二位就表示2的1次方,以此类推,十就是2的3次方加2的一次方也就是1010),在这个时候我们以每个四位为一组,就可以得到数据的表示方法:00 00 00 0a(在16进制数中,a表示10,b表示11,c表示12,d表示13,e表示14,f表示15)
0000 0000 0000 0000 0000 0000 0000 1010
0 0 0 0 0 0 0 a
4、指针变量
请看以下代码:
#include<stdio.h>
int main()
{
int a = 10;
int* p = &a;
//我们把a这个变量的地址储存在这个变量p中,这个p就叫做指针变量,类型为int*
return 0;
}
编号代表地址,而地址也可以成为指针,有一定的指向作用,所以叫做指针变量。简单地说指针就是地址。
那我们如何理解这个int* p = &a;呢?
(1)中间的*表示p是个指针变量,注意指针变量是p,而不是*p
(2)int表示指针指向的对象是整形
(3)p为指针变量,接受&a的内容,也就是变量的首地址
在了解这些后,我们也可以创建其它类型的指针变量,比如:
#include <stdio.h>
int main()
{
char ch = 'w';
char* pc = &ch;//字符型变量的指针
*pc = 'q';
printf("%c\n", ch);
return 0;
}
//输出:q
5.解引用操作符*
int main()
{
int a = 10;
int* p = &a;
printf("%d", *p);//这里的*p表示对指针变量p解引用,找到其对应的内容
return 0;
}
//输出:10
注意:int*p = &a;中的*不表示解引用,通过解引用符号,我们可以轻易地找到指针地址的对应值
6.指针变量的大小
#include <stdio.h>
int main()
{
printf("%d\n", sizeof(char *));
printf("%d\n", sizeof(short *));
printf("%d\n", sizeof(int *));
printf("%d\n", sizeof(double *));
return 0;
}
你可能认为输出结果是:1 2 4 8
但实际上是:4\8 4\8 4\8 4\8(4或8)
原因:指针变量储存的是地址,也就是说指针变量的大小取决于存放一个地址需要的空间的大小,32位平台下地址是32个bit位(即4个字节),而64位平台下地址是64个bit位(即8个字节),所以指针变量的大小就是4或8.
结论:指针大小在32位平台是4个字节,64位平台是8个字节。
二、指针变量类型
1.指针加减整数
#include <stdio.h>
int main()
{
int n = 10;
char *pc = (char*)&n;
int *pi = &n;
printf("%p\n", &n);
printf("%p\n", pc);
printf("%p\n", pc+1);
printf("%p\n", pi);
printf("%p\n", pi+1);
return 0;
}
//结果:
//007FFC20
//007FFC20
//007FFC21
//007FFC20
//007FFC24
在这里我们可以看到,不管打印int*还是char*指针对应的地址,它们的结果都是一样的。
那么我们为什么不能将所有的指针统一为一个数据类型呢?
然而,当我们观察两种类型的指针加一后的地址时,不难发现,int*类型的地址向后移动了4字节,char*类型的地址向后移动了1字节.
(1)总结:指针的类型决定了指针向前或者向后走一步有多大
2.指针的解引用
#include <stdio.h>
int main()
{
int n = 0x11223344;
char* pc = (char*)&n;
int* pi = &n;
*pc = 0;
printf("%x\n", n);
*pi = 0;
printf("%x\n", n);
return 0;
}
//结果:
//11223300
//0
在内存中,0x11223344这个数字以小端字节序存储(44 33 22 11),先用char* 的指针解引用只能访问一个字节,所以会把第一个字节改为0,也就会打印11223300;而 int* 的指针解引用能访问四个字节,所以会把第四个字节都改为0,也就会打印0
(2)总结:指针的类型决定了,对指针解引用的时候有多大的权限(能操作几个字节)。
三、野指针
野指针就是指针指向的位置是不可知的,就像一条没有拴住的恶犬,接近它是会受伤的。
1.野指针的成因
(1)指针没有初始化
#include<stdio.h>
int main()
{
int* p;//没有初始化
*p = 20;//不清楚地址在哪里,为野指针
return 0;
}
(2)指针的越界访问
#include<stdio.h>
int main()
{
int arr[10]={1,2,3,4,5,6,7,8,9,10};
int i = 0;
int* p = arr;
for(i=0; i<=10; i++)
{
printf("%d ",*(p+i));
//可以读取到arr[10],越界访问,为野指针
}
return 0;
}
(3)指针指向的空间被释放
#include<stdio.h>
int* add(int x,int y)
{
int d = x+y;
int* p = &d;
return p;//返回和的地址
}
int main()
{
int a = 10;
int b = 20;
int* c = add(a,b);
//因为退出了函数,原来p指针这个地址也就不在程序的作用域内了,c就成为了野指针
printf("%d ",*c);
//这个解引用虽然数值是正确的,但由于这个空间不在程序的作用域内,
//我们无法确定是否会有其他的操作会改变它的值
return 0;
}
2.避免野指针的方法
(1)指针初始化
(2)小心指针越界
(3)指针指向空间释放即使置NULL
(4)避免返回局部变量的地址
(5)指针使用之前检查有效性
可以在使用指针时加上下面的代码:
#include <stdio.h>
int main()
{
int a = 10;
p = &a;
//使用的指针一定要有准确的地址
int *p = NULL;
//不用的指针记得置空
if(p != NULL)
{
*p = 20;
}
//空指针不能解引用,加上判断
return 0;
}
四、指针运算
1.指针加减整数
#define N_VALUES 5
float values[N_VALUES];
float *vp;
//指针+-整数;指针的关系运算
for (vp = &values[0]; vp < &values[N_VALUES];)
//这里虽然下标为五的元素属于越界访问,但是并没有读取它的内容,不算越界访问
{
*vp++ = 0;
//vp先解引用,然后再++
}
指针加减整数可以让地址向后或向前移动对应的字节数。
2.指针减指针
int my_strlen(char *s)
{
char *p = s;
while(*p != '\0' )
p++;
return p-s;
}
指针减指针可以求出中间所差的类型对应字节数的个数
3.指针的关系运算
for(vp = &values[N_VALUES]; vp > &values[0];)
{
*--vp = 0;
}
指针的比较实际上就是十六进制数字的比较
五、指针和数组
#include <stdio.h>
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,0};
printf("%p\n", arr);
printf("%p\n", &arr[0]);
int i = 0;
for(i=0; i<10; i++)
{
printf("&arr[%d] = 0x%p",i,&arr[i])
}
return 0;
}
//结果:
//00DBFD88
//00DBFD88
//&arr[0] = 0x00DBFD88 = p+0
//&arr[1] = 0x00DBFD8C = p+1
//&arr[2] = 0x00DBFD90 = p+2
//&arr[3] = 0x00DBFD94 = p+3
//&arr[4] = 0x00DBFD98 = p+4
//&arr[5] = 0x00DBFD9C = p+5
//&arr[6] = 0x00DBFDA0 = p+6
//&arr[7] = 0x00DBFDA4 = p+7
//&arr[8] = 0x00DBFDA8 = p+8
//&arr[9] = 0x00DBFDAC = p+9
数组名为数组首元素地址:arr = 00DBFD88,arr[0] = 00DBFD88
&arr[0] = 0x00DBFD88 = p+0
&arr[1] = 0x00DBFD8C = p+1
&arr[2] = 0x00DBFD90 = p+2
&arr[3] = 0x00DBFD94 = p+3
&arr[4] = 0x00DBFD98 = p+4
&arr[5] = 0x00DBFD9C = p+5
&arr[6] = 0x00DBFDA0 = p+6
&arr[7] = 0x00DBFDA4 = p+7
&arr[8] = 0x00DBFDA8 = p+8
&arr[9] = 0x00DBFDAC = p+9
从这些结果我们得到arr[i]等同于*(p+i),这也就解释了数组中元素的访问其实使用了指针的思想,也让我们了解到数组元素的访问可以使用指针。
六、二级指针
1.指针变量也是变量,是变量就有地址,那指针变量的地址就可以储存在二级指针内 。
2.二级指针解引用需要两次才能找到变量
#include<stdio.h>
int main()
{
int a = 0;
int* p1 = &a;
int** p2 = &p;
//二级指针,存放指针变量的地址
//可以看作int* *p2,前面的int*表示指向的对象为int*类型,后面的*表示p2为指针变量
printf("%d",**p2);//p2解引用一次得到指针变量p1,再解引用得到a
return 0;
}
七、字符指针
1.字符指针的使用
#include<stdio.h>
int main()
{
char a = 'w';
char* p = &a;
printf("%c",*p);
//这是最简单的使用方法
const char* pstr = "hello bit.";
//这里指针保存了这个字符串的首字符地址而不是整个字符串
printf("%s\n", pstr);
//按字符串打印内存中的数据会打印到\0终止
return 0;
}
2.笔试题
#include <stdio.h>
int main()
{
char str1[] = "hello world.";
char str2[] = "hello world.";
const char *str3 = "hello world.";
const char *str4 = "hello world.";
if(str1 ==str2)
printf("str1 and str2 are the same\n");
else
printf("str1 and str2 are not the same\n");
if(str3 ==str4)
printf("str3 and str4 are the same\n");
else
printf("str3 and str4 are not the same\n");
return 0;
}
//结果:
//str1 and str2 are not same
//str3 and str4 are same
这段代码中的str1与str2是两个不同的数组,元素的内容都是hello world.,而str3与str4是两个指针变量,内容都是hello world.的首字符地址。
数组储存在栈区,每创建一个新的数组都需要占用内存空间存储,所以两个地址不同;字符串常量储存在静态区,不需要储存多个这样的字符串,所以两个指针变量都指向了同一个地址。
八、指针数组
指针数组也是数组,只是存放的元素是指针。
#include<stdio.h>
int main()
{
int a = 0;
int b = 0;
int c = 0;
int* arr[3]={&a,&b,&c};
//这就是一个简单的指针数组
return 0;
}
九、数组指针
1.数组指针的定义和创建
定义:指向数组的指针
#include<stdio.h>
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
//int arr[10]是一个数组,我们首先去掉数组名
//int [10]在中间写上指针变量名p,再写上*表示p为指针变量
//最后为了防止被解析为指针数组再加上括号:int (*p)[10],这就是一个指向数组的指针
//[10]表示指向的数组有是个元素,前面的int表示数组的元素为int类型
return 0;
}
2.数组名和&数组名
#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;
}
//结果:
//arr = 00EFFCDC
//&arr= 00EFFCDC
//arr+1 = 00EFFCE0
//&arr+1= 00EFFD04
arr = 00EFFCDC,&arr= 00EFFCDC二者虽然在内容上是一样的,但是arr+1 = 00EFFCE0跳过了4字节,&arr+1= 00EFFD04跳过了整个数组的40字节,二者加一跳过的字节数不同。
3.数组指针的使用
重要思想:一个二维数组可以看作多个一维数组的组合,但二维数组在内存中是连续存放的。
#include<stdio.h>
void print1(int arr[3][5], int r, int c)//二维数组传参可以直接写数组
{
int i = 0;
for (i = 0; i < r; i++)
{
int j = 0;
for (j = 0;j < c;j++)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
}
void print2(int (*arr)[5], int r, int c)//但是在本质上,用数组指针接收会更好
//由于arr[1]=*(p+1),我们用指针的思想改变代码
{
int i = 0;
for (i = 0; i < r; i++)
{
int j = 0;
for (j = 0;j < c;j++)
{
//printf("%d ",arr[i][j]);
//printf("%d ",*((arr[i])+j));
printf("%d ", *(*(arr+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 };
printf("print1\n");
print1(arr, 3, 5);//二维数组的数组名也是首元素地址,但是这个首元素是首个一位数组的地址
printf("print2\n");
print2(arr, 3, 5);
return 0;
}
//结果:
//print1
//1 2 3 4 5
//2 3 4 5 6
//3 4 5 6 7
//print2
//1 2 3 4 5
//2 3 4 5 6
//3 4 5 6 7
请看下面的代码,注意理解数据的类型
#include<stdio.h>
int main()
{
//当去掉变量名时剩下的就是数据的类型
int parr[5];//整形数组,共有五个元素
int* parr1[10];//整型指针的数组,共有十个元素
int(*parr2)[10];//数组指针,指向的数组有十个整型元素,指针的类型为int(*)[10]
int(*parr3[10])[5];//指针数组,包含十个数组指针,指向的是个数组都是有五个整型元素
return 0;
}
十、数组与指针传参
1.一维数组传参
#include <stdio.h>
void test(int arr[10])//可以直接写整形数组
{}
void test(int arr[])//数组的元素个数可以省略
{}
void test(int* arr)//本质上数组名是指针
{}
void test2(int* arr[20])//这是个整型指针数组,不符合
{}
void test2(int** arr)//这是个二级指针,也不符合
{}
int main()
{
int arr[10] = { 0 };
test(arr);
test2(arr);
}
2.二维数组传参
void test(int arr[3][5])
{}
void test(int arr[][])
{}
void test(int arr[][5])
{}
//总结:二维数组传参,函数形参的设计只能省略第一个[]的数字。
//因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素才方便运算。
//二维数组的首元素地址是第一行一维数组的地址
void test(int* arr)//这是一个整型指针,不符合
{}
void test(int* arr[5])//这是一个整型指针数组,不符合
{}
void test(int(*arr)[5])//这是一个整型数组指针,符合
{}
void test(int** arr)//这是一个二级指针,不符合
{}
int main()
{
int arr[3][5] = { 0 };
test(arr);
}
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.二级指针传参
#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;
}
十一、函数指针
1.函数指针
函数指针是指向函数的指针
#include<stdio.h>
void print(int n)
{
}
int main()
{
//首先函数的定义去掉函数名:void (int n)
//在中间加上(*)表明它是指针变量:void (*)(int)
//写上变量名,也可以删去内部参数的变量名:void (*p1)(int)
void (*p1)(int) = print;//初始化
void (*p2)(int) = &print;
printf("0x%p\n", p1);
printf("0x%p\n", p2);
return 0;
}
//结果:
//0x009013F2
//0x009013F2
(1)函数名与&函数名都是函数的地址
(2)这个函数地址储存在函数区里,所以函数的地址与它是否被调用无关
2.函数指针实现计算器
#include<stdio.h>
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 menu()
{
printf("**************************\n");
printf("*** 1.Add ****** 2.Sub ***\n");
printf("*** 3.Mul ****** 4.Div ***\n");
printf("********* 0.exit *********\n");
printf("**************************\n");
}
//打印初始界面
void calu(int (*f)(int, int))//接收函数
{
int ret = 0;
int a = 0;
int b = 0;
printf("请输入两个值:");
scanf("%d %d", &a, &b);
ret = f(a, b);//调用相应函数
printf("结果为:%d\n", ret);
}
//通过函数指针可以简化代码
int main()
{
menu();
int input = 0;
do
{
printf("请输入:");
scanf("%d", &input);
switch(input)//根据input的值判断加减乘除
{
case 0:
{
printf("退出程序");
break;
}
case 1:
{
calu(Add);//传参相应的函数指针
break;
}
case 2:
{
calu(Sub);
break;
}
case 3:
{
calu(Mul);
break;
}
case 4:
{
calu(Div);
break;
}
default:
{
printf("请重新输入\n");
break;
}
}
}while (input);
return 0;
}
十二、函数指针数组
1.函数指针数组的定义
函数指针数组是存储函数指针的数组
#include<stdio.h>
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;
}
int main()
{
int (*arr[5])(int, int) = { Add,Sub,Mul,Div };
//函数指针数组,元素类型为int (*)(int, int)
//内部写上数组名和元素个数:int (*arr[5])(int, int)
int (**p[5])(int, int)
return 0;
}
2.函数指针数组简化代码
#include<stdio.h>
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 menu()
{
printf("**************************\n");
printf("*** 1.Add ****** 2.Sub ***\n");
printf("*** 3.Mul ****** 4.Div ***\n");
printf("********* 0.exit *********\n");
printf("**************************\n");
}
void calu(int (*f)(int, int))
{
int ret = 0;
int a = 0;
int b = 0;
printf("请输入两个值:");
scanf("%d %d", &a, &b);
ret = f(a, b);
printf("结果为:%d\n", ret);
}
int main()
{
menu();
int input = 0;
int (*arr[5])(int, int) = { 0,Add,Sub,Mul,Div };//函数指针数组,对应数字对应函数
do
{
printf("请输入:");
scanf("%d", &input);
if(input>=1 && input<=4)
{
calu(arr[input]);//对应函数,大量简化代码
}
else if (input == 0)
{
printf("退出程序");
}
else
{
printf("请重新输入\n");
}
} while (input);
return 0;
}
十三、指向函数指针数组的指针
指向函数指针数组的指针就是指向函数指针数组的指针
禁止套娃~
是不是已经晕了,其实这样可以无限套娃。了解这些定义方法是帮我们认识这些指针与数组的定义方法。
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)[5])(const char*) = &pfunArr;
return 0;
}
十四、回调函数
定义:回调函数就是一个被作为参数传递的函数,一个函数作为另一个函数参数。
在下面,我们通过qsort(快速排序)函数来讲解
1.qsort函数
(1)qsort函数定义在stdlib.h的头文件中,注意包含头文件。
(2)qsort一共有四个参数,需要排序元素的首地址(void*),元素的个数(size_t),每个元素的大小(size_t),用于定义判定大小方式的compare函数,也就是回调函数的使用。
(3)compare函数需要满足参数为void*的指针,两个元素相减的结果为正数,前大于后;两个元素相减的结果为负数,后大于前;两个元素相减的结果为零,二者相等。
(4)qsort函数可以排序任何类型的数据且默认排升序。
#include<stdio.h>
#include<stdlib.h>
int compare(const void* e1, const void* e2)
{
return (*(int*)e1 - *(int*)e2);
}
int main()
{
int arr[10] = { 9,8,7,6,5,4,3,2,1,0 };
qsort(arr, sizeof(arr) / sizeof(arr[0]), sizeof(arr[0]), compare);
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
2.利用冒泡排序思想模拟实现qsort函数
//使用冒泡排序思想模拟实现qsort函数
#include<stdio.h>
int int_compare(const void* e1, const void* e2)//设计的函数,这只是int类型的比较,还可以编写其他函数排序其他数据
{
return (*(int*)e1 - *(int*)e2);
}
void exchange(char* p1, char* p2, int sz)//一个字节一个字节交换
{
int i = 0;
for (i = 0; i < sz; i++)
{
int temp = 0;
temp = *(p1+i);
*(p1+i) = *(p2+i);
*(p2 + i) = temp;
}
}
void my_sort(void* p, int n, int sz, int compare(const void*, const void*))
{
char* arr = (char*)p;
int i = 0;
for (i=0; i<n-1; i++)
{
int j = 0;
for (j=0; j<n-i-1; j++)
{
if (compare(arr + sz * j, arr + sz * (j + 1)))//以自己设计的函数的返回值确定先后顺序
{
exchange(arr + sz * j, arr + sz * (j + 1), sz);
}
}
}
}
int main()
{
int arr[10] = { 10,9,8,7,6,5,4,3,2,1 };
my_sort(arr, sizeof(arr)/sizeof(arr[0]), sizeof(arr[0]),int_compare);
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
//结果:1 2 3 4 5 6 7 8 9 10