I've been reading a lot of questions and answers on here about C pointers, but haven't found anything explaining why C doesn't allow addresses to be stored in regular ints-- it seems to be to be simpler to type:
int a = 100;
int b = &a;
int c = &b; //etc
rather than
int a = 100;
int* b = &a;
int** c = &b; //etc
Actually, I'm not sure if all compilers disallow this, or if it just flags a warning. But If I'm getting the same thing accomplished, (storing the address of a variable for reference), what difference does it make if I use pointer notation or just assign the values directly?
Because if all addresses were stored only in a common data type, such as int
, or void *
, then the only information you'd have would be the address, which is not enough to use the data at that address. In particular, in order to use the data at that address, you also need to know:
How much data is stored at that address; and
What that data is intended to represent.
As you may know, different data types in C are stored in different amounts of memory. char
always takes one byte, and it's typical these days for short
to take two bytes, and int
to take four bytes, for instance.
The address of a variable is always the address of the first byte of the data stored in that variable. If, for instance, you were to define int var = 0xDEADBEEF;
and the compiler decided to store that variable at memory location 100, then the memory would look like this (on a little-endian system with four byte int
s):
-----------------------------------------
| 100 | 101 | 102 | 103 |
| 0xEF | 0xBE | 0xAD | 0xDE |
-----------------------------------------
If, on the other hand, you were to define char c = 0x09;
and the compiler were to store that at memory location 200, then it might look like this:
-----------------------------------------
| 200 | 201 | 202 | 203 |
| 0x09 | Garbage | Garbage | Garbage |
-----------------------------------------
So if this was allowed:
int main(void) {
int var = 0xDEADBEEF;
char c = 0x09;
int pv = &var; /* Not allowed in ISO C */
int pc = &c; /* Not allowed in ISO C */
int sum = my_func(pv, pc);
....
}
int my_func(int p1, int p2) {
int a = *p1; /* Not allowed in ISO C */
int b = *p2; /* Not allowed in ISO C */
return a + b;
}
then how would the compiler know what values to assign to a
and b
in my_func()
? Sure, the compiler knows the address of the data that p1
points to, but when it wants to dereference that pointer, find out the value of the data, and store the value in a
, does it take the value at memory location 100
? Or the value at memory locations 100
and 101
? Or the value at memory locations 100
, 101
, 102
and 103
? Or something else?
If you just stored all pointers in one type of variable, the compiler would know where each data element began in memory, but it would have no idea how much data was at that address.
If, on the other hand, you require people to store pointers to int
in a int *
, and pointers to char
in a char *
, then the compiler does know. If you have an int *
pointer containing the address 100
, and you ask the compiler to deference it, then it knows you want the value stored at address 100
and the next three bytes, since the fact it's an int *
tells the compiler that an int
is stored there, and an int
takes four bytes (on this particular hypothetical implementation).
On the other hand, if you store address 100
in a char *
and ask the compiler to go get the value at address 100
, it knows to only get the one byte value stored at that single memory address, and to ignore any following bytes, because it knows a char
occupies a single byte.
So when you have data types of different sizes, knowing their addresses is not enough. You also need to know how many bytes of memory they occupy, otherwise when you try to retrieve the value, you don't know how many bytes of memory to read. If you use a different pointer type to point to each basic type, then you do know how many bytes of memory to read.
Consider this (non-portable) program, on a little-endian architecture that uses ASCII:
#include <stdio.h>
void print_string(void * ptr) {
char * c = ptr;
printf("String is: %s\n", c);
}
void print_int(void * ptr) {
int * p = ptr;
printf("Int is: %d\n", *p);
}
int main(void) {
char * c = "Ptr";
int n = 7500880;
print_string(&n);
print_string(c);
print_int(&n);
print_int(c);
return 0;
}
which outputs:
paul@local:~/src/c/scratch$ ./testmem
String is: Ptr
String is: Ptr
Int is: 7500880
Int is: 7500880
paul@local:~/src/c/scratch$
It turns out that sometimes strings and int
s are somehow the same things. How is this so?
In this case, the three-character string "Ptr"
(four characters, including the terminating null) will be stored as the ASCII character 'P'
, which is 0x50
, followed by the ASCII character 't'
, which is 0x74
, followed by the ASCII character 'r'
, which is 0x72
, following by the null character, which is 0x00
. If the string began at memory location 100, the four byte would look like this:
-----------------------------------------
| 100 | 101 | 102 | 103 |
| 0x50 | 0x74 | 0x72 | 0x00 |
-----------------------------------------
The number 7500880
, which is 0x00727450
represented as hexadecimal, will be stored with the bytes in reverse order on a little-endian machine, and storing the four-byte integer 7500880
will also be represented by the four bytes:
-----------------------------------------
| 200 | 201 | 202 | 203 |
| 0x50 | 0x74 | 0x72 | 0x00 |
-----------------------------------------
So those four bytes in memory can be interpreted either as the integer 7500880
or as the string "Ptr"
- on this particular machine, the bit pattern to represent both pieces of data are identical. In other words, if I showed you this block of memory in the absence of any context:
-----------------------------------------
| 300 | 301 | 302 | 303 |
| 0x50 | 0x74 | 0x72 | 0x00 |
-----------------------------------------
and asked you, "did I store the string "Ptr"
at memory location 300
, or did I store an int
with the value 750880
at memory location 300
?", you would not be able to tell me. For that matter, I could have stored a four byte RGBA value at that location, with a red value of 80, and green value of 116, a blue value of 114, and an alpha value of 0, and you'd be none the wiser.
This is actually a specific observation of a general fact, that any information we store on a computer has to be represented as bits and bytes, and that the bits and bytes do not, themselves, have any meaning. This turns out to be true for any encoding whatsoever. For instance, if we stored the string "Ptr"
in UTF-16 rather than ASCII, then instead of being represented as 0x50747200
, it would be represented as 0x5000740072000000
, but the meaning would be the same.
So, if we are representing information as bits, and we want to have any fighting chance at all of any actual communication occurring then we need to be sure that the receiver knows how to decode the bits in a method consistent with the way we encoded them. I can sent you the message "SOS" in morse code with ... --- ...
, but if you have a broken morse code table that tells you ...
actually stands for "H", and ---
stands for "A", then the message will be lost forever.
The upshot of all this is that even when you know the size of the data stored at a particular address, this is still not enough - you also have to know how the data is represented.
For example, on my system, both long
and double
take up 8 bytes, and the value 0xDEADBEEF
can be represented precisely in both types, but the bit pattern for the long
will be:
0xEFBEADDE00000000
and the bit pattern for the double
will be:
0x41EBD5B7DDE00000
Same value, completely different representation at the bit level. So again, even if I know the address of the variable, and I know that it takes up 8 bytes, I still need to know more information - I need to know the type of the data because I cannot make sense of it without knowing how the data is represented, and long
and double
do not represent the same data in the same way.
So, to go back to the short answer, if you just used int
to store any and all pointers, or even if you just used a void *
, you couldn't make any sense of the data you found at those addresses, because you need to know both how much data is there, and how to interpret the bits you find there. When a pointer to an int
is stored in an int *
, and a pointer to a double
is stored in a double *
, and a pointer to a pointer to a char
is stored in a char **
, then you do know both of these things, and you can therefore make sense of your data.