 In an earlier video, I discussed the distinction between reference variables and value variables. A reference variable holds the address of a value, whereas a value variable directly holds a value itself. Variables in C are always value variables. Here, for example, this float variable Alex occupies in memory the number of bytes required to store a float value and directly stores any float value assigned to it. C does, however, have a near-functional equivalent of reference variables in the form of pointers. A pointer is a data type that represents an address and so effectively can reference a value elsewhere in memory. There isn't just one single data type called pointer, instead there is a pointer type for every other type in the language, e.g. an int pointer type, a double pointer type, and a char pointer type. The idea is that, say, int pointer values represent addresses of int values, double pointer values represent addresses of double values, char pointer values represent addresses of char values, and so forth. To declare a pointer, we use asterisk like a modifier on the base type. This syntax can be very confusing, especially when we introduce some complications later. So, for clarity, I'll display asterisks denoting a pointer declaration in orange to distinguish them from other uses of asterisk. This asterisk of a pointer declaration need not be written adjacent to the variable name as we see here. Whitespace after the asterisk is permitted and whitespace before the asterisk is only optional. However, for reasons that will become clear later, this is the preferred style. The reference operator returns a pointer value representing the address of a variable. More accurately, the reference operator works on what the C standard calls L values, which are any expressions which are valid targets of assignment. The L stands for left, as in the left side of an assignment operator. Variables are the most obvious kind of L value, but as we'll see shortly, there are a few other kinds. So, the reference operator works on any expression which is a valid target of assignment. Anyway, in this example, we declare an int variable i, an int pointer variable p, and then use the reference operator to assign to p an int pointer value representing the address of i. It's very important to understand that the reference operator used on an int variable produces an int pointer, not any other kind of pointer. If, say, we instead had a char variable c, the reference operator used on c returns a char pointer value, and so this assignment isn't valid. We cannot assign a char pointer value to an int pointer variable. An int pointer and a char pointer are two different types, just like an int and a char are different types. However, because all pointers are addresses and addresses in a single program are ultimately all the same kind of underlying data, a pointer of any type can always be cast to any other kind of pointer. Here, we cast the char pointer value to an int pointer, and so the compiler accepts this assignment. As we'll discuss shortly, pointer conversions like this can lead to nasty bugs if you're not careful. The dereference operator returns the value at the address represented by a pointer. The type of the returned value depends upon the type of the pointer, e.g. dereferencing an int pointer returns an int value. Confusingly, the dereference operator is an asterisk. I'll display the dereference operator in green to make it easily distinguishable from other uses of asterisk. Anyway, here we have an int variable i with a value 3, then an int pointer variable p to which we assign the pointer representing the address of i. In the last line, we use the dereference operator to get the int value at the address represented by the pointer p, and then add this int value to 2. So, because p represents the address of the variable i, which currently has the value 3, this adds 3 to 2, and so the new value assigned to i is 5. Now, aside from variables, dereferencing expressions are the other primary example of L values. A dereference as the target of an assignment represents a location to which we can assign a value. So, here we again have an int variable i and an int pointer variable p holding a pointer value representing the address of i. In the last line, we're assigning the int value 6 to the dereference of p, meaning the value 6 is copied to that address represented by the pointer value. Because p represents the address of i, this assignment does the same thing as if we assigned 6 to i directly. Be clear that the value's type must be the same as the type of the pointer. If p were, say, a float pointer, then the value to assign would have to be a float. Aside from dereferencing pointers, we can also perform addition and subtraction operations on pointers with integers. When we add an integer to a pointer, we get a pointer value of the same type, representing an address that is that number of places above in memory. To explain what I mean by place, it's easiest to just consider an example. Here, the reference of i returns an int pointer, and adding 2 to that pointer value returns an int pointer value that represents the address that is 2 ints above in memory. So if our compiler treats ints as 4 bytes, the address represented by this new pointer is the address of i plus 8. We generally don't know the exact addresses of our variables, but supposing that the variable i happens to be located at address, say, ffff008, then p would be assigned a pointer representing the address ffff0010, which is 8 bytes higher. Remember, we're dealing in hex here. Whatever the actual addresses and whatever the size of ints for your particular compiler, the consistent part is that there is space to store 2 ints starting at the original address, going up to but not including the new address. We can also subtract integers from pointers, which produces a pointer representing an address that is lower in memory rather than higher. So again, assuming 4 byte ints and assuming that the variable i happens to be located at address ffff008, then p would be assigned a pointer representing the address ffff0000, which is 8 bytes lower. Once we have a pointer value produced by addition or subtraction, we may dereference it just as we may any other pointer value. So here we can dereference p to get an int value even though p currently points to an address which may not even contain a variable. Regardless, this dereference will read the bytes at the represented address as an int value and copy that int value to the variable i. Likewise, we can assign to the dereference of pointers produced by a pointer arithmetic. This here copies the int value 6 to the bytes located at the address pointed to by p, wherever that may be in memory. It's very important to understand how dangerous pointer arithmetic can be. While our trivial examples here are a legal code that will compile, they wouldn't make sense in a real program and could trigger program failure. In these examples, we have no idea what resides at the memory pointed to by the results of our pointer arithmetic, and so it is totally unsafe to either read or write the bytes at those locations. As we'll see shortly, pointer arithmetic should only be used to get pointers that point to addresses which we know have been allocated in our process and for which we know what type of data is there. Not only can we add integers to pointers and subtract integers from pointers, we can also subtract one pointer from another if they are pointers of the same type. Doing so returns an integer expressing the distance between the two pointers in terms of that type. For example, if we have two float pointers that represent addresses 36 bytes apart, well then, assuming 4 byte floats, subtracting the pointer representing the lower address from the other pointer will return 9, because 9 floats fit in the space represented by the addresses of the two float pointers. If we reverse the subtraction, if we subtract the pointer representing the higher address from the other, then we would get negative 9. You might expect that C would also allow us to add one pointer to another, but this is not the case because adding one memory address to another produces no useful information. The fact that my house is located at street number 3 and yours is located at street number 5 has no logical relationship to the house that might exist at street number 8. In some low-level programming, such as when writing device drivers, situations may arise in which we wish to read or write a specific fixed address. For such scenarios, we can cast an integer to a pointer to get a pointer value representing that specified address. Here, casting a hexadecimal number to a char pointer returns a char pointer representing that specific address. When we then dereference p in the next line, we get back the char value at that address. Again, this is a very dangerous thing to do if you don't know for sure what, if anything, resides at these addresses. What C calls a null pointer is simply a pointer value representing the address 0. C guarantees that address 0 will be left unused, so a null pointer is guaranteed to represent nothing. Normally, we can't assign numbers directly to a pointer, but the number little 0 is a special case. When you assign the number little 0 to a pointer, C assigns a null pointer to the target without you having to cast the number to a pointer. Just like with number values, we can compare pointer values with comparison operators. As you would expect, if two pointers represent the same address, they will test equal, and if one pointer represents a higher address in memory than another, it is greater than the other. Also like number values, pointer values have truth value. A null pointer is considered false, while any non-null pointer is considered true. So, because C always leaves address 0 unused, we know that after assigning the dereference of variable i to p, the pointer held in p will hold a non-null pointer value. Therefore, this if condition will test true, and this not operation will return 0. If instead we assign a null pointer to p, then the same if condition will test false, and this non-operation will return 1. Void, as we briefly mentioned earlier, is a special type that represents nothing. A void pointer is a generic pointer type that can substitute for any other kind of pointer without casting. Here we have a void pointer variable terry, and an int pointer variable illsa. When we assign illsa to terry or terry to illsa, we can do so without casting. You may recall that we said C is a weakly typed language. Well, here's an example of what that means. Here we have a float variable f to which we assign the value 98.6. We then have a char pointer variable p to which we assign the reference of f cast to a char pointer, such that the value of p now represents the address of the first byte of the float variable f. We then increment p by 2, making the value of p represent the address of the third byte of the float. In the last line, we assign 1 to the dereference of p, so the char value 1 is written in the third byte of the float. Effectively, we have arbitrarily overwritten a portion of our float. In a strongly typed language, I should only be able to manipulate a piece of data with operations expressly defined for that type. But here we're using pointers to do an end run around the normal float operations. Arbitrally munging a random byte of a float makes no sense as far as the float type is defined. But see, let's us do this because it is weakly typed. In C, it's the programmer's responsibility to preserve the sanctity of the data types, not the language's responsibility.