Lecture 29 - Pointers III

Goals

  • Learn how to return arrays from functions
  • Learn how to manually allocate memory to the heap with malloc and calloc
  • Learn some more about working with structs

Returning arrays

What about returning arrays?

array2.c


This code doesn't compile because we have two problems

func6.c: In function ‘f’: func6.c:14:10: warning: returning ‘int *’ from a function with return type ‘int’ makes integer from pointer without a cast [-Wint-conversion] 14 | return a; | ^ func6.c: In function ‘main’: func6.c:20:5: error: assignment to expression with array type 20 | a = f(); | ^


- we have the wrong return type
- we can't reassign  an array variable

Building on what we learned above, we can probably guess that we are returning the address of the array's values in memory.

If we want to store an address, we need a new data type: the **pointer**

We specify we want a pointer by adding a `*` to the declaration

```c
int * f(){

The function now returns a pointer (we don't have to do anything to the return statement since a is already an address)

We do the same thing to the variable we are going to store the result in


int * a;
a = f();

Unfortunately, this gives us a warning

func6.c: In function ‘f’:
func6.c:14:10: warning: function returns address of local variable [-Wreturn-local-addr]
   14 |   return a;
      |          ^

Why is returning the address of a local variable a bad thing?

the array is stored in a stack frame that we are about to dispose of

Creating arrays

Okay, let's return to the problem we were trying to resolve -- how can we return a new array from a function?

We need some memory that is not on the stack so that it is still available after the function call is done. So we need some new place in memory that will persist until we are done with it.

We are going to make a request to the operating system to ask it to find us a safe place for our data. We have two functions for doing this

  • malloc -- allocate a single block of memory
  • calloc -- allocate a sequence of contiguous memory blocks

These do pretty much exactly the same thing -- the only difference is in the arguments. For malloc we just ask for a big chunk of memory and it returns a pointer to it. The calloc function works very similarly. it just takes an extra argument that allows us to specify how many of these little blocks we want (which makes it ideal for allocating arrays). They both return (void * ) because it doesn't know what we are hoping to store there. Specifying that is left to us.

The sizeof function will lay a vital role in this process, helping us to determine how much space we need.

The location for these extra pieces of memory is a different part of main memory called the heap. The heap lives very low in memory and grows up towards the stack.

Warning -- allocating memory is extremely useful, and also quite dangerous. When we ask for a chunk of memory, we are given a pointer to the allocated space. Keeping track of it is our problem.

This probably doesn't sound that bad. However, it isn't that difficult to forget the address by letting a variable fall out of scope. We now have an orphaned block of memory. Our code can't use it because we have forgotten where it is. The system also doesn't know where it is -- it just knows that it was allocated. This little piece of memory will hang out, unusable, until the program exits. The longer we run the program, the more of these will clog up the works.

We call this process of gradually losing blocks of accessible memory to lost references a memory leak, and they are surprisingly common

In some languages, there is a garbage collector that runs periodically to remove all of the inaccessible blocks that have been allocated. We have to do it by hand in C.

  • free() - we pass this the address of something we want to release and it will release it back into the wild.
int *createArray(int length)
{
  int *p = calloc(length, sizeof(int));

  for (int i = 0; i < length; i++)
  {
    p[i] = i;
  }

  return p;
}

int main(int argc, char *argv[])
{
  int *a = createArray(5);

  for (int i = 0; i < 5; i++)
  {
    printf("%d: %d (%p)\n", i, a[i], &a[i]);
  }
}
$ ./array3
0: 0 (0xa832a0)
1: 1 (0xa832a4)
2: 2 (0xa832a8)
3: 3 (0xa832ac)
4: 4 (0xa832b0)

Notice that the addresses are now much lower in memory

Structs

What about structs? Well... the story is a little murkier there. struct can actually be passed by value and returned by value. How that works depends on the compiler and the size of the struct. For smaller structs, it might just cram the whole thing inside of a register -- we have 8 bytes to play with after all. For larger ones, it might break it up across registers. For returns, it might even reserve some space in the stack of the caller and pass the location for data to be returned there. It should "just work"

However, it is pretty common that we will want to reserve the memory ourselves if we are creating data structures.

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>

#define MIN(x,y) ((x < y) ? x : y)


struct color
{
  uint8_t r;
  uint8_t g;
  uint8_t b;
};

struct color *mix(struct color *c1, struct color *c2)
{
  struct color *result = (struct color *)malloc(sizeof(struct color));
  result->r = MIN(255, c1->r + c2->r);
  result->g = MIN(255, c1->g + c2->g);
  result->b = MIN(255, c1->b + c2->b);

  return result;
}

int main(int argc, char *argv[])
{

  struct color c1, c2;

  c1.r = 10;
  c1.g = 45;
  c1.b = 100;

  c2.r = 5;
  c2.g = 100;
  c2.b = 200;

  struct color *r = mix(&c1, &c2);

  printf("rgb(%u, %u, %u)\n", r->r, r->g, r->b);
}

The important thing to note here is that we are adding some new syntax. Creating structs on the heap and passing around pointers to them is just a common thing that we have a special syntax for "dereference the pointer and then access a field"

c->r is equivalent to (*c).r if c is a pointer to a struct color

Mechanical level

vocabulary

  • memory leak

Skills


Last updated 05/12/2023