C++入门(二)

文章目录

  • 七、引用
    • [7.1 引用的概念](#7.1 引用的概念)
    • [7.2 引用的使用](#7.2 引用的使用)
    • [7.3 引用的特性](#7.3 引用的特性)
    • [7.4 常引用](#7.4 常引用)
    • [7.5 引用的使用场景](#7.5 引用的使用场景)
    • [7.6 传值、传引用效率比较](#7.6 传值、传引用效率比较)
    • [7.7 引用和指针的区别](#7.7 引用和指针的区别)
  • 八、内联函数
    • [8.1 内联函数的概念](#8.1 内联函数的概念)
    • [8.2 内联函数的特性](#8.2 内联函数的特性)

七、引用

7.1 引用的概念

引用并不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间

7.2 引用的使用

类型& 引用变量名(对象名) = 引用实体

cpp 复制代码
//引用的使用
void Test()
{
	int a = 10;
	int& ra = a;//<====定义引用类型,此时ra就是变量a的别名,ra和a是同一块内存空间
	printf("a的地址为%p\n", &a);
	printf("ra的地址为%p\n", &ra);

}

int main()
{
	Test();
	return 0;
}

我们可以看到变量a和引用变量ra的地址是一样的,说明他们共用一块内存空间

7.3 引用的特性

使用引用时需要注意一下几点特性

  • 1. 引用在定义时必须初始化
cpp 复制代码
int main()
{
	int a = 10;
	int& b;//错误写法
	int& b = a;//正确写法
	return 0;
}
  • 2. 一个变量可以有多个引用
cpp 复制代码
int main()
{
	int a = 10;
	//下面的b,c,d均是变量a的别名
	int& b = a;
	int& c = a;
	int& d = c;
	printf("%p %p %p %p\n", &a, &b, &c, &d);
	return 0;
}
  • 3. 引用一旦引用一个实体,就不能引用其他实体
cpp 复制代码
int main()
{
	int a = 10;
	int& b = a;//b是a的别名
	int c = 20;

	//能不能将b改成c的别名呢?
	b = c;//不行,这条语句是将c的值赋给引用变量b,即修改变量a的值,并不是让b引用c

	printf("&a = %p &b = %p &c = %p\n", &a, &b, &c);
	printf("a = %d b = %d c = %d\n", a, b, c);
	return 0;
}
  • 4. 引用类型必须和引用实体是同种类型的
cpp 复制代码
int main()
{
	int a = 10;
	double& b = a;//这种写法会报错
	return 0;
}

我们可以看到编译器报错说是"非常量限定",那如果我们加上const修饰呢?

cpp 复制代码
const double& b = a;

我们发现编译通过了,说明上面不是因为int和double类型不一样而报错,那究竟是为什么呢?

实际上,由于引用实体和引用变量的类型不同,编译器会自动进行隐式类型转换,编译器会生成一个double类型的临时变量tmp,然后将a的内容以某种形式放到临时变量tmp中,然后再让b引用临时变量tmp

cpp 复制代码
int main()
{
	int a = 10;
	const double& b = a;

	//类似于下面的步骤
	int a = 10;
	double tmp = a;//将a的值转换赋给tmp
	const double& b = tmp;//b再引用tmp
	return 0;
}

由于临时变量具有常属性,因此tmp的类型就是const double,用double类型的引用变量引用const double类型的变量,这是一种权限的放大,是不被允许的

这就是为什么编译器会报出非常量限定的错误的原因,引用常量d需要加上const修饰,权限的平移是被允许的

最后,本来临时变量tmp在当前语句结束后就会销毁,但此时被b所引用,其生命周期就自动被延长到与引用变量 b 的生命周期相同

分析了这么多,我们用代码验证一下:

cpp 复制代码
int main()
{
	int a = 10;
	const double& b = a;

	printf("&a = %p , &b = %p\n", &a, &b);//求a,b空间的地址

	printf("修改前 a = %d , b = %.2lf\n", a, b);
	a = 20;
	//b = 30; //这句代码会报错,被const修饰的变量不可修改
	printf("修改后 a = %d , b = %.2lf\n", a, b);
	return 0;
}

我们发现a的地址和b的地址不同,这说明了b并不是变量a的引用,而是引用了新形成的临时变量。并且,当我们对a进行修改时,b中的内容并没有发生改变,这也印证了a和b不是同一块内存空间,最后,当我们想要对b的内容进行修改时,编译器会直接报错,说明b所在的空间具有常属性

7.4 常引用

被const关键字修饰的引用变量我们称为常引用,我们无法通过常引用来修改引用实体的值,如下:

cpp 复制代码
#include<iostream>
using namespace std;
int main()
{
	int a = 10;
	const int& b = a;
	//b++;//会报错,b是常引用,无法修改
	a++;//a是普通变量,允许修改
	cout << "a = " << a << ' ' << "b = " << b;
	return 0;
}

前面我们提到了权限不能放大,也就是说:普通引用不能引用常属性变量。但是,权限允许平移或者缩小,即常引用可以引用常属性变量、常引用可以引用普通变量。如下:

cpp 复制代码
#include<iostream>
using namespace std;
int main()
{
	int a = 10;
	const int& b = a;//权限的缩小,const引用普通变量,编译正常

	const int aa = 10;
	const int& bb = aa;//权限的平移,const引用const变量,编译正常

	int& cc = aa;//权限的放大,普通引用const变量,报错
	return 0;
}

7.5 引用的使用场景

引用的使用场景一般有两个:做函数参数、做函数返回值

  • 1. 引用作为函数参数

在C语言中,如果我们调用函数时使用传值调用,那么形参的改变是不会影响实参的,形参是实参的临时拷贝。如果我们想在函数中对实参进行修改,那就必须使用传址调用,通过地址对实参的值进行修改。

而在C++中,新增了引用的语法,我们可以使用引用作为函数的形参,此时形参就是实参的一个别名,并不会额外开辟空间,形参和实参共同内存空间,修改形参也就是对实参进行修改。

具体实现方式如下:

cpp 复制代码
#include<iostream>
using namespace std;
//引用作为函数参数
void Swap(int& x, int& y)
{
	int tmp = x;
	x = y;
	y = tmp;

}
int main()
{
	int a = 10;
	int b = 20;
	cout << "交换前:" << "a = " << a << "b = " << b << endl;
	Swap(a, b);
	cout << "交换后:" << "a = " << a << "b = " << b << endl;
}
  • 2. 引用作为函数返回值

引用也可以作为函数的返回值,如下:

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

int& Count()
{
	static int n = 0;//n是一个静态变量,函数调用结束后不会销毁
	cout << n << endl;
	return n;
}
int main()
{
	int& k = Count();

	k++;

	Count();

	return 0;

}

在Count()函数通过引用返回n,此时main函数中的引用变量k就是n的别名,当我们在main函数中修改k,就相当于对静态变量n做修改

但是,如果以下情况使用引用返回会出现什么情况呢

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

int& Add(int a, int b)
{
    int c = a + b;
    cout << "在Add函数内: c = " << c << ", &c = " << &c << endl;
    return c;
}

int main()
{
    int& ret = Add(1, 2);
    cout << "第一次调用后: ret = " << ret << ", &ret = " << &ret << endl;
    
    Add(3, 4);
    cout << "第二次调用后: ret = " << ret << ", &ret = " << &ret << endl;
    
    // 可能得到7,也可能是垃圾值
    cout << "最终结果: " << ret << endl;
    
    return 0;
}

很惊讶的发现,最终ret变量的值是随机值,这是为什么呢?

这里就要谈谈上述代码出现的野引用问题了

在这段代码中,问题的根源在于 Add 函数返回了一个局部变量的引用。每当程序调用一个函数时,系统都会为该函数在栈上分配一块独立的"栈帧",用于存放该函数的参数和局部变量。Add(1, 2) 第一次被调用时,局部变量 c(值为 3)就被放在这一块栈帧中,并且 ret 这个引用被绑定到了 c 所在的那块栈空间上。然而,当函数返回后,这个栈帧随即被系统回收------也就是说,这块内存以后可以被其他函数再次使用,但里面的字节内容不会立即清空。于是此时 ret 就成了悬空引用:它指向一块已经不再属于 Add 的内存,只是内容暂时还留着 3

当第二次调用 Add(3, 4) 时,系统又创建了新的栈帧,而大多数编译器会"刚好"复用第一次调用所使用过的那块栈空间,把新的局部变量 c(值为 7)放进去。这样,原本已经失效的那块地址被新的 c=7 覆盖,而 ret 仍然指向这块地址,于是在第二次调用结束后,ret 的内容"神奇地"变成了 7。由于这块内存已经不受任何函数合法管理,今后它是否保持 7,又是否被其他操作改写,完全是不可预测的,因此程序的结果是未定义行为

总结来说:返回局部变量的引用会导致引用指向一块随时可能被复用或改写的内存,从而产生悬空引用。第一次调用的结果"看似正确",第二次调用导致的值"莫名改变",其实都是栈帧复用带来的假象,因此这种写法在 C++ 中是绝对不能使用的

总结:函数返回时,如果出了函数作用域,返回对象还在(还没销毁还给系统),则可以使用引用返回;如果已经还给系统了,则必须使用传值返回

7.6 传值、传引用效率比较

在C/C++中,以值作为参数或者返回值类型,在传参和返回期间,函数并不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时拷贝,因此用值作为参数或者返回值类型,需要额外进行拷贝,效率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。

而如果以引用作为参数或者返回值类型,由于引用是作为变量的别名,并不会额外开辟空间形成拷贝。因此在传参和返回期间,就相当于直接传递实参或将变量本身直接返回,效率大大提升

下面我们通过代码来更直观地看看二者的效率差距:

  • 1. 值和引用作为函数参数的效率差距
cpp 复制代码
#include <iostream>
#include <time.h>

struct A { int a[10000]; };

void TestFunc1(A a)
{
	;
}

void TestFunc2(A& a)
{
	;
}

void TestRefAndValue()
{
	A a;
	// 以值作为函数参数
	size_t begin1 = clock(); //clock()函数返回程序运行到调用clock()函数所耗费的时间,单位是ms
	for (size_t i = 0; i < 100000; ++i)
		TestFunc1(a);
	size_t end1 = clock();

	// 以引用作为函数参数
	size_t begin2 = clock();
	for (size_t i = 0; i < 100000; ++i)
		TestFunc2(a);
	size_t end2 = clock();

	// 分别计算两个函数运行结束后的时间
	cout << "TestFunc1(A)-time:" << end1 - begin1 << endl;
	cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;
}
int main()
{
	TestRefAndValue();
	return 0;
}
  • 2. 值和引用作为返回值类型的效率差距
cpp 复制代码
#include <time.h>
struct A
{
	int a[10000];
}a;
// 值返回
A TestFunc1()
{
	return a;
}
// 引用返回
A& TestFunc2()
{
	return a;
}
void TestRefAndValue()
{
	// 以值作为函数的返回值类型
	size_t begin1 = clock();
	for (size_t i = 0; i < 100000; ++i)
		TestFunc1();
	size_t end1 = clock();

	// 以引用作为函数的返回值类型
	size_t begin2 = clock();
	for (size_t i = 0; i < 100000; ++i)
		TestFunc2();
	size_t end2 = clock();

	// 计算两个函数运算完成之后的时间
	cout << "TestFunc1 time:" << end1 - begin1 << endl;
	cout << "TestFunc2 time:" << end2 - begin2 << endl;
}
int main()
{
	TestRefAndValue();
	return 0;
}

通过上述代码的比较,我们发现传值和引用在作为传参以及返回值类型上效率相差很大。传引用的效率远高于传值。因此能使用引用就尽量使用引用,提高效率

7.7 引用和指针的区别

在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。

但在底层实现上实际是有空间的,因为引用是按照指针方式来实现的。这点我们可以参照二者编译后生成的汇编代码证明

cpp 复制代码
int main()
{
	
	int a = 10;
	//引用
	int& ra = a;
	ra = 20;
	//指针
	int* pa = &a;
	*pa = 20;
	return 0;
}

可见,引用和指针的汇编代码是一模一样的,最后都是通过变量a的地址来修改a

不过,引用和指针还是有不同点的,如下:

  1. 引用概念上定义一个变量的别名,指针存储一个变量地址
  2. 引用在定义时必须初始化,指针没有要求
  3. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
  4. 引用必须初始化,故没有NULL引用,但有NULL指针
  5. sizeof的含义不同,sizeof(引用变量)的结果为引用实体的类型大小,而sizeof(指针)始终是地址空间所占字节个数(32位平台下占4个字节,64位平台下占8个字节)
  6. 引用自增即引用的实体增加1,指针自增即指针向后偏移一个类型的大小
  7. 有多级指针,但是没有多级引用
  8. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
  9. 从安全性的角度,引用比指针使用起来相对更安全

引用能否代替指针?不能

在链表里面删除结点的操作需要指针改变指向,但是引用怎么能用来改变指针的指向呢,引用是用来辅助指针的,像链表里面的尾插用引用来代替二级指针,还有两个数据的交换我们也可以使用引用,所以在C++里面引用不能来替代指针。

八、内联函数

8.1 内联函数的概念

以inline关键字修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,不会调用函数建立栈帧,因此内联函数提升程序运行的效率

我们可以通过汇编代码来验证加上inline的函数是否会被调用:

  • 1. 没加inline关键字:
cpp 复制代码
int Add(int x, int y)
{
	return x + y;
}

int main()
{
	int ans = 0;
	ans = Add(1, 2);
	return 0;
}
  • 2. 加上inline关键字:
cpp 复制代码
inline int Add(int x, int y)
{
	return x + y;
}
int main()
{
	int ans = 0;
	ans = Add(1, 2);
	return 0;
}

可以看到,内联函数并不会生成对应的call指令,而是直接被替换到函数调用处,减少了调用函数建立栈帧的开销

注意事项:内联函数的效果需要在release模式才会体现。因为在debug模式下编译器默认不会对代码进行优化,顾不会进行展开

当然我们也可以进行设置,方法如下:

  1. 找到当前项目属性设置页:
  2. 设置调试信息格式:
  3. 设置内联函数扩展:

8.2 内联函数的特性

  1. inline是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会用函数体替换函数调用。缺陷:可能会使目标文件变大;优势:少了调用建立栈帧开销,提高程序运行效率
  2. inline对于编译器而言只是一个建议,不同编译器关于inline实现机制可能不同,一般建议:将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、不是递归、且频繁调用的函数采用inline修饰,否则编译器会忽略inline特性
  3. C语言实现宏函数也会在预处理时替换展开,但是宏函数实现很复杂很容易出错,且不方便调试,C++设计inline目的就是为了替代C的宏函数
    以下为《C++prime》第五版关于inline的描述:
  4. inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,符号表中就没有函数地址了,链接就会找不到
cpp 复制代码
// in.h
#include <iostream>
using namespace std;
inline void f(int i);
 
// in.cpp
#include "in.h"
void fun(int i)
{
    cout << i << endl;
}
 
// main.cpp
#include "in.h"
int main()
{
    fun(10);
    return 0;
}

报错原因:由于in.h文件中只有函数的声明没有定义,顾在编译阶段main.cpp中的fun() 函数无法进行展开,只能在链接阶段进行链接。但是由于in.cpp的fun()函数被声明为内联函数,fun()函数并不会进入符号表,最后就会导致链接时找不到函数地址,报错

相关推荐
CappuccinoRose1 小时前
MATLAB学习文档(二十八)
开发语言·学习·算法·matlab
爱敲代码的loopy1 小时前
MATLAB函数全称解析:旋转翻转找数字
开发语言·matlab
月屯2 小时前
后端go完成文档分享链接功能
开发语言·后端·golang
Franciz小测测2 小时前
Python连接RabbitMQ三大方案全解析
开发语言·后端·ruby
代码雕刻家3 小时前
C语言的左对齐符号-
c语言·开发语言
小肖爱笑不爱笑3 小时前
2025/11/19 网络编程
java·运维·服务器·开发语言·计算机网络
郑州光合科技余经理3 小时前
开发指南:海外版外卖跑腿系统源码解析与定制
java·开发语言·mysql·spring cloud·uni-app·php·深度优先
编程之路,妙趣横生3 小时前
STL(五) priority_queue 基本用法 + 模拟实现
c++
一念一花一世界3 小时前
Arbess从初级到进阶(9) - 使用Arbess+GitLab实现C++项目自动化部署
c++·ci/cd·gitlab·arbess