C++入门基础(下篇)——引用、inline、nullptr

目录

一. C++入门基础

11. 引用

11.2 引用的特性

11.3引用的使用

11.4 const引用

11.5 指针和引用的关系

12. inline

13. nullptr


一. C++入门基础

11. 引用

11.2 引用的特性

  1. 引用在定义是必须初始化,即不能不初始化就使用。(类比指针,若指针不初始化,则其值为随机指向)
  2. 一个变量可以有多个引用。
  3. 引用一旦引用一个实体,就再也不能引用其他实体。
#include<iostream>
using namespace std;

int main()
{
    int a = 10;

    // 编译报错:“ra”: 必须初始化引⽤
    //int& ra;

    int& b = a;

    int c = 20;
    // 这⾥并⾮让b引⽤c,因为C++引⽤不能改变指向,
    // 这⾥是⼀个赋值
    b = c;

    cout << &a << endl;
    cout << &b << endl;
    cout << &c << endl;

    return 0;
}

 思考:那么,我们思考一下这个问题,即可以在C++中用引用代替指针吗?

答案:是不可以的,引用不能代替指针,而且引用不能改变指向,前面数据结构章节所讲的链表、二叉树、堆等一系列的数据结构,这些结构都是包括有值(value)和指针(指向上一个结点或下一个结点),这些结构就算改变了(这种改变即是增删改这几种方式),它们也可以通过改变指针的指向,去指向改变后应该指向的结点。然而C++中的引用不能做到这一点,不能改变指向。但是在其他语言中,比如Java语言中,没有指针这样的概念,可以使用引用来改变指向。所以在不同的语言中,引用和指针的使用要慎重,毕竟作用不同,效果也就大相径庭了。

现在补充一下之前数据结构中漏写的一个知识点:

越界:第一,越界在编译器中是不一定报错的;第二,越界在编译器中读是不报错的;第三,越界写入是不一定报错的,其中系统对越界的检查一般是抽查的形式(设置抽查位置:写入后在程序结束时,看是否被修改,修改了抽查位置的话就一定是写入时越界了)检查的。

11.3引用的使用

  1. 引⽤在实践中主要是于引用传参引用做返回值减少拷贝提高效率改变引用对象时同时改变被引用对象
  2. 引⽤传参跟指针传参功能是类似的,引⽤传参相对更⽅便⼀些。
  3. 指针需要开辟空间,而引用不需要开辟空间。
  4. 指针是间接操作对象,引用时对象的别名,对别名的操作就是对真实对象的直接操作空指针没有任何指向,删除无害,引用是别名,删除引用就删除真实对象。
  5. 引⽤返回值的场景相对⽐较复杂,此时为简单场景,如图:

传值:左边STTop是一般传值返回的场景,通过函数调用,传参st1实参进入&rs这个形参,assert断言后通过返回函数传值返回到一个拷贝创建的临时对象中,这个临时对象具有常性,然后在这个临时对象中进行++,所以代码运行时就会提示报错信息为:           

引用:右边&STTop是通过引用返回的应用场景,引用在这里经调用后就直接在开辟的空间里,就
引用的对象直接进行++,不需要拷贝临时对象,此时运行成功。

传址:传址操作返回一个指针,调用函数完还需解引用再去++,运行是没有报错的,虽然有点麻烦,但是传址方法与引用方法大差不差,引用与指针的差异还需得在更复杂的情况中才能体现出来。
        6.引⽤和指针在实践中相辅相成,功能有重叠性,但是各有特点,互相不可替代。C++的引⽤跟其他语⾔的引⽤(如Java)是有很⼤的区别的,除了⽤法,最⼤的点,C++引⽤定义后不能改变指向, Java的引⽤可以改变指向。
        7.我们可以看出,“引用”和指针的主要区别是:指针通过某个指针变量指向一个对象后,对它所指向的变量 间接操作。 程序中使用指针,程序的可读性差;而 引用本身就是目标变量的别名,对引用的操作就是对目标变量的操作。
        8.⼀些主要⽤C代码实现版本数据结构教材中,使⽤C++引⽤替代指针传参,⽬的是简化程序,避开复杂的指针。
typedef struct ListNode
{

    int val;
    struct ListNode* next;

}LTNode, *PNode;

// 指针变量也可以取别名,这⾥LTNode*& phead就是给指针变量取别名
// 这样就不需要⽤⼆级指针了,相对⽽⾔简化了程序
//void ListPushBack(LTNode** phead, int x)
//void ListPushBack(LTNode*& phead, int x)
void ListPushBack(PNode& phead, int x)

11.4 const引用

  1. 可以引⽤⼀个const对象,但是必须⽤const引⽤。const引用也可以引用普通对象,因为对象的访问权限在引用过程中可以缩小,但是不能放大。例子如下:
    int main()
    {
        const int a = 10;
        // 编译报错:error C2440: “初始化”: ⽆法从“const int”转换为“int &”
        // 这⾥的引⽤是对a访问权限的放⼤
        //int& ra = a;
    
        // 这样才可以
        const int& ra = a;
        // 编译报错:error C3892: “ra”: 不能给常量赋值
        //ra++;
    
        // 这⾥的引⽤是对b访问权限的缩⼩
        int b = 20;
        const int& rb = b;
        // 编译报错:error C3892: “rb”: 不能给常量赋值
        //rb++;
    
        //这里不是权限的放大或缩小,而是拷贝赋值
        const int x =0;
        int y = x;
    
        //权限的放大同样在指针中也有体现,例如:
        const int a = 10;
        const int* p1 = &a;
        int* p2 = p1;
    
        //权限不能放大,但能缩小
        int b = 20;
        int* p3 = &b;
        const int* p4 = p3;
    
        //这里不存在权限的放大,因为const修饰的是p5本身,而不是指向的内容
        int* const p5 = &b;
        int* p6 = p5;
    
        return 0;
    }
  2. 需要注意的是类似,如下代码:
    int main()
    {
        int& rb = a*3; 
        double d = 12.34; 
        int& rd = d;
    }
    这样⼀些场景下a*3的和结果保存在⼀个临时对象中,在类型转换中会产⽣临时对象存储中间值,也就是时,rb和rd引⽤的都是临时对象,⽽C++规定临时对象具有常性,所以这⾥就触发了权限放⼤,必须要⽤常引⽤才可以。
    #include<iostream>
    using namespace std;
    
    void f1(const int& rx)
    {
        //引用传参,若想传参可以改变的话,就可不加const
        //加上了const,就可以使权限缩小
    }
    
    int main()
    {
        int a = 10;
    
        const int& ra = 30;
    
        // 编译报错: “初始化”: ⽆法从“int”转换为“int &”
        // int& rb = a * 3;
        const int& rb = a*3;
    
        double d = 12.34;
        // 编译报错:“初始化”: ⽆法从“double”转换为“int &”
        // int& rd = d;
        // 若没const,则会先将d存到一个int类型的临时变量里面,后再存到rd中
        const int& rd = d;
        
        f1(a);
        f1(a * 3);
        f1(d);
        
        return 0;
    }
  3. 所谓临时对象就是编译器需要⼀个空间暂存表达式的求值结果时临时创建的⼀个未命名的对象, C++中把这个未命名对象叫做临时对象
  4. 综上所诉,我们来做一个总结:const引用的价值有:第一,可以引用cosnt对象;第二,可以引用普通对象;第三,可以引用临时对象。const引用应用比较宽泛。

11.5 指针和引用的关系

  1. 语法概念上引⽤是⼀个变量的取别名不开空间,指针是存储⼀个变量地址,要开空间。
    int main()
    {
        int x = 0;
        int& rx = x;
        rx += 1;
        
        int* ptr = &x;
        *ptr += 1;
    
        return 0;
    }

    扩展知识:这个代码,从引用和指针两个角度看开辟空间的问题,我们从底层汇编语言来分析这个问题,如下,汇编语言中的引用和指针两个角度的汇编代码几乎一致,其中的引用到最后也变为指针来表示,所以,我们从汇编语言的角度来看,引用也是用指针来得已实现的

  2. 引⽤在定义时必须初始化,指针建议初始化,但是语法上不是必须的。
  3. 引⽤在初始化时引⽤⼀个对象后,就不能再引⽤其他对象;⽽指针可以在不断地改变指向对象
  4. 引⽤可以直接访问指向对象,指针需要解引⽤才是访问指向对象
  5. sizeof中含义不同,引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节,64位下是8byte)。代码如下:
    int main()
    {
        char x = 'a';
        char& rx = x;
        cout << sizeof(rx) << end1;
        char* ptr = &x;
        cout << sizeof(ptr) << end1;
    
        return 0;
    }
  6. 指针很容易出现空指针和野指针的问题,引用很少出现,引用使用起来相对更安全⼀些

12. inline

  1. ⽤inline修饰的函数叫做内联函数,编译时C++编译器会在调⽤的地⽅展开内联函数,这样调⽤内联函数就不需要建立栈帧了,就可以提高效率
  2.  inline对于编译器⽽⾔只是⼀个建议,也就是说,你加了inline编译器也可以选择在调⽤的地⽅不展开,不同编译器关于inline什么情况展开各不相同,因为C++标准没有规定这个。inline适用于频繁调用的短小函数,对于递归函数,代码相对多⼀些的函数,这样会导致可执行程序变大,从而影响进程,加上inline也会被编译器忽略,最终决策权在编译器手上。(怕不靠谱的程序员,是编译器的防御系统。)
  3. 尽可能把代码短小,频繁调用的函数设置为内联函数。内联函数只是一种建议,如果函数内部包括循环,递归,或者  代码量大且复杂,这些函数即使设置了内联函数,系统也不会当做内联函数来处理。
  4. C语⾔实现宏函数也会在预处理时替换展开,但是宏函数实现很复杂很容易出错的,且不⽅便调试,C++设计了inline目的就是替代C的宏函数。我们都知道,宏函数的本质是替换,调用时不用建立栈帧,有提效作用,同时又有很多坑,分号是一条语句的结束标志。我们思考一下以下问题:
    // 实现⼀个ADD宏函数的常⻅问题
    //#define ADD(int a, int b) return a + b;
    //#define ADD(a, b) a + b;
    //#define ADD(a, b) (a + b)
    
    // 正确的宏实现
    #define ADD(a, b) ((a) + (b))
    
    int main()
    {    
        // 为什么不能加分号?
    
        int ret = ADD(1, 2);
        //经过宏函数替换后,变成
        //int ret = ((1) + (2));
    
        cout << ret << end1;    
        
        //若后面加了分号的话,就会变成
        //int ret = ((1) + (2));;
        //这样比较一般的巧合没什么问题,但是如果时下面这一种情况呢?会变成什么样子呢?
    
        cout << ADD(1, 2) << end1;
        //cout << ((1) + (2)); << end1;
    
        if(ADD(1, 2))
        {
    
        }    
        //这样程序就会报错,所以不可以在后面加分号
    
    
    
        // 为什么要加外⾯的括号?相同的例子
        //这是加外面括号的正确情况:
        cout << ADD(1, 2) * 3 << end1;
        //cout << ((1) + (2)) * 3 << end1;
    
        //这是不加外面括号的错误情况,导致运算优先顺序不同了:
        cout << ADD(1, 2) * 3 << end1;
        //cout << (1) + (2) * 3 << end1;
    
    
    
        // 为什么要加⾥⾯的括号?
        int x = 1, y = 2;
        ADD(x & y, x | y);
        //不加里面的括号会变成(x & y + x | y)
        //同样导致运算优先顺序不同了
        return 0;
    }
    
  5. VS编译器 debug版本下面默认是不展开inline的,release版本则是会默认展开incline的,这样⽅便调试,debug版本想展开需要设置⼀下以下两个地⽅。在创建的项目处点击右键,选择属性,再按照以下步骤进行设置:
  6. 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;
    }
    
    
    // main.cpp
    #include "F.h"
    int main()
    {
        // 链接错误:⽆法解析的外部符号 "void __cdecl f(int)" (?f@@YAXH@Z)
        f(10);
        return 0;
    }

    13. nullptr

  1. NULL实际是一个宏,在传统的C头⽂件(stddef.h)中,可以看到如下代码:
    #ifndef NULL
        #ifdef __cplusplus
            #define NULL 0
        #else
            #define NULL ((void *)0)
        #endif
    #endif
  2. C++中NULL可能被定义为字⾯常量0,或者C中被定义为无类型指针(void*)的常量。不论采取何种定义,在使⽤空值的指针时,都不可避免的会遇到⼀些麻烦,本想通过f(NULL)调⽤指针版本的f(int*)函数,但是由于NULL被定义成0,调⽤了f(int x),因此与程序的初衷相悖。f((void*)NULL); 调⽤会报错。
  3. C++11中引⼊nullptr,nullptr是⼀个特殊的关键字,nullptr是⼀种特殊类型的字⾯量,它可以转换成任意其他类型的指针类型。使⽤nullptr定义空指针可以避免类型转换的问题,因为 nullptr只能被隐式地转换为指针类型,而不能被转换为整数类型
    #include<iostream>
    using namespace std;
    
    void f(int x)
    {
        cout << "f(int x)" << endl;
    }
    
    void f(int* ptr)
    {
        cout << "f(int* ptr)" << endl;
    }
    
    int main()
    {
        f(0);
        // 本想通过f(NULL)调⽤指针版本的f(int*)函数,但是由于NULL被定义成0,调⽤了f(int
    x),因此与程序的初衷相悖。
        f(NULL);
        f((int*)NULL);
    
        // 编译报错:error C2665: “f”: 2 个重载中没有⼀个可以转换所有参数类型
        // f((void*)NULL);
    
        f(nullptr);
    
        return 0;
    
    }

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值