You can view memory as one huge array of bytes (chars). This array has 232 or 264 elements. Usually, we consider the indices to this array in hexadecimal. In other words, the array goes from 0x0 to 0xffffffff (or 0xffffffffffffffff).
A pointer is simply an index of this array. Whenever we allocate x bytes of memory, we are reserving x contiguous elements from the memory array. If we set a pointer to these bytes, then that pointer will be the index of the first allocated byte in memory.
For example, look at the following program (in src/p3.c):
/* Print out pointers of local variables */
#include <stdio.h>
#include <stdlib.h>
int main()
{
int i;
char j[14];
int *ip;
char *jp;
ip = &i;
jp = j;
printf("ip = 0x%lx. jp = 0x%lx.\n", ip, jp);
return 0;
}
|
This program allocates one integer (i), an array of 14 characters (j), and two pointers (ip and jp). It then sets the pointers so that they point to the memory allocated for i and j. Finally, it prints out the values of those pointers -- these are indices into the memory array.
Unfortunately, when we try to compile this, we get warnings. Don't worry about them yet -- we'll get to that. It still compiles correctly.
When we run it, we get output that differs from machine to machine (and sometimes from run to run). For example -- here are different outputs from when I ran this in August, 2026 (Larry is my neighbor, and "Larry's Laptop" is the laptop that was in his closet gathering dust, so I put Ubuntu on it. Also, thanks to Jamin Hanna for buying a restored 1989 Macbook SE and running this code for me):
Machine Processor Output ------- --------- ------ Macbook Pro Apple M5 ip = 0x16bb2d360. jp = 0x16bb2d36a. Raspberry Pi3 (64-bit) ARM Cortex-A53 ip = 0x7fcb5e617c. jp = 0x7fcb5e6168. Raspberry Pi5 ARM Cortex-A76 ip = 0x7ffff6d281ec. jp = 0x7ffff6d281d8. Larry's Laptop Intel Core i3 ip = 0x7ffde8aa9be4. jp = 0x7ffde8aa9bfa. com1599@eecs.utk.edu Intel Xeon e5 ip = 0x7fff59fa25cc. jp = 0x7fff59fa25be. com2237@eecs.utk.edu Intel Core i7 ip = 0x7ffc2d4762fc. jp = 0x7ffc2d4762ee. Macbook Pro Intel Core i7 ip = 0x7ffee8d06e20. jp = 0x7ffee8d06e2a. Macbook SE/30 (1989) Motorola 68030 ip = 0xfffffe18. jp = 0xfffffe0a. |
What this means is that on that first machine (The Macbook Pro with an Apple M5 processor), when we view memory as an array, elements 0x16bb2d360, 0x16bb2d361, 0x16bb2d362, and 0x16bb2d363 are allocated for the local variable i, and elements 0x16bb2d36a through 0x16bb2d367 are allocated for the array j. When you run this on your own machine, you will get different pointer values. Regardless of the pointer values, ip will point to the first of the four bytes of i, and jp will point to the first of the fourteen bytes of j.
Note that I said ``jp = j'' and not ``jp = &j''. This is because when treated as an expression, an array is equivalent to a pointer. The only difference is that you cannot assign a value to an array variable. Thus, you can say ``jp = j'', but you cannot say ``j = jp''. Moreover, you cannot take the address of an array variable -- saying ``&j'' is illegal. (I went over all of that in the last lecture).
Pointers are a little like scalars -- they too can be declared as globals, locals or parameters, and can be assigned values, passed as parameters, and returned from procedures. On our lab machines, pointers are 8 bytes. Thus, in p3.c, there are 34 bytes of local variables allocated in the main() procedure -- 4 for i, 14 for j, 8 for ip, and 8 for jp.
One of the nice things about 32-bit machines is that their pointers don't seem so unwieldy. Back when it ran a 32-bit operating system, my Pi3 gave the following output to the program:
pi@raspberrypi:~/CS360/cs360-lecture-notes/CStuff-2$ bin/p3 ip = 0x7eeb050c. jp = 0x7eeb04fc pi@raspberrypi:~/CS360/cs360-lecture-notes/CStuff-2$That feels more manageable, doesn't it? Unfortunately, 32-bit machines are pretty hard to find these days -- most likely in microcontrollers, which you don't program very much (fun though they are to program)!
/* This program assigns a char to an int, and the int to a float.
Although it looks benign, there are some things going on
beneath the hood (changing number formats). */
#include <stdio.h>
#include <stdlib.h>
int main()
{
char c;
int i;
float f;
c = 'a';
i = c;
f = i;
printf("c = %d (%c). i = %d (%c). f = %f\n", c, c, i, i, f);
return 0;
}
|
The statement `i = c' is a type cast, as is the statement `f = i'. There are no surprises when we run this:
UNIX>. bin/p4 c = 97 (a). i = 97 (a). f = 97.000000 UNIX>
Some type castings, like the ones above, are very natural. The C compiler will do these for you without complaining. For most others, the C compiler will spit out a warning, unless you specifically tell it that you are doing a type cast. This is a way of telling the compiler ``Yes, I know what I'm doing.''
An example is program src/p3.c above -- as mentioned, when we compile it, we get warnings:
UNIX> make bin/p3
gcc -o bin/p3 src/p3.c
src/p3.c:16:39: warning: format specifies type 'unsigned long' but the argument has type 'int *'
[-Wformat]
printf("ip = 0x%lx. jp = 0x%lx\n", ip, jp);
~~~ ^~
src/p3.c:16:43: warning: format specifies type 'unsigned long' but the argument has type 'char *'
[-Wformat]
printf("ip = 0x%lx. jp = 0x%lx\n", ip, jp);
~~~ ^~
%s
2 warnings generated.
UNIX>
What's going on is that the compiler parses the format string of printf() and gleans that
"%lx" desires a long unsigned int, but it's getting an (int *). You go ahead
and perform a type cast on the argument to tell the compiler "Yes, this is an (int *),
but treat it like a (long unsigned int), please. I know what I'm doing." That's in
src/p5.c. To the right, I've used a typedef to make those typecasts
a little less unweidly. It's a good trick to know:
src/p5.c
/* Adding typecast statements to make the
warnings from src/p3.c go away. */
#include <stdio.h>
#include <stdlib.h>
int main()
{
int i;
char j[14];
int *ip;
char *jp;
ip = &i;
jp = j;
printf("ip = 0x%lx. jp = 0x%lx\n",
/* Here they are. */
(long unsigned int) ip,
(long unsigned int) jp);
return 0;
}
|
src/p5a.c
|
The compiler, happy that you have taken responsibility for using mixmatched types, compiles it without any warnings:
UNIX> make bin/p5 bin/p5a gcc -o bin/p5 src/p5.c gcc -o bin/p5a src/p5a.c UNIX> bin/p5 ip = 0x7fff57350268. jp = 0x7fff5735025a UNIX> bin/p5a ip = 0x7fff52ba1268. jp = 0x7fff52ba125a UNIX>As an aside, some compilers won't print out those warnings. For example, on my Pi-3, there is no warning on src/p3.c. So it goes.
/* A program where we inadvisedly typecast a pointer to an int and back again.
On machines with 8-byte pointers, this is a buggy activity, because you lose
data when you typecast from an integer to a pointer. */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef long unsigned int LUI;
int main()
{
char s[4];
int i;
char *s2;
/* Copy the string "Jim" to s, then turn the pointer into an integer i.
Print out the pointer's value, and i's value. */
strcpy(s, "Jim");
i = (int) s;
printf("Before incrementing i.\n");
printf("i = %d (0x%x)\n", i, i);
printf("s = %ld (0x%lx)\n", (LUI) s, (LUI) s);
/* Now increment i, and turn it back into a pointer.
Print out the pointers, and then attempt to print out what they point to. */
i++;
s2 = (char *) i;
printf("\n");
printf("After incrementing i.\n");
printf("s = 0x%lx. s2 = 0x%lx, i = 0x%x\n", (LUI) s, (LUI) s2, i);
printf("s[0] = %c, s[1] = %c, *s2 = %c\n", s[0], s[1], *s2);
return 0;
}
|
When you set i equal to s, you are losing 4 bytes of information, because ints are four bytes, and pointers are eight. When you set s2 back to i, it fills in the four bytes that i is missing, typically with zeros, but sometimes with -1's. In either case, it will be an illegal address, and you will get a segmentation violation.
Here is output from my Macbook Pro in 2011:
UNIX> bin/p8 Before incrementing i. # Notice that the last 8 hex digits of i and s match i = -206846176 (0xf3abc720) s = 140737281509152 (0x7ffff3abc720) After incrementing i. s = 0x7ffff3abc720. s2 = 0xfffffffff3abc721, i = 0xf3abc721 Segmentation fault # This is because s2 is an illegal address. UNIX>Why does s2 have all of those f's? Because the sign bit of i is negative. Thus, when we set s2 to i, it fills in the missing four bytes with ones, making s2 negative.
The compilers on our lab machines are happy to warn you about your potential problems, as evidenced by the warnings here:
UNIX> make bin/p8
gcc -o bin/p8 src/p8.c
src/p8.c:30:8: warning: cast to 'char *' from smaller integer type 'int' [-Wint-to-pointer-cast]
s2 = (char *) i;
^
1 warning generated.
UNIX>
On a machine with 32-bit pointers, this code will work fine, because now integers and
pointers are the same size. The compiler will not complain either:
# This was on a Raspberry Pi3 with a 32-bit operating system: pi@raspberrypi:~/CS360/cs360-lecture-notes/CStuff-2$ make bin/p8 gcc -o bin/p8 src/p8.c pi@raspberrypi:~/CS360/cs360-lecture-notes/CStuff-2$ bin/p8 Before incrementing i. i = 2126570764 (0x7ec0e50c) s = 2126570764 (0x7ec0e50c) After incrementing i. s = 0x7ec0e50c. s2 = 0x7ec0e50d, i = 0x7ec0e50d s[0] = J, s[1] = i, *s2 = i pi@raspberrypi:~/CS360/cs360-lecture-notes/CStuff-2$If we instead use a long for i instead of an int, everything works fine, since longs and pointers are guaranteed to be the same size, be that 4 or 8 bytes. The program src/p9.c makes the requisite changes:
/* This is the same as src/p8.c, but we've changed i to a long. */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef long unsigned int LUI;
int main()
{
char s[4];
long i;
char *s2;
strcpy(s, "Jim");
i = (long) s;
printf("Before incrementing i.\n");
printf("i = %ld (0x%lx)\n", i, i);
printf("s = %ld (0x%lx)\n", (LUI) s, (LUI) s);
i++;
s2 = (char *) i;
printf("\n");
printf("After incrementing i.\n");
printf("s = 0x%lx. s2 = 0x%lx, i = 0x%lx\n", (LUI) s, (LUI) s2, i);
printf("s[0] = %c, s[1] = %c, *s2 = %c\n", s[0], s[1], *s2);
return 0;
}
|
UNIX> make bin/p9 gcc -o bin/p9 src/p9.c UNIX> bin/p9 i = 140733481930528 (0x7fff1132cf20) s = 140733481930528 (0x7fff1132cf20) s = 0x7fff1132cf20. s2 = 0x7fff1132cf21, i = 0x7fff1132cf21 s[0] = J, s[1] = i, *s2 = i UNIX>Compilers and machines all differ. Some machines (like the Pi) have 32-bit pointers and rather laconic compilers that don't give you many warnings. Others, like our lab machines, have 64-bit pointers and downright chatty compilers. My philosophy is to try to program so that none of them have warnings. That can be a challenge, but you should strive to do the same.
SYNOPSIS
#include <stdlib.h>
void *malloc(size_t size);
void free(void *ptr);
Like new, malloc() allocates bytes of memory from the operating system.
Unlike new, which requires you to give it information about the data type that it is
allocating, malloc() simply asks for the number of bytes, and if it is successful,
it will return a pointer to at least that many bytes, allocated for you by the operating
system. It returns a void *, which means it's a pointer, but malloc() doesn't
know what it's pointing to. Fortunately, you do know what it's pointing to, and that is what
you set its return value to.
To figure out how many bytes you need from malloc(), you call sizeof(type). For example, to allocate one integer, you would call malloc(sizeof(int)). Often you want to allocate an array of a data type. To do that, you multiply sizeof(type) by the number of elements. Your pointer will point to the first of these elements. The next element will be sizeof(type) bytes after the pointer. And so on. We'll explore this more soon.
For now, take a look at src/pm.c:
/* The point of this program is to show how one may pass a region of bytes
(an array) from procedure to procedure using a pointer. */
#include <stdio.h>
#include <stdlib.h>
/* This allocates n integers, error checks and returns a pointer to them. */
int *give_me_some_ints(int n)
{
int *p;
int i;
p = (int *) malloc(sizeof(int) * n);
if (p == NULL) { fprintf(stderr, "malloc(%d) failed.\n", n); exit(1); }
return p;
}
/* This takes a pointer to n integers and assigns them to random numbers. */
void fill_in_the_ints(int *a, int n)
{
int i;
for (i = 0; i < n; i++) a[i] = lrand48();
}
/* This reads the command line, allocates, assigns and prints n integers. */
int main(int argc, char **argv)
{
int *array;
int size;
int i;
if (argc != 2) { fprintf(stderr, "usage: pm size\n"); exit(1); }
size = atoi(argv[1]);
array = give_me_some_ints(size);
fill_in_the_ints(array, size);
for (i = 0; i < size; i++) printf("%4d %10d\n", i, array[i]);
return 0;
}
|
The procedure give_me_some_ints() allocates an array of n integers and returns a pointer to the array. fill_in_the_ints() takes a pointer to the array, plus its size, and fills it in. Because we are passing pointers, no copies of the array are made. In other words, fill_in_the_ints() fills in the array that was created by the malloc() call. Finally, we print out the array.
Note the difference between C and C++ here:
free() returns the memory so that it may be reused. It is analogous to delete in C++. You simply pass it the pointer that malloc() returned. Don't pass it any other pointer, or a pointer that has already been freed, or really ugly things can happen (you'll see this in detail later in the class).
Although some people disagree with this, I am of the opinion that you should only free memory that you are going to need for reuse, or perhaps that the system may want to use for other programs. We'll see an example of that in the next lecture. If you are simply allocating memory and then exiting your program, don't bother freeing the memory. The operating system will reclaim it when the program exits.
|
In C, an array only exists with respect to a type. For example, "char a[10]" is an array
of 1-byte integers, and "int b[10]" is an array of four-byte integers.
The array itself is a contiguous region of bytes whose size is the number of bytes in the type multiplied by the number of elements in the array. Continuing the example above, a is composed of 10 bytes, and b is composed of 40 bytes. When treated as a pointer, the array points to the first byte in the region, and when you access element i of the array, that accesses bytes i*s to (i+1)*s-1, where s is the size of the type. Again continuing the examples above, a[3] accesses byte 3 of the region (zero indexed), and b[3] accesses bytes 12-15. |
You'll note that like arrays, pointers only exist in reference to a type. When you dereference a pointer, you get bytes 0 through s-1, starting at the value of the pointer.
And here's the confusing part: when you add i to the value of a pointer, it moves the pointer forward i values of the type. Therefore, it adds i*s to the value of the pointer. That is "pointer arithmetic", and you'll see it a lot in this class.
The reason element 0 is inaccessible is that it's a common bug to forget to initialize a pointer. When that happens, the pointer's value is zero, and when you try to dereference it, you'll generate a segmentation violation, which helps you find the bug.
The program src/pa.c is kind of like the "Hello World" of CS360 -- our first intentional segmentation violation:
/* Our first intentional segmentation violation. When we try to
access the element at address zero, (NULL is zero), the hardware
tells us that it's illegal by generating a segmentation violation. */
#include <stdio.h>
#include <stdlib.h>
int main()
{
char *s;
s = NULL;
printf("%d\n", s[0]); /* Seg fault here */
return 0;
}
|
On many machines (typically older ones), whenever you access a scalar type, its value in memory must be aligned. What this means is that if the type is 4 bytes, its location in memory must start at a memory index that is a multiple of 4. For example, if i is an (int *), then if i is not a multiple of 4, dereferencing i will be an error. This error is manifested by a bus error. Unfortunately, our current machines don't demonstrate this problem, so I won't write code for it.
In order to meet this requirement, compilers and runtime libraries have been designed with three features:
UNIX> bin/pd1-ci
Here is the Char_Int struct:
typedef struct {
char b;
int i;
} Char_Int;
The size of a Char_Int is 8
I have allocated an array, ci, of two Char_Int's at location 0x600003fb4030
&(ci[0].b) = 0x600003fb4030
&(ci[0].i) = 0x600003fb4034
&(ci[1].b) = 0x600003fb4038
&(ci[1].i) = 0x600003fb403c
UNIX>
|
Although the struct only uses five bytes (one for b and four for i), the size of the struct is 8 bytes. When we look at the pointers, we see that the first byte of the struct is where you find b, and i is four bytes after b. The three bytes between b and i are unused. Why is this so? The reason is that the pointer for i must be a multiple of four, and b's address has to come before i's address (this is a compiler standard). The only way to make that so is to have the three bytes after b be unused.
UNIX> bin/pd2-cccci
Here is the CCCC_Int struct:
typedef struct {
char b1;
char b2;
char b3;
char b4;
int i1;
} CCCC_Int;
The size of a CCCC_Int is 8
I have allocated an array, cccci, of two CCCC_Int's at location 0x600003a08030
&(cccci[0].b1) = 0x600003a08030
&(cccci[0].b2) = 0x600003a08031
&(cccci[0].b3) = 0x600003a08032
&(cccci[0].b4) = 0x600003a08033
&(cccci[0].i1) = 0x600003a08034
&(cccci[1].b1) = 0x600003a08038
&(cccci[1].b2) = 0x600003a08039
&(cccci[1].b3) = 0x600003a0803a
&(cccci[1].b4) = 0x600003a0803b
&(cccci[1].i1) = 0x600003a0803c
UNIX>
|
As you can see, this struct makes optimal use of memory -- the variables take up 8 bytes, and the size of the data structure is 8. The pointer for i is aligned - Perfect!
UNIX> bin/pd3-cici
Here is the C_I_C_I struct:
typedef struct {
char b1;
int i1;
char b2;
int i2;
} C_I_C_I;
The size of a C_I_C_I is 16
I have allocated an array, cici, of two C_I_C_I's at location 0x600000244040
&(cici[0].b1) = 0x600000244040
&(cici[0].i1) = 0x600000244044
&(cici[0].b2) = 0x600000244048
&(cici[0].i2) = 0x60000024404c
&(cici[1].b1) = 0x600000244050
&(cici[1].i1) = 0x600000244054
&(cici[1].b2) = 0x600000244058
&(cici[1].i2) = 0x60000024405c
UNIX>
|
In order to make sure that the pointers for i1 and i2 are aligned, we have to waste the three bytes after b1 and the three bytes after b2. If we defined b2 right after b1, the size of the data structure would be 12 rather than 16.
UNIX> bin/pd4-ic
Here is the Int_Char struct:
typedef struct {
int i;
char b;
} Int_Char;
The size of a Int_Char is 8
I have allocated an array, ic, of two Int_Char's at location 0x600002054030
&(ic[0].i) = 0x600002054030
&(ic[0].b) = 0x600002054034
&(ic[1].i) = 0x600002054038
&(ic[1].b) = 0x60000205403c
UNIX>
|
Most students think that sizeof(Int_Char) should be five, because the i pointer is the beginning of the struct, and should be aligned, and the b pointer doesn't have to worry about alignment. That would be true if we only allocated one of these structs. However, if we allocate an array of two structs, then the second one needs to be aligned too -- if the size of the struct were 5, then ic[1].i would not be aligned. To fix that, the size of the struct is 8, and the three bytes after b are wasted.
The bottom line is that if you care about memory, you should lay your structs out so that you don't waste too much memory. If you don't care about memory, don't worry about it (or just keep writing programs in Python.....)
/* This program shows how you lose information as you
convert data from larger types to smaller types. */
#include <stdio.h>
#include <stdlib.h>
int main()
{
char c;
int i;
int j;
i = 10000;
c = i; /* We are losing information here, because 10000 cannot be stored in a byte. */
j = c;
printf("I: %d, J: %d, C: %d\n", i, j, c);
printf("I: 0x%04x, J: 0x%04x, C: 0x%04x\n", i, j, c);
return 0;
}
|
Since c is a char, it cannot hold the value 10000. It will instead hold the lowest order byte of i, which is 16 (0x10). Then when you set j to c, you'll see that j becomes 16.
Also, even our chatty compiler doesn't complain about it. Make sure you understand this bug and the output below:
UNIX> make pc gcc -g -c pc.c gcc -g -o pc pc.o UNIX> pc I: 10000, J: 16, C: 16 I: 0x2710, J: 0x0010, C: 0x0010 UNIX>