1.C++引用
1.1引用的概念和定义
引用不是新定义⼀个变量,而是给已存在变量取了⼀个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同⼀块内存空间。比如四大名著中林冲,他有一个外号叫豹子头,类比到C++里就像变量a,有一个别名叫b,它们所代表的其实都是一个东西,只是名称不同。
类型& 引用别名 = 引用对象
C++中为了避免引入太多的运算符,会复用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;
}
根据代码调试结果和运行结果都可以看到,a,b,c,d共用的是一块内存空间。
1.2引用的特性
• 引用在定义时必须初始化
int main()
{
int a = 10;
//如果引用没有被初始化,会报下面这个错误
// error C2530: “b”: 必须初始化引用
//int& b;
int& b = a;
return 0;
}
• ⼀个变量可以有多个引用
这个特性在之前代码中已经有所体现。
• 引用一旦引用一个实体,再不能引用其他实体
int main()
{
int a = 10;
int b = 20;
int& ra = a;
ra = b;
cout << &a << endl;
cout << &b << endl;
cout << &ra << endl;
return 0;
}
上面这个代码我们要格外注意的是ra = b
并不是让ra
引用b
,而是将b
赋值给ra
,这将导致ra
连带着a
的值发生改变,地址却不会有变化,如果真是引用,那么ra
和b
的地址打印结果应该相同。调试结果如下:
在C++中引用不能改变指向,一旦确定,就无法指向其他变量。
1.3引用的使用
引用在实践中主要是用于引用传参和引用做返回值时减少拷贝提高效率和改变引用对象时同时改变被引用对象。
引用传参举个最简单的Swap函数例子:
void Swap(int& x, int& y)
{
int temp = x;
x = y;
y = temp;
}
int main()
{
int a = 10, b = 20;
Swap(a, b);
cout << a << endl;//20
cout << b << endl;//10
return 0;
}
之前写Swap
函数传参我们要借助指针传参,因为直接传参传的是形参,形参的改变不会改变实参,我们现在可以用引用来代替指针的写法更便捷,因为引用传参不需要显式解引用(*)或取地址(&)操作。引用必须初始化且不能重新绑定,减少了空指针风险。
引用传参跟指针传参功能是类似的,引用传参相对更方便⼀些。
引用和指针在实践中相辅相成,功能有重叠性,但是各有特点,互相不可替代。C++的引用跟其他语言的引用(如Java)是有很大的区别的,除了用法,最大的点,C++引用定义后不能改变指向,Java的引用可以改变指向。
引用做返回值相比传参要复杂一点,我们这里也看一个例子:
int& getElement(int arr[], int index) {
return arr[index];
}
int main() {
int data[3] = { 10, 20, 30 };
getElement(data, 1) = 200;
for (int i = 0; i < 3; ++i)
{
cout << data[i] << " ";
}
return 0;
}
在这段代码中,getElement
函数使用 引用返回值(int&) 的核心作用是:允许通过函数返回值直接修改原始数组中的元素。
普通值返回(int)的局限性:
如果函数返回值类型为 int(值返回),getElement(data, 1)
会返回 data[1]
的拷贝值(20)。此时执行 getElement(data, 1) = 200
; 会报错,因为 无法对临时拷贝值进行赋值(临时值是右值,不能作为赋值的左值)。
引用返回的优势:
返回引用时,getElement(data, 1) 等价于 data[1] 的别名。对返回值的赋值操作会直接作用于原始数组元素,就像直接操作 data[1] 一样。
在后面的博文中我们会进一步对引用返回值进行探究。
1.4 const引用
const修饰变量我们在之前的博文中有所提及,大家可以去看指针(一)这篇博文。
在这里我们要用const修饰引用,看下面的例子:
int main()
{
const int a = 10;
//error C2440 : “初始化”: 无法从“const int”转换为“int& ”
//int& ra = a;
const int& ra = a;//right
int b = 20;
const int& rb = b;
//error C3892 : “rb”: 不能给常量赋值
//rb++;
b++;//right
return 0;
}
C++ 中引用初始化的重要规则:非 const 引用不能绑定到 const 对象,但 const 引用可以绑定到非 const 对象。const引用增加了只读限制,编译器禁止通过该引用修改内存。因为对象的访问权限在引用过程中可以缩小,但是不能放大。
int main()
{
int a = 10;
//error C2440 : “初始化”: 无法从“int”转换为“int& ”
//int& ra = a * 3;
const int& ra = a * 3;
double d = 10.3;
//error C2440: “初始化”: 无法从“double”转换为“int &”
//int& rd = d;
const int& rd = d;
return 0;
}
需要注意的是类似 int& rb = a*3; double d = 12.34; int& rd = d;
这样⼀些场景下a*3的结果保存在⼀个临时对象中, int& rd = d 也是类似,在类型转换中会产生临时对象存储中间值,也就是说,rb和rd引用的都是临时对象,而C++规定临时对象具有常性,所以这里就触发了权限放大,必须要用常引用才可以。
所谓临时对象就是编译器需要⼀个空间暂存表达式的求值结果时临时创建的⼀个未命名的对象,C++中把这个未命名对象叫做临时对象。
其实这里编译器的报错也不是很对,并不是无法转换,而是C++规定临时对象具有常性,权限要匹配的上。
还要注意的是,这里ra和rd的地址空间并不与a和d的地址空间相同,看调试信息:
1.5引用与指针的关系
C++中指针和引用就像两个性格迥异的亲兄弟,指针是哥哥,引用是弟弟,在实践中他们相辅相成,功能有重叠性,但是各有自己的特点,互相不可替代。
• 语法概念上引用是⼀个变量的取别名不开空间,指针是存储⼀个变量地址,要开空间。
• 引用在定义时必须初始化,指针建议初始化,但是语法上不是必须的。
• 引用在初始化时引用⼀个对象后,就不能再引用其他对象;而指针可以在不断地改变指向对象。
• 引用可以直接访问指向对象,指针需要解引用才是访问指向对象。
• sizeof中含义不同,引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节,64位下是8byte)
• 指针很容易出现空指针和野指针的问题,引用很少出现,引用使用起来相对更安全⼀些。
2.缺省参数
• 在 C++ 中,缺省参数(Default Arguments) 是指函数声明时为参数指定一个默认值,当函数调用时未传递该参数时,编译器会自动使用默认值。这可以简化函数调用,减少函数重载的数量。(有些地方把缺省参数也叫默认参数)
• 缺省参数分为全缺省和半缺省参数,全缺省就是全部形参给缺省值,半缺省就是部分形参给缺省值。C++规定半缺省参数必须从右往左依次连续缺省,不能间隔跳跃给缺省值。
• 带缺省参数的函数调用,C++规定必须从左到右依次给实参,不能跳跃给实参。
//C++规定半缺省参数必须从右往左依次连续缺省,不能间隔跳跃给缺省值。
void Func1(int a = 10, int b, int c = 30)//err
void Func1(int a, int b = 20, int c)//err
void Func1(int a = 10, int b, int c)//err
//全缺省
void Func1(int a = 10, int b = 20, int c = 30)//right
{
cout << a << endl;
cout << b << endl;
cout << c << endl;
}
int main()
{
//带缺省参数的函数调用,C++规定必须从左到右依次给实参,不能跳跃给实参。
Func1(, 2, 3);//err
func1(, , 3);//err
Func1(1, , 3);//err
Func1();
Func1(1);
Func1(1,2);
Func1(1,2,3);
return 0;
}
函数声明和定义分离时,缺省参数只能在函数声明中指定,不能在函数定义中重复指定
3.函数重载
C++支持在同⼀作用域中出现同名函数,但是要求这些同名函数的形参不同,可以是参数个数不同或者类型不同。这样C++函数调用就表现出了多态行为,使用更灵活。C语言是不支持同⼀作用域中出现同名函数的。
// 1、参数类型不同
void Swap(int& a, int& b)
{
int temp = a;
a = b;
b = temp;
}
void Swap(double& a, double& b)
{
double temp = a;
a = b;
b = temp;
}
// 2、参数个数不同
void f()
{
cout << "f()" << endl;
}
void f(int a)
{
cout << "f(int a)" << endl;
}
// 3、参数类型顺序不同
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;
}
// 返回值不同不能作为重载条件,因为调⽤时也⽆法区分
//error C2556 : “int fxx(void)” : 重载函数与“void fxx(void)”只是在返回类型上不同
//error C2371: “fxx”: 重定义;不同的基类型
void fxx()
{
}
int fxx()
{
return 0;
}
int main()
{
fxx();
return 0;
}
我们要注意一种情况,当函数重载与缺省参数在同一作用域中结合时,极有可能引发二义性(Ambiguity)问题,导致编译器无法确定该调用哪个函数。
void f()
{
cout << "f()" << endl;
}
void f(int a = 10)
{
cout << "f(int a)" << endl;
}
int main()
{
f();// error C2668: “f”: 对重载函数的调用不明确
return 0;
}
4.inline
在 C++ 中,inline 关键字用于定义内联函数,其核心目的是通过将函数体直接嵌入调用处来减少函数调用的开销,提高程序运行效率。
4.1 内联函数的作用
- 减少函数调用开销常规函数调用需要保存寄存器、跳转指令、恢复现场等操作,存在固定开销。
- 内联函数会在编译阶段将函数体直接替换到调用处,避免了这些开销,尤其适合短小、高频调用的函数。
inline int add(int a, int b)
{
return a + b;
}
int main()
{
int c = add(1, 2);// 编译后等价于 int c = 1 + 2;
return 0;
}
内联函数具有函数的所有优点(类型检查、作用域规则),同时具备宏的展开特性。
4.2语法与规则
在函数声明或定义前加 inline 关键字(通常放在定义处)。
inline void func(); // 声明(可选)
inline void func() { /* 函数体 */ } // 定义(必须标记 inline)
内联函数的限制
- 函数体应简洁:复杂函数(如循环、递归、switch)可能被编译器忽略 inline 请求。
- 必须在调用前可见:内联函数的定义需在调用点之前(通常放在头文件中),否则编译器无法展开。
- 与 static 结合:static inline 函数具有文件作用域,避免链接冲突。
编译器的自主性
inline 是对编译器的建议,而非强制命令。编译器会根据函数复杂度、优化级别等决定是否真正内联。
4.3内联函数与宏的对比
我们之前学过的宏其实有很多隐患,不说隐患,你现在写一个Add宏,你能正确写出来吗?
// 实现⼀个ADD宏函数的常⻅问题
//#define ADD(int a, int b) return a + b;
//#define ADD(a, b) a + b;
//#define ADD(a, b) (a + b)
// 正确的宏实现
#define ADD(a, b) ((a) + (b))
// 为什么不能加分号?
// 为什么要加外⾯的括号?
// 为什么要加⾥⾯的括号?
int main()
{
int ret = ADD(1, 2);
cout << ADD(1, 2) << endl;
cout << ADD(1, 2)*5 << endl;
int x = 1, y = 2;
ADD(x & y, x | y); // -> (x&y+x|y)
return 0;
}
我们可以发现宏定义是有非常多坑的,但是用内联函数就没有以上问题了,特别简单。
inline int Add(int a, int b)
{
return a + b;
}
特性 | 内联函数 | 宏 |
---|---|---|
类型安全 | 有(编译时类型检查) | 无(仅文本替换) |
作用域 | 遵循函数作用域规则 | 全局有效 |
参数计算 | 仅计算一次(按值传递) | 可能多次计算(如 ADD(x++, y++)) |
调试支持 | 可调试(保留函数名等信息) | 调试困难(展开后无原始名称) |
语法错误检查 | 有 | 无(替换后由编译器检查) |
4.4何时使用inline
推荐场景:
- 高频调用的小函数
- 替代简单宏:避免宏的副作用,同时保持效率。
- 模板函数:结合内联减少编译期开销。
不推荐场景:
- 函数体复杂(如包含循环、递归):编译器可能拒绝内联,甚至导致代码膨胀。
- 大函数:内联会导致目标代码体积增大,可能降低缓存效率。
- 递归函数:递归深度可能导致栈溢出,且难以有效内联。