引入:
在上一课中我们提到,cout << 的背后,其实不是普通的语法,而是类对象和运算符一起完成的功能。而这种“协作”的本质,其实就是函数
C++ 中的大多数“高级玩法”,归根结底都离不开函数。函数不仅是程序的基本构建单元,也是很多强大机制的基础。本章我们就来认识一下 C++ 在函数方面做出的几个“增强”,让函数变得智能,用起来更灵活、更安全。
0.C++函数的基本使用指南
在讲函数增强机制之前,我们得先把以下的函数“基本功”打扎实。以下内容是对函数的定义、声明、作用和语法要点的快速复习。
什么是函数?
函数是一段可以重复使用的代码块,它接受输入(参数),执行操作,然后返回结果(或不返回)
我们不可能把所有的操作都堆在main函数里面,函数的存在让程序更模块化、清晰、可维护。
函数声明VS定义
•声明:提前告诉编译器“有这个函数”,但并未告知编译器如何实现。
int add(int a,int b);//声明
•定义:真正写出函数的实现细节
返回值类型 函数名(参数列表){
//函数体,写具体要做的事情
return 返回值;
}
声明是承诺,定义是兑现
注意事项:
•函数声明一般写在前面或.h文件里面,只写函数声明,不写函数体
•声明中参数名可省略,但参数类型不可以省略
•有声明就必须有定义
•函数不能嵌套定义;
•同名函数想要实现不同的功能,要靠函数重载(本节正文会提到)
接下来我们就可以进入正题了!
1.缺省参数/默认参数(Default arguments)
---------------------有些话,不说也懂
什么是缺省参数?
有些函数的参数,并不是每次调用都需要传。例如,一个打印提示语的函数,默认提示是"Hello",只有在特殊情况下才换
这时就可以为参数设置一个默认值,这就是“默认值”(或叫“缺省参数”)
默认参数的好处
•调用更简洁
•函数更灵活,多元化
•兼容更多使用场景
如果你传了值,程序用你的;如果没传,程序就用默认的
语法规则:
返回类型 函数名(参数1 = 默认值,参数2 = 默认值,...);
你可以给函数的部分或全部参数设置默认值,未传入的参数会自动使用默认值。
例子
void greet(string name = "Tom"){
cout<<"Hello," << name << "!" <<endl;
}
int main(){
greet();//输出:Hello,Tom!(默认值)
greet("Stella");//输出你想传的值
return 0;
}
注意:
•默认值必须从右向左连续赋值,中间不能跳
•默认参数和函数重载不能混用过多,否则会让调用变得不清晰
•默认参数适合功能变化不大的函数,逻辑简单,易于理解
2.函数重载(Function Overloading)
---------------------同名但不同用,多态化的体现
什么是函数重载?
有时候,你希望多个函数“同名不同功能(便于使用者理解)”比如都叫Print(),但有的打印整数、有的打印小数、有的打印字符串
为了不取一堆奇怪的函数名,C++允许你用相同的函数名、不同的参数列表来定义多个函数。
这就叫函数重载(Function Overloading)
语法规则:
同一个作用域内,可以定义多个名字相同的函数,只要它们满足这些重载规则:
•参数的类型不同
•参数的个数不同
•参数的顺序不同(较少见)
•返回值类型不能单独作为重载条件
例子:
void print();
//无参
void print(int a);
//参数个数不同
void print(double d);
//参数类型不同
void print(int a,double b);
//参数类型和数量都不同
注意:
默认参数+重载也容易冲突
void show(int a = 1); // 版本1:带默认参数的函数
void show(); // 版本2:无参函数
// 调用
show(); // 歧义!
根据C++的重载决议规则,当有多个函数可以同样好地匹配一个调用时,就会产生歧义。在这种情况下:
-
两个函数都是完全匹配(不需要任何转换)
-
没有哪一个函数比其他函数更"特化"
-
因此编译器无法自动选择,只能报错
3.引用(高效传参)
😮 引用的本质:编译器自动解引用的“安全指针”
我们都知道,在C语言中,函数参数的传递是按值传递(pass by value)的,这意味着当你将一个变量作为参数传递给函数时,函数内部得到的是该变量的副本,而不是原始变量本身。
因此,在函数内部修改参数的值,并不会影响外部的原始变量。因此如果想让函数修改外部变量,只能用指针。比如交换两个变量的值:
#include <iostream>
using namespace std;
void swap(int* a, int* b){//a 是一个指针变量,存储的是某个 int 类型变量的内存地址
int temp = *a;// 把指针 a 指向的值(如 x)存入 temp
*a = *b; // 把指针 b 指向的值写给指针a指向的值
*b = temp;// 把 temp(原 x 的值)赋给 b 指向的值(修改 y)
}
int main() {
int x = 10, y = 20;
cout << "交换前: x = " << x << ", y = " << y << endl;
// 调用 swap,传入 x 和 y 的地址
swap(&x, &y);
cout << "交换后: x = " << x << ", y = " << y << endl;
return 0;
}
这虽然能实现功能,但也带来很多隐患和麻烦:
•调用时必须传地址,写法繁琐 swap(&x, &y);
•需要小心地使用 * 解引用,一不留神就可能出错
•指针可以为 null,也可以乱指地址,容易造成程序崩溃
于是,C++ 引入了“引用”——一种更安全、更简洁的方式来“高效传参”。
引用其实就是C++为了解决指针带来的“繁琐和不安全”而引入的语法糖
什么是引用?
引用(Reference),是为已有变量起的一个别名,用来“无拷贝”地访问原变量。
语法规则:
int a = 10;
int& b = a; // b就是a的别名,b和a共用一份内存
一旦 b 被创建,它就绑定了 a,以后无论是 b++ 还是 a++,它俩的值都会同步变化。
引用传参 = 高效 + 安全 + 简洁
而操作引用就是操作变量本身 而操作普通变量只是操作这个变量复制出来的一个值
以下是用引用传参去写交换两个变量值的例子:
#include <iostream>
using namespace std;
void swap(int& a,int& b){
int temp = a;
a = b;
b = temp;
}
int main() {
int x = 10, y = 20;
cout << "交换前: x = " << x << ", y = " << y << endl;
// 调用swap函数
// 注意:直接传递变量,不需要取地址操作
// x和y会分别绑定到swap函数的a和b引用上
swap(x, y);
cout << "交换后: x = " << x << ", y = " << y << endl;
return 0;
}
这里提一个我们以后会经常用到的东西:
常量引用(const reference)
void showMessage(const string& msg){
cout<<"Message:"<<msg<<endl;
}
用途:
•当你只想传参又不想改变它;
•通常用于传递大型对象时避免拷贝,同时又不需要修改
const引用可以绑定临时值(如函数返回值或字面量)是值传递做不到的。
引用传参的优势在于:
•避免不必要的拷贝,提升性能
当传递大型对象(如结构体、类实例)时,值传递(Pass by Value) 会触发完整的对象拷贝,导致额外的内存和计算开销。
引用传递 直接操作原对象,无需复制,显著提高效率,尤其适用于高频调用或大数据场景。
•语法简洁,降低出错风险
相比指针(* 和 & 操作),引用在语法层面更接近普通变量,无需手动解引用(Dereference),减少因指针误用(如空指针、野指针)导致的运行时错误。
•类型安全(Type Safety)
引用必须在初始化时绑定有效对象(不能为 null),而指针可能指向无效地址。这减少了空引用(Null Reference)风险,增强代码健壮性。
引用和指针的底层实现是一样的,引用的语法比指针的语法更简洁,但又不能完全替代指针。
为了更直观的理解两者的异同,我们来看一个对比表
指针和引用
| 比较维度 | 指针 | 引用 |
| 定义语法 | int* p =&a; | int& r =a; |
| 是否必须初始化 | 否,可以先声明再赋值 | 是,必须在声明时绑定目标 |
| 是否可以为空 | 可以为nullptr | 不允许空引用 |
| 是否重新绑定 | 可以随时指向其他变量 | 一旦绑定,终身不变 |
| 访问值的方式 | 使用*p解引用 | 直接使用引用 |
| 使用安全性 | 存在野指针、空指针等风险 | 更安全,排除常见低级错误 |
| 内存表现 | 占用额外内存存放地址 | 一般无额外开销(但取决于编译器) |
| 适合场景 | 更灵活,适合数组、动态内存、底层操作 | 更简洁,适合函数传参、返回别名等 |
| 是否能替代彼此 | 引用不能完全替代指针 | 指针也不能简洁地替代引用语法 |
注意:
什么时候不能用引用?
✖:返回局部变量的引用
这是最常见,最危险的错误:
int& getNumber(){
int num =10;
return num;//错误,num是局部变量,函数结束时就销毁了
这会导致悬垂引用,后续访问这个引用会引发未定义行为。
总结
C++ 让函数不仅仅是代码的封装,更是效率、安全性、表达力的体现。通过“默认参数”、“重载”和“引用”等特性,我们可以:
写出更灵活的函数接口;
复用函数名,减少命名复杂度;
提高传参效率,同时避免错误。
三个关键词记住本课: 简洁、灵活、高效。
下一课预告:类和对象——你的程序开始拥有“生命”!
类是什么?对象和类有什么关系?
第四课你将学会用代码“造物”,学习如何用类创建属于自己的数据类型,并通过对象来操作它。
练习题(带思考)
✅ 基础练习
1. 写一个函数 printInfo,用来输出对象的属性信息。它有两个参数:name(string)和 age(int),其中 age 有默认值 18。
2. 实现两个重载函数 add,一个加 int,一个加 double。
3. 写一个函数 swapValue(int& a, int& b) 交换两个数,并在 main 函数中调用验证。
✅ 思考题
1. 默认参数和函数重载能否同时用于一个函数?在什么情况下会冲突?
2. 为什么引用必须初始化,而指针不必须?你能想到可能的场景例子吗?
以下是笔者整理的一些课程相关知识在面试中的常见考法,作为复习和扩展练习参考。
✅ 基础面试题
一、默认参数(Default Arguments)
Q1:默认参数为什么必须从右向左连续指定?
A:因为调用函数时是按顺序匹配参数的,若中间跳过,会造成调用歧义。
Q2:默认参数可以在函数声明和定义中同时指定吗?
A:不可以。默认参数只能出现在函数声明中,定义时不能再次指定。否则编译器报错或行为未定义。
二、函数重载(Function Overloading)
Q1:以下代码是否构成重载?为什么?
int func(int a);
double func(int a); // 错误
A:不构成重载。C++ 的函数重载只看参数列表(参数数量、类型、顺序),不看返回值类型。
Q2:类里面两个成员函数只有const不一样,算重载吗?为什么?(含类和对象内容)
A:算重载!因为低层级制不同:
-
非
const成员函数:隐含的this指针类型是ClassName*(可修改对象)。 -
const成员函数:隐含的this指针类型是const ClassName*(不可修改对象)。
class MyClass {
public:
void print() { // 隐含 this 是 MyClass*
std::cout << "Non-const\n";
}
void print() const { // 隐含 this 是 const MyClass*
std::cout << "Const\n";
}
};
int main() {
MyClass obj;
const MyClass cObj;
obj.print(); // 调用非const版本,输出 "Non-const"
cObj.print(); // 调用const版本,输出 "Const"
}
三、引用(Reference)
Q1:引用和指针的本质区别是什么?
A:
引用是别名,不能为 null,不能改变绑定;
指针是地址变量,可以为 null,可以改指向;
引用安全,指针灵活。
Q2:为什么函数参数推荐用 const T&?
A:
避免拷贝(效率高);
const 保证不被修改(安全);
特别适合传递大对象。
进阶面试题
一、重载决议(Overload Resolution)
Q1:以下调用会匹配哪个重载?为什么?
void print(int);
void print(double);
float x = 3.14f;
print(x); // ?
A:匹配 print(double),因为从 float → double 是提升,而不是缩窄,比转成 int 更合适。
Q2:默认参数与重载冲突时怎么办?
A:尽量不要混用默认参数和重载,会引发歧义。例如:
void f(int a, int b = 1);
void f(int a);
f(5); // 哪个版本?冲突!
二、引用的底层实现
Q1:引用的底层是指针吗?
A:是的,大多数编译器内部将引用实现为 T* const(指向 T 的常量指针),但在语法层面它是“自动解引用”的。
端午加更!明天还会有一篇。其余时间正常周四更

835

被折叠的 条评论
为什么被折叠?



