[C++] Clases y objetos (4) Algunas optimizaciones del compilador cuando la lista de inicialización, miembro estático, amigo, clase interna, objeto anónimo, copia el objeto.

prefacio

Este capitulo es el capitulo final de nuestras clases y objetos en C++, pero la dificultad de este capitulo no es demasiado grande, son todos los rincones y rincones de la clase, es suficiente para memorizar y entender, creo que despues de tal mucho tiempo de aprendizaje de clases y objetos, comprenderá que la orientación a objetos también tiene una comprensión más profunda.Finalmente, después de haber aprendido algunos puntos de conocimiento en las esquinas, hablemos sobre la comprensión de clases y objetos.


Primero, habla sobre el constructor.

Aunque hemos presentado el constructor en detalle en Clases y Objetos (2) , todavía tenemos que seguir hablando del constructor aquí, porque el constructor es demasiado complicado (el padre de C++ no lo diseñó bien al principio, y era repetido muchas veces más tarde) La aplicación de parches hace que el constructor sea bastante complicado), pero esta vez hablar sobre el constructor no es tan difícil como antes. Esta vez estamos hablando de un conocimiento fragmentado del constructor. Miremos el código primero y pensemos de nuevo:

#include<iostream>
using namespace std;
class Date
{
    
    
public:
	Date(int year=10, int month=10, int day=10)
	{
    
    
		_year = year;
		_month = month;
		_day = day;
		_a = 10; //编译失败,const修饰的变量不能改变
	}
private:
	int _year;
	int _month;
	int _day;
	const int _a;//编译失败,const 变量未初始化
};

Al crear un objeto, el compilador llama al constructor para dar a cada variable miembro en el objeto un valor inicial apropiado. Aunque el objeto ya tiene un valor inicial después de llamar al constructor anterior, no se puede llamar una variable miembro en el objeto. Para la inicialización de variables, las declaraciones en el cuerpo del constructor solo pueden llamarse asignación de valor inicial, no inicialización . Porque la inicialización solo se puede inicializar una vez, y el cuerpo del constructor se puede asignar varias veces.

Puede pensar que este tipo de detalle es demasiado quisquilloso, pero no es el caso. Algunas variables son muy importantes para la inicialización. Por ejemplo, las variables constmodificadas solo se pueden inicializar una vez y no se pueden asignar, lo que requiere que distingamos cuidadosamente entre la inicialización y la asignación.

Agreguemos una constvariable modificada a la variable miembro del código anterior y veamos qué sucede.
inserte la descripción de la imagen aquí

constEncontramos que la compilación falló ¿Es tan difícil que esas variables modificadas no se puedan definir en la clase ? No, de hecho, podemos asignar valores a las variables de acuerdo con los valores predeterminados mencionados en Clases y objetos (2)const , pero los valores predeterminados solo aparecieron después de la actualización de C++, entonces, ¿cómo solucionamos el problema en C++ antes de la actualización de C++?

¡La respuesta son las listas de inicialización!

1. Lista de inicialización

En primer lugar, cuando pensamos en la definición de un objeto problemático, ¿dónde están definidos los miembros del objeto? Resolver este problema resuelve el constproblema anterior de que la variable modificada no se puede definir, porque constla variable modificada debe inicializarse cuando se define, es decir, la definición y la inicialización están juntas.
La respuesta es que los miembros del objeto están específicamente definidos en la lista de inicialización , así que entendamos juntos la lista de inicialización.

una definicion

Lista de inicialización: comienza con dos puntos , seguido de una lista de miembros de datos separados por comas , cada "variable de miembro" seguida de un valor inicial o una expresión entre paréntesis.
Código de ejemplo:

#include<iostream>
using namespace std;
class Date
{
    
    
public:
	Date(int year , int month , int day)
		:_year(year)//初始化列表,也是成员变量定义的地方,这里才是真正的初始化
		,_month(month)
		,_day(day)
		,_a(10)
	{
    
    
		
	}
private:
	int _year;
	int _month;
	int _day;
	const int _a;
};
int main()
{
    
    
	Date d1(10,10,10);
	return 0;
}

inserte la descripción de la imagen aquí

b.Características

① Cada variable miembro solo puede aparecer una vez en la lista de inicialización (la inicialización solo se puede inicializar una vez)

inserte la descripción de la imagen aquí

②La clase contiene los siguientes miembros, que deben colocarse en la lista de inicialización para la inicialización:

  • Variables miembro de referencia (deben inicializarse cuando se definen)
  • variables miembro const (deben inicializarse cuando se definen)
  • Cuando un miembro de un tipo personalizado y la clase no tienen un constructor predeterminado (ningún constructor predeterminado significa que los parámetros deben pasarse durante la inicialización)
  1. Cuando observamos por primera vez los miembros de tipos personalizados y la clase no tiene un constructor predeterminado , no inicializamos en la lista de inicialización

inserte la descripción de la imagen aquí


2. Cuando observamos el miembro de tipo personalizado y la clase tiene un constructor predeterminado , no lo inicializamos en la lista de inicialización.
inserte la descripción de la imagen aquí
La conclusión es: en la lista de inicialización, si no escribimos nada, el compilador no se ocupará del tipo incorporado y llamará a su construcción predeterminada para el tipo personalizado.

Sugerencia: intente usar la inicialización de la lista de inicialización, porque ya sea que use la lista de inicialización o no, para las variables miembro de tipo personalizado, primero debe usar la inicialización de la lista de inicialización.

③El orden en que se declaran las variables miembro en la clase es el orden en que se inicializan en la lista de inicialización, independientemente de su orden en la lista de inicialización

Considere el resultado del siguiente código:

#include<iostream>
using namespace std;
class A
{
    
    
public:
	A(int a)
		:_a1(a)
		, _a2(_a1)
	{
    
    }
	void Print() {
    
    
		cout << _a1 << " " << _a2 << endl;
	}
private:
	int _a2;
	int _a1;
};
int main() {
    
    
	A aa(1);
	aa.Print();
}

inserte la descripción de la imagen aquí
El resultado puede sorprenderte, analicémoslo detenidamente.

inserte la descripción de la imagen aquí

2. La palabra clave explícita

Los constructores no solo pueden construir e inicializar objetos, sino que también tienen la función de conversión de tipos para un solo parámetro o un constructor con valores predeterminados, excepto el primer parámetro, que no tiene un valor predeterminado.
Nota: De acuerdo con la definición, el constructor aquí debe pasar parámetros

Veamos el siguiente fragmento de código.

#include<iostream>
using namespace std;
class A
{
    
    
public:
	A(int a)
		:_a1(a)
		, _a2(_a1)
	{
    
    }
private:
	int _a1;
	int _a2;
};
int main() {
    
    
	A aa1(1);  
	A aa2 = 1;//这里不是拷贝构造(拷贝构造是用一个对象初始化一个对象),
	         //这里也不是赋值重载(赋值重载是用一个已经初始化的对象赋值给另一个已经初始化过的对象)
	         //是否可以编译通过?
	return 0;
}

La respuesta es que se puede compilar y podemos ver su valor monitoreando

inserte la descripción de la imagen aquí
Entonces, ¿por qué se puede compilar aquí? ¡ La respuesta es conversión de tipo implícita !
Cuando explicamos las referencias, una vez dijimos que la razón por la cual el siguiente código puede pasar es que i se convertirá implícitamente en una variable temporal de tipo doble, y la variable temporal es constante, ¡y d se refiere a esta variable temporal de tipo doble!

int i = 10;
const double&d = i;

Lo mismo es cierto aquí, el proceso específico es el siguiente:
inserte la descripción de la imagen aquí
después de comprender el principio de conversión y asignación de tipo de parámetro único, veamos cómo hacer múltiples parámetros.


#include<iostream>
using namespace std;
class A
{
    
    
public:
	A(int a,int b)
		:_a1(a)
		, _a2(b)
	{
    
    }
private:
	int _a1;
	int _a2;
};
int main() {
    
    
	A aa1(1,2);  //调用构造函数
	A aa2 = {
    
    1,2}; //多个参数类型转化
	return 0;
}

Aquí podemos ver que múltiples parámetros son los que { }usamos para dar valores
inserte la descripción de la imagen aquí

Tenga en cuenta que la conversión de un solo parámetro es compatible con C ++ 98, y la conversión de múltiples parámetros es compatible con C ++ 11

Pero a veces no queremos que esto suceda, lo que requiere que modifiquemos el constructor con explícito, lo que prohibirá la conversión implícita del constructor.

Agreguemos el mismo código.Finalmente hemos terminado de hablar sobre el constructorexplicit
inserte la descripción de la imagen aquí
aquí ... ¡necesitamos revisar mucho más tarde!

Dos miembros estáticos

1. Definición

Los miembros de clase declarados como estáticos se denominan miembros estáticos de la clase , las variables miembro modificadas con estática se denominan variables miembro estáticas y las funciones miembro modificadas con estática se denominan funciones miembro estáticas .

¡Las variables miembro estáticas deben inicializarse fuera de la clase ! Porque las listas de inicializadores solo pueden inicializar miembros no estáticos

Código de ejemplo:

#include<iostream>
using namespace std;
class A
{
    
    
public:
	A()
		:_a(10)
		,_b('a')
	{
    
    

	}
private:
	int _a;
	char _b;
	static int c;//这里不能给缺省值,缺省值是给初始化列表使用的,
	             //初始化列表只能初始化非静态成员
};
int A::c = 10;
int main()
{
    
    
	A aa;
	return 0;
}

inserte la descripción de la imagen aquí

2. Características

  1. Los miembros estáticos son compartidos por todos los objetos de clase , no pertenecen a un objeto específico y se almacenan en el área estática
  2. Las variables miembro estáticas deben definirse fuera de la clase , la palabra clave estática no se agrega al definir y solo se declara en la clase
  3. Se puede acceder a los miembros estáticos de clase por nombre de clase :: miembro u objeto estático miembro estático
  4. Los miembros estáticos también son miembros de una clase, sujetos a calificadores de acceso público, protegido y privado.
  5. Las funciones de miembros estáticos no tienen un puntero oculto y no pueden acceder a ningún miembro no estático

inserte la descripción de la imagen aquí

3. Practica

Hablando de eso, veamos una pregunta de la entrevista:
implemente una clase y calcule cuántos objetos de clase se crean en el programa.
Primero analizamos el problema. Para contar cuántos objetos de clase se crean, debemos observar las características unificadas de los objetos de clase. Sabemos que todos los objetos creados deben pasar por el constructor (la construcción de copias también es una sobrecarga del constructor) , por lo que solo es necesario definir una variable estadística de un miembro estático (observe la característica 1: los miembros estáticos son compartidos por todos los objetos de clase y no pertenecen a un objeto específico ), y cada vez que se crea un objeto, simplemente agregamos uno a esta variable estadística estática!

Código de ejemplo:

#include<iostream>
using std::cout;
using std::endl;
class A
{
    
    
public:
	A()
		:_a(10)
	{
    
    
		_count++;
	}
	A(const A& tmp)
	{
    
    
		_a = tmp._a;
		_count++;
	}
private:
	static int _count;
	int _a;
};
int A::_count = 0;
int main()
{
    
    
	A aa1;
	A aa2;
	A aa3(aa1);
	A aa4(aa2);
	return 0;
}

inserte la descripción de la imagen aquí
Pensemos en dos preguntas más:

  1. ¿Puede una función miembro estática llamar a una función miembro no estática?
    ¡La respuesta es no! Las funciones miembro estáticas no tienen thispunteros. Llamar funciones miembro no estáticas requiere pasar thispunteros. Las funciones miembro estáticas no pueden pasar punteros a funciones miembro no estáticas this, por lo que no se pueden llamar.
  2. ¿Puede una función miembro no estática llamar a una función miembro estática de una clase?
    ¡La respuesta es sí! Las funciones miembro no estáticas tienen thispunteros. Llamar funciones miembro estáticas no necesita pasar thispunteros. Las funciones miembro no estáticas pueden pasar punteros a funciones miembro estáticas this, para que puedan ser llamadas.

3. Amigos

Sabemos que generalmente es imposible acceder a miembros privados en una clase fuera de la clase, pero cuando queremos acceder a miembros privados en una clase fuera de la clase, necesitamos algunos medios especiales, como amigo

1. Introducción

Los amigos brindan una forma de salir del encapsulamiento y, a veces, conveniencia. Pero los amigos aumentarán el grado de acoplamiento y destruirán la encapsulación, por lo que los amigos no deben usarse con más frecuencia.

Los amigos se dividen en: función de amigo y clase de amigo

2. Función de amigo

Una función amiga puede acceder directamente a los miembros privados de una clase. Es una función ordinaria definida fuera de la clase y no pertenece a ninguna clase, pero debe declararse dentro de la clase y deben agregarse palabras clave al declarar friend.

#include<iostream>
using namespace std;
class Date
{
    
    
	friend ostream& operator<<(ostream& _cout, const Date& d);//友元函数的声明
	friend istream& operator>>(istream& _cin, Date& d);//友元函数的声明
public:
	Date(int year = 1900, int month = 1, int day = 1)
		: _year(year)
		, _month(month)
		, _day(day)
	{
    
    }
private:
	int _year;
	int _month;
	int _day;
};
//运算符重载
ostream& operator<<(ostream& _cout, const Date& d)
{
    
    
	_cout << d._year << "-" << d._month << "-" << d._day;//不使用友元函数,无法访问这里的成员变量。
	return _cout;
}
istream& operator>>(istream& _cin, Date& d)
{
    
    
	_cin >> d._year;		//不使用友元函数,无法访问这里的成员变量。
	_cin >> d._month;		//不使用友元函数,无法访问这里的成员变量。
	_cin >> d._day;			//不使用友元函数,无法访问这里的成员变量。
	return _cin;
}
int main()
{
    
    
	Date d;
	cin >> d;
	cout << d << endl;
	return 0;
}

a.Características

①Las funciones amigas pueden acceder a miembros privados y protegidos de la clase , pero no a las funciones miembro de la clase
②Las funciones amigas no pueden modificarse con const (las funciones amigas no tienen thispunteros)
③Las funciones amigas pueden declararse en cualquier parte de la definición de la clase y no se ven afectadas por la clase Restricciones de calificadores de acceso
④ Una función puede ser una función amiga de varias clases
⑤ El principio de llamar a una función amiga es el mismo que el de una función normal

3. Clase de amigos

Todas las funciones miembro de una clase amiga pueden ser funciones amigas de otra clase y pueden acceder a miembros no públicos de otra clase.

a.Características

  • La relación de amistad es unidireccional y no intercambiable.
    Por ejemplo, la siguiente clase de hora y clase de fecha, declara la clase de fecha como su clase amiga en la clase de hora, luego puede acceder directamente a las variables de miembros privados de la clase de hora en la clase de fecha, pero desea acceder a los miembros privados de la clase de fecha en las variables de clase de tiempo no lo hacen.
  • La relación de amistad no se puede transmitir
    Si C es amigo de B y B es amigo de A, no se puede explicar que C sea amigo de A.
  • Las relaciones de amistad no se pueden heredar.
//友元类
class Time
{
    
    
	friend class Date; // 声明日期类为时间类的友元类,则在日期类中就直接访问Time类中的私有成员变量
public:
	Time(int hour = 0, int minute = 0, int second = 0)
		: _hour(hour)
		, _minute(minute)
		, _second(second)
	{
    
    }
private:
	int _hour;
	int _minute;
	int _second;
};
class Date
{
    
    
public:
	Date(int year = 1900, int month = 1, int day = 1)
		: _year(year)
		, _month(month)
		, _day(day)
	{
    
    }
	void SetTimeOfDate(int hour, int minute, int second)
	{
    
    
		// 直接访问时间类私有的成员变量
		_t._hour = hour;
		_t._minute = minute;
		_t._second = second;
	}
private:
	int _year;
	int _month;
	int _day;
	Time _t;
};

Cuarto, la clase interna

1. Definición

Concepto: si una clase se define dentro de otra clase, la clase interna se denomina clase interna . La clase interna es una clase independiente, no pertenece a la clase externa y mucho menos accede a los miembros de la clase interna a través del objeto de la clase externa. Las clases externas no tienen ningún acceso privilegiado a las clases internas .

Nota: La clase interna es la clase amiga de la clase externa. Consulte la definición de la clase amiga. La clase interna puede acceder a todos los miembros de la clase externa a través de los parámetros de objeto de la clase externa. Pero la clase exterior no es amiga de la clase interior.

2. Características:

  1. Las clases internas se pueden definir como públicas, protegidas y privadas en las clases externas y se ven afectadas por los calificadores de acceso .
  2. Tenga en cuenta que la clase interna puede acceder directamente a los miembros estáticos en la clase externa sin el nombre de clase/objeto de la clase externa.
  3. sizeof (clase externa) = clase externa, no tiene nada que ver con la clase interna .

código de ejemplo

//内部类
#include<iostream>
using namespace std;
class A
{
    
    
private:
	static int k;
	int h;
public:
	class B // B天生就是A的友元
	{
    
    
	public:
		void foo(const A& a)
		{
    
    
			cout << k << endl;//OK
			cout << a.h << endl;//OK
		}
	};
};
int A::k = 1;
int main()
{
    
    
	A::B b;
	b.foo(A());
	return 0;
}

5. Objetos anónimos

1. Definición

Un objeto anónimo significa que definimos un objeto pero no tiene nombre (también hay una estructura anónima en lenguaje C). El escenario de uso de un objeto anónimo suele ser: se necesita una variable temporalmente, pero no queremos que lo haga. jugar un papel importante.
Formato definido: nombre de clase ()

//例如 A是一个类
A();//定义一个匿名对象

2. Características

El ciclo de vida de un objeto anónimo es solo la línea en la que se encuentra, y llamará automáticamente al destructor después de la siguiente línea.

//匿名对象
#include<iostream>
using namespace std;
class A
{
    
    
public:
	A(int a = 0)
		:_a(a)
	{
    
    
		cout << "A(int a)" << endl;
	}
	~A()
	{
    
    
		cout << "~A()" << endl;
	}
private:
	int _a;
};
class Solution {
    
    
public:
	int Sum_Solution(int n) {
    
    
		//...
		return n;
	}
};
int main()
{
    
    
	A aa1;
	// 不能这么定义对象,因为编译器无法识别下面是一个函数声明,还是对象定义
	//A aa1();
	
	// 但是我们可以这么定义匿名对象,匿名对象的特点不用取名字,
	// 但是他的生命周期只有这一行,我们可以看到下一行他就会自动调用析构函数
	A();
	A aa2(2);
	// 匿名对象在这样场景下就很好用,如果不定义匿名对象我们就要创建一个Solution这样的对象。
	Solution().Sum_Solution(10);
	return 0;
}

6. Algunas optimizaciones del compilador al copiar objetos

Con el desarrollo y la iteración del compilador, el compilador actual es muy inteligente. Generalmente, en el proceso de pasar parámetros y devolver valores en algunos compiladores no demasiado antiguos, el compilador generalmente hará algunas optimizaciones para reducir la copia de objetos, esto es sigue siendo muy útil en algunos escenarios.

¡Echemos un vistazo juntos!

//拷贝对象时的一些编译器优化
#include<iostream>
using namespace std;
class A
{
    
    
public:
	A(int a = 0)
		:_a(a)
	{
    
    
		cout << "A(int a)" << endl;
	}

	A(const A& aa)
		:_a(aa._a)
	{
    
    
		cout << "A(const A& aa)" << endl;
	}

	A& operator=(const A& aa)
	{
    
    
		cout << "A& operator=(const A& aa)" << endl;
		if (this != &aa)
		{
    
    
			_a = aa._a;
		}
		return *this;
	}

	~A()
	{
    
    
		cout << "~A()" << endl;
	}

private:
	int _a;
};

void f1(A aa)
{
    
    

}
A f2()
{
    
    
	A aa;
	return aa;
}
A f3()
{
    
    
	return A();
}

int main()
{
    
    
	// 传值传参 不优化
	A aa1;		
	f1(aa1);
	cout << "----------------------------------------------------" << endl;

	// 传值返回
	f2();    //不优化  调用一个构造函数+一个拷贝构造   因为是两行代码编译器不敢擅自优化
	cout << "----------------------------------------------------" << endl;

	// 隐式类型,构造+拷贝构造->优化为直接构造
	f1(1);
	// 一个表达式中,构造+连续拷贝构造->优化为一个构造
	f1(A(2));
	cout << "----------------------------------------------------" << endl;

	// 一个表达式中,连续拷贝构造+拷贝构造->优化一个拷贝构造
	A aa2 = f2();
	cout << "----------------------------------------------------" << endl;

	// 一个表达式中,连续拷贝构造+赋值重载->无法优化
	aa1 = f2();
	cout << "----------------------------------------------------" << endl;

	f3();	//一行中构造+拷贝构造 ->优化为一个构造
	A aa3 = f3();	//一行中构造+拷贝构造+拷贝构造 ->优化为一个构造
	return 0;
}

inserte la descripción de la imagen aquí

7. Comprende de nuevo las clases y los objetos

Las computadoras físicas en la vida real no saben, las computadoras solo conocen datos en formato binario. Si desea que la computadora reconozca entidades en la vida real, los usuarios deben describir las entidades a través de algún lenguaje orientado a objetos y luego escribir programas para crear objetos antes de que la computadora pueda reconocerlos. Por ejemplo, si desea que la computadora reconozca la lavadora, necesita:

  1. El usuario primero necesita abstraer la realidad de la entidad de la lavadora, es decir, comprender la lavadora al nivel del pensamiento humano, qué atributos tiene la lavadora y qué funciones tiene, es decir, un proceso de cognición abstracta. de la lavadora
  2. Después de 1, las personas tienen una comprensión clara de la lavadora en sus mentes, pero la computadora aún no está clara en este momento. Para que la computadora reconozca la lavadora en la imaginación de las personas, las personas necesitan pasar algún tipo de lenguaje orientado a objetos ( Por ejemplo: C++, Java, Python, etc.) describa la lavadora con clases e introdúzcala en la computadora
  3. Después de 2, hay una clase de lavadora en la computadora, pero la clase de lavadora solo describe el objeto de la lavadora desde la perspectiva de la computadora. A través de la clase de lavadora, se pueden instanciar objetos de lavadora específicos. En este momento, el la computadora puede lavar la lavadora ¿Qué es?
  4. El usuario puede usar el objeto de la lavadora en la computadora para simular la entidad de la lavadora en la realidad.

En la etapa de clase y objeto, todos deben darse cuenta de que una clase describe un cierto tipo de entidad (objeto), describe qué atributos y métodos tiene el objeto y forma un nuevo tipo personalizado una vez que se completa la descripción, se puede instanciar el objeto específico solo con este tipo personalizado.

inserte la descripción de la imagen aquí

Supongo que te gusta

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