我的C++奇迹之旅:内联函数和auto关键推导和指针空值

2024-04-07 05:04

本文主要是介绍我的C++奇迹之旅:内联函数和auto关键推导和指针空值,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

请添加图片描述

文章目录

  • 📝内联函数
  • 🌠 查看内联函数inline方式
    • 🌉内联函数特性
    • 🌉面试题
  • 🌠auto关键字(C++11)
    • 🌠 auto的使用细则
    • 🌉auto不能推导的场景
  • 🌠基于范围的for循环(C++11)
      • 🌠范围for的语法
      • 🌉 范围for的使用条件
  • 🌉 指针空值nullptr(C++11)
    • 🌠C++98中的指针空值
  • 🚩总结


📝内联函数

内联函数是一种编译器优化技术,它可以将函数的代码直接插入到函数调用的地方,而不是通过函数调用的方式。这样可以减少函数调用的开销,提高程序的执行效率。

举个例子,当你在一个项目中,想要频繁调用一个Add函数

int Add(int x, int y)
{return x + y;
}

当你调用一千次,一万次,函数栈帧相应的要建立这么多次,对于代码空间和时间考虑,消耗大,也浪费。
此时,你肯定在想到C语言中的来在代码进行预处理解决

#define ADD(a, b) ((a) + (b))

当然你也要注意括号问题,在使用宏定义时需要格外小心,因为宏定义是在编译时进行替换的,如果定义不当可能会导致一些意料之外的行为,避免出现以下有关括号写法问题:

#define ADD(a, b) a + b;
#define ADD(a, b) (a + b)
#define ADD(int a, int b) return a + b;
这个宏定义是错误的。宏定义中不能包含 return 语句,因为宏展开时会直接替换代码,而不是像函数那样有返回值。

代码测试

#define _CRT_SECURE_NO_WARNINGS 1
#define ADD(a, b) ((a) + (b))
#include <stdio.h>
int main()
{int ret = ADD(1, 2);printf("%d\n", ADD(1, 2));int x = 1, y = 2;printf("%d\n", ADD(x & y, x | y));return 0;
}

C++中,在函数声明前增加inline 关键字来告诉编译器这个函数为内联函数:

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

inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数调用建立栈帧的开销,内联函数提升程序运行的效率。当编译器编译运行到内联函数,将会把函数调用的代码,直接替换,不需要再去Call该函数的地址,然后再通过这个函数的地址去寻找函数的代码,这样可以避免函数调用时建立栈帧的开销,提高程序的运行效率。这在反汇编中汇编代码中操作中有所体现,让我们看看:

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
inline int Add(int begin, int end)
{return begin + end;
}int main()
{int ret = 0;ret = Add(6, 8);return 0;
}

例子中,代码的在前面原本的代码中加多了一个inline,因此这个函数是内联函数,编译器会将main()函数中的Add(6,8)先把6放在在寄存器eax中,然后让8与寄存器eax进行相加,这样的直接操作就实现了函数的相加,相比调用函数开销,提高了程序的运行效率。
这是反汇编对比图:
在这里插入图片描述
在这里插入图片描述

🌠 查看内联函数inline方式

查看内联函数的方式确实需要根据编译模式的不同而采取不同的方法:
在 Visual Studio 2019 中,查看内联函数的步骤如下:

  1. 在 Debug 模式下:

    • 找到项目 -> 属性
    • 在这里插入图片描述
    • -> C/C++ -> 常规-> 调试信息格式 -> 程序数组路库(/Zi)在这里插入图片描述 -
    • -> 选完上面再接着 -> 优化-> 内联函数扩展 -> 直适用于_inline(/Ob1)
    • 在这里插入图片描述
    • 点击确定,然后按下F10,右击鼠标找到反汇编,即可。
    • 在这里插入图片描述
    • 这样在 Debug 模式下也能看到内联函数被展开的汇编代码
  2. 在 Release 模式下:

    • 同样可以查看生成的汇编代码,如果没有看到对应的 call 指令,就说明该函数被内联展开了
      在这里插入图片描述

另外,Visual Studio 2019 还提供了一个更直观的方式来查看内联函数的情况:

  • 在代码编辑器中,将鼠标悬停在内联函数的调用处,Visual Studio 会弹出一个提示框,显示该函数是否被内联展开。
    在这里插入图片描述

🌉内联函数特性

  • inline是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段会用函数体替换函数调用,缺陷:可能会使目标文件变大,优势:少了调用开销,提高程序运行效率。

  • inline对于编译器而言只是一个建议,不同编译器关于inline实现机制可能不同,一般建议:将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、不是递归、且频繁调用的函数采用inline修饰,否则编译器会忽略inline特性。下图为《C++prime》第五版关于inline的建议:
    在这里插入图片描述

  • inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到。

  • F.h

#include <iostream>
using namespace std;
inline void f(int i);
  • F.cpp
#include "F.h"
void f(int i)
{cout << i << endl;
}
  • test.cpp
#include "F.h"
int main()
{f(10);return 0;
}

链接错误:main.obj : error LNK2019: 无法解析的外部符号 "cpp void __cdecl f(int)" (?f@@YAXH@Z),该符号在函数 _main 中被引用

🌉面试题

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

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

  1. 常量定义 换用const enum
  2. 短小函数定义 换用内联函数

🌠auto关键字(C++11)

在早期C/C++auto的含义是:使用auto修饰的变量,是具有自动存储器的局部变量,但遗憾的是一直没有人去使用它,大家可思考下为什么?

C++11中,标准委员会赋予了auto全新的含义即:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。
简言之:使用 auto 声明变量时,编译器会自动推导出变量的类型,无需显式指定。

int TestAuto()
{return 10;
}
int main()
{int a = 10;auto x = 42;       // x 的类型被推导为 intauto y = 3.14;     // y 的类型被推导为 doubleauto z = "hello";  // z 的类型被推导为 const char*auto d = TestAuto();cout << typeid(x).name() << endl;cout << typeid(y).name() << endl;cout << typeid(d).name() << endl;//auto e; 无法通过编译,使用auto定义变量时必须对其进行初始化return 0;
}

在这里插入图片描述

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

🌠 auto的使用细则

  1. auto与指针和引用结合起来使用
    auto声明指针类型时,用autoauto*没有任何区别,但用auto声明引用类型时则必须加&
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;
}

在这里插入图片描述

  1. 在同一行定义多个变量
    当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
int TestAuto()
{auto a = 1, b = 2;auto c = 3, d = 4.0; // 该行代码会编译失败,因为c和d的初始化表达式类型不同return a + b + c + d;
}int main()
{auto d = TestAuto();cout << typeid(d).name() << endl;return 0;
}

在这里插入图片描述

🌉auto不能推导的场景

  1. auto不能作为函数的参数
// 此处代码编译失败,auto不能作为形参类型,因为编译器无法对a的实际类型进行推导
int TestAuto(auto a)
{return a = 1; 
}int main()
{auto d = TestAuto(2);cout << typeid(d).name() << endl;return 0;
}

在这里插入图片描述

  1. auto不能直接用来声明数组
    auto 关键字确实不能直接用来声明数组。这是 C++ 语言的一个特性限制。

在 C++ 中,数组是一种特殊的数据结构,它的大小和元素类型在编译时就必须确定。而 auto 关键字是用来进行类型推导的,它无法推导出数组的大小和元素类型。

所以,下面的代码是无法编译通过的:

auto arr[] = {1, 2, 3, 4, 5}; // 错误: 无法使用 auto 推导数组类型

正确的做法是使用显式的类型声明:

int arr[] = {1, 2, 3, 4, 5};

或者使用 std::array 容器,它可以与 auto 关键字配合使用:

std::array<int, 5> arr = {1, 2, 3, 4, 5};
auto arr2 = arr; // arr2 的类型被推导为 std::array<int, 5>
  1. 为了避免与C++98中的auto发生混淆,C++11只保留了auto作为类型指示符的用法
  2. auto在实际中最常见的优势用法就是跟以后会讲到的C++11提供的新式for循环,还有lambda表达式等进行配合使用。

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

🌠范围for的语法

C++98中如果要遍历一个数组,可以按照以下方式进行:

int main()
{int array[] = { 1, 2, 3, 4, 5 };for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i){array[i] *= 2;}for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i){cout << array[i] << " ";}cout << endl;return 0;
}

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

int main()
{int array[] = { 1, 2, 3, 4, 5 };// C++11 范围for// 自动取数组array中,赋值给e// 自动++,自动判断结束for (auto& e : array){e *= 2;}for (auto e : array){cout << e << " ";}cout << endl;return 0;
}

在这里插入图片描述

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

🌉 范围for的使用条件

  1. for循环迭代的范围必须是确定的
    对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。
    注意:以下代码就有问题,因为for的范围不确定
void TestFor(int array[])
{for(auto& e : array)cout<< e <<endl;
}

因为 array 在这里是一个指针,而不是一个数组。使用范围 for 循环遍历指针是不合适的,因为循环的范围是未知的。

  • . 对于数组而言,数组的大小在编译时就已经确定了,所以我们可以直接使用数组的首地址和末地址作为迭代范围:
void TestFor(int array[], size_t size)
{for (size_t i = 0; i < size; ++i)std::cout << array[i] << std::endl;
}
  • 对于类而言,如果想使用范围 for 循环,则需要提供 begin()end() 成员函数,返回指向容器首尾元素的迭代器:
class MyContainer {
public:int* begin() { return data; }int* end() { return data + size; }// ...
private:int data[10];size_t size;
};void TestFor(MyContainer& c)
{for (auto& e : c)std::cout << e << std::endl;
}
  1. 迭代的对象要实现++和==的操作。(关于迭代器这个问题,以后会讲,现在提一下,没办法讲清楚,现在大家了解一下就可以了)

🌉 指针空值nullptr(C++11)

🌠C++98中的指针空值

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

void TestPtr()
{
int* p1 = NULL;
int* p2 = 0;
// ……
}

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

#ifndef NULL//这是一个预处理指令,检查是否已经定义了 NULL 宏。如果没有定义,则执行下面的代码块。
#ifdef __cplusplus//这个预处理指令检查是否在 C++ 编译环境下。如果是 C++ 编译环境,则执行下面的代码块。
#define NULL   0//在 C++ 编译环境下,将 NULL 宏定义为 0。这是因为在 C++ 中,0 可以隐式转换为任何指针类型,所以将 NULL 定义为 0 是合理的
#else//如果不是 C++ 编译环境,则执行这个代码块。
#define NULL   ((void *)0)//在 C 语言编译环境下,将 NULL 宏定义为(void *)0。这里使用 (void *) 进行强制类型转换,将整数 0 转换为 void * 类型,这样可以表示一个空指针
#endif//结束 #ifdef __cplusplus 的条件编译块
#endif//结束 #ifndef NULL 的条件编译块

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

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

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


🚩总结

请添加图片描述

这篇关于我的C++奇迹之旅:内联函数和auto关键推导和指针空值的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/881698

相关文章

【C++ Primer Plus习题】13.4

大家好,这里是国中之林! ❥前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站。有兴趣的可以点点进去看看← 问题: 解答: main.cpp #include <iostream>#include "port.h"int main() {Port p1;Port p2("Abc", "Bcc", 30);std::cout <<

C++包装器

包装器 在 C++ 中,“包装器”通常指的是一种设计模式或编程技巧,用于封装其他代码或对象,使其更易于使用、管理或扩展。包装器的概念在编程中非常普遍,可以用于函数、类、库等多个方面。下面是几个常见的 “包装器” 类型: 1. 函数包装器 函数包装器用于封装一个或多个函数,使其接口更统一或更便于调用。例如,std::function 是一个通用的函数包装器,它可以存储任意可调用对象(函数、函数

hdu1171(母函数或多重背包)

题意:把物品分成两份,使得价值最接近 可以用背包,或者是母函数来解,母函数(1 + x^v+x^2v+.....+x^num*v)(1 + x^v+x^2v+.....+x^num*v)(1 + x^v+x^2v+.....+x^num*v) 其中指数为价值,每一项的数目为(该物品数+1)个 代码如下: #include<iostream>#include<algorithm>

C++11第三弹:lambda表达式 | 新的类功能 | 模板的可变参数

🌈个人主页: 南桥几晴秋 🌈C++专栏: 南桥谈C++ 🌈C语言专栏: C语言学习系列 🌈Linux学习专栏: 南桥谈Linux 🌈数据结构学习专栏: 数据结构杂谈 🌈数据库学习专栏: 南桥谈MySQL 🌈Qt学习专栏: 南桥谈Qt 🌈菜鸡代码练习: 练习随想记录 🌈git学习: 南桥谈Git 🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈�

【C++】_list常用方法解析及模拟实现

相信自己的力量,只要对自己始终保持信心,尽自己最大努力去完成任何事,就算事情最终结果是失败了,努力了也不留遗憾。💓💓💓 目录   ✨说在前面 🍋知识点一:什么是list? •🌰1.list的定义 •🌰2.list的基本特性 •🌰3.常用接口介绍 🍋知识点二:list常用接口 •🌰1.默认成员函数 🔥构造函数(⭐) 🔥析构函数 •🌰2.list对象

06 C++Lambda表达式

lambda表达式的定义 没有显式模版形参的lambda表达式 [捕获] 前属性 (形参列表) 说明符 异常 后属性 尾随类型 约束 {函数体} 有显式模版形参的lambda表达式 [捕获] <模版形参> 模版约束 前属性 (形参列表) 说明符 异常 后属性 尾随类型 约束 {函数体} 含义 捕获:包含零个或者多个捕获符的逗号分隔列表 模板形参:用于泛型lambda提供个模板形参的名

uva 10014 Simple calculations(数学推导)

直接按照题意来推导最后的结果就行了。 开始的时候只做到了第一个推导,第二次没有继续下去。 代码: #include<stdio.h>int main(){int T, n, i;double a, aa, sum, temp, ans;scanf("%d", &T);while(T--){scanf("%d", &n);scanf("%lf", &first);scanf

6.1.数据结构-c/c++堆详解下篇(堆排序,TopK问题)

上篇:6.1.数据结构-c/c++模拟实现堆上篇(向下,上调整算法,建堆,增删数据)-CSDN博客 本章重点 1.使用堆来完成堆排序 2.使用堆解决TopK问题 目录 一.堆排序 1.1 思路 1.2 代码 1.3 简单测试 二.TopK问题 2.1 思路(求最小): 2.2 C语言代码(手写堆) 2.3 C++代码(使用优先级队列 priority_queue)

【C++高阶】C++类型转换全攻略:深入理解并高效应用

📝个人主页🌹:Eternity._ ⏩收录专栏⏪:C++ “ 登神长阶 ” 🤡往期回顾🤡:C++ 智能指针 🌹🌹期待您的关注 🌹🌹 ❀C++的类型转换 📒1. C语言中的类型转换📚2. C++强制类型转换⛰️static_cast🌞reinterpret_cast⭐const_cast🍁dynamic_cast 📜3. C++强制类型转换的原因📝

C++——stack、queue的实现及deque的介绍

目录 1.stack与queue的实现 1.1stack的实现  1.2 queue的实现 2.重温vector、list、stack、queue的介绍 2.1 STL标准库中stack和queue的底层结构  3.deque的简单介绍 3.1为什么选择deque作为stack和queue的底层默认容器  3.2 STL中对stack与queue的模拟实现 ①stack模拟实现