首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >【C++入门篇】学习C++就看这篇--->内联函数、auto关键字、范围for

【C++入门篇】学习C++就看这篇--->内联函数、auto关键字、范围for

作者头像
HABuo
发布2025-05-23 09:55:58
发布2025-05-23 09:55:58
2440
举报

前言: 本篇博客我们继续入门篇的学习,入门篇呢,知识很杂碎,希望我们都理解的基础之上记住它,这些知识是为我们后续的学习打下好的基础,万事开头难,希望大家保持定力,坚持到最后,我们就都会成为优秀的C++程序员,大家加油!本篇博客将介绍下述知识点: 1. 内联函数 2. auto关键字(C++11) 3. 基于范围的for循环(C++11) 4. 指针空值---nullptr(C++11)。希望大家有所收获!

一、内联函数

1.1 概念

  • 以inline修饰的函数叫做内联函数
  • 编译时C++编译器会在调用内联函数的地方展开
  • 没有函数调 用建立栈帧的开销,内联函数提升程序运行的效率

啥意思,挺抽象的,我们举例子说明: 看下述正常的函数调用:

代码语言:javascript
复制
int Add(int left, int right)
{
	return left + right;
}
int main()
{
	int ret = 0;
	ret = Add(1, 2);
	return 0;
}

这是不是就是再正常不过的函数调用它的汇编代码显示如下:

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

代码语言:javascript
复制
inline int Add(int left, int right)
{
	return left + right;
}
int main()
{
	int ret = 0;
	ret = Add(1, 2);
	return 0;
}

展开的意思就是在main函数里执行到Add函数那里会将Add函数直接展开在这里即main函数的对应位置,正常的话我们是怎么样,是不是在栈区相应位置为Add开辟对应的栈帧啊,这里就不用了,因为你的代码就拷贝到调用的位置了,还开毛啊。证明如下:

上述是汇编代码,调用一个函数的时候,开辟栈帧需要call指令,可以看到上述加入inline修饰之后并没有call指令出现。

1.2 特性

  • 1️⃣inline是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会 用函数体替换函数调用。

解释:什么意思,怎么就牺牲空间了,你看你每次调用是不是都要展开,那可想而知,你调用100次展开100次那这个代码量吓人,而你之前的不一样啊,之前函数有它独立的栈帧你每次调用就可以了。所以它是以空间换时间的做法。

缺陷:可能会使目标文件变大 优势:少了调用开销,提高程序运 行效率。

  • 2️⃣ inline对于编译器而言只是一个建议,不同编译器关于inline实现机制可能不同。

一般建议将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、不是递归、且频繁调用的函数采用inline修饰对于代码长度过大的,编译器会忽略inline特性。 vs下可能函数有20行左右就不会施行了,大家可以试一下。如下所示:

  • 3️⃣inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址 了,链接就会找不到。
代码语言:javascript
复制
// F.h
inline void f(int i);
// F.cpp
#include "F.h"
void f(int i)
{
	cout << i << endl;
}
// main.cpp
#include "F.h"
int main()
{
	f(10);
	return 0;
}

解释:因为我们之前的函数调用都是通过地址去找到的,但是你现在没有函数地址,你把声明展开有什么用,我找不到函数的定义,所以在程序处理过程的链接阶段,即各个.c文件链接到一起时就会报下面的错误: // 链接错误:main.obj : error LNK2019: 无法解析的外部符号 "void __cdecl f(int)" (?f@@YAXH@Z),该符号在函数 _main 中被引用

1.3 应用场景(记住

🚩C语言当中我们知道宏这个好东西。 优点:

  • 1.增强代码的复用性。
  • 2.提高性能。

缺点:

  • 1.不方便调试宏。(因为预编译阶段进行了替换)
  • 2.导致代码可读性差,可维护性差,容易误用。
  • 3.没有类型安全的检查 。

所以C++有哪些技术替代宏?

  • 1. 内联函数就可以替代宏函数(短小函数定义)
  • 2. 常量的定义可以用const enum替代

二、auto关键字(C++11)

背景知识:

相信大家在C语言学习初始阶段被一些float、double、long、long long什么乱七八糟的关键字搞的头大,我是这样的🤭,不清楚大家是什么样 。C++到了后期会有更加复杂的类型,如下所示的代码。

代码语言:javascript
复制
std::map<std::string, std::string>::iterator it = m.begin();

std::map<std::string, std::string>::iterator 是一个类型,但是该类型太长了,特别容易写错。相信大家会说这能难倒我?直接typedef不就行了。 确实,使用typedef给类型取别名确实可以简化代码,但是typedef有会遇到新的难题:

代码语言:javascript
复制
typedef char* pstring;
int main()
{
	const pstring p1; // 编译成功还是失败?
	const pstring* p2; // 编译成功还是失败?
	return 0;
}

上述是第一个错误,为什么?我们好像还没遇到这种情况,下面进行解释:就是说const是修饰pstring而pstring是一个typedef类型,即char* const,即是常量指针,常量指针必须在声明时初始化(因为其指向不可更改),而第二个是char* const*是一个指向常量指针的普通指针,则不必须进行初始化,但一般好的代码风格就是进行初始化。 因此

  • const修饰typedef类型时,作用于整个类型(指针本身为常量)。
  • 常量指针(char* const)必须初始化,而指向常量指针的普通指针(char* const*)无需初始化。

大家发现你typedef可以,但是是不是要提前先了解其中代表的含义啊,有时候要做到这点并非那么容易,因为在一个庞大的项目当中,大家共同开发,各自对各自的代码比较了解,换个人可能就不太行了。 而C++有个东西就可以解决这个问题,那就是auto。

2.1 auto简介

auto声明的变量必须由编译器在编译时期推导而得,不废话,直接看代码:

代码语言:javascript
复制
int a = 10;
 auto b = a;
 auto c = 'a';

直接管它什么double、char、float等直接auto自动给你匹配对应的类型。上面那句代码也是这样

代码语言:javascript
复制
std::map<std::string, std::string>::iterator it = m.begin();
auto it = m.begin();

是不是特别方便,所以auto就是这个功能,提供了很大的方便。

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

2.2 auto的使用细则

1️⃣auto与指针和引用的结合

🚩 用auto声明指针类型时,用auto和auto*没有任何区别

代码语言:javascript
复制
int a = 10;
auto b = &a;
auto* c = &a;

上述auto会自动判别,对于auto b = &a;,auto就是int*,对于auto* c = &a;就是int

🚩 但用auto声明引用类型时则必须加&

代码语言:javascript
复制
int a = 10;
auto& b = a;
auto c = a;

上述就不一样了,因为auto c = a;你是赋值还是引用,所以对于引用来说就必须带上&

2️⃣在同一行定义多个变量

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

代码语言:javascript
复制
void TestAuto()
{
	auto a = 1, b = 2;
	auto c = 3, d = 4.0;  // 该行代码会编译失败,因为c和d的初始化表达式类型不同
}

2.3 auto不能推导的场景

1️⃣ auto不能作为函数的参数

代码语言:javascript
复制
// 此处代码编译失败,auto不能作为形参类型,因为编译器无法对a的实际类型进行推导
void TestAuto(auto a)
 {}

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

代码语言:javascript
复制
void TestAuto()
{
	int a[] = { 1,2,3 };
	auto b[] = { 4,5,6 };
}

三、基于范围的for循环(C++11)

背景知识:C语言当中我们遍历个数组是怎么做的,是不是就是利用一个循环对每一个元素进行访问如下代码所示,我相信初学时,大家都被什么范围,这带等号,那不带等号搞得很烦😅,当然现在肯定不成问题。

代码语言:javascript
复制
void TestFor()
{
	int array[] = { 1, 2, 3, 4, 5 };
	for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i)
		array[i] *= 2;
	for (int* p = array; p < array + sizeof(array) / sizeof(array[0]); ++p)
		cout << *p << endl;
}

3.1 范围for的语法

所以,对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因 此C++11中引入了基于范围的for循环。for循环后的括号由冒号(:)分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。如下所示:

代码语言:javascript
复制
int TestFor()
{
	int array[] = { 1, 2, 3, 4, 5 };
	for (auto e : array)
		cout << e << " ";
	return 0;
}

注意:上面仅是遍历,如果是想要修改其中的值是不行的,因为e是另开辟的一个空间与数组没有任何关系,它无法进行修改,证明如下:

所以上面那个范围for可以这样理解,将array数组中的每个元素依次赋值给e,并且每次循环后会自动++,由于数组元素可以是整型、浮点型,甚至是一个class类,所以使用auto自动识别类型。因此前面我们学的引用在这里就可以用上了, 就可以进行修改其中的数组里的值了。

代码语言:javascript
复制
	for (auto& e : array)
		e *= 2;

但我有个疑惑,就是前面我们在学习引用的时候就了解到,引用只能绑定到一个已经存在的对象,这里为什么一个e却能绑定到数组当中的每一个元素,数组当中的每一个元素不都是不同的空间吗?😶‍🌫️不清楚大家有这样的疑惑没,如果大家也有这样的疑惑,我们真是同道中人,经过我的查阅,下面为大家解惑:

在C++的范围for循环中,auto& e 在每次迭代时都会创建一个新的引用变量,并将其绑定到当前处理的元素上。每个迭代中的e都是独立的,仅在当前循环迭代的作用域内有效,实际当中范围for等效的代码如下:

代码语言:javascript
复制
int array[] = {1, 2, 3};
for (auto& e : array)
    e *= 2;


int array[] = {1, 2, 3};
auto* begin = std::begin(array); // 获取数组起始指针
auto* end = std::end(array);     // 获取数组结束指针
for (; begin != end; ++begin) {
    int& e = *begin; // 每次迭代创建新引用
    e *= 2;          // 通过引用修改原元素
}

但是又不对啊,你上面没有引用的时候,我看到每个迭代e的地址都是一样的啊,是不是没有引用的场景和这里不一样呢?你的疑惑很好,我也有这样的疑惑😢,之后我查找一些资料得知,上述没有引用的场景中,每个迭代的e仍然是独立的,新创建的,和加入引用的场景是一样的,但是为什么地址是一样的呢?这里涉及到涉及 栈内存分配编译器优化

1️⃣栈内存重用

  • 每个迭代中的 e 是局部变量,分配在栈上。
  • 当一次迭代结束时,e 的内存被释放,但栈指针(stack pointer)会回退到之前的位置。
  • 下一次迭代时,新变量 e 会在同一块栈内存地址上重新分配,因为前一个 e 的内存已经被释放,编译器会复用这块内存。

2️⃣作用域隔离

  • 每次迭代的 e 属于不同的作用域,虽然地址可能相同,但它们的生命周期完全不重叠。
  • 编译器知道这些变量不会同时存在,因此可以安全地复用内存地址。

也就是说,如果每次迭代之前产生一个新的局部变量,将这块e复用的空间占用了,就有可能产生不同的地址,但是也不绝对,这与编译器的优化策略和栈帧管理细节有关,我们不再细究。

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

3.2 范围for的使用条件

1️⃣for循环迭代的范围必须是确定的

对于数组而言,就是数组中第一个元素和最后一个元素的范围 对于类而言,应该提供 begin和end的方法,begin和end就是for循环迭代的范围。

注意:以下代码就有问题,因为for的范围不确定

代码语言:javascript
复制
void TestFor(int array[])
{
	for (auto& e : array)
		cout << e << endl;
}

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

四、指针空值nullptr(C++11)

4.1 C++98中的指针空值

在良好的C/C++编程习惯中,声明一个变量时最好给该变量一个合适的初始值,否则可能会出现 不可预料的错误,比如未初始化的指针。如果一个指针没有合法的指向,我们基本都是按照如下 方式对其进行初始化:

代码语言:javascript
复制
int* p1 = NULL;
int* p2 = 0;

NULL实际是一个宏,在传统的C头文件(stddef.h)中,可以看到如下代码:

代码语言:javascript
复制
#ifndef NULL
 #ifdef __cplusplus
 #define NULL    0
 #else
 #define NULL    ((void *)0)
 #endif
 #endif

可以看到,NULL可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量。不论采取何 种定义,在使用空值的指针时,都不可避免的会遇到一些麻烦,比如:

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

程序本意是想通过f(NULL)调用指针版本的f(int*)函数,但是由于NULL被定义成0,因此与程序的 初衷相悖。 结果如下所示:

在C++98中,字面常量0既可以是一个整形数字,也可以是无类型的指针(void*)常量,但是编译器 默认情况下将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转(void *)0。

4.2 指针空值nullptr

指针空值nullptr就是解决上述问题的

  • 1️⃣在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入 的。
  • 2️⃣在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同
  • 3️⃣为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr

五、总结

本篇博客我们总结学习了下述知识点,内联函数使用 inline 修饰,编译时会在调用处展开,可提升效率,但编译器可能不采纳此建议,且声明定义不宜分离。auto 关键字(C++11)可自动推导变量类型,结合指针引用需注意用法,但不能作函数参数及直接声明数组。基于范围的 for 循环(C++11)语法简洁,用于遍历数组等,要求迭代范围确定且对象实现相应操作。nullptr(C++11)作为指针空值,较 NULL 更安全清晰。这些特性使代码更简洁高效,增强可读性与维护性,推动 C++ 发展。希望大家有所收获!

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

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

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、内联函数
    • 1.1 概念
    • 1.2 特性
    • 1.3 应用场景(记住)
  • 二、auto关键字(C++11)
    • 2.1 auto简介
    • 2.2 auto的使用细则
    • 2.3 auto不能推导的场景
  • 三、基于范围的for循环(C++11)
    • 3.1 范围for的语法
    • 3.2 范围for的使用条件
  • 四、指针空值nullptr(C++11)
    • 4.1 C++98中的指针空值
    • 4.2 指针空值nullptr
  • 五、总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档