在C++编程中,细节的设计往往决定着代码的简洁性、灵活性与效率。你是否厌倦了C语言函数的参数繁琐的传参?是否期待一种不使用命名空间的引入也能让同一函数名在多场景下应用?是否想要避开指针的操作直接作用于原始数据的方法呢?在C++的殿堂中,缺省参数、函数重载和引用如同三把耀眼的利器,它们以精妙的方式,彻底的超越了C语言的的局限,显著的提升了代码的简洁性、灵活性与效率,带我们进入更加强大的编程范式,更是C++支持抽象编程和多范式设计的基石,本文将为你深入解析这三大利器的核心原理、使用场景与实战技巧。
1.缺省参数
C++中的缺省参数也称为默认实参,在函数的很多次调用中它们都被赋予了同一个值,我们把这个反复出现的值称为函数的默认实参。又因为在调用函数的时候允许省略部分或者全部参数,所以常被称为缺省参数(这里我就以缺省参数来称呼了)。我们为每一个形参都提供了缺省值,缺省值作为形参的初始值出现在形参的列表中,缺省参数分为全缺省参数和半缺省参数,我们可以为一个或多个形参定义缺省值,不过要注意的是,一旦有某个形参被赋予了缺省值,那么它后面的所有形参都必须要有缺省值。
全缺省参数
全缺省参数就是为所有的形参都提供了一个默认值,我们可以传0个到所有数量的参数,如果我们想使用默认实参,只要再调用函数的时候省略该实参就可以了,我们来看下面一段代码来感受一下吧:
#include<ipstream>
using namespace std;
void test(int a=10,int b=20,int c=30)
{
cout<<“a=”<<a<<endl;
cout<<“b=”<<b<<endl;
cout<<“c=”<<c<<endl;
}
int main()
{
test();
test(1);
test(1,2);
test(1,2,3);
return 0;
}
函数调用时实参按其位置解析,缺省值负责填补函数调用时减少的尾部实参(从右往左,连续的设置,不允许跳跃)。
半缺省参数
半缺省参数只有部分参数提供了缺省值,我们看下面一段代码:
void test(int a, int b = 20, int c = 30)
{
cout << "a=" << a << endl;
cout << "b=" << b << endl;
cout << "c=" << c << endl;
cout << endl;
}
int main()
{
test(1);
test(1,2);
test(1,2,3);
return 0;
}
当设计含有缺省值的函数时,其中一项任务就是合理的设置形参的顺序,尽量让不怎么会使用缺省值的参数放在前面。注意:对于函数的声明,通常的习惯是将其放在头文件中,并且一个函数只声明一次,但是多次声明一个函数也是合法的,在给定的作用域中一个形参只能被赋予一次缺省值。换句话说,函数的后续声明只能为之前那些没有缺省值的形参添加缺省值,而且该形参右边的所有形参都必须有缺省值。函数的声明和定义分离时,缺省值不能再函数的声明和定义中重复出现,必须在声明时给定缺省值。为了方便大家理解请看下面两张图片:
现在我为大家举一个缺省参数的例子:
#include <iostream>
#include <cstdlib>
#include <cassert>
struct Stack {
int* data;
int top;
int capacity;
};
void StackInit(Stack* ps, int capacity = 4);
int main() {
Stack s1, s2;
// 实例1:使用缺省参数初始化栈(初始容量为默认的4)
StackInit(&s1); // 未提供capacity,使缺省值4
std::cout << "Stack s1 初始容量: " << s1.capacity << std::endl;
// 实例2:显式指定初始容量,覆盖缺省值
StackInit(&s2, 10); // 为capacity传10
std::cout << "Stack s2 初始容量: " << s2.capacity << std::endl;
//省略了释放内存
return 0;
}
void StackInit(Stack* ps, int capacity) {
assert(ps&&capccity>0)
ps->data = (int*)malloc(capacity * sizeof(int));
if (ps->data == NULL) {
perror("malloc fail");
exit(-1);
}
ps->top = -1;
ps->capacity = capacity;
}
这里我简单的举例了一下栈初始化的例子,我们定义一个初始化栈的函数。通常初始化时我们可能只想分配默认大小的内存,但有时我们预先知道需要存储大量数据,希望初始分配更大空间以避免频繁扩容。
2.函数重载
C++中的函数重载可是一个强大特性,它可以允许你在同一个作用域中定义多个同名函数。如果同一个作用域内的几个函数名字相同但参数类型不同,我们称为函数重载。编译器会根据调用函数时提供的实参信息,自动选择最匹配的函数版本。这使得你可以用同一个函数名执行相似但操作不同的任务,显著地提高了代码地可读性和灵活性,我们先来看一个简单的例子来间的了解一下:
#include<iostream>
using namespace std;
int add(int a,int b)
{
return a+b;
}
double add(double a,double b)
{
return a+b;
}
int main()
{
cout<<"a+b="<<add(1,2)<<endl;
cout<<"a+b="<<add(10.24.20.48)<<endl;
return 0;
}
2.1函数重载规则
理解规则是正确使用函数重载的基础,那我们来一起了解一下函数重载的规则吧:
1.函数名必须相同:所有重载的函数都必须使用相同的函数名。
2.参数列表必须不同:这是构成重载的核心,参数列表不同主要体现在:
- 参数类型不同:
void print(int a); void print(double a);
- 参数个数不同:
void test(int a,int b); void test(int a,int b,int c);
- 参数顺序不同:
void test(int a,double b); void test(double a,int b);
3.返回值不同不能作为函数重载的条件:仅仅返回值类型不同,而参数列表相同,会引发编译错误。(因为调用时无法区分)
2.2重载和const形参
在讨论函数重载和const的关系之前,我需要给大家引入两个关于const的概念。我们都知道指针是一个对象,他又可以指向另一个对象,因此,指针本身是不是一个常量及其所指向的内容是不是一个常量是两个相互独立的话题。我们一般用名词顶层const表示指针本身就是个常量,用底层const表示指针所指向的对象是一个常量。函数重载和各种const形参的情况:
1.值传递
值传递时,形参是实参的一个副本。函数内部修改的是副本,不影响原始数据。因此,无论形参前是否加(即顶层 const),对于函数的调用者来说没有区别。
void print(int x);
void print(const int x);//重定义
2.指针传递
指针传递要区分顶层const和底层const。
顶层const
void modifyPointer(int* ptr);
void modifyPointer(int* const ptr); // 重定义
原因:int * ptr和int* const ptr都是指向非常量整数的指针类型。指针本身是否为常量在函数形参中无法区分。
底层const
void printData(int* ptr); // 版本1:可修改所指数据
void printData(const int* ptr); // 版本2:不可修改所指数据
int main() {
int a = 10;
const int b = 20;
printData(&a); // 调用版本1
printData(&b); // 调用版本2
return 0;
}
原因:int* 和const int* ptr是两种不同类型的指针,它们决定了函数内部是否能通过指针修改所指数据。
3.引用传递(下一个讲解的大标题就是引用,可以先看万引用再回来看)
引用传递本身没有顶层 const(因为引用一旦绑定就不能改变),所以所有用于引用的 const 都是底层 const,修饰的是所引用的对象。因此,const引用和非const引用构成重载。
void modifyValue(int& ref); // 版本1:可修改引用对象
void modifyValue(const int& ref); // 版本2:不可修改引用对象
int main() {
int x = 5;
const int y = 10;
modifyValue(x); // 调用版本1
modifyValue(y); // 调用版本2
modifyValue(20); // 也可调用版本2 (字面量只能绑定到const引用)
return 0;
}
4.成员函数中的 const (非常特殊且重要!)
在类的成员函数中,const关键字放在函数参数列表之后,表示该函数是 const 成员函数。它承诺不会修改类的任何非静态成员变量(除非成员被mutable修饰)。const 成员函数和非 const 成员函数即使参数列表完全相同,也被视为重载。
class MyArray {
public:
int& operator[](size_t index)// 用于非const对象,可修改
{
return data[index];
}
const int& operator[](size_t index) const // 用于const对象,只读
{
return data[index];
}
private:
int* data;
size_t size;
};
int main() {
MyArray arr;
const MyArray constArr;
arr[0] = 42; // 调用非const版本
int value = constArr[0]; // 调用const版本
return 0;
}
3.引用
C++中的引用不是创建一个新变量,而是给已经存在的变量取个别名(可以理解为取了个外号)。
类型& 引用别名(外号)=应用的对象
像是下面这个例子:
#include<iostream>
using namespace std;
int main()
{
int a=10;
int& b=a;
int& c=a;
int& d=b;
cout<<&a<<endl;
cout<<&b<<endl;
cout<<&c<<endl;
cout<<&d<<endl;
return 0;
}
3.1引用的特性
1.必须初始化
引用在定义时必须绑定到一个已存在的对象或变量,不能先声明后赋值。上面的例子中已经表现出来了,这里就不在写了。
2.一旦绑定之后,就不得修改
引用一旦初始化,就不能再绑定其他的对象(引用很专一的)。
#include<iostream>
using namespace std;
int main()
{
int a=10,b=20;
int& c=a;
c=b;//不能引用其他的变量,这里相当于将b的值赋值给a
cout<<a<<endl;
return 0;
}
这时候有人就说了,你那里是c=b,根本不是int& c=b,那么我们写成int& c=b不就重定义了嘛: