C++从零开始系列篇(二):C++入门——函数重载,引用,inline与nullptr

🧑‍💻博主名称:鱼子星_

✅数据结构专栏:【数据结构】

✅算法竞赛专栏:【算法竞赛】

✅C++系列专栏:【C++从零开始系列】


前言

欢迎来到C++从零开始系列教程!在掌握了C++基础语法后,我们即将深入探索C++相较于C语言的几个核心增强特性。本篇将系统讲解函数重载、引用、内联函数和nullptr这四个关键概念,它们是C++高效编程的基石。

为什么学习这些特性?

  1. 函数重载:告别C语言中为不同参数类型编写不同函数名的繁琐,让同名函数根据参数自动匹配,提升代码可读性和维护性。

  2. 引用:简化指针操作,提供更安全、更直观的变量别名机制,是C++高效传参和返回值的核心手段。

  3. 内联函数:在保留函数结构的同时,消除函数调用开销,是性能敏感场景的优化利器。

  4. nullptr:解决C语言中NULL在C++中的类型歧义问题,提供类型安全的空指针表示。

学习目标

通过本篇学习,你将能够:

  • ✅ 理解并应用函数重载规则,避免常见陷阱
  • ✅ 掌握引用的本质、特性及使用场景
  • ✅ 区分引用与指针的异同,选择合适的数据传递方式
  • ✅ 合理使用内联函数优化程序性能
  • ✅ 正确使用nullptr替代传统的NULL

一. 函数重载

在C语言中,一个程序有两个同名的函数是不被允许的,当需要调用同一个功能仅仅只是参数类型不同时只能写两个名字不相同的函数。C++中提出了函数重载的功能使得相同名字的函数可以被兼容......

1. 函数重载的概念与定义

函数重载可以理解成"一个函数"重载了多种不同的功能。调用该函数时编译器将会自行根据函数参数区分要调用的功能。如下,两个函数就是构成了一个重载函数swap

cpp 复制代码
void swap(int* x, int* y)
{
	int tmp = *x;
	*x = *y
	*y = tmp;
}
void swap(double* x, double* y)
{
	double tmp = *x;
	*x = *y
	*y = tmp;
}

【注意】:C++中构成函数重载并不一定需要函数内部的逻辑是相同的,但是需要让编译器能区分不同功能的函数。函数重载可以由多个函数一起构成。

2. 构成函数重载的规则

前面说了,要构成函数重载的一大前提就是让编译器能区分功能不同但名字相同的函数 。那要如何让编译器能区分呢?通过函数的参数。

要使同名的函数的构成函数重载就只能让函数的参数不同,不同的方式如下:

  • 函数参数的类型不同
cpp 复制代码
//可以构成函数重载
void func(int a, int b)
{
	cout << a << " " << b << endl;
}
void func(double a, double b)
{
	cout << a << " " << b << endl;
}
  • 函数参数的顺序不同
cpp 复制代码
//可以构成函数重载
void func(int a, double b)
{
	cout << a << " " << b << endl;
}
void func(double a, int b)
{
	cout << a << " " << b << endl;
}

能变换的样式有很多,只要是可以让编译器区分的,都可以构成函数重载。现在思考之前学习的缺省参数,那根据缺省参数个数的不同是否可以构成函数重载?

【思考1】缺省参数的个数不同能否作为函数重载的条件?

cpp 复制代码
//可以构成函数重载
void func(int a = 1, int b = 2)
{
	cout << a << " " << b << endl;
}
void func(int a, int b)
{
	cout << b << " " << a << endl;
}

这样的两个函数看着参数确实是不一样的,但是当调用func函数时,会有歧义产生。如下:

cpp 复制代码
int main()
{
	func(10, 20);
	return 0;
}

当传递两个参数时,两个函数都是可以被调用的,此时编译器会区分不了到底需要执行哪个函数。为了避免这个问题,所以C++统一规定了缺省参数的不同不能构成函数重载。

【解答1】不能。会产生歧义,编译器区分不了。

知道了缺省参数的不同的无效性。那么如果域不相同的两个函数能否构成函数重载?(例如:一个函数在命名空间中,一个函数在全局域中)

【思考2】作用域的不同能否构成函数重载?

cpp 复制代码
namespace test
{
	//命名空间内部
	void func(int a, int b)
	{
		cout << a << " " << b << endl;
	}
}
//全局域
void func(double a, double b)
{
	cout << a << " " << b << endl;
}
using test::func;
int main()
{
	int a, b;
	double c, d;
	cin >> a >> b >> c >> d;
	func(a, b):
	func(c, d);
	return 0;
}

主要还是判断编译器能否区分,所以这样也是可以的。

【解答2】:可以。


2.1 不能构成函数重载的情况(编译器不能区分)

  1. 函数参数类型相同
  2. 缺省参数个数的不同
  3. 函数的返回值类型不同

2.2 能构成函数重载的情况(编译器能区分)

  1. 函数参数类型不同
  2. 函数在不同的作用域

二. 引用

在C语言中,如果在函数中想改变实参的值就需要传递指针,但是大量指针的操作会让程序变得很难看并且很容易出错。为了解决这个问题,C++引入了一个新的类型:引用

1. 引用的概念与定义

引用可以看成是一个变量的别名,一般定义成:类型& + 变量名 = 引用

  • 初始化:假设 b 的类型为引用,b 引用了 a 那么 b 就是 a 的别名(别名可以单纯理解成外号,比如李逵外号黑旋风,作者外号叫彭于晏),所以引用的初始化是你要引用的对象。
  • 基础作用:假设 b 就是 a 的别名了,那么 a 就是 b,b 就是 a ,修改 b 也就是修改了 a ,修改 a 同理。
  • 本质:引用的本质其实就是两个变量操控同一个内存空间(黑旋风吃饭了 ==> 李逵吃饭了)。
  • 区分:因为引用的定义带上了一个 & 符号,所以它和取地址的区分方法就是看 & 符号前面是否带有类型,如果有,那就是引用,如果没有就代表是取地址。
cpp 复制代码
int main()
{
	int a = 10;
	int& b = a;
	b += 10;
	cout << a << endl;
	a += 10;
	cout << b << endl;
	return 0;
}

2. 引用的特性

  • 引用定义时必须被初始化
  • 引用被初始化后,不能修改指向的对象,只能进行值的修改操作
  • 在语法上引用被定义不需要额外开辟空间

3. 引用的使用

3.1 引用做函数参数

  1. 通过引用改变实参
    对比这两个函数,一个使用指针来改变实参,一个通过引用来改变实参。因为引用本质就是别名,所以它是可以改变实参的。
cpp 复制代码
//使用指针交换
void swap(int* x, int* y)
{
	int tmp = *x;
	*x = *y;
	*y = tmp;
}
//使用引用交换
void swap(int& x, int& y)
{
	int tmp = x;
	x = y;
	y = tmp;
}
int main()
{
	int a = 1, b = 2;
	swap(a, b);
	swap(&a, &b);
	return 0;
}
  1. 通过引用提高传参效率
    使用引用做函数参数也提高了传参的效率。普通的函数传参是将实参的值拷贝给形参,使用指针传参是通过拷贝实参的地址给形参,而引用传参是直接将实参的别名给形参。
    第一种的传递的效率取决于实参的大小,如果参数是一个数组那传参效率会非常低。第二种传递效率在第一种上有提高,传一个地址即可。但是第三种就完全没有拷贝这个说法,直接给别名,效率最高。
cpp 复制代码
struct List
{
	int arr[100];
	int n;
};
//效率最低
void test01(List LT)
{
	cout << LT.n << endl;
}
void test02(List* LT)
{
	cout << LT->n << endl;
}
//效率最高
void test03(List& LT)
{
	cout << LT.n << endl;
}

3.2 引用做函数返回值

引用不仅可以作为函数的参数,还可以当成函数的返回值。使用引用做返回值需要分情况,使用不当会造成越界访问和野引用的后果。

  1. 返回内存在堆上的变量引用
cpp 复制代码
#include<iostream>
using namespace std;
int b = 2;
//返回全局域的引用
int& test01(int a = 1)
{
	b += a;
	return b;
}
//返回局部域的引用
int& test02(int a = 1)
{
	return a;
}
int main()
{
	int a1 = test01();
	int& b1 = test01();
	//不安全行为 会造成不可预估的后果!!!
	int a2 = test02();
	int& b2 = test02();
	return 0;
}
  • ✔️使用类型接收返回值:可以直接使用普通的类型的变量来接收引用返回值。其作用就是简单的值拷贝。如代码中的变量 a1 返回值可以预测
  • ✔️使用引用接收返回值:使用对应类型的引用来接收引用返回值,此时拿到的是返回的变量的别名。所以拿到返回值后,可以间接改变作为返回值得变量。例如:b1 就是 b 的别名,改变b1就相当于 b 被改变了。返回值可以预测

【其他操作】使用引用作为函数的返回值还有一个很好玩的操作。

cpp 复制代码
test01() += 10;
cout << b << endl;   //此时b为12
  1. 返回局部变量的引用
    因为函数中的局部变量的生命周期由函数决定,当函数执行结束,返回引用时,局部变量被销毁了,此时返回局部变量的引用就是不安全的行为。
  • ❌使用类型接收返回值:由于局部变量在函数执行结束时被销毁了,所以此时接收的值时未知的,不同的编译器可能会返回不同的值。返回值不可预测
  • ❌使用引用接收返回值:使用引用接收局部变量的别名是一个更加危险的操作,相当于是接收了一个已经被销毁的变量的别名,这个也可以称为野引用。局部变量的内存已经被销毁了,再对这个空间进行操作会出现无法预估的后果。返回值不可预测

【类比理解】使用引用接收一个已经被销毁的局部变量的引用就相当于是你开的酒店的房间的上一个住户偷偷配了一个钥匙......很危险。

4. const引用

4.1 const引用的常见情况

在C++中引用可以和 const 关键字搭配使用,一般是用来限制引用的权限,使得其不能修改引用对象的值。const 和引用搭配的场景一般有3个:

  • ✅权限平移:当引用的对象本身就带有常性时,该对象自身就是不可改变的所以作为该对象的引用自然就不可以改变或是引用和被引用的对象都是变量。这种情况就可以看成是权限平移。
  • ✅权限缩小 :当引用的对象为变量,该对象可以被改变,而引用确被 const修饰,代表不能通过这个引用来修改对象的值。此时就可以看成是权限缩小。
  • ❎权限放大:当引用的对象本身带有常性,而引用的类型是一个变量,因为被引用对象本身就是不可改变的,此时使用可变的引用就是权限放大。当遇到权限放大时编译器会报错。

【小提示】一个对象具有常性代表这个对象的值不可以被改变,常见的具有常性的对象有:被 const 修饰的变量,字面常量,临时变量......

cpp 复制代码
#include<iostream>
using namespace std;

int main()
{
	//权限平移
	const int a1 = 10;
	const int& b1 = a;
	int c1 = 10;
	int& d1 = c1;
	//权限缩小
	int a2 = 10;
	const int& b2 = a2;
	//权限放大 此时编译器会报错 具有常性的变量不能被改变
	const int a3 = 10;
	int& b3 = a;
	int& c3 = 20;
	
	return 0;
}

【拓展知识】根据被 const 修饰的引用具有常性,不能改变值,请判断下面的程序中哪个 const 引用有问题,如果没有问题,请指出它们分别是权限平移,权限缩小,权限放大哪个情况?

cpp 复制代码
#include<iostream>
using namespace std;
int main()
{
	int i = 10;
	double d = 3.14;
	const int& a1 = 10;        //(1)
	const int& a2 = i * 10;   //(2)
	int& a3 = d; 			 //(3)
	const int& a4 = d;      //(4)
	return 0;
}

【答案】(3)是错误的语句,(1)(2)(4)都是权限平移。

【解释】d 的是 double 类型的变量,当它赋值给a3时其中还有一步隐式转换,将 d 的值转换成一个临时的整型变量再赋值给a3。前面说了临时变量具有常性因为a3是可变的引用,所以(3)是权限放大的情况,语句错误。(1)(2)(4)参考临时变量具有常性理解。

4.2 const引用的应用场景

const 引用一般在作函数的参数时使用。一般情况下是根据函数的实参是否可以改变来决定是否使用 const 引用。

cpp 复制代码
#include<iostream>
using namespace std;
void f(int& a)
{
	a++;   //形参的改变会影响实参
	cout << a << endl;
}
void f(const int& a)
{
	a++;   //形参的改变不会影响实参
	cout << a << endl;
}
int main()
{
	const int x = 1;
	int y = 2;
	//非常性参数
	f(y);
	//常性参数
	f(x);
	f(3.14);
	f(10 * y);
	return 0;
}

如上代码,当需要函数的实参不能被改变时,就调用使用 const int& a 做形参的函数,反之调用使用int& a做形参的函数。

【小提示】函数的形参是否被 const 关键字修饰也可以构成函数重载。

5. 引用和指针的区别

为了更直观地展示引用和指针的核心差异,下表从多个维度进行了对比:

对比维度 引用 (Reference) 指针 (Pointer)
本质 变量的别名,是已存在对象的另一个名字 存储对象地址的变量
内存占用 语法概念上不占用额外存储空间(编译器通常实现为常量指针) 占用独立内存空间(32位系统4字节,64位系统8字节)
初始化要求 必须在定义时初始化,且初始化后不能更改绑定对象 定义时可以不初始化(但强烈建议初始化),初始化后可以更改指向
空值 (NULL/nullptr) 不能绑定到空值(不存在"空引用") 可以指向空值(NULL或nullptr)
访问方式 直接访问,使用方式与普通变量相同(ref = 5; 间接访问,需要通过解引用操作符**ptr = 5;
多级间接 不支持 多级引用(如int&&在C++11中是右值引用,不是引用的引用) 支持 多级指针(如int** pp
算术运算 不支持 算术运算(如ref++是对绑定对象的操作) 支持 指针算术(如ptr++ptr + n
安全性 更高(无空引用、必须初始化、不能重新绑定) 较低(可能空指针、野指针、内存泄漏)
可读性 更高(代码更简洁,意图更明确) 较低(需要处理地址和解引用)
典型应用场景 函数参数传递(避免拷贝)、函数返回值(返回左值)、范围for循环 动态内存管理、数据结构(链表、树)、数组遍历、与C语言接口交互

5.1 核心区别总结

  1. 语义层面:引用是"别名",指针是"地址"。
  2. 安全层面:引用设计上更安全(无空值、必须绑定、不可重绑定),指针更灵活但也更危险。
  3. 使用层面:引用使代码更简洁,指针提供更低层的控制。

重要提示 :在C++程序中,许多场景下使用引用可以替代指针,使程序可读性更高、安全性更强 。但引用并不能完全替代指针,例如:

  • 需要动态内存分配和释放(malloc/free
  • 需要指向不同对象
  • 需要表示"可能不存在"的状态(空值)
  • 实现链表、树等数据结构时,节点间的连接
  • 与C语言库接口交互时

三. inline(内联函数)

1. 内联函数的概念与定义

在讲内联函数前,还需要回顾一下C语言的知识:。回顾如何使用宏来写一个具有加法功能的函数。以下为3种常见的错误写法:

cpp 复制代码
#include<iostream>
using namespace std;

#define Add(a, b) a + b
#define Add(a, b) (a + b);
#define Add(a, b) (a + b)

int main()
{
	//第一个宏的错误点
	int a = Add(1, 2) * 10;   // a = 1 + 2 * 10;
	//第二个宏的错误点
	if(Add(1, 2)) // if((1 + 2);)
	//第三个宏的错误点
	int b = (1 | 2, 3 & 4);  // b = 1 | 2 + 3 & 4;
	return 0;
}

正确的宏的加法函数为:#define Add(a, b) ((a) + (b))。由此可以得出宏的优缺点。

😀宏的优点 :预处理阶段展开/不会创建函数栈帧/效率高。

😣宏的缺点:复杂/没有类型检查/不安全。

因为C语言中的宏的缺点有很多,于是C++就在原有的宏上进行查缺补漏,而内联函数就是C++查缺补漏的产物,简单说内联函数就是一个进阶的宏。它在原有的宏的特性上结合了函数的定义解决了宏的原先的缺点。

内联函数的定义 :函数名的前面加上一个关键字inline

cpp 复制代码
//内联函数
inline int Add(int a, int b)
{
	return a + b;
}

2. 内联函数的使用

内联函数虽然可以提升效率,但是并不是所有的情况都适合使用内联函数的。

2.1 使用场景

  • ✅适用场景:当函数的实现较为简单,且在程序中多数地方使用了该函数
  • ❎不适用场景:函数的实现复杂,或者时递归函数就不适合适用将该函数定义为内联函数。由于内联函数在预处理阶段会被展开,当函数实现逻辑复杂时,会使得编译的代码量很大,导致程序的空间有很多无端的浪费

【注意】程序进行编译时,编译器会根据内联函数的复杂程度自行决定是否展开。也就是说一个函数被定义成了内联函数并不代表它一定会被展开,是否展开还需要编译器来决定。

2.2 内联函数定义与声明

内联函数的定义与声明不可以分离(其中涉及到编译原理的知识,这里不深入讲解),如果需要跨文件使用内联函数,内联函数的定义需要在头文件(xxx.h)中实现。

四. nullptr

在C语言中,初始化和表示一个空指针都要使用NULL,它本质上是一个宏,定义在传统C的头文件stddef.h中,其源码如下:

c 复制代码
#ifndef NULL
	#ifdef __cplusplus
		#define NULL 0
	#else
		#define NULL ((void *)0)
	#endif
#endif

可以看到 NULL 在C语言中表示的是((void *)0),而在C++中表示的确是 0,在C++中使用 NULL 会出现如下歧义:

cpp 复制代码
#include<iostream>
using namespace std;
void f(int a)
{
	cout << "f(int a)" << endl;
}
void f(int* p)
{
	cout << "f(int* p)" << endl;
}
int main()
{
	f(NULL);   //输出结果为:f(int a)
	return 0;
}

所以C++中引入了一个新的关键字nullptrnullptr是一个特殊的关键字,它只能被转换成任意类型的指针而不能转换成整型,这就解决了 NULL 在C++中会出现歧义的问题。所以在后续的C++程序中,使用nullptr作为空指针更好。

本篇完结

下期预告:

  • C++从零开始系列篇(三):C++类和对象------类的定义,类的实例化,类的大小,this指针
相关推荐
程序猿乐锅1 小时前
【 苍穹外卖day03 | 菜品管理 】
java·开发语言·数据库·mysql
派大鑫wink1 小时前
Java 高级编程技巧(生产级实用,覆盖性能、并发、设计、JVM、语法、避坑)
开发语言·python
JSON_L1 小时前
PHP实现大文件分片上传
开发语言·php
凤山老林1 小时前
JDK 11 升级至 JDK 17
java·开发语言·jdk17·jdk升级·jdk11
指令集梦境1 小时前
图解:单调栈算法模板(Java语言)
java·开发语言·算法
日取其半万世不竭2 小时前
Memos 私人碎片笔记怎么搭?Docker 加 Caddy 一小时跑起来
笔记·docker·容器
小灰灰搞电子2 小时前
C++ boost::circular_buffer 详解:原理、用法与实战
开发语言·c++·boost
sheeta19982 小时前
LeetCode 每日一题笔记 日期:2026.06.16 题目:3612. 字符串特殊符号处理
笔记·算法·leetcode
Hanniel2 小时前
Python描述符(下):内置机制揭秘
开发语言·python·机器学习