C++基础精讲篇第3讲:内联函数+auto关键字

这一讲是承接《 C++基础精讲篇第2讲》的内容补充之一。读者们可以先把上一讲的知识学习了以后,再学习这一讲的相关知识。这一讲主要为大家分享C++特有的内联函数、auto关键字、基于范围的for循环以及指针空值。希望大家在我的文章的帮助下,能有所收获,那我们就开始学习吧。

第3讲知识脉络

目录

1、内联函数

1.1 定义

1.2 用法介绍

1.3 特性

1.3.1 使用注意事项

1.3.2 举例说明

1.4 与宏相比内联的优势

2、auto关键字

2.1 概念

2.2 使用细则

2.2.1 auto与指针和引用结合起来使用

2.2.2 在同一行定义多个变量时的场景

2.2.3 auto不能推导的场景

 2.3 auto用法举例

3、结语



1、内联函数

1.1 定义

      以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数压栈的开销,其可以提高程序运行的效率。

1.2 用法介绍

       首先我们写一个常规的求和函数,如下图所示,在调试过程中,利用vs编译器的“反汇编”命令查看程序的底层原理,从汇编中可以看出,利用push命令将实参1和2传递给形参a和b,然后利用call命令调用ADD函数,最终得到求和结果。这是常规的函数调用的利用汇编查看的底层原理。

#include<stdio.h>
using namespace std;
 int ADD(int a, int b)
{
	return a + b;
}

int main()
{
	cout << ADD(1, 2) << endl;

	if (ADD(1, 2)) 
	{
		cout << ADD(1, 2) << endl;
	}

	return 0;
}
常规情况下的函数调用汇编语言

      在分析内联函数的工作原理时,首先需要对VS编译器进行设置,首先需要确保在Debug模式下,因为在release条件下,可能会被系统优化,使得查看不了内联函数的效果,然后分以下三步骤进行设置即可。

步骤1
步骤2
步骤3

       当我们完成以上步骤设置后,采用以下函数分析:同样利用上面分析的ADD求和函数,此时我们在ADD函数前面加上“inline”,我们才用同样的调试方法,利用vs编译器的“反汇编”命令查看程序的底层原理,从汇编中可以看出,利用inline将函数展开,而不是采用call命令进行调用。

#include<stdio.h>
using namespace std;
//内联:在函数名前用inline表示
//在符合其条件的情况下,在调用的地方展开

inline int ADD(int a, int b)
{
	return a + b;
}


int main()
{
	cout << ADD(1, 2) << endl;

	if (ADD(1, 2)) 
	{
		cout << ADD(1, 2) << endl;
	}

	return 0;
}
利用内联函数的反汇编底层原理

      通过上面的对比,我们可以看到,利用inline将函数变成内联函数,相比于正常的通过开辟函数栈帧进行函数调用的方法,利用内联函数则是直接将函数展开,可以极大的提高程序的运行效率。但inline不是无所不能的,其也是需要在一定条件下使用,下面我将重点分析。

1.3 特性

1.3.1 使用注意事项

1、inline是一种以空间换时间的做法,省去调用函数的开销。但正是因为如此,如果代码很长,或者有循环/递归的函数不适宜将其作为内联函数,因为在编译过程中如果将代码展开,则经过编译出来的可执行程序会变很大。

2、inline对于编译器而言只是一种建议,编译器会自动优化,如果定义为inline的函数体内有循环/递归等,编译器优化时会忽略掉内联。

3、inline不建议声明和定义分离,分离会导致链接错误,因为inline将函数展开以后,就不存在函数地址了,此时链接就不能在符号表中找到该函数,即链接失效。

1.3.2 举例说明

针对使用注意事项1和2,我将一起举例为读者说明原因:

举例1:含有多条语句时的情况(此时inline不展开)

     如下程序所示,在函数fun中,我多次调用C++中的打印函数打印变量“a+b”,可以我们发现当重复执行多条打印语句时,此时inline就别编译器忽略了,从汇编角度可以看到增加了内联常规的函数二者汇编指令是一致的,这也能更好的证实这第1点。

#include<stdio.h>
using namespace std;
 
inline void fun(int a, int b)
{

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

}


int main()
{
	fun(100, 200);
	return 0;
}
含有内联的情况
#include<stdio.h>
using namespace std;
 
 void fun(int a, int b)
{

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

}


int main()
{
	fun(100, 200);
	return 0;
}
常规情况

举例2:函数中含有循环语句时的情况

       在fun函数中,我创建了两个循环语句,两个循环共计循环24次,对比增加了内联和常规的函数,我们会发现,其汇编指令是一致的,说明在编译中,编译器自动忽略内联,这也证实了第2点。

#include<stdio.h>
using namespace std;

 void fun(int a, int b)
{
	int c = 0;
	for (c = 0; c < (a + b); c++)
	{
		cout << c ;
	}
	cout << endl;
	int d = 0;
	for (d = 0; d < (a + b); d++)
	{
		cout << d << endl;
	}

}

int main()
{
	fun(10, 2);
	return 0;
}
含有内联的情况的
#include<stdio.h>
using namespace std;

inline void fun(int a, int b)
{
	int c = 0;
	for (c = 0; c < (a + b); c++)
	{
		cout << c ;
	}
	cout << endl;
	int d = 0;
	for (d = 0; d < (a + b); d++)
	{
		cout << d << endl;
	}

}
常规情况

 举例3:函数的声明和定义分开时

        从下面的例子中可以看到:当函数声明和定义分开时,此时采用inline将函数展开,会发现出现找不到函数地址了,也就是说在链接的过程中,call找不到函数名,在符号表中没有找到该函数。为此,不能在函数声明和定义分开的情况下使用内联。

函数声明和定义分开时采用内联

       经过上面的分析,可以总结一点就是:内联只有在符合条件的情况下展开。 读者们在使用的过程中要特别注意。

1.4 与宏相比内联的优势

       在c++中设计内联函数的目的就是为了替代C语言中宏函数的优化,在C++中不在建议使用宏,具体原因我将在下面给出分析。首先我们先来看看宏使用的优缺点:

优点:

1、增强代码的复用性。比如在定义某一常变量时,#define x 10 ,诸如此类情况,通过需求改变x待取的值,就可以实现不同的作用,这也印证了宏在增强代码的复用性方面的作用;

2、提高性能;比如:#define Add(a,b)  ((a)+(b)),在此类程序代码少情况中,如果采用函数的形式,当我们在程序中需要多次调用该Add函数时,正常情况下,使用一次就会开辟一次函数栈帧,这样就会导致效率低下,而如果采用宏函数,则程序执行效率则会提高很多;

缺点:

1、不方便调试宏:因为在预编译阶段,就进行了替换,此时不方便调试;

2、由于第一条在预编译阶段进行替换,会导致代码可读性差,可维护性差,容易误用等

3、因为宏在编译过程中是进行替换,为此就不存在类型安全检查

       综合以上宏的优缺点分析,我们会发现:内联函数很好的继承了宏的优点以及避开了宏的缺点,这也就是为什么在C++中不在建议使用宏的原因(这不是个人建议,而是统一是这样。)

总结:C++中在这两方面替代宏:1、常量定义,换用const;2、函数定义,换用内联函数。

2、auto关键字

2.1 概念

       在早期的C/C++中auto的含义是:使用auto修饰的变量,是具有自动存储器的局部变量。但当时很少有人采用,在C++11中,标准委员会赋予了auto全新的含义即:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。

       注意:使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译时期会将auto替换为变量实际的类型。

简而言之,就是auto能够自动推导类型,但其不是诸如int、char等的类型声明符。

2.2 使用细则

2.2.1 auto与指针和引用结合起来使用

#include<stdio.h>
using namespace std;
int main()
{
	int x = 10;
	auto a = &x;
	auto* b = &x;
	auto& c = x;
	cout << typeid(a).name() << endl;
	cout << typeid(b).name() << endl;
	cout << typeid(c).name() << endl;
	*a = 20;
	*b = 30;
	c = 40;
	return 0;
}
用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&

2.2.2 在同一行定义多个变量时的场景

       当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。

#include<stdio.h>
using namespace std;
int main()
{
	int a = 0;
	auto b = 2, c = 3;
//	auto d = 1, e = 4.0; //该编译会出错,初始化表达式类型需要一致
	auto d = 1, e = 4;

	cout << typeid(a).name() << endl;
	cout << typeid(b).name() << endl;
	cout << typeid(c).name() << endl;
	cout << typeid(d).name() << endl;
	cout << typeid(e).name() << endl;
	return 0;

2.2.3 auto不能推导的场景

1、auto不能作为函数的参数

auto不能作为函数参数

2、auto不能直接用来声明数组

auto不能定义数组,编译器不允许

 2.3 auto用法举例

       在这里引入一个C++11中才有的新名词“基于范围的for循环”,auto可以和“范围for”一起使用,以下面例子为例说明:

#include<stdio.h>
using namespace std;

int main()
{
	int a[] = { 1,2,3,4,5,6 };
	//常规写法
	for (int i = 0; i < sizeof(a) / sizeof(a[0]); i++)
	{
		cout << a[i] << ' ';
	}
	cout << endl;

	//auto用法:结合范围for使用
	//自动取a的值给e,自动++,自动判断结束
	//自动迭代
	for (auto e : a)
	{
		cout << e << ' ';
	}
	cout << endl;
	return 0;
}

       从上面的例子可以看出,auto和“范围for”配合使用,在书写循环程序方面会带来很多便利,下面简要介绍“范围for”的用法。

       for循环后的括号由冒号":"分为两部分:前一部分是范围内用于迭代的变量,后部分则是表示被迭代的范围,即自动取变量a中的值给e,自动赋值,自动++,自动迭代。

       注意事项:for循环迭代的范围必须是确定的(对于数组而言,就是数组中的第一个元素和最后一个元素的范围);迭代的对象要实现++和==的操作。

3、结语

       今天这一讲是承接前面两篇文章的继承和补充,也是在学习C++我们需要掌握的基本概念及用法,在后面的章节中,我们将要开始C++中的类和对象环节,欢迎大家点赞、关注、支持!!!

C++基础精讲篇第1讲:输入输出流+命名空间+缺省参数

C++基础精讲篇第2讲:函数重载+引用


版权声明:本文为King_lm_Guard原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。