前言
本篇文章旨在复习指针的基础用法,对于指针的原理并不涉及
用法讲解
前两个用法是常用的基础用法,最后一个用法是在嵌入式开法中的应用。此外,特别说明,本文介绍的知识基础用法,更高级的用法如:函数指针、二级指针、结构体指针等并不涉及。
传递参数
使用指针作为函数的参数,此时该参数称为实参。
指针传参两大特点:
- 执行效率高,不必再多拷贝一份数据
- 通过地址访问数据,主函数与子函数均可以访问数据
传递大容量参数
使用指针传递大容量的参数,主函数和子函数使用的是同一套数据, 避免了参数传递过程中的数据复制,提高了运行效率,减少了内存占用
如何理解普通传参与指针传参:
有一个学霸和一个学渣,两个人是关系很好的朋友,所以学渣的作业都是抄学霸的。
普通传参可以理解为:学霸担心直接把作业给学渣,学渣会破坏它的作业,所以就另外找一张纸把答案再抄一份给学渣,这就是普通传参,计算机会临时开辟内存空间存放参数,在调用函数时会将数据拷贝到这片内存空间中,真正的数据并没有被访问,子函数只是拿到了数据的拷贝,就像学渣并没有拿到学霸的作业,只是拿到了拷贝的版本。
对应代码展示如下:
#include <stdio.h>
void fun(int x)
{
x = 1;//试图在子函数中修改变量的值
}
int main()
{
int m;
m = 2;//给m赋值
printf("%d\n", m);//输出m的值,应为2
fun(m);//调用fun函数
printf("%d\n", m);//输出m的值,仍为2
return 0;
}
//上述代码说明在子函数中修改变量的值并没有成功,原因是局部变量在子函数执行完后就会被销毁,所以当子函数执行完后,x就会被销毁
指针传参可以理解为:学渣想要抄学霸的作业,但是作业是一片作文,如果此时学霸再拷贝一份的话,会非常麻烦,所以学霸只好把作文的原本交给学渣,此时学渣拿到了学霸的作业,这就是指针传参,在调用函数时,子函数拿到了数据的地址,通过该地址可以直接访问到数据,就像学渣拿到了学霸的作业一样。
对应代码如下:
#include <stdio.h>
void fun(int *x)
{
*x = 1;//在子函数中修改变量的值
}
int main()
{
int m;
m = 2;//给m赋值
printf("%d\n", m);//输出m的值,应为2
fun(&m);//调用fun函数
printf("%d\n", m);//输出m的值,此时输出值为1
return 0;
//m的值被改变,原因是在子函数中通过m的地址访问到了m,故可以完成对m的修改
}
下面的代码为指针能够提高效率的体现:
在数据量非常大时,如果用形参传参,则会非常浪费内存,并且效率非常低,所以采用指针传参,直接访问数据,而不进行拷贝。
#include <stdio.h>
//找一个数组中的最大值
//数组的长度很长,那么如果再使用形参传参,效率很低
//使用指针传参
int FindMax(int* array)
{
int max = 0;
for (int i = 0; array[i] != 0; i++)
{
if (max < array[i])
{
max = array[i];
}
}
return max;
}
int main()
{
int max = 0;
int num[] = { 12,23,34,45,65,78,97,56,53,5242,24,24,224,24,2,5,5,5,45,35,345,35,4,4,45,0 };//[]内可以不写,因为数组名就是存储数组首地址的指针
//末尾用0作为结束标志
printf("%d", FindMax(num));
//关于数组和指针,两者虽然不能说完全相同,但在许多情况下都是等价的,可以互相混用
//即elemtype array等价于elemtype array[]等价于elemtype *array,都是数组的**首地址**,elemtype代表某个数据结构的类型
//那么从另一个角度来理解数组下标为什么要从零开始:array为首元素的地址,指向首元素
//而(array+i)为指针向后偏移i位,此时指针指向第i-1个元素,即指向第i个元素应该偏移i-1位,那么指向首元素偏移0位
//array[i]与(array+i)时等价的,也就是说:array[i]也是让指针偏移i-1位,所以数组下标从零开始
//则上述子函数中的array[i]也可以写为array+i
}
前面说到,学霸是因为担心学渣破坏他的作业才将作业拷贝一份的,那么此时将作业直接交给学渣,这种风险仍然存在,但此时学霸不希望学渣破坏他的作业。换言之,在用指针传参时,可以在子函数内修改数据,但是我们此时不希望数据被修改,因为会导致错误,有没有什么方法呢?
先来看数据会被修改的代码:
#include <stdio.h>
int FindMax(int* array)
{
int max = 0;
*(array + 9) = 1;
for (int i = 0; array[i] != 0; i++)
{
if (max < array[i])
{
max = array[i];
}
}
return max;
}
int main()
{
int max = 0;
int num[] = { 12,23,34,45,65,78,97,56,53,5242,24,24,224,24,2,5,5,5,45,35,345,35,4,4,45,0 };
//末尾用0作为结束标志
printf("%d ", FindMax(num));
printf("%d", num[9]);
}
执行结果如下图:
显然最大值被我们在子函数中修改为1
那么防止值被修改的方法为:
使用const进行修饰,被const修饰过的称为常量,如果此时再修改,将会报错。
对应代码如下:
#include <stdio.h>
int FindMax(const int* array)
{
int max = 0;
*(array + 9) = 1;
for (int i = 0; array[i] != 0; i++)
{
if (max < array[i])
{
max = array[i];
}
}
return max;
}
int main()
{
int max = 0;
int num[] = { 12,23,34,45,65,78,97,56,53,5242,24,24,224,24,2,5,5,5,45,35,345,35,4,4,45,0 };
//末尾用0作为结束标志
printf("%d ", FindMax(num));
printf("%d", num[9]);
}
执行结果为:
编译器会报错,警告我们不能修改数据
事实上,许多的库函数都会加上const修饰,为了确保数据不会被乱改:
传递输出参数
使用指针传递输出参数,利用主函数和子函数使用同一套数据的特性,实现数据的返回, 可实现多返回值函数的设计
由于指针传参的缘故,使子函数与主函数都可以访问数据,而在上面的例子中,我们不希望其被修改,但事物都是具有两面性的,其可以修改数据的作用在某些情况又是我们所需要的,比如说,使一个子函数有多个返回值,该返回值实际上就是子函数的参数
#include <stdio.h>
void FindMax(const int* array,int *count,int *max)
{
for (int i = 0; array[i] != 0; i++)
{
if (*max < array[i])
{
*max = array[i];
*count = 1;
}
else if (*(max) == array[i])
{
(*count)++;//这里要注意后置++的优先级高于*,而我们要使(*count),所以要加括号,不能写为*count++
}
}
}
int main()
{
int max = 0;
int cnt = 0;
int num[] = {1,2,3,4,5,5,5,5,5,0};
FindMax(num, &cnt,&max);
printf("%d %d",max,cnt);
}
输出结果为:
显然通过指针我们做到了让函数可以返回两个值,实现了多返回值
传递输出参数的意思就是将子函数的参数作为返回值
传递返回值
将模块内的公有部分返回,让主函数持有模块的**“句柄”**,便于程序对指定对象的操作
句柄:可以将句柄理解为把手,只要握住了这个把手,就可以对其进行访问
通常我们会对某个代码块进行封装,而传递返回值常用来访问封装内的部分(注意:该部分必须是全局变量,因为局部变量在函数结束后会被销毁)
对应代码如下:
#include <stdio.h>
/********************///封装的定时器
int Time[3] = { 1,2,3 };//全局变量
int* GetTime()
{
return Time;//返回指向数组的指针
}
/********************/
int main()
{
int* ptr;
ptr = GetTime();//使用指针ptr接受GetTime返回的指向Time数组的指针
for (int i = 0; i < 3; i++)
{
printf("%d\n", ptr[i]);
}
}
运行结果如下:
成功访问到了Time数组的值
类似的操作还有C语言的文件操作,fopen等函数的返回值都是指针,此处不多赘述
直接访问物理地址下的数据
访问硬件指定内存下的数据
访问硬件指定内存下的数据,如设备ID号等
单片机的ID号,使用手册有详细介绍
对应代码如下:
#include <REGX52.H>
#include "LCD1602.h"
void main()
{
unsigned char *p;
LCD_Init();
p=(unsigned char *)0xF1;//0xF1为整型数据,而p是无符号字符型指针,在C语言的语法中是不能跨级别或者跨类型赋值的,所以此处需要强制类型转换
LCD_ShowHexNum(2,1,*p,2);
while(1)
{
}
}
将复杂格式的数据转换为字节
将复杂格式的数据转换为字节,方便通信与存储
这一个功能是我们最常用的功能
我们想使用串口或无线模块向电脑发送数据,但是串口或无线模块发送数据只能以字节为单位,但是我们所发送的数据形式是多种多样的(整型、浮点型、字符型、结构型等等),所以就需要使用指针进行数据转换
转换方法:使用指针unsigned char *ptr
原理:数据都是以字节存储的,我们所看到的复杂数据只不过是对其按照一定的码制进行了编码,我们只需进行反编码即可,反编码也需要遵循一定的规则,而这个规则就是unsigned char *ptr,因为unsigned char恰好是一个字节,所以用其进行强制转换可以将数据转换为字节。换言之,我们将复杂数据看作是一个unsigned char类型的数据,然后再用一个指向该数组的指针就可以将数据转换为字节形式。体现在代码中就是将elemtype*的指针转换为unsigned char * 类型的指针,因为unsigned char 是一个字节一个字节的读取.反之我们接收到的以字节形式发送而来的数据,只要提前知道该数据的类型elemtype,再使用elemtype 进行强制转换(将unsigned char的指针转换为elemtype*类型的指针)*,即对发送来的数据进行编码,将数据还原。
对应代码如下:
#include <stdio.h>
/********************///封装的无线模块
unsigned char Data[20];//使用char Data[]为数据发送的地点或位置
void SendData(const unsigned char *data,unsigned char count)//data为待发送数据,待发送的数据不能被修改,所以要加const修饰
{
unsigned char i;
for (i = 0; i < count; i++)
{
Data[i] = data[i];//向char Data[]发送数据
}
}
void ReceiveData(const unsigned char* data, unsigned char count)
{
unsigned char i;
for (i = 0; i < count; i++)
{
data[i] = Data[i];//接收发送在Data中的数据
}
}
/********************/
int main()
{
unsigned char i;
unsigned char DataSend[] = { 0x12,0x34,0x56,0x78 };
float num = 12.345;//待发送的复杂数据
unsigned char* p;
p = (unsigned char*)#//将float*的指针转换为unsigned char * 类型的指针,
//因为unsigned char *是一个字节一个字节的读取
SendData(DataSend, 4);
/************/ // 查看发送的数据
printf("\nData=");
for (i = 0; i < 20; i++)
{
printf("%x ", Data[i]);
}
/************/
unsigned char DataReceive[4];
float *fp; //将unsigned char*的指针转换为float*类型的指针,还原数据
ReceiveData(DataReceive, 4);
fp = (float*)DataReceive;
printf("\nDataReceive=");
for (i = 0; i < 4; i++)
{
printf("%x ", DataReceive[i]);
}
}
以上内容就是对指针基础应用的总结。