Menu
Coddy logo textTech

Sobrecarga do Operador de Atribuição

Parte da seção Programação Orientada a Objetos do Journey de C++ da Coddy — lição 44 de 104.

O operador de atribuição por cópia (=) é chamado quando você atribui um objeto existente a outro. Diferente do construtor de cópia (que cria um novo objeto), o operador de atribuição trabalha com um objeto que já existe e pode ter recursos que precisam ser limpos primeiro.

class Buffer {
    int* data;
    size_t size;
    
public:
    Buffer(size_t s) : size(s), data(new int[s]) {}
    ~Buffer() { delete[] data; }
    
    Buffer& operator=(const Buffer& other) {
        if (this != &other) {              // 1. Verificação de autoatribuição
            delete[] data;                  // 2. Limpar recurso existente
            size = other.size;              // 3. Copiar os dados
            data = new int[size];
            for (size_t i = 0; i < size; ++i)
                data[i] = other.data[i];
        }
        return *this;                       // 4. Retornar *this
    }
};

A verificação de autoatribuição (this != &other) é crítica. Sem ela, b = b; excluiria os dados antes de tentar copiá-los, causando comportamento indefinido.

O operador retorna *this por referência para permitir o encadeamento como a = b = c;. Isso corresponde ao comportamento dos tipos integrados.

Para classes que gerenciam recursos, você frequentemente implementará tanto operadores de atribuição por cópia quanto por movimentação:

Buffer& operator=(Buffer&& other) noexcept {
    if (this != &other) {
        delete[] data;           // Limpa o recurso atual
        data = other.data;       // Rouba o recurso
        size = other.size;
        other.data = nullptr;    // Deixa a origem em um estado válido
        other.size = 0;
    }
    return *this;
}

O operador de atribuição por movimento transfere a propriedade em vez de copiar, tornando operações como vec[0] = createBuffer(); muito mais eficientes.

challenge icon

Desafio

Fácil

Vamos construir uma classe DynamicString que gerencia um array de caracteres alocado dinamicamente e lida adequadamente com a atribuição entre objetos existentes. Este é um cenário prático onde você precisa transferir dados de string de um objeto para outro com segurança, sem vazamentos de memória ou ponteiros pendentes.

Você criará dois arquivos para organizar seu código:

  • DynamicString.h: Defina uma classe DynamicString que armazena uma string no estilo C usando memória dinâmica. Sua classe deve gerenciar:
    • Um ponteiro char* privado para os dados da string
    • Um size_t privado para o comprimento
    • Um construtor que recebe um const char* e cria uma cópia profunda (deep copy)
    • Um destrutor que libera adequadamente a memória alocada
    • Um operador de atribuição por cópia que realiza uma cópia profunda com proteção contra autoatribuição
    • Um operador de atribuição por movimento que transfere a propriedade de forma eficiente
    • Um método c_str() que retorna a string interna (const)
    • Um método length() que retorna o comprimento da string (const)

    Seu operador de atribuição por cópia deve seguir os quatro passos essenciais: verificar a autoatribuição, limpar os recursos existentes, copiar os novos dados e retornar *this. O operador de atribuição por movimento deve "roubar" o ponteiro da origem e deixar a origem em um estado vazio válido (nullptr com comprimento 0).

  • main.cpp: Leia três strings da entrada (cada uma em uma linha separada). Crie três objetos DynamicString: str1 com a primeira entrada, str2 com a segunda entrada e str3 com a terceira entrada.

    Demonstre seus operadores de atribuição:

    1. Imprima o estado inicial de todas as três strings
    2. Use a atribuição por cópia: str1 = str2;
    3. Imprima str1 e str2 após a atribuição por cópia (ambas devem ter o mesmo conteúdo)
    4. Use a atribuição por movimento: str2 = std::move(str3);
    5. Imprima str2 após a atribuição por movimento, e o comprimento de str3 (deve ser 0 após ser movida)

    Formato de saída:

    Initial:
    str1: <value>
    str2: <value>
    str3: <value>
    After copy (str1 = str2):
    str1: <value>
    str2: <value>
    After move (str2 = std::move(str3)):
    str2: <value>
    str3 length: 0

Use <cstring> para strlen e strcpy. Marque seu operador de atribuição por movimento como noexcept. Inclua <utility> em main.cpp para std::move. Não se esqueça dos header guards em seu arquivo de cabeçalho.

Folha de consulta

O operador de atribuição por cópia (=) atribui um objeto existente a outro. Ele deve lidar com a limpeza dos recursos existentes antes de copiar os novos dados.

Estrutura do Operador de Atribuição por Cópia

ClassName& operator=(const ClassName& other) {
    if (this != &other) {              // 1. Self-assignment check
        // 2. Clean up existing resources
        // 3. Copy data from other
    }
    return *this;                       // 4. Return *this
}

Exemplo com Memória Dinâmica

class Buffer {
    int* data;
    size_t size;
    
public:
    Buffer& operator=(const Buffer& other) {
        if (this != &other) {
            delete[] data;                  // Clean up existing resource
            size = other.size;
            data = new int[size];
            for (size_t i = 0; i < size; ++i)
                data[i] = other.data[i];
        }
        return *this;
    }
};

A verificação de autoatribuição (this != &other) evita a exclusão de dados antes de copiá-los em casos como b = b;.

Retornar *this por referência permite o encadeamento: a = b = c;.

Operador de Atribuição por Movimento

O operador de atribuição por movimento transfere a propriedade em vez de copiar, aumentando a eficiência:

ClassName& operator=(ClassName&& other) noexcept {
    if (this != &other) {
        // Clean up current resource
        // Steal the resource from other
        // Leave other in valid state (nullptr, 0, etc.)
    }
    return *this;
}

Exemplo de Atribuição por Movimento

Buffer& operator=(Buffer&& other) noexcept {
    if (this != &other) {
        delete[] data;
        data = other.data;       // Steal the resource
        size = other.size;
        other.data = nullptr;    // Leave source valid
        other.size = 0;
    }
    return *this;
}

Marque a atribuição por movimento como noexcept para um desempenho ideal com containers padrão.

Experimente você mesmo

#include <iostream>
#include <string>
#include <utility>
#include "DynamicString.h"

using namespace std;

int main() {
    // Leia três strings da entrada
    string input1, input2, input3;
    getline(cin, input1);
    getline(cin, input2);
    getline(cin, input3);

    // Crie três objetos DynamicString
    DynamicString str1(input1.c_str());
    DynamicString str2(input2.c_str());
    DynamicString str3(input3.c_str());

    // TODO: Imprima o estado inicial de todas as três strings
    // Formato:
    // Inicial:
    // str1: <value>
    // str2: <value>
    // str3: <value>

    // TODO: Use atribuição por cópia: str1 = str2;

    // TODO: Imprima str1 e str2 após a atribuição por cópia
    // Formato:
    // Após a cópia (str1 = str2):
    // str1: <value>
    // str2: <value>

    // TODO: Use atribuição por movimento: str2 = std::move(str3);

    // TODO: Imprima str2 após o movimento, e o comprimento de str3
    // Formato:
    // Após o movimento (str2 = std::move(str3)):
    // str2: <value>
    // comprimento de str3: 0

    return 0;
}
quiz iconTeste seus conhecimentos

Esta lição inclui um quiz rápido. Comece a lição para respondê-lo e acompanhar seu progresso.

Todas as lições de Programação Orientada a Objetos