请叫我鸽子王!已经很久没有发文章了哈哈哈,终于回归了不是吗
今天带来的是指针的进阶篇,指针是C语言中最复杂的部分之一,今天我们围绕的是指针的进阶部分来学习。
C语言:指针(基础全介绍)_m0_62319039的博客-CSDN博客https://blog.csdn.net/m0_62319039/article/details/121648203?spm=1001.2014.3001.5501可以回顾一下博主以前关于指针基础的介绍,话不多说,我们开始进阶篇的学习。
目录
1.字符指针
2.指针数组
3.数组指针
4.数组参数、指针参数
4.2 二维数组传参
4.3 一级指针传参
4.4 二级指针传参
5.函数指针
6.回调函数:qsort (快速排序)
6.2 qsort 排序结构体
7.指针和数组练习题解析
7.1 一维数组
?7.2 字符数组
7.3 二维数组
8.指针笔试题
8.1
8.2
8.3?
8.4
8.5
8.6
8.7
8.8
1.字符指针
char ch = 'w';
char* p = &ch;
char* p = "abcdef";
字符指针其实就是指针中存放的是字符的地址。前者我们在这假设w的地址是0x12ff80,p的地址是0x124440,那么p里面存放的就是0x12ff80,
后者p中存放的是首字符a的地址。
int main()
{
char arr1[] = "abcdef";
char arr2[] = "abcdef";
const char* str1 = "abcdef";
const char* str2 = "abcdef";
return 0;
}
结论就是:arr1!=arr2,str1==str2。本质上,str1和str2指向的是同一份"abcdef"。
arr1和arr2却是不同的两个字符串。
char* arr[] = {"abc","qwer","zhangsan"};
同样,字符指针数组也可以这么表示。
2.指针数组
存放指针的数组就是指针数组。
基本形式:
int main()
{
int arr1[] = {1,2,3,4,5};
int arr2[] = {2,3,4,5,6};
int arr3[] = {3,4,5,6,7};
int* arr[] = {arr1,arr2,arr3};
return 0;
}
其中,arr就是一个指针数组。
每一个元素都表示对应的数组的首元素地址。例如,arr1就是一个指针,指向arr1这个数组的首元素。
3.数组指针
是一种指针。
char* p = &ch;//字符指针,指向字符的指针
int* p = &ch;//整形指针,指向整型的指针
那么数组指针就是指向数组的指针。
int * p1[10];//p1先和数组结合,每个元素存放的是int*类型
int (*p2)[10];//p2先和*结合,再和数组结合,每个元素就是int
//p2就是一种数组指针
但是要弄清楚p2,我们先要了解数组名。
我们知道,数组名就是首元素地址。对于一个数组arr,arr和&arr的区别是什么呢?
?可以看到,arr和arr+1差的是一个4个字节,但是&arr和&arr+1差的是整个数组的大小。
其实后者中间的就是一个数组指针。
int main()
{
char arr[5];
char (*pa)[5] = &arr;
}
通俗易懂的说,pa就是指向arr的地址的一个元素,这个元素是一个int类型的指针(int*)。
重要等式:
arr[i] == *(arr+i) == p[i] == *(p+i)
接下来我们来理解一段代码:
int arr[5];arr是一个整形数组,有5个元素,每个元素是int类型
int *p1[10];p1是一个数组,数组有10个元素,每个元素的类型是int*,p1是是指针数组
int (*p2)[10];p2和*结合,说明p2是一个指针,该指针指向一个数组,数组是10个元素,每个元素是int类型的。p2是一个数组指针
int (*p3[10])[5];p3先和方块结合,是一个数组,数组有10个元素,也就是int(*)[5],所以p3是一个存放了10个int(*)[5]类型的数组
p3如图
4.数组参数、指针参数
#include<stdio.h>
void test(int arr[])
{}
void teat(int arr[10])
{}
void teat(int *arr)
{}
void test2()int *arr[20])
{}
void test2(int **arr)
{}
int main()
{
int arr[10] = {0};
int *arr2[20] = {0};
test(arr);
test2(arr);
return 0;
}
可以看到每一个传参接收的形式都是正确的。
4.2 二维数组传参
二维数组传参,行可以省略,列不能省略(就是横着的不能省略),int[][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);
return 0;
}
4.3 一级指针传参
void print(int * p)
当一个函数的参数部分为一级指针的时候,函数能接收什么参数?
void test(int* p)
{}
int main()
{
int a = 10;
int*ptr = &a;
int arr[10] = {0};
test(&a);
test(ptr);
test(arr);
}
以上这些都可以。
4.4 二级指针传参
void test(int** ptr)
当一个函数的参数部分为二级指针的时候,函数能接收什么参数?
void test(char** p)
{}
int main()
{
char ch = 'w';
char* p = &ch;
char** pp = &p;
char* arr[5];
test(&p);
test(pp);
test(arr);
return 0;
}
5.函数指针
函数是不存在首元素的地址这一说法的,也就是说&ADD和ADD是一样的意思。
函数指针的基本形式:
int ADD(int x,int y)
{
return x+y;
}
int main()
{
int (*pf)(int,int) = &ADD;
return 0;
}
其中,int(*pf)()int就是一个函数指针
pf跟*结合,说明这是指针,后面跟着括号,说明这是个函数指针。函数参数是int int,返回类型也是int。
?如何调用?
int Add(int x,int y)
{
return x+y;
}
int main()
{
int (*pf)(int,int) = &Add;*pf就是一个函数指针,指向Add函数的地址
int sum = (*pf)(2,3);pf就是函数指针解引用
printf("%d",sum);
return 0;
}
实例1:
(*(void(*)())0)();
1.把0强制类型转换为void(*)()这种类型的函数指针
2.再去调用0地址处这个参数为无参,返回类型是void的函数
这是依次函数调用,调用0地址处的函数。
实例2:
void(*signal(int,void(*)(int)))(int);
1.signal是一个函数声明
这个函数的参数有两个,第一个是int类型,第二个是函数指针,该指针指向的函数参数int,返回类型是void
2.signal函数的返回类型也是函数指针,该指针指向的函数参数是int,返回类型是void。
利用函数指针实现加法减法计算器:
我们先完成加减部分的代码,十分简单:
int ADD(int x,int y)
{
return x+y;
}
int SUB(int x,int y)
{
return x-y;
}
主函数:
#include<stdio.h>
int main()
{
int input = 0;
int x = 0;
int y = 0;
int ret = 0;
int(*pf[3])(int,int) = {0,ADD,SUB};这里是初始化函数指针数组,每一个函数指针都指向相对应函数的地址
scanf("%d",&input);
ret = pf[input](x,y);
printf("%d",ret);
return 0;
}
6.回调函数:qsort (快速排序)
qsort是C语言的一个库函数,基于快速排序算法。
我们先来看qsort的基本形式:
void qsort(void* base,指针
size_t num,数组大小,单位是字节
size_t width,每个元素的大小
int(* compare)(const void* e1,const void* e2)
);
比如int arr[10],那么num就是10,width就是4
转到MSDN,
?这是什么呢?这是说e1指向的元素比e2指向的元素小的话,返回值就<0,其他同理。
具体来说,
int main()
{
int arr[] = {1,4,2,6,5,3,7,9,0,8};
int sz = sizeof(arr)/sizeof(arr[0]);
qsort(arr,sz,sizeof(arr[0],compare)
return 0;
}
其中compare函数是传进来的一个函数,我们来完成这个函数.
这个函数的作用就是,比较e1和e2的大小。
int compare(const void*e1,const void* e2)
{
*e1
此时对e1进行解引用操作,编译器会报警告:非法的间接寻址
为什么会出现这个状况呢?因为我们给出的需要排序的类型是int,但是e1给出的类型却是void。我们把整型变量放到void类型中,编译器会报警告。
解决办法就是:我们把需要排序的内容放到一个void*类型中
int a = 0;
void* p = &a;
问题来了,我们为什么要用void* 这个类型呢?
在最初创作qsort函数的时候,创作者并不知道排序的内容的类型是什么,也许是整形数组,字符数组,浮点型数组,结构体数组。写成void*就可以全部接受。
1.void* 是一种无类型的指针,无具体类型的指针。
2.void* 的指针变量可以存放任意类型的地址。
3.void* 的指针不能直接进行解引用操作。
4.void* 的指针不能直接进行加减操作。
用法:
用void* 时应该把其强制类型转换成相应的形态,再解引用。
int compare(const void*e1,const void*e2)
{
if( *(int*)e1 > *(int*)e2)
return 1;
else if( *(int*)e1 > *(int*)e2)
return -1;
else
return 0;
}
这样子e1,e2就能正常解引用操作了。
但其实这段代码比较冗余,可以适当简化:
int compare(const void*e1,const void*e2)
{
return *(int*)e1 - *(int*)e2;
}
这样子就跟MSDN中qsort的定义是一样的了。?
至于具体是怎么完成快速排序的,这里就不进行深究了。过程和冒泡排序是比较相似的,大家有兴趣可以自己尝试编写一下。
6.2 qsort 排序结构体
我们先给定一个结构体:
struct Stu
{
char name[20];
int age;
float score;
};
再把qsort函数写出来:
void test()
{
struct Stu arr[] = {{"zhangsan",20,87.5f},{"lisi",22,99.0f},{"wangwu",10,68.5f}};
int sz = sizeof(arr)/sizeof(arr[0]);
qsort(arr,sz,sizeof(arr[0]),compare);
}
只要把compare函数写出来,跟之前的整型比较相似,只要改变强制类型转换的形态就行了:
int compare(const void*e1,const void*e2)
{
if(((struct Stu*)e1)->score > ((struct Stu*)e2)->score)
{
return 1;
}
else if(((struct Stu*)e1)->score < ((struct Stu*)e2)->score)
{
return -1;
}
else
{
return 0;
}
}
要注意,本次排序是通过score的升序排序的!
最后再完成打印函数:
void print_stu(struct Stu arr[],int sz)
{
int i = 0;
for(i = 0; i < sz; i++)
{
printf("%s %d %f\n",arr[i].name,arr[i].age,arr[i].score);
}
printf("\n");
}
每一步都是遵循着qsort函数的基本形式来完成,利用函数指针完成地址的传递。结构体的一点特点多注意一下,这样一个快速排序的函数就完成了。
7.指针和数组练习题解析
7.1 一维数组
数组名是数组首元素的地址,但是有两个例外:
1.sizeof(数组名),这里的数组名表示整个数组
2.&数组名,这里的数组名也表示整个数组
int a[] = { 1,2,3,4 };
printf("%d\n", sizeof(a));
数组名单独放在sizeof内部,计算的是整个数组的大小,单位是字节,16
printf("%d\n", sizeof(a + 0));
a表示的是首元素的地址,a + 0也是数组首元素的地址,地址大小就是4 / 8
printf("%d\n", sizeof(*a));
a表示的首元素地址,* a就是对首元素的地址的解引用,就是首元素,大小是4
printf("%d\n", sizeof(a + 1));
a表示的首元素地址,a + 1表示第二个元素的地址,地址大小就是4 / 8
printf("%d\n", sizeof(a[1]));
a[1]是数组的第二个元素,大小是4
printf("%d\n", sizeof(&a));
&a取出的是数组的地址,地址的大小就是4 / 8
printf("%d\n", sizeof(*&a));
*和& 相当于抵消了,sizeof(a),大小是16
printf("%d\n", sizeof(&a + 1));
&a + 1直接跳过这个数组,取到的是后面的地址,地址的大小就是4 / 8
printf("%d\n", sizeof(&a[0]));
&a[0]取出数组第一个元素的地址,大小就是4 / 8
printf("%d\n", sizeof(&a[0] + 1));
&a[0] + 1就是第二个元素的地址,大小就是4 / 8
?7.2 字符数组
char arr[] = { 'a','b','c','d','e','f' };
printf("%d\n", sizeof(arr));
arr作为数组名单独放在sizeof内部,计算的是整个数组的大小,单位是字节,6
printf("%d\n", sizeof(arr + 0));
arr就是首元素的地址,+0也还是地址,大小4/8
printf("%d\n", sizeof(*arr));
arr就是首元素的地址,*arr就是首元素,是一个字符,大小是1
printf("%d\n", sizeof(arr[1]));
arr[1]就是数组的第二个元素,是一个字符,大小是1
printf("%d\n", sizeof(&arr));
&arr取出的是地址,大小是4/8
printf("%d\n", sizeof(&arr + 1));
&arr取出的是数组的地址,&arr+1跳过的是整个数组,但是还是地址,大小是4/8
printf("%d\n", sizeof(&arr[0] + 1));
&arr[0]是第一个元素的地址,+1就是第二个元素的地址,大小就是4/8
char arr[] = { 'a','b','c','d','e','f' };
printf("%d\n", strlen(arr));
因为无法知道\0的位置,所以这是个随机值
printf("%d\n", strlen(arr + 0));
arr是首元素的地址,arr+0还是首元素的地址,结果还是随机值
printf("%d\n", strlen(*arr));
&arr取到的是a,但是会识别为a的ASCII码值,传给strlen的就是97,strlen会把97作为起始位置开始统计,会形成内存访问冲突
printf("%d\n", strlen(arr[1]));
和上一个一样
printf("%d\n", strlen(&arr));
&arr是arr数组的地址,虽然类型和strlen的参数类型有所差异,但是传参过去后,还是从第一个字符的位置向后数位置,结果还是随机
printf("%d\n", strlen(&arr + 1));
随机值
printf("%d\n", strien(&arr[0] + 1));
随机值
char arr[] = "abcdef";
printf("%d\n", sizeof(arr));
7字节
printf("%d\n", sizeof(arr + 0));
地址,4/8
printf("Xd\n", sizeof(*arr));
a,就是1
printf("%d\n", sizeof(arr[1]));
1
printf("Xd\n", sizeof(&arr));
地址4/8
printf("%d\n", sizeof(&arr + 1));
地址,4/8
printf("%d\n", sizeof(&arr[0] + 1));
地址,4 / 8
printf("%d\n", strlen(arr));
首元素地址,到\0,大小是6
printf("%d\n", strlen(arr + 0));
6
printf("%d\n", strien(*arr));
err
printf("%d\n", strien(arr[1]));
err
printf("Xd\n", strlen(&arr));
6
printf("%d\n", strlen(&arr + 1));
随机值
printf("%d\n", strlen(&arr[0] + 1));
5
char* p = "abcdef";
p指向的是字符串的地址
printf("%d\n", sizeof(p));
p是一个指针变量,大小是地址的地址4/8
printf("%d\n", sizeof(p + 1));
地址+1还是一个地址,大小还是4/8
printf("%d\n", sizeof(*p));
p是char*的指针,解引用访问一个字节,所以大小是1
printf("%d\n", sizeof(p[0]));
p[0] 等价于 *(p+0)也就是*p,大小是1
printf("%d\n", sizeof(&p));
p是个变量,取出的是p的地址,是地址大小就是4/8
printf("%d\n", sizeof(&p + 1));
类型是char**,还是一个地址,大小就是4/8
printf("%d\n", sizeof(&p[0] + 1));
&p[0]就是a的地址,那么最终取出的就是b的地址,地址大小就是4/8
char* p = "abcdef";
printf("%d\n", strlen(p));
从p开始数字符串的长度,长度就是6
printf("%d\n", strlen(p + l));
跟上面的一样,长度是5
printf("%d\n", strlen(*p));
传过去的是ASCII码值97,,err
printf("%d\n", strien(p[0]));
err
printf("%d\n", strlen(&p));
随机值
printf("%d\n", strien(&p + 1));
随机值
printf("%d\n", strlen(&p[0] + 1));
p[0]等价于*(p+0)等价于*p,就是从第二个字符的位置向后数字符串,大小是5
7.3 二维数组
int main()
{
int a[3][4] = { 0 };
printf("%d\n", sizeof(a));
计算的是整个数组的大小,3*4*4=48
printf("%d\n", sizeof(a[0][0]));
第一个元素,大小是4
printf("%d\n", sizeof(a[0]));
表示的是第一行的数组名,计算的是第一行的大小,大小是16
printf("%d\n", sizeof(a[0] + 1));
a[0]作为第一行的数组名,没有&,没有单独放在sizeof内部,所以a[0]表示的就是首元素的地址,
即a[0][0]的地址,a[0]+1就是第一行第二个元素的地址,大小就是4/8
printf("%d\n", sizeof(*(a[0] + 1)));
第一行第二个元素,大小是4
printf("%d\n", sizeof(a + 1));
a是二维数组的数组名,没有&,没有单独放在sizeof内部,a表示首元素的地址,即第一行的地址,a+1就是第二行地址,是类型为int(*)[4]的数组指针
printf("%d\n", sizeof(*(a + 1)));
*(a+1)就是第二行,相当于第二行的数组名,sizeof计算的是第二行的大小,就是16
printf("%d\n", sizeof(&a[0] + 1));
a[0]时第一行的地址,&a[0]是第一行的地址,+1就是第二行的地址,4/8
printf("%d\n", sizeof(*(&a[0] + 1)));
上面的例子再解引用,拿到的是第二行,就是a[1],大小是16
printf("%d\n", sizeof(*a));
a是二维数组的数组名,没有&,没有单独放在sizeof内部,a表示首元素的地址,*a也就是二维数组的首元素,也就是第一行,大小就是16
printf("%d\n", sizeof(a[3]));
int[4],就是16
return 0;
}
二维数组的相关理解比较难,特别是要清楚二维数组的首元素就是第一行,+1就直接拿到了第二行。
8.指针笔试题
8.1
int main()
{
int a[5] = { 1,2,3,4,5 };
int* ptr = (int*)(&a + 1);
printf("%d %d", *(a + 1), *(ptr - 1));
return 0;
}
&a+1跳过的是真个数组,所以ptr是在5的后面的那个地址,所以最终打印的是2和5
8.2
struct Test
{
int Num;
char* pcName;
short sDate;
char cha[2];
short sBa[4];
}*p;
假设p 的值为0x100000,如下表达式的值分别是多少?
已知结构体Test类型的变量大小是20个字节
int main()
{
p = (struct Test*)0x100000;
printf("%p\n", p + 0x1);
printf("%p\n", (unsigned long)p + 0x1);
printf("%p\n", (unsigned int*)p + 0x1);
return 0;
}
这个题我们一个一个看,
printf("%p\n", p + 0x1);
在这里,p的结构体类型的指针,p作为一个指针+1,
那么就是跳过整个结构体,结构体的大小是20个字节,
以16进制打印出来就是0x100014
printf("%p\n", (unsigned long)p + 0x1);
这里把p强制类型转换成为了无符号的整型,一个整型加上一个1,
就变成了简单的数字加减,所以打印出来的是0x100001
printf("%p\n", (unsigned int*)p + 0x1);
p转换成了整形指针,整形指针+1就是+4个字节,
打印出来就是0x100004
8.3?
int main()
{
int a[4] = { 1, 2, 3, 4 };
int* ptr1 = (int*)(&a + 1);
int* ptr2 = (int*)((int)a + 1);
printf("%x", ptr1[-1]);
printf("%x",*ptr2);
return 0;
}
printf("%x", ptr1[-1]);
ptr1[-1]等价于*(ptr1-1),ptr1是&a+1也就是4后面的地址,
那么-1以后就是4
printf("%x",*ptr2);
至于这个,我们慢慢分析:
首先把a转换为整型类型,再+1,我们假设这个地址是0x10,那么+1就是0x11,再强制类型转换为整型指针,也就是说只是把地址往后移动了一个字节,我们在这里以小端为例:
?那么读取的时候读取到的就是00 00 00 02,所以还原出来的数字就是0x02000000,所以这个题最终打印出来的结果就是4和2000000
8.4
int main()
{
int a[3][2] = { (0, 1),(2, 3),(4, 5) };
int* p;
p = a[0];
printf("%d", p[0]);
return 0;
}
首先,这是个逗号表达式,也就是说,这个二维数组是int a[3][2] = {1,3,5,0,0,0}
分析到这里就容易了,p指向的就是1,打印的结果是1
8.5
int main()
{
int a[5][5];
int(*p)[4];
p = a;
printf("%p", &p[4][2] - &a[4][2]);
printf("%d", &p[4][2] - &a[4][2]);
return 0;
}
首先要知道,&p[4][2] 和 &a[4][2]是不同的含义,前者是一个数组指针,而后者确实二维数组访问的元素的首地址,&p[4][2]意思就是*(*(p+4)+2)
绿色的是&p[4][2],其中p认为它指向的是4个整型的数组,p+1跳过的就是4个字节,所以绿色的方块就是&p[4][2],红色的方块就是&a[4][2]
printf("%p", &p[4][2] - &a[4][2]);
printf("%d", &p[4][2] - &a[4][2]);
%p打印的是地址,两个元素中间差了-4,所以%d打印出来的就是-4,那么-4在内存中通过转换,补码就是11111111111111111111111111111100,这个值16进制打印出来的就是ff ff ff fe,所以两个结果分别是 fffffffe,-4
8.6
int main()
{
int aa[2][5] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
int* ptr1 = (int*)(&aa + 1);
int* ptr2 = (int*)(*(aa + 1));
printf("%d,%d", *(ptr1 - 1), *(ptr2 - 1));
return 0;
}
这个题就相对来说比较简单了,&aa+1拿到的是10后面那个地址,再-1拿到的就是10
ptr2就是aa+1再解引用,因为aa是一个二维数组,aa+1拿到的就是第二行的地址,也就是6,再-1拿到的就是5,打印出来就是10和5
8.7
int main()
{
char* a[] = { "work","at","alibaba" };
char** pa = a;
pa++;
printf("%s\n",* pa);
return 0;
}
这个其实很简单,pa指向的就是work,+1就是at
8.8
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;
}
?首先这是简单的分析,指针指向的位置
printf("%s\n", **++cpp);
++cpp后,cpp指向的位置也发生了变化:
?这时解引用拿到的是c+2,最后打印出来的就是point
printf("%s\n", *-- * ++cpp + 3);
这个乍一看,*--是什么东西?我们研究的是cpp这个对象,cpp++后再解引用,拿到的是c+1,再--,c+1变成了c,于是指向的是原来c指向的地址,所以是ENTER,但是最后还+3,所以打印的就是 ER
printf("%s\n", *cpp[-2] + 3);
cpp[-2]+3也就是*(cpp-2)+3,指向的是FIRST,+3就是ST
printf("%s\n", cpp[-1][-1] + 1);
cpp[-1][-1]等价于*(*(cpp-1)-1),,最终指向的是EW
|