the four pillars, briefly

PillarWhat it actually means
Abstractionyou define what an object should be — a Person class, not one class per individual person — and instantiate specific objects from that definition
Encapsulationbundle an object's data with the methods that operate on it, and keep the data private unless another class genuinely needs it
Inheritancereuse an existing class's attributes and behavior by deriving a new class from it — see Inheritance & Polymorphism
Polymorphismthe same interface behaves differently depending on the actual object — see Inheritance & Polymorphism

encapsulation, in practice

Encapsulation isn't about secrecy — it's about reducing how many parts of the codebase are entangled with each other. If a class's internals are private and only reachable through its public methods, you can change how it's implemented (swap a linked list for a vector, change a formula) without touching any code that uses the class, as long as the public interface stays the same. A public data member is a promise you're making to every caller; a private one with public accessor methods is a promise you can revise later.

aggregation vs. composition vs. association

All three describe one object holding a relationship to another, but they differ in who owns the lifetime — and that difference is entirely about whether the containing object's destructor is responsible for the other object's destruction.
RelationshipLifetimeExample
Composition"has-a" (strong)the contained object is destroyed with the containera Car holds an Engine by value, or allocates and deletes it itself
Aggregation"has-a" (weak) / "uses-a"the contained object outlives the container — the container just borrows ita Department holds a pointer to a Teacher created (and destroyed) elsewhere
Associationgeneral term covering bothany relationship where one class refers to another

class Engine { public: Engine(int id) { std::cout << "Engine ctor\n"; } ~Engine() { std::cout << "Engine dtor\n"; } };

class Car {                    // COMPOSITION — Car owns its Engine outright
  Engine engine;                // held by value: constructed with Car, destroyed with Car
public:
  Car(int id) : engine(id) {}
};

{
  Car c(1);                     // prints "Engine ctor"
}                                 // c goes out of scope — prints "Engine dtor" automatically
            

class Teacher { public: Teacher(std::string n) : name(n) {} std::string name; };

class Department {              // AGGREGATION — Department borrows a Teacher it doesn't own
  Teacher *teacher;              // just a pointer to something created elsewhere
public:
  Department(Teacher *t) : teacher(t) {}
  ~Department() { /* deliberately does NOT delete teacher */ }
};

Teacher *t = new Teacher("Dr. Lee");   // created outside Department's control
{
  Department d(t);                      // d borrows t
}                                         // d is destroyed — t is untouched, still valid
delete t;                                  // whoever created t is responsible for destroying it
            
The pointer vs. value distinction in the code isn't the actual rule — a class can hold a pointer and still be composition, if its destructor deletes what the pointer points to (as in Pointers & References's heap-allocated Bus/Engine example). What determines aggregation vs. composition is strictly who calls delete — or, equivalently today, which object holds the owning smart pointer.
Both relationships above, side by side, with constructor/destructor output tracing exactly when each Engine and Teacher lives and dies: TopNotchNote/cpp/oop_relationships_composition_aggregation.cpp

friend functions

A friend declaration grants a specific function (or another class) access to a class's private members, bypassing encapsulation on purpose for that one relationship. It's a deliberate escape hatch, not a workaround to reach for by default — per Scott Meyers, most binary operators don't actually need it (see Operator Overloading), since they can be implemented purely in terms of the class's already-public interface.

class Matrix {
  double data[9];
  friend class MatrixInverter;   // MatrixInverter can now touch Matrix's private data directly
};
            

static members

A static data member is shared by every instance of the class — there's exactly one copy, not one per object, which is why it can't be initialized in a constructor and needs a definition outside the class. A static member function has no this pointer at all: it can only touch other static members, and it's callable through the class name without any instance existing.

class Widget {
  static int totalCount;          // declaration only
public:
  Widget() { totalCount++; }
  static int count() { return totalCount; }   // no 'this' — can't touch non-static members
};
int Widget::totalCount = 0;        // definition — exactly one copy, lives outside any single Widget

Widget a, b, c;
Widget::count();                    // 3 — called via the class name, the recommended style
a.count();                          // also legal, but calling it via an instance is discouraged
            
One subtlety worth knowing: static members aren't counted in sizeof(Widget), since they don't live inside any individual instance — only non-static data members and (if present) the vptr contribute to an object's size. See C++ Best Practices for the full struct-padding/sizeof breakdown.

block scope and shadowing

Not specific to classes, but the same shadowing hazard from initialization lists (Classes & Constructors) applies to nested scopes generally: declaring a new variable with the same name as an outer one hides the outer one for the rest of the inner block, without any error or warning by default.

int a = 0;
{
  a = 1;
  int a;              // a NEW variable named a — shadows the outer one starting here
  a = 2;               // sets the inner a, not the outer one
  std::cout << a;      // 2
}
std::cout << a;         // 1 — the outer a was set to 1 before the inner a was declared, and never touched again
            

where to go from here

Inheritance & Polymorphism — "is-a," the relationship this page deliberately set aside.
Design Patterns — composition-based patterns like Strategy and Decorator that favor "has-a" over "is-a."

reference

cppreference — static members
cppreference — friend declaration