the rule that matters most

Every overloaded operator must take at least one argument of a user-defined type — you can't redefine what + means for two built-in ints. And there's no automatic relationship between related operators: defining operator+ doesn't give you operator+= for free, and vice versa. If you want both, and want them to behave consistently, you write both — the idiomatic way is to implement operator+= first, then define operator+ in terms of it.

member vs. non-member operators

Member functionNon-member function
Left operandimplicitly *thisan explicit first parameter
Arguments (binary op)one (the right operand)two (both operands)
Implicit conversion of left operanddoesn't happen — left operand must already be the class typeworks — the compiler can convert a built-in left operand via a non-explicit constructor
Accessfull access to private membersneeds friend, or must go through the public interface
That "implicit conversion of left operand" row is the concrete reason Scott Meyers' advice (define unary operators like += as members, but binary operators like + as non-members) holds up in practice, not just as a style rule:

class Complex {
  double re, im;
public:
  Complex(double re = 0, double im = 0) : re(re), im(im) {}
  Complex & operator+=(const Complex &rhs) { re += rhs.re; im += rhs.im; return *this; }
  Complex operator+(const Complex &rhs) const { return Complex(*this) += rhs; }   // member version
};

Complex a(1, 2), b(3, 4);
a + b;     // fine either way — a.operator+(b)
a + 2;     // fine — 2 implicitly becomes Complex(2, 0), then a.operator+(Complex(2,0))
2 + a;     // ERROR with the member version — the compiler would need 2.operator+(a), and int has no members
            

Complex operator+(const Complex &lhs, const Complex &rhs) { return Complex(lhs) += rhs; }  // non-member

2 + a;     // now fine — operator+(Complex(2,0), a), found via argument-dependent lookup
            
To fully avoid the cost of that implicit int→Complex conversion (a temporary Complex gets constructed just to be added and discarded), overload for the mixed-type cases directly instead of relying on the conversion:

Complex operator+(const Complex &lhs, const Complex &rhs) { return Complex(lhs) += rhs; }
Complex operator+(const Complex &lhs, double rhs)          { return Complex(lhs) += rhs; }
Complex operator+(double lhs, const Complex &rhs)          { return Complex(rhs) += lhs; }
            
There's no need to make these friend if they're implemented in terms of a public += — reserve friend for the rarer case where an operator genuinely needs to reach into private state that no public member exposes.

== and != together


class Complex {
  double re, im;
public:
  bool operator==(const Complex &rhs) const { return re == rhs.re && im == rhs.im; }
  bool operator!=(const Complex &rhs) const { return !(*this == rhs); }   // defined in terms of ==
};
            
Defining != as !(*this == rhs) is the same idea as + in terms of +=: implement the comparison once, and get the inverse for free without a second, independently-maintained implementation that could drift out of sync.

prefix vs. postfix ++/--

Both overload the same operator token, distinguished by a dummy int parameter on the postfix version that exists purely to give the compiler two different signatures to choose between — it's never actually passed a value. Prefix returns a reference to the (already-modified) object; postfix has to save the old value in a local before modifying, then return that saved copy — which is why postfix is inherently a little more expensive than prefix, for any type more complex than a built-in int.

class Counter {
  int value = 0;
public:
  Counter & operator++() {           // prefix: ++c
    value += 1;
    return *this;                     // returns the updated object itself
  }
  Counter operator++(int) {            // postfix: c++ — the "int" here is just a marker, never used
    Counter temp = *this;              // save the pre-increment value
    value += 1;
    return temp;                       // return the OLD value, by value (a copy)
  }
};

Counter c;
++c;    // "pre-increment" — modifies c, then the expression's value is the new c
c++;    // "post-increment" — the expression's value is the old c, then c is modified
            
Prefer ++c over c++ in a plain statement (a for loop increment, for instance) where the returned value isn't used — it skips the copy postfix has to make, for free, with identical observable behavior.

four ways to write a binary operator, worked example

Free function, member function, friend function, and friend-declared-but-defined-outside — all four produce identical behavior for e1 + e2, differing only in where they live and what they can access: TopNotchNote/cpp/operator_overloading_binary_forms.cpp

conversion operators

A single-argument constructor defines an implicit conversion into the class (see Classes & Constructors). A conversion operator defines the reverse: an implicit conversion out of the class into some other type.

class Rational {
  int num, den;
public:
  Rational(int n, int d) : num(n), den(d) {}
  operator double() const { return static_cast<double>(num) / den; }   // implicit Rational -> double
};

Rational r(1, 2);
double d = r;             // 0.5 — the conversion operator ran implicitly
std::cout << r + 0.25;    // 0.75 — r is silently converted to participate in the addition
            
Same caution as single-argument constructors: an implicit conversion operator can fire in places you didn't intend (overload resolution, comparisons). Mark it explicit (C++11 onward) if you want the conversion to require static_cast<double>(r) spelled out rather than happening silently.

where to go from here

Classes & Constructors — the constructor side of implicit conversion.
Move Semantics — operator= implemented via copy-and-swap.
Templates & Generics — a template version of operator+ that works across types that define +=.

reference

cppreference — operator overloading