The calloc part is one of the most common blind spots I see among C programmers.
I try to avoid the malloc(n * sizeof (...)) pattern as much as possible. Sure there are lots of cases where it can never overflow, and you might save a bit of overhead from the zeroing and overflow checking, but most of that overhead might also be imaginary depending on allocator internals, and even kernel internals. It's the sort of thing it only makes sense to optimise when you've already squeezed out every bit of performance. And by then you've probably minimised dynamic allocation as much as possible anyway.
It's also very easy to think something like "well, n is passed in as a parameter, but it's a static function, and I know all the callers. So it's fine".
But now every caller in the future has to be aware of this possibility.
Calloc is the function originally intented to allocate arrays. Instead of accepting a number of bytes, it takes two unsigned integers(size_t): the number of array members, and the the size of each member. And it checks whether the result of multiplying them fits in a size_t. If not, it returns NULL, allocating nothing(and also sets errno, iirc). Then you can have your code detect it, crash or report an error, and avoid memory corruption. Even if you sloppily don't check calloc's return value, you're probably just gonna segfault which is unlikely to lead to data leaks or code execution
If you use malloc(n * size), and n is too large, it could wrap around, malloc gets a smaller number than the program thinks it allocated. Which means that even if the program does bounds/null checking on the array later on, it has the wrong bounds. This can be used to access or modify other objects on the heap, or even modify allocator internals in some cases, depends on the implementation details of the allocator.
So what I meant was, you better be careful using malloc(n * size) unless n is a constant. If it's in any way tied to program behaviour or user input, it's a hole waiting to happen.
calloc has its own set of gotchas, though. For instance, it may allocate a different amount of memory than you requested, and it comes with the overhead of zeroing out the allocated memory.
Neither of these may matter to you, but when they do, they really matter. So you still have to be thoughtful about using it. Not so different from how you have to be thoughtful about using malloc.
I tend to see zeroed memory as an advantage in the vast majority of cases. And when it's actually significant overhead then s/calloc(/reallocarray(NULL,/
The thing I like about almost always allocating through calloc is this: I know that if my code is somehow not initialising memory properly, the resulting bug will be the same each time, and therefore faster to reproduce and debug. Not that I misinitialise my memory very frequently anymore, it's not that hard to get right.
Surprisingly often, I've found that so much of my data should probably default to zero anyway, so it doesn't really matter all that much.
Calloc can over-allocate, which i always found annoying myself, although at least with calloc, you know that if you only index the pointer modulo the n you passed onto calloc, you won't invoke any demons from the underworld.
But yeah, in general, to really know what you're doing in C, you kind of have to understand memory allocators at a fairly deep level, because the footguns are aplenty. You need to have a mental model of the heap and stack.
Multiplying array length by sizeof(element type) can overflow.
Of course, you can write your own malloc_array() that uses __builtin_mul_overflow() and doesn't come with calloc's drawback (the cost of zeroing the allocated memory).
OpenBSD's libc has reallocarray for this, which is realloc with the same bounds checking as calloc, but if the first parameter is NULL, it's just calloc without the zeroing.
And I believe you'll find it in glibc too these day? Or if not, there's always libbsd, which has lots of handy stuff anyways.
I try to avoid the malloc(n * sizeof (...)) pattern as much as possible. Sure there are lots of cases where it can never overflow, and you might save a bit of overhead from the zeroing and overflow checking, but most of that overhead might also be imaginary depending on allocator internals, and even kernel internals. It's the sort of thing it only makes sense to optimise when you've already squeezed out every bit of performance. And by then you've probably minimised dynamic allocation as much as possible anyway.
It's also very easy to think something like "well, n is passed in as a parameter, but it's a static function, and I know all the callers. So it's fine".
But now every caller in the future has to be aware of this possibility.