• <C++入门基础>【下】


    一、内联函数

    什么是内联函数

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

    为什么存在内联函数

    我们知道,普通的函数需要建立栈帧空间

    因此,可以利用宏来定义一些小的函数(行数较少的)

    这样,函数调用的时候其实就是语句的直接替换,而不存在函数栈帧的创建和销毁

    比如,一个实现两个数相加的函数

    //普通函数
    int Add(int x,int y)
    {
        return x+y;
    }
    //宏函数形式
    #define ADD(a,b) ((a)+(b))
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    对比于普通函数来说,宏的优点就很明显了

    • 使用宏函数,可以不用建立栈帧,减小系统开销,提高运行效率

    但是显然,宏也有缺点

    1. 相比于函数,宏定义的函数可读性差,函数体复杂
    2. 宏函数参数没有类型检查,容易出现bug(如不同类型的相加)
    3. 宏在预处理阶段就直接整体替换展开了,不方便调试

    所以,为什么存在内联函数?

    内联函数的存在就是为了解决宏的缺点,同时保留宏的优点

    内联函数的查看

    既然你说,内联函数不会调用函数,直接展开

    有什么证据呢?

    如图所示,这是没有加inline修饰的函数

    可以看到反汇编中存在call Add这条命令,也就是会调用函数

    image-20220802091551750

    但是如果加了inline修饰呢?

    注意

    • 如果在release模式下,查看汇编代码中是否存在call Add即可
    • 但是如果在debug模式,需要进行设置,因为inline属于一种优化,debug模式下为了方便调试默认不会进行优化

    以VS2019为例,关闭优化:

    image-20220802095551458

    image-20220802095718084

    可以看到,关闭优化之后,执行到Add函数的时候,并没有执行优化

    说明inline是存在的

    内联的注意事项

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

      缺陷:可能会使目标文件变大

      优势:少了调用开销,提高程序运行效率

    2. inline对于编译器只是一个建议,不同编译器关于inline实现机制可能不同

      一般函数规模较小(展开汇编 小于10行),没有递归。并且会频繁调用的函数,编译器会采纳inline的建议。否则编译器会忽略inline的建议。因为如果展开汇编代码很多的话,会导致程序的大小变大!

      比如:

      如果一个函数有100行,调用10000次
      比较程序的语句数量
      不展开:10000个call + 100 个指令
      展开:100*10000 条指令 
      可执行程序会明显变大
      
      • 1
      • 2
      • 3
      • 4
      • 5

      而可执行程序变大的结果就是:

      比如更新《王者荣耀》这个游戏,500MB的更新内容硬是要更新2个G,难受不难受😫

      如《C++primer》中所述

      image-20220802101347427

    3. inline的声明和定义不要分离,如果在.h中声明inline func(),然后在test.cpp中进行定义,main.cpp中进行调用。那么当调用的时候,因为有头文件所以编译可以通过.但是链接的时候,就去test.cpp生成的目标文件的符号表中去找函数的地址(call动作),但是由于func()inline修饰的,所以编译器并不会把函数的地址放入符号表,所以就会出现链接错误

    如果函数用inline属性修饰,那么编译器统一不会把该函数的信息放到符号表

    所以内联函数一般是在.h中直接定义的,或者在需要调用的本.cpp文件中定义

    二、auto关键字

    auto用来干啥

    在C语言中,auto是用来修饰局部变量的,意味着该变量在该代码块内要有效,出代码块自动销毁

    但是在C++中,有了新的用法:自动推导变量类型

    int a = 10;
    auto b = a; //自动推导b的类型为a的类型(整形)
    auto c = 'c';//自动推导c的类型为字符型
    auto sum = Add(a,b);//自动推导sum的类型为函数的返回值类型
    
    /* 分别打印变量类型的名字 --- typeid()*/
    cout<<typeid(b).name<<endl;
    cout<<typeid(c).name<<endl;
    cout<<typeid(sum).name<<endl;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    当然auto的使用场景不限于此

    auto的使用场景

    基于范围的for循环

    通常在写for循环的时候,需要我们自己标注好循环的范围

    但是有时会犯错误,C++因此引入了范围for循环

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

    int arr[]={1,2,3,,4,5,6,7,8};
    for(auto e : arr)
    {
        cout<<e<<" ";
    }
    //自动依次取 arr的元素赋值给e
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    这样,不管数组元素的类型是什么,都会自动打印每一个元素

    注意:范围for循环什么时候不能用

    for循环的迭代返回必须是确定的,对于数组而言,就是第一个元素到最后一个元素

    比如:

    void test(int arr[])
    {
        for(int e:arr)
        {
            cout<<e<<" ";
        }
    }
    // 数组形参其实就是首元素地址,而非整个数组
    // 所以此时迭代找不到范围,出错!
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    自动推导变量类型

    如果变量的类型特别长,那么写起来就很麻烦
    这时候就体现出来auto的价值了

    举个例子:

    std::map::iterator it = dict.begin()

    就可以替换成:auto it = dict.begin()

    auto的使用细节

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

    auto与指针:autoauto*一样

    auto与引用:auto&

     int x = 10;
    auto a = &x;//自动推导a是int*
    auto* b = &a;//auto* 表明b是一个指针,这时候右边必须传指针
    auto& c = x;// 表明c是一个引用(别名),自动推导c的类型为int
    
    • 1
    • 2
    • 3
    • 4

    这样就可以利用范围for来修改数组元素了

    //利用引用,e就是每一次该元素的别名,利用别名进行修改元素
    for(auto& e : arr)
    {
        e++;//每个元素++
    }
    
    *********************************************
    
    //利用指针可以吗?--不可以!
    for(auto* e:arr)
    {
        *e? 
    }
    /* 注意:范围for是不可以用指针的
    因为范围for是把数组的每一个元素传递
    只能用传值接受和传引用接收,指针不识别*/
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    1. 在同一行定义多个变量

    最好不用auto一行定义多个,如果多个变量的类型存在不同,是无法识别的!

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

    auto a = 10, b = 30; // 这样可以识别
    auto c = 10, d = 30.0;// 一个是int,一个是double。无法识别
    
    • 1
    • 2

    注意

    使用auto定义变量时必须对其进行初始化,因为在编译阶段,编译器就会根据后面初始化给的值来推导出auto的类型。并在编译期间就会把auto替换为变量的实际类型

    auto不能使用的场景

    1. auto不能作为函数参数
    //此时代码编译会出错,auto不能作为形参类型
    //因为编译器无法推导出形参的实际类型
    void test(auto a)
    {
        /***/
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    准确点来说,函数调用需要开辟栈帧。而编译器给函数开辟栈帧之前就已经根据形参、函数体里的开辟的空间的大小算好要开辟的栈帧大小了。而auto的形参对于编译器来说,不知道要开辟多大的空间。所以会报错

    1. auto不用来声明数组
    void test()
    {
        int a[]={1,2,3};
        auto b[]={4,5,6};
        //编译器无法自动推导数组的类型
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    三、空指针nullptr

    C语言中,空指针是NULL,是一个宏

    在C++中NULL似乎也可以用,但是C++中的NULL其实是有问题的。C++大佬在设计的时候可能没有考虑全面

    在C++98中,字面常量0既可以是一个整形数字,也可以是无类型的指针(void*)常量

    但是编译器默认情况下 将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转(void *)0。

    看一下在C++中NULL的定义

    #ifndef NULL
    #ifdef __cplusplus
    #define NULL 0
    #else
    #define NULL ((void *)0)
    #endif
    #endif
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    可以看到,NULL在C++中其实就是 0

    在C语言中才是(void*)0,即空指针

    所以C++如果用NULL做空指针,就会出现这种情况

    //f函数构成函数重载
    void f(int)
    {
     	cout<<"f(int)"<<endl;
    }
    void f(int*)
    {
     	cout<<"f(int*)"<<endl;
    }
    
    int main()
    {
        int* p = NULL;
        f(NULL);//调用 f(int)
        f(0);// 调用f(int)
        f(p);// 调用f(int*)
        
        /* 显然,f(NULL)我们本想调用 f(int*)
        但是却调用成了 f(int) */
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    所以为了补C++的坑,C++11中引入了nullptr作为空指针

    注意

    1. 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的
    2. C++11中,sizeof(nullptr)sizeof((void*)0)所占的字节数相同
  • 相关阅读:
    基于 Hive 的 Flutter 文档类型存储
    思维模型 奶嘴乐理论
    复现urlcode编码绕过xss限制两个demo
    DataWhale AI夏令营 大模型微调Task1笔记
    机械产品设计全过程的差异有哪些?
    【快应用】应用打开显示白屏
    LoadRunner下载与安装
    CSDN: ABTest流量分层分桶机制
    Go 语句与表达式深度解析
    前端代码规范
  • 原文地址:https://blog.csdn.net/K_04_10/article/details/126157133