引用概念

举报
是店小二呀 发表于 2025/09/09 14:53:50 2025/09/09
【摘要】 引用概念:引用不是新定义一个变量,而是个已存在变量取了一个别名(外号),编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间 比如:李逵,在家称为"铁牛",江湖上人称"黑旋风" (李逵=="铁牛" =="黑旋风" )语法:类型说明符& 引用变量名(对象名)=引用实体(不能是常量) ​ 引用类型必须和引用实体是同种类型void TestPef(){    int a=10;  ...

引用概念:引用不是新定义一个变量,而是个已存在变量取了一个别名(外号),编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间 比如:李逵,在家称为"铁牛",江湖上人称"黑旋风" (李逵=="铁牛" =="黑旋风" )

语法:

类型说明符& 引用变量名(对象名)=引用实体(不能是常量) ​ 引用类型必须和引用实体是同种类型

void TestPef()
{
   int a=10;
   int& pa=a;//pa是a的别名
   
   printf("&a==%p\n", &a);
printf("&pa==%p\n", &pa);
   //从地址上,可以得出它和它引用的变量共用同一块内存空间
}

这就说明了修改对象a的也会影响对象pa,同理反之。

void TestPef()
{
   int a=10;
   int& pa=a;//pa是a的别名  
   a++;
   printf("a==%d pa==%d\n", a,pa);//11 11
   pa++;
printf("a==%d pa==%d\n", a,pa);//12 12
}

引用特性:

  • 引用在定义时必须初始化(否则在编译阶段会报错)

  • 引用一旦引用一个实体,再不能引用其他实体

  • 一个变量可以有多个引用,引用变量也可以取别名

int main()
{
  // 1.引用必须初始化
   int a=10;
   int& b;//(否则在编译阶段会报错)
    b=a;
   
   //2.引用定义后,不能改变指向(这一点很重要)
   int& b=a;
   int c=10;
   b=c;//不是改变指向,而是b(a)赋值为10
   
   //3.一个变量可以有多个别名,别名也有别名
   int &d=b ;//d是b的别名,b是a的别名,则d是a的别名
   return 0;
}

常引用(权限可以缩小,但是不能放大)

void TestConstRef()
{
int a=0;
   int& b=a;
   //支持-权限缩小
   const int& c=a;
   
   //不支持-权限放大
   const int x=10;
   int& y=x;//此时的x只有读权限,没有写权限
   const int& y=x;
   
   //表达式的返回值是临时对象,而临时对象具有常性
   int& n=a+x//这里是属于权限放大
   const int& n=a+x;
   
}

引用的使用场景:

一、做参数(a、输出型参数 b、对象比较大,减少拷贝,提高效率)

如果参数是指针类型,使用时需要对其解引用操作,但是使用引用可以避免解引用操作

void Swap(int* x,int* y)
{
int tmp=*x;
   *x=*y;
   *y=tmp;
}
void Swap(int& x,int& y)
{
int tmp=x;
   x=y;
   y=tmp;
}
void PushBack(struct Node**pphead,int x)
{
*pphead=newndoe;
}
void PushBack(struct Node* & pphead,int x)
{
pphead=newndoe;
}

在调用效率方面也有了很大的提升

#include <time.h>

struct A
{
int a[10000];
};
void TestFunc1(A a)
{
}
void TestFunc2(A &a)
{
}
int 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;

return 0;
}


二、做返回值(a、修改返回对象 b、减少拷贝提高效率)

首先先说明,以是引用做返回值的错误用法

int& func()
{
int a = 0;
return a;
}
int main()
{
int ret = func();//第一种
   int ret = func();//第二种
cout << ret << endl;
return 0;
}

解释:第一种,当func()函数运行结束,函数内存将被系统收回,int&做为返回值,返回的是a的别名,ret取出了这块空间的值,但是这空间到底是什么?可能有两种结果,编译器是否对这块函数栈帧清空,对此这样子操作是不具备安全性的。

第二种,如果再通过引用接收,ret就是指向这块已经被回收的空间,这样属于野引用,访问这块空间,好比成租房子,当房子合同到期,如果非法进入会出现不安全的影响。


关于编译器是否对该函数栈帧清空,可以看一个有意思的东西

解释:对于ret值为0,说明编译器没有对函数栈帧清空,当调用fx()函数时,ret值为随机值,函数栈帧已经清空过。但是当引用做返回值,ret的值神奇等于了b的值,以为函数申请的空间是可以复用的。

结论:返回变量出来函数的作用域就生命周消耗(局部变量要消耗,就不能引用返回)

下面是引用做返回值的正确用法

返回值不采用局部变量,那么可以使用全局变量、静态变量、堆上变量等返回。(类/结构体中可以直接访问变量,不同传递this指针,临时变量具有常性)

比如实现修改顺序表任意位置的数值,需要两个步骤通过下标找到数据,并对数据进行修改,但是通过引用可以一步找到数据返回直接修改。


值和引用的作为返回值类型的性能比较

//这里对于将变量和函数定义在结构体中,这样子的话变量出了函数的作用域生命周期也不会销毁


引用和指针的区别

在语法概念上,引用是一个别名,没有独立空间,同其引用实体共用同一块空间,但是在底层实现上,实际引用是有开辟空间的,因引用是按照指针方式实现的。

int main()
{
int a=10;
int& ra=a;
cout<<"&a"<<&a<<endl;
cout<<"&ra"<<&ra<<endl;
return 0;
}

可以引用和指针的汇编代码对比下


指针和引用的功能类似的,有重叠的

C++的引用,对于指针使用比较复杂的场景进行一些替换,让代码更简单易懂,但是不能完全替换指针:引用定义后,不能改变指向

引用和指针的不同点

1.引用概念上定义一个变量的别名,指针存储一个变量地址

2.引用在定义时必须初始化,指针没有要求

3.引用在初始化引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体

4.没有NULL引用,但是有NULL指针

5.在sizeof中含义不同:引用结果为引用类型的大小,但是指针始终是地址空间所占字节个数(32位平台下占4个字节)

6.引用自加既引用的实体增加1,指针自加即指针后偏移一个类型的大小

7.有多级指针,但是没有多级引用

8.访问实体方式不同,指针需要显示解引用,引用编译器自己处理

9.引用比指针使用起来相对更安全

内敛函数

内敛函数的概念

内敛函数:以关键字inline修饰的函数,编译时C++编译器会在调用(内敛)函数的地方展开。对此没有函数调用建立栈帧的开销,内敛函数可以提升程序运行的效率,但是内敛函数也是需要代价的。

注:这本身是一种代码优化行为,可以在release模式下,参考call Add函数地方,是否被展开。如果需要在debug模式下,需要对编译器进行设置,否则不会展开(在debug模式下,编译器默认不会对代码进行优化,可以通过解决任务管理器->配置->C/C++->常规(程序数据库)->优化->内联函数扩展->只适用于_inline)

内敛函数的特性;

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

2.inline对于编译器而言只是一个建议/请求,编译器可以选择忽略这个建议/请求,不同编译器关于inline实现机制可能不同。

3.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()
{
f(10);
return 0;
}
// 链接错误:main.obj : error LNK2019: 无法解析的外
f(int)" (?f@@YAXH@Z),该符号在函数 _main 中被引用

第一点:内敛函数展开,编译程序空间变大(指令变多),空间是编译好的可执行程序,并不是我们理解的内存。这就导致个问题,当1000个行需要调用swap函数,如果swap不是内敛函数,合计是10+1000(这里10是swap内部行数),如果swap是内敛函数,合计是10*1000。对此就出现了可能使得目标文件变大。

第二点;不同编译器对内联函数展开行数限定不同,但是内敛是有限制的,大函数和递归是不会展开,而是调用该函数使用

第三点:

问题:为什么需要声明和定义分离


在声明这里定义Add函数,在预处理阶段会被展开,那么两个.cpp文件都有一份Add函数,就会出现命名冲突。对此需要声明和定义分离

那么如果我就想要在.h定义Add函数怎么办呢?可以使用static关键字,使用函数的外部链接属性转为内部链接属性,只在当前文件可见,简单来说就是不进符号表,就没有命名冲突了。


同时也可以使用内敛函数解决,如果在.cpp定义内敛函数,因为内敛函数不需要调用函数,就没有函数的地址,简单来说也是不会进符号表,对于需要在其他文件中需要使用该函数,会发生链接错误。对此,需要将内敛函数的声明和定义放在一块,在头文件展开并且链接时不会出现命名冲突。

对于三种方法的使用推荐:

1.如果是大函数,可以使用声明和定义分离、static修饰2.如果是小函数,可以使用内敛。

【面试题】

宏的优缺点?

优点:

增加代码的复用性

提高性能

缺点:

不方便调式宏(预处理阶段进行了替换)

导致代码可读性差,可维护性差,容易误用

没有类型安全的检查

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

常量定义换用const enum

短小函数定义 换用内敛函数

auto关键字(C++11)

前文:

由于程序中使用到的类型也越来越复杂,导致了类型难于拼写,含义不明确导致容易出错。

比如:std::map<std::string, std::string>::iterator 是一个类型,但是该类型太长了,特别容易写错。当然可以使用typedef给类型取别名,简化代码。同时这里也有属于typedef的不足。

typedef的陷阱

typedef char * pstring;
int main()
{
const pstring p1//编译成功还是失败?-->失败
const pstring* p2//编译成功还是失败?-->成功
return 0;
}

首先需要提出一个问题:“const pstring”是否等于const char *。答案是这两个是不等于的,这里const pstring中的const给予了整个指针本身常性,并且形成了常量指针(char * const).对此,在无论什么时候,只要指针声明typedef,那么在typedef名称加一个const使得指针本身是常量。对于p2来说,const pstring* p2转化后是char* const *p2,那么p2是个二级指针,所以p2不初始化也是可以的。

关于typedef不足

1.如果使用typedef过程中,给const扯上关系,使用上容易出现问题,这里不是简单的字符串替换

2.虽然 typedef 并不真正影响对象的存储特性,但在语法上它还是一个存储类的关键字,就像 auto、extern、static 和 register 等关键字一样

typedef static int INT_STATIC;

不可行的原因是不能声明多个存储类关键字,由于 typedef 已经占据了存储类关键字的位置,因此,在 typedef 声明中就不能够再使用 static 或任何其他存储类关键字了

auto简介:

在早期C/C++中auto的含义是:使用auto修饰的变量,该变量具有自动存储器的局部变量,但是没有人使用它,因为意义不大,这里指向的是局部变量,那么当函数结束,局部变量出了作用域,生命周期结束,变量会自动销毁,对此使用没有意义。

C++11中,auto被赋予了新的含义:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译中推导而得。

打印变量类型办法(了解即可)

typeid(对象名).name()可以得到变量的类型

auto的使用(自动识别类型)

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

auto与指针和引用结合起来使用。当auto声明指针类型时,用auto和auto*没有任何区别(如果使用auto *,则表示指向的变量是指针类型)但用auto声明引用类型时,则必须加&

int TestAuto()
{
return 10;
}
int main()
{
int a = 10;
auto b = a;
auto d = TestAuto();
auto d=&a;
return 0;
}

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

在使用的过程中,需要知道右边上是什么类型的,如果不熟悉,会影响可读性,在auto做返回值有所体现。

对于上面这样子使用auto是没有价值的,主要还是对于类型很烦很长,使用auto进行自动推导类型。

auto不能推导的场景

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

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

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

4.auto在实际中最常见的优势用法,就是跟新试for循环,lambda表达式等进行配合使用。

auto不能作为函数的参数,但是可以做函数的返回值,但是需要慎用auto去做返回值,跟可读性有关系,以下为例,如果多层嵌套函数,返回值都是auto,那么得到返回值的类型,需要一个个函数去检查。


10.指针空值

在C/C+良好的编程习惯中,对于未初始化的指针,一个没有合法的指向的指针,基本会进行初始化。int *p=NULL\0。对此在C头文件<stddef.h>中,可知NULL实际是一个宏。

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


对此NULL可能定义有两种(字面常量0,无类型指针(void*)的常量),cpp中使用NULL指针空值,可能会遇到一些问题。

void f(int)
{
cout << "haha" << endl;
}
void f(int *)
{
cout << "hehe" << endl;
}
int main()
{
f(0);
f((int*)NULL);
f(NULL);
return 0;
}


按照预期,传NULL应该调用int *的函数,但是NULL被定义成0,对此预期和结果不匹配。对此为了区分Cpp和C,cpp引出了nullptr关键字代替NULL的使用。

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

注:

1.nullptr是C++11作为新关键字引入的,在使用过程中,不需要包含头文件

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

3.为了提高代码的健壮性,在后续cpp使用中最好使用nullptr表示指针空值






【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。