作者主页
📚lovewold少个r博客主页
⚠️本文重点:c++ 缺省参数 引用与指针
😄每日一言:青春就像一只容器,装满了不安躁动青涩与偶尔的疯狂。
目录
缺省参数
缺省函数的定义
缺省参数分类
函数重载
为什么C语言不支持函数重载?
引用
引用的基本概念
常引用
引用使用场景
引用作为函数参数
引用作为函数的返回值
传值、传引用效率比较
引用和指针
内联函数
内敛函数的概念
内联函数的优缺点
内联函数的限制
内联函数与宏
总结
前言
本章章节,主要会从c++的特性入手,结合c语言的前面的缺陷对比讲解。从而了解和掌握缺省参数、函数重载、引用和内联函数的特点关键。
如果我们C语言的基础比较好,相对于对C++的一些特性理解就会更加清晰,初期C++作为C语言的一个升级版本,我们其实入门掌握的就是升级了什么,多了什么,为什么要多?也是对祖师爷想法的剖析,有助于我们以后也能解决在编程问题中发现的缺陷。
缺省参数
缺省参数,首先顾名思义既缺乏和省略的参数。相对于C语言的对参数的必须传递完整不同,C++能实现不传递声明的参数和传递部分参数。
缺省函数的定义
缺省参数是声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参。
缺省参数分类
全缺省
全缺省是声明或定义函数时为函数的全部的参数指定缺省值。在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参。如图所示,函数声明给abc都声明了缺省值,在函数调用过程中,缺省值的传递是可以0,1,2,3个按照声明中最多的参数个数达到上限,从左往右依次传递实参。
半缺省参数
半缺省是声明或定义函数时为函数的部分的参数指定缺省值。在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参。
要注意的是函数的缺省参数必须从右往左声明,不能跳跃的和从左往右指定缺省值。这一点在传值调用是有必然联系的。因为传值调用必须规范,从左往右,对于函数参数没有指定的缺省值如果不传递值本身编译也无法通过。
注意:
- 1. 半缺省参数必须从右往左依次来给出,不能间隔着给
- 2. 缺省参数不能在函数声明和定义中同时出现
- 3. 缺省值必须是常量或者全局变量
- 4. C语言不支持(编译器不支持)
函数重载
C++ 允许在同一范围内指定多个同名函数。 这些函数称为重载函数或重载。 利用重载函数,你可以根据参数的类型和数量为函数提供不同的语义。
函数重载:是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数或类型或类型顺序)不同,常用来处理实现功能类似数据类型不同的问题。
这就好比对于一个函数,我可以根据传递的参数类型的不同区分他是哪一个函数。既函数的参数声明作为了函数名修饰的一部分。因此C++可以根据参数类型和数量去提供不同的语义。对于编译器而言,语义就是上下文联系,函数名作为上文,参数作为下文,上下联系后就能确定是同名函数的哪一个。即根据多胞胎的细微区别即可区分谁是谁。
参数类型不同
定义参数类型不同的在调用函数的时候对函数参数进行判断即可区分。
参数个数不同
参数顺序不同(本质和参数类型一样)
为什么C语言不支持函数重载?
简单来讲是因为在链接阶段生成符号表的时候,C语言对函数取名的时候,只会拿函数名进行取名,如果有函数重载的话,没办法区分函数的不同。而C++在取名的时候,是将函数名和参数类型的首字符结合起来对函数的取名,这样就可以区分函数的不同。(细节过程后续会出一节C语言和C++的函数修饰命名规范)。
注意:对于重载函数,要保证是参数不同而非返回值不同。前者可在调用过程就对函数进行确定,但是后者不同,在确定是哪一个函数的过程中就会先产生冲突,即二义性。因此无论是重载函数的过程还是调用函数的过程,都要避免歧义和冲突。
引用
“白龙马,蹄朝西,跟着唐三藏还有三徒弟~”等等,接下来是C++ 的引用部分。什么是引用呢?
引用的基本概念
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
篇章提到白龙马,我们来看看以下人都是谁,孙悟空从起初石猴进行初始化,后续被取名,当官被取名弼马温,又称自己为齐天大圣,别人根据他的齐天大圣又称之为大圣。孙悟空又名悟空······可以看见,无论称呼为何,其本质上都是那一个石猴,虽然我们都以孙悟空称之。
这和C++又有啥关系,关系来了。C++ 中引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
通过引用,我们给一个变量可以取一个别名,对别名的操作也会改变起本质。即给孙悟空别名叫弼马温,让他当官了,本质上是一个石猴在天庭当了个养马的官。
类型 & 引用变量名(对象名) = 引用实体;
上述代码首先声明一个变量a,然后声明一个ra他是a的别名。
注意:引用类型必须和引用实体是同种类型的
接触过C语言后接触引用,感觉和指针基本一样,例如:
对ra来讲他本身作为a的别名,对于pal来讲他是a的地址,对ra的直接访问有种(*解引用运算符隐式理解)的感觉。
但是实际上,除了用法的不同以外,还有其他差别,比如引用在声明时期必须将其初始化,不能和指针一样,先声明再赋值。对于引用来讲,创立初始化于一个变量关联便开始服务与他。对于一个变量,可以有多个引用。对于引用,只能有唯一一个实体。
常引用
当以const声明定义一个常变量的时候,引用只能同级别引用,也得加上const,否则可以试想一下,对于一个常量10,通过引用可以对10操作改为2,这不是荒唐么,10=2?很显然不能这样子操作。
引用使用场景
引用作为函数参数
void Swap(int x, int y) { int tmp = x; x = y; y = tmp; } void Swap_r(int& x, int& y) { int tmp = x; x = y; y = tmp; } int main() { int a = 100; int b = 1; Swap(a, b); cout <<"传值调用结果--" << " a:" << a << " b:" << b << endl; Swap_r(a, b); cout << "传引用调用结果--" << " a:" << a << " b:" << b << endl; return 0; }
看上述代码运行,我们声明并创建了一个交换函数用于交换a,b变量的值。在C语言中,我们知道要改变变量本事,得传递指针。因为,对于第一个函数,传值调用的过程中,传递的是a,b的值得临时拷贝,因此函数内部的过程不影响其变量本身。而引用作为函数参数,相当于在调用过程中,对引用和传递的参数进行了初始化,此时函数内部的x,y变成了a,b的别名。对xy的操作会等同于对ab的操作。
当然我们也可以通过传递指针变量对ab进行改变,这在形式上和引用有所区别,即函数内部要对此进行解引用。当然,如果传递多级指针变量,这样的操作就更加显得繁琐。而引用,能更有助于可读性的提升。
引用作为函数的返回值
在上述代码中,我们使用传递了两个变量执行加法操作,在函数内部我们定义了一个变量,并进行了引用还回。即我们对n取了别名,并返回了这个别名,在函数外部,我们使用引用接受了返回变量。此时ret为返回变量n的别名。
在函数创建和调用的过程中,本质上是在栈上为函数开辟了空间,执行完函数后,栈空间会被回收,因此传递过程中,起初ret作为n的别名,能得到n的值,但是后续函数空间被回收,n被回收,ret的作为n的别名,n更改了ret也会更改。
可以看见引用作为函数返回值在内存中不产生被返回值的副本;(注意:正是因为这点原因,所以返回一个局部变量的引用是不可取的。因为随着该局部变量生存期的结束,相应的引用也会失效。程序状态就未知) 。这就和返回临时变量的指针一样,出了函数的作用域,指针对于空间的指向就会被回收,指向也就失去了意义。
如果要将引用做为返回值应当是返回作为参数传递给函数的引用,即作用域在函数外部的变量。这个变量可以为静态变量,全局变量以及需要其存在作用域内部的变量。
注意:如果函数返回时,出了函数作用域,如果返回对象还在(还没还给系统),则可以使用引用返回, 如果已经还给系统了,则必须使用传值返回。
传值、传引用效率比较
struct A { int a[1000]; };
void TestFunc1(A aa) {}
void TestFunc2(A& aa) {}
void TestRefAndValue()
{
A a;
// 以值作为函数参数
size_t begin1 = GetCycleCount();
for (size_t i = 0; i < 100000; ++i)
{
TestFunc1(a);
}
size_t end1 = GetCycleCount();
// 以引用作为函数参数
size_t begin2 = GetCycleCount();
for (size_t i = 0; i < 100000; ++i)
TestFunc2(a);
size_t end2 = GetCycleCount();
// 分别计算两个函数运行结束后的时间
cout << "TestFunc1(A)-time:" << end1 - begin1 << endl;
cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;
}
int main()
{
TestRefAndValue();
return 0;
}
#include <time.h>
struct A { int a[10000]; };
A a;
// 值返回
A TestFunc1() { return a; }
// 引用返回
A& TestFunc2() { return a; }
void TestReturnByRefOrValue()
{
// 以值作为函数的返回值类型
size_t begin1 = clock();
for (size_t i = 0; i < 100000; ++i)
TestFunc1();
size_t end1 = clock();
// 以引用作为函数的返回值类型
size_t begin2 = clock();
for (size_t i = 0; i < 100000; ++i)
TestFunc2();
size_t end2 = clock();
// 计算两个函数运算完成之后的时间
cout << "TestFunc1 time:" << end1 - begin1 << endl;
cout << "TestFunc2 time:" << end2 - begin2 << endl;
}
int main()
{
//TestRefAndValue();
TestReturnByRefOrValue();
return 0;
}
经过比较,我们发现现传值和引用在作为传参以及返回值类型上效率相差很大。因此,传引用在和传值相比效率更加高效。
引用和指针
引用在语法上不额外开辟内存空间,但是实际上和指针的实现过程是一样的。
- 引用和指针的不同点:
- 1. 引用概念上定义一个变量的别名,指针存储一个变量地址。
- 2. 引用在定义时必须初始化,指针没有要求
- 3. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型 实体
- 4. 没有NULL引用,但有NULL指针
- 5. 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占 4个字节)
- 6. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
- 7. 有多级指针,但是没有多级引用
- 8. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
- 9. 引用比指针使用起来相对更安全
内联函数
内敛函数的概念
以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数调用建立栈帧 的开销,内联函数提升程序运行的效率。
内敛函数通俗来讲就是在程序编译过程中直接在调用地方展开,不同于普通调用过程中。内联函数和正常的编写方式其实大差不差,在前面加上关键字inline而已,为何能提升效率呢。
其主要原因得深入程序内部,在我们执行程序的时候,即由一串机器语言指令组成,在运行程序的时候,操作系统将这些指令载入到计算机内存中,因此每一串指令都有自己的内存地址。计算机随后会逐本执行这些指令。有时会进行跳转,比如循环或者分支语句,常规的函数调用也使程序跳转到另一个地址,并在函数结束时候返回。来回的跳转并记录跳跃位置意味使用函数会产生额外的花销。
内敛不同于常规编译,即使用函数定义去替换函数调用,在调用地方直接展开,这就意味着他不需要跳转,而是直接执行编译过程。
相应的,如果内联函数作为简短且常使用函数,则可以优化跳转过程的时间消耗,如果本身执行其函数时间就很长,跳转消耗时间相对于函数运行时间只占很小一部分,这种展开就没有任何意义,因为只优化的跳转时间只占一小部分,而展开占用的空间却大幅度增加了。因此要有选择性的使用内联函数。
内联函数的优缺点
引入内联函数的目的是为了解决程序中函数调用的效率问题,程序在编译器编译的时候,编译器将程序中出现的内联函数的调用表达式用内联函数的函数体进行替换,而对于其他的函数,都是在运行时候才被替代。这其实就是个利用空间代价换时间的做法。
缺陷:可能会使目标文件变大,优势:少了调用开销,提高程序运行效率。
内联函数的限制
有些函数即使声明为内联的也不一定会被编译器内联,这点很重要,比如递归函数就不会被正常内联。递归函数不应该声明成内联函数,因为递归调用堆栈的展开并不像循环那么简单, 比如递归层数在编译时可能是未知的, 大多数编译器都不支持内联递归函数。
- 在内联函数内不允许使用循环语句和开关语句;(因为本身内敛函数就是优化指令跳转过程的额外开销,使用得不偿失)
- 内联函数的定义必须出现在内联函数第一次调用之前;(不能定义声明分离)
- 正常内联函数不超过10行(具体看编译器),避免过多调用造成的过多代码空间占用。
内联函数与宏
#define max(a,b) ((a)+(b))
inline int Max(int x, int y)//内联函数
{
return (x > y) ? x : y;
}
int main()
{
cout << "Max (20,10): " << Max(20, 10) << endl;
cout << "max(20,20): " << max(20, 20) << endl;
return 0;
}
宏在定义的过程中主要是替换,而不是通过传递参数实现的。
优点:增强代码的复用性(例如改变宏定义参数更改整个程序宏变量)和提高性能。
缺点:不方便调试宏(因为预编译阶段进行了替换) 。代码可读性差,可维护性差,容易误用。没有类型安全的检查 。
在c++中,有如下方式能替代宏,且解决其缺陷:
1. 常量定义换用const enum
2. 短小函数定义换用内联函数
总结
本文简要总结了C++中的函数、引用和内联函数这些关键概念和特性,以及它们的用途和优势。
缺省参数
- 缺省参数允许在函数定义中为一个或多个参数提供默认值,提高函数的灵活性和可读性。
函数重载
- 函数重载是通过相同的函数名定义多个函数,但它们的参数列表必须不同。这使得可以根据不同参数选择正确的函数进行调用。
引用
- 引用允许创建变量的别名,而不是副本,提高代码效率和可读性。
- 常引用用于指示不能修改引用的方式,用于数据保护。
- 引用可以用作函数参数,使函数能够修改传递的变量,还可以用作函数的返回值,实现多值返回。
传值、传引用效率比较
- 传值需要复制整个对象,传引用只传递引用,对于大型对象,传引用通常更高效。
内联函数
- 内联函数通过inline关键字定义,将函数的代码直接嵌入到调用处,减少函数调用的开销,提高程序执行效率。
- 内联函数适用于小型函数,提高代码可读性。
作者的实力有限,文章难免出现纰漏,有不对的地方欢迎指正!