You were probably taught that "variables hold a value," and that "different types hold different types of values."

The first statement is correct, but doesn’t show the full picture of what a variable is. The second statement is wrong.

What is a variable?

In programming, a variable refers to a segment of memory which is to be read from or written to during calculations.

This segment of memory has a specific length. Standard ints today are four bytes long. Since a bit can only store a certain amount of information, and bytes are collections of eight bits, there is only so much information that a four-byte int can hold.

Specifically, one of the thirty-two bits must be used to hold the sign of the number. The rest thirty-one are used to indicate the magnitude of the number. As such, a thirty-two bit int (or int32) can store integers between, inclusive, \(-2,147,483,648\) and \(2,147,483,647\).

A type does not refer to the type of value that is stored, but rather the type of value that should be interpreted from the stored data. A float, for example, which stores the value of \(\sqrt{2}\) to its highest precision, will be interpreted as something like \(1.414214\). However, if you interpret its data as an int, then you’ll get \(1,068,827,891\).

It’s kind of like words across different languages. In English, "die" means to cease living. In Dutch, "die" means this one. They’re denoted — stored — the same way, but the interpretation is different, which leads to unfortunate misunderstandings like this:

Mama, die, die, die…​; source: tumblr

Variables in Memory

In programming, a variable refers to a segment of memory which is to be read from or written to during calculations.
— Me, in like the first paragraph

Variables are put into segments of memory. So what’s memory?

I’m sure you’re familiar with what RAM sticks are: they’re your "working memory" while your computer runs. Each stick of RAM is delimited into little chunks of eight bits called bytes. So that we can find these bytes, each and every one of them has an address.

You can think of working memory as a giant warehouse.

Amazon warehouse; source: The Sun

In each little shelf is a small sheet of paper, holding an eight-bit binary number (or, if you wish, a number between 0 and 255).

The CPU is like a little office in the next building over. It doesn’t understand what’s going on; all it does is take instructions and carry them out.

One instruction says to assign byte \(\text{2300003A}_{16}\)[1] the value at byte \(\text{23000038}_{16}\). The CPU (office) requests for byte \(\text{23000038}_{16}\) from the RAM (warehouse), and so the workers at the warehouse retrieve this data for the CPU. The CPU then reads that the data is \(\text{00}_{16}\), and so tells the warehouse to assign byte \(\text{23000038}_{16}\) the value \(\text{00}_{16}\).

This is what an assignment between variables actually is[2]: the CPU requests from RAM, then tells RAM to change a value.

Now, most types take more than one byte, but the same process still works. If you’re using ints (four bytes), then the CPU will request four bytes and tell memory to assign four bytes. If you’re working with floats (four bytes), then the CPU will request and ask to write four bytes, but will use floating-point operations instead.

Consider what the following code does:

1
2
    int i = 0, j = 5;
    int k = i + j;

The CPU, reading these instructions, will assign memory values to each int. If we say that i is address 100, j is at 104, and k is at 108, then the process will be something like this:

  1. CPU tells RAM to assign 100 (four bytes) to 00000007

  2. CPU tells RAM to assign 104 (four bytes) to 00000005

  3. CPU requests the values of addresses 100 (four bytes) and 104 (four bytes), obtaining 000000007 and 00000005

  4. CPU adds these values together , obtaining 0000000C

  5. CPU tells RAM to assign 108 (four bytes) to 0000000C

Type Modifiers

Notice that we are specifying how many bytes to to assign and request. What if we only needed to use two bytes for our variable? What if we needed more than four bytes, because we expect to use numbers larger than \(2,147,483,647\)? What if we don’t need to store negatives in our variable?

This is where type modifiers come in. They modify the lengths and behaviours of variables.

There’s a few you should know about:

  • short: halves the length of a variable. Applicable to int only.

  • long: doubles the length of a variable. Applicable to int, long[3], char, and double[4]

  • unsigned: the first bit of the variable will now be used as another digit rather than a sign indicator. Applicable to int, long, and char.

  • signed: opposite of above. Not really too useful, since it isn’t really necessary much of the time.

  • const: prevents this variable from being altered. This is actually a compile-time behaviour, and doesn’t affect how the hardware handles things.

  • static: declares this variable with a global lifetime, despite that it may be in a non-global scope. Not particularly useful for your purposes.

There are many questions where you may be asked to compute something very large; these are where long longs become vital.

1
2
3
    short int si;       //declare a short int (2-byte int) called si
    long long ll;       //declare a long long (8-byte int) called ll
    unsigned int ui;    //declare an unsigned int (4-byte u-int) called ui

Oftentimes the standard library will use types like uint32_t, or size_t. The <stdint.h> and <stddef.h> libraries contains these types. If you get compiler errors that mention these, now you might know what the heck they are!

Pointers

Recall our little exercise at the end of the Variables in Memory section.

Addresses are just numbers — four-byte numbers, at that, if you’re running 32-bit programs[5]. What if you wanted to say "hey, get me the value stored at the address indicated by that variable"?

a pointer; source: wikipedia

Essentially, you’re setting a variable to reference some address in memory.

Now, they’re not actually references.

Important

Pointers are not actually references.

Important

Pointers are not actually references.

Pointers merely store a number. When interpreted as a pointer, it refers to an address. But this is only when interpreted with the right instructions.

Changing the value of the pointer itself will only move the "arrow of reference." It doesn’t change what the pointer is pointing to.

To use it, you must dereference it first by telling the CPU, "hey, instead of just changing this address (the pointer), change the address (the address "referenced" by the pointer) that this address (the pointer) stores."

Take a second to digest that sentence; the distinctions are important.

There’s a few new operations you now need to know:

1
2
3
4
5
6
7
8
9
10
    int a = 5;
    int *p;
    p = &a;
    *p += 50;
    new p[a];
    *p = 0;
    *(p+1) = 1;
    p[2] = 2;
    delete[] p;
    p = NULL;
  • Line 2: when declaring a pointer, an asterick must be placed before the variable name, which denotes it as a pointer. This is a type modifier, applicable to all types[6]. Whitespace does not matter: int * p, int* p, int *p, int*p all do the same thing[7].

  • Line 3: to set a pointer point to another variable, use the addressof operator. In C++, that’s &.

  • Line 4: to actually use the memory you’re referring to, you have to dereference a pointer with the dereference operator *[8]

  • Line 5: you can dynamically allocate memory using the new and new[] operator. In this case, p became a pointer "pointing to an array"[9] of length 55 (that was the value of a).

  • Line 6: dereferencing p now refers to the "first element" of "the array".

  • Line 7: to access the next element, go 1 block after the location specified by p. This is why arrays are zero-indexed.

  • Line 8: as a shorthand to the previous notation, you can use the index operator for quick pointer arithmetic.

  • Line 9: when you’re done with memory, delete it. If you keep asking for more memory, then you’ll run out sometime and you’ll get a segfault. No one likes a segfault.

  • Line 10: if a pointer isn’t in use — that is, it’s not referring to anything useful — then it should be assigned to NULL. Attempting to dereference a null pointer will immediately crash your program, which will immediately tell you whether or not you’ve made an incorrect reference.

Important

Why use null pointers if they’ll crash your program? The alternatives are likely worse.

If you delete memory, but leave your pointer pointing to that deleted (no longer yours) memory, you’ll have a dangling pointer. Some other process might then grab that memory. What if that other process is your operating system?

What if you then write over that memory that your operating system is using?

At least you can check if a pointer is null by checking equality: p == NULL.

Note

Notice that with pointer arithmetic we can pretty much add or subtract anything we want. If you really wanted, you can try accessing p[55], despite it being past the length of the array.

Or p[-1].

Be careful when using pointers and indicies in C++; you’ll need to keep track of everything yourself.

Because of how pointers can be used to dynamically refer to memory, they are extremely helpful.

Memory and String Manipulation with <string.h>

Recall that in the last lesson that I mentioned that you can use char[] for strings. Since char*[10] can also be an array of char, they can also be strings. In C, this is exactly what strings are: char*.

Memory and string manipulation in the <string.h> library work exclusively with pointers. The most important function you need to know is memset().

If you’d like to really practice working with pointers, try writing your own versions of some of the functions in <string.h>, starting with memset() and memcpy().

Pointers in STL Functions

Many functions in the standard library use pointers to denote the beginning and end of ranges[11]:

1
2
3
4
    int arr[]={9,8,7,6,5,4,3,2,1,0}; //size 10
    std::vector<int> varr(arr, arr+10);
    std::sort(arr, arr+10);
    std::cout << std::min_element(arr, arr+10);

Multidimensional Arrays

In many languages, you can have multidimensional arrays. In C++, we don’t worry about classifying what arrays are single-dimensional and what are multidimensional, because a two-dimensional array is just an array of arrays[12]. A three-dimensional array is just an array of arrays of arrays.

1
2
3
4
5
6
7
8
	//make a 10x10 multiplication table
    int **two_d_arr = new int*[10];
    for (int i = 0; i < 10; ++i)
    {
        two_d_arr[i] = new int[10];
        for (int j = 0; j < 10; ++j)
            two_d_arr[i][j] = i * j;
    }

Line 7 is equivalent to *(*(two_d_arr+i)+j) = i * j;


1. this is hexadecimal, a 16-digit base (or radix) for numbers. It’s a common way to represent binary numbers, since it’s much more compact than writing every bit out. This way, each digit represents one nibble, or half-byte.
2. actually there’s also the cache between the RAM and CPU, which speeds up this retrieval process. But that’s not so important to know right now.
3. due to legacy issues, the long is a 4-byte int. long int may actually refer to a 4-byte int depending on your operating system, and so if you want an 8-byte int you’ll need a long long.
4. actually, it makes doubles 12 bytes long up from 8 bytes; doubles are already "long floats."
5. if you’re running 64-bit programs, they’re eight bytes.
6. a pointer is a type, and so you can have a pointer to a pointer. Or a pointer to a pointer to a pointer. Or a pointer to a pointer to a pointer to a pointer to a pointer…​ you get the point. You’d declare it as int ***** fifth_dimentional_pointer.
7. you can declare different levels of pointers on the same line: int a, *b, **c gives you an int a, a pointer to int b, and a pointer to a pointer to int c.
8. the dereference operator has a lower precedence than the access operator .; if you’re using a pointer to a struct or class, then you need specify that you’re dereferencing first: (*obj).member. There is a shorthand for this: obj->member does the same thing.
9. it isn’t, it’s pointing a memory location which the operating system treats as a contiguous block of memory. It can act like an array, but it needn’t.
10. pointer to char
11. if you have two pointers, l, the "beginning" of the range, and r, the end, then the range will include l and exclude r. In interval notation, that’s [l,r).
12. actually, "multidimensional arrays" as a specific type do have some optimizations over "arrays of arrays", but they aren’t particularly important here.