2
$\begingroup$

I was trying to understand pointers by watching YouTube videos. However, I could not understand

  • How do they work?
  • Why do we use them?
  • When do we use them?
$\endgroup$
2
  • 1
    $\begingroup$ If you're trying to use YouTube to learn coding, you're really in the wrong place! You need to have to stuff written down to be able to reread it. And you need to actually type stuff in yourself and run the code, because coding is as practical a skill as sawing wood. Go to w3schools.com and follow their C++ tutorial. (Don't worry - C and C++ are close enough for what you're doing.) $\endgroup$
    – Graham
    Nov 1 '21 at 8:12
  • 1
    $\begingroup$ Yes, I'm aware of it, but I'm a visual learner, so I try to watch YouTube videos first, then read and take notes so everything is clear when I start coding. @Graham $\endgroup$
    – salluc 1
    Nov 1 '21 at 15:37
6
$\begingroup$

In general, a pointer is a variable which holds the address of a variable. You can use https://godbolt.org/ to find out the assembly equivalent of a pointer. For example,

void func(){
    int* pointer;
    int a = 3;
    pointer = &a;
}

compiles to

pushq   %rbp
movq    %rsp, %rbp
movl    $3, -12(%rbp)
leaq    -12(%rbp), %rax
movq    %rax, -8(%rbp)
nop
popq    %rbp
ret

This is a full function so there is some useless stuff. The useful stuff is the following:

movl    $3, -12(%rbp)
leaq    -12(%rbp), %rax
movq    %rax, -8(%rbp)

The first line puts 3 in the variable a which is allocated on the stack. The second line uses the lea instruction to put the address of the variable a in RAX. The third line puts RAX in the pointer called pointer.

Now for the stuff I called useless earlier. When you enter a function in C++/C, it will:

  1. Push RBP on the stack;

  2. Put RSP in RBP;

  3. Decrement RSP of the allocated stack space for the function.

After those 3 steps, the variables local to the function are accessed using negative relative offsets from RBP. In this case, the substracting RSP part has been optimized away.

This is how pointers work at the low level. They are allocated on the stack if they are local to the function. If they aren't, then they are allocated in the data segment of the executable. This segment will be accessed by default using a relative offset from RIP (the instruction pointer).

Now that excludes smart pointers and stuff like that which are best practice nowadays. A useful program (excluding libraries) is probably using C++ and some variant/custom smart pointers nowadays. Otherwise, this developer is wasting time to end up with a buggy/crashing program. As to libraries, most of the time, the allocation is left to the user of the library by calling some allocating functions.

Why do we use them?

When do we use them?

I think those 2 questions ask the same thing. In general, pointers are used because they allow to allocate memory. You cannot allocate memory in C++/C without receiving a pointer to the allocated memory. This is due to the underlying way that the operating system works. The user mode program makes a syscall asking for space in RAM and the OS returns a pointer to the beginning of the space it allocated.

This allows for dynamic programming where you don't know the size of everything at compilation time.

This allocated space will also survive exiting the local scope of the function where it was allocated. This is useful especially if you are working in several threads which is often the case in real world applications.

I would tend to say that statically allocated pointers are used mostly with arrays. The name of the array in C++/C is a pointer to the first member of the array. Otherwise, they are used for dynamic allocation because it cannot be done differently due to the way the underlying OS works.

How do they work?

This is hard to explain in one small answer. The statically allocated pointer is a variable allocated on the stack. It contains the address of another variable or of an array member. It can even contain an absolute address. For example, one can write:

unsigned int* pointer = (unsigned int*)0x8000;

The pointer called pointer now points to address 0x8000. By typing *pointer = 3;, address 0x8000 now contains 3. This is to avoid of course in a user mode program but can be useful when writing an OS from scratch. This is undefined behaviour in a user mode program because no one knows if the address 0x8000 in the virtual address space has been allocated for your program. Accessing this address could do anything including a page fault and killing your program.

$\endgroup$
6
  • $\begingroup$ This is a rather dangerous answer, given the target audience. It silently assumes x86-64 (in the register names), but that's a CPU with many registers. Since a pointer fits into a register, a local pointer variable will likely not end up on the stack at all. $\endgroup$
    – MSalters
    Nov 1 '21 at 8:55
  • $\begingroup$ I'm quite sure @salluc1 is a beginner so I assumed x86-64 desktop as a programming environment. $\endgroup$
    – user123
    Nov 1 '21 at 10:50
  • $\begingroup$ As shown in my example, gcc does put the address of 'a' on the stack after RAX. $\endgroup$
    – user123
    Nov 1 '21 at 10:57
  • $\begingroup$ I've copied your code to godbolt.org, and it turns your code into func(): ret. No surprise there, as the code has no observable effects. $\endgroup$
    – MSalters
    Nov 1 '21 at 11:21
  • 1
    $\begingroup$ Thank you so much that was really helpful @user123 $\endgroup$
    – salluc 1
    Nov 1 '21 at 19:24
3
$\begingroup$

While the answer of user123 covers a number of the technicalities well, I think a small memory illustration may be useful. You can generally think of a pointer as an index into physical memory. Let's see a little bit of what that might look like:

Empty Memory:

[####][####][####][####][####][####][####][####]
64      68       72      76      80      84      88      92

Here I use a '#' to represent a byte (8 bits, each of which can be 0 or 1). I have separated them in groups of four because that is the size of an int and unsigned int in C. Unsigned int ranges from 0 to 4,294,967,295 and int from -2,147,483,648 to 2,147,483,647. We haven't initialized any memory here, but there still are bits there, they're pretty random because they are simply whatever that memory was last used for (or even more random in a freshly booted system). Below each set of four, I am writing the address of the memory. Suppose I allocate an integer array of length 2. The operating system will give back a pointer, say myPointer, that contains the start of the allocated block of memory, in this case suppose it is 72.

So now I have the pointer myPointer with a value of 72.
myPointer = 72
*myPointer dereferences myPointer, so this gives us the value of the thing being pointed to. In order to do this operation we need to know what type we're trying to read. Let's take a close look at the memory from 72-79, 8 bytes:
[11111101][11110000][11100111][00100100][00100110][00111000][10011000][00000011]
72                                                                     76
A bunch of random bits. If we try to read the first number as an unsigned integer (so we read the first 4 bytes starting from 72, i.e. [11111101][11110000][11100111][00100100], we get 4260423460, but if we read it as a signed integer (same for bytes) we get -34543836 (I think at least, though I guess this could theoretically be different in some implementations of C/C++ http://www.open-std.org/jtc1/sc22/wg14/www/docs/n2218.htm ) Here we will read by default as a signed integer, since we allocated an integer array and thus have an integer type pointer.

Ok, suppose we follow best practice and initialize our memory, we can do it with dereferencing as follows: *myPointer = 0;
*(myPointer+1) = 0;
Why 1 you ask? Don't we need to move over 4 bytes? Yes, but C and C++ know the type of the pointer and while doing pointer arithmetic they automatically multiply the increment by the size of the type (4 for integers, and since this was an integer array we will have an integer pointer), so here\ (myPointer+1) is 76 and (myPointer+2) is 80. There is more coverage regarding dereferening of pointers here: https://stackoverflow.com/questions/4955198/what-does-dereferencing-a-pointer-mean Anyway, after proper initialization to zero, the memory is all zeros:
[00000000][00000000][00000000][00000000][00000000][00000000][00000000][00000000]
72                                                                     76

As a convenient shorthand we can write *(myPointer+j) as myPointer[j] in C and C++, which is the notation you will usually see for accessing arrays. Since addition is commutative, myPointer+j is the same as j+myPointer, so it is also the case that myPointer[j] is the same as j[myPointer], though this later form is not intuitive and you're highly unlikely to see it in real code.

This answer is incomplete, but was too long to make as a comment, apologies for the partial answer.

$\endgroup$
1
  • $\begingroup$ Thank you very much for making things so clear for me. $\endgroup$
    – salluc 1
    Nov 1 '21 at 19:37

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.