This is a quick review of pointers in C. If you are not an expert in pointers, I would strongly
urge you to read everything on this page carefully. All the words here are carefully chosen to be
as correct as possible. If there is anything you do not understand or wondering about the choice
of words, please feel free to send an e-mail to the instructor. (If you see a bug on this page, please
also let the instructor know.)
Just like integers and characters, a pointer is a primitive data type in C/C++.
An integer takes up 4 bytes, a character takes up 1 byte,
and a pointer takes up 4 bytes on a 32-bit machine (and 8 bytes on a 64-bit machine, but we will assume that
we are running on a 32-bit machine for the rest of this discussion).
A pointer is simply a 4-byte quantity that contains a memory address.
Here's an example of how you would declare an integer variable, a character variable, and a pointer variable in C:
The first thing you need to get use to thinking about variables in this class is that a variable refers to an address, i.e., a memory location. We use variables to talk about memory! In an assignment statement, the left-hand-side refers to a memory location that can store a value. The right-hand-side is evaluated to obtain a value and this value is put into the memory location referred by the left-hand-side. When you see a variable somewhere in the right-hand-side of an assignment statement, you need to see what's stored in the memory location referred to by that variable and use the value stored there. In the above example, n refers to a memory location that can hold a 4-byte quantity. (Please note that the word "refer" has nothing to do with C++'s "reference variable".) By declaration, the compiler will interpret this memory location as containing an integer. Similarly, c refers to a memory location that can hold one byte of data. By declaration, the compiler will interpret this memory location as containing an character. Finally, p refers to a memory location that can hold a 4-byte quantity. Since it's declared (void*), the compiler just knows that it's a pointer, it can point to anything, i.e., it can contain a memory address of any object. (I'm using the word "object" to refer to anything in C. It can be an integer, a character, a data structure, a pointer, or even a pointer to a pointer of a pointer of a pointer.) n = 5 means to put an integer value of 5 into the memory location referred to by n. c = 'z' means to put an encoded character 'z' into the memory location referred to by c. p = NULL means to put a value of 0 into the memory location referred to by p since NULL is simply defined as 0. We would call p a "NULL pointer" because it contains memory address zero. It is known that there is nothing at memory address zero. It's perfectly okay to point to it. But if you try to "see" what's in memory address zero (i.e., by dereferencing p), you will get a segmentation fault and your program will be killed by the operating system. Please note that we often depict pointers by drawing arrows from a pointer type to another object. There are of course no arrows in memory. It would be more accurate to simply put a value of 0 into p in the above picture.
Continuing from the example above, let's see some assignments operations.
m = n means to evaluate the right-hand-side to get a numeric value and put the value into the memory location that's referred to by m. If you see a variable on the right-hand-side, you need to use the value at the memory location that's referred to by the variable. Similarly, p2 = p means to evaluate the right-hand-side to get a numeric value and put the value into the memory location that's referred to by p2. Since p is a variable on the right-hand-side, if you use the value at the memory location that's referred to by p, you would get 0. Therefore, you put 0 into p2. As a result, p2 will contain the same value as p (i.e., p2 will end up pointing to the same object as p).
On a 32-bit machine, the size of the address space (supposedly addressable memory locations) of a program is 232 bytes.
An address is a number that can be used to refer to a memory location in the address space of a program.
Therefore, an address is 4 bytes long on a 32-bit machine.
The data type of a variable's address is a pointer data type. All pointer data types are compatible.
Since a variable refers to a memory location, you can actually get the memory location to which it refers: &n denotes the address of n. Therefore, p3 = &n means to put the address of n into the memory location that's referred to by p3. The same address can also be put into the memory location that's referred to by p4 (pointer of a different type) since all pointer data types are compatible. Please note that it's perfectly valid to do: int *p5 = (int*)0x12345678;because it is possible to use 0x12345678 to refer to a memory location in the address space of a program. But if you try to dereference p5 and use what's in memory location 0x12345678, you need to make sure there's really something at location 0x12345678 or you will get a segmentation fault and your program will be killed by the operating system.
To dereference a pointer means to follow the pointer to where it points to and fetch what's there.
For example:
First, n set to contain 23. m = *p4 means to evaluate the right-hand-side and put the value in m. If we have just p4 on the right-hand-side, the right-hand-side would evaluate to what's in p4. But we have *p4 instead (where * is the dereference operator). So, we need to follow the pointer to where it points to and fetch what's there. p4 points to n; therefore, *p4 evaluates to what was in n and you end up putting 23 into m. Next, *p4 = 876 means to put a numeric value of 876 into the memory location referred to by *p4. Since p4 is a pointer, to get the memory location that's referred to by *p4, you dereference the p4 pointer by following the address stored at p4. As it turns out, you get the memory location referred to by n. So you end up putting 876 into n.
An array variable has the same type as a pointer. For example, if you have:
then a[0] and p5[0] refers to the same memory location, a[1] and p5[1] refers to the same memory location, and a[2] and p5[2] refers to the same memory location. You need to be careful with pointer math. Although p5 evaluates to the address of variable a, the expression p5+1 will be calculated as p5+sizeof(int) (which is p5+4, which is the same as &a[1]) because p5 is declared as a pointer to int. Similarly, since p6 is declared as a pointer to short, the expression p6+1 will be calculated as p6+sizeof(short), which is p6+2, which would not correspond to any element of the a[] array. Here's one weird thing about C. If a is an array, then a refers to the address of that array. Then what is &a? As it turns out, in C, if a is an array, &a is treated the same as a. Although an array has the same type as a pointer, this is how an array is different from a pointer.
In C++, there are "call by value", "call by pointer", and "call by reference" and it's all very confusing.
In C, there is only one way to pass parameters in making a function call and that's using "call by value".
This means that all function parameters are copied. For example, if you have:
void foo(a, b, c) { ... }and you call foo() by doing: foo(x, y, z)effectively, it's like performing assignment operations (although in reality, it's a lot more complicated as you will learn in Ch 3 of the textbook): a = x; b = y; c = z;no matter what the types of a, b, c, x, y, and z are.
Let's define a data structure:
typedef struct tagFoo { int x; int y[3]; char z[8]; } Foo; static Foo foo;The above says that the Foo data type/structure has 3 fields. The first field is referred to as x and it's a 4-byte integer. The 2nd field is referred to as y and it's an array of three 4-byte integers. The 3rd field is referred to as z and it's an array of eight one-byte characters. The size of a Foo data structure is therefore 4+12+8=24 bytes. The last line says that foo is an instance of the Foo data structure. Therefore, foo occupies 24 bytes of memory. Furthermore, you can use the "." operator to refer to different fields inside a data structure. Thus, foo.x refers to the first 4 bytes in foo (or zero byte into the foo data structure), foo.y refers to the next 12 bytes in foo (or 4 bytes into the foo data structure), and foo.z refers to the last 8 bytes in foo (or 16 bytes into the foo data structure). When you do: Foo *foo_ptr=(Foo*)malloc(sizeof(Foo)):you ask the memory allocator to allocate 24 contiguous bytes of memory from the heap and returns the first memory address of this block of data and store that address in the foo_ptr variable. Please understand that, colloquially, we say that foo_ptr is a "heap variable". Technically, that's incorrect. In the code above, foo_ptr is a local variable (a local pointer variable) that contains a heap address (or, "contains an address of a memory location that's in the heap segment of the address space"). When you then do: foo_ptr->yIt's simply a shorthand for: (*foo_ptr).y(*foo_ptr), according to what I wrote above, means that you interpret what's stored in foo_ptr as an address and you dereference the pointer. In this case, when you dereference that pointer, you get the first address of the 24 bytes of memory that corresponds to the Foo data structure. Then you use the "." operator to access a field inside that data structure.
It's very important to understand when memory exists for all types of variables. There are 3 types of variable: local variable,
dynamic variable, and global variable.
When you "create" a local variable, you are allocating memory "on the stack" (to be more specific, memory is allocated inside the "stack frame" that corresponds to the function in which the local variable is declared). We will discuss this in detail in Ch 3. When you create a local variable, this type of memory allocation is called "automatic allocation". Memory is allocated automatically when the function is called and it's deallocated automatically when the function is returned. So, for "automatic memory allocation", the lifetime of a local variable is the lifetime of the function, i.e., the variable starts to exist at the beginning of the function; when this function calls other functions, this variable continues to exist (why? because you create new stack frames on top of the current stack frame); when the function returns, the variable stops existing (why? because you delete the current stack frame). This is something very important to understand. What address do you get when you create a local variable? Well, it depends on where your current top of the stack is because when a function is called, a stack frame is created on top of the current stack frame. Then your local variables are automatically allocated inside that stack frame. Therefore, if you call the same function from different functions, the local variables may have differnt addresses. For dynamically allocated memory, the lifetime of a dynamically allocated block of memory is different and you, the programmer, also gets to decide when it starts existing and when it stops existing. When you call malloc(), the block of memory starts to "exist". Then you can make function calls or return from function calls, the block of memory continues to "exist". When you call free(), the block of memory "stops existing". We will also discuss malloc() and free() in detail in Ch 3. What address do you get when you call malloc()? Potentially, you can get a different address every time you call malloc(). If you free a block of memory that was returned from malloc(), you may get the same address next time you call malloc(), but then again, you may not. Dynamic memory is managed by a "memory allocator" which we will discuss in Ch 3. Please understand that in both cases, when I said "stop existing", it does not mean that they disappear from the address space. It means that you must not assume that the content in those memory locations are unchanged. Once a variable "stops existing", the content of the memory location it refers to can change "randomly" (and that's why you shouldn't refer to them any more). Finally, for global variables, they start existing when the program starts. They stop existing when the program is dead. You, as the programmer, get to decide what type of variable to use. Therefore, it's extremely important to understand the lifetime of each type of variable. |