• 【C++】入门(二):引用、内联、auto


    书接上回:【C++】入门(一):命名空间、缺省参数、函数重载

    六、引用

    引用的概念

    引用就是给已经存在的变量 取别名编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间

    引用的使用场景

    1. 引用做参数

    作用1:输出型参数

    学习C的时候 写Swap()函数 通过传实参地址的方式,使用指针交换两个变量里面的值 。而加入引用这个语法,则通过别名的方式 交换两个变量里面的值。

      void Swap(int* a, int *b)
      {
      	//...
      }
      
      void Swap(int& a, int &b)
      {
      	int tmp = a;
      	a = b;
      	b = tmp;
      }
      
      int main()
      {
      	int x = 0, y = 1;
      	Swap(&x, &y);
      	Swap(x, y);
      
      	return 0;
      }
    
    作用2:对象比较大,减少拷贝,提高效率

    对象较大的情况下,对比传值传参 和 传引用传参 的代码效率

    #include 
    #include
    using namespace std;
    struct A { int a[10000]; };
    void TestFunc1(A a) {}
    void TestFunc2(A& a) {}
    void main()
    {
    	A a;
    	// 以值作为函数参数
    	size_t begin1 = clock();
    	for (size_t i = 0; i < 10000; ++i)
    		TestFunc1(a);
    	size_t end1 = clock();
    
    	// 以引用作为函数参数
    	size_t begin2 = clock();
    	for (size_t i = 0; i < 10000; ++i)
    		TestFunc2(a);
    	size_t end2 = clock();
    
    	// 分别计算两个函数运行结束后的时间
    	cout << "TestFunc1(A)-time:" << end1 - begin1 << endl;
    	cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;
    }
    

    我们可以看到一个40000字节的对象,分别使用传值传参、传引用传参,且传参次数10000次。两种传参方式使用的时间有一定差异。引用作为函数参数 消耗的时间小于1 ms,所以是0。
    演示结果
    总结:这些使用场景,指针也可以适应,但是引用更方便

    2. 引用作为返回值

    错误示范
    • 一般情况
      1️⃣ 被调函数传值返回。 返回a的拷贝 寄存在寄存器里的临时空间 函数栈帧销毁之前 这块要返回的临时空间就已经生成好了
      在这里插入图片描述
    int func()
    {
    	int a = 0;
    	return a;
    }
    int main()
    {
    	int ret = func();
    	cout << ret << endl;
    
    	return 0;
    }
    
    • 错误使用引用的情况
      2️⃣ 被调函数func传引用返回。 返回a的别名 但函数调用结束,栈帧销毁,a的别名还在访问这个空间——野引用
      在这里插入图片描述
    int& func()
    {
    	int a = 0;
    	return a;
    }
    
    int main()
    {
    	int ret = func();
    	cout << ret << endl;
    
    	return 0;
    }
    

    3️⃣被调函数传引用返回 ,主函数引用接收。返回a的别名、用a的别名接收,意味着ret也是a的别名,但栈帧结束,变量a已经被销毁了,ret还回去访问那块空间,所以ret是野引用。
    在这里插入图片描述

    int& func()
    {
    	int a = 0;
    	return a;
    }
    
    int main()
    {
    	int& ret = func();
    	cout << ret << endl;
    
    	return 0;
    }
    

    结论:返回变量出了函数作用域就生命周期就结束了,所以不能用引用返回

    正确示范

    什么情况下,可以用引用返回?
    答:全局变量、静态变量、堆上变量 可以用引用返回

    🌰 实际场景中的例子:
    使用引用语法修改用C语言写的链表

    struct SeqList
    {
    	int* a;
    	int size;
    	int capacity;
    };
    
    void SLInit(SeqList& sl)
    {
    	sl.a = (int*)malloc(sizeof(int) * 4);
    	// ..
    	sl.size = 0;
    	sl.capacity = 4;
    }
    
    void SLPushBack(SeqList& sl, int x)
    {
    	//...扩容
    	sl.a[sl.size++] = x;
    }
    
    // 修改
    void SLModity(SeqList& sl, int pos, int x)
    {
    	assert(pos >= 0);
    	assert(pos < sl.size);
    
    	sl.a[pos] = x;
    }
    
    int SLGet(SeqList& sl, int pos)
    {
    	assert(pos >= 0);
    	assert(pos < sl.size);
    
    	return sl.a[pos];
    }
    
    int main()
    {
    	SeqList s;
    	SLInit(s);
    	SLPushBack(s, 1);
    	SLPushBack(s, 2);
    	SLPushBack(s, 3);
    	SLPushBack(s, 4);
    
    	for (int i = 0; i < s.size; i++)
    	{
    		cout << SLGet(s, i) << " ";
    	}
    	cout << endl;
    
    	for (int i = 0; i < s.size; i++)
    	{
    		int val = SLGet(s, i);
    		if (val % 2 == 0)
    		{
    			SLModity(s, i, val * 2);
    		}
    	}
    	cout << endl;
    
    	for (int i = 0; i < s.size; i++)
    	{
    		cout << SLGet(s, i) << " ";
    	}
    	cout << endl;
    	return 0;
    }
    

    演进为C++玩法的版本
    C++中结构体中可以定义函数 ➡️ 类 成员变量 、 成员函数

    // C++
    struct SeqList
    {
    	// 成员变量
    	int* a;
    	int size;
    	int capacity;
    
    	// 成员函数
    	void Init()
    	{
    		a = (int*)malloc(sizeof(int) * 4);
    		// ...
    		size = 0;
    		capacity = 4;
    	}
    
    	void PushBack(int x)
    	{
    		// ... 扩容
    		a[size++] = x;
    	}
    	// 读写返回变量
    	int& Get(int pos)
    	{
    		assert(pos >= 0);
    		assert(pos < size);
    
    		return a[pos];
    	}
    
    	int& operator[](int pos)
    	{
    		assert(pos >= 0);
    		assert(pos < size);
    return a[pos];
    	}
    };
    int main()
    {
    	SeqList s;
    	s.Init();
    	s.PushBack(1);
    	s.PushBack(2);
    	s.PushBack(3);
    	s.PushBack(4);
    
    	for (int i = 0; i < s.size; i++)
    	{
    		//cout << s.Get(i)<< " ";
    		cout << s[i] << " ";
    		//cout << s.operator[](i) << " ";
    	}
    	cout << endl;
    
    	for (int i = 0; i < s.size; i++)
    	{
    		/*if (s.Get(i) % 2 == 0)
    		{
    			s.Get(i) *= 2;
    		}*/
    		if (s[i] % 2 == 0)
    		{
    			s[i] *= 2;
    		}
    	}
    	cout << endl;
    
    	for (int i = 0; i < s.size; i++)
    	{
    		cout << s.Get(i) << " ";
    	}
    	cout << endl;
    
    	return 0;
    }
    

    3.总结引用的价值

    1️⃣引用做参数
    🅰️可以作为输出型参数 。🅱️对象较大,减少拷贝提高效率
    2️⃣做返回值
    🅰️修改返回对象。 🅱️减少拷贝提高效率

    引用特性

    1. 引用必须初始化:定义的引用的时候就要确定该引用是谁的别名

    2. 引用定义后,不能改变指向

    3. 一个变量可以有多个引用 、多个别名

    int main()
    {
    	int a = 0;
    
    	// 1、引用必须初始化
    	//int& b;
    	// b = c;
    
    	// 2、引用定义后,不能改变指向
    	int& b = a;
    	int c = 2;
    	b = c;  // 不是改变指向,而是赋值
    
    	// 3、一个变量可以有多个引用,多个别名
    	int& d = b;
    
    	return 0;
    }
    

    思考:引用可以替代指针吗?
    答:引用不能完全替代指针,指针和引用的功能是类似的,C++的引用,对指针使用比较复杂的场景进行一些替换,让代码更简单易懂,但是不能完全替代指针,引用不能完全替代指针原因:引用定义后,不能改变指向

    🌰引用不可以替代指针的栗子:
    双向链表中,必须要改变指向,但引用不能改变指向。
    在这里插入图片描述
    🌰C++的引用 对指针使用比较复杂的场景 可以进行一些替换:
    例如:
    原来C语言 写链表头插入 实参传头节点指针 的 地址 。形参需要用二级指针接收。

    void PushBack(struct Node* phead, int x)
    {
    	// phead = newnode;
    }
    
    void PushBack(struct Node** pphead, int x)
    {
    	// *pphead = newnode;
    }
    
    

    现在C++ 使用引用: 给指针类型的变量(头节点指针)取别名

    void PushBack(struct Node*& phead, int x)
    {
    	//phead = newnode;
    }
    
    int main()
    {
    	struct Node* plist = NULL;
    
    	return 0;
    }
    

    📘杂七杂八的小知识点:数据结构教科书常见代码解读

    //给 结构体 重命名为LNode 
    //给 结构体类型的指针 重命名为 PNode
    typedef struct Node
    {
    	struct Node* next;
    	struct Node* prev;
    	int val;
    }LNode, *PNode;
    //PNode& phead  给PNode变量(结构体类型的指针)取别名 phead 
    void PushBack(PNode& phead, int x)
    {
    	//phead = newnode;
        //给引用赋值相当于改变PushBack()函数外面的链表头结点指针的指向
    }
    

    引用和指针的区别(面试题)

    从两个维度对比 语法角度、底层角度对比

    语法:
    1. 引用是别名,不开空间。指针是地址,语法上需要开空间存储地址
    2. 引用必须初始化 、指针可以初始化也可以不初始化。
    3. 引用不可以改变指向、指针可以改变指向
    4. 引用相对更安全 ,没有空引用、但是有空指针,容易出现野指针,不容易出现野引用

    底层:
    汇编层面上,没有引用,引用都是用指针实现的,引用编译后也转换成指针了

    调到反汇编调试观察:

    在这里插入图片描述
    结论:底层都需要开空间1、引用在底层是用指针实现的。2、语法含义和底层实现是背离的

    常引用

    由于引用存在一些隐患,所以我们可以加关键字const 使用常引用 。例如下面代码,对于变量b,它是a的别名,如果对引用b修改, 那么a的值就会改变。但是如果不希望a的值被修改,我们就可以给引用前面加上const 使得引用只有读取a的值的权限,但不能修改。例如,a的引用c就是这样的权限。

    int main()
    {
        int a = 0;
        //eg.1权限正常的引用 
        int &b =a;//b对a可读可写
        b++;
        
        //eg.2权限缩小
        const int& c = a;//c对a只有读取权限
        
        //eg.3权限放大(错误示范)
        const int x = 10;
        //int& y = x;  //注意不能权限放大
        
        //eg.4 权限平移(正确示范)
        const int& y = x //y可以读取x的值
            
        //eg.5 
        const int& z =10; //z是常量的别名
        
        //eg.6
        const int& m = a+x;// a+x表达式的返回值 是一个临时变量 临时变量具有常性
        
        //eg.7 (错误示范)
        int& n = a+x;
        
        return 0;
    }
    

    在这里插入图片描述
    在这里插入图片描述

    七、内联函数

    回顾:C语言如何避免函数频繁调用的问题?

    ➡️宏

    面试题1:实现两个数相加的宏函数

    #define Add(x,y) ((x)+(y))
    

    易错点:1、宏不是函数 2、不要写分号 3、括号控制优先级

    核心点:宏是预处理阶段进行替换

    提问:为什么要加里面的括号?
    答:因为x、y可能不是被单一变量所替换,而是其他表达式替换x、y ,防止表达式中的个别值先进性加法运算,从而运算符执行顺序和预期不符,导致运算结果错误。

    面试题2:宏的优缺点

    缺点:
    1️⃣坑很多,不易控制 2️⃣ 不能调试 3️⃣没有安全类型的检查
    优点:
    增强代码复用性、提高效率

    面试题3:C++有哪些技术替换宏?

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

    内联的概念

    内联 : 在调用地方展开 。所指的展开就是:不建立栈帧,在当前函数的栈帧里执行要调用函数的代码

    反汇编演示 具体是如何不建立栈帧展开的
    图片
    思考:能不能为所有函数 都加上inline ?
    不行 ,➡️ 内联函数的缺点

    内联函数的缺点

    使用内联🆚不使用内联

    假设func()函数100 行代码,并且一共要调用1w次分别考虑使用内联 、不使用内联的情况合计起来有多少指令?
    使用内联

    假设inline展开,合计指令数目为:100*1w。意味着 最后的可执行程序会变得很大

    ​解释:100行代码在1W个位置调用,那么1w个位置都会多出100行,所以就是100W行
    不使用内联

    假设inline不展开,合计指令数目:100+1w

    解释:1W个位置调用 底层就会是汇编指令 call func(),call func()后会从符号表找到这个函数的地址 然后去执行该函数的代码。

    总结:由此可见,如果是代码量较大的函数 使用inline 内联展开调用的话,会造成代码膨胀。所以内联函数只适合加在代码量较小的函数上!

    注意:
    1.inline对于编译器而言只是一个建议,不同编译器关于inline实现机制可能不同,一般建议:将函数规模较小不是递归、且频繁调用的函数采用inline修饰,否则编译器会忽略inline特性。
    2.inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就无法找到该函数。

    为什么做声明和定义分离?

    如果在头文件中进行函数定义,以下代码存在函数名重定义的问题

    问题分析
    Stack.cpp 包含了头文件 Test.cpp也包含了头文件。头文件的包含 在预处理阶段 会进行内容替换。也就是说Add()函数被定义两次。Stack.cpp Test.cpp 会分别产生Stack.o、Test.o。链接的时候 会把他们的符号表合并在一起,各自都有叫函数名修饰过的Add()函数,编译器就会认为函数重定义。

    所以我们要做声明和定义分离!

    //Stack.h
    #progam once
    #include
    using namespace std
    int Add(int a,int b)
    {
        cout<<"int Add(int a,int b)"<<endl;
        return a+b;
    }
    //Stack.cpp
    #include"Stack.h"
    //Test.cpp
    #include"Stack.h"
    int main()
    {
        Add(1,2);
        return 0;
    }
    

    总结:解决重定义的方案?

    1.声明和定义分离

    2.Static,改变链接属性,该函数地址不会加入符号列表,只在当前文件可见。(适用于大函数)

    3.使用内联函数,因为要在调用处展开,所以该函数地址也不会加入符号列表。(适用于小函数)

    //Stack.h
    #progam once
    #include
    using namespace std
    inline int Add(int a,int b)
    {
        cout<<"int Add(int a,int b)"<<endl;
        return a+b;
    }
    //Stack.cpp
    #include"Stack.h"
    //Test.cpp
    #include"Stack.h"
    int main()
    {
        Add(1,2);
        return 0;
    }
    

    八、auto关键字

    简介

    C++11规定,程序员使用auto关键字就可以不指定数据类型,编译器通过右边的值的类型自动推导出左边值的数据类型。使用auto关键字 必须初始化,因为在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为变量实际的类型。

    使用细则

    1.auto与指针和引用结合起来使用
    auto* 必须初始化为指针 ,auto声明引用类型时则必须加&
    🌰栗子

    auto p1 = &i
    auto* p2 = &i;
    //auto* p3 = i;//会报错
    auto& r = a;
    

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

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

    auto不能推导的场景

    1、auto不能作为函数的参数

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

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

    void TestAuto() 
    { 
    int a[] = {1,2,3}; 
    auto b[] = {456};
    } 
    

    3、为了避免与C++98中的auto发生混淆,C++11只保留了auto作为类型指示符的用法

    4、auto在实际中最常见的优势用法就是跟以后会讲到的C++11提供的新式for循环,还有 lambda表达式等进行配合使用

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

    依次取数组中值赋值给e,自动迭代,自动判断结束

    for (auto e : array)
    {
    	cout << e << " ";
    }
    cout << endl;
    

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

    C语言中 我们这样初始化指针:

    int* p1 = NULL;
    

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

     #ifndef NULL
     #ifdef __cplusplus
     #define NULL    0
     #else
     #define NULL    ((void *)0)
     #endif
     #endif
    

    可以看到,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表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入 的。

    2、在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。

    3、为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr

  • 相关阅读:
    linux下doc转docx
    java类似stl建树代码(ACM)
    敏捷组织 | 企业克服数字化浪潮冲击的路径
    矩阵分析与应用-06-概率密度函数01
    批量导出导入数据及附件文件ZIP包
    FTP协议 21
    阿里云CPaaS,上榜Gartner全球代表服务商
    MySQL的日志管理与备份、恢复
    第P9周:YOLOv5-Backbone模块实现
    KNN算法 c++实现
  • 原文地址:https://blog.csdn.net/weixin_61400132/article/details/139334263