【C语言——指针进阶】
前言
前面,在指针初阶中我们学习了什么是指针,指针和指针类型,野指针,指针运算,指针和数组,二级指针,指针数组。在本章中,我们将继续来了解指针。本章的重点内容有字符指针,数组指针,指针数组,数组传参和指针传参,函数指针,函数指针数组,指向函数指针数组的指针,回调函数,指针和数组面试题的解析
1.字符指针
在指针类型中我们知道有一种指针类型叫做字符指针char*
字符指针的使用方式一:
int mian()
{
char ch = 'w';
char* p = &ch;
*p = 'c';
return 0;
}
字符指针的使用方式二:
int main()
{
char* p = "abcdef";
printf("%s", p);
return 0;
}
这里是把字符串放在了指针变量p里面了吗?不是这样的.这里是把字符串的首地址放在了指针变量中.
字符指着指向字符串:
方式一:
char* p = "abcdef";
易错:
char* p = "abcdef";
*p = 'w';
这段代码是错误的,指针变量p解引用找到的是字符串的首元素,首元素是常量,常量不能被赋值.编译器报错: 不可修改的左值.
方式二:
char arr[] = "abcdef";
char* p = arr;
*p = 'w';
这段代码是正确的,因为对指针变量p解引用得到的是数组的首元素,数组是变量,可以被修改.
为了防止方式一中的错误发生,方式一的代码应修改为:
const char* p = "abcdef";
被const关键词修饰之后,指针变量p指向的内容不可以修改.
重要的事情说三遍:
这里不是将字符串放在了指针变量中,而是将字符串的首元素的地址放在了指针变量p中.
理解了上面的内容,再来看看这一道面试题:
int mian()
{
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 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;
}
解析:
这里的str1和str2是两个数组名,它们在内存中开辟了两块儿空间,所以str1和str2的地址是不一样的,所以str1不等于str2.
str3和str4是两个指针,它们指向同一个字符串,指向了同一块空间,所以str3和str4是相同的.
2.指针数组
在指针初阶中我们已经已经学习了指针数组,指针数组是一个存放指针的数组.
让我们复习一下,下面的指针数组是什么意思.
int* arr[10];//整型指针数组,用来存放整型的指针
char* arr1[10];//字符指针数组,用来存放一级字符指针
char** arr2[10];//二级字符指针数组,用来存放一级字符指针
字符型指针数组:
int main()
{
//在字符指针数组中存放了三个字符串的首地址
char* arr[] = { "abcdef","hehe","gert" };
for (int i = 0; i < 3; i++)
{
printf("%p\n", arr[i]);
}
return 0;
}
整型指针数组:
int main()
{
int arr1[] = { 1,2,3,4 };
int arr2[] = { 5,6,7,8 };
int arr3[] = { 9,10,11,12 };
//在整型指针数组中存放三个整型指针
int* arr[] = { arr1,arr2,arr3 };
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 4; j++)
{
printf("%2d ", *(arr[i] + j));
}
printf("\n");
}
return 0;
}
3.数组指针
3.1 数组指针的定义
数组指针是指向数组的指针,类比于整型指针,指向整型的指针.数组指针的定义:
int(*p)[10];
*号与p结合,表明p是一个指针变量,除了变量名p,剩下的 int ( *)[10]是它的类型.表明该指针变量指向一个存放10个整型元素的数组.
注意:[]的优先级高于 *,所以必须加上()来保证p先和 *结合.
3.2 &数组名VS数组名
&数组名和数组名有什么区别?
**数组名是首元素的地址,&数组名取出的是整个 数组的地址.**它们在地址上是相同的,意义不同,也就是加减一个整数时,跳过的字节数不同.
例如:
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和&arr,两者的地址都是一样的.但是加一之后,arr跳过了4个字节,也就是一个整型的大小.&arr跳过了40个字节,也就是一个数组的大小.所以说,在意义上,arr代表数组首元素的地址,&arr代表整个数组的地址.这一内容.在数组章节详细的叙述过.感兴趣的朋友可以看看.
数组名和&数组名总结
数组名在绝大多数情况下表示的都是数组首元素的地址,但是有两个例外:
1.sizeof(数组名)--------sizeof内部单独放一个数组名时,表示的是整个数组.计算得到的是数组的总大小.
2.&数组名表示的是整个数组,取出的是整个数组的大小.从地址值的角度来看,数组名和&数组名的值是一样的,但是意义不一样.
3.3 数组指针的使用
3.3 1 一维数组传参 形参是一维数组
void print(int arr[], int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
}
int main()
{
int arr[] = { 1,2,3,4,5,6 };
int sz = sizeof(arr) / sizeof(arr[0]);
print(arr, sz);
return 0;
}
3.3 2 二维数组传参 形参是二维数组
void print(int arr[][3], int row, int col)
{
int i = 0;
for (i = 0; i < row; i++)
{
for (int j = 0; j < col; j++)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
}
int main()
{
int arr[3][3] = {1,2,3,4,5,6,7,8,9};
print(arr, 3,3);
return 0;
}
3.3 3 二维数组传参 形参是数组指针
二维数组的元素是一维数组,二维数组的数组名表示首元素的地址。即一维数组的地址,数组的地址存放在数组指针中,所以二维数组传参,形参可以是数组指针。
例子:
void print(int (*arr)[3], int row, int col)
{
int i = 0;
for (i = 0; i < row; i++)
{
for (int j = 0; j < col; j++)
{
//这里的arr[i]相当于某一行的数组名,加上j表示访问该行的其它元素
printf("%d ", *(arr[i]+j));
}
printf("\n");
}
}
int main()
{
int arr[3][3] = {1,2,3,4,5,6,7,8,9};
print(arr, 3,3);
return 0;
}
学了指针数组和数组指针之后,看看下面的代码表示的是什么意思
int arr[5];//表示有5个元素的整型数组
int* parr1[10];//表示一个指针数组,该数组中存放了10个整型的指针
int(*parr2)[10];//数组指针,指向存放10个整型元素的数组
int(*parr3[10])[5];
//它是一个数组,数组里面有10个元素,数组的类型是int (*)[5];
//即数组中存放10个数组指针,这些指针指向有5个元素的整型数组,所以它是一个数组指针的数组
4.数组参数 指针参数
4.1 一维数组传参
//正确:一维数组传参,形参是一维数组,不用表明有几个元素,
//因为它不会真的创建一个数组,它仅仅是寻址
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);
return 0;
}
总结:
一维数组传参(除指针数组外),形参可以是一维数组,可以是一级指针;一维指针数组传参,形参可以是一维指针数组,也可以是二级指针。
4.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);
return 0;
}
总结:二维数组传参,形参可以是二维数组,也可以是数组指针
4.3 一级指针传参
void print(int* p, int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", *(p + i));
}
}
int main()
{
int arr[4] = { 1,2,3,4 };
int* p = arr;
int sz = sizeof(arr) / sizeof(arr[0]);
print(p, sz);
return 0;
}
思考:
当一个函数的参数部分是一级指针时,函数能接收什么参数?
答:可以接受地址,数组,一级指针
4.4 二级指针传参
void test(int **ptr)
{
printf("num = %d\n", **ptr);
}
int main()
{
int n = 10;
int* p = &n;
int** pp = &p;
test(pp);
test(&p);
int* arr[10];
test(arr);
return 0;
}
思考:
二级指针作为函数参数时,可以接受什么参数呢?
答:二级指针,一级指针的地址,指针数组的数组名
5.函数指针
5.1 什么是函数指针
函数指针,顾名思义,指向函数的指针,也就是存放函数地址的指针,那函数有地址吗
void test()
{
printf("hehe\n");
}
int main()
{
printf("%p\n", test);
printf("%p\n", &test);
return 0;
}
事实证明,函数是有地址的,并且函数名和&函数名得到的结果是一样的。
5.2 函数指针的声明
void test()
{
printf("hehe\n");
}
void (*p)();//正确
void* p();//错误
函数指针的声明:
void (*test)();
✳与p结合,说明p是一个指针,p是变量名,除了p之外的部分就是类型,p的类型是 void (* )(); 即p是一个函数指针,指向一个没有参数,返回值为void 的函数。
练习一下以下函数指针的声明:
int test(int a,int b)
{}
int (*pt)(int,int)
char test1(int a, char b)
{}
char (*pp)(int,char)
5.3 函数指针的使用
int Add(int x, int y)
{
return x + y;
}
int main()
{
int (*pt)(int, int) = Add;
printf("%d ", pt(2, 3));
return 0;
}
读下面两段有趣的代码:
代码一:
(*(void(*)())0)();
解析:void(*)()是一个函数指针类型。在它的外面套上一层()代表着强制转换。将0这个整型类型的数强制转换成一个函数指针类型。 ✳对0解引用,访问0这个函数指针指向的函数,该函数的参数为0.
代码二、
void (*signal(int, void(*)(int)))(int);
解析:内部 signal(int, void()(int)):signal先和()结合,说明signal是一个函数。它有两个参数,参数类型分别为int型和一个函数指针类型,整个void (signal(int, void()(int)))(int);,除了函数名和参数类型:signal(int, void()(int));剩下的就是函数的返回值类型,也就是void ( )(int);所以,该段代码是一个函数,它的返回值类型是一个函数指针类型,它的参数是一个int型和一个函数指针类型。
代码二太过复杂,如何简化:
typedef void(* ptr)(int);//将ptr定义为void (* )(int)类型
ptr signal(int,ptr);//用ptr类型代替代码二中的void (* )(int)部分
6.函数指针数组
函数指针数组是存放函数指针的数组。
6.1 函数指针数组的定义
int (*parr1[10])();
这里的parr1先和[]结合,表示一个数组,数组的元素类型为int (* )();就是说数组的类型是函数指针类型。数组中每一个元素都是一个函数指针。
6.2 函数指针数组的使用
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("-----------------------------\n");
}
int main()
{
int input = 1;
int x, y;
int (*arr[5])() = { 0,add,sub,mul,div };//巧妙设计:输入0是退出
while (input)
{
printf("----------------------------\n");
printf(" 1.add 2.sub\n");
printf(" 3.mul 4.div\n");
printf(" 0.退出\n");
printf("-----------------------------\n");
printf("请输入你的选择:\n");
scanf("%d", &input);
int ret = 0;
if (input == 0)
{
break;
}
if (input >= 1 && input <= 4)
{
printf("请输入两个操作数:");
scanf("%d %d", &x, &y);
ret = arr[input](x, y);//函数指针数组的使用
}
else
printf("输入有误");
printf("ret = %d\n", ret);
}
return 0;
}
7.指向函数指针数组的指针
指向函数指针数组的指针是一个指针,它指向的数组中的每一个元素都是一个函数指针。
int main()
{
//函数指针
void (*ptr)(const char*) = test;
//函数指针数组
void(*ppt[5])(const char*);
ppt[0] = ptr;
//指向函数指针数组的指针
void(*(*pt)[5])(const char*) = &ppt;
return 0;
}
8.回调函数
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
8.1 回调函数实现计算器(加减乘除)
//回调函数实现计算器(加减乘除)
//加减乘除函数实现
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 pf(int(*pt)(int, int))
{
printf("请输入两个操作数:");
int x = 0;
int y = 0;
scanf("%d %d", &x, &y);
int ret = pt(x, y);//回调函数
printf("计算结果为:%d\n", ret);
}
void menu()
{
printf("-----------------------------\n");
printf("---1.Add 2.sub---\n");
printf("---3.mul 4.div---\n");
printf("---0.退出---\n");
}
int main()
{
int input = 0;
do
{
menu();
printf("请输入你的选择:> ");
scanf("%d", &input);
switch (input)
{
case 1:
pf(Add);
break;
case 2:
pf(Sub);
break;
case 3:
pf(mul);
break;
case 4:
pf(div);
break;
case 0:
printf("退出程序\n");
break;
default:
printf("输入错误,请重新输入\n");
break;
}
} while (input);
return 0;
}
8.2 回调函数模拟实现qsort函数(采用冒泡排序)
//用回调函数模拟实现qsort函数(采用冒泡排序)
//比较函数
int cmp(const void* s1, const void* s2)
{
return (*(int*)s1 - *(int*)s2);
}
void swap(const void* p, const void* p1, int sd)
{
for (int i = 0; i < sd; i++)//一个字节一个字节的交换
{
char temp = *((char*)p+i);
*((char*)p + i) = *((char*)p1 + i);
*((char*)p1 + i) = temp;
}
}
void bubble_sort(void* base, int sz, int sd,int(*cmp)(void*,void*))//qsort功能需要的参数
{
for (int i = 0; i < sz - 1; i++)
{
for (int j = 0; j < sz - i - 1; j++)
{
if (cmp((char*)base + j * sd, (char*)base + (j + 1) * sd)>0)//如果前者比后者大,则交换
{
swap((char*)base + j * sd, (char*)base + (j + 1) * sd, sd);
}
}
}
}
int main()
{
int arr[] = { 1,3,2,5,6,8,4,9,12};
int sz = sizeof(arr) / sizeof(arr[0]);
bubble_sort(arr, sz, sizeof(arr[0]), cmp);
for (int i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
总结:
本篇中我们深刻的讲解了字符数组,指针数组,数组指针:数组指针的定义、数组名和&数组名的区别,数组指针的使用:一维数组传参,二维数组传参的参数问题。数组参数,指针参数。函数指针:什么是函数指针,函数指针的声明,函数指针的使用。函数指针数组:函数指针数组的定义,函数指针数组的使用,指向函数指针数组的指针。回调函数,回调函数实现计算器,回调函数模拟实现qsort函数。该篇内容较为深刻,其中涉及到的知识点,可以翻翻我往期的博客。
一键三连,点个关注,不迷路~