引言:
在现实生活中,当我们要找一个东西时,我们会思考,这个东西在哪儿,这个东西的地址是什么。同样,在计算机的内部世界中,空间也十分广大,要在这块广大空间中找到一个特定的位置是非常困难的,而为了方便这一操作,我们将这一块空间的每一块区域都设置一个编号,这个编号也叫做地址,在C语言中也被叫做指针。本篇我们就来聊聊关于指针的各种知识。
指针也就是内存地址,指针变量是用来存放内存地址的变量,在同一CPU构架下,不同类型的指针变量所占用的存储单元长度是相同的,而存放数据的变量因数据的类型不同,所占用的存储空间长度也不同。有了指针以后,不仅可以对数据本身,也可以对存储数据的变量地址进行操作。指针描述了数据在内存中的位置,标示了一个占据存储空间的实体,在这一段空间起始位置的相对距离值——百度百科
int a = 10;
char str[10] = { 0 };
地址的产生
那么可能有小伙伴又会问了,地址是怎么产生的呢?
地址是物理的电线上产生的
在32位机器上,有32根地址线,这32根地址线都可以产生0/1两个二进制数,32个0/1组成二进制序列,我们就把这个二进制序列称为地址
同理,在64位机器上,有64根地址线,就会有64个0/1组成的二进制序列,我们同样也把这个二进制序列称为地址。
&
printf()
函数将这个地址打印出来,我们就需要格式化输出符%p
#include
int main()
{
int a = 10;
int* pt = &a;
printf("变量a的地址是%p\n", pt);
return 0;
}
*
int num = 10;
int *pi = #
*pi = 20;
printf("%d",*pi);
我们前面说到,在32位的机器中,我们需要32个比特位来存储地址,而32个比特位就是4个字节,因此,在32位机器上,指针的大小就是4个字节。
同理,在64位的机器中,我们需要64个比特位来存储地址,而64个比特位就是8个字节,因此,在64位机器上,指针的大小就是8个字节。
我们可以用操作符sizeof()
来进行查看:
#include
int main()
{
int a = 10;
int* pt = &a;
char b = 'a';
char* pc = &b;
printf("指针变量pt的大小是%d\n", sizeof(pt));
printf("指针变量pc的大小是%d\n", sizeof(pc));
return 0;
}
&
用来取出变量的地址*
用来对指针变量进行解引用我们知道,存储数字时,我们有整型,浮点型等一系列数据类型来对不同类型的数字进行存储,那么指针有没有类型呢?当然有!
int num = 10;
#
char* pc = NULL;
short* ps = NULL;
int* pi = NULL;
long* pl = NULL;
long long* pll = NULL;
float* pf = NULL;
double* pd = NULL;
………………
type + *
*
不是解引用操作符那有些小伙伴就会有疑惑了,既然指针变量的大小要么是4个字节,要么是8个字节,那我们定义的指针类型如int*、char*等还有什么意义呢?接下来我们就来探讨指针类型到底是来干嘛的。
#include
int main()
{
int num_1 = 0x11223344;
int* pi = &num_1;
int num_2 = 0x11223344;
char* pc = &num_2;
*pi = 0;
*pc = 0;
return 0;
}
pi指向的num_1被成功置零,这个好理解,但我们惊讶地发现,pc指向的num_2只有一个字节的数据被置零,最后的结果竟然是0x11223300
由此可以知道,指针类型int*解引用可以访问4个字节,指针类型char*解引用只能访问1个字节,而数据类型int和char的大小又是4个字节和1个字节,由此我们可以得出结论:指针类型可以决定指针解引用的时候访问多少个字节(即指针的权限)
再来看一个例子:
#include
int main()
{
int num = 10;
int* pi = #
char* pc = #
printf("%p\n", pi);
printf("%p\n", pc);
printf("%p\n", pi + 1);
printf("%p\n", pc + 1);
return 0;
}
现在,我们可以对指针定义的方式type + *
做出更多的解释了:
可能有小伙伴会写出这样的代码:
#include
int main()
{
int* p;
*p = 20;
printf("%d\n", *p);
return 0;
}
我们发现编译器会给我们报错:
那为什么我们不允许这个现象存在呢?我们知道,如果局部变量未初始化,那么它指向的就是一个随机值,同理,如果指针变量不初始化,那它指向的就是随即一块地址空间,我们要改变这块随即地址的数据,显然是不被允许的。
这个未初始化的局部变量指针p,我们就称之为野指针
野指针:即指针指向的位置是不可知的(随机的、不正确的的、没有明确限制的)
以下几种情况会导致野指针的形成:
#include
int main()
{
int* p;
*p = 20;
return 0;
}
#include
int main()
{
int arr[5] = { 1,2,3,4,5 };
int* pi = arr; //这段代码是什么意思,后面会讲到
for (int i = 0; i < 10; i++)
//当指针指向的范围超出数组arr的范围时,pi就是野指针
printf("%d ", arr[i]);
return 0;
}
#include
int* Text()
{
int a = 10;
return &a;
}
int main()
{
int* pi = Text();
printf("%d\n", *pi);
return 0;
}
指针存在以下几种运算
#include
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* pi = &arr[0];
for (int i = 0; i < 10; i++)
printf("%d ", *(pi + i));
printf("\n");
return 0;
}
#include
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* pi = &arr[0];
int* pc = &arr[9];
printf("%d\n", pc - pi);
printf("%d\n", pi - pc);
return 0;
}
可以得到结果:
为什么呢?
我们知道指针是地址,而地址是有大小的
因此,指针的关系运算,就是地址(指针)大小的比较
我们来看下面的代码:
#include
#define N 5
int main()
{
float nums[N];
float* vp;
for (vp = &nums[N]; vp > &nums[0]; )
*--vp = 0;
return 0;
}
由分析图我们可以知道,该代码实现的是对nums数组置0的功能
注:我们需要清楚的一点是,虽然该代码在大多数编译器上都可以顺利完成任务,但我们还是要避免这样写,因为标准并不确保可行
标准规定:允许指向数组元素的指针与指向数组最后的一个元素后面的那个内存位置的指针(如下图的p2)比较,但是不允许与指向第一个元素之前的那个内存位置的指针(如下图的p0)进行比较。
举个例子:
char* pc = NULL;
short* ps = NULL;
int* pi = NULL;
long* pl = NULL;
long long* pll = NULL;
float* pf = NULL;
double* pd = NULL;
………………
都称之为一级指针。
#include
int main()
{
int num = 10;
int* p = #
return 0;
}
int **p = &p;
int num = 10;
int * p = #
int* *pp = &p;
int** *ppp = &pp;
………………
可能有很多小伙伴对指针变量和数组的关系还不太熟悉,现在我们来梳理一下
首先,我们不能将指针变量和数组混为一谈
其次,指针变量和数组之间也有一定的联系
sizeof(数组名)
,这里的数组名不是数组首元素的地址,这里的数组名代表整个数组,sizeof(数组名)
,计算的是整个数组的大小,单位是字节&数组名
,这里的数组名表示整个数组,&数组名
取出的是整个数组的地址#include
int main()
{
int arr[] = { 1,2,3,4,5 };
printf("arr = %p\n", arr);
printf("&arr[0] = %p\n", &arr[0]);
printf("&arr = %p\n", &arr);
printf("sizeof(arr) = %d\n", sizeof(arr));
return 0;
}
sizeof(arr) = 20
得到在这里arr代表整个数组,我们没有异议,但有小伙伴可能会疑惑,&arr
和arr
代表的地址相等,为什么就说&arr
的arr代表的就是整个数组呢?int [5] *(实际上写成int * [5] 更加准确)、int *
,这是两个截然不同的类型。实际上,int *是指向int型数据的指针类型,而int * [5]则是一个指向长度为5的数组的指针类型,简称数组指针。因此,我们说,在&arr
中,arr代表的是整个数组。接下来,我们就来仔细讨论两个容易混淆的概念:数组指针和指针数组
注:学习之前,我们有必要知道,操作符[]
的优先级要高于操作符*
的优先级
int * a[10]
,a首先和”[10]“结合,说明这是一个存储10个元素的数组,再和前面的”int *“结合,就说明了存储元素的元素类型是int *型的指针类型,即这是一个指针数组。#include
int main()
{
int nums1[3] = { 1,2,3, };
int nums2[3] = { 4,5,6, };
int nums3[3] = { 7,8,9, };
//将一维数组nums1,nums2,nums3的首元素的地址存入指针数组nums中,模拟二维数组
int* nums[3] = { nums1, nums2, nums3 };
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 3; j++)
printf("%d ", nums[i][j]);
printf("\n");
}
return 0;
}
我们可以通过类比来对数组指针这一概念进行分析,我们知道:
整型指针——指向整型变量的指针,存放整型变量的地址的指针变量
字符指针——指向字符变量的指针,存放字符变量的地址的指针变量
数组指针——指向数组的指针,存放数组的地址的指针变量
我们先来看看下面的例子:
int* p1[10]; //p1是什么?
int(*p2)[10]; //p2是什么?
int* p1[10]
,由运算符的优先级,p1先和[10]结合,说明这是一个数组,再和前面的int*结合,说明数组元素的类型是一个int*型指针,即p1是一个指针数组int(*p2)[10]
,由于括号的存在,p2先和*结合,说明p2是一个指针,而int [10]则说明了这个指针指向的是大小为10个int型的数组,即p2是一个数组指针&(数组名)
取出的是整个数组的地址,这个地址的指针类型是一个数组指针,那么具体的,这个数组指针有什么特殊之处呢?我们来看下面的代码:#include
int main()
{
int nums[10] = { 0 };
printf("%p\n", nums);
printf("%p\n", nums + 1);
printf("%p\n", &nums[0]);
printf("%p\n", &nums[0] + 1);
printf("%p\n", &nums);
printf("%p\n", &nums + 1);
return 0;
}
&(数组名)
得到的类型是int (*)[10]
,是数组指针,数组指针加1后会跨过整个数组的长度,而这个数组为int [10]
,因此会跨过40个字节,如图:int nums[] = { 1,2,3,4,5 };
&nums;
//定义一个数组指针来指向数组nums,该如何定义?
int (*p)[10] = &nums
,切勿写成int [10] (*p)
我们用指针来打印一维数组:
#include
int main()
{
int nums[] = { 1,2,3,4,5 };
int* p = nums;
for (int i = 0; i < 5; i++)
printf("%d ", *(p + i));
printf("\n");
return 0;
}
#include
int main()
{
int nums[] = { 1,2,3,4,5 };
int(*pi)[5] = &nums;
for(int i = 0; i < 5; i++)
printf("%d ", *(*pi + i));
/*
由于pi是整个数组的地址,因此要先进行解引用得到这个数组
此时*pi就是数组名,再通过+i就可以遍历整个数组
*/
printf("\n");
return 0;
}
自定义函数打印二维数组
#include
void Print(int nums[][4], int row, int col)
{
for (int i = 0; i < row; i++)
{
for (int j = 0; j < col; j++)
printf("%d ", nums[i][j]);
printf("\n");
}
printf("\n");
}
int main()
{
int nums[3][4] = { {1,2,3,4},{5,6,7,8},{9,10,11,12} };
Print(nums, 3, 4);
return 0;
}
#include
void Print(int (*p)[4], int row, int col)
{
for (int i = 0; i < row; i++)
{
for (int j = 0; j < col; j++)
printf("%d ", *(*(p + i) + j));
/*
p是一个数组指针代表组成二维数组的每个一维数组的地址
p+i代表跳过i行,即跳过i个一维数组的地址,
*(p+i),即对一维数组的地址解引用,得到这个一维数组,做到对二维数组每行的遍历
可以将*(p+i)看成一维数组的数组名,再加j就是每次跳过一维数组的一个元素
*(*(p + i) + j)就可以做到对数组列的遍历
*/
printf("\n");
}
}
int main()
{
int nums[3][4] = { {1,2,3,4},{5,6,7,8},{9,10,11,12} };
/*
nums为二维数组的数组名,代表首元素的地址,而二维数组的首元素为一个一维数组
则nums代表了一个一维数组的地址,是一个数组指针,指针类型为int(*)[3]
*/
Print(nums, 3, 4);
return 0;
}
sizeof(数组名)
,这里的数组名不是数组首元素的地址,这里的数组名代表整个数组,sizeof(数组名)
,计算的是整个数组的大小,单位是字节&数组名
,这里的数组名表示整个数组,&数组名
取出的是整个数组的地址接下来,我们来做一些练习题来加深对数组和指针的理解
#include
/*
` 正确
数组传参时,不会真实的创建数组,因此可以不传入数组的大小
*/
void test1(int arr[])
{}
//正确,不必多说
void test1(int arr[10])
{}
/*
正确
传入的是int *型指针故可以用int *型的指针变量接收
*/
void test1(int *arr)
{}
/*
正确
传入的arr2为指针数组的首地址,用指针数组接受没问题
*/
void test2(int *arr[20])
{}
/*
正确
arr2为指针数组的首地址,指针类型为int **,故可用指针类型为int **的形参接收
*/
void test2(int **arr)
{}
int main()
{
int arr1[10] = { 0 };
int* arr2[20] = { 0 };
test1(arr1); //传入的是首元素地址,类型为int *
test2(arr2); //传入的是首元素地址,类型为int **
return 0;
}
#include
//正确
void test(int arr[3][5])
{}
//错误
void test(int arr[][])
{}
//正确
void test(int arr [][5])
{}
/*
总结:二维数组传参,函数形参设计只能省略第一个[]数字
因为对一个二维数组,可以不知道有多少行,但是必须知道一行有多少列
这样才方便运算
*/
/*
错误
应该用一个数组指针来接收
*/
void test(int *arr)
{}
/*
错误
int *arr[5]是一个指针数组,而不是数组指针
*/
void test(int *arr[5])
{}
/*
正确
int (*arr)[5]是一个指向存放5个int型数组的数组指针,符合条件
*/
void test(int (*arr)[5])
{}
/*
错误
传入的是数组指针而不是二级指针
*/
void test(int **arr)
{}
int main()
{
int arr[3][5] = { 0 };
test(arr); //arr是数组名,即首元素的地址,即一维数组的地址,是一个数组指针
return 0;
}
#include
int main()
{
int a[] = { 1,2,3,4, };
printf("%d\n", sizeof(a)); //16.sizeof(数组名)计算整个数组的大小,故打印4*4 = 16
printf("%d\n", sizeof(a + 0)); //4/8.在这里a为数组首元素的地址,加0后还是首元素地址,是地址大小就是4/8
printf("%d\n", sizeof(*a)); //4.*a == *(a + 0) == a[0] = 1,即数组第一个元素,故打印sizeof(int) = 4
printf("%d\n", sizeof(a + 1)); //4/8.在这里a为数组首元素的地址,加1后是第二个元素地址,是地址大小就是4/8
printf("%d\n", sizeof(a[1])); //4.a[1]即数组第一个元素,故打印sizeof(int) = 4
printf("%d\n", sizeof(&a)); //4/8.&a即取出整个数组的地址,是地址大小就是4/8
printf("%d\n", sizeof(*&a)); //16.对数组取地址再解引用,得到的还是原来的数组,即*&a == a,故打印4*4 = 16
printf("%d\n", sizeof(&a + 1)); //4/8.&a取出数组的地址后再加1,得到的还是地址,是地址大小就是4/8
printf("%d\n", sizeof(&a[0])); //4/8.&a[0]得到的是地址,是地址大小就是4/8
printf("%d\n", sizeof(&a[0] + 1)); //4/8.地址加1,得到的还是地址,是地址大小就是4/8
return 0;
}
#include
int main()
{
int a[3][4] = { 0 };
printf("%d\n", sizeof(a));
//48
//sizeof(数组名)计算的是整个数组的大小
//故打印3*4*4 = 48
printf("%d\n", sizeof(a[0][0]));
//4
//a[0][0]是数组第一个元素
//故打印sizeof(int) = 4
printf("%d\n", sizeof(a[0]));
//16
//a[0]是组成二维数组的第一个一维数组的数组名
//sizeof(数组名)计算的是整个数组的大小,故为16
printf("%d\n", sizeof(a[0] + 1));
//4/8
//a[0]是组成二维数组的第一个一维数组的数组名,是首元素的个地址,
//加1则跳过一个元素,指向一维数组的第二个元素,也就是整个数组的第二个元素
//地址加1后还是地址,是地址大小就是4/8
printf("%d\n", sizeof(*(a[0] + 1)));
//4
//由上面的分析,a[0] + 1指向整个数组的第二个元素
//故打印sizeof(int)
printf("%d\n", sizeof(a + 1));
//4/8
//这里a为二维数组首元素即第一个一维数组的地址,加1后即第二个一维数组的地址
//是地址大小就是4/8
printf("%d\n", sizeof(*(a + 1)));
//16
//由上面的分析,a + 1为第二个一维数组的地址,解引用后就是第二个一维数组的数组名,即a[1]
//sizeof(数组名)计算的是整个一维数组的大小
//故打印4 * 4 = 16
printf("%d\n", sizeof(&a[0] + 1));
//4/8
//a[0]即第一个一维数组的数组名,&数组名即得到整个数组的地址,加1后即跳过整个数组
//&a[0] + 1指向的就是第二个一维数组,是第二个一维数组的地址
//地址加一后还是地址,是地址大小就是4/8
printf("%d\n", sizeof(*(&a[0] + 1)));
//16
//由上面的分析/&a[0] + 1是第二个一维数组的地址,解引用后就是第二个一维数组的数组名
//sizeof(数组名)计算的是整个一维数组的大小
//故打印4 * 4 = 16
printf("%d\n", sizeof(*a));
//16
//*a == *(a + 0) == a[0],即第一个一维数组的数组名
//sizeof(数组名)计算的是整个一维数组的大小
//故打印4 * 4 = 16
printf("%d\n", sizeof(a[3]));
//16
//a[3],即第四个一维数组的数组名
//sizeof(数组名)计算的是整个一维数组的大小
//故打印4 * 4 = 16
return 0;
}
注:
有小伙伴看到printf("%d\n", sizeof(a[3]));
这串代码可能会认为a[3]不是越界了吗,为什么还可以正常打印呢?
在探讨这个问题之前,我们先来看一串代码:
#include
int main()
{
int a = 7;
short s = 4;
printf("%d\n", sizeof(s = a + 2));
printf("%d\n", s);
return 0;
}
sizeof()
在计算时,只会关注()
内是什么类型,并不会进行具体的计算字符指针和整型指针类似,有很多共同之处,但当涉及到字符数组时,还是要对某些情况进行说明,来看下面的代码
#include
int main()
{
char str[] = "abcde";
char* pc = "abcde";
return 0;
}
const char* pc = "abcde"
更加准确#include
int main()
{
char str1[] = "hello world";
char str2[] = "hello world";
const char* p1 = "hello world";
const char* p2 = "hello world";
if (str1 == str2)
printf("str1 and str2 are same\n");
else
printf("str1 and str2 are not same\n");
if (p1 == p2)
printf("p1 and p2 are same\n");
else
printf("p1 and p2 are not same\n");
return 0;
}
strcmp()
来实现对两个字符串的比较,而不是直接拿数组名来比。#include
int main()
{
char arr[] = "abcdef";
printf("%d\n", sizeof(arr));
//7
//sizeof(数组名)计算的是整个数组的大小(注意要加上'\0')
printf("%d\n", sizeof(arr + 0));
//4/8
//这里的数组名代表整个数组的地址,地址加减整数还是地址
//是地址大小就是4/8
printf("%d\n", sizeof(*arr));
//1
//这里的arr代表数组首元素的地址,解引用后得到数组首元素'a'
//故打印sizeof(char) = 1
printf("%d\n", sizeof(arr[1]));
//1
//arr[1]代表数组第二个元素
//故打印sizeof(char) = 1
printf("%d\n", sizeof(&arr));
//4/8
//&arr表示取出整个数组的地址
//是地址大小就是4/8
printf("%d\n", sizeof(&arr + 1));
//4/8
//地址加减整数还是地址
//是地址大小就是4/8
printf("%d\n", sizeof(&arr[0] + 1));
//4/8
//&arr[0]取出第一个元素的地址后加一,得到第二个元素的地址
//是地址大小就是4/8
return 0;
}
#include
int main()
{
char arr[] = { 'a','b','c','d','e','f' };
printf("%d\n", sizeof(arr));
//6
//sizeof(数组名)计算的是整个数组的大小
printf("%d\n", sizeof(arr + 0));
//4/8
//这里的数组名代表整个数组的地址,地址加减整数还是地址
//是地址大小就是4/8
printf("%d\n", sizeof(*arr));
//1
//这里的arr代表数组首元素的地址,解引用后得到数组首元素'a'
//故打印sizeof(char) = 1
printf("%d\n", sizeof(arr[1]));
//1
//arr[1]代表数组第二个元素
//故打印sizeof(char) = 1
printf("%d\n", sizeof(&arr));
//4/8
//&arr表示取出整个数组的地址
//是地址大小就是4/8
printf("%d\n", sizeof(&arr + 1));
//4/8
//地址加减整数还是地址
//是地址大小就是4/8
printf("%d\n", sizeof(&arr[0] + 1));
//4/8
//&arr[0]取出第一个元素的地址后加一,得到第二个元素的地址
//是地址大小就是4/8
return 0;
}
#include
int main()
{
char *p = "abcdef";
printf("%d\n", sizeof(p));
//4/8
//p是一个指针变量,是指针变量大小就是4/8
printf("%d\n", sizeof(p + 1));
//4/8
//指针变量加减整数还是指针变量
//是指针变量大小就是4/8
printf("%d\n", sizeof(*p));
//1
//p指向字符串首元素'a',解引用后得到首元素'a'
//故打印sizeof(char) = 1
printf("%d\n", sizeof(p[0]));
//1
//arr[0]代表数组第1个元素
//故打印sizeof(char) = 1
printf("%d\n", sizeof(&p));
//4/8
//&p表示取出字符指针p的地址
//是地址大小就是4/8
printf("%d\n", sizeof(&p + 1));
//4/8
//地址加减整数还是地址
//是地址大小就是4/8
printf("%d\n", sizeof(&p[0] + 1));
//4/8
//&p[0]取出第一个元素的地址后加一,得到第二个元素的地址
//是地址大小就是4/8
return 0;
}
char *
类型,得到的位置应该在指针p的后一个位置#include
#include
int main()
{
char arr[] = "abcdef";
printf("%d\n", strlen(arr));
//6
//计算字符串长度,为6
printf("%d\n", strlen(arr + 0));
//6
//arr+0还是首元素地址,计算的还是原来的字符串长度,为6
printf("%d\n", strlen(*arr));
//无法运行
//strlen()操作的必须是指向字符串的地址,*arr代表该字符数组的首元素'a',不是地址
//如果我们将字符'a'作为参数传入,字符‘a'的ASCII码为97,那么strlen()就会从97这个地址开始访问
//这就形成了内存的非法访问
printf("%d\n", strlen(arr[1]));
//无法运行
//分析如上
printf("%d\n", strlen(&arr));
//6
//&arr地址的开始值和数组首元素地址相同,计算的还是原来的字符串长度
//故打印6
printf("%d\n", strlen(&arr + 1));
//随机值
//取出整个数组的地址后加一,将跳过整个字符串,指向一块位置空间,'\0'的位置也未知
//故打印随机值
printf("%d\n", strlen(&arr[0] + 1));
//5
//&arr[0]取出第一个元素的地址后加一,得到第二个元素的地址
//现在字符串长度比原来少1
//故5
return 0;
}
#include
#include
int main()
{
char arr[] = { 'a','b','c','d','e','f' };
printf("%d\n", strlen(arr));
//随机值
//该字符数组没有结束符'\0',要strlen()要读到'\0'才会停止
//故打印随机值
printf("%d\n", strlen(arr + 0));
//随机值
//该字符数组没有结束符'\0',要strlen()要读到'\0'才会停止
//故打印随机值
printf("%d\n", strlen(*arr));
//无法运行
//strlen()操作的必须是指向字符串的地址,*arr代表该字符数组的首元素'a',不是地址
//如果我们将字符'a'作为参数传入,字符‘a'的ASCII码为97,那么strlen()就会从97这个地址开始访问
//这就形成了内存的非法访问
printf("%d\n", strlen(arr[1]));
//无法运行
//分析如上
printf("%d\n", strlen(&arr));
//随机值
//该字符数组没有结束符'\0',要strlen()要读到'\0'才会停止
//故打印随机值
printf("%d\n", strlen(&arr + 1));
//随机值
//取出整个数组的地址后加一,将指向一块位置空间,'\0'的位置也未知
//故打印随机值
printf("%d\n", strlen(&arr[0] + 1));
//随机值
//&arr[0]取出第一个元素的地址后加一,得到第二个元素的地址
//但该字符数组没有结束符'\0',要strlen()要读到'\0'才会停止
//故打印随机值
return 0;
}
#include
#include
int main()
{
char *p = "abcdef";
printf("%d\n", strlen(p));
//6
//p指向的值是字符串首元素地址
//计算字符串长度,为6
printf("%d\n", strlen(p + 1));
//5
//p指向的值是字符串首元素地址,加1后则指向第二个元素的地址
//则长度为5
printf("%d\n", strlen(*p));
//无法运行
//strlen()操作的必须是指向字符串的地址,*p代表该字符数组的首元素'a',不是地址
//如果我们将字符'a'作为参数传入,字符‘a'的ASCII码为97,那么strlen()就会从97这个地址开始访问
//这就形成了内存的非法访问
printf("%d\n", strlen(p[0]));
//无法运行
//分析如上
printf("%d\n", strlen(&p));
//随机值
//字符指针p和其指向的字符串常量的地址没有直接关联,&p后的地址为一块未知空间
//故打印随机值
printf("%d\n", strlen(&p + 1));
//随机值
//分析如上
printf("%d\n", strlen(&p[0] + 1));
//5
//&ap[0]取出第一个元素的地址后加一,得到第二个元素的地址
//现在字符串长度比原来少1
//故5
return 0;
}
做了这么多题,想必小伙伴们对于字符指针,字符串,字符数组,字符串常量有了较深刻的理解,我们有必要做一些总结:
sizeof()
和strlen()
之间的区别,sizeof()
是一个操作符,计算的是括号内所占字节的大小;strlen()
是字符串操作函数,求的是字符串的有效长度,且必须遇到‘\0’
才会停止。‘\0’
,而字符串必须有‘\0’
作为结束符顾名思义,函数指针就是指向函数的指针
我们不妨先来思考这样一个问题:函数是否也有地址呢?来看下面的代码:
#include
void Text(int num, char *ch)
{}
int main()
{
printf("%p\n", &Text);
return 0;
}
可以得到:
#include
void Text(int num, char *ch)
{}
int main()
{
printf("%p\n", Text);
printf("%p\n", &Text);
return 0;
}
Text()
,如果我们要将它的地址&Text
存入变量pf中,我们要如何定义这个函数指针pf呢?同样,我通过画流程图来分析:我们通过下面的例子来讲解:
#include
void Print(int nums[10], int numsSize)
{
for (int i = 0; i < numsSize; i++)
printf("%d ", nums[i]);
printf("\n");
}
int main()
{
int nums[10] = { 1,2,3,4,5,6,7,8,9,10 };
//定义函数指针
void (*p)(int[10], int) = Print;
//不用函数指针,直接调用
Print(nums, 10);
//用函数指针调用
(*p)(nums, 10); //可以先对函数指针解引用再调用
p(nums, 10); //也可以直接调用
return 0;
}
都可以得到正确的结果:
顾名思义,函数指针数组就是存放函数指针的数组
例如,我们要将下面这三个函数存入函数指针数组中,我们该如何定义?
void Text1(int num1, int num2)
{}
void Text2(int num1, int num2)
{}
void Text3(int num1, int num2)
{}
void (*)(int, int)
,我们假设数组名为p,由于这是一个指针数组,因此p要先和[3]
结合再和*结合,又因为[]
的优先级高于*
,因此我们可以这样写void (* p[3])(int, int)
,这样这个函数指针数组就定义好了**,这个数组代表的是存放着3个指针类型为void (*)(int, int)
的函数指针数组**现在问大家一个问题:如果要实现一个简单的两个整数之间的计算器(加、减、乘、除、取模、相与……),大家要如何实现呢?用switch case
语句?那未免太过繁杂,如果我们用刚刚所学习的函数指针数组来实现,那就可以让我们的代码精简许多
#include
int add(int num1, int num2)
{
return num1 + num2;
}
int sub(int num1, int num2)
{
return num1 - num2;
}
int mul(int num1, int num2)
{
return num1 * num2;
}
int div(int num1, int num2)
{
return num1 / num2;
}
void meau()
{
printf("***************************\n");
printf("***** 1->add 2->sub ***\n");
printf("***** 3->mul 4->div ***\n");
printf("***** 0->exit ***\n");
}
int main()
{
int (*pArr[5])(int, int) = { 0,add,sub,mul,div };
int input;
int num1, num2;
do
{
printf("要进行怎样的运算:\n");
meau();
scanf_s("%d", &input);
if (1 <= input && 4 >= input)
{
printf("请输入两个整数:");
scanf_s("%d %d", &num1, &num2);
int ret = pArr[input](num1, num2); //利用函数指针数组调用函数,简化代码
printf("结果为:%d\n", ret);
}
else if (input == 0)
printf("退出\n");
else
printf("输入错误,重新输入\n");
} while (input);
return 0;
}
回调函数就是一个通过函数指针调用的函数。如果把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另一方调用,用于对该事件或条件进行响应。
对于上面的简易计算器的实现,我们也可以用回调函数来实现:
include<stdio.h>
int add(int num1, int num2)
{
return num1 + num2;
}
int sub(int num1, int num2)
{
return num1 - num2;
}
int mul(int num1, int num2)
{
return num1 * num2;
}
int div(int num1, int num2)
{
return num1 / num2;
}
//将实现计算的函数作为参数,再利用函数指针实现回调函数,从而避免代码的冗余
void calculator(int (*p)(int, int))
{
int num1, num2;
printf("请输入两个整数:");
scanf_s("%d %d", &num1, &num2);
int ret = p(num1, num2);
printf("结果为:%d\n", ret);
}
void meau()
{
printf("***************************\n");
printf("***** 1->add 2->sub ***\n");
printf("***** 3->mul 4->div ***\n");
printf("***** 0->exit ***\n");
}
int main()
{
int input;
do
{
printf("要进行怎样的运算:\n");
meau();
scanf_s("%d", &input);
switch (input)
{
case 1:
calculator(add);
break;
case 2:
calculator(sub);
break;
case 3:
calculator(mul);
break;
case 4:
calculator(div);
break;
case 0:
printf("退出\n");
break;
default:
printf("输入错误,重新输入\n");
}
} while (input);
return 0;
}
qsort()
也是使用回调函数的方式来实现的qsort()
的函数原型为:void qsort (void* base, size_t num, size_t size, int (*compar)(const void*,const void*));
,其中int (*compar)(const void*,const void*)
就是利用了回调函数,而具体的qsort()
的工作原理,怎么使用,这里便不再讲述。通过这一章的学习,相信小伙伴们对于指针这一概念以及指针和数组、字符串、函数等之间的关系有了较为深刻的认识,也应该可以将指针这一利器运用自如了,最后我们以一道笔试题来结束这一次的学习,同时也检验一下大家的学习成果。
#include
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;
}
解析戳这里
最后的最后,如果觉得本篇文章对你有所帮助的话,还请点个小小的赞支持一下喔
共勉!!!( ̄y▽ ̄)╭ Ohohoho…