0

When I am working on Pset4 - Recover, I realize I do not understand the concept of array and pointer. Although I finally worked it out, I would be very grateful if you guys can explain the following:

Why this works:

char *filename;
filename = "001.jpg";

Why the one below doesn't work:

char *filename;
sprintf(filename, "001.jpg");

It does not work even I assign NULL as requested by the IDE:

char *filename = NULL;
sprintf(filename, "001.jpg");

It finally works as below, but I don't know why:

char *filename = malloc(sizeof(char)*8);
sprintf(filename, "001.jpg");

It also works if I use array instead of pointer:

char filename[8];
sprintf(filename, "001.jpg");

To explore more on the use of array and pointer, I started a new program to test out different declaration, initialization and assignment of array and pointer.

char *name; // Not working, IDE requested to assign NULL
name[0] = 1; 

char *name = NULL;
name[0] = 1; // Not working, because no memory assigned to store "1"?

char *name = malloc(2);
name[0] = 1; // why this works?

char *name = malloc(0);
name[0] = 1; // why this still works?

char *name = malloc(2);
name[0] = "A"; // why this doesn't work?

Can we still use the expression [ ], when it is declared as pointer, but not array?!

1 Answer 1

3

Before I answer this question and important clarification must be made about what it means for something to "work". There is a term that will come up a lot when talking about C (and other languages) called "undefined behavior". Programs which invoke undefined behavior may compile just fine and may even appear to work just fine, but it is not guaranteed to always do so. Some examples of undefined behavior include reading from an uninitialized variable (int x; printf("%d\n", x)), writing to memory that you don't own (int arr[5]; arr[100] = 0) or overflowing a signed integer (an integer that can represent negative values). Some of the examples that you refer to as "working" actually invoke undefined behavior (which I will cover below).

The first non-sprintf example

char *filename;
filename = "001.jpg";

This works because string literals (things enclosed in quotes) are stored inside the executable itself. When the code is compiled, the compiler essentially just goes through and replaces "001.jpg" with its address within the executable1. In this instance, the compiler takes care of allocating the space for the string for you.

How sprintf works

The declaration of sprintf looks like int sprintf(char *str, const char *format, ...). sprintf creates the formatted string based on format and the additional arguments you supply and writes it to the memory address2 stored in str. The general outling of the function looks like the following (sans the actual formatting part):

int sprintf(char *str, const char *format, ...) {
    // number of chars we've stored in `str`
    int str_index = 0;
    // For each character in the format string
    for (const char *format_ptr = format; *format_ptr != '\0'; format_ptr++) {
        if (*format_ptr == '%') {
            // Handle formatting
        } else {
            // *format_ptr is just a regular character, so just store it in `str`
            str[str_index] = *format_ptr;
        }
        str_index++;
    }
    return str_index;
}

The rest of your sprintf examples

Let's go through the rest of your examples one by one:

char *filename;
sprintf(filename, "001.jpg");

I said in the above section that sprintf writes the resulting string to the memory address stored in its str argument, so what is the memory address stored in filename? Well since filename is not initialized, it's undefined; it's just some random address in memory. This doesn't work because you're writing the contents of "001.jpg" to some arbitrary location in memory which is undefined behavior. This is the same reason that char *str; *str = 'c'; is undefined behavior; you're writing 'c' to an arbitrary location in memory;

char *filename = NULL;
sprintf(filename, "001.jpg");

This doesn't work for a reason similar to the last example, but rather than trying to write the contents of "001.jpg" to some arbitrary location in memory, it tries to write it beginning at the NULL address. Writing to/reading from the NULL pointer is undefined behavior which (may) crash your program.

char *filename = malloc(sizeof(char)*8);
sprintf(filename, "001.jpg");

This works because filename contains the address of a block of memory that you've allocated, and it doesn't invoke undefined behavior because you've allocated enough space to store "001.jpg" plus the null byte.

char filename[8];
sprintf(filename, "001.jpg");

Again this works because filename refers to a block of memory that you've allocated (the only difference being that it is on the stack rather than the heap).

Your malloc examples

char *name; // Not working, IDE requested to assign NULL
name[0] = 1; 

This invokes undefined behavior as mentioned above. name contains some undefined value and name[0] = 1 tries to interpret that arbitrary value as a memory address and writes to it.

char *name = NULL;
name[0] = 1; // Not working, because no memory assigned to store "1"?

Spot on. NULL cannot be written to or read from.

char *name = malloc(2);
name[0] = 1; // why this works?

This is correct because malloc allocated two bytes on the heap and returned the address of this allocated block. It's perfectly valid to read from/write to this block.

char *name = malloc(0);
name[0] = 1; // why this still works?

This is actually undefined behavior and only works by happenstance (and certainly is not guaranteed to always work). malloc(0) may return one of two things (depending on the implementation):

  1. A NULL pointer
  2. Some other pointer that is allowed to be passed to free

In this case, it is doing the latter. malloc is returning to you an address but one that does not actually refer to a block you're supposed to write to.

char *name = malloc(2);
name[0] = "A"; // why this doesn't work?

This is again (probably) undefined behavior, but perhaps not the reason you'd expect. As mentioned in my first answer, the compiler will replace "A" with a memory address that points to the first character of that string. On 64-bit computers, pointers are (usually) 8 bytes (can store values from 0-18446744073709551615) while chars are one byte (can store values from either -127-128 or 0-255 depending on if it is signed or not). name[0] = "A" will convert the memory address into an 8-byte integer (that is probably quite large) and attempt to store that in a one-byte char. This will overflow the char, which if it is signed on your platform, results in undefined behavior. Even if the behavior is defined, it certainly not copy the string as you would presumably expect. To copy the string, you would do the following: strcpy(name, "A");

1The compiler will often put these string literals in read-only areas of memory which is why attempting to modify a string literal (char *str = "hello"; str[0] = 'm') is undefined behavior.

2It's possible that the resulting string will be too long for the amount of memory you've allocated for str which is why it is generally recommended that you use snprintf instead of sprintf which also takes the size of the buffer, ensuring that it won't accidentally write to out-of-bounds memory.

3
  • Thanks, @chad. To clarify, do you mean that whatever in double quotes (string literals) would be returned as an address of memory storing the string literals? In this sense, name[0] = "A", would simply mean storing a 8 byte memory address ("A") to one byte char (name[0]), and therefore overflowing the char? How about name[0] = 'A' ? With single quote, 'A' should be still a char, right? Moreover, could you elaborate on the use of [ ] for pointer? Can we declare a pointer, but using [ ] to refer to certain character, e.g. name[2] = 9; or should we use *name[2] = 9; for dereferencing??
    – Alex Yu
    Commented Apr 16, 2017 at 9:57
  • To your first question, yeah pretty much. It'd be possible I suppose on some weird architecture for the address to be less than 256, in which case it wouldn't overflow the character, but that'd be pretty weird and still certainly not what you want. name[0] = 'A' is fine so long as name refers to a block of memory of at least one byte. array[index] is actually just syntactic sugar for *(array + index), so yes you can declare a pointer and use the square bracket notation. The dereferencing is included in the above. You index arrays and pointers the same way.
    – chad
    Commented Apr 28, 2017 at 17:03
  • An interesting thing about how array[index] desugars is that since addition is communicative, it is completely valid to write 1[name] since this desugars to *(1 + name) which is the same as name[1]. Don't do this though because it'd pretty unreadable. Also note that name[0] is equivalent to *name since it desugars to *(name + 0)
    – chad
    Commented Apr 28, 2017 at 17:07

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .