[C++] Clases y objetos (2) Función de copia de constructor destructor

prefacio

En el capítulo anterior, presentamos el conocimiento básico de las clases y los objetos, incluido el concepto y la definición de las clases, así como los calificadores de acceso a la clase, la creación de instancias de la clase, el cálculo del tamaño de la clase y el indicador que debe pasar el lenguaje C (Nosotros no (no es necesario pasarlo en C++, el compilador lo implementará automáticamente por nosotros)

pero esto no es suficiente. Las clases son lo más importante en los lenguajes de programación orientados a objetos. Debemos seguir entendiéndolos en profundidad. Los primeros tres de las seis funciones miembro predeterminadas de .


1. Las 6 funciones miembro predeterminadas de la clase

Si no hay miembros en una clase, simplemente se denomina clase vacía.
¿Realmente no hay nada en la clase vacía? No, cuando alguna clase no escribe nada, el compilador generará automáticamente las siguientes 6 funciones miembro predeterminadas.

Función miembro predeterminada: la función miembro generada por el compilador sin implementación explícita por parte del usuario se denomina función miembro predeterminada.

clase vacía:

class Date
{
    
    

};

inserte la descripción de la imagen aquí

En segundo lugar, el constructor

1. Introducción

Para el escenario de uso del constructor, primero veamos un código simple:

#include<iostream>
#include<stdlib.h>
using namespace std;
typedef int DataType;
class Stack
{
    
    
public:
	void Init(int _capacity = 4)//缺省参数
	{
    
    
		DataType* tmp = (DataType*)malloc(sizeof(DataType) * _capacity);
		if (nullptr == tmp)
		{
    
    
			perror("malloc fail:");
			exit(-1);
		}
		_a = tmp;
		_Top = 0;
		_capacity = _capacity;
	}
	void Push(int num)
	{
    
    
		//判断是否应该扩容
		if (_Top - 1 == _capacity)
		{
    
    
			_capacity *= 2;
			DataType* tmp = (DataType*)realloc(_a,sizeof(DataType) * _capacity);
			if (nullptr == tmp)
			{
    
    
				perror("malloc fail:");
				exit(-1);
			}
			_a = tmp;
		}
		_a[_Top] = num;
		_Top++;
	}
private:
	DataType* _a;
	int _Top;
	int _capacity;
};
int main()
{
    
    
	Stack s1;
	s1.Push(1);
	s1.Push(2);
	s1.Push(3);
	return 0;
}

Se estrelló después de correr, pensando en por qué?
inserte la descripción de la imagen aquí
La respuesta es: no inicializamos nuestra pila, no le dimos espacio y, naturalmente, ¡no pudimos insertar datos! Esto es normal. Hay muchos ejemplos de este tipo sin inicializar y luego colapsar. Tenemos que inicializar cada vez que queremos usar la pila, lo que nos hace sentir muy incómodos. ¿Podemos inicializarla automáticamente cuando creamos un objeto? La respuesta es sí, ¡ese es el constructor !

2. Concepto

El constructor es una función miembro especial con el mismo nombre que el nombre de la clase , a la que el compilador llama automáticamente al crear un objeto de tipo de clase para garantizar que cada miembro de datos tenga un valor inicial adecuado, y se llama solo una vez en toda la vida. ciclo del objeto .

3. Características

El constructor es una función miembro especial. Cabe señalar que aunque el nombre del constructor se llama construcción, la tarea principal del constructor no es abrir espacio para crear objetos, sino inicializar objetos.
Sus características son las siguientes:

  1. El nombre de la función es el mismo que el nombre de la clase.
  2. No devuelve ningún valor y no nos permite escribir valores de retorno.
  3. El compilador llama automáticamente al constructor correspondiente cuando se crea una instancia del objeto.
  4. Los constructores pueden estar sobrecargados.
  5. Si no hay un constructor definido explícitamente en la clase (en lenguaje sencillo: escriba el constructor usted mismo), el compilador de C++ generará automáticamente un constructor predeterminado sin parámetros, y una vez que el usuario defina explícitamente el compilador, ya no lo generará.

Como dijimos antes, cuando no escribimos nada en la clase, el compilador generará automáticamente un constructor para nosotros. Por supuesto, el constructor generado automáticamente por el compilador puede no ser lo que queremos. También podemos escribir el constructor nosotros mismos. Cuando Si escribimos el constructor nosotros mismos, el compilador ya no generará el constructor por nosotros.

Así que modifiquemos el código anterior.

#include<iostream>
#include<stdlib.h>
using namespace std;
typedef int DataType;
class Stack
{
    
    
public:
	Stack(int capacity = 4)//缺省参数,此类构造函数可以传也可以不传递形参
	{
    
    
		DataType* tmp = (DataType*)malloc(sizeof(DataType) * capacity);
		if (nullptr == tmp)
		{
    
    
			perror("malloc fail:");
			exit(-1);
		}
		_a = tmp;
		_Top = 0;
		_capacity = capacity;
	}
	void Push(int num)
	{
    
    
		//判断是否应该扩容
		if (_Top - 1 == _capacity)
		{
    
    
			_capacity *= 2;
			DataType* tmp = (DataType*)realloc(_a, sizeof(DataType) * _capacity);
			if (nullptr == tmp)
			{
    
    
				perror("malloc fail:");
				exit(-1);
			}
			_a = tmp;
		}
		_a[_Top] = num;
		_Top++;
	}
private:
	DataType* _a;
	int _Top;
	int _capacity;
};
int main()
{
    
    
	Stack s1(20);//此处不是函数调用,而是类的实例化顺便给构造函数传参数
	//Stack s1;    //如果是这样则会采用缺省值,即默认开辟4个int类型的空间大小。
	s1.Push(1);
	s1.Push(2);
	s1.Push(3);
	s1.Push(4);
	s1.Push(5);
	s1.Push(6);
	s1.Push(7);
	return 0;
}


inserte la descripción de la imagen aquí

inserte la descripción de la imagen aquí
El código se ejecuta correctamente, lo que indica que el compilador llama automáticamente Stacka la función que escribimos para nosotros


. Veamos el constructor de una clase.

#include<iostream>
using namespace std;
class Date
{
    
    
public:
	Date()//无参数的构造函数
	{
    
    
	}
	Date(int year, int month, int day)//函数重载
	{
    
    
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
    
    
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
    
    
	Date d1();//报错,错误的调用无参构造函数,会被识别为函数声明!!!
	//warning C4930: “Date d3(void)”: 未调用原型函数(是否是有意用变量定义的?)
	Date d2;//正确的调用无参构造函数
	Date d3(2023,2,10);//正确的调用必须传参的构造函数
};

Nota: si un objeto se crea a través de un constructor sin argumentos, no se requieren paréntesis después del objeto; de lo contrario, se convierte en una declaración de función.

Después de leer los constructores que necesitan pasar parámetros y los constructores que no necesitan pasar parámetros, echemos un vistazo a los constructores implementados por el propio compilador.

//不写构造函数
#include<iostream>
using namespace std;
class Date
{
    
    
public:
	void Print()
	{
    
    
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
    
    
	Date d1;
	d1.Print();
	return 0;
};

inserte la descripción de la imagen aquí
¡La respuesta es muy extraña! ¿No significa que cuando no escribimos un constructor, el compilador generará un constructor por sí mismo? ¿Y el papel del constructor es darle al objeto un valor de inicialización razonable? ¿Por qué el resultado impreso es un valor aleatorio? ¿El constructor generado por el sistema parece inútil? ? ?

La respuesta es: C++ divide los tipos en tipos integrados (tipos básicos) y tipos personalizados . Los tipos integrados son los tipos de datos proporcionados por el lenguaje, como: int/char..., los punteros y los tipos personalizados son los tipos que definimos nosotros mismos usando class/struct/union. Las siguientes reglas se aplican a los constructores predeterminados generados por el compilador:

Para constructores generados por defecto:

  1. Los miembros de tipos integrados no se procesan
  2. Para un miembro de un tipo personalizado, se llamará a su constructor predeterminado.
    (El constructor predeterminado incluye: todos los constructores predeterminados, constructores que no pasan parámetros y constructores predeterminados generados por el sistema)

Para la clase anterior, dado que los miembros de la clase son todos tipos incorporados , el constructor predeterminado generado por el compilador de acuerdo con las reglas anteriores no procesa los tipos incorporados, por lo que lo que vemos sigue siendo un valor aleatorio.

Veamos el siguiente código para ayudarlo a comprender esta regla

#include<iostream>
using namespace std;
class Time
{
    
    
public:
	Time()
	{
    
    
		cout << "Time()" << endl;
		_hour = 0;
		_minute = 0;
		_second = 0;
	}
private:
	int _hour;
	int _minute;
	int _second;
};
class Date
{
    
    
private:
	// 基本类型(内置类型)
	int _year;
	int _month;
	int _day;
	// 自定义类型
	Time _t;
};
int main()
{
    
    
	Date d;
	return 0;
}

inserte la descripción de la imagen aquí

Después de leer este ejemplo, creo que tiene una buena comprensión de esta regla, pero todavía tenemos una pregunta. ¿Qué pasa si solo queremos que el tipo incorporado se inicialice con el tipo personalizado?

6. En C++ 11, se aplicó un parche para solucionar el defecto de no inicialización de los miembros de tipo incorporados, es decir, las variables de miembros de tipo incorporados pueden recibir valores predeterminados cuando se declaran en una clase. (similar a los parámetros predeterminados)

#include<iostream>
using namespace std;
class Time
{
    
    
public:
	Time()
	{
    
    
		cout << "Time()" << endl;
		_hour = 0;
		_minute = 0;
		_second = 0;
	}
private:
	int _hour;
	int _minute;
	int _second;
};
class Date
{
    
    
private:
	// 基本类型(内置类型)
	int _year = 2023;   //给默认值
	int _month = 2;    //给默认值
	int _day = 1;     //给默认值
	// 自定义类型
	Time _t;
};
int main()
{
    
    
	Date d;
	return 0;
}

inserte la descripción de la imagen aquí
7. Tanto el constructor sin parámetros como el constructor predeterminado se denominan constructores predeterminados y solo puede haber un constructor predeterminado.
Nota: Los constructores sin argumentos, los constructores predeterminados completos y los constructores que no escribimos para que los genere el compilador de manera predeterminada pueden considerarse constructores predeterminados.

class Date
{
    
    
public:
Date()
{
    
    
_year = 1900;
_month = 1;
_day = 1;
}
Date(int year = 1900, int month = 1, int day = 1)
{
    
    
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
// 以下测试函数能通过编译吗?
int main()
{
    
    
	Date d1;
	return 0;
}

La respuesta es no, no se pasan parámetros, la función no sabe si llamar a una función sin parámetros o una función con todos los valores predeterminados.

3. Destructor

1. Concepto

A través del estudio del constructor anterior, sabemos cómo surgió un objeto y cómo desapareció ese objeto.
Destructor: Al contrario de la función del constructor, el destructor no completa la destrucción del objeto en sí mismo, y el compilador realiza la destrucción del objeto local. Cuando se destruye el objeto, llamará automáticamente al destructor para completar la limpieza de los recursos en el objeto.

2. Características

Un destructor es una función miembro especial cuyas características son las siguientes:

  1. El nombre del destructor tiene el prefijo ~ antes del nombre de la clase .
  2. Sin parámetros y sin tipo de retorno.
  3. Una clase solo puede tener un destructor. Si no se define explícitamente, el sistema generará automáticamente un destructor predeterminado. Nota: los destructores no se pueden sobrecargar
  4. Cuando finaliza el ciclo de vida del objeto, el sistema de compilación de C++ llama automáticamente al destructor.

Código de ejemplo:

#include<iostream>
using namespace std;
typedef int DataType;
class Stack
{
    
    
public:
	Stack(int capacity = 3)
	{
    
    
		_array = (DataType*)malloc(sizeof(DataType) * capacity);
		if (NULL == _array)
		{
    
    
			perror("malloc申请空间失败!!!");
			return;
		}
		_capacity = capacity;
		_size = 0;
	}
	void Push(DataType data)
	{
    
    
		// CheckCapacity();
		_array[_size] = data;
		_size++;
	}
	// 析构函数
	~Stack()
	{
    
    
		if (_array)
		{
    
    
			free(_array);
			_array = NULL;
			_capacity = 0;
			_size = 0;
		}
		cout << "~Stack()" << endl;
	}
private:
	DataType* _array;
	int _capacity;
	int _size;
};
int main()
{
    
    
	Stack s;
	s.Push(1);
	s.Push(2);
}

inserte la descripción de la imagen aquí
5. Respecto al destructor generado automáticamente por el compilador, ¿se hará algo? En el siguiente programa, veremos que el destructor predeterminado generado por el compilador llama a su destructor para el miembro de tipo personalizado.

#include<iostream>
using namespace std;
class Time
{
    
    
public:
	~Time()
	{
    
    
		cout << "~Time()" << endl;
	}
private:
	int _hour;
	int _minute;
	int _second;
};
class Date
{
    
    
private:
	// 基本类型(内置类型)
	int _year = 1970;
	int _month = 1;
	int _day = 1;
	// 自定义类型
	Time _t;
};
int main()
{
    
    
	Date d;
	return 0;
}

inserte la descripción de la imagen aquí

Después de que se ejecuta el programa, la salida: ~Time()
no crea directamente un objeto de la clase Time en el método principal, ¿por qué llama al destructor de la clase Time al final?
Porque: el objeto de fecha d se crea en el método principal, y d contiene 4 variables miembro, entre las cuales _año, mes y día son miembros de tipo incorporados, y no se requiere la limpieza de recursos al destruir.Finalmente, el sistema puede directamente recuperar su memoria;

y _t es un objeto de la clase Time, por lo que cuando se destruye d, se debe destruir el objeto _t de la clase Time contenido en él, por lo que se debe llamar al destructor de la clase Time.

Sin embargo: el destructor de la clase Time no se puede llamar directamente en la función principal. Lo que realmente debe liberarse es el objeto de la clase Date, por lo que el compilador llamará al destructor de la clase Date. Si Date no se proporciona explícitamente, el compilador le dará a la clase Date Generate un destructor predeterminado, el propósito es llamar al destructor de la clase Time dentro de él, es decir, cuando se destruye el objeto Date, es necesario asegurarse de que cada objeto personalizado dentro de él se pueda destruir correctamente.

La función principal no llama directamente al destructor de la clase Time, pero llama explícitamente al destructor predeterminado generado por el compilador para la clase Date.

Nota: cree un objeto de qué clase para llamar al destructor de esa clase y destruya un objeto de esa clase para llamar al destructor de esa clase

Cuarto, el constructor de copias.

1. Concepto

Cuando usamos clases para crear objetos, inevitablemente se producirá un comportamiento de copia. Por ejemplo, al crear objetos, ¿podemos crear un nuevo objeto que sea igual a un objeto existente?

Copiar constructor: solo hay un único parámetro formal, que es una referencia al objeto de este tipo de clase (generalmente decoración const), que el compilador llama automáticamente cuando crea un nuevo objeto con un objeto de tipo de clase existente.

a. ¿Por qué necesitamos la construcción de copias?

Cuando el compilador C/C++ copia variables, no es un asunto sencillo. Para los tipos integrados, el compilador C/C++ puede copiarlos por sí mismo (copia byte a byte). Para los tipos personalizados, el compilador C/C++ no puede ser copiado, sólo a través de la función de copia.

inserte la descripción de la imagen aquí

(¡La copia de la pila necesita usar la función de copia para una copia profunda!)

2. Características

El constructor de copias también es una función miembro especial con las siguientes características:

  1. El constructor de copia es una forma sobrecargada del constructor.
//拷贝构造函数
#include<iostream>
using namespace std;
class Date
{
    
    
public:
	Date(int year = 1900, int month = 1, int day = 1)
	{
    
    
		_year = year;
		_month = month;
		_day = day;
	}
	//Date(const Date d) // 错误写法:编译报错,会引发无穷递归
	Date(const Date& d) // 正确写法
	{
    
    
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
    
    
	Date d1;
	Date d2(d1);//利用拷贝构造创建一个与d1相同的d2
	//Date d2 = d1;//与上一行的意思一致,要调用拷贝构造
	return 0;
}
  1. El parámetro del constructor de copia es solo uno y debe ser una referencia a un objeto de tipo de clase , y el compilador informará directamente un error si se usa el método de paso de valor, porque causará llamadas recursivas infinitas.

Cuando queremos crear un d2 con los mismos datos que d1, necesitamos llamar a la función de copia.①
Para llamar a la función de copia, necesitamos pasar parámetros porque los parámetros son parámetros formales y los parámetros formales son una copia temporal de los parámetros reales.
②Así que tenemos que volver a llamar a la función de copia. Al llamar a la función de copia, necesitamos pasar parámetros porque los parámetros son parámetros formales y los parámetros formales son una copia temporal de los parámetros reales.
③Entonces tenemos que volver a llamar a la función de copia.Al llamar a la función de copia, necesitamos pasar parámetros porque los parámetros son parámetros formales, y los parámetros formales son una copia temporal de los parámetros reales.
④Así que tenemos que volver a llamar a la función de copia.Al llamar a la función de copia, necesitamos pasar parámetros porque los parámetros son parámetros formales, y los parámetros formales son una copia temporal de los parámetros reales.
...
...
_

Diagrama de lógica:
inserte la descripción de la imagen aquí

Otra pregunta es ¿por qué añadimos los parámetros de la construcción de la copia const?
La respuesta es: ¡Me temo que lo copiaremos al revés!

inserte la descripción de la imagen aquí

inserte la descripción de la imagen aquí

inserte la descripción de la imagen aquí

3. Si no se define explícitamente, el compilador generará un constructor de copia predeterminado. El objeto constructor de copia predeterminado se copia en orden de bytes de acuerdo con el almacenamiento de memoria.Este tipo de copia se denomina copia superficial o copia de valor .

//默认生成的拷贝构造函数
#include<iostream>
using namespace std;
class Time
{
    
    
public:
	Time()
	{
    
    
		_hour = 1;
		_minute = 1;
		_second = 1;
	}
	Time(const Time& t)
	{
    
    
		_hour = t._hour;
		_minute = t._minute;
		_second = t._second;
		cout << "Time::Time(const Time&)" << endl;
	}
private:
	int _hour;
	int _minute;
	int _second;
};
class Date
{
    
    
private:
	// 基本类型(内置类型)
	int _year = 1970;
	int _month = 1;
	int _day = 1;
	// 自定义类型
	Time _t;
};
int main()
{
    
    
	Date d1;
	// 用已经存在的d1拷贝构造d2,此处会调用Date类的拷贝构造函数
	// 但Date类并没有显式定义拷贝构造函数,则编译器会给Date类生成一个默认的拷贝构造函数
	Date d2(d1);
	return 0;
}

inserte la descripción de la imagen aquí

Nota: En el constructor de copia predeterminado generado por el compilador, el tipo integrado se copia directamente en modo byte, mientras que el tipo personalizado se copia llamando a su constructor de copia.

4. Si la aplicación de recursos no está involucrada en la clase, el constructor de copias puede escribirse o no, una vez que la aplicación de recursos está involucrada, el constructor de copias debe escribirse, de lo contrario, es una copia superficial.

Piense en una pregunta: el constructor de copia predeterminado generado por el compilador ya puede copiar el valor del orden de bytes , ¿todavía necesitamos escribir el constructor de copia para el tipo incorporado? La respuesta es si escribir la estructura de copia, todavía tenemos que referirnos a la función 4 de la estructura de copia, ¿hay alguna aplicación de soporte involucrada? ! !

Veamos el siguiente fragmento de código:

#include<iostream>
using namespace std;
typedef int DataType;
class Stack
{
    
    
public:
	Stack(size_t capacity = 10)
	{
    
    
		_array = (DataType*)malloc(capacity * sizeof(DataType));
		if (nullptr == _array)
		{
    
    
			perror("malloc申请空间失败");
			return;
		}
		_size = 0;
		_capacity = capacity;
	}
	void Push(const DataType& data)
	{
    
    
		// CheckCapacity();
		_array[_size] = data;
		_size++;
	}
	~Stack()
	{
    
    
		if (_array)
		{
    
    
			free(_array);
			_array = nullptr;
			_capacity = 0;
			_size = 0;
		}
	}
private:
	DataType* _array;
	size_t _size;
	size_t _capacity;
};
int main()
{
    
    
	Stack s1;
	s1.Push(1);
	s1.Push(2);
	s1.Push(3);
	s1.Push(4);
	return 0;
}

inserte la descripción de la imagen aquí
El código anterior necesita construcción de copias, de lo contrario, ¡las dos pilas se afectarán entre sí! ¡Puede ver que la característica 4 de la estructura de copia es la base para que escribamos o no escribamos la estructura de copia!

3. Escenarios típicos de llamadas del constructor de copias

  1. Crear un nuevo objeto usando un objeto existente
  2. El tipo de parámetro de función es un objeto de tipo de clase.
  3. El tipo de valor de retorno de la función es un objeto de tipo de clase

Recordatorio: para mejorar la eficiencia del programa, trate de usar el tipo de referencia al pasar parámetros a objetos generales y use referencias tanto como sea posible de acuerdo con la escena real al regresar.

conclusión V

El contenido de este capítulo es bastante difícil para los principiantes, pero estas funciones miembro son muy importantes, ¡así que asegúrese de entenderlas bien! ¡Después de aprenderlos, creo que tu nivel mejorará aún más!

Supongo que te gusta

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