[C++] Detailed introduction to C++ references takes you from shallow to deep understanding of references


1. The concept of reference

Reference is to give an alias to an existing variable. The compiler will not open up memory space for the reference variable. It shares the same memory space with the variable it refers to. The operation on the reference object is exactly the same as the direct operation on the variable.

Its definition format is: type & reference variable name = defined variable name.
For example

int a = 10;
int& b = a; //给a起一个别名叫 b,对 b 进行任何操作都和直接对a的操作是一样的 

example code

#include<iostream>
using namespace std;
int main()
{
    
    
	int a = 10;
	int& b = a;
	a++;
	printf("a=%d\n", a);
	printf("b=%d\n", b);
	printf("a=%p\n", &a);
	printf("b=%p\n", &b);
	return 0;
}

insert image description here

Two, the characteristics of the reference

  1. References must be initialized when they are defined
  2. A variable can have multiple references
  3. Once a reference refers to an entity, it cannot refer to another entity
#include<iostream>
using namespace std;
void Test1()
{
    
    
	int a = 10;
	int b = 20;
	// int& ra; // 引用未初始化,该条语句编译时会出错
	int& ra = a;
	//int& ra = b; //该条语句编译时会出错	C2374 “ra” : 重定义;多次初始化	
	int& rra = a;
	int& rrra = rra; //给别名取别名

	int* pa = &a;
	int*& rpa = pa;//给指针取别名

	cout << &a << endl;
	cout << &ra << endl;
	cout << &rra << endl;
	cout << &rrra << endl;

	cout << endl;

	cout << pa << endl;
	cout << rpa << endl;
}
int main()
{
    
    
	Test1();
	return 0;
}

insert image description here

3. Special references - common references

The core of the understanding of constant references is: the authority of pointers and references can only be reduced or maintained, but not enlarged

1. Constant references to variables

#include<iostream>
using namespace std;
int main()
{
    
    
	const int a = 10;

	//引用时:
	//int& ra = a;  //报错  a不能被修改,故ra也不能被修改,属于权限放大
	const int& ra = a; //正常 a不能被修改,ra也不能被修改,属于权限保持

	//指针时:
	//int* pa = &a; //报错  a不能被修改,故pa也不能通过指针修改a,属于权限放大
	const int* pa1 = &a; //正常 a不能被修改,pa1也不能通过指针修改a,属于权限保持
	const int* const pa2 = &a; //正常 a不能被修改,pa2也不能通过指针修改a,且pa2不能指向其他地址,属于权限缩小

	int b = 20;
	int& rb = b; //正常 b与rb权限一致,属于权限保持
	const int& rrb = b; //正常 但是rb不能被修改,属于权限缩小

	int* ptr = NULL;
	int*& rptr1 = ptr; //正常 ptr与rptr1权限一致,属于权限保持
    int*& const rptr2 = ptr; //正常 但是rptr2不能改变指向,属于权限缩小
	return 0;
}

2. Constant references to constants

Note: Temporary variables are constant. Temporary variables will be generated during the return value of the function and the process of forced type conversion, so they are also constant.

#include<iostream>
using namespace std;
int Count()
{
    
    
	static int n = 0;
	n++;
	return n;
}
int main()
{
    
    
	const int& a = 10;//正常,10是常数,属于权限保持。
	const int& b = Count(); //不加const会报错,临时变量具有常性!
	
	int c = 10;
	const double& d = c;//不加const会报错,d是c强制类型转化为double类型的过程中,double类型临时变量的引用
}

4. Referenced usage scenarios

1. As a function parameter

#include<iostream>
using namespace std;
void Swap(int& left, int& right)
{
    
    
	int temp = left;
	left = right;
	right = temp;
}
int main()
{
    
    
	int a = 10;
	int b = 20;
	Swap(a, b);
	cout << "a=" << a << endl;
	cout << "b=" << b << endl;
	return 0;
}

insert image description here
引用传参If the formal parameter is a reference type, the formal parameter is an alias of the actual parameter, so the modification is equivalent to modifying the value of the variable directly on the basis of the actual parameter.

2. Make the return value of the function

To figure out how to use references as the return value of a function, we first have a clearer understanding of the process of function return.

First look at such a piece of code

#include<iostream>
using namespace std;
int Count()
{
    
    
	int n = 0;
	n++;
	return n;
}
int main()
{
    
    
	int ret = Count();
	return 0;
}

When the code is actually executed, the stack frame of the main() function will be created first. When the first sentence of code is executed, a function call is found, and then the stack frame of the Count() function will be created, and the code will be executed inside the Count() function.
insert image description here

After the code is executed, the stack frame of the Count() function will be destroyed, and then return to the stack frame of the main() function. However, when the variable n is destroyed in the stack frame of the Count() function, n will also be destroyed, so There is no way to pass n to ret in the main() function, so a temporary variable is needed to save the value of the return value n.

After the stack frame of the Count() function is destroyed and returns to the stack frame of the main() function, the temporary variable copies the value of n to ret, which is the process of the function returning.
insert image description here

The temporary variables here are divided into two types

  • When the return value is a variable that takes up less space, registers are usually used as temporary variables
  • When the return value is a structure variable that takes up a large space, it is usually opened in advance in the stack frame of the function that calls the Count() function.

You may think that the reason why the function returns like this is because the variable n is a local variable, and there is no way for the local variable to exist after leaving the scope. If n becomes a global variable , will there be no need for a temporary variable?

The answer is that temporary variables will still be created when the function returns, because the compiler is "fool-like" in doing function return, so isn't it a bit of a loss? Is there a way to solve this problem? The answer is yes: the reference is the return value of the function .

Since the reference variable is in the same memory space as the original variable, returning the reference variable is equivalent to directly returning the return value of our function, so that the temporary variable can be eliminated. But when using reference return, you must pay attention to the return value must still exist after leaving the function, otherwise it is illegal access

Code example 1

//1.用引用做函数的返回值,可以减少返回值的拷贝
#include<iostream>
using namespace std;
int& Count()        //使用引用返回
{
    
    
	static int n = 0;//将 n 的生命周期延长 防止非法访问
	n++;
	return n;
}
int main()
{
    
    
	int ret = Count();
	cout << ret << endl;
	return 0;
}

insert image description here

Since we use a reference as the return value, the return value is an alias of a variable, so we can operate on this alias, that is to say, we can modify the return value Code
Example 2

//2.使用引用返回,调用者可以修改返回对象的值
#include<iostream>
using namespace std;
int n = 10;//定义全局变量 n

int& Count()
{
    
    
	n++;
	return n;
}
int main()
{
    
    
	Count()=100; //如果Count()的返回值不是引用类型,则会报错 E0137	表达式必须是可修改的左值	
	cout << n << endl;
	return 0;
}

insert image description here

It can be seen that the reference as the return value of the function has two advantages and one point of attention:

  1. Using references as the return value of the function can reduce the copying of the return value
  2. With return by reference, the caller can modify the value of the returned object
  3. Points to note: When returning by reference, the returned object must be static or global, or belong to the previous stack frame, or apply for space on the heap.

Through the above explanation, I believe you have a good understanding of the return value of the reference, so let us look at the following piece of code, what is the output of the following code? Why?

#include<iostream>
using namespace std;
int& Add(int a, int b)
{
    
    
	int n = a + b;
	return n;
}
int main()
{
    
    
	int& ret = Add(1, 2);
	cout << "Add(1, 2) is :" << ret << endl;
	return 0;
}


The answer is: the result is random, and the code has bugs, and the specific result is undefined. If you get the answer right, it means you have learned very well. If you get the wrong answer, just listen to me slowly.


This is actually the problem of illegal access . When the code is actually executed, the stack frame of the main() function will be created first. When the first sentence of code is executed, a function call is found, and then the stack frame of the Count() function is created and entered. The Count() function executes the code internally.
insert image description here
After the code is executed, the stack frame of the Count() function will be destroyed, and then return to the stack frame of the main() function. However, when the variable n is destroyed in the stack frame of the Count() function, n will also be destroyed, n After this space is reclaimed by the operating system, the operating system may or may not clear the values ​​in the space, that is, the space of n may be 3 or a random value.

And because we use the reference as the return value, ret is an alias of the original space of n. Accessing ret is equivalent to accessing the destroyed n (this is already an illegal access), but the space of n may It is 3 or it may be a random value, so the output result is uncertain.
insert image description here

5. Comparison of pass-by-value and pass-by-reference efficiency

Values ​​are used as parameters or return value types. During parameter passing and return, the function does not directly pass actual parameters or return the variable itself directly, but passes actual parameters or returns a temporary copy of the variable, so values ​​are used as parameters Or the return value type is very inefficient, especially when the parameter or return value type is very large, the efficiency is even lower.

1. Comparison of the efficiency of passing values ​​and passing references when passing parameters

#include<iostream>
#include <time.h>
using namespace std;
#define N 10000
typedef struct A {
    
     
	int a[N]; 
}A;
void TestFunc1(A a) //传值
{
    
    
}
void TestFunc2(A& a) //传引用
{
    
    
}
void TestRefAndValue()
{
    
    
	A a;
	for (int i = 0; i < N; i++)
	{
    
    
		a.a[i] = i;
	}
	// 以值作为函数参数
	long begin1 = clock();
	for (long i = 0; i < 10000; ++i) 
	{
    
    
		TestFunc1(a);
	}
	long end1 = clock();

	// 以引用作为函数参数
	long begin2 = clock();
	for (long i = 0; i < 10000; ++i)
	{
    
    
		TestFunc2(a);
	}
	long end2 = clock();

	// 分别计算两个函数运行结束后的时间
	cout << "TestFunc1(A)-time:" << end1 - begin1 << endl;
	cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;
}
int main()
{
    
    
	TestRefAndValue();
	return 0;
}

insert image description here
There are about nine times more, of course yours may be different from mine, but it should be faster to pass references!

2. Comparison of passing value and passing reference efficiency when returning

#include<iostream>
#include <time.h>
using namespace std;
#define N 10000
typedef struct A {
    
    
	int a[N];
}A;
A a;

A TestFunc1(A a)
{
    
    
	return a;
}
A& TestFunc2(A a)
{
    
    
	return a;
}
void TestRefAndValue()
{
    
    
	for (int i = 0; i < N; i++)
	{
    
    
		a.a[i] = i;
	}
	// 以值作为函数参数
	long begin1 = clock();
	for (long i = 0; i < 10000; ++i)
	{
    
    
		TestFunc1(a);
	}
	long end1 = clock();

	// 以引用作为函数参数
	long begin2 = clock();
	for (long i = 0; i < 10000; ++i)
	{
    
    
		TestFunc2(a);
	}
	long end2 = clock();

	// 分别计算两个函数运行结束后的时间
	cout << "TestFunc1(A)-time:" << end1 - begin1 << endl;
	cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;
}
int main()
{
    
    
	TestRefAndValue();
	return 0;
}

insert image description here
About 3 times, of course yours may be different from mine, but it should be that the reference return is faster!

6. Analysis of the underlying principles of citations

Before analyzing the underlying principles of references, we still emphasize that in terms of C++ grammar, we still believe that references do not open space! ! In the grammatical concept, a reference is an alias, which has no independent space and shares the same space with its referenced entity.

With such common knowledge, let's look at such a piece of code

#include<iostream>
using namespace std;
int main()
{
    
    
	int a = 10;
	int& ra = a;
	ra = 20;

	int* pa = &a;
	*pa = 30;
	return 0;
}

The code is very simple, let's look at its assembly code
insert image description here

Between every two C++ codes is: the assembly implementation of the previous C++ code, which is also the underlying principle of this C++ statement.

①0Ah (h stands for hexadecimal, so 0Ah is 10) This assembly means assigning a value of 10.
mov is to move
dword double word (x32 is four bytes, x64 is eight bytes)
ptr pointer abbreviation means the data in the pointer
[] is an address value,

lea is to take the address, rax is a register, this assembly means assigning the address of a to rax

qword double word (x32 is four bytes, x64 is eight bytes), this sentence is to give the value of rax to ra

The address of a is placed in rax, and rax can be assigned to ra, indicating that ra is a pointer,
and ra is a reference in C++, indicating that the reference is implemented with a pointer, and its size is the size of a pointer. That is to say, the reference actually opens up a new space.
But we still think that the reference does not open a space in the grammatical sense, and shares the same space with its referenced entity.

④⑤ These two compilations mean that first assign ra to rax, and then assign 14h(20) to the space pointed to by the address in rax.

⑥⑦⑧⑨This is the operation on pointers. Compared with the operation on references in ②③④⑤, you will find that they are actually the same.

In short, the underlying implementation of references is pointers, and references actually need to open up new space, but we generally say that references cannot open up space from the perspective of C++ syntax.

7. Summary

Differences between references and pointers:

  1. A reference conceptually defines an alias for a variable, and a pointer stores the address of a variable.
  2. References must be initialized when they are defined , pointers are not required
  3. After the reference refers to an entity during initialization, it
    refer to other entities, and the pointer can point to any entity of the same type at any time
  4. There are no NULL references , but there are NULL pointers
  5. The meaning is different in sizeof: the reference result is the size of the reference type, but the pointer is always the number of bytes occupied by the address space (4 bytes under the 32-bit platform)
  6. The self-increment of the reference means that the referenced entity increases by 1, and the self-increment of the pointer means that the pointer offsets the size of a type backward
  7. Multi-level pointers, but no multi-level references
  8. There are different ways to access entities, the pointer needs to be explicitly dereferenced, and the reference compiler handles it by itself
  9. References are relatively safer to use than pointers

Guess you like

Origin blog.csdn.net/qq_65207641/article/details/128866787