【C++】——引用、inline内联函数、nullptr

前言

本人其他博客:恋风诗
本文出现的代码见gitee:mozhengy

正文

1.引用

1.1 引用的概念与定义

引⽤不是新定义⼀个变量,⽽是给已存在变量取了⼀个别名,(比如说诸葛亮、诸葛孔明、卧龙先生都是一个人,本质完全一致)
编译器不会为引⽤变量开辟内存空间,它和它引⽤的变量共⽤同⼀块内存空间。

格式为
类型&引⽤别名=引⽤对象;

#include<iostream>

using namespace std;

int main()
{
	int a = 5;
	int& b = a;

	cout << a << endl;
	cout << b << endl;

	cout << &a << endl;
	cout << &b << endl;
	return 0;
}

在这里插入图片描述

可以看到
1.a和b的值相等,都是5,说明b只是a换了一个名字,即起别名
2.a和b的地址一致,引用不会单独开辟内存空间
在这里插入图片描述

1.2 引用的特性

1.引用在定义时必须初始化
C语言指针可能有类似的写法,但在c++引用是错误的

int main()
{
	int a = 0;
	//int& ra;
	//ra = a;
	return 0;
}

结果
2.一个变量可以有多个引用

int main()
{
	//一个变量可以被多次引用
	int a = 0;
	int& ra = a;
	int& rra = a;
	int& b = a;

	cout << a << " " <<&a<< endl;
	cout << ra << " " <<&ra<< endl;
	cout << rra << " " <<&rra<< endl;
	cout << b << " " <<&b<< endl;

	cout << endl;

	int y = 13;
	ra = 13;
	cout << ra << " " << &ra << endl;
	cout << rra << " " << &rra << endl;
	cout << a << " " << &a << endl;
	return 0;
}

在这里插入图片描述
可以看出不论a引用几次,结果和地址都是一样的。
3.引⽤⼀旦引⽤⼀个实体,再不能引⽤其他实体
变量可以有多个引用,但引用只能对应一个变量


int main()
{
	int a = 8;
	int& ra = a;

	int b = 5;
	ra = b;
	return 0;
}

这里不是对b的引用,而是对ra的赋值。

1.3 const引⽤

这里会牵扯到权限的放大和缩小
须知:常量——只读,变量——可读可写
引用过程中,权限只能平移或缩小,不能放大

举例:

int main()
{
	//int& a = 0;
	const int& ra = 0;
	return 0;
}

取消注释出现报错:
报错

  • 0是常量,权限是只读,而int是可读可写,int& a = 0;权限放大,不可行;
  • const int& ra = 0;中const修饰int变量作为引用类型权限是只读,可以接受常量,属于权限平移
int main()
{
	const int& b = 10;
	//int& c = b;
	const int& rb = b;
	return 0;
}
  • const修饰的int变量可以接受常量10,权限都是只读,属于权限平移
  • 第一行结束后,b表示常量,int& c = b;int是可读可写的,属于权限放大,不合规在这里插入图片描述
  • const int&修饰只读,属于权限平移没问题

所谓临时对象就是编译器需要⼀个空间暂存表达式的求值结果时临时创建的⼀个未命名的对象,C++中把这个未命名对象叫做临时对象。

int main()
{
	double d = 7.1;
	//int& e = d;
	const int& rd = d;

	return 0;
}
  • double d = 7.1;定义一个变量,不是引用仅仅是定义,可读可写

  • 使用int作为引用类型,权限也是可读可写的,但是这里不能直接引用,因为引用类型int和引用实体的类型double不同,会有隐式类型转换,这里有个知识点,此时会生成一个const int 的临时变量,权限为只读,从int&到const int 属于权限放大,如图
    在这里插入图片描述

  • 此时应该使用const int& ,该类型权限是只读,属于权限平移


1.4 引用的使用场景

1.做参数
常见操作:Swap交换函数通过引用做参数的方法,使Swap函数的参数ra,rb分别作为函数外的变量a,b的别名,通过函数内部的操作即可影响函数外面的变量,这种参数称为输出型参数

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

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

int main()
{
	int a = 0;
	int b = 8;

	cout << a << " " << b << endl;
	Swap(a, b);
	cout << a << " " << b << endl;
	return 0;
}

第一种是引用,第二种是指针,可以达到同样的效果在这里插入图片描述
2.做返回值
这种写法理论上是错的,但VS2022会“包庇”过去,可以见warning在这里插入图片描述
为什么了?

  • c是局部变量,调用函数建立栈帧后,会销毁,见空间归还给操作系统,这里就要看操作系统了
  • 若对空间清理,原来的数据已经不存在,现在是一个随机值,引用对返回值起别名接受,返回随机值
  • 若不对空间清理,返回值是原有值
  • 注意此时使用别名访问那块空间时,会出现野指针,非法访问内存。
    错误写法:
int& Add(int a, int b)
{
	int c = a + b;

	return c;
}

int main()
{
	int a = 1;
	int b = 2;

	int& c = Add(a, b);

	cout << c << endl;

	return 0;
}

返回值时局部变量时,离开作用域后不应该采用引用做返回值,而应该传值做返回值,即产生临时变量返回。
正确的是

int& Add(int a, int b)
{
	static int c = a + b;
	return c;
}

int main()
{
	int a = 1;
	int b = 2;

	int& d = Add(a, b);
	cout << d;
	return 0;
}

总的来说,引⽤在实践中主要是于引⽤传参和引⽤做返回值中减少拷⻉提⾼效率和改变引⽤对象时同时改变被引⽤对象。

1.5 指针与引用的区别
对比项引用指针
语法概念是变量的别名,不额外开辟空间存储变量的地址,需要开辟空间
初始化要求定义时必须初始化建议初始化,但语法上不是必需的
指向的可变性初始化后不能再引用其他对象可以不断改变指向的对象
访问对象方式可直接访问指向的对象需要解引用才能访问指向的对象
sizeof 运算结果结果为引用类型的大小始终是地址空间所占字节个数(32 位平台下占 4 个字节,64 位平台下占 8 个字节)
安全性很少出现问题,使用起来相对更安全容易出现空指针和野指针问题
多级情况不存在多级引用存在多级指针
自增自减含义对引用对象的值进行自增自减操作指针会指向下一个或上一个同类型的内存地址
作为函数参数传递引用会直接修改实参的值传递指针可以通过解引用修改实参的值,但也可传递指针副本而不修改实参
与数组结合可以引用数组,如 int arr[5]; int (&ref)[5] = arr;可以指向数组首元素,如 int arr[5]; int *ptr = arr;

2. 内联函数

2.1. 概念介绍

使用 inline 修饰的函数被称为内联函数,内联函数会在调用的地方直接展开,没有调用函数栈帧的开销,提高程序效率。

2.2. 内联函数的使用
  • inline 修饰:当函数没有 inline 修饰时,程序执行到函数调用会产生类似 call + 函数地址 的形式,此时需要开辟栈帧、压栈、寄存器等操作,存在性能消耗。

    #include <iostream>
    using namespace std;
    
    int Add(int a, int b) {
        return a + b;
    }
    
    int main() {
        cout << Add(1, 2);
        return 0;
    }
    

    反汇编显示存在 call Add 指令,即调用函数操作。

  • inline 修饰:当函数有 inline 修饰时,反汇编中不会出现 call + 函数地址 的形式,调用内联函数的地方直接展开代码。

    #include <iostream>
    using namespace std;
    
    inline int Add(int a, int b) {
        return a + b;
    }
    
    int main() {
        cout << Add(1, 2);
        return 0;
    }
    

    反汇编显示直接展开 Add 函数逻辑,无调用操作。

2.3. 内联函数的特点
  1. 内联函数是以空间换时间的方法,在编译阶段,编译器将函数当作内联函数直接展开,用函数体代替函数调用,减小了函数调用开辟栈帧的消耗,提高程序效率。
  2. 内联函数会导致代码膨胀,即可执行程序 .exe 变大。例如:
    • Func 函数有 50 行指令,main 函数有 10000 个地方调用 Func
      • 内联情况下:生成的可执行程序约有 10000 * 50 = 50 万行指令。
      • 非内联情况下:生成的可执行程序约有 10000 + 50 = 10050 行指令。
  3. 函数过长(一般代码行超过 10 行)时,不建议设置为内联函数;短小且经常调用的函数,可设置为内联函数。
  4. 内联函数只是对编译器的一种建议,是否采纳取决于编译器。即使声明为内联,若代码过长,编译器可能仍按普通函数处理(如通过反汇编观察到仍有函数调用操作)。
  5. 内联函数不适合递归、代码过长的函数,适用于代码较短(小于 10 行)且在程序中频繁调用的函数。
  6. 内联函数声明和定义不分离。因为各文件单独编译链接,若内联函数仅声明为内联,在定义处无内联属性,函数名不会进入符号表,链接时无法找到函数地址,导致链接失败。
2.4. 内联函数与宏的对比
  • 宏的优缺点
    • 优点:代码复用性高,直接替换效率高。
    • 缺点:复杂易出错,不能调试,没有类型安全检查。
  • 内联函数替代宏(C++ 中短小频繁调用的函数)
    • 优点:可以调试,直接替换效率高。
    • 缺点:可能导致代码膨胀。

3.nullptr 空指针

3.1. C 语言中 NULL 的问题

在 C 语言中,NULL 被定义为宏(#define NULL 0#define NULL (void*)0),但编译器处理 NULL 常量求值结果为 0(整形常量表达式)。例如:

void Func(int a) {
    cout << "void Func(int a)" << endl;
}

void Func(int* a) {
    cout << "void Func(int* a)" << endl;
}

int main() {
    Func(0);        // 调用 void Func(int a)
    Func(NULL);     // 调用 void Func(int a),因 NULL 被宏替换为 0
    return 0;
}

运行结果会调用 void Func(int a),无法达到调用 void Func(int* a) 的目的。

3.2. C++ 中 nullptr 的使用

在 C++ 中,通常用 nullptr 替换 NULL。例如:

void Func(int a) {
    cout << "void Func(int a)" << endl;
}

void Func(int* a) {
    cout << "void Func(int* a)" << endl;
}

int main() {
    Func(0);        // 调用 void Func(int a)
    Func(NULL);     // 调用 void Func(int a)
    Func(nullptr);  // 调用 void Func(int* a)
    // 测试 sizeof
    cout << sizeof(0) << endl;        // 4(假设 32 位环境,实际依环境)
    cout << sizeof(NULL) << endl;     // 4(本质是 0)
    cout << sizeof((void*)0) << endl; // 8(64 位环境指针大小)
    cout << sizeof(nullptr) << endl;  // 8(64 位环境,与指针大小相同)
    return 0;
}

nullptr 能正确匹配 int* 类型的函数参数,避免上述 NULL 的问题。在 C++ 中,为避免异常情况,通常使用 nullptr 替代 NULL

总结

在这里插入图片描述

以上就是本文的全部内容,有用还希望关注点赞支持一下,后续有新的内容会及时补充的

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值