当前位置: 首页 > news >正文

【哇! C++】缺省参数、函数重载与引用

 

目录

一、缺省参数

1.1 全缺省参数

 1.2 半缺省参数

1.3 声明和定义分离后的缺省参数

1.4  编译器对程序的处理

二、函数重载

2.1 函数重载的定义

 2.2 支持函数重载的原理

 三、引用

3.1 引用的概念

3.2 引用的特性

3.3 引用的应用价值

3.3.1 做参数

3.3.2 做返回值

3.4 引用和指针的区别 


一、缺省参数

        缺省参数是声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参。

#include<iostream>
using namespace std:

void Func(int a = 0)
{
    cout << a << endl;
}

int main()
{
    Func(1); //传参1,调用函数,a的值为1
    Func();  //不传参也可以调用,调用以后,a的值就是0

    return 0;
}

        缺省参数分为全缺省和半缺省两类:

1.1 全缺省参数

  1. 函数定义时所有参数都分别指定了缺省值。
  2. 调用时可以不用传递任何参数,调用函数时采用缺省值。
#include<iostream>
using namespace std;

void Func(int a = 10, int b = 20, int c = 30)
{
    cout << "a = " << a << endl;
    cout << "b = " << b << endl;
    cout << "c = " << c << endl;
}
int main()
{
    Func(1, 2, 3);
    Func(1, 2);
    Func(1);
    Func();
    
    return 0;
}

 1.2 半缺省参数

        缺省部分参数,并且缺省值必须从右往左连续给,缺省参数不能跳跃给。

        例如:void Func(int a, int b = 20, int c);是错误的写法。

#include<iostream>
using namespace std;

void Func(int a, int b = 20, int c = 30)
{
	cout << "a = " << a << endl;
	cout << "b = " << b << endl;
	cout << "c = " << c << endl << endl;
}
int main()
{
	Func(1, 2, 3);
	Func(1, 2);
	Func(1);

	return 0;
}

        值得注意的是:函数Func合计起来有4种调用方式,例如,传递两个参数,在调用Func时,就指定给了前两个参数,不能跳跃传递。

1.3 声明和定义分离后的缺省参数

        在实践写程序的过程中,往往需要定义和声明分离,那么,如果定义和声明分离的话,在使用缺省参数的时候,能不能同时给呢?

        C++规定,定义和声明分离时不能同时是缺省参数,否则会出现重定义默认参数应该在声明的时候给缺省参数,定义的时候不给缺省参数。例如:

//Stack.h
#include<stdlib.h>

struct Stack
{
	//...
	int* a;
	int size;
	int capacity;
};

void StackInit(struct Stack* ps, int n = 4);
void StackInit(struct Stack* ps, int x);


//Test.cpp
#include"Stack.h"
int main()
{
	struct Stack st1;
    StackInit(&st1, 100);

	struct Stack st3;
	StackInit(&st3);

    struct Queue q;

	return 0;
}

1.4  编译器对程序的处理

         编译器在预处理阶段,.h文件会被展开。当需要使用函数的时候,就在全局域找到了对应函数的声明。此时,需要了解函数调用的定义和声明的本质,例如StackInit(&st1, 100);。

        函数调用会在编译时,在底层形成一个汇编指令如call StackInit(0x00112233);。而此处没有函数的地址,因为这里只有声明,为call StackInit(?)那在什么时候会去找地址呢

  • 我们知道,编译器编译时有几个阶段:预处理编译(检查语法并搜索)汇编链接
  • call StackInit(?)会在链接的时候,通过名字StackInit去找地址并把(?)处填上。
  • 在整个编译阶段,只有声明,程序是能通过的。检查语法并搜索,在main函数中用了struct Stack的类型,编译器先在局部搜索,后转向全局搜索,能搜索的到。相反的,struct Queue的类型是搜索不到的。
  • 此外,StackInit也会进入全局搜索,只有声明是可以的,参数传递也是匹配的。
  • 调用StackInit(&st3);时,在语法检查阶段,声明都对应不上,所以会报错。
  • 假如声明的时候不给缺省参数,此时为void StackInit(struct Stack* ps, int n);,编译的时候发现,调用StackInit(&st3)函数的时候发现不匹配,所以就报错了。
  • 这就证明了,缺省参数只能在声明的时候给。调用StackInit(&st3)函数时,传递一个参数,另一个参数使用缺省值,相当于n传了参数4,类似于StackInit(&st3, 4)。

        声明和定义分离也体现了:声明就是函数的原型,有函数名、参数、类型、返回值。在编译阶段,通过声明就能够做到检查函数名、参数、类型、返回值能不能一一对应,合乎语法,编译就能通过。举个例子,例如AB同时做任务,A直接把任务做完上传,直接做完就是没有声明的过程,而直接定义;B答应这周做好,答应这周做好就是一个声明。(声明是一种承诺,定义是一种兑现)

        汇编代码是指令,函数本质也是多条指令的集合,所以函数的地址是这些指令的第一句指令的地址,类似于数组的地址。所以,调用函数的本质就是,call这个函数的地址,然后跳到这个地址指向的地方去,找到这些指令,然后把这些指令依次取给CPU,去依次执行,就完成了函数的功能。

声明和定义分离后,程序就分成了3个文件Stack.h、Stack.cpp、Test.cpp:

  1. 预处理阶段,要做的事情:展开头文件、宏替换、条件编译、去掉注释,此时变2个文件了,即Stack.cpp、Test.cpp分别生成Stack.i、Test.i。
  2. 编译阶段:检查语法->生成汇编代码 Stack.i、Test.i分别生成Stack.s、Test.s,之间是独立的。
  3. 汇编阶段:把汇编代码转成二进制机器码Stack.s、Test.s分别生成Stack.o、Test.o。
  4. 链接阶段:把Stack.o、Test.o合并到一起,有些地方得用函数名去其他文件中找函数地址。所以链接错误经常会报:LNK2019:.....,此时就要看声明所对应的函数定义阶段了,此时也可以说,有函数的定义采用函数的地址。

二、函数重载

2.1 函数重载的定义

        函数重载是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(1.参数个数或2.类型或3.类型顺序)不同,常用来处理实现功能类似数据类型不同的问题。

#include<iostream>
using namespace std;

void f(int a, char b)
{
    cout << "f(int a, char b)" << endl;
}

void f(char a, int b)
{
    cout << "f(char a, int b)" << endl;
}

int main()
{
    f(10, 'a');
    f('a', 10);

    return 0;
}

 2.2 支持函数重载的原理

        C语言不支持重载,链接时,直接用函数名去找地址。如果函数同名,编译器就不知道到底要找哪个函数。为什么C++支持函数重载?

        C++是通过函数修饰规则来区分,只要参数不同,修饰出来的名字就不一样,名字修饰产生唯一内部名称,是支持重载的关键。

        名称修饰是编译器在编译源代码时为函数、类等名称添加额外信息的过程,生成内部链接名称。该内部链接名称包含原名称以及其他信息,如参数类型、返回类型等。以Linux下C++编译器编译为例,可能生成的内部名称为:

_Z3Addii
int Add(int left, int right)
{
	cout << "int Add(int left, int right)" << endl;
	return left + right;
}
///
_Z3Adddd(Linux)
double Add(double left, double right)
{
	cout << "double Add(double left, double right)" << endl;
	return left + right;
}

其中:_Z表示前缀,3是(Add)字节长度,i i - 参数为int int,d d - 参数为double double

        可以通过Linux下观察C和C++编译器编译后结果:

        1. 采用C语言编译器编译后结果

        2. 采用C语言编译器编译后结果

 三、引用

3.1 引用的概念

        引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。

类型& 引用变量名(对象名) = 引用实体;
#include<iostream>
using namespace std;

int main()
{
    int a = 0;
    int& b = a;

    cout << &a << endl;//00CFFA68
    cout << &b << endl;//00CFFA68

    a++;
    b++;
    
    int& c = a;
    int& d = c;
    d++;

    return 0;
}

        C++中,复用了&符号。放在1个变量前是取地址,放在两个变量中间是按位与,放在类型名后变量前是引用。

        由上述程序中,可以知道,b就是a的别名,如果有需要,别名可以取多个,别名可以取别名,此时b、c、d都是a的别名。

3.2 引用的特性

        C++规定,引用的3条特性:

  1. 引用必须初始化;
  2. 引用定义后,不能改变指向;
  3. 一个变量可以有多个引用。
int main()
{
	int a = 0;//&a == 0x00f9f9d0	
	//1.引用必须初始化
	//int& b;//err
	
	//2.引用定义后,不能改变指向
	int& b = a;//&b == 0x00f9f9d0
	int c = 2;//&c == 0x00f9f9b8
	b = c;//不是改变指向,而是赋值,此时a == 2

	//3.一个变量可以有多个引用
	int& d = b;

	return 0;
}

3.3 引用的应用价值

3.3.1 做参数

#include<iostream>
using namespace std;

void Swap(int* a, int* b)//传地址
{
	//...
}
void Swap(int& a, int& b)//代表a是x的别名,b是y的别名
{
	//...//此时的交换就是x和y的交换
	int tmp = a;
	a = b;
	b = tmp;
}
int main()
{
	int x = 0, y = 1;
	//Swap(&x, &y);

	Swap(x, y);

	return 0;
}

引用做参数有以下两个特点:

  1. 输出型参数 - 改变形参会影响实参,不仅仅是用的,而且要修改这个变量,让外边也拿到这个修改后的值。相对于输入型参数 - 给一个参数就是直接用的。
  2. 传参对象比较大时,可以减少拷贝,提高效率。

        有了这些效果,指针也可以,但是感觉上引用更方便,那么C++中,引用能不能替代指针?

        指针和引用的功能是类似的。指针的作用就是指向找到并改变,引用也同样可以找到改变。C++的引用,对指针使用比较复杂的场景进行一些替换,让代码更简单易懂,但是不能完全替代指针。引用不能完全替代指针的原因就是:引用定义后,不能改变指向。如:假设三个节点的链表删除中间节点,就不能用引用替代指针。

typedef struct Node
{
    struct Node* next;
    struct Node* prev;
    int val;
}LNode, *PNode;
//把struct Node重命名为LNode,把struct Node* 重命名为PNode

void PushBack(struct Node* phead, int x)
{
    phead = newnode;
}

void PushBack(struct Node*& phead, int x)
{
    phead = newnode;
}

int main()
{
    PNode plist = NULL;
    return 0;
}

3.3.2 做返回值

#include<iostream>
using namespace std;

int func()
{
    int a = 0;
    return a;
}

//编译器肯定不会用a做返回值,所以在出func函数作用域的时候,会产生一个临时变量,会把a给临时变量
//如果a比较小,会存在寄存器中

int main()
{    
    int ret = func();
    cout << "ret = " << ret << endl;

    return 0;
}

        这里的返回值不是a,因为a在func函数中,当调用func函数结束后,a会销毁,用a作为返回值可能导致ret不是0而是随机值。我们可以通过程序实现传引用返回,返回a的别名。

#include<iostream>
using namespace std;

int& func()
{
    int a = 0;
    return a;//返回a的引用
}

int main()
{    
    int ret = func();
    cout << "ret = " << ret << endl;

    return 0;
}

        返回的是a的引用,ret就是a的别名的别名,此时意味着ret==a,野引用。

 证明1:

#include<iostream>
using namespace std;

int& func()
{
    int a = 0;
    return a;//返回a的引用
}

void fx()
{
    int b = 1;
}

int main()
{
    int ret = func();
    cout << ret << endl;//0

    fx();
    cout << ret << endl;//随机值
    return 0;
}

 证明2:

#include<iostream>
using namespace std;

int& func()
{
	int a = 0;
	return a;
}
int& fx()
{
	int b = 1;
	return b;
}

int main()
{
	int& ret = func();
	cout << ret << endl;//0
	fx();
	cout << ret << endl;//1

	return 0;
}

 证明3:

#include<iostream>
using namespace std;

int& Add(int a, int b)
{
    int c = a + b;
    return c;
}

int main()
{
    int& ret = Add(1, 2);
    Add(3, 4);
    cout << "Add(1, 2) is :"<< ret <<endl;

    return 0;
}

        其原理图为:

 

        基于此,可以得出以下结论:返回的变量出了函数的作用域就生命周期到了要销毁(局部变量),不能用引用返回。

        什么情况下可以用引用返回? - 哪些变量出了作用域不销毁呢?

        全局变量、static静态变量、malloc堆上变量等可以用引用返回。即如下代码:

#include<iostream>
using namespace std;

int& func()
{
	static int a = 0;
	return a;
}

int main()
{
	int ret = func();
	cout << ret << endl;

	return 0;
}

3.4 引用和指针的区别 

语法的角度上:

  1. 引用是别名,不开空间;指针是地址,需要开空间存地址;
  2. 引用必须初始化,指针可以初始化也可以不初始化;
  3. 引用不能改变指向,指针可以;
  4. 引用相对更安全,没有空引用。但是有空指针,容易出现野指针,但是不容易出现野引用;
  5. sizeof计算大小、++、解引用访问等方面的区别。引用结果为引用类型的大小,指针始终是地址空间所占字节个数;
  6. 有多级指针,但是没有多级引用。

底层的角度上:

  • 汇编层面上,没有引用,都是指针,引用编译后也转换成指针了。
#include<iostream>
using namespace std;

int main()
{
	int a = 10;
	int& ra = a;//语法上不开空间,底层开空间
	ra = 20;

	int* pa = &a;//语法上开空间,底层开空间
	*pa = 20;
	
	return 0;
}

        综上:1.引用底层是用指针实现的;2.语法含义和底层实现是背离的。

相关文章:

  • 【C++】策略模式
  • 迭代、递归、回溯和动态规划
  • span标签 鼠标移入提示框 el-tooltip element-ui
  • twisted实现MMORPG 游戏数据库操作封装设计与实现
  • python学opencv|读取图像(六十八)使用cv2.Canny()函数实现图像边缘检测
  • Linux内核 - 非仿生机器人之感知主控系统(协议栈)
  • 3D打印学习
  • 【DDD系列-2】风暴出的领域模型
  • 解决 MyBatis Plus 在 PostgreSQL 中 BigDecimal 精度丢失的问题
  • Android remount failed: Permission denied 失败解决方法
  • 基于单片机的智能安全插座(论文+源码)
  • DeepSeek计算机视觉(Computer Vision)基础与实践
  • Electron 客户端心跳定时任务调度库调研文档 - Node.js 任务调度库技术调研文档
  • js考核第三题
  • 嵌入式经常用到串口,如何判断串口数据接收完成?
  • IIC总线,也称为I²C或Inter-Integrated Circuit协议
  • bootplus管理系统 file/download 任意文件下载漏洞
  • Python与R机器学习(1)支持向量机
  • AI技术未来趋势
  • 人工智能泡沫效应
  • 萨洛宁、康托罗夫、长野健……7月夏季音乐节来很多大牌
  • 上海发布台风红色预警?实为演练,今日下午局部中雨下班请注意
  • 甘肃省白银市一煤矿发生透水事故,3人失联
  • 上海浦江游览南拓新航线首航,途经前滩、世博文化公园等景点
  • 申伟强任上海申通地铁集团有限公司副总裁
  • 学人、学术、学科、学脉:新时代沾溉下的中国西方史学史