Because stringWithFormat:
uses the format itself to figure out how many arguments it needs.
There are two basic ways to do it (handle variable argument lists).
First is to be told in advance how many arguments there are, either a length or something like a format string. Examples of this is:
int arr[] = {6, 3, 1, 4, 1, 5, 9};
// ^
// |
// +--- number of elements following.
or:
NSString *message = [NSString stringWithFormat: @"Your age is %d", age];
// ^
// |
// only one format element ---+
Second is a sentinel value, such as your nil/NULL/0
at the end:
int arr[] = {3, 1, 4, 1, 5, 9, -1};
// ^
// |
// marks end of data ---+
Now, obviously, the sentinel method only works if you can distinguish between real data and the sentinel value (easy in the above case since the digits of PI are all positive numbers between 0 and 9 inclusive).
Technically, I guess you could combine them as well (such as a count of groups with each group having a sentinel value), but I've not seen that used in the wild very often.