前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >C++打怪升级(三)- 内联函数 、auto、范围for循环

C++打怪升级(三)- 内联函数 、auto、范围for循环

作者头像
怠惰的未禾
发布2023-04-27 21:44:12
4960
发布2023-04-27 21:44:12
举报
文章被收录于专栏:Linux之越战越勇

前言

内联函数是什么,我们来一起看看吧!


引子

在C语言中,我们通常会把完成特定功能的代码封装为一个函数,这样的函数可能完成者复杂的功能从而具有较多的代码长度,同时也有着许许多多的只完成简单功能的函数,这些函数内部通常只有几行代码。 比如: 完成交换功能的函数

代码语言:javascript
复制
//交换两个整数
void Swap(int* x, int* y) {
	int tmp = *x;
	*x = *y;
	*y = tmp;
}
int main() {
    
	int a = 10;
	int b = 20;
	Swap(&a, &b);
	return 0;
}

完成两个整数相加的函数

代码语言:javascript
复制
//两个整数相加
int Add(int a, int b) {
	return a + b;
}

int main() {

	int a = 10;
	int b = 20;
	int ret = Add(a, b);
	return 0;
}

我们知道,调用函数时会在栈区开辟栈帧空间,返回后函数栈帧销毁,所以存在着一定的且不可忽略的系统开销。 对于复杂或代码较多的函数我们只能选择调用函数,在C语言中一般不规避上述开销; 但是对于功能简单的函数,代码可能只有几行,并且经常被其他函数调用,我们其实是有方式来规避掉调用函数时的栈帧开销的。在C语言中是有着宏的,我们可以利用宏来定义宏函数来解决这个问题。 因为功能简单的函数代码一般只有几行,转换为宏函数的代码也只有几行,所以转换比较容易。

代码语言:javascript
复制
//宏 - 交换两个整数
#define SWAP(x, y) int tmp = x;\
					x = y;\
					y = tmp
代码语言:javascript
复制
//宏 - 两个整数相加
#define ADD(x, y) ((x) + (y))

宏定义之后,出现宏定义的地方都会在预处理阶段被直接替换,相当于在出现宏定义的地方展开。 优点:

  1. 提高了程序执行的效率,不再有函数栈帧创建和销毁时的开销
  2. 增强了代码复用性,不需要再重新写了,可以直接调用

可见C语言使用宏已经能够初步解决小函数(代码少)的调用开销问题,但是宏定义是存在挺明显的缺点的:

  1. 首先就是宏定义不能够调试
  2. 替换操作也破坏了代码结构,特别是多次替换,导致代码可读性差、可维护性差、容易被误用。
  3. 其次宏没有类型检查,也就不安全,容易出错且不易发现。

C++从C而来,也对C做出了一些改进。那么C++是否选择了C语言的这种采用宏的方法呢? 显然是没有的,宏的缺点太过显眼了,C++中便引入了新的方式 -** 内联函数** 来解决小函数多次调用时存在的系统开销问题。


内联函数

概念

以关键字inline修饰的函数称为内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数调用建立栈帧的开销,从而内联函数能够提升程序运行的效率。 这里展开的意思就是直接用函数体替换函数被调用的位置。

代码语言:javascript
复制
//内联函数 - 交换两个整数
inline void Swap(int* x, int* y) {
	int tmp = *x;
	*x = *y;
	*y = tmp;
}
int main() {
    
	int a = 10;
	int b = 20;
	Swap(&a, &b);
	return 0;
}
代码语言:javascript
复制
//inline 两个整数相加
inline int Add(int a, int b) {
	return a + b;
}

int main() {

	int a = 10;
	int b = 20;
	int ret = Add(a, b);
	return 0;
}

特性

内联函数优缺点

内联函数inline是一种以空间(编译时进行)换时间(运行时进行)的做法,如果编译器将函数当做内联函数处理,在编译阶段会用函数体替换函数调用。 优点:减少了函数调用的系统开销,提高了程序的运行效率缺点:如果内联函数被调用太多次,会产生代码膨胀,导致编译生成的目标文件过大(安装包过大)。

内联函数一定会展开吗?

先说结论:不一定,取决于编译器。 inline对于编译器来说只是一个建议或请求,不同的编译器堆inline的实现机制可能不同,编译器是否接受我们发出的请求也不受我们控制,而是由编译器自己决定。 inline一般用于修饰函数规模较小(一般是几行代码)、非递归、调用频繁的函数。 对于函数规模较大(几十行或上百行代码)、递归函数,即使我们使用inline修饰,编译器也不会再调用这些函数的地方展开,而是像普通函数调用那样call

代码语言:javascript
复制
//11行代码输出
inline void function() {
	cout << "hello C++!\n" << endl;
	cout << "hello C++!\n" << endl;
	cout << "hello C++!\n" << endl;
	cout << "hello C++!\n" << endl;
	cout << "hello C++!\n" << endl;
	cout << "hello C++!\n" << endl;
	cout << "hello C++!\n" << endl;
	cout << "hello C++!\n" << endl;
	cout << "hello C++!\n" << endl;
	cout << "hello C++!\n" << endl;
	cout << "hello C++!\n" << endl;
}

int main() {

	function();
	return 0;
}

内联函数一般定义在哪?

先说结论:内联函数一般定义在需要调用内联函数的源文件内,或者直接定义在头文件内,在包含头文件即可来看这个错误:

为什么? 为什么内联函数不能像普通函数那样声明和定义分离呢?

这里将会涉及:

内联函数与普通函数比较;

而内联函数呢,在编译时,inline修饰函数并没有也不需要进入符号表(而是直接在编译时被编译器用函数体给替换了), 在编译时由于test.cpp中只有内联函数的声明,而不知道Add函数具体定义,所以编译器没有办法在main函数中调用Add函数处展开。 但是这并没有报错,如果这里报错应该是编译错误,但现在报的是链接错误,所以编译没问题。 在链接阶段test.o会到其他目标文件中寻找Add函数大的有效地址。 那么看链接阶段: 在链接阶段,test.o符号表中只有Add函数的无效地址(因为只是声明),而Add.o符号表中也没有Add函数的地址,导致了main函数调用了Add函数,却怎么都找不到Add函数的地址,这发生在链接阶段,所以是链接错误。

内联函数分离和不分离的比较;

对于内联函数前面已经知道:内联函数与其主调函数在同一源文件或内联函数在头文件中,内联函数都可以正常展开。因为在不需要再去找被调内联函数在哪了,可以直接展开内联函数了。

声明和定义分离,就会找不到内联函数的地址了。


代替宏的方式

C++中除了可以用内联函数代替宏定义之外,还可以使用const常变量、enum常量来代替宏常量。


auto关键字

概念

auto关键字C语言原本就有,含义是auto修饰的变量,是具有自动存储器的局部变量。 早期C++也沿用了C的auto,不过很鸡肋,没啥用。 C++11中,标准委员会赋予了auto全新的含义即:auto不再是一个存储类型指示符,而是作为一 个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得到。

怎样使用

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

  1. auto与指针和引用结合使用 用auto声明指针类型时,用autoauto*没有任何区别,但用auto声明引用类型时则必须加&
代码语言:javascript
复制
#include <iostream>
using namespace std;

int main() {
    
	int a = 10;
	auto b = a;
	auto c = &a;
	auto* d = &a;
	auto& e = a;
	//auto f;
	cout << "a: " << a << endl;
	cout << "b: " << b << endl;
	cout << "c: " << c << endl;
	cout << "d: " << d << endl;
	cout << "e: " << e << endl;
	return 0;
}

  1. 在同一行定义多个变量 当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译 器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量
代码语言:javascript
复制
#include <iostream>
using namespace std;

int main() {
	int a = 10;
	auto b = 1, c = a;
	//auto d = 10, f = 3.14;//error

	cout << "a: " << a << endl;
	cout << "b: " << b << endl;
	cout << "c: " << c << endl;
	return 0;
}

**typeid(变量名).name() ** 头文件#include <typeinfo> 返回储存变量类型的字符串的地址,

代码语言:javascript
复制
#include <iostream>
using namespace std;

int main() {
	int a = 10;
	auto b = a;
	auto c = &a;
	auto* d = &a;
	auto& e = a;
	//auto f;

	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;
}

auto不适用的情况:

  1. auto不能作为函数参数

  1. auto不能用来声明数组

  1. C++11中只保留了auto作为类型指示符的用法,以此来避免与C++98中的auto混淆

范围for循环

概念

在C语言和C++98中如果想要遍历一个数组,我们可以使用for循环

代码语言:javascript
复制
#include <iostream>
using namespace std;

int main() {

	int array[] = { 1,2,3,4,5,6,7,8,9,10 };
	for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i) {
		cout << array[i] << " ";
	}
	cout << endl;
	return 0;
}

对于一个有范围的集合而言,以前都是我们明确给出循环的范围,C++11中则引入了基于范围的for循环,不需要我们指定,而是范围for循环自动控制范围:

for循环后的括号由冒号:分为两部分:第一部分是范围内用于迭代的变量第二部分表示被迭代的范围

代码语言:javascript
复制
#include <iostream>
using namespace std;

int main() {

	int array[] = { 1,2,3,4,5,6,7,8,9,10 };
	for (auto e : array) {
		cout << e << " ";
	}
	cout << endl;
	return 0;
}
代码语言:javascript
复制
#include <iostream>
using namespace std;

int main() {

	int array[] = { 1,2,3,4,5,6,7,8,9,10 };
	for (auto& e : array) {
		e *= 2;
		cout << e << " ";
	}
	cout << endl;
	return 0;
}

与普通for循环类似,可以用continue来结束本次循环,也可以用break来跳出整个循环

代码语言:javascript
复制
int main() {

	int array[] = { 1,2,3,4,5,6,7,8,9,10 };
	for (auto& e : array) {
		if (e == 8)
			break;
		e *= 2;
		cout << e << " ";
	}
	cout << endl;
	return 0;
}
代码语言:javascript
复制
int main() {

	int array[] = { 1,2,3,4,5,6,7,8,9,10 };
	for (auto& e : array) {
		if (e == 8)
			continue;
		e *= 2;
		cout << e << " ";
	}
	cout << endl;
	return 0;
}

使用条件

for循环迭代的范围必须是确定的。

对于数组范围是第一个元素和最后一个元素的范围; 错误举例:

代码语言:javascript
复制
int main() {

	int array[] = { 1,2,3,4,5,6,7,8,9,10 };
	int* p1 = array;//数组首元素的地址
	for (auto e : p1)//error
		cout << e << " "
		cout << endl;

	int(*p2)[10] = &array;//整个数组的地址
	for (auto e : p2)//error
		cout << e << " "
		cout << endl;
	return 0;
}

对于类来说,begin和end是for循环的范围。

迭代的对象要实现++和==的操作


指针空值nullptr

我们在定义一个变量时可能并不知道该变量应该赋予的初值是什么,这时我们往往可以给其一个简单的初值。

代码语言:javascript
复制
int a = 0;
double b = 0.0;
char c = 0;
int* p = NULL;
int* p = 0;

在C++98中存在这样的一个有关NULL问题: NULL#define定义的宏常量,一般用于为没有有效指向的指针赋值,表示指针空值。 在C语言中它是(void*)0整型字面值0再强制类型转换为void*的指针 在C++98中,字面常量0既可以是一个整型数字,也可以是无类型的指针(void*)常量,但是编译器 默认情况下将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转(void*)0

这样就会引起一些问题。

代码语言:javascript
复制
void func(int)
{
	cout << "f(int)" << endl;
}
void func(int*)
{
	cout << "f(int*)" << endl;
}
int main()
{
	func(0);
	func(NULL);
	func((int*)NULL);
	return 0;
}

但是C++并不好修改这个问题,只能保留这个问题,因为有很多人和企业使用这C++。 于是C++11便引入了一个关键nullptr来解决这个问题:

在C++11中,sizeof(nullptr) sizeof((void*)0)所占的字节数相同。 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr

代码语言:javascript
复制
void func(int)
{
	cout << "f(int)" << endl;
}
void func(int*)
{
	cout << "f(int*)" << endl;
}
int main()
{
	func(0);
	func(nullptr);
	func((int*)NULL);
	return 0;
}

结语

本节主要介绍了内联函数的概念,并稍微了解了重获新生的auto关键字和新引入的nullptr关键字。 下次再见!


本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022-10-02,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 引子
  • 内联函数
    • 概念
      • 特性
        • 内联函数优缺点
        • 内联函数一定会展开吗?
        • 内联函数一般定义在哪?
      • 代替宏的方式
      • auto关键字
        • 概念
          • 怎样使用
          • 范围for循环
            • 概念
              • 使用条件
              • 指针空值nullptr
              • 结语
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档