Operador [ ] O operador [ ] , quando redefinido, precisa de ser um membro da classe! O operador recebe um único argumento. class CVector { unsigned m_nElements; int* m_arElements; public: int& operator[](unsigned pos); CVector& operator=(const CVector& rv); CVector(unsigned el = 0); CVector(const CVector& v); virtual ~CVector(); }; int& CVector::operator[](unsigned pos) { assert(pos < m_nElements); return m_arElements[pos]; } int main () { CVector v (5); v [3]= 3; int k = v[2]; return 0; } Operador , (vírgula) O operador vírgula garante que o operando esquerdo é avaliado antes do operando direito. O resultado do operador vírgula é o resultado da avaliação do operando direito. cout << (7,8) << endl; 8 const CIntPtr& CIntPtr::operator , (const CIntPtr& r) const { cout << "operador ," << endl; return r; } void D() const { std::cout << *m_pInt << std::endl; }; int main () { CIntPtr p1(10), p2(3); (p1, p2).D(); //3 return 0; } Este operador é raramente redefinido. Operadores new e delete Quando cria algum objecto na zona dinâmica da memória, primeiro é chamado o operador new() e a seguir é chamado o construtor para o objecto. Quando remove um objecto criado na zona dinâmica da memória, primeiro é chamado o destrutor apropriado e a seguir é chamado o operador delete(). Os construtores e os destrutores são chamados sempre e é impossível alterar a maneira como estes são chamados. Contudo é possível controlar como a memória é reservada e libertada. Para tal pode-se redefinir os operadores new e delete que serão chamados em vez da versão por defeito. É possível redefinir os operadores new e delete globais (para serem utilizados por todos os objectos criados na zona dinâmica da memória) ou locais (para serem utilizados por todos os objectos de uma classe específica criados na zona dinâmica da memória). Se redefinir os operadores new e delete globais perde completamente acesso às versões existentes por defeito! Por essa razão, no caso geral, não é aconselhada a redefinição dos operadores new e delete globais. int main () { void* operator new(size_t size) double *d = new double; { void* p = malloc(size); delete d; if (p) memset(p, 0, size); int* str = new int[5]; return p; delete [] str; } void* operator new [] (size_t size) CIntPtr p1(10); { void* p = malloc(size); if (p) return 0; memset(p, 0, size); } return p; } ::operator new (sizeof(double)) void operator delete [] (void* p) ::operator delete (d) { free (p); } void operator delete (void* p) { free(p); } ::operator new [] (sizeof(int) * 5) ::operator delete [] (str) Se redefinir os operadores new e delete para uma classe, estas funções por defeito são estáticas. Como consequência não têm o ponteiro this e não modificam o estado do objecto, apenas reservam a memória para o construtor poder inicializá-la e apagam esta memória depois da execução do destrutor. void* CIntPtr::operator new (size_t size) { void* p = malloc(size); if (p) memset(p, 0, size); return p; } void CIntPtr::operator delete (void* p) { free (p); } void* CIntPtr::operator new [] (size_t size) { void* p = malloc(size); if (p) memset(p, 0, size); return p; } void CIntPtr::operator delete [] (void* p) { free (p); } int main () { CIntPtr* p1 = new CIntPtr(10); delete p1; CIntPtr* p2 = new CIntPtr[3]; delete [] p2; return 0; } class BASE { int a [3]; public: BASE() { cout << "cb\t"; } virtual ~BASE() { cout << "db\t"; } void* operator new(size_t t) { void* p = malloc(t); return p; } void operator delete(void* p) { free (p); } }; class DERIVED : public BASE { int b [2]; public: DERIVED() { cout << "cd\t"; } ~DERIVED() { cout << "dd\t"; } }; void main(void) { BASE *p = new DERIVED; delete p; } BASE* p = new DERIVED; delete p; BASE* p1 = new BASE; delete p1; No caso do destrutor não virtual temos: cb cd db // O resultado: cb // O resultado: dd cd db Dado que por defeito os operadores locais new e delete são funções estáticas, elas não podem ser declaradas como virtuais. Os operadores locais new e delete são herdadas pelas classes derivadas. Redefinição de operadores unários postfix e prefix A linguagem C++ permite distinguir entre redefinição de operadores unários prefix e postfix, tais como ++x e x++. Ou seja, o compilador vai ter que distinguir, por exemplo, entre uma versão postfix e uma versão prefix do operador ++ . Para que a redefinição do operador incremento se possa utilizar na versão de pós-incremento e na versão de pré-incremento, é necessário que cada uma delas, quando analisadas pelo compilador, tenha algo que as distinga. Assim, as versões prefix são redefinidas como habitualmente e as versões postfix é que vão ter uma pequena diferença. As funções operadores podem ser ou membros da classe ou funções amigas da classe EXEMPLO: Supondo uma classe X com um objecto x1 e a necessidade de redefinição do operador decremento nas versões postfix e prefix: class X { //............... public: X& operator--(); // versão prefix const X operator--(int); // versão postfix }; --x1; x1--; Na versão prefix o compilador ao ver uma expressão do tipo --x1 gera o seguinte código: x1.operator--() Pelo contrário, o compilador ao ver uma expressão do tipo x1-- gera o seguinte código: x1.operator--(qualquer_inteiro) Este valor “qualquer_inteiro”, apenas serve para distinguir a lista de argumentos utilizada pela versão postfix da lista utilizada pela versão prefix. Nem todos os operadores unários do C++ podem ter versões prefix e postfix, só o ++ e o -- é que é possível redefinir nas duas versões. const CIntPtr CIntPtr::operator++(int) { CIntPtr temp = *this; (*m_pInt)++; return temp; } const CIntPtr CIntPtr::operator--(int) { CIntPtr temp = *this; (*m_pInt)--; return temp; } CIntPtr& CIntPtr::operator++() { (*m_pInt)++; return *this; } CIntPtr& CIntPtr::operator--() { (*m_pInt)--; return *this; } int main () { CIntPtr p1, p2(7) p1 = p2++; //7 p1 = ++p2; //9 p1 = --p2; //8 p1 = p2--; //8 return 0; } Operador -> (desreferência) Pode ser redefinido como um operador unário postfix. Possibilita a utilização de objectos como se fossem ponteiros. class P { CIntPtr* p; public: CIntPtr* operator->() { return p; }; P() { p = new CIntPtr(3); }; ~P() { delete p; }; }; class CIntPtr { int* m_pInt; public: CIntPtr(); CIntPtr(int); CIntPtr(const CIntPtr& r); virtual ~CIntPtr(); void D() const { std::cout << *m_pInt << std::endl; }; //... int main () { P ptr; ptr->D(); (ptr.operator ->())->D(); }; return 0; } O operador ->, quando redefinido, tem que ser uma função membro. O valor de retorno deve ser obrigatoriamente ou um ponteiro ou um objecto do tipo ao qual podemos aplicar o operador ->. O operador -> é normalmente redefinido para criar ponteiros inteligentes: class CContentor { unsigned m_nSize; CIntPtr** m_arP; public: CContentor() { m_arP = 0; m_nSize = 0; }; ~ CContentor() { delete [] m_arP; }; void Add(CIntPtr* p) { CIntPtr** array = new CIntPtr* [m_nSize+1]; memcpy(array, m_arP, m_nSize * sizeof(CIntPtr*)); array[m_nSize++] = p; delete [] m_arP; m_arP = array; } ... ... class iterator; friend class iterator; class iterator { CContentor& m_cont; unsigned m_nIndex; public: iterator (CContentor& m) : m_cont(m) { m_nIndex = 0; }; CIntPtr* operator->() const { return m_cont.m_arP [m_nIndex]; }; iterator operator++(int) { iterator ant = *this; if (m_nIndex < m_cont.m_nSize) m_nIndex++; return ant; }; bool operator*() const { if (m_nIndex < m_cont.m_nSize) return true; return false; } }; }; O it é um ponteiro inteligente: int main () { CContentor m; CIntPtr p1(1), p2(2), p3(3), p4(4); m.Add(&p1); m.Add(&p2); m.Add(&p3); m.Add(&p4); CContentor::iterator it(m); while (*it) it++->D(); return 0; } Sobrecarga de operadores A declaração de um operador sobrecarregado é semelhante à declaração duma função. A única diferença é que o nome desta função deve ter a forma operator x onde x é o operador que está a ser sobrecarregado. O número de argumentos dum operador sobrecarregado é: 0 –unário/membro; 1 – unário/global ou binário/membro; 2 – binário/global. Argumentos Caso o argumento dum operador só seja analisado e não modificado, este deve ser passado como uma referência para um objecto constante: ... operator + (const type& r); Caso o argumento dum operador seja modificado dentro da função, este deve ser passado como uma referência para um objecto não constante (esta situação surge em todos os operadores de atribuição: =, +=, etc.): ... operator *= (type& l, const type& r); Valores de retorno O tipo do valor de retorno depende do significado do operador! Os operadores de atribuição devolvem normalmente referências para objectos não constantes: type& operator = (const type& r); Os operadores lógicos devolvem normalmente valores do tipo bool: bool operator >= (const type& r); Caso o operador produza um valor novo, torna-se necessário criar um objecto novo e devolvé-lo por valor (que pode ser constante ou não): const type operator - (const type& r); Optimização do retorno const type type::operator+ (const type& r) { type tmp (alguma_coisa + r. alguma_coisa); return tmp; } const type type::operator+ (const type& r) { return type(alguma_coisa + r. alguma_coisa); } Membro ou não membro? Quando o operando esquerdo precisa de ser um lvalue o operador respectivo é normalmente declarado como membro. Operador Uso recomendado Todos os operadores unários membro = () [] -> ->* membro obrigatoriamente Operadores de atribuição (além do =) membro Operadores binários que não modificam o estado do objecto não membro Uma das razões principais de uso de operadores redefinidos como funções globais em vez das funções membro é que neste caso a conversão automática entre tipos pode ser aplicada a qualquer operando (esquerdo e direito). Membro ou não membro? Quando o operando esquerdo é um objecto da classe, o operador respectivo pode ser definido como membro desta classe. Quando o operando esquerdo não é um objecto da classe, o operador deve ser definido como uma função não membro. friend ostream& operator << (ostream& os, const type& r); CIntPtr p1; std::cin >> p1; std::cout << p1 << std::endl; //operadores de entrada/saída friend std::ostream& operator << (std::ostream& os, const CIntPtr& a); friend std::istream& operator >> (std::istream& is, CIntPtr& a); using std::ostream; ostream& operator << (ostream& os, const CIntPtr& a) { return os << *a.m_pInt; } using std::istream; istream& operator >> (istream& is, CIntPtr& a) { return is >> *a.m_pInt; } using std::ostream; ostream& operator << (ostream& os, const CVector& v) { for (unsigned i = 0; i < v.m_nElements; i++) os << v.m_arElements[i] << '\t'; os << std::endl; return os; } using std::istream; istream& operator >> (istream& is, CVector& v) { char c; CVector aux(1); delete [] v.m_arElements; v.m_arElements = 0; v.m_nElements = 0; while ( (c = is.get()) && c != '\n') { if (isdigit (c)) // só funciona para inteiros { // compostos por um só dígito aux[0] = c - '0'; v = v + aux; } } return is; } Quando o operando esquerdo é um objecto da classe, o operador respectivo pode ser definido como membro desta classe. Quando o operando esquerdo não é um objecto da classe, o operador deve ser definido como uma função não membro. CIntPtr(int); const CIntPtr operator+ (const CIntPtr& r); CIntPtr p1(1), p2(2), p3(3); p1 = p2 + p3; //5 p1 = p2 + 10; //14 p1 = 10 + p2; CIntPtr(int); const CIntPtr friend operator+ (const CIntPtr& l, const CIntPtr& r);