【走进C++ (3)】缺省参数 函数重载 引用
2026/7/15 3:07:34 网站建设 项目流程

文章目录

  • 一、缺省参数
    • 1、概念
    • 2、分类和使用
      • 2.1 全缺省参数
      • 2.2 半缺省参数
      • 2.3 参数传递特性
      • 2.4 总结
    • 3、声明和定义
  • 二、函数重载
    • 1、概念
    • 2、构成函数重载的三种情况
    • 3、不能构成函数重载的情况
    • 4、可以构成重载但会引起歧义的情况
    • 5、函数重载的原理(拓展学习)
  • 三 、引用
    • 1、概念
    • 2、特性
    • 3、使用场景
      • 3.1 做参数
      • 3.2 做返回值
    • 4、传值、传引用效率比较
    • 5、引用和指针的区别

一、缺省参数

1、概念

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

简而言之,当函数调用时提供了实参,则使用实参;如果没有提供对应实参,则使用缺省值。具体的代码实现如下:

#include <iostream> using namespace std; void Func(int a = 0) { cout << a << endl; } int main() { Func(); // 打印0 Func(5); // 打印5 return 0; }

第一次调用Func时,没有指定实参,就会使用缺省值 0 作为参数的值。

第二次调用Func时,指定了实参,所以照常传入,这里指定的是 5,参数就使用传入的值,也就是 5。

2、分类和使用

2.1 全缺省参数

函数中所有的参数都有缺省值。

#include <iostream> using namespace std; void Func(int a = 1, int b = 10, int c = 20) { cout << "a = " << a << " "; cout << "b = " << b << " "; cout << "c = " << c << endl; } int main() { Func(); // 一个都不传 Func(0); // 只传1个 Func(0, 1); // 只传2个 Func(0, 1, 2); // 全部都传 return 0; }

运行结果如下:

在这个程序中,执行的流程是这样的:

第一次调用Func()时,没有指定实参,就会使用缺省值作为参数的值。
第二次调用Func(0)时,传入了一个实参,所以a采用传入的值,其他的参数则继续使用缺省值作为参数的值。
第三次调用Func(0,1)时,传入了两个实参,所以a/b采用传入的值,c则继续使用缺省值作为参数的值。
第四次调用Func(0,1,2)时,全部参数都传入参数,所以都是采用传入的值作为参数值,缺省值则不起作用。

由此也可以看到,在有缺省参数的时候,调用该函数传参时,传参的顺序是从左到右依次传入的。

2.2 半缺省参数

半缺省参数要求:缺省值必须从参数列表的右侧连续开始设置。

半缺省并不是缺省一半的参数,而是缺省一部分,可以是1个,也可以是多个。

#include <iostream> using namespace std; void Func(int a, int b, int c = 20) { cout << "a = " << a << " "; cout << "b = " << b << " "; cout << "c = " << c << endl; } int main() { // 因为a和b参数不带缺省值,所以调用时,一定要传该参数 // Func(); // 错误 // Func(0); // 错误 Func(0, 1); // 没缺省的参数一定要传值 Func(0, 1, 2); // 全部都传 return 0; }

运行结果如下:

2.3 参数传递特性

既然可以全缺省,也可以半缺省,那可以中间缺省吗?

#include <iostream> using namespace std; void Func(int a = 10, int b, int c = 20) { cout << "a = " << a << " "; cout << "b = " << b << " "; cout << "c = " << c << endl; } int main() { Func(, 1, ); return 0; }

运行结果如下,可以发现,编译器报错。这是因为按照语法要求,缺省参数的设置只能从右到左;而且函数调用时,参数的传递也是从左到右,Func(, 1, )本身也是错误的调用方式,因此不可以这么传递。

2.4 总结

1)缺省值必须是常量或者全局变量,通常使用常量,因为缺省参数必须是一个在编译阶段能够确定的表达式。
2)半缺省参数必须从右侧连续设置,不能跳过中间参数。
3)C语言不支持缺省参数(编译器不支持)。

3、声明和定义

缺省参数不能在函数声明和定义中同时出现(即同一个作用域下不能同时重复指定),在实际开发中,通常建议将缺省参数写在函数声明中,而函数定义中不再设置缺省值。原因如下:

因为C++采用单文件独立编译机制,编译器在处理函数调用点时只能看到声明(接口),若将缺省值写在定义中,调用者将无法获知默认值而报错。同时,C++标准严格禁止在声明和定义中同时设置缺省值,这会导致二义性,即使两处设置的值完全相同,编译器也不会去比对,而是直接报重定义默认参数的语法错误。当函数的声明与定义位于同一个文件中,且定义在调用点之前时,才允许直接在定义处设置缺省值。

#include <iostream> using namespace std; // 声明和定义一起,而且也在调用点之前,可以直接设置缺省值 void Func1(int a = 1) { cout << "a = " << a << endl; } // 声明中设置缺省值 void Func(int a = 10, int b = 20, int c = 30) int main() { Func(); Func1(); return 0; } // 定义中不再设置任何缺省值 void Func(int a, int b, int c) { cout << "a = " << a << " "; cout << "b = " << b << " "; cout << "c = " << c << endl; }

二、函数重载

1、概念

自然语言中,一个词可以有多重含义,人们可以通过上下文来判断该词真实的含义,即该词被重载了。

比如:以前有一个笑话,国有两个体育项目大家根本不用看,也不用担心。一个是乒乓球,一个是男足。前者是“谁也赢不了!”,后者是“谁也赢不了!”

函数重载是函数的一种特殊情况,函数重载是指在同一作用域中,可以定义多个同名函数,这些函数通过【参数列表不同】进行区分

2、构成函数重载的三种情况

函数重载要求函数参数列表不同,分别是参数数量类型类型顺序不同,也只有这三种情况才能构成函数重载。

1)参数数量不同

#include <iostream> using namespace std; void f(int a, int b) { cout << "f(int a, int b)" << endl; } void f(int a, int b, int c) { cout << "f(int a, int b, int c)" << endl; } int main() { f(1, 2); f(1, 2, 3); return 0; }

运行结果如下:

2)参数类型不同

#include <iostream> using namespace std; int Add(int left, int right) { cout << "Add(int left, int right)" << endl; return left + right; } double Add(double left, double right) { cout << "Add(double left, double right)" << endl; return left + right; } int main() { Add(1, 2); Add(1.1, 2.2); return 0; }

运行结果如下:

3)参数类型顺序不同

参数列表中的类型顺序不同,本质也属于参数类型不同的一种情况。

#include <iostream> using namespace std; // 类型顺序不同,本质也是类型不同 void f(int a, char b) { cout << "f(int a,char b)" << endl; } void f(char b, int a) { cout << "f(char b, int a)" << endl; } int main() { f(1, 'a'); f('a', 1); return 0; }

运行结果如下:

3、不能构成函数重载的情况

有些情况下,比如返回值不同、缺省值不同就不能构成函数重载。

1)返回值不同

运行会报错,调用函数时无法根据参数列表确定调用哪个函数,因此无法构成重载。

#include <iostream> using namespace std; void f(int a, int b) { cout << "void f(int a, int b)" << endl; } int f(int a, int b) { cout << "int f(int a, int b)" << endl; } int main() { f(1, 2); // 报错,不知道调用谁 return 0; }

2)缺省值不同

编译器同样无法区分两个函数,因此也会运行报错。

#include <iostream> using namespace std; void f(int a) { cout << "f(int a)" << endl; } void f(int a = 1) { cout << "f(int a = 1)" << endl; } int main() { f(10); // 报错,不知道调用谁 return 0; }

4、可以构成重载但会引起歧义的情况

f()没有传参,此时既可以走无参f(),也可以走缺省参数的f(int a = 1),此时会产生歧义,编译器无法确定,因此会报错。

#include <iostream> using namespace std; void f() { cout << "f()" << endl; } void f(int a = 1) { cout << "f(int a = 1)" << endl; } int main() { f(); // 报错,两个函数都可以调用,存在歧义,不知道调用谁 f(10); // 成功,调用f(int a = 1) return 0; }

5、函数重载的原理(拓展学习)

C++支持函数重载的本质原理就是:名字修饰(name Mangling)

C/C++中,一个程序要运行起来,需要经历以下几个阶段:预处理、编译、汇编、链接。

1)当前a.cpp中调用了b.cpp中定义的Add函数时,编译后链接前,a.o的目标文件中并没有Add函数的具体地址,因为Add是在b.cpp中定义的,函数Add地址保存在b.o中。那么怎么办呢?

2)所以链接阶段就是专门处理这种问题,链接器看到a.o中存在对Add的引用,但是没有对应的函数定义,就会到b.o的符号表中查找Add的符号信息,并将两者链接起来。

3)那么链接时,面对Add函数,链接接器会使用哪个名字去找呢?这里每个编译器都有自己的函数名修饰规则。

4)由于Windowsvs的修饰规则过于复杂,而Linuxg++的修饰规则简单易懂,下面我们使用了g++演示了这个修饰后的名字。

5)通过下面可以看出,在Linux下采用gcc编译时,函数符号名通常不会发生改变;而采用g++编译时,编译器会对函数名进行修饰,将函数参数类型等信息加入到修饰后的名字中【_Z+函数长度+函数名+类型首字母】。其意思是:

_Z 表示 C++ 名字修饰; 3 表示函数名 Add 的长度; Add 表示函数名; ii 表示两个 int 类型参数。

gccg++编译器分别编译后结果:

结论:在linux下,采用gcc编译完成后,函数名字的修饰没有发生改变;采用g++编译完成后,函数名字的修饰发生改变,编译器将函数参数类型信息添加到修改后的名字中。

Windows下名字修饰规则如下:

对比Linux会发现,windowsvs编译器对函数名字修饰规则相对复杂难懂,但道理都是类似的。

6)通过这里就理解了C语言没办法支持重载,因为同名函数没办法区分。而C++是通过函数修饰规则来区分,只要参数列表不同,经过名字修饰后生成的符号名通常就不同,因此 C++ 可以支持函数重载。

7)如果两个函数的函数名和参数列表完全相同,只是返回值不同,则不能构成函数重载,因为函数调用时编译器不会根据返回值进行区分。

三 、引用

1、概念

引用不是新定义一个变量,而是给已存在变量取了一个别名,从语法层面来看,引用不会创建新的对象,它和引用实体表示的是同一个对象。比如:张三,可以叫成小张。

语法上:类型& 引用变量名(引用名) = 引用实体。这里的&并不是取地址运算符,而是引用声明符,用于定义引用。。更具体的代码如下:

#include <iostream> using namespace std; int main() { int a = 0; int& b = a; // b就是a的别名 // 观察两者的地址,可以发现是相同的 cout << &b << endl; cout << &a << endl; b++; a++; cout << a << " " << b << endl; // 两者的值也会同时改变 return 0; }

运行结果如下:

从运行结果可以看到,变量和引用取地址得到的是同一个地址。在语法层面,可以将引用理解为原变量的另一个名字,而不是新创建了一个对象。

2、特性

1)引用在定义时必须初始化,否则编译无法通过。

int main() { int a = 0; int& b; b = a; // 报错,必须在定义时初始化 return 0; }

2)一个变量可以有多个引用,对该变量的引用起引用也是对这个变量的引用。

int main() { int a = 0; int& b = a; // 引用1 int& c = a; // 引用2 int& d = b; // 也是对a的引用 cout << &a << endl; cout << &b << endl; cout << &c << endl; cout << &d << endl; return 0; }

运行结果如下:

可以发现,它们的地址都相同,因此无论是直接引用a,还是对引用b再次建立引用,本质上都是引用同一个对象a

3)引用一旦引用一个实体,就不能引用其他实体

int main() { int a = 0; int& b = a; int c = 1; b = c; // 实际这一步是进行赋值操作,无法改变引用指向 return 0; }

执行b = c后,并不会改变引用的绑定关系,而是把c的值赋给b所引用的对象,也就是a。所以可以得出结论:在定义别名后,就是谁的别名,而不能重新变成其他人的别名。

需要注意的是,指针是可以改变指向的,但当我们查看编译后的汇编代码时,可以发现引用的操作和指针的操作的汇编代码是一样的,因此可以认为,大多数编译器会采用指针的方式实现引用,但这属于编译器实现细节,C++标准并没有规定引用必须采用指针实现。

int main() { int a = 1; int& r = a; int b = 2; int* p = &b; int c = 3; p = &c; // 改变指针指向 return 0; }

4)常引用,临时对象(右值)默认只能绑定到const引用,因此通常使用常引用来引用常量、临时对象以及类型转换产生的临时对象(临时变量具有常性)。

int main() { const int a = 10; //int& ra = a; // 该语句编译时会出错,a为常量 const int& ra = a; // int& b = 10; // 该语句编译时会出错,b为常量 const int& b = 10; double d = 12.34; //int& rd = d; // 该语句编译时会出错,类型不同 const int& rd = d; return 0; }

3、使用场景

3.1 做参数

引用做参数,有两层价值:

1)做输出型参数,可以直接修改实参。
2)减少对象拷贝,提高传参效率,尤其适用于较大的对象或自定义类型。

虽然使用指针同样可以实现这两种效果,但引用的写法更加简洁,也更加符合普通变量的使用习惯。下面以交换两个数为例:

void Swap(int& a, int& b) { int tmp = a; a = b; b = tmp; } int main() { int x = 0, y = 1; Swap(x, y); cout << x << " " << y << endl; return 0; }

在调用Swap()时,参数ab分别是xy的引用,相当于分别成为了它们的别名,因此在函数内部修改ab,实际上就是修改xy

如果采用普通值传递,那么函数内部修改的是形参的副本,无法影响实参;如果采用指针传参,则需要传递地址,并通过解引用进行访问:

void Swap(int* a, int* b) { int tmp = *a; *a = *b; *b = tmp; }

相比之下,引用不需要取地址和解引用,也不存在多级指针的问题,因此在不需要使用空指针或修改绑定关系的情况下,引用通常比指针更加简洁、易读。

3.2 做返回值

1)错误做法

引用也可以作为函数返回值,但必须保证被引用对象在函数返回后仍然存在,否则就会产生野引用。

int& Func() { int a = 10; return a; } // 用于演示代码在Func的栈空间中覆盖此代码,使ret的值发生改变 // 因此代码的结构和Func保持一致 int Func1() { int a = 20; return a; } int main() { int& ret = Func(); cout << ret << endl; Func1(); cout << Func1() << endl; return 0; }

函数中的变量a是局部变量,它存储在函数栈帧中。当Func()执行结束后,函数栈帧被销毁,局部变量a的生命周期也随之结束,因此返回的引用已经失效,形成了野引用。

在某些环境下,由于该栈空间暂时还没有被其他数据覆盖,第一次输出可能仍然是10。但当继续调用Func1()时,新的函数调用很可能会复用之前释放的栈空间,导致原来保存10的那块栈空间被新的局部变量20覆盖。,此时再次访问ret,就有可能输出20

需要注意的是,这种现象只是某一种可能的结果。由于程序访问的是生命周期已经结束的对象,因此其行为属于未定义行为(错误的逻辑),不同的编译器、不同的优化选项,甚至同一程序在不同运行环境下,其结果都可能不同。

2)正确做法

引用返回值要求返回对象必须在函数结束后依然存在,例如可以返回:全局变量、静态局部变量、堆空间对象等

int& Func() { static int a = 10; return a; } int main() { // 返回的是对象本身,而不是副本 // 因此Func(),即变量a,可以被直接赋值 Func() = 100; cout << Func() << endl; }

由于static修饰的局部变量生命周期贯穿整个程序运行期间,因此返回它的引用是安全的。

因此,也可以看出引用做返回值最大的作用就是:

避免对象拷贝,提高效率;如果返回的是非常量引用,还可以直接修改返回对象。

4、传值、传引用效率比较

以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回值通常需要构造返回对象的副本,因此,当参数或返回值是较大的对象时,采用值传递(值返回)会产生对象拷贝,效率通常低于引用传递(引用返回)。
为了便于观察差异,建议在关闭编译器优化的情况下对下面代码进行测试:

#include <iostream> #include <time.h> using namespace std; struct A { int a[10000]; }; void Func1(A a) {} void Func2(A& a) {} A b; A Func3() { return b; } A& Func4() { return b; } int main() { A a; // 以值作为函数参数 size_t begin1 = clock(); for (size_t i = 0; i < 10000; ++i) Func1(a); size_t end1 = clock(); // 以引用作为函数参数 size_t begin2 = clock(); for (size_t i = 0; i < 10000; ++i) Func2(a); size_t end2 = clock(); // 以值作为函数的返回值类型 size_t begin3 = clock(); for (size_t i = 0; i < 100000; ++i) Func3(); size_t end3 = clock(); // 以引用作为函数的返回值类型 size_t begin4 = clock(); for (size_t i = 0; i < 100000; ++i) Func4(); size_t end4 = clock(); cout << "Func1(A)-time:" << end1 - begin1 << endl; cout << "Func2(A&)-time:" << end2 - begin2 << endl; cout << "Func3-time:" << end3 - begin3 << endl; cout << "Func4-time:" << end4 - begin4 << endl; return 0; }

运行结果如下:

通过上述代码运行结果可以发现,对于较大的对象,采用引用作为参数或返回值通常比值传递和值返回效率更高,因为可以避免对象拷贝。

但对于内置类型(如intchardouble等),由于对象本身很小,值传递与引用传递的性能差异通常可以忽略,更应优先考虑代码的可读性。

5、引用和指针的区别

1)语法上:

1、引用是别名,语法层面不开空间,指针是地址,需要开空间存地址
2、引用必须初始化,指针可以初始化也可以不初始化
3、引用不能改变绑定对象,指针可以
4、引用相对更安全,没有空引用,但是有空指针,容易出现野指针,但是不容易出现野引用(做返回值可能会出现)
5、没有NULL引用,但有NULL指针
6、在sizeof中含义不同:引用结果为引用对象的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)
7、引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
8、指针可以形成多级指针,而引用没有多级引用;访问对象时,指针需要显式解引用,而引用可以像普通变量一样直接使用,由编译器完成相关处理。

2)底层上:

在大多数编译器中,引用通常采用指针的方式实现,因此在汇编层面可以看到引用操作最终会转换成类似指针的操作。不过,这属于编译器实现细节,C++标准并没有规定必须这样实现。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询