地址变量与函数进阶

指针与函数的高级用法

  • 1.数组
  • 2.函数的重载
  • 3.函数的指针类型参数
  • 4.可变参数函数链表
  • 5.函数指针
  • 6.指针函数
  • 7.内联函数
  • 8.总结

在上节中我们简单谈论了指针变量,这节我们就来讨论指针变量的实际应用。

1.数组

相信有一定C语言基础的小伙伴一定很熟悉这个类型。数组可以连续地存储指定类型的多个元素,并通过下标找到相应的元素:

# include<stdio.h>
# include<iostream>
using namespace std;
int main()
{int a[4]; // 声明一个可以装下4个int类型数字的数组for(int i=0;i<4;i++) // 为数组赋值{a[i]=i*i; }
}

在这个例子中,我们的a就是一个数组的名称,我们用一个表格形象表示存储的现状:

0149
a[0]a[1]a[2]a[3]

赋值结束后,我们可以通过使用下标索引的方式找到其存储内容,比如我想要找到9就可以写成a[3]:

for(int i=0;i<4;i++) // 打印数组
{cout<<a[i]; 
}

看起来和python的列表类型很像,但在Python中,对标C++数组的类型叫做紧凑型数组,并且C++中的数组是不可以用复数作为索引的。我们在声明中写下的**int a[4]**是申请了最多可以装三个int数字的数组,但是由于数组的索引是从0开始的,所以我们在使用数组时,最多只能写到a[3],这一点需要额外注意,因为有些编译环境不会对下标超过索引范围给出错误提示或警告。
现在,我们看看数组类型在存储地址上有什么特点:

int a[3];
for(int i=0;i<3;i++) // 打印a[i]元素所在的地址
{cout<<&a[i]<<" ";cout<<a+i<<endl;
}// 输出为:0x61fe10 0x61fe10
//        0x61fe14 0x61fe14
//        0x61fe18 0x61fe18

从输出中可以看出,a[0],a[1],a[2]的地址是一个公差为4的等差数列,而一个整型数据所占空间刚好为4字节,这说明数组类型是连续的存储空间,每个元素的起始位置紧挨着上个元素的终止位置。还有一点需要注意,如果我们直接打印a,会打印出a数组的首地址,如果对这个地址进行加一操作,它会变成数组下一个元素的存储地址。既然如此,我们就可以用解引用的方式索引到数组的元素:

nt a[3];
for(int i=0;i<3;i++) // 打印a[i]元素所在的地址
{cout<<*(a+i)<<" ";
}
// 输出为:0 1 4 9

2.函数的重载

C语言中,定义一个重名变量会产生编译错误,定义重名函数也是如此;在python中,如果有两个函数名称相同,那么后定义的函数会将先定义的函数覆写,被覆写的函数将无法被调用。但是在C++中,定义同名函数并不会报错,甚至可以通过重载函数的方式避免同名函数无法被引用。这是由于C++识别函数不止依赖函数名称,还会依赖参数列表。简单地说,多个同名函数,只要参数的数量或参数类型有所不同,就会被识别成不同函数,每个函数都可以被调用。但需要注意的是,如果两个同名函数仅仅只有返回值类型或(和)形参名称不同是不可以构成重载的。下面我们举个例子:

void test(int a,char b)
{cout<<a<<" "<<b<<endl;}
void test(char a,int b)
{cout<<a<<" "<<b<<endl;}
void test(int a,char b,double c)
{cout<<a<<" "<<b<<" "<<c<<endl;}
// int test(int num,char letter){} // 无法重载,会报错
int main()
{test(1,'a');test('a',1);test(1,'a',1.1);
}
// 输出为:1 a
//        a 1
//        1 a 1.1

函数重载的设定很大程度上方便了代码的书写。下面我们再来学些难点,并用上函数重载。

3.函数的指针类型参数

上节中我们说到过,用C++中使用return只能返回一个值。那么如果我希望自定义函数中修改的多个值在主函数中依然可以使用,又该怎么办呢?想要回答这个问题我们就需要首先弄清楚形参是如何工作的。
形式参数本质上是对实际参数的拷贝,即申请一个同样大小的空间,将实际参数复制进去,从而让我们在函数体内也能使用实参的值、但是形式参数是完全独立于实际参数的。
在这里插入图片描述

形式参数之所以不能再函数体外部使用,是因为形式参数存储的内容会在离开该函数时被销毁了。但如果我们使用地址值作为形式参数,那么通过形式参数取内容的方式就可以直接找到该形式参数对应地址的内容。如果在这基础上修改了这个地址内容:
在这里插入图片描述

这样一来,尽管形式参数被销毁,我们依然可以通过实际参数找到这个物理地址,读到里面的存储内容,是不是就完成了在主函数中可以使用形式参数修改后的值这个任务了呢?我们来尝试一下:

void add(int a,int b,int* sum)
{*sum=a+b;}
int main()
{int sum;add(1,2,&sum);cout<<sum<<endl;
}
// 输出为:3

这样,我们就可以直接修改主函数中sum参数的数值了,不需要借助return。还是挺神奇的吧~

4.可变参数函数链表

在python中,我们可以借助元组完成传入未指定数量的参数,但是在C++中想要输入任意数量的参数却不是那么简单。我们需要一个连续的空间,并且需要告知计算机开拓的空间大小。听起来很复杂,但也是有固定的书写套路。当我们需要的:

// 首先,我们引用一个头文件:
#include<stdarg.h>
void out(int num,...) // 定义一个使用可变参数的函数,其中第一个参数代表希望传入的参数数量// ...代表在需要接收的参数数量和类型未知
va_list zerro_loc; // 定义一个列表,变宏参数为可变参数。可以简单理解成这里寻找到// num的地址,并可以根据这个地址继续向后开拓空间
va_start(zerro_loc,num); // 列表得以初始化
int val;
for(int i=0;i<num;i++) // 用首参数num作为列表长度标记{val=va_arg(zerro_loc,int); // 当前地址位置向后移动int字节个单位,// 而后将对应长度的地址存储内容取出,取出内容按照int类型的处理方式处理并赋值给valcout<<val<<' '; // 打印接收到的内容va_end(zerro_loc); // 用于栈归位}

如果看了注释也不能理解,可以仅仅是记住这种用法。这种函数链表只能处理可变参数均为int型的任务。并且存在隐患。我们来试着调用一下:

int main()
{out(5,1,2); // 多出的空间也会有储存内容out(4,1,2,3,4); out(5,1,2,3,4,5,6); // 丢失6
}

运行的结果为:
在这里插入图片描述
这种可变参数用起来比较蹩脚,如果我需要处理参数为不同类型且数量未知的任务,又该怎么定义呢?

template <typename T, typename... Args> // 定义参数链表
void printArgs(T con, Args... ar) 
{cout<<con<<endl; // 具体任务的执行算法,这里的具体任务为打印当前参数printArgs(ar...); // 递归调用
}
void printArgs() // 重载printArgs函数,当参数列表为空时终止递归
{cout <<"\n"<<"is the end"<<endl;
}
int main() {printArgs(1,"hello",3.14,'Z'); // 每调用该函数一次,参数列表的第一个元素都会// 被记录在函数中并销毁return 0;
}

运行结果为:
在这里插入图片描述
向上面一样,如果大家不理解具体实现,可以把他当成是一个模板记住就好啦。

5.函数指针

这里应该算得上是C++的深水区了,坚持到这里的小伙伴都是好样的。
在C++里,我们可以把函数本身当做是一种数据类型,既是数据类型,就可以搭配指针变量。首先我们来看一段代码:

int sub(int a,int b)
{return a-b;}
int add(int a,int b)
{return a+b;}
int main()
{int (*fp1)(int,int),(*fp2)(int,int); // 声明两个函数指针fp1和fp2fp1=sub;fp2=add;
}

以上就是C++函数指针的定义和赋值。从这段代码中不难发现,其实函数名本身就是个地址变量,而调用函数可以具象成将参数塞到函数的地址里,并执行函数内容。
我们对以上的代码稍作修改:

int sub(int a,int b)
{return a-b;}
int add(int a,int b)
{return a+b;}
int main()
{int (*fp[2])(int,int); // 声明一个函数指针数组fp[0]=sub;fp[1]=add;cout<<fp[0](1,2)<<endl;    // fp[0](1,2)等价于sub(1,2)cout<<(*fp[1])(1,2)<<endl; //(*fp[1])(1,2)等价于fp[1](1,2)// 这说明函数指针的内容就是函数指针地址cout<<reinterpret_cast<void*>(fp[0])<<endl; // 想要输出函数指针的地址值,需要现将函数指针转换成空指针
}

让我们来看一下输出吧:
在这里插入图片描述

但这样看来,函数指针也就只是给函数换了个名字而已。那么函数指针有什么妙用吗?

6.指针函数

指针函数指的是返回值类型为指针的函数。由于指针已经被我们所熟悉,这里直接给大家上个例子:

int* add(int a,int b)
{a+=b;// return &a; // 必须事先声明指针类型并存储c的地址才能返回,不可以直接返回c的地址值int *p=&a;return p;
}
int main()
{cout<<*add(1,2)<<endl;
}

在上一节中,我提到过函数可以被理解成一种数据类型。既然是数据类型,就可以加个*成为对应的指针类型。那么指针函数的返回值可以是函数指针吗?当然是可以的:

int add(int a, int b) 
{return a + b;}
int sub(int a,int b)
{return a-b;}
int (*getAddFunction(bool a))(int, int) 
{if (a){return add;} // 如果指针函数形参为true则返回add函数地址else  // 如果指针变量形参为假则返回sub函数地址{return sub;}
}int main() {int (*fp1)(int, int),(*fp2)(int, int);fp1=getAddFunction(1); // fp1=addfp2=*getAddFunction(0); // fp2=subint result1 = fp1(3, 4);int result0 = fp2(3,4);cout<<result1<<endl;cout<<result0<<endl;return 0;
}

这就是函数指针的神奇效果啦,在实战中,这样的操作可以解决很多难题的,不懂的小伙伴建议多看几遍。

7.内联函数

使用inline修饰的函数就是内联函数。如果我们的函数声明和定义分开写,那么inline就需要写两次。其标准写法如下例:

inline int add(int a,int b);
inline int add(int a,int b)
{return a+b;}

内联函数不算C++的重点内容,我们只需稍作了解即可。

8.总结

本节我们介绍了数组,并对利用指针的知识对数组进行了分析;介绍了函数的重载,这是个C++中相当好用的功能之一。此外,我们还介绍了给函数传递未知数量和类型的参数的方法,以及函数要如何使用这些参数。函数指针和指针函数是本节最大的难点,函数指针是一种特殊的指针类型,而指针函数则是返回值类型为指针的函数。函数指针和指针函数搭配起来会有意想不到的效果。可以说,到此为止面对过程编程的主要难点都被我们攻克了,下一节我们一起开始面对对象编程!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/322097.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Java 新手如何使用Spring MVC 中的查询字符串和查询参数

目录 前言 什么是查询字符串和查询参数&#xff1f; Spring MVC中的查询参数 处理可选参数 处理多个值 处理查询参数的默认值 处理查询字符串 示例&#xff1a;创建一个RESTful服务 总结 作者简介&#xff1a; 懒大王敲代码&#xff0c;计算机专业应届生 今天给大家…

使用“反向代理服务器”的优点是什么?

反向代理服务器是一种网络架构模式&#xff0c;通常位于客户端和实际服务器之间&#xff0c;用于处理客户端请求并转发到实际服务器。以下是使用反向代理服务器的优点&#xff1a; 1.安全性&#xff1a;反向代理服务器可以提供额外的安全层。通过在反向代理服务器上配置防火墙和…

八、Lua脚本详解—— 超详细操作演示!

八、Lua脚本详解 —— 超详细操作演示&#xff01; 八、Lua脚本详解8.1 Lua 简介8.2 Linux 系统的Lua8.2.1 Lua 下载8.2.2 Lua 安装8.2.3 Hello World 8.3 Win 系统的Lua8.4 Lua 脚本基础8.4.1 注释8.4.2 数据类型8.4.3 标识符8.4.4 运算符8.4.5 函数8.4.6 流程控制语句8.4.7 循…

实例:NodeJS 操作 Kafka

本人是C#出身的程序员&#xff0c;c#很简单就能实现&#xff0c;有需要的可以加我私聊。但是就目前流行的开发语言&#xff0c;尤其是面向web方向应用的&#xff0c;我感觉就是Nodejs最简单了。下面介绍&#xff1a; 本文将会介绍在windows环境下启动Kafka&#xff0c;并通过n…

Parallels虚拟机启动后,Mac主机无法上网怎么办

文章目录 1.问题2.解决&#xff1a; 1.问题 部分用户在运行Parallels Desktop的Windows 11打开后&#xff0c;Windows上网没有问题 &#xff0c;但是Mac主机不能访问带域名的网站&#xff0c;而访问带ip的网站没问题&#xff0c;退出parallels虚拟机以后&#xff0c;mac网络恢…

SparkStreaming基础解析(四)

1、 Spark Streaming概述 1.1 Spark Streaming是什么 Spark Streaming用于流式数据的处理。Spark Streaming支持的数据输入源很多&#xff0c;例如&#xff1a;Kafka、Flume、Twitter、ZeroMQ和简单的TCP套接字等等。数据输入后可以用Spark的高度抽象原语如&#xff1a;map、…

解决在test以外的目录下导入junit无效

以上引用来自src目录下的文件&#xff0c;可以看到&#xff0c;和junit有关的导入都飘红&#xff0c;但明明junit已经被正确导入进了项目中。 再看右侧的Maven的依赖下方&#xff0c;junit的右边有一个很不起眼的(test) 这是因为junit作为测试框架&#xff0c;可能包含仅适用于…

LLM Agent之再谈RAG的召回信息密度和质量

话接上文的召回多样性优化&#xff0c;多路索引的召回方案可以提供更多的潜在候选内容。但候选越多&#xff0c;如何对这些内容进行筛选和排序就变得更加重要。这一章我们唠唠召回的信息密度和质量。同样参考经典搜索和推荐框架&#xff0c;这一章对应排序重排环节&#xff0c;…

ASP.NET Core路由中间件[1]: 终结点与URL的映射

一、路由注册 我们演示的这个ASP.NET Core应用是一个简易版的天气预报站点。如果用户希望获取某个城市在未来N天之内的天气信息&#xff0c;他可以直接利用浏览器发送一个GET请求并将对应城市&#xff08;采用电话区号表示&#xff09;和天数设置在URL中。如下图所示&#xff…

Mybatis一级缓存

文章目录 Mybatis一级缓存原理一级缓存特点命中原则生命周期源码解读设计理念Spring集成 Mybatis一级缓存原理 一级缓存特点 自动启用 通过在setting中设置localCacheScope STATEMENT&#xff08;默认为SESSION&#xff09;全局禁用一级缓存 在Dao接口方法上添加注解&#xff…

【GO语言卵细胞级别教程】01.GO基础知识

01.GO基础知识 目录 01.GO基础知识1.GO语言的发展历程2.发展历程3.Windowns安装4.VSCode配置5.基础语法5.1 第一段代码5.2 GO执行的流程5.3 语法规则5.4 代码风格5.5 学习网址 1.GO语言的发展历程 Go语言是谷歌公司于2007年开始开发的一种编程语言&#xff0c;由Robert Griese…

Spring之强大的DefaultListableBeanFactory

系列文章目录 如何查看类继承结构参考这里 文章目录 系列文章目录一、DefaultListableBeanFactory的类继承实现结构二、实现接口 一、DefaultListableBeanFactory的类继承实现结构 二、实现接口 AliasRegistry&#xff1a;支持别名功能&#xff0c;一个名字可以对应多个别名B…