C++ 引用与内联函数详情

目录
  • 引用初阶
    • 什么是引用
    • 为何要有引用
    • 引用指向同一块空间
    • 引用的特性
      • 定义时必须初识化
      • 一个变量可以多次引用
      • 引用一旦引用了一个实例,不能在再引用其他的实例
  • 引用进阶
    • 常引用
      • 权限
      • 临时变量具有常属性
    • 引用的场景
      • 做参数
      • 返回值
      • 引用做返回值
    • 引用不会开辟空间
    • 引用和指针比较
  • 内联函数
    • 为何存在 内联函数
    • 展开短小的函数
    • 内联函数的特性
      • 较大的函数编译器不会发生内联
      • 声明定义一起

引用初阶

引用是C++的特性的之一,不过C++没有没有给引用特意出一个关键字,使用了操作符的重载。引用在C++中很常见,常见就意味着它很重要。我们分两个境界来谈谈引用,初阶是我们能在书上看到的.

什么是引用

引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间 .所谓的取别名,就是取外号,张三在朋友面前可能叫三子,在长辈面前可能叫三儿,但是无论是叫三子还是三儿,他们叫的就是张三,这是无可否认的.引用的符号&和我们取地址的&操作符一样的,后面我们就会知道这是操作符构成重载的原因.

我们可以理解所谓的引用.

#include <stdio.h>
int main()
{
	int a = 10;
	int& b = a;       //  b 是 a的一个别名
	printf("%p\n", &a);
	printf("%p\n", &b);
	return 0;
}

为何要有引用

引用有很多优点,其中有一个就是可以简化代码,我们在C语言中写过如何交换两个整型数据,需要借助一维指针,但是这有一点麻烦,使用引用就可以很好的解决这个问题.

#include <stdio.h>
void swap(int& a, int& b)
{
	int c = a;
	a = b;
	b = c;
}
int main()
{
	int a = 10;
	int b = 20;
	printf("交换前  a = %d b = %d\n", a, b);
	swap(a, b);
	printf("交换后  a = %d b = %d\n", a, b);
	return 0;
}

引用指向同一块空间

我们无论给变量取多少外号,但是这些外号所指向的空间是一样的,我们只要修改一个外号的空间,就会导致其他外号值得修改.

#include <iostream>
using namespace std;
int main()
{
	int a = 10;
	int& b = a;  //b 是  a  的别名
	int& c = b;  //还可以对c取别名
	return 0;
}

int main()
{
	int a = 10;
	int& b = a;  //b 是  a  的别名
	int& c = b;  //还可以对c取别名
	c = 20;
	cout << "a: " << a << endl;
	cout << "b: " << a << endl;
	cout << "c: " << a << endl;
	return 0;
}

引用的特性

即使引用很好使用,但是我们还要关注他们的一些特性.

定义时必须初识化

这个要求很严格,我们必须在引用的时候给他初始化,否则就会报错.

#include <iostream>
using namespace std;
int main()
{
	int a = 10;
	int& b;
	return 0;
}

一个变量可以多次引用

这个特性我们前面和大家分享过了,我们可以对一个变量多次取外号,每一个外号都是是这个变量的别名,我们甚至可以对外号取外号,不过我们一般不这么干.

引用一旦引用了一个实例,不能在再引用其他的实例

这个特性完美的阐释了引用的专一性,一旦我们给某一个变量起了一个别名,这个别名就会跟着这个变量一辈子,绝对不会在成为其他的变量的别名.这个也是和指针很大的区别,指针比较花心.

这个我们就不通过代码打印各个变量的地址了.,我们观看另外一种现象,通过观察我们看到a的值也被修改了,可以确定 b 仍旧a的别名.

int main()
{
	int a = 10;
	int& b = a;
	int c = 20;
	// 这个一定是赋值
	b = c;
	cout << "a: " << a << endl;
	return 0;
}

引用进阶

我们已经看到引用的优点,但是这是引用的基础用法,要是到那里,我们一般看书都可以做到,现在要看看引用更加详细的知识.

常引用

我们在C语言中,专门分享过const相关的知识,那么我们如何对const修饰的变量取别名呢?这是一个小问题.

const int a = 10;

我们看看下面的方法.

int main()
{
	const int a = 10;
	int& b = a;        //报错
	const int& c = a;  // 不报错
	return 0;
}

到这里我们就可以发现,我们取别名的时候也用const修饰就可以了,这是我们从现象中的得出的结论,但是这又是因为什么呢?我们需要知道他们的原理.

权限

不知道大家有没有在学习中看到过这样一种现象,对于一个文件,我们可能存在仅仅阅读的权限,自己无法修改,但是其他的人有可能有资格修改,这就是权限的能力,const修饰变量后,使得变量的加密程度更加高了,我们取别名的时候,总不能把这个权限给过扩大了,编译器这是不允许的,也就是说我们取别名的时候,权限只能往低了走,绝对不能比原来的高.

下面就是把权限往缩小了给

int main()
{
    int a = 10;
	const int& b = a;
	return 0;
}

常量取别名必须要是const修饰的,因为常量不能修改.

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

临时变量具有常属性

这个是语法的一个知识点,我们记住就可以了,现在我们就要看看为什么这个代码会可以运行.

我们常引用 还有最后一个问题,如果我们是double类型的变量,如何给取一个int类型的别名?

需要用const修饰,而且会发生截断,

int main()
{
	double d = 1.2;
	const int& a = d;       //需要用const修饰
	cout << a << endl;
	return 0;
}

它的本质不是取别名,而类似于给常量取别名,而且还会发生截断.这个说截断也不太合适,这会产生一个临时变量,我把原理图放在下面.

int main()
{
	double d = 1.2;
	const int& a = d;
	cout << "&a :" <<&a << endl;
	cout << "&d :" <<&d << endl;
	return 0;
}

引用的场景

我们需要来看看引用的使用场景,它主要有两大作用.

  • 做参数
  • 做返回值

做参数

我们可以使用引用来交换两个整型数据.

#include <stdio.h>
void swap(int& a, int& b)
{
	int c = a;
	a = b;
	b = c;
}
int main()
{
	int a = 10;
	int b = 20;
	printf("交换前  a = %d b = %d\n", a, b);
	swap(a, b);
	printf("交换后  a = %d b = %d\n", a, b);
	return 0;
}

但是使用引用作为参数可能会出现权限不太匹配的错误,所以说我们需要const修饰.

下面就是由于权限问题,我们没有办法来给常量却别名,这就需要const修饰.

void func(int& a)
{
}
int main()
{
	int a = 10;
	double b = 10.2;

	func(a);
	func(10);
	func(b);
	return 0;
}

返回值

我们先看看返回值的原理,再说说引用做返回值.

返回值的原理:

我们需要谈谈编译器是如何使用返回值的,以下面的代码为例.

int func()
{
	int n = 1;
	n++;
	return n;
}

int main()
{
	int ret = func();
	return 0;
}

编译器会看这个返回值的空间大不大,如果不大,就把的数据放到一个寄存器中,如果很大,看编译器的机制,有的编译器甚至可能在main函数中开辟这个空间来临时保存数据.

这是由于当函数结束后,函数栈帧会销毁,n的空间也会被释放,所以要有一个寄存器来保存数据.

下面的代码就可以表现出来:

int main()
{
	int& ret = func();
	return 0;
}

引用做返回值

引用做返回值会有很大的要求,这个和我们普通的返回值可不一样.说实话,我不想和大家分享的那么深,但是已经到这里了,只能这样了.

下面的代码,我们可以理解,就是给静态变量n取一个别名,我们把它返回到了ret.

int& func()
{
	static int n = 1;
	n++;
	return n;
}
int main()
{
	int ret = func();
	cout << ret << endl;
	return 0;
}

也就是说我们n取一个别名,把这个别名作为返回值返回给函数调用者.

int& func()
{
	static int n = 1;
	n++;
	cout << &n << endl;
	return n;
}

int main()
{
	int& ret = func();     //  注意 是 int&
	cout << &ret << endl;
	return 0;
}

注意事项:

到这里我们就可以看看引用做返回值的注意事项了,我们一定要保证做返回值得引用得数据在函数栈帧销毁后空间不被释放,否则就会发生下面得事情.

我们得到的是一个随机值,我们拿到了变量 n 的别名,但是在func结束后空间就被释放了,下一次函数的调用函数栈帧会覆这篇空间,运气好的话,我们有可能拿到准确值,但是无论如何访问都越界了.

int& func()
{
	int n = 1;
	n++;
	return n;
}

int main()
{
	int& ret = func();
	printf("这是一条华丽的分割线\n");
	cout << ret << endl;
	cout << ret << endl;
	return 0;
}

引用不会开辟空间

前面我们说了传参需要有一定的要求,但是这不是说引用做参数不行,我们使用引用传参不会发生拷贝,这极大的提高了代码的效率.

我们定义一个大点的结构体,来看看拷贝传参和引用传参的效率.一般情况下相差大概20倍左右.

typedef struct A
{
	int arr[10000];
} A;

void func1(A a)
{

}

void func2(A& a)
{

}

void TestRefAndValue()
{
	A a;

	// 以值作为函数参数
	size_t begin1 = clock();
	for (size_t i = 0; i < 100000; ++i)
		func1(a);
	size_t end1 = clock();

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

	// 分别计算两个函数运行结束后的时间
	cout << "func1(A)-time:" << end1 - begin1 << endl;
	cout << "func2(A&)-time:" << end2 - begin2 << endl;
}

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

引用和指针比较

它们有一个本质的区别,在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间 ,但是引用的底层还是开辟空间的,因为引用是按照指针方式来实现.我们看看汇编代码.

  • 引用不会开辟空间,但是指针会开辟相应的空间.

底层引用和指针是一样的.

int main()
{
	int a = 10;
	//语法没有开辟空间  底层开辟了
	int& b = a;
	b = 20;

	//语法开辟空间  底层也开辟了
	int* pa = &a;
	*pa = 20;
	return 0;

我们来看看它们其他的比较小的区别,我就不详细举例了.

  • 引用在定义时必须初始化,指针没有要求
  • 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
  • 没有NULL引用,但有NULL指针
  • sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)
  • 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
  • 有多级指针,但是没有多级引用
  • 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
  • 引用比指针使用起来相对更安全

内联函数

以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数压栈的开销,内联函数提升程序运行的效率,本质来说就和我们的宏比较相似.

记住我们不关心他们在那个过程中展开,只需要记住他会展开就可以了.我么看看内联函数的知识点.

为何存在 内联函数

对于一些比较小的函数,我们总是调用函数开销比较的,我们是不是存在一个方法减少这个开销呢?C语言通过宏来实现,C++支持C语言,所以我们可以通过行来实现,那么你给我写一个两数现相加的宏,要是你写的和下面的不一样,就代表你忘记了一部分知识点.

#define ADD(x,y) ((x) + (y))       //不带  ;   括号要带

我们就可以理解了,宏很好,但是写出一个正确的宏很困难,但是写一个函数就不一样了,所以一些大佬就发明了内敛函数,用来替代宏的部分功能.

展开短小的函数

函数内联不内联不是你说了算,我们用inline修饰就是告诉编译器这个函数可以展开,至于是否展开还是看编译器,一般之后展开比较短小的函数,较大的函数不会展开,像递归的那些也不可以.

inline void swap(int& x, int& y)
{
	int ret = x;
	x = y;
	y = ret;
}

int main()
{
	int a = 1;
	int b = 2;
	swap(a,b);
	cout << "a: " << a << endl;
	cout << "b: " << b << endl;
	return 0;
}

我们来看看内联函数,如果函数不是内联了的,汇编语言会call

void swap(int& x, int& y)
{
	int ret = x;
	x = y;
	y = ret;
}

如果在上述函数前增加inline关键字将其改成内联函数,在编译期间编译器会用函数体替换函数的调用.

由于我们使用的是VS编译器,这里需要看看内联函数的汇编语言.

在release模式下,查看编译器生成的汇编代码中是否存在call swap,但是编译器会发生优化,我们通过debug模式下,但是需要设置.

inline修饰的较短函数展开了,没有call

inline void swap(int& x, int& y)
{
	int ret = x;
	x = y;
	y = ret;
}

内联函数的特性

我们需要来看看函数的基本的特性

  • inline是一种以空间换时间的做法,省去调用函数额开销。所以代码很长或者有循环 / 递归的函数不适宜使用作为内联函数。
  • inline 对于编译器而言只是一个建议,编译器会自动优化,如果定义为inline的函数体内有循环/递归等,编译器优化时会忽略内联。
  • inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到。

较大的函数编译器不会发生内联

编译器会自动判别这个函数给不该内联,要是一个函数比较大,里面存在递归,那么还不如不展开呢.一般是10行为依据.

inline void f()
{
	cout << "hello" << endl;
	cout << "hello" << endl;
	cout << "hello" << endl;
	cout << "hello" << endl;
	cout << "hello" << endl;
	cout << "hello" << endl;
	cout << "hello" << endl;
	cout << "hello" << endl;
	cout << "hello" << endl;
	cout << "hello" << endl;
	cout << "hello" << endl;
	cout << "hello" << endl;
	cout << "hello" << endl;
	cout << "hello" << endl;
	cout << "hello" << endl;
	cout << "hello" << endl;
	cout << "hello" << endl;
	cout << "hello" << endl;
}
int main()
{
	f();
	return 0;
}

声明定义一起

如果我们声明和定义的分离,运行时编译器会找不到这个函数的地址的.

这里我么建议直接把内联函数直接放到自定义的头文件中

inline void f()
{
}

到此这篇关于C++ 引用与内联函数详情的文章就介绍到这了,更多相关C++ 引用内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • C++成员解除引用运算符的示例详解

    下面看下成员解除引用运算符,C++允许定义指向类成员的指针,对这种指针进行声明或解除引用时,需要使用一种特殊的表示法.例: class Example { private: int feet; int inches; public: Example(); Example(int ft); ~Example(); void show_in()const; void show_ft()const; void use_ptr()const; }; 如果没有具体的对象,则inches成员只是一个标签.也

  • c++内联函数(inline)使用详解

    介绍内联函数之前,有必要介绍一下预处理宏.内联函数的功能和预处理宏的功能相似.相信大家都用过预处理宏,我们会经常定义一些宏,如 复制代码 代码如下: #define TABLE_COMP(x) ((x)>0?(x):0) 就定义了一个宏. 为什么要使用宏呢?因为函数的调用必须要将程序执行的顺序转移到函数所存放在内存中的某个地址,将函数的程序内容执行完后,再返回到转去执行该函数前的地方.这种转移操作要求在转去执行前要保存现场并记忆执行的地址,转回后要恢复现场,并按原来保存地址继续执行.因此,函数调

  • C++入门(命名空间,缺省参数,函数重载,引用,内联函数,auto,范围for)

    一.C++关键字 C++总共有63个关键字,在入门阶段我们只是大致了解一下就可,在后续博客中会逐渐讲解 二.命名空间 相信学过C++的同学,一定都写过下面这个简单的程序 #include<iostream> using namespace std; int main() { cout<<"hello world"<<endl; return 0; } 我们先来看第二行代码,using namespace std , 这行代码是什么意思呢 ? 这里我们

  • c++中的内联函数inline用法实例

    问题描述:类中成员函数缺省默认是内联的,如果在类定义时就在类内给出函数定义,那当然最好.如果在类中未给出成员函数定义,而又想内联该函数的话,那在类外要加上 inline,否则就认为不是内联的.内联函数的inline要加在函数前面,不可以加在声明前面. class A { public:void Foo(int x, int y) { } // 自动地成为内联函数 } //正确写法: // 头文件 class A { public: void Foo(int x, int y); } // 定义文

  • c++ 内联函数和普通函数的区别

    前言 内联函数是c++为了提高程序的运行速度做的改进,它与普通函数区别在于: 编译器如何将它们组合到程序中.所以我们需要深入到程序内部. 我们的最终的可执行程序由 一组机器指令组成.程序运行时,计算机逐步执行指令. Ⅰ.常规函数 常规函数调用时会使程序跳到另一个地址(函数的地址),并且在函数结束时返回. 执行函数调用指令,立即存储该指令的地址,并将函数参数保存到的堆栈. 跳到函数起点的内存单元,执行函数代码(将返回值保存到寄存器中. 跳回被保存指令的地址处. 这一过程和系统中的中断很类似.来回跳

  • C++ 引用与内联函数详情

    目录 引用初阶 什么是引用 为何要有引用 引用指向同一块空间 引用的特性 定义时必须初识化 一个变量可以多次引用 引用一旦引用了一个实例,不能在再引用其他的实例 引用进阶 常引用 权限 临时变量具有常属性 引用的场景 做参数 返回值 引用做返回值 引用不会开辟空间 引用和指针比较 内联函数 为何存在 内联函数 展开短小的函数 内联函数的特性 较大的函数编译器不会发生内联 声明定义一起 引用初阶 引用是C++的特性的之一,不过C++没有没有给引用特意出一个关键字,使用了操作符的重载.引用在C++中

  • C++内联函数详情

    内联函数是C++当中为了提高程序运行效率的设计,老实讲我没有在其他语言当中看到类似的设计.它和常规函数之间的主要区别不在于编写的方式,而是在于C++编译器会将内联函数组合到程序当中执行. 要解释这个过程会稍稍有些复杂,我们需要从编译的过程说起.对于编译型语言而言,编译器做的事情是把人类写出来人能读懂的代码翻译成机器能够识别.执行的机器语言,一般是一串十六进制的指令.随后计算机逐步执行这些指令,完成我们想要的功能. 当我们调用函数时,其实本质上是指令跳转,先记录下当前运行的指令位置,跳转到函数所在

  • C++类与对象深入之引用与内联函数与auto关键字及for循环详解

    目录 一:引用 1.1:概念 1.2:引用特性 1.3:常引用 1.4:使用场景 1.5:引用和指针的区别 二:内联函数 2.1:概念 2.2:特性 2.3:面试题 三:auto关键字 3.1:auto简介 3.2:auto使用细则 3.3:auto不能推导的场景 四:基于范围的for循环 4.1:范围for循环的语法 4.2:范围for循环的使用条件 一:引用 1.1:概念 引用不是定义一个新的变量,而是给已经存在的变量取一个别名.注意:编译器不会给引用变量开辟内存空间,他和他的引用变量共用同

  • C++示例分析内联函数与引用变量及函数重载的使用

    目录 1.内联函数 1.1为什么使用内联函数 1.2语法 2.引用变量 2.1为什么要使用引用变量 2.2语法 2.3对于C语言的改进 3. 函数重载 3.1默认参数 3.2函数重载 1.内联函数 1.1为什么使用内联函数 减少上下文切换,加快程序运行速度. 是对C语言中的宏函数的改进. 1.2语法 #include<iostream> using namespace std; inline double square(double x){ return x*x; } int main(){

  • 深入探讨:宏、内联函数与普通函数的区别

    内联函数的执行过程与带参数宏定义很相似,但参数的处理不同.带参数的宏定义并不对参数进行运算,而是直接替换:内联函数首先是函数,这就意味着函数的很多性质都适用于内联函数,即内联函数先把参数表达式进行运算求值,然后把表达式的值传递给形式参数.    内联函数与带参数宏定义的另一个区别是,内联函数的参数类型和返回值类型在声明中都有明确的指定:而带参数宏定义的参数没有类型的概念,只有在宏展开以后,才由编译器检查语法,这就存在很多的安全隐患.    使用内联函数时,应注意以下问题:    1)内联函数的定

  • C#条件编译、内联函数、CLS介绍

    1.条件编译 #if 条件编译会隐藏非条件(#else if)代码,我们开发中很可能会忽略掉这部分代码,当我们切换条件常量到这部分代码时,很可能因为各种原因导致报错. 如果使用特性进行条件编译标记,在开发过程中就可以留意到这部分代码. [Conditional("DEBUG")] 例如,当使用修改所有引用-修改一个类成员变量或者静态变量名称时,#if 非条件中的代码不会被修改,因为这部分代码“无效”,而且使用 [Conditional("DEBUG")] 的代码则跟

  • C++深入探索内联函数inline与auto关键字的使用

    目录 1.内敛函数 1.1问题引入 1.2内联函数的概念 1.3内敛函数的特性 2.auto关键字 2.1 auto简介 2.2 auto的使用细则 2.3 auto不能推导的场景 2.4 auto与新式for循环使用 1.内敛函数 1.1问题引入 我们在使用C语言中我们都学过函数,我们知道函数在调用的过程中需要开辟栈帧.如果我们需要频繁的调用一个函数,假设我们调用10次Add()函数,那我们就需要建立10次栈帧.我们都知道在栈帧中要做很多事情,例如保存寄存器,压参数,压返回值等等,这个过程是很

  • C++中类的成员函数及内联函数使用及说明

    目录 成员函数 成员函数中出现的参数 类相关的非成员函数 自己定义的对象作为函数的形参 成员函数返回值是对象的引用 成员函数调用成员函数 内联函数 成员函数 即在类内定义的方法.通过对象名.成员函数()的方式可以调用. 一般将成员函数定义在类外,因为成员函数一般比较复杂. 简单的成员函数可以定义在类内,定义在类外的方式如下所示: 成员函数中出现的参数 对于成员函数中出现的参数,首先先在成员函数内部查找其声明,如在成员函数内没找到,则在类内寻找,类内所有权限的成员都可以被考虑,即使声明部分出现在函

随机推荐