The thing to remember about sizeof is that it is a compile-time operator1; it returns the number of bytes based on the type of the operand.
The type of arr is int [24], so sizeof arr will evaluate to the number of bytes required to store 24 int values. The type of ptr is int *, so sizeof ptr will evaluate to the number of bytes required to store a single int * value. Since this happens at compile time, there's no way for sizeof to know what block of memory ptr is pointing to or how large it is.
In general, you cannot determine how large a chunk of memory a pointer points to based on the pointer value itself; that information must be tracked separately.
Stylistic nit: a preferred way to write the malloc call is
int *ptr = malloc(sizeof *ptr * N);
In C, you do not need to cast the result of malloc to the target pointer type2, and doing so can potentially mask a useful diagnostic if you forget to include stdlib.h or otherwise don't have a prototype for malloc in scope.
Secondly, notice that I pass the expression *ptr as the operand to sizeof rather than (int). This minimizes bugs in the event you change the type of ptr but forget to change the type in the corresponding malloc call. This works because sizeof doesn't attempt to evaluate the operand (meaning it doesn't attempt to dereference ptr); it only computes its type.
1 The exception to this rule occurs when
sizeof is applied to a variable-length array; since the size of the array isn't determined until runtime, a
sizeof operator applied to a VLA will be evaluated at runtime.
2 Note that this is
not the case in C++; a cast is required, but if you're writing C++ you should be using
new and
delete instead of
malloc and
free anyway. Also, this is only true since C89; older versions of C had
malloc return
char * instead of
void *, so for those versions the cast
was required. Unless you are working on a
very old implementation (such as an old VAX mini running an ancient version of VMS), this shouldn't be an issue.