I was trying to wrap my head around the concept of a variable.
Obviously it is implicitly clear how a variable works. However, I was trying to explicate my implicit knowledge and ran into some difficulties. Here is what I came up with:
A variable is a container of a certain size.
The size is both dependant of the data type in the declaration of the variable and the hardware (what specifically? word size?).
A variable has an address in memory that is stored within the preallocated size of that container (how is the name of the variable connected to its memory address?).
Within the reserved spot in memory for that variable, a value that corresponds to the data type of the declaration can be stored.
What of that is wrong or not precise (I'm sure much)? How can it be explained better?
In C, a variable consists of two things: an identifier and an object.
An identifier is a string of text that is used in source code to denote the object. (Identifiers may also denote functions, structure members, and other things.)
An object is “a region of data storage in the execution environment, the contents of which can represent values” (C 2018 3.1.15 1).
We generally think of an object has having a certain type. The type determines the meaning of the value stored in an object—the same bits may mean 3.75 when interpreted as a float but 1,081,081,856 when interpreted as an int. The C standard defines some properties of how types are represented (such as that some form of binary is used for integers) and requires C implementations to define the rest (except for certain aspects of bit-fields).
Therefore the “final say” on how any object is represented is up to each C implementation. Most C implementations are influenced by the hardware, as they are designed to work efficiently on their target systems, but a C implementation may provide 37-bit int objects on hardware that uses 32-bit words.
Earlier, I said we generally think of an object has having a certain type. When an identifier for an object is defined, storage is reserved for it. The amount of that storage is determined by the type. However, the actual interpretation of the value of an object depends on the expression used to access it. Almost all the time, we access an object using its declared type: After declaring float x;, we use x = 3.75; printf("%g\n", x);, and so on, and the type used to access the object in these expressions is float, the declared type of x. But C is flexible and allows us to set a char pointer to the memory using char *p = (char *) &x;, and then we can access the bytes of x using c[0], c[1], and so on. In this case, the type of the expression used to access the object, or its parts, is char, so we get char values instead of float values when using these expressions to access the object.
The compiler knows and arranges the connection between an identifier and its storage (memory). When an identifier for an object is defined, the compiler will plan storage for it (subject to program optimization by the compiler). That storage may be in a data section of the program or in the stack section or somewhere else. The compiler knows of ways to refer to the storage. Locations in the stack may be referred to by offsets relative to a stack pointer or a frame pointer. Locations in data sections may be referred to by offsets relative to a base address stored in a particular register by the program loader. Locations may be referred to by offsets relative to section starts or by absolute memory addresses. Whatever the case may be, when the compiler needs to generate instructions that access an object, it generates suitable instructions. This may be an instruction that includes in the instruction itself an offset relative to the stack pointer. Or it could be two or more instructions that add the offset to a base register and then use the result to access memory. Or it could be a partially generated instruction that is later completed when the program loader adjusts it to have the final address.
Related
I was playing with C, and I just discovered that a and &a yield to the same result that is the address to the first element of the array. By surfing here over the topics, I discovered they are only formatted in a different way. So my question is: where is this address stored?
This is an interesting question! The answer will depend on the specifics of the hardware you're working with and what C compiler you have.
From the perspective of the C language, each object has an address, but there's no specific prescribed mechanism that accounts for how that address would actually be stored or accessed. That's left up to the compiler to decide.
Let's imagine that you've declared your array as a local variable, and then write something like array[137], which accesses the 137th element of the array. How does the generated program know how to find your array? On most systems, the CPU has a dedicated register called the stack pointer that keeps track of the position of the memory used for all the local variables of the current function. As the compiler translates your C code into an actual executable file, it maintains an internal table mapping each local variable to some offset away from where the stack pointer points. For example, it might say something like "because 64 bytes are already used up for other local variables in this function, I'm going to place array 64 bytes past where the stack pointer points." Then, whenever you reference array, the compiler generates machine instructions of the form "look 64 bytes past the stack pointer to find the array."
Now, imagine you write code like this:
printf("%p\n", array); // Print address of array
How does the compiler generate code for this? Well, internally, it knows that array is 64 bytes past the stack pointer, so it might generate code of the form "add 64 to the stack pointer, then pass that as an argument to printf."
So in that sense, the answer to your question could be something like "the hardware stores a single pointer called the stack pointer, and the generated code is written in a way that takes that stack pointer and then adds some value to it to get to the point in memory where the array lives."
Of course, there are a bunch of caveats here. For example, some systems have both a stack pointer and a frame pointer. Interpreters use a totally different strategy and maintain internal data structures tracking where everything is. And if the array is stored at global scope, there's a different mechanism used altogether.
Hope thi shelps!
It isn't stored anywhere - it's computed as necessary.
Unless it is the operand of the sizeof, _Alignof, or unary & operators, or is a string literal used to initialize a character array in a declaration, an expression of type "N-element array of T" is converted ("decays") to an expression of type "pointer to T", and the value of the expression is the address of the first element of the array.
When you declare an array like
T a[N]; // for any non-function type T
what you get in memory is
+---+
| | a[0]
+---+
| | a[1]
+---+
...
+---+
| | a[N-1]
+---+
That's it. No storage is materialized for any pointer. Instead, whenever you use a in any expression, the compiler will compute the address of a[0] and use that instead.
Consider this C code:
int x;
void foo(void)
{
int y;
...
}
When implementing this program, a C compiler will need to generate instructions that access the int objects named x and y and the int object allocated by the malloc. How does it tell those instructions where the objects are?
Each processor architecture has some way of referring to data in memory. This includes:
The machine instruction includes some bits that identify a processor register. The address in memory is in that processor register.
The machine instruction includes some bits that specify an address.
The machine instruction includes some bits that specify a processor register and some bits that specify an offset or displacement.
So, the compiler has a way of giving an address to the processor. It still needs to know that address. How does it do that?
One way is the compiler could decide exactly where everything in memory is going to go. It could decide it is going to put all the program’s instructions at addresses 0 to 10,000, and it is going to put data at 10,000 and on, and that x will go at address 12300. Then it could write an instruction to fetch x from address 12300. This is called absolute addressing, and it is rarely used anymore because it is inflexible.
Another option is that the compiler can let the program loader decide where to put the data. When the software that loads the program into memory is running, it will read the executable, see how much space is needed for instructions, how much is needed for data that is initialized to zero, how much space is needed for data with initial values listed in the executable file, how much space is needed for data that does not need to be initialized, how much space is requested for the stack, and so on. Then the loader will decide where to put all of these things. As it does so, it will set some processor registers, or some tables in memory, to contain the addresses where things go.
In this case, the compiler may know that x goes at displacement 2300 from the start of the “zero-initialized data” section, and that the loader sets register r12 to contain the base address of that section. Then, when the compiler wants to access x, it will generate an instruction that says “Use register r12 plus the displacement 2300.” This is largely the method used today, although there are many embellishments involving linking multiple object modules together, leaving a placeholder in the object module for the name x that the linker or loader fills in with the actual displacement as they do their work, and other features.
In the case of y, we have another problem. There can be two or more instances of y existing at once. The function foo might call itself, which causes there to be a y for the first call and a different y for the second call. Or foo might call another function that calls foo. To deal with this, most C implementations use a stack. One register in the processor is chosen to be a stack pointer. The loader allocates a large amount of space and sets the stack pointer register to point to the “top” of the space (usually the high-address end, but this is arbitrary). When a function is called, the stack pointer is adjusted according to how much space the new function needs for its local data. When the function executes, it puts all of its local data in memory locations determined by the value of the stack pointer when the function started executing.
In this model, the compiler knows that the y for the current function call is at a particular offset relative to the current stack pointer, so it can access y using instructions with addresses such as “the contents of the stack pointer plus 84 bytes.” (This can be done with a stack pointer alone, but often we also have a frame pointer, which is a copy of the stack pointer at the moment the function was called. This provides a firmer base address for working with local data, one that might not change as much as the stack pointer does.)
In either of these models, the compiler deals with the address of an array the same way it deals with the address of a single int: It knows where the object is stored, relative to some base address for its data segment or stack frame, and it generates the same sorts of instruction addressing forms.
Beyond that, when you access an array, such as a[i], or possibly a multidimensional array, a[i][j][k], the compiler has to do more calculations. To do this, compiler takes the starting address of the array and does the arithmetic necessary to add the offsets for each of the subscripts. Many processors have instructions that help with these calculations—a processor may have an addressing form that says “Take a base address from one register, add a fixed offset, and add the contents of another register multiplied by a fixed size.” This will help access arrays of one dimension. For multiple dimensions, the compiler has to write extra instructions to do some of the calculations.
If, instead of using an array element, like a[i], you take its address, as with &a[i], the compiler handles it similarly. It will get a base address from some register (the base address for the data segment or the current stack pointer or frame pointer), add the offset to where a is in that segment, and then add the offset required for i elements. All of the knowledge of where a[i] is is built into the instructions the compiler writes, plus the registers that help manage the program’s memory layout.
Yet one more point of view, a TL;DR answer if you will: When the compiler produces the binary, it stores the address everywhere where it is needed in the generated machine code.
The address may be just plain number in the machine code, or it may be a calculation of some sort, such as "stack frame base address register + a fixed offset number", but in either case it is duplicated everywhere in the machine code where it is needed.
In other words, it is not stored in any one location. Talking more technically, &some_array is not an lvalue, and trying to take the address of it, &(&some_array), will produce compiler error.
This actually applies to all variables, array is not special in any way here. The address of a variable can be used in the machine code directly (and if compiler actually generates code which does store the address somewhere, you have no way to know that from C code, you have to look at the assembly code).
The one thing special about arrays, which seems to be the source of your confusion is, that some_array is bascially a more convenient syntax for &(some_array[0]), while &some_array means something else entirely.
Another way to look at it:
The address of the first element doesn't have to be stored anywhere.
An array is a chunk of memory. It has an address simply because it exists somewhere in memory. That address may or may not have to be stored somewhere depending on a lot of things that others have already mentioned.
Asking where the address of the array has to be stored is like asking where reality stores the location of your car. The location doesn't have to be stored - your car is located where your car happens to be - it's a property of existing. Sure, you can make a note that you parked your car in row 97, spot 114 of some huge lot, but you don't have to. And your car will be wherever it is regardless of your note-taking.
I am a bit confused by the key word "register" in C.
It seems to tell the compiler that it should be stored in a register which means that some of my variables are stored in Registers and some in memory? If so, is there a way to find out whether my value is stored in a register or in Memory?
For example:
int *x = (int*) 0x1234;
X does not seem to point on a register now, because such addresses are for memory.
But I have tried several times to find a different looking address (also using "register" key word). Even on the internet nobody seems to care.
So my main question: How does the address in a pointer look when it points on a register?
EDIT: I can't find an answer to my main question at the end of my question in the other question. My question is NOT about the key word "register", I just mentioned it.
The storage class specifier register was created on first release of C language to instruct compiler to use one of the processors registers to hold the variable having the scope to allow for faster access. At that time code writing required much attention from the programmer because the compilers were not so smart to detect errors or correct them leading to buggy code.
Anyway while growing-up the C standard got some tricks that made difficult to understand some details.
The storage class specifier register is one of those. So let's start looking at specs before to comment them.
From ISO/IEC 9899:2011, C11 standard:
§6.7.1 Storage-class specifiers
A declaration of an identifier for an object with
storage-class specifier register suggests that access to the object be
as fast as possible. The extent to which such suggestions are
effective is implementation-defined. (See note 119)
And the note 119:
NOTE
The implementation may treat any register declaration simply as an auto declaration. However, whether or not addressable
storage is actually used, the address of any part of an object
declared with storage-class specifier register cannot be computed,
either explicitly (by use of the unary & operator as discussed in
6.5.3.2) or implicitly (by converting an array name to a pointer as discussed in 6.3.2.1). Thus, the only operator that can be applied to
an array declared with storage-class specifier register is sizeof.
This means that the storage specifier register is a suggestion to the compiler to use processor registers or any other means to make the variable access as fast as possible, but the compiler can take a different decision. It can treat the declared variable as a standard auto variable.
In the last case it could be technically possible to obtain the storage address, but this is explicitly prohibited by the standard as explained in the note, but also enforced in the constraints of §6.5.3.2 Address and indirection operators:
The operand of the unary & operator shall be either a function
designator, the result of a [] or unary * operator, or an lvalue that
designates an object that is not a bit-field and is not declared with
the register storage-class specifier.
Of course some uncompliant compilers may let you apply the operator.
Now going back to your very question:
int *x = (int*) 0x1234; X does not seem to point on a register now,
because such addresses are for memory. But I have tried several times
to find a different looking address (also using "register" key word).
Even on the internet nobody seems to care.
Your example is wrong, you are declaring a pointer to an int and assigning an arbitrary address to it. This has nothing to do the register storage specifier.
Declare register int *x = (int*) 0x1234; and then try to apply the & operator to x as in int **pp = &x; (note that we are declaring a pointer to a pointer to int that is what we get taking the address of a pointer to int).
You'll get an error on compliant compilers.
So my main quesition: How does the address in a pointer look when it
points on a register?
The answer is simple: it doesn't resemble anything because it can't exist in standard C.
How does the address in a pointer look when it points on a register?
Pointers are a concept of memory, registers do not have addresses. Every processor has a limited, fixed amount of registers (8 - 16 probably).
As others have mentioned, the register is not really useful anymore and even sometimes ignored by compilers.
To understand what registers really are, consider this example:
int a = k / 53; // k is an int defined somewhere else...
int b = a * 9;
// a is not used after the above line
Now, we have not declared a with register but any reasonable compiler will still keep a in a register.
The reason for this is that to perform any operation the operands have to be in certain registers. The the result of the operation will then e.g. be stored in the register of the first operand.
In order to compute a from the above example, the compiler will write code to load k (which presumably is in memory) into a certain register (let's call it register A) and 53 into another. After the computation is done, register A will contain the result of the operation. Since we are going to multiple that result by nine in the next line anyway, we can just keep it were it is, load 9 into the other register and multiply. Storing the value into memory and then loading it back into a register would just waste a lot of time.
Note that declaring a with volatile would prevent optimizations like this and force the compiler to actually store and load a. (Although volatile does not make any sense with this example here at all, and is hardly ever useful if you do not, for instance, interface special kind of hardware.)
In modern compilers, an object you declare in C does not necessarily have a single location in which it exists. To optimize your program, the compiler may keep it in a register at some times, on the stack at others, or in a particular memory location at others. Normally, you do not need to know which.
When you take the address of the object and work with pointers, the compiler makes your program work as if the object did have a fixed address. Often this is done by putting the object in an assigned memory location, at least for the duration of time that you are using its address, but the compiler is allowed to achieve the end results of your program by other means.
If you declare an object with register, you are not supposed to, by the C standard, take its address. However, some compilers may permit this.
In most architectures you cannot point to a CPU register as they are not memory mapped. (8051 is one architecture I can think of where the core registers are memory mapped).
If you are referring to peripheral registers on the other hand; these are memory mapped and have addresses in the address space, and the declaration would be just as you have it - except you do need the volatile type modifier.
volatile int* x = (volatile int*)0x1234 ;
The register keyword has nothing to do with memory mapped peripheral registers.
Since addresses are numbers and can be assigned to a pointer variable, can I assign any integer value to a pointer variable directly, like this:
int *pPtr = 60000;
You can, but unless you're developing for an embedded device with known memory addresses with a compiler that explicitly allows it, attempting to dereference such a pointer will invoke undefined behavior.
You should only assign the address of a variable or the result of a memory allocation function such as malloc, or NULL.
Yes you can.
You should only assign the address of a variable or the result of a memory allocation function such as malloc, or NULL.
According to the pointer conversion rules, e.g. as described in this online c++ standard draft, any integer may be converted to a pointer value:
6.3.2.3 Pointers
(5) An integer may be converted to any pointer type. Except as
previously specified, the result is implementation-defined, might not
be correctly aligned, might not point to an entity of the referenced
type, and might be a trap representation.
Allowing the conversion does, however, not mean that you are allowed to dereference the pointer then.
You can but there's a lot of considerations.
1) What does that mean?
The only really useful abstraction when this actually gets used is that you need to access a specific memory location because something is mapped to a specific point, generally hardware control registers (less often: a specific area in flash or from the linker table). The fact that you are assigning 60000 (a decimal number rather than a hexadecimal address or a symbolic mnemonic) makes me quite worried.
2) Do you have "odd" pointers?
Some microcontrollers have pointers with strange semantics (near vs far, tied to a specific memory page, etc.) You may have to do odd things to make the pointer make sense. In addition, some pointers can do strange things depending upon where they point. For example, the PIC32 series can point to the exact same data but with different upper bits that will retrieve a cached copy or an uncached copy.
3) Is that value the correct size for the pointer?
Different architectures need different sizes. The newer data types like intptr_t are meant to paper over this.
Would we call the pointers in C a data type (of type void*) or just an operation done on an unsigned integer which makes its value get interpreted as a virtual memory address ? If it is a data type, which hardware and software factors impact its range ?
I think you are mixing two things together - Indirection operator and pointer variables.
Indirection operator (*) - The indirection operator is a unary operator that can be used to obtain the value stored at the memory location referenced by a pointer variable.
pointer variables - In C pointers are variables of specific type (e.g. int,char, void) that store addresses and can be null.
The size of pointers depends upon different factors. You can go to below link. They have explained it well.
What is the size of a pointer? What exactly does it depend on?
C thinks your whole computer is one massive array of bytes. Obviously this isn't very useful, but then C layers on top of this massive array of bytes the concept of types and sizes of those types.
Creating a block of memory inside your computer.
"Pointing" the name ptr at the beginning of that block().
This indirectly means it needs a type specifier which will be same as the data type it is pointing to.
Yes they are data types. Like void *.
intptr_t integers can store any pointers except function pointers. So you can think of such pointers to data objects as special integers. (But remember integer and pointer are different types.)
Your compiler generates code for your target environment. E.g. if you compile with the option -m32 then pointer size is probably 32 bits. It can be run on 32-bit architecture where the CPU register can store 32-bit memory address. Although 64-bit architecture may still support 32-bit application, you can use the option -m64 for compilation to benefit from bigger pointers (probably 64 bits).
You're asking about a lot of ideas. I'm going to try and cite the C standard as much as possible to give the official idea, then break it down into something digestible.
Would we call the pointers in C a data type (of type void *) or just an operation done on an unsigned integer which makes its value get interpreted as a virtual memory address?
From the C standard:
6.2.5 Types
A pointer type may be derived from a function type, an object type, or an incomplete type, called the referenced type. A pointer type describes an object whose value provides a reference to an entity of the referenced type. A pointer type derived from the referenced type T is sometimes called ‘‘pointer to T’’. The construction of a pointer type from a referenced type is called ‘‘pointer type derivation’’.
6.3.2.3 Pointers
An integer may be converted to any pointer type. Except as previously specified, the result is implementation-defined, might not be correctly aligned, might not point to an entity of the referenced type, and might be a trap representation.
Any pointer type may be converted to an integer type. Except as previously specified, the result is implementation-defined. If the result cannot be represented in the integer type, the behavior is undefined. The result need not be in the range of values of any integer type.
A pointer isn't a data type (at least in the traditional sense of the phrase) or an integer - it's a C programmer's way of denoting the location of a certain object. While we tend to think of that as being a number - i.e., 0xf000b3a8 - there's no reason it has to be a number. It just has to mark the location of a particular object. Furthermore, the type of the object it points to dictates its own pointer type. So a pointer with type of "float *" is equivalent to "pointer to float". It is not a float type - it's a pointer type.
For a concrete example of this, think about how you organize your bedroom - perhaps you have a bed, a desk, a set of drawers, a closet, and a bedside table. Before you go to bed, you pull out a sticky note and write "Remember headphones on desk". You then put that sticky note on your backpack. That sticky note is a pointer to your headphones! It represents a reminder to you of where those headphones are located - your desk, not a number.
The reason we tend to think of pointers as integers is from sections 6.3.2.3.5 and 6.3.2.3.6. Computers - as they're currently designed - don't understand the concept of "a desk" or "a closet". They only understand numbers - so we organize them accordingly. As a result, the C standard dictates that integers must be able to be converted into pointers, and vice-versa.
If it is a data type, which hardware and software factors impact its range?
Again, a pointer isn't really a data type - it denotes the location of a certain object (that in turn has a certain type that dictates the pointer's type). So we can't really talk about the range of a pointer, per se. What we can talk about is the range of possible memory addresses (and thus the range of possible locations that a pointer could point to!).
Back in the day, when dinosaurs roamed the earth and Nixon was in office (I'm a young'un, what can I say), Intel came out with the Intel 8008 microprocessor1. This puppy was the world's first 8-bit CPU - that is, it could perform mathematical operations on 8-bit values. Assuming 2's complement notation, this allows unsigned values of 0 to 28-1 (0 to 255) and signed values of -27 to 27-1 (-128 to 127). Furthermore, it had an external 14-bit address bus that could address up to 16KB of memory. This equates to 214 (16384) memory locations, each containing 8 bits. The full range is thus from byte 0 to byte 16383.
Fast-forward to today. Most modern desktop and laptop PCs run on 64-bit processors. These support (theoretically) 264 bytes of memory - a whopping 18,446,744,073,709,551,616 locations. This equates to approximately 16 exabytes - where one exabyte is 1,000,000 terabytes. (It's speculated that Google stores approximately this much data in their datacenters). Currently, however, most architectures only use the lower 48-bits2.
These represent the total number of physical locations that a pointer in C could theoretically point to. In reality, because of how virtual memory works, the range that you as a programmer will see is far more limited. That's a story for another day, however.
A pointer is a variable whose value is the address of another variable. Generally speaking, a pointer in C can point to any address within the virtual memory, and NULL.
A pointer is generally a variable whose value is the address of another variable, e.g. direct address of the memory location. Like any variable or constant, you must declare a pointer before you can use it to store any variable address.
The actual data type of the value of all pointers, whether integer, float, character, or otherwise, is the same, a long hexadecimal number that represents a memory address.
e.g: int *ip; /* pointer to an integer */
The run-time representation of a pointer value is typically a raw memory address (perhaps augmented by an offset-within-word field), but since a pointer's type includes the type of the thing pointed to, expressions including pointers can be type-checked at compile time. Pointer arithmetic is automatically scaled by the size of the pointed-to data type.
AS for void pointers (void *) point to objects of unspecified type, and can therefore be used as "generic" data pointers. Since the size and type of the pointed-to object is not known, void pointers cannot be dereferenced, nor is pointer arithmetic on them allowed, although they can easily be (and in many contexts implicitly are) converted to and from any other object pointer type.
See wiki
I'm still wondering why in C you can't simply set something to be another thing using plain variables. A variable itself is a pointer to data, is it not? So why make pointers point to the data in the variable when you can simply use the original variable? Is it to access specific bits (or bytes, I guess) of data within said variable?
I'm sure it's logical, however I have never fully grasped the concept and when reading code seeing *pointers always throws me off.
One common place where pointers are helpful is when you are writing functions. Functions take their arguments 'by value', which means that they get a copy of what is passed in and if a function assigns a new value to one of its arguments that will not affect the caller. This means that you couldn't write a "doubling" function like this:
void doubling(int x)
{
x = x * 2;
}
This makes sense because otherwise what would the program do if you called doubling like this:
doubling(5);
Pointers provide a tool for solving this problem because they let you write functions that take the address of a variable, for example:
void doubling2(int *x)
{
(*x) = (*x) * 2;
}
The function above takes the address of an integer as its argument. The one line in the function body dereferences that address twice: on the left-hand side of the equal sign we are storing into that address and on the right-hand side we are getting the integer value from that address and then multiply it by 2. The end result is that the value found at that address is now doubled.
As an aside, when we want to call this new function we can't pass in a literal value (e.g. doubling2(5)) as it won't compile because we are not properly giving the function an address. One way to give it an address would look like this:
int a = 5;
doubling2(&a);
The end result of this would be that our variable a would contain 10.
A variable itself is a pointer to data
No, it is not. A variable represents an object, an lvalue. The concept of lvalue is fundamentally different from the concept of a pointer. You seem to be mixing the two.
In C it is not possible to "rebind" an lvalue to make it "point" to a different location in memory. The binding between lvalues and their memory locations is determined and fixed at compile time. It is not always 100% specific (e.g. absolute location of a local variable is not known at compile time), but it is sufficiently specific to make it non-user-adjustable at run time.
The whole idea of a pointer is that its value is generally determined at run time and can be made to point to different memory locations at run time.
No, a variable is not a pointer to data. If you declare two integers with int x, y;, then there is no way to make x and y refer to the same thing; they are separate.
Whenever you read or write from a variable, your computer has to somehow determine the exact location of that variable in your computer's memory. Your computer will look at the code you wrote and use that to determine where the variable is. A pointer can represent the situation where the location is not known at the time when you compile your code; the exact address is only computed later when you actually run your code.
If you weren't allowed to use pointers or arrays, every line of code you write would have to access specific variables that are known at compile time. You couldn't write a general piece of code that reads and writes from different places in memory that are specified by the caller.
Note: You can also use arrays with a variable index to access variables whose location is not known at compile time, but arrays are mostly just syntactical sugar for pointers. You can think about all array operations in terms of pointer operations instead. Arrays are not as flexible as pointers.
Another caveat: As AnT points out, the location of local variables is usually on the stack, so they are a type of variable where the location isn't known at compile time. But the only reason that the stack works for storing local variables in a reentrant function is that your compiler implements hidden pointers called the stack pointer and/or frame pointer, and functions use these pointers to find out which part of memory holds their arguments and local variables. Pointers are so useful that the compiler actually uses them behind your back without telling you.
Another reason: C was designed to build operating systems and lots of low level code that deals with hardware. Every piece of hardware exposes its interface by means of registers, and on nearly all architectures, registers are mapped into the CPU memory space, and they have not to be in the same address always (thanks to jumper settings, PnP, autoconfig, and so on)
So the OS writer, while writing a driver for instance, needs a way to deal with what seems memory locations, only that they don't refer RAM cells.
Pointers serve to this purpose by allowing the OS writer to specify what memory location he or she wants to access to.