C语言的指针(进阶)
你好,这里是新人 Sunfor
这篇是我最近对于C语言指针的学习心得和错题整理
有任何错误欢迎指正,欢迎交流!
会持续更新,希望对你有所帮助,我们一起学习,一起进步
前言
在上一篇更新的指针初阶内容(点击进入回顾),我们大致了解了何为指针,以及如何应用指针
但是指针的内容远远不止于此,接下来,让我们一起向指针的进阶内容迈进!
一、字符指针变量
首先,字符指针变量是指向字符型数据的指针
它用于存储字符型变量或字符数组的内存地址
int main()
{
char p = 'w';
char* ptr = &p;
printf("%c\n", *ptr);
return 0;
}
在这段代码中,ptr是一个字符指针
存储了字符变量p的地址
因此可以通过 *ptr 访问 p 的值
int main()
{
char arr[] = "Hello World!";
char* parr = arr;
while (*parr != '\0')
{
printf("%c ", *parr);
parr++;
}
return 0;
}
在这段代码中
通过字符指针 parr遍历打印字符串arr的每一个字节
直到遇到字符串结束标志 ‘\0’
了解了基本用法之后
我们可以一起来做一道练习题
#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 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 是赋值了相同的字符内容,但是开辟了两块不同的空间
str3 和str4 是两个字符指针,指向了同一个地址,也就是在同样一块空间中
运行结果如下:
二、数组指针变量
在上一节初阶指针的内容中,我们了解了什么是指针数组
那么在这一节中,我们会学习什么是数组指针
1.数组指针的概念
数组指针是一个用于指向数组的指针
让我们再来复习一下数组和指针的关系
- 数组名 是指针常量
- 数组指针 是指向整个数组的指针
它是一个指针,指向数组的起始位置,同时包含有关数组大小的信息
了解了数组指针的基本概念后,我们在代码中如何分辨谁是数组指针呢?
int* p1[10];
int(*p2)[10];
p1 和 p2 分别是什么呢?
首先,我们要知道 [] 的优先级高于 *
所以必须加上 () 来保证 p 先和 * 结合
在上述例子中
p2 先和 * 结合,说明 p2 是一个指针变量
指向一个大小为10的整型数组
所以 p2 是一个数组指针
而 p1 则先与 [] 结合 再与 int* 结合
表示 p1是一个可以存放10个 int* 类型元素的数组
所以 p1 是一个指针数组
2.数组指针如何初始化
知道如何分辨数组指针之后,我们要学习如何初始化数组指针
大家还是否记得在初阶指针内容中我们提到的对数组名的理解
例如:如果arr为数组名
arr 可以直接表示数组首元素的地址,但是有两个例外:sizeof(arr) 和 &arr
- sizeof(arr) 计算的是整个数组的大小
- &arr 取出的是整个数组的地址
那么数组指针变量的初始化需要用到的就是 &arr(数组名)
在这张图片中我们可以观察到:
p和&arr的类型是一样的
3.二维数组传参的本质
有了对数组指针的了解之后,我们就可以深入了解二维数组传参的本质了
在之前学习的知识中如果我们有二维数组需要传递参数给函数时我们是如何实现的呢
void text(int arr[3][5], int a, int b)
{
int i = 0;
int j = 0;
for (i = 0; i < a; i++)
{
for (j = 0; j < b; 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},
};
text(arr, 3, 5);
return 0;
}
这样调用二维数组我们可以明显地看出
text函数的行数和列数都是确定好的
这样使得函数不够灵活
同样的我们可以通过图片来理解一下二维数组
在图片中我们可以观察到
二维数组的数组名表示的是第一行数组的地址
第一行的一维数组的类型是 int[5]
所以第一行数组的地址是数组指针 int(*)[5]
这样,我们就明白二维数组传参,形参部分可以写成数组,也可以写成指针
#define ROWS 3
#define COLS 5
void text(int arr[][COLS], int rows)
{
int i = 0;
int j = 0;
for (i = 0; i < rows; i++)
{
for (j = 0; j < COLS; j++)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
}
int main()
{
int arr[ROWS][COLS] = { {1,2,3,4,5},
{2,3,4,5,6},
{3,4,5,6,7},
};
text(arr, ROWS);
return 0;
}
把我们刚才的代码进行优化
首先传给函数二维数组的地址
其次,将行列的信息改为宏定义,使得函数的灵活性更高
4.数组指针VS指针数组
认识和了解数组指针和指针数组之后,我们需要知道这两者之间的区别和练习,这样也方便我们之后准确的使用
- 声明方式的对比
- 使用方式的对比
- 用途的对比
数组指针:通常用于函数参数,表示传递整个数组到函数中
允许函数操作整个数组或通过指针访问数组中的元素
指针数组:用于存储多个指针,常用于处理动态内存分配、字符串数组等场景
三、函数指针变量
根据我们之前对于整型指针,数组指针的学习
我们可以类比得到:函数指针变量是用来存放函数地址的,我们可以通过地址来调用函数
在之前的学习中,我们只是使用函数,但没有去注意函数的地址
#include<stdio.h>
void text()
{
printf("hello world");
}
int main()
{
printf("text: %p\n",text);
printf("&text:%p\n", text);
return 0;
}
输出结果如下:
我们可以看到函数确实是有地址的
函数名就是函数的地址
我们同样可以通过 &函数名 的方式获得函数的地址
那么存放函数的地址,就需要用到函数指针
void text()
{
printf("hello world");
}
void(*pf1)() = &text;
void(*pf1)() = text;
int Add(int x,int y)
{
return x + y;
}
int(*pf3)(int, int) = &Add;
int(*pf4)(int x, int y) = Add;//x,y可以省略
这就是函数指针变量的声明
接下来我们看看怎么使用函数指针变量
#include<stdio.h>
int Add(int x, int y)
{
return x + y;
}
int main()
{
int(*pf3)(int, int) = Add;
printf("%d\n", (*pf3)(1, 3));
printf("%d\n", pf3(2, 4));
return 0;
}
我们来看看运行的结果
说明以上两种使用方式都是OK的
四、函数指针数组
在之前的初阶指针内容中我们已经学习了指针数组
那么什么是函数指针数组呢把函数的地址存到一个数组中,那么这个数组就叫做函数指针数组
#include<stdio.h>
int sub(int a, int b)
{
return a - b;
}
int mul(int a, int b)
{
return a * b;
}
int main()
{
int(*fun_arr[3])(int, int) = { add,sub,mul };
int a = 10, b = 5;
printf("Add:%d\n", fun_arr[0](a, b));
printf("Sub:%d\n", fun_arr[1](a, b));
printf("mul:%d\n", fun_arr[2](a, b));
return 0;
}
运行结果如下:
在这个例子中
fun_arr是一个包含3个指向 int返回类型函数的指针的数组,每个函数接受两个 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("*** 0:exit ***\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 = add(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;
}
void calc(int(*pf)(int, int))
{
int ret = 0;
int x, y;
printf("请输入操作数:");
scanf("%d %d", &x, &y);
ret = pf(x, y);
printf("ret = %d\n", ret);
}
int main()
{
int input = 1;
do
{
printf("*************************\n");
printf("*** 1:add 2:sub ***\n");
printf("*** 3:mul 4:div ***\n");
printf("*** 0:exit ***\n");
printf("请选择:");
scanf("%d", &input);
switch (input)
{
case 1:
calc(add);
break;
case 2:
calc(sub);
break;
case 3:
calc(mul);
break;
case 4:
calc(div);
break;
case 0:
printf("退出程序\n");
break;
default:
printf("选择错误\n");
break;
}
} while (input);
return 0;
}
通过对比,我们可以发现
利用回调函数,我们可以把调用的函数的地址以参数的形式传递过去,使用函数指针接收,函数指针指向什么函数就调用什么函数,这样使得代码的逻辑更加清晰且有逻辑性
六、习题整理分析
了解和学习指针的知识之后,更重要的是学会如何正确使用指针
接下来,我会从我自己的错题出发,总结归纳指针的知识
下面函数的输出结果为
int main()
{
unsigned long pulArray[] = { 6,7,8,9,10 };
unsigned long* pulPtr;
pulPtr = pulArray;//取到数组的首元素的地址 则为6的地址
*(pulPtr + 3) += 3;//pulPtr 再向后+3 则指向9的地址 解引用后为9 再+3 = 12
printf("%d,%d\n", *pulPtr, *(pulPtr + 3));//*pulPtr为6 *(pulPtr + 3)为12
return 0;
}
结合图像来理解
写一个函数,可以逆序字符串
首先我们需要理出大致的思路
逆序字符串,我们可以利用指针,将指针的首元素的地址与最后一位的地址交换
从首元素向后走,从最后一个向前走,依次交换
可以放入一个循环中,循环条件是:左指针的地址小于右指针的地址
void Revert(char* str)
{
char* left = str;
char* right = str + strlen(str) - 1;
while (left < right)
{
char temp = *left;
*left = *right;
*right = temp;
left++;
right--;
}
}
int main()
{
char str[] = "Hello world";
printf("%s\n", str);
Revert(str);
printf("%s\n", str);
return 0;
}
我选择的是D
但是全局函数是不受函数的结束而结束,在函数中改变全局变量,主调函数中可以看到改变之后的结果
正确答案是A
一个函数只能返回一个结果
实现一个函数,可以左旋字符串中的K个字符
初步逻辑:
设计一个循环,使其可以旋转1次,执行k次
void leftRound(char* src, int time)
{
int i, j, tmp;
int len = strlen(src);
time %= len;
//取模操作:优化效率,当time大于字符串长度len时,重复的移动操作是冗余的
//例如:当字符串的长度为6时,左移10次相当于左移4次,因为移动6次就回到原始位置
//所以10次移动等同于10 % 6 = 4次
for (i = 0; i < time; i++)//循环执行指定的移动次数
{
tmp = src[0];//保存字符串的第一个字符,因为我们将要修改src,需要先保留这个字符
for (j = 0; j < len - 1; j++)//将字符串中的字符向左移动一位
{
src[j] = src[j + 1];
}
src[j] = tmp;//将之前保存的第一个字符tmp放在字符串的最后一个位置
}
}
我一开始使用的就是这也方法,这个思路是我们最容易想到的,但是一次次转还是太麻烦且浪费时间
那我们就可以试试拼接的方法
void leftRound(char* src, int time)
{
int len = strlen(src);
int pos = time % len;//字符串实际左移的个数
char tmp[256] = { 0 };//临时数组,用于存放操作后的数据
strcpy(tmp, src + pos);//将需要操作的数的后面的数字全部复制到tmp数组中
strncat(tmp, src, pos);//将要操作的两个数追加到刚才的数据的末尾
strcpy(src, tmp);//将完成操作的字符串重新赋值给src
}
上面的方法相对于第一种来说,效率已经提高不少,但是需要创建一个临时数组,也有些麻烦,让我们试着使用反转方法
void reverse_part(char* str, int start, int end)
{
int i, j;
char tmp;
for (i = start, j = end; i < j; i++, j--)
{
tmp = str[i];
str[i] = str[j];
str[j] = tmp;
}
}
void leftRound(char* str, int time)
{
int len = strlen(str);
int pos = time % len;
reverse_part(str, 0, pos - 1);//逆序前端
reverse_part(str, pos, len - 1);//逆序后端
reverse_part(str, 0, len - 1);//整体逆序
}
可能反转的操作有些抽象,让我们结合图像来理解
每一次反转操作中都利用了字符串的交换来有效地完成轮转,比之前的两个方法都更加高效
调整数组使奇数全部位于偶数的前面
编写思路:
定义2个下标:left(指向数组的起始位置)和right(指向数组的最后一个元素的位置)
left往后找 遇到偶数停下
right往前找,遇到奇数停下
如果奇数偶数都找到了,就交换这两个数据的位置
直到两个指针相遇
void swap_arr(int arr[], int sz)
{
int left = 0;
int right = sz - 1;
int tmp = 0;
while (left < right)
{
// 从前往后,找到一个偶数,找到后停止
while ((left < right) && (arr[left] % 2 == 1))
{
left++;
}
// 从后往前找,找一个奇数,找到后停止
while ((left < right) && (arr[right] % 2 == 0))
{
right--;
}
// 如果偶数和奇数都找到,交换这两个数据的位置
// 然后继续找,直到两个指针相遇
if (left < right)
{
tmp = arr[left];
arr[left] = arr[right];
arr[right] = tmp;
}
}
}
使用指针打印数组内容,不使用数组下标
思路:
首先我们需要定义数组,然后定义指针,先指向数组的首地址,然后依次++
int main()
{
int arr[] = { 1,2,3,4,5,6.7,8,9,0 };
int* p = arr;
for (int i = 0; i < (sizeof(arr) / sizeof(arr[0])); i++)
{
printf("%d ", *p);
p++;
}
return 0;
}
冒泡排序
实现对整型数组的冒泡排序
思路:
遍历数组,对数组中相邻的两个元素进行比较
若为升序,前一个数据大于后一个数据时,两者交换位置上的信息
直到所有数据比较完
void BubbleSort(int arr[], int size)
{
for (int i = 0; i < size - 1; i++)//外层循环确定比较的趟数
{
for (int j = 1; j < size - i; j++)
//内层循环从第二个数开始,相邻两个数进行比较
//size - i是为了避免不及要的比较,因为每比完一次,就会把最后一位的元素排好
{
if (arr[j - 1] > arr[j])
{
int tmp = arr[j - 1];
arr[j - 1] = arr[j];
arr[j] = tmp;
}
}
}
}
int main()
{
int arr[] = { 9,10,2,1,3,6,7,4,8,5 };
int size = sizeof(arr) / sizeof(arr[0]);
BubbleSort(arr, size);
for (int i = 0; i < size; ++i)
{
printf("%d ", arr[i]);
}
return 0;
}
我选择了A
正确答案是B
首先排除C、D,它们两个在前半句都不是定义而是函数的声明
然后再看AB,不同点在于后面参数的选择,函数定义中的参数要与使用时保持一致
值得注意的是:对于函数名来说,前面的&和*都会被忽略
一个数组中只有两个数字是出现一次,其他所有数字都出现了两次
编写一个函数找出两个只出现一次的数字
主要利用异或思想,异或两次等于没异或
如果有两个数字只出现一次,那么异或完的结果> 就是这两个数异或
例如1 2 3 4 2 1 异或完应该是 3^4得到111
以这一位的值将结果分为两组
再分别异或这两组数 就可以找到这两个数
void findnumber(int* num, int sz, int* result1, int* result2)
{
int a = 0;
//1.异或所有数字
for (int i = 0; i < sz; i++)
{
a ^= num[i];
}
//确定a的最低位的1
int low_bit = a & -a;
//初始化结果
*result1 = 0;
*result2 = 0;
//根据low_bit的值将数字分为两组
for (int i = 0; i < sz; i++)
{
if (num[i] & low_bit)//表示最低为1
{
*result1 ^= num[i];
}
else//表示最低位为0
*result2 ^= num[i];
}
}
int main()
{
int num[] = { 1,2,3,4,5,1,2,3,4,6 };
int sz = sizeof(num) / sizeof(num[0]);
int result1, result2;
findnumber(num, sz, &result1, &result2);
printf("只出现两次的数字:%d and %d\n", result1, result2);
return 0;
}
我选择的是A
正确答案是C
首先题目考察我们的是二维数组本质的理解
二维数组相当于数组的数组,int arr[3][5]相当于是3个元素的arr,每个元素是int[5], 所以int [5]的类型不能省略,数组的元素个数可以省略
A丢失了类型中的5,B变成了指针数组,D省略了5,留下了3
我的答案是C
正确答案是B,D
首先在题干中指针的数组传递给子函数变为指针的指针,也就是二级指针
那么A,C都是一级指针,直接排除
B、D的写法都是ok的,还可以写成char* arr[]
后记
关于指针的进阶学习,我们就先总结到这里
当然我们对于指针的学习还是路漫漫其修远兮!
毕竟指针对于之后我们学习和理解数据结构以及各种编程语言的深度学习,都起着重要的作用
之后我还会不定期更新指针的相关内容和知识的补充
大家可以持续关注~
free talk
这篇博客整理了大概两周的时间,但是指针的学习,我却拖拉了两个月,很多时候,人们都会为自己的不作为和懒惰找无数的借口,尤其是对于学习这件事, 仔细想想很多事都没有学习给人的感觉更充实,至少r如果我们真的沉下心去学习,就会有收获,就会有成长,“人最宝贵的东西是生命.生命对人来说只有一次.因此一个人的一生应该是这样度过的:当他回首往事的时候,他不会因为虚度年华而悔恨,也不会因为碌碌无为而羞耻”
我的这些碎碎念是希望激励自己,让自己有继续向前的勇气,如果看到这篇博客的你也能有所收获,我也感觉很荣幸!