

文章目录
Hello,大家好,今天我们就来聊聊有关C++中的引用知识📖
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间
比如:李逵,在家称为"铁牛",江湖上人称"黑旋风"。【水浒108将各个有称号】

那要怎么去“引用”呢?
[&],它是【按位与】,也是【取地址】,但是在C++中呢,它叫做【引用】它的语法是怎样的呢?
类型& 引用变量名(对象名) = 引用实体;
int a = 10;
int& b = a;


int&,那么就必须要去对其进行一个初始化,指定一个其引用的对象,否则就会报错int a = 10;
int& b = a;
int& c;

a == b == cint a = 10;
int& b = a;
int& c = a;

int a = 10;
int& b = a;
int& c = b;

int a = 10;
int c = 20;
int& b = a;
int& b = c;


这个特性和常量指针与指针常量很类似,可以一起记忆
double类型,c2引用c1,所以c2也是double类型的。其他类型可以自己试试看double c1 = 3.14;
double& c2 = c1;
然后我们重点来说说有关指针这一块的引用【⭐】
int a = 10;
int* p = &a;
int*& q = p;
int*代表q是一个指针类型,&则表示指针q将会去引用另一个指针
int*&的这个写法要认识一下,我在下面还会讲到
以上就是有关C++中的引用所要介绍的特性,还望读者牢记😁
知道了引用的基本特性后,接下去我们来聊聊有关它的使用场景
void swap1(int* px, int* py)
{
int t = *px;
*px = *py;
*py = t;
}
swap1(&a, &b);
void swap2(int& x, int& y)
{
int t = x;
x = y;
y = t;
}
通过运行结果来看确实也可以起到交换两数的功能

void swap(int* px, int* py)
{
int t = *px;
*px = *py;
*py = t;
}
void swap(int& x, int& y)
{
int t = x;
x = y;
y = t;
}

通过调试来看一下吧💻

在讲解引用的特性时,我说到了引用的类型不仅仅限于普通变量,还可以是指针。但上面说的是普通指针,接下去我们来说说结构体指针,也涉及到了引用类型在做参数时的场景
typedef struct SingleNode {
struct SingleNode * next;
int val;
}SLNode;
void PushFront(SLNode** SList, int x)
{
SLNode* newNode = BuyNode(x);
newNode->next = *SList;
*SList = newNode;
}
int main(void)
{
SLNode* slist;
PushFront(&slist, 1);
return 0;
}
SList也就相当于是外部函数外部传入的实参slist。这就是很多学校《数据结构》的教科书中统一的写法,说是使用了纯C实现,但却利用了C++中的【引用】,如果没有学习过C++的小伙伴一定是非常难受😖void PushFront(SLNode*& SList, int x);
PushFront()内部我们也可以去做一个修改,直接使用形参SList即可,无需考虑到要对二级指针进行解引用变为一级指针void PushFront(SLNode*& SList, int x)
{
SLNode* newNode = BuyNode(x);
newNode->next = SList;
SList = newNode;
}

最后再来补充一点,很多教科书不仅仅是像上面这种写法,而且还会更加精简,它们将结构体定义成这种形式👇
typedef struct SingleNode {
struct SingleNode * next;
int val;
}SLNode, *PNode;
typedef的作用,对于SLNode而言其实就是对这个结构体的类型由进行了一个typedef,也就是对其进行一个重命名,这样我们在使用这个结构体定义变量的时候就不需要再去写struct SingleNode slist了,直接写成SLNode slist即可typedef struct SingleNode SLNode
*PNode是什么意思呀❓”。这也是我重点要说明的部分,其实这就相当于是对struct SingleNode*做了一个typedef,也就是对这个结构体指针的类型做了一个重命名叫做【PNode】,那后面如果要使用这个结构体指针的话直接使用的【PNode】即可typedef struct SingleNode* PNode
于是对于头插的形参部分又可以写成下面这种形式,与SLNode*& SList是等价的
void PushFront(PNode& SList, int x);
最后,我们来讲讲有关引用做参的第三个场景,也就是在递归调用的时候
指针的解引用来带动外部的变化BTNode* ReBuildTree(char* str, int* pi)
char str[20] = "abc##de#g##f###";
int i = 0;
BTNode* root = ReBuildTree(str, &i);
InOrder(root);
pi好了),因为pi就是i的引用,两者是等价的,属于同一块空间,因此无论函数递归调用多少层,内部参数的变化会也会带动外部的变化BTNode* ReBuildTree(char* str, int& pi)
{
if (str[pi] == '#')
{
pi++;
return NULL;
}
BTNode* root = (BTNode*)malloc(sizeof(BTNode));
root->val = str[pi++];
root->left = ReBuildTree(str, pi);
root->right = ReBuildTree(str, pi);
return root;
}
BTNode* root = ReBuildTree(str, i);
第二种引用的场景就是【做返回值】,因为这种场景在后面说到的C++里的类和对象中会大量出现,而且由于引用的语法很难理解,因此我会带你一步步学习,搞懂这这一种场景的使用
在讲引用做返回值之前我需要讲解一些知识点作为铺垫,希望正在阅读的你也可以认真观看和思考,这对下面的理解至关重要
int Add(int x, int y)
{
int c = x + y;
return c;
}
int main(void)
{
int ret = Add(1, 2);
cout << "ret = " << ret << endl;
return 0;
}
call指令的时候,会把call指令的下一条指令地址压入栈中,相当于记住了这个地址。
eax)
call指令的下一跳指令开始执行,此时我们通过汇编指令mov就可以知道编译器将【eax】中临时存放的函数的返回值转存到了这个临时变量ret中
如果上面有些看不懂的话可以看看👉函数栈帧的创建和销毁
知道了这些以后我们再来对比一下下面的两个Count函数,你觉得它们哪里不太一样呢🤨
int Count()
{
int n = 0;
n++;
// ...
return n;
}
int Count()
{
static int n = 0;
n++;
// ...
return n;
}
static的区别。通过画出函数调用的堆栈图我们可以看出对于两个不同的Count()函数而言其内部临时变量所存放的位置是不同的。我们知道,对于函数中的普通变量而言,是存放在当前所开辟函数的栈帧中的,即存放在内存中的栈区;但是对于函数中的静态变量而言,是不存放在当前函数栈帧中的,而是存放在内存中的静态区,包括平常可能会使用到的全局变量也是存放在其中首先你必须要清楚的一些点:
4 / 8B,若是一个函数需要返回空间很大的东西时就无法承载,就比如说要返回一个结构体就可能会放不下,因为结构体中存在各种各样的数据类型。所以对于临时变量而言有下面两种形式

静态区中的变量在函数栈帧销毁之后是如何返回给到外界的值做接收的呢?那有同学想:既然它都不存在于这个函数的栈帧中,那么也就不需要临时变量了吧,直接返回这个n不就好了
eax最后通过【mov】指令将寄存器中存放的临时值给到ret所在的这块空间
有些不太理解函数的调用和返回过程的同学可能就会钻牛角尖🐂提出这样的问题:做这么一个临时变量做返回不是很麻烦吗,为什么不先把这个值返回给外界,然后再销毁函数栈帧呢?
call指令去调了这个函数,它的栈帧就被建立起来。此时就在我们就正处在这个函数的栈帧内部了,每个栈帧都是通过[ebp]和[esp]来维护它所在这块空间的。虽然是记住了call指令的下一跳指令,但是没有记住需要接收的这个【ret】,因此在被调用的函数栈帧内部是无法找到这个接收变量ret的,是很难定位到它所在的这个空间的。但是我们又想把这个值返回回去,此时只能借助一个出了栈帧不会销毁的容器去承载、暂时保存一下这个返回值,然后当我们回到call指令的下一条指令时继续往下执行,才能顺理成章地找到这个【ret】,然后将寄存器中存放的临时值再赋值给到它做接收希望我这么说你可以真正理解了这个过程👆
【总结一下】:
临时变量去充当返回值【小一点的话可能是寄存器eax,大一点可能是在上一层栈帧开好的】然后再返回给外界的值做接受通过上面的示例你应该会觉得对于【栈区】而言使用临时变量返回还是合情合理的,可以【静态区】为什么也要通过临时变量来返回呢,这不是多此一举吗?
int& Count()
{
static int n = 0;
n++;
// ...
return n;
}

如果你想要进一步了解其返回的过程和直接【传值返回】有何区别,此时可以通过汇编来看看
eax中,而是通过汇编里的一个属性操作符offset进行了n个位置的偏移(汇编这一块我研究的不是很深,感兴趣的老铁可以去看看 链接)
但其实也不局限于上面的这两种,只要是出了作用域不会销毁都可以使用【传引用返回】
#define N 10
typedef struct Array
{
int a[N];
int size;
}AY;
int PostAt(AY& ay, int i)
{
assert(i < N);
return ay.a[i];
}
PostAt()函数,其所返回的ay.a[i]出了这个函数的作用域之后会不会销毁?很明显它并不是一个静态变量或者是全局变量,而是在外部就已经开好的一个结构体变量,其实也算是一个局部变量,只是它不存在于PostAt()这个函数的栈帧中,而是在main函数的栈帧中AY ay;
// 修改返回值
for (int i = 0; i < N; i++)
{
PostAt(ay, i) = i * 10;
}
int& PostAt(AY& ay, int i)

PostAt()函数,是C++11在STL中出现的新函数叫做【at()】,功能就是我上面所实现的这些,随着后面C++STL的学习会说到这一块,感兴趣的可以提前了解一下
【总结一下】:
在上面,我介绍到了一种对函数返回进行优化的方法 ——> 传引用返回,于是有的同学就觉得它很高大上,因此所以函数都使用了传引用返回,你认为可以吗?
int& Add(int a, int b)
{
int c = a + b;
return c;
}
int main()
{
int& ret = Add(1, 2);
Add(3, 4);
cout << "Add(1, 2) is :" << ret << endl;
return 0;
}
返回局部变量或临时变量的地址,上面我有说到过对于传引用返回而言并不需要临时变量去进行拷贝,返回的是这个变量的别名c只是存在于Add()函数栈帧中的一个临时变量而已,上面我们说到过对于出了作用域就会销毁的变量是不可以进行返回的,因此会报出下面这个Warning


接下去就来好好谈一谈究竟问题出在哪里🔍
&的话,那这就是我们平常写的一个函数,然后外界去做一个接受。但若是加上引用之后就不对了,因为这是一个临时变量,出了当前作用域后会随着函数栈帧的销毁而销毁,此时就已经出现问题了👈
如果此刻直接去访问【ret】的话它的值会是多少呢?答:可能是3,可能是7,也有可能是一个随机值
Add(3,4);屏蔽掉。那么此时的结果就有可能是3,有可能是随机值
< 原理解说 >

可能就像上面这么说不是很好理解,我们通过【薛定谔的猫🐱】这个梗来进行讲解

通过上面这个案例我想说明什么呢?
接下去我们再来详细说说ret使用引用接收这一块较难理解的地方
int& ret = Add(1, 2);
int ret = Add(1, 2);
好了,看到这里,相信你对引用做返回值的使用场景应该有了很深刻的理解,来做个总结

最后的话再带读者来回顾一下【传值返回】和【传引用返回】
在上一模块,我介绍了有关引用的两种使用场景,相信你在学习了之后也是一头雾水,学它有什么用呢?和普通的传值有何区别?本模块就来对【传值】和【传引用】这两种方式来做一个对比
#include <time.h>
struct A {
int a[10000];
};
void TestFunc1(A a){}
void TestFunc2(A& a){}
void TestRefAndValue()
{
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 << endl;
cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;
}
19ms,引用做参则是0ms,虽然二者存在差距,但是差距并不大。传引用作参替代了我们在C语言中学习的二级指针,无需考虑传入一级指针的地址,然后再函数内部在做解引用
不过呢,这个时间其实还看不出引用的强大之处,我们通过另一个场景,来看看值返回与引用返回二者的差距是否会大一些
// 值返回
A TestFunc1()
{
return a;
}
// 引用返回
A& TestFunc2()
{
return a;
}

看到这里之后你对C++中的【引用】应该是有一个很清晰的概念了,也知道了它的强大之处。接下去我来普及一个东西叫做【常引用】,也是引用里面很重要的一块知识点
权限放大 —— 用普通的变量替代只读变量
首先来看下下面这段代码,你认为什么地方有问题?
const了,首先对于【const】关键字修饰的变量具有常性,是不可以被随意修改的,但此时变量d引用了c,那么c和d就从属于同一块地址了,不过变量d不具有常性,因此它是可以被修改的。那么这个时候就会产生歧义了,也就出现了问题int a = 1;
int& b = a;
const int c = 2;
int& d = c;

权限保持 —— 用只读的变量替代只读变量
const int c = 2;
const int& d = c;

权限缩小 —— 用只读的变量替代普通变量
int c = 2;
const int& d = c;

看完了上面这段三点你应该清楚了常引用是怎样一个概念,但是有很多同学在学习了常引用之后却将其他知识混淆了,所以我专门拎出几块容易搞混的给读者说明
1. 权限方法只适用于引用和指针类型
const int m = 1;
int n = m; //普通变量不受约束


2. 临时变量具有常属性【⭐】
int Count()
{
int n = 0;
n++;
// ...
return n;
}
int ret = Count();
int& ret = Count();

void print(string& str)
{
cout << str << endl;
}
int main(void)
{
print("hello world!");
return 0;
}
Windows环境下运行

LInux环境下运行

const呀,怎么会和常扯上关系呢?hello world!而言,其实就是准备传入函数的一个实参,此时编译器根据字符串hello world构造一个string类型的临时对象,这个临时变量具有const属性,当这个临时变量传递给非const的string&引用类型时,无法隐式完成const到非const的类型转换,造成了一个权限的放大,便出现上面的编译错误❌const做修饰即可,这样便可以做到【权限保持】,顺利通过编译✔void print(const string& str)

通过以上代码,可以看出在设计函数时,形参尽可能地使用const,这样可以使代码更为健壮,将错误暴露于编译阶段
3. 类型转换都会产生临时变量
double类型的变量引用了一个整型的变量,可以看到也出现了我们上面所碰到的一些编译问题int i = 10;
double& rd = i;

int类型的变量给到一个double类型的变量做引用,那类型的都不一样肯定是会出问题呀!”const,却不会出现问题了,你怎么解释呢😎
(double)i你我们在C语言都有学过,这是一种的显式的强制类型转换,将一个int类型的变量强制转换为了double类型,但其实在编译器看来,却不是这样的👈i并不是被转换成了一个【double】类型,而是产生了一个【double】类型的临时变量,然后把i的值按照【double】的类型放到了这个临时变量中,在C语言数据存储章节我们有提到过对于浮点数放到内存中是要分为整数部分和小数部分的,按照对应的权值转换为二进制的形式存放到内存中int i = 10;
cout << (double)i << endl;
int i = 10;
double dd = i;
i,而是i在进行类型转换的时候产生一个【double】类型的临时变量,rd是对它进行了一个引用const做一个【权限保持】就不会出问题了const double& rd = i;
有关【const常】和引用之间的语法点其实还有很多,但涉及到一些读者的水平,将上面这些都理解了也算懂了七八十,后面有机会再做补充😊
好,最后我们对指针和引用这一块来做一个小结,相信你一定觉得它们之间有着千丝万缕般的关系🔗
int a = 10;
int& ra = a;
cout<<"&a = "<<&a<<endl;
cout<<"&ra = "<<&ra<<endl;
int a = 10;
int* pa = &a;
不过从【汇编层面】来看,其实二者是一样的,引用也是用指针去实现的,也会开空间
int main()
{
int a = 10;
int& ra = a;
ra = 20;
int* pa = &a;
*pa = 20;
return 0;
}

我们可以来浅浅分析一下🔍
lea是【load effective address】加载有效地址,将变量a中存放的内容放到寄存器eax中lea eax,[a]
eax中的值,也就是变量i的地址放入变量【ra】的地址所指示的内存单元中,虽然引用使用的都是同一块空间,但是在底层还是开出了一块空间来存放,这个我们可以不用关心mov dword ptr [ra],eax
eaxmov eax,dword ptr [ra]
14h(十六进制)也就是20放到寄存器eax的值(指向变量i的地址)所指示的内存单元中mov dword ptr [eax],14h
其实难以理解的读者可以通过这么一个生活中的案例去理解

所以对于引用来说在我们看来是不会开新空间的,但实际上底层却做了相反的事,和我们想的是千差万别
下面对本文所讲解的内容进行一个总结,希望读者可以分清楚引用和指针之间的区别👈
int a = 10;
int& b = NULL; //×
int* pa = NULL; //✔



int a = 1;
int& b = a;
int& c = b;
int&&这种形式再去引用,因为这个涉及到C++中的左值、右值引用,后续专门出文章讲解int&& c = b;

接下去放一道阿里的历年笔试题,比较经典,也容易出错
关于引用以下说法错误的是( )
A.引用必须初始化,指针不必
B.引用初始化以后不能被改变,指针可以改变所指的对象
C.不存在指向空值的引用,但是存在指向空值的指针
D.一个引用可以看作是某个变量的一个“别名”
E.引用传值,指针传地址
F.函数参数可以声明为引用或指针类型
【答案】:E
【解析】;下面的解析均有演示过,此处不再演示💻
A. 引用必须初始化,必须在定义引用时明确引用的是哪个变量或者对象,否则语法错误,指针不初 始化时值为随机指向
B.引用一旦定义时初始化指定,就不能再修改,指针可以改变指向
C.引用必须初始化,不能出现空引用,指针可以赋值为空
D.简单粗暴的引用理解可以理解为被引用变量或对象的"别名"
👉E.有看过汇编,引用表面是传值,其实底层也是传地址,只是这个工作有编译器来做,所以错误
F.函数调用为了提高效率,常使用引用或指针作为函数参数传递变量或对象
最后,来总结一下本文所学习的内容📖
以上就是本文要介绍的所有内容,如果觉得有帮助可以给个三连哦🌹🌹🌹

总的来说,我对ruby还比较陌生,我正在为我正在创建的对象编写一些rspec测试用例。许多测试用例都非常基础,我只是想确保正确填充和返回值。我想知道是否有办法使用循环结构来执行此操作。不必为我要测试的每个方法都设置一个assertEquals。例如:describeitem,"TestingtheItem"doit"willhaveanullvaluetostart"doitem=Item.new#HereIcoulddotheitem.name.shouldbe_nil#thenIcoulddoitem.category.shouldbe_nilendend但我想要一些方法来使用
我试图在一个项目中使用rake,如果我把所有东西都放到Rakefile中,它会很大并且很难读取/找到东西,所以我试着将每个命名空间放在lib/rake中它自己的文件中,我添加了这个到我的rake文件的顶部:Dir['#{File.dirname(__FILE__)}/lib/rake/*.rake'].map{|f|requiref}它加载文件没问题,但没有任务。我现在只有一个.rake文件作为测试,名为“servers.rake”,它看起来像这样:namespace:serverdotask:testdoputs"test"endend所以当我运行rakeserver:testid时
作为我的Rails应用程序的一部分,我编写了一个小导入程序,它从我们的LDAP系统中吸取数据并将其塞入一个用户表中。不幸的是,与LDAP相关的代码在遍历我们的32K用户时泄漏了大量内存,我一直无法弄清楚如何解决这个问题。这个问题似乎在某种程度上与LDAP库有关,因为当我删除对LDAP内容的调用时,内存使用情况会很好地稳定下来。此外,不断增加的对象是Net::BER::BerIdentifiedString和Net::BER::BerIdentifiedArray,它们都是LDAP库的一部分。当我运行导入时,内存使用量最终达到超过1GB的峰值。如果问题存在,我需要找到一些方法来更正我的代
Rails2.3可以选择随时使用RouteSet#add_configuration_file添加更多路由。是否可以在Rails3项目中做同样的事情? 最佳答案 在config/application.rb中:config.paths.config.routes在Rails3.2(也可能是Rails3.1)中,使用:config.paths["config/routes"] 关于ruby-on-rails-Rails3中的多个路由文件,我们在StackOverflow上找到一个类似的问题
我需要从一个View访问多个模型。以前,我的links_controller仅用于提供以不同方式排序的链接资源。现在我想包括一个部分(我假设)显示按分数排序的顶级用户(@users=User.all.sort_by(&:score))我知道我可以将此代码插入每个链接操作并从View访问它,但这似乎不是“ruby方式”,我将需要在不久的将来访问更多模型。这可能会变得很脏,是否有针对这种情况的任何技术?注意事项:我认为我的应用程序正朝着单一格式和动态页面内容的方向发展,本质上是一个典型的网络应用程序。我知道before_filter但考虑到我希望应用程序进入的方向,这似乎很麻烦。最终从任何
我在我的项目中添加了一个系统来重置用户密码并通过电子邮件将密码发送给他,以防他忘记密码。昨天它运行良好(当我实现它时)。当我今天尝试启动服务器时,出现以下错误。=>BootingWEBrick=>Rails3.2.1applicationstartingindevelopmentonhttp://0.0.0.0:3000=>Callwith-dtodetach=>Ctrl-CtoshutdownserverExiting/Users/vinayshenoy/.rvm/gems/ruby-1.9.3-p0/gems/actionmailer-3.2.1/lib/action_mailer
刚入门rails,开始慢慢理解。有人可以解释或给我一些关于在application_controller中编码的好处或时间和原因的想法吗?有哪些用例。您如何为Rails应用程序使用应用程序Controller?我不想在那里放太多代码,因为据我了解,每个请求都会调用此Controller。这是真的? 最佳答案 ApplicationController实际上是您应用程序中的每个其他Controller都将从中继承的类(尽管这不是强制性的)。我同意不要用太多代码弄乱它并保持干净整洁的态度,尽管在某些情况下ApplicationContr
我想向我的Controller传递一个参数,它是一个简单的复选框,但我不知道如何在模型的form_for中引入它,这是我的观点:{:id=>'go_finance'}do|f|%>Transferirde:para:Entrada:"input",:placeholder=>"Quantofoiganho?"%>Saída:"output",:placeholder=>"Quantofoigasto?"%>Nota:我想做一个额外的复选框,但我该怎么做,模型中没有一个对象,而是一个要检查的对象,以便在Controller中创建一个ifelse,如果没有检查,请帮助我,非常感谢,谢谢
我注意到像bundler这样的项目在每个specfile中执行requirespec_helper我还注意到rspec使用选项--require,它允许您在引导rspec时要求一个文件。您还可以将其添加到.rspec文件中,因此只要您运行不带参数的rspec就会添加它。使用上述方法有什么缺点可以解释为什么像bundler这样的项目选择在每个规范文件中都需要spec_helper吗? 最佳答案 我不在Bundler上工作,所以我不能直接谈论他们的做法。并非所有项目都checkin.rspec文件。原因是这个文件,通常按照当前的惯例,只
我正在使用active_admin,我在Rails3应用程序的应用程序中有一个目录管理,其中包含模型和页面的声明。时不时地我也有一个类,当那个类有一个常量时,就像这样:classFooBAR="bar"end然后,我在每个必须在我的Rails应用程序中重新加载一些代码的请求中收到此警告:/Users/pupeno/helloworld/app/admin/billing.rb:12:warning:alreadyinitializedconstantBAR知道发生了什么以及如何避免这些警告吗? 最佳答案 在纯Ruby中:classA