Índice
1. As seis funções de membro padrão da classe
2.2.1 Sobrecarga de construtores:
2.2.2 Todos os construtores padrão:
4.1 O conceito de construtor de cópias
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: