在上节中我们简单谈论了指针变量,这节我们就来讨论指针变量的实际应用。
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就是一个数组的名称,我们用一个表格形象表示存储的现状:
0 | 1 | 4 | 9 |
---|---|---|---|
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数组的首地址,如果对这个地址进行加一操作,它会变成数组下一个元素的存储地址。既然如此,我们就可以用解引用的方式索引到数组的元素:
int 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类型的处理方式处理并赋值给val
cout<<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和fp2
fp1=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=add
fp2=*getAddFunction(0); // fp2=sub
int 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++中相当好用的功能之一。此外,我们还介绍了给函数传递未知数量和类型的参数的方法,以及函数要如何使用这些参数。函数指针和指针函数是本节最大的难点,函数指针是一种特殊的指针类型,而指针函数则是返回值类型为指针的函数。函数指针和指针函数搭配起来会有意想不到的效果。可以说,到此为止面对过程编程的主要难点都被我们攻克了,下一节我们一起开始面对对象编程!