True, operator isn’t bounds-checked, but bounds-checking at access is the least efficient and laziest way to avoid buffer overruns; and in any case, containers also have ::at(), which does do bounds-checks, if you’re happy with the performance penalty.
But the problem with overruns happens not at access, but earlier, when you compute the index that you want to put between those brackets, and that is where C++ containers are vastly superior to the features offered by C. In my (reasonably long) experience of debugging C codebases, overruns happen because code is relying on “in-band” methods of determining the length of structures, which can be lost when those structures are copied into inadequately small buffers.
The C-string itself is the classic example of this problem: copy a string that has strlen() == 10 into char and... oops. no terminating NUL anymore - even if you used the overflow-safe strncpy(), because that function does not guarantee a NUL-terminated result, contrary to what most new developers would expect.
By contrast, vector::size() gives you the number of elements in the container (or bytes in the string). It’s explicit, it’s cheap to call, and it’s always present. It makes it trivial to sanitise indices at entry to modules, and prevent a whole swathe of overrun scenarios.
Rule 1: When you’re doing random-access with un-trusted indices, use ::at(), which does perform range-checking.
Rule 2: For iteration, use for (auto i = container.begin(); i!= container.end(); ++i) rather than iterating by index on size(). It’s the same cost, and will still work without a recompile if you change 'container' to be a set or some other type later on. ('++i' avoids a performance penalty if some iterators have expensive copy behaviour, but most compilers do now make this substitution if you say i++ without using 'i' in an expression)
Neither of those options are available in C without writing your own container classes.