why not C-style casts

A C-style cast (type)expr silently picks whichever of static_cast, const_cast, and reinterpret_cast would compile — you can't tell which one you're getting just by reading it, and neither can a code reviewer. Each C++-style cast below does exactly one specific, greppable thing, which is the whole point of preferring them.
CastDoesChecked?
static_castconversions the compiler can verify make sense at compile timecompile-time only, no runtime check
dynamic_castsafe downcasting through a polymorphic hierarchyruntime check — fails safely (null or exception)
const_castadds or removes const/volatile — nothing elsecompile-time only
reinterpret_castreinterprets the same bits as an unrelated typenone — you're on your own

static_cast

The general-purpose cast — numeric conversions, base↔derived pointer conversions, void* back to a concrete type. It has no runtime check, so it's fast, but it also means it will happily compile a cast that's wrong at runtime if you get the relationship backward (see downcasting below).

double d = 3.9;
int i = static_cast<int>(d);       // 3 — same as a C-style cast here, but explicit about intent

void *vp = &i;
int *ip = static_cast<int*>(vp);    // fine — recovering a concrete type from void*

Base *b = new Derived();
Derived *d2 = static_cast<Derived*>(b);  // compiles — but only actually safe if b really points at a Derived
            

dynamic_cast, upcasting, and downcasting

Upcasting (derived → base) is always safe and happens implicitly for public inheritance — a derived object genuinely is a base object, plus more. Downcasting (base → derived) is not automatically safe, because a base pointer might point at a plain base object, or at any one of several derived types — the compiler can't know which from the static type alone.

class Base { public: virtual ~Base() = default; };     // needs at least one virtual function
class D1 : public Base {};
class D2 : public Base {};

Base *pbd = new D1();
D1 *good = dynamic_cast<D1*>(pbd);   // OK — pbd really points at a D1

Base *pbb = new Base();
D1 *bad = dynamic_cast<D1*>(pbb);    // pbb doesn't point at a D1 — dynamic_cast returns nullptr

if (D1 *checked = dynamic_cast<D1*>(pbd)) {
  // safe to use checked as a D1* here
}
            
dynamic_cast only works on polymorphic types — the class needs at least one virtual function, because the cast relies on the same RTTI machinery the vtable carries (see Inheritance & Polymorphism). On a non-polymorphic hierarchy it won't compile at all — use static_cast and take responsibility for the correctness yourself. On references rather than pointers, a failed dynamic_cast throws std::bad_cast instead of returning null, since there's no such thing as a null reference to return.
dynamic_cast does its check at runtime, which makes it measurably slower than static_cast — reach for it specifically when you don't statically know which derived type a base pointer actually holds, not as a default habit.

const_cast

Adds or strips const/volatile — nothing else. The canonical legitimate use is calling a legacy or third-party function that takes a non-const pointer but is known not to actually modify what it points to.

void legacyPrint(char *s) { std::cout << s; }   // doesn't modify s, but its signature doesn't say so

void print(const std::string &s) {
  legacyPrint(const_cast<char*>(s.c_str()));      // risky if legacyPrint lied about not modifying it
}
            
Using const_cast to actually write through a pointer that was originally declared const is undefined behavior, not just bad style — the compiler is allowed to have placed that object in read-only memory. Every use of const_cast is worth treating as a design smell worth a second look, not a routine tool.

reinterpret_cast

Reinterprets the same bit pattern as a different, usually unrelated type — no conversion happens, no check happens, it's the closest C++ gets to "trust me." Mostly seen casting between function pointer types, or in low-level code that needs to view an object's raw bytes.

int i = 0x41424344;
char *bytes = reinterpret_cast<char*>(&i);   // view i's 4 bytes as a char array — implementation-defined, endianness-dependent
            

quick decision guide

Ask, in order: converting a number, or moving up/down a class hierarchy you're confident about — static_cast. Moving down a hierarchy where you're not sure of the actual type — dynamic_cast. Only touching const/volatile — const_cast. Reinterpreting raw bytes as an unrelated type — reinterpret_cast, and treat reaching for it as a signal to double-check the design first.
All four, run back to back, so you can see which checks happen at compile time, which happen at runtime, and which don't happen at all: TopNotchNote/cpp/casting_four_casts_demo.cpp

where to go from here

Inheritance & Polymorphism — the vtable/RTTI machinery dynamic_cast relies on.
Pointers & References — the void* case static_cast is most often used to undo.

reference

cppreference — static_cast
cppreference — dynamic_cast