[C++] Seis funções de membro padrão em classes e objetos (meio) (1)

Índice

1. As seis funções de membro padrão da classe

2. Construtor

2.1 O conceito de construtor

2.2 Características

2.2.1 Sobrecarga de construtores:

2.2.2 Todos os construtores padrão:

3. Destruidor

3.1 O conceito de destruidor

3.2 Características

4. Copie o construtor

4.1 O conceito de construtor de cópias

4.2 Características


1. As seis funções de membro padrão da classe

Se não houver membros em uma classe, ela é simplesmente chamada de classe vazia. Não há realmente nada na classe vazia? Não, quando qualquer classe não escreve nada, o compilador irá gerar automaticamente as seguintes 6 funções de membro padrão.

2. Construtor

2.1 O conceito de construtor

Vamos dar uma olhada na inicialização da classe date aqui:

class Date
{
public:
    void Init(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;
    d1.Init(2022, 7, 5);
    d1.Print();
    
    return 0;
}

resultado da operação:

Somos novos em C++, então devemos inicializar assim.

Se instanciarmos muitos objetos e esquecermos de inicializá-los, o resultado da execução do programa pode ser valores aleatórios ou podem ocorrer problemas.

Aqui o patriarca C++ pensou nisso e projetou um construtor para nós.

Vamos primeiro ver o resultado de esquecer de inicializar e imprimir diretamente:

Aqui está um valor aleatório, então por que isso? Vamos olhar para baixo.

O construtor é uma função de membro especial com o mesmo nome que o nome da classe, que é chamado automaticamente pelo compilador ao criar um objeto de tipo de classe para garantir que cada membro de dados tenha um valor inicial adequado e é chamado apenas uma vez em toda a vida ciclo do objeto.

2.2 Características

O construtor é uma função de membro especial. Deve-se notar que, embora o nome do construtor seja chamado de construção, a principal tarefa do construtor não é abrir espaço para criar objetos, mas inicializar objetos.
Suas características são as seguintes:
1. O nome da função é igual ao nome da classe.
2. Nenhum valor de retorno (não nulo, não há necessidade de escrever). 3. O compilador chama automaticamente o construtor correspondente
quando o objeto é instanciado . 4. O construtor pode estar sobrecarregado.

Vamos primeiro escrever um construtor de classe de data para ver:

class Date
{
public:
	Date()//构造函数,无参构造
	{
		cout << "Date()" << endl;
		_year = 1;
		_month = 1;
		_day = 1;
	}

	void Print()
	{
		cout << _year << "/" << _month << "/" << _day;
	}


private:
	int _year;
	int _month;
	int _day;
};

Vamos testá-lo:

O construtor não é chamado em nossa função principal, mas a marca que fizemos é impressa aqui, e aqui experimentamos que o construtor é chamado automaticamente quando o objeto é instanciado.
Vamos ver o que acontece quando comentamos o construtor que escrevemos:

Podemos ver que depois de comentar, ainda pode ser impresso, mas é apenas um valor aleatório. Porque quando não escrevemos, o compilador gera automaticamente um construtor padrão e o chama automaticamente.

C++ divide os tipos em tipos integrados (tipos básicos): como int, char, double, int*... (o tipo personalizado* também é);

Tipos personalizados: como class, struct, union...

E aqui podemos ver que os membros de tipos internos não serão processados.No C++ 11, as variáveis ​​de membro são suportadas para fornecer valores padrão, que podem ser considerados como preenchimento de lacunas.

2.2.1 Sobrecarga de construtores:

class Date
{
public:
	Date()
	{
		cout << "Date()" << endl;
		_year = 1;
		_month = 1;
		_day = 1;
	}
	
	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;
	d1.Print();
	Date d2(2023, 8, 1);//这里初始化必须是这样写,这是语法
	d2.Print();

    return 0;
}

resultado da operação:

Nota : Quando instanciamos um objeto, quando o construtor chamado não tem parâmetros, não podemos adicionar parênteses após o objeto, a sintaxe estipula.

Se escrito assim, o compilador não pode dizer se é uma declaração de função ou uma chamada . d2 não ficará confuso porque há passagem de valor e a declaração da função não aparecerá dessa forma.

2.2.2 Todos os construtores padrão:

Na verdade, podemos combinar os dois construtores acima em um

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	void Print()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}


private:
	int _year;
	int _month;
	int _day;
};
int main()
{
    Date d1;
	d1.Print();

	Date d2(2023, 8, 1);
	d2.Print();

	Date d3(2023, 9);
	d3.Print();

    return 0;
}

resultado da operação:

O construtor padrão completo é o mais aplicável. A construção sem argumentos e o padrão completo podem existir ao mesmo tempo, mas não é recomendável escrever dessa maneira. Embora nenhum erro seja relatado, não queremos passar parâmetros ao chamar o padrão completo. O compilador não sabe qual construção estamos deseja chamar, o que causará ambiguidade.

Vejamos o problema de implementação de uma fila com duas pilhas:

class Stack
{
public:
	Stack(int n = 4)
	{
		if (n == 0)
		{
			_a = nullptr;
			_size = -1;
			_capacity = 0;
		}
		else
		{
			int* tmp = (int*)realloc(_a, sizeof(int) * n);
			if (tmp == nullptr)
			{
				perror("realloc fail");
				exit(-1);
			}
			_a = tmp;

			_size = -1;
			_capacity = n;
		}
	}

	void Push(int n)
	{
		if (_size + 1 == _capacity)
		{
			int newcapacity = _capacity == 0 ? 4 : 2 * _capacity;
			int* tmp = (int*)realloc(_a, sizeof(int) * newcapacity);
			if (nullptr == tmp)
			{
				perror("realloc fail:");
				exit(-1);
			}
			_a = tmp;
			_capacity = newcapacity;
		}

		_a[_size++] = n;
	}

	int Top()
	{
		return _a[_size];
	}

	void Pop()
	{
		assert(_size > -1);
		_size--;
	}

	void Destort()
	{
		free(_a);
		_a = nullptr;
		_size = _capacity = 0;
	}

	bool Empty()
	{
		return _size == -1;
	}

private:
	int* _a;
	int _size;
	int _capacity;
};
class MyQueue
{
private:
	Stack _pushst;
	Stack _popst;

};

Em circunstâncias normais, precisamos escrever o construtor por nós mesmos para determinar o método de inicialização.As variáveis ​​de membro são todos tipos definidos pelo usuário, portanto, podemos considerar não escrever o construtor. O construtor padrão do tipo personalizado será chamado .

Resumo: Construtores sem argumentos, construtores padrão completos e construtores gerados por padrão pelo compilador, se não os escrevermos, podem ser considerados como construtores padrão e só pode haver um construtor padrão (a coexistência múltipla causará ambiguidade) .

3. Destruidor

3.1 O conceito de destruidor

Destruidor: Ao contrário da função do construtor, o destruidor não completa a destruição do próprio objeto, e a destruição do objeto local é feita pelo compilador. Quando o objeto for destruído, ele chamará automaticamente o destruidor para concluir a limpeza dos recursos do objeto.

3.2 Características

O destruidor é uma função de membro especial e suas características são as seguintes:
1. O nome do destruidor é o caractere ~ adicionado antes do nome da classe.
2. Sem parâmetros e sem tipo de retorno.
3. Uma classe pode ter apenas um destruidor. Se não for definido explicitamente, o sistema gerará automaticamente um destruidor padrão (o tipo interno não será processado e o tipo personalizado chamará seu próprio destruidor). Observação: os destruidores não podem ser sobrecarregados.
4. Quando o ciclo de vida do objeto terminar, o sistema de compilação C++ chamará automaticamente o destruidor.

Vamos primeiro olhar para o destruidor da classe date:

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
        cout << "Date(int year = 1, int month = 1, int day = 1)" << endl;
		_year = year;
		_month = month;
		_day = day;
	}

	void Print()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}

	~Date()
	{
		cout << "~Date()" << endl;
		_year = 0;
		_month = 0;
		_day = 0;
	}

private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1;
	Date d2;

    return 0;
}

resultado da operação:

Podemos ver aqui que o destruidor também é chamado automaticamente.

Nós não escrevemos, o compilador gera automaticamente um destruidor padrão.

A ordem de chamada do destruidor é semelhante à da pilha, e os que são instanciados posteriormente são destruídos primeiro.

Se não houver aplicativo de recurso na classe, o destruidor não pode ser gravado e o destruidor padrão gerado pelo compilador é usado diretamente, como a classe Date; quando houver um aplicativo de recurso, ele deve ser gravado, caso contrário, será causar vazamento de recursos, como a classe Stack.

Vamos fazer um desenho para ver:

O destruidor na pilha substitui a destruição da pilha:

class Stack
{
public:
	Stack(int n = 4)
	{
		if (n == 0)
		{
			_a = nullptr;
			_top = -1;
			_capacity = 0;
		}
		else
		{
			int* tmp = (int*)realloc(_a, sizeof(int) * n);
			if (tmp == nullptr)
			{
				perror("realloc fail");
				exit(-1);
			}
			_a = tmp;

			_top = -1;
			_capacity = n;
		}
	}

	~Stack()
	{
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}
	//void Destort()
	//{
	//	free(_a);
	//	_a = nullptr;
	//	_top = _capacity = 0;
	//}

	void Push(int n)
	{
		if (_top + 1 == _capacity)
		{
			int newcapacity = _capacity == 0 ? 4 : 2 * _capacity;
			int* tmp = (int*)realloc(_a, sizeof(int) * newcapacity);
			if (nullptr == tmp)
			{
				perror("realloc fail:");
				exit(-1);
			}
			_a = tmp;
			_capacity = newcapacity;
		}

		_a[_top++] = n;
	}

	int Top()
	{
		return _a[_top];
	}

	void Pop()
	{
		assert(_top > -1);
		_top--;
	}

	bool Empty()
	{
		return _top == -1;
	}

private:
	int* _a;
	int _top;
	int _capacity;
};
class MyQueue
{
private:
	Stack _pushst;
	Stack _popst;

};

Para coisas como a pilha, nosso destruidor substitui a função de destruição e o destruidor será chamado automaticamente.No passado, precisávamos chamar manualmente a interface da função de destruição, mas agora não precisamos chamá-la.

Portanto, a maior vantagem dos construtores e destruidores é que eles são chamados automaticamente.

4. Copie o construtor

4.1 O conceito de construtor de cópias

Construtor de cópia: Existe apenas um único parâmetro formal , que é uma referência ao objeto desse tipo de classe (geralmente const decoration), que é chamado automaticamente pelo compilador ao criar um novo objeto com um objeto de tipo de classe existente.

4.2 Características

O construtor de cópia também é uma função de membro especial e suas características são as seguintes:
1. O construtor de cópia é uma forma sobrecarregada do construtor.
2. O parâmetro do construtor de cópia é apenas um e deve ser uma referência ao mesmo tipo de objeto , e o compilador causará infinitas chamadas recursivas ao usar o método de passagem por valor.
3. Se não for explicitamente definido, o compilador irá gerar um construtor de cópia padrão. O objeto construtor de cópia padrão é copiado em ordem de byte de acordo com o armazenamento de memória.Esse tipo de cópia é chamado de cópia rasa ou cópia de valor.

A construção da cópia é como copiar e colar.

O parâmetro do construtor de cópia é apenas um e deve ser uma referência a um objeto do tipo classe, e o compilador causará chamadas recursivas infinitas ao usar o método de passagem por valor.

Copiar por valor causará recursão infinita, então vamos escrever um construtor de cópia.

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	//拷贝构造
	Date(Date d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

	void Print()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}

	~Date()
	{
		_year = 0;
		_month = 0;
		_day = 0;
	}

private:
	int _year;
	int _month;
	int _day;
};
void func(Date d)
{
	d.Print();
}

int main()
{
	Date d1(2023, 8, 2);
	func(d1);

    return 0;
}

A cópia do tipo interno é uma cópia direta e a cópia do tipo personalizado precisa ser concluída chamando a construção da cópia.

Em vs2019, o compilador para passar parâmetros por valor reportará um erro:

Portanto, se escrevermos um construtor de cópia, o parâmetro formal deve ser uma referência do mesmo tipo:

A referência é para apelidar a variável, e a ordem da chamada automática do destruidor é para destruir após a definição.Ao copiar, d1 não foi destruído, então a referência pode ser usada, para que não cause cópia recursiva.

Vamos mascarar o construtor de cópia que escrevemos e ver o que acontece:

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	//拷贝构造
	//Date(Date& d)
	//{
	//	_year = d._year;
	//	_month = d._month;
	//	_day = d._day;
	//}

	void Print()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}

	~Date()
	{
		_year = 0;
		_month = 0;
		_day = 0;
	}

private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1(2023, 8, 2);
	Date d2(d1);
	d2.Print();
	
    return 0;
}

resultado da operação:

 Descobrimos que, se não o escrevermos, ainda podemos copiá-lo. Isso ocorre porque, se não o escrevermos, o compilador gera um construtor de cópia por padrão. Para cópias superficiais como a classe de data, o construtor padrão gerado pode perceber a cópia.

Vejamos a construção da cópia da pilha novamente:

typedef int DataType;
class Stack
{
public:
	Stack(int n = 4)
	{
		if (n == 0)
		{
			_a = nullptr;
			_size = -1;
			_capacity = 0;
		}
		else
		{
			int* tmp = (int*)realloc(_a, sizeof(int) * n);
			if (tmp == nullptr)
			{
				perror("realloc fail");
				exit(-1);
			}
			_a = tmp;

			_size = -1;
			_capacity = n;
		}
	}

	//拷贝构造
	Stack(Stack& s)
	{
		_a = s._a;
		_size = s._size;
		_capacity = s._capacity;
	}

	~Stack()
	{
		free(_a);
		_a = nullptr;
		_size = _capacity = 0;
	}

	void Push(int n)
	{
		if (_size + 1 == _capacity)
		{
			int newcapacity = _capacity == 0 ? 4 : 2 * _capacity;
			int* tmp = (int*)realloc(_a, sizeof(int) * newcapacity);
			if (nullptr == tmp)
			{
				perror("realloc fail:");
				exit(-1);
			}
			_a = tmp;
			_capacity = newcapacity;
		}

		_a[_size++] = n;
	}

	int Top()
	{
		return _a[_size];
	}

	void Pop()
	{
		assert(_size > -1);
		_size--;
	}

	bool Empty()
	{
		return _size == -1;
	}

private:
	int* _a;
	int _size;
	int _capacity;
};
int main()
{
	Stack s1;
	Stack s2(s1);
	
    return 0;
}

Aqui escrevemos a construção da cópia para a pilha, vamos tentar a construção da cópia:

Por que uma exceção é levantada aqui?

Vamos depurar e ver:

Aqui podemos ver que o endereço de _a de s1 é o mesmo de _a de s2. Quando s2 for copiado, ele será destruído. Depois que _a de s2 for liberado, s1 chamará o destruidor novamente. Quando você liberar _a, o espaço de _a foi liberado, o que causará uma exceção de ponteiro nulo.

Portanto, para objetos com aplicação de espaço, a cópia profunda deve ser realizada ao escrever a construção da cópia.

Vamos corrigir o código:

typedef int DataType;
class Stack
{
public:
	Stack(int n = 4)
	{
		if (n == 0)
		{
			_a = nullptr;
			_size = -1;
			_capacity = 0;
		}
		else
		{
			int* tmp = (int*)realloc(_a, sizeof(int) * n);
			if (tmp == nullptr)
			{
				perror("realloc fail");
				exit(-1);
			}
			_a = tmp;

			_size = -1;
			_capacity = n;
		}
	}

	//拷贝构造
	Stack(Stack& s)
	{
		cout << "Stack(Stack& s)" << endl;
		//深拷贝
		_a = (DataType*)malloc(sizeof(DataType) * s._capacity);
		if (nullptr == _a)
		{
			perror("malloc fail:");
			exit(-1);
		}

		memcpy(_a, s._a, sizeof(DataType) * (s._size+1));
		_size = s._size;
		_capacity = s._capacity;
	}

	~Stack()
	{
		free(_a);
		_a = nullptr;
		_size = _capacity = 0;
	}

	void Push(int n)
	{
		if (_size + 1 == _capacity)
		{
			int newcapacity = _capacity == 0 ? 4 : 2 * _capacity;
			int* tmp = (int*)realloc(_a, sizeof(int) * newcapacity);
			if (nullptr == tmp)
			{
				perror("realloc fail:");
				exit(-1);
			}
			_a = tmp;
			_capacity = newcapacity;
		}

		_a[_size++] = n;
	}

	int Top()
	{
		return _a[_size];
	}

	void Pop()
	{
		assert(_size > -1);
		_size--;
	}

	bool Empty()
	{
		return _size == -1;
	}

private:
	int* _a;
	int _size;
	int _capacity;
};

resultado da operação:

Resumo: Como Date, não precisamos implementar a estrutura de cópia, e aquela gerada por padrão pode ser usada; Stack precisa que implementemos a estrutura de cópia de cópia profunda, e a padrão causará problemas; não há necessidade para escrever uma cópia para todos os membros de construção de tipos personalizados, o construtor de cópia do tipo personalizado será chamado.

Extensão:

Acho que você gosta

Origin blog.csdn.net/Ljy_cx_21_4_3/article/details/132089939
Recomendado
Clasificación