[C++] La introducción detallada a las referencias de C++ lo lleva de una comprensión superficial a una comprensión profunda de las referencias


1. El concepto de referencia

Referencia es dar un alias a una variable existente. El compilador no abrirá espacio de memoria para la variable de referencia. Comparte el mismo espacio de memoria con la variable a la que se refiere. La operación en el objeto de referencia es exactamente la misma que la directa operación sobre la variable.

Su formato de definición es: tipo y nombre de variable de referencia = nombre de variable definido.
Por ejemplo

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

código de ejemplo

#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;
}

inserte la descripción de la imagen aquí

Dos, las características de la referencia.

  1. Las referencias deben inicializarse cuando se definen
  2. Una variable puede tener varias referencias.
  3. Una vez que una referencia se refiere a una entidad, no puede referirse a otra entidad
#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;
}

inserte la descripción de la imagen aquí

3. Referencias especiales - referencias comunes

El núcleo de la comprensión de las referencias constantes es: la autoridad de los punteros y las referencias solo se puede reducir o mantener, pero no aumentar.

1. Referencias constantes a 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. Referencias constantes a constantes

Nota: Las variables temporales son constantes. Las variables temporales se generarán durante el valor de retorno de la función y el proceso de conversión forzada de tipos, por lo que también son constantes.

#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. Escenarios de uso referenciados

1. Como parámetro de función

#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;
}

inserte la descripción de la imagen aquí
引用传参Si el parámetro formal es un tipo de referencia, el parámetro formal es un alias del parámetro actual, por lo que la modificación equivale a modificar el valor de la variable directamente en función del parámetro actual.

2. Hacer el valor de retorno de la función

Para descubrir cómo usar las referencias como el valor de retorno de una función, primero tenemos una comprensión más clara del proceso de retorno de la función.

Primer vistazo a tal pieza de código

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

Cuando el código se ejecuta realmente, primero se creará el marco de pila de la función main (). Cuando se ejecuta la primera oración del código, se encuentra una llamada de función y luego se crea el marco de pila de la función Count (). , y el código se ejecutará dentro de la función Count().
inserte la descripción de la imagen aquí

Después de ejecutar el código, el marco de pila de la función Count() se destruirá y luego volverá al marco de pila de la función main(). Sin embargo, cuando la variable n se destruya en el marco de pila de Count() función, n también se destruirá, por lo que no hay forma de pasar n a ret en la función main(), por lo que se necesita una variable temporal para guardar el valor del valor devuelto n.

Después de que el marco de pila de la función Count() se destruye y vuelve al marco de pila de la función main(), la variable temporal copia el valor de n a ret, que es el proceso de retorno de la función.
inserte la descripción de la imagen aquí

Las variables temporales aquí se dividen en dos tipos

  • Cuando el valor devuelto es una variable que ocupa menos espacio, los registros se suelen utilizar como variables temporales
  • Cuando el valor devuelto es una variable de estructura que ocupa un gran espacio, generalmente se abre de antemano en el marco de la pila de la función que llama a la función Count().

Puede pensar que la razón por la cual la función regresa así es porque la variable n es una variable local, y no hay forma de que la variable local exista después de salir del ámbito. Si n se convierte en una variable global, ¿no habrá necesidad ? para una variable temporal?

La respuesta es que las variables temporales aún se crearán cuando la función regrese, porque el compilador es "tonto" al hacer el retorno de la función, ¿no es una pérdida? ¿Hay alguna manera de resolver este problema? La respuesta es sí: la referencia es el valor de retorno de la función .

Dado que la variable de referencia está en el mismo espacio de memoria que la variable original, devolver la variable de referencia es equivalente a devolver directamente el valor de retorno de nuestra función, por lo que la variable temporal puede eliminarse. Pero al usar el retorno de referencia, debe prestar atención al valor de retorno que aún debe existir después de salir de la función, de lo contrario, es un acceso ilegal

Ejemplo de código 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;
}

inserte la descripción de la imagen aquí

Como usamos una referencia como valor de retorno, el valor de retorno es un alias de una variable, entonces podemos operar sobre este alias, es decir, podemos modificar el valor de retorno Código Ejemplo
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;
}

inserte la descripción de la imagen aquí

Se puede ver que la referencia como valor de retorno de la función tiene dos ventajas y un punto de atención:

  1. El uso de referencias como el valor de retorno de la función puede reducir la copia del valor de retorno
  2. Con devolución por referencia, la persona que llama puede modificar el valor del objeto devuelto
  3. Puntos a tener en cuenta: al devolver por referencia, el objeto devuelto debe ser estático o global, pertenecer al marco de pila anterior o solicitar espacio en el montón.

A través de la explicación anterior, creo que tiene una buena comprensión del valor de retorno de la referencia, así que veamos el siguiente fragmento de código, ¿cuál es el resultado del siguiente código? ¿Por qué?

#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;
}


La respuesta es: el resultado es aleatorio, y el código tiene errores, y el resultado específico no está definido. Si obtiene la respuesta correcta, significa que ha aprendido muy bien. Si obtiene la respuesta incorrecta, solo escúcheme lentamente.


Este es en realidad el problema del acceso ilegal . Cuando el código se ejecuta realmente, el marco de pila de la función main() se creará primero. Cuando se ejecuta la primera oración del código, se encuentra una llamada de función y luego el marco de pila de la función Count() es creada e ingresada La función Count() ejecuta el código internamente.
inserte la descripción de la imagen aquí
Después de ejecutar el código, el marco de pila de la función Count() se destruirá y luego volverá al marco de pila de la función main(). Sin embargo, cuando la variable n se destruya en el marco de pila de Count() función, n también se destruirá, n Después de que el sistema operativo reclame este espacio, el sistema operativo puede borrar o no el valor en el espacio, es decir, el espacio de n puede ser 3 o un valor aleatorio.

Y como usamos la referencia como valor de retorno, ret es un alias del espacio original de n.Acceder a ret equivale a acceder a la n destruida (esto ya es un acceso ilegal), pero el espacio de n puede ser 3 o puede ser un valor aleatorio, por lo que el resultado de salida es incierto.
inserte la descripción de la imagen aquí

5. Comparación de la eficiencia de paso por valor y paso por referencia

Los valores se utilizan como parámetros o tipos de valores devueltos.Durante el paso y la devolución de parámetros, la función no pasa directamente los parámetros reales ni devuelve la variable en sí, sino que pasa los parámetros reales o devuelve una copia temporal de la variable, por lo que los valores se utilizan como parámetros O el tipo de valor de retorno es muy ineficiente, especialmente cuando el tipo de parámetro o valor de retorno es muy grande, la eficiencia es aún menor.

1. Comparación de la eficiencia de pasar valores y pasar referencias al pasar parámetros

#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;
}

inserte la descripción de la imagen aquí
Hay unas nueve veces más, por supuesto que el tuyo puede ser diferente al mío, ¡pero debería ser más rápido para pasar referencias!

2. Comparación del valor de paso y la eficiencia de referencia de paso al regresar

#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;
}

inserte la descripción de la imagen aquí
Aproximadamente 3 veces, por supuesto, el tuyo puede ser diferente al mío, ¡pero debería ser que el retorno de referencia es más rápido!

6. Análisis de los principios subyacentes de las citas

Antes de analizar los principios subyacentes de las referencias, aún enfatizamos que en términos de gramática de C++, ¡aún creemos que las referencias no abren espacios! ! En el concepto gramatical, una referencia es un alias, que no tiene espacio independiente y comparte el mismo espacio con su entidad referenciada.

Con tal conocimiento común, echemos un vistazo a tal pieza de código.

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

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

El código es muy simple, veamos su código ensamblador
inserte la descripción de la imagen aquí

Entre cada dos códigos de C++ está: la implementación de ensamblaje del código de C++ anterior, que también es el principio subyacente de esta declaración de C++.

①0Ah (h significa hexadecimal, por lo que 0Ah es 10) Este conjunto significa asignar un valor de 10.
mov es para mover
dword palabra doble (x32 son cuatro bytes, x64 son ocho bytes) la abreviatura del puntero
ptr significa que los datos en el puntero
[] son ​​un valor de dirección,

lea es tomar la dirección, rax es un registro, este conjunto significa asignar la dirección de a a rax

qword palabra doble (x32 es de cuatro bytes, x64 es de ocho bytes), esta oración es para dar el valor de rax a ra

La dirección de a se coloca en rax, y rax se puede asignar a ra, lo que indica que ra es un puntero,
y ra es una referencia en C++, lo que indica que la referencia se implementa con un puntero, y su tamaño es el tamaño de un puntero. Es decir, la referencia en realidad abre un nuevo espacio.
Pero seguimos pensando que la referencia no abre un espacio en el sentido gramatical, y comparte el mismo espacio con su entidad referenciada.

④⑤ Estas dos compilaciones significan que primero asigne ra a rax y luego asigne 14h(20) al espacio señalado por la dirección en rax.

⑥⑦⑧⑨Esta es la operación con punteros. Comparada con la operación con referencias en ②③④⑤, encontrará que en realidad son lo mismo.

En resumen, la implementación subyacente de las referencias son los punteros, y las referencias en realidad necesitan abrir un nuevo espacio, pero generalmente decimos que las referencias no pueden abrir espacio desde la perspectiva de la sintaxis de C++.

7. Resumen

Diferencias entre referencias y punteros:

  1. Una referencia define conceptualmente un alias para una variable y un puntero almacena la dirección de una variable.
  2. Las referencias deben inicializarse cuando se definen , no se requieren punteros
  3. Después de que la referencia hace referencia a una entidad durante la inicialización, no
    hacer referencia a otras entidades y el puntero puede apuntar a cualquier entidad del mismo tipo en cualquier momento.
  4. No hay referencias NULL , pero hay punteros NULL
  5. El significado es diferente en sizeof: el resultado de referencia es el tamaño del tipo de referencia, pero el puntero es siempre el número de bytes ocupados por el espacio de direcciones (4 bytes en la plataforma de 32 bits)
  6. El autoincremento de la referencia significa que la entidad a la que se hace referencia aumenta en 1, y el autoincremento del puntero significa que el puntero compensa el tamaño de un tipo hacia atrás.
  7. Punteros de varios niveles, pero sin referencias de varios niveles
  8. Hay diferentes formas de acceder a las entidades, el puntero debe ser desreferenciado explícitamente y el compilador de referencia lo maneja solo
  9. Las referencias son relativamente más seguras de usar que los punteros.

Supongo que te gusta

Origin blog.csdn.net/qq_65207641/article/details/128866787
Recomendado
Clasificación