Pointers, Dynamic memory, and Segmentation faults

We’ve alluded to pointers a few times in the previous sections, and it seems about time we actually cover them. Pointers in C have a reputation for being confusing. Don’t worry though! We’ll go through plenty of examples. You’ll be past this hurdle to learning C in no time. Indeed, we’ve already been using them.

All variables, named constants, even functions, in your program have to be stored in memory somewhere. Memory is separated (roughly) into 1 byte blocks, and each block has an address. Pointers are variables that store these addresses.

Reference and dereference operators

The fundamental operators for producing and interacting with pointers are the reference & and dereference * (unary) operators. For any given variable, the reference operator & produces a pointer to that variable. That is to say, it finds the address of where that variable resides in memory. The dereference operator does precisely the opposite. Dereferencing a pointer gives back whatever is actually stored at that location.

Consider this code snippet:

 1/* File: pointers1.c
 2 * Author: Ian May
 3 * Purpose: Demonstrate reference and dereference operators
 4 */
 5
 6#include <stdlib.h>
 7#include <stdio.h>
 8
 9int main()
10{
11  /* Declare a few variables */
12  int n = 5;
13  double q = 1.3;
14  
15  /* Declare a few pointers */
16  int *pn; /* Uninitialized pointer */
17  double *pq = &q; /* Pointer initialized to address of variable q */
18  
19  /* Use these pointers */
20  pn = &n; /* Assignment into a pointer */
21  printf("n = %d, and is stored at address %p\n",n,pn);
22  printf("q = %f, and is stored at address %p\n",q,pq);
23
24  /* Equivalently we could print q using */
25  printf("q = %f, and is stored at address %p\n",*pq,pq);
26
27  /* Assign value to variable by dereferencing a pointer */
28  double r = *pq; /* Equivalent to 'double r = q;' */
29  printf("r = %f\n",r);
30
31  /* Modify a variable through a pointer to it */
32  *pq = -5.2;
33  printf("Now q = %f, and is stored at address %p\n",q,pq);
34  
35  /* There can be multiple pointers to one location */
36  double *po;
37  po = pq; /* Assigning pointer to pointer copies address, not value */
38  *po = 68.8; /* Modify q through the other pointer */
39  printf("Now q = %f, and is stored at address %p\n",q,pq);
40  
41  return 0;
42}

Download this code

This can be compiled by calling:

gcc pointers1.c -o pointers1.ex

Let’s walk through this program rather carefully. First consider how a pointer is declared, e.g.:

int *pn;

Note that the dereference operator is present within the declaration. There are two ways to think about what is happening here, and it is beneficial to turn this declaration into an English sentence. As written you could say “pn is a variable that dereferences into an int”. The declaration can equivalently be written as:

int* pn;

which you might interpret as meaning “pn has type int*, which denotes a pointer to an int”. This is arguably the correct interpretation in C++, but the former is more common in C as it more accurately reflects how the type system works. You can say either one to yourself, and the key message is that turning statements with pointers into full sentences can help you reason about what is happening.

Let’s make the same observations about the next line:

double *pq = &q;

Again, the left hand side says pq is something that dereferences into a double. Furthermore, we are initializing pq to hold the address of the variable q by using the reference operator.

Later, we print the value q and the address where it is stored. Note that addresses are most conveniently written in hexadecimal, which can be done through the %p format specifier. What can you observe about the relationship between the address of n and of q?

The previous question raises another: if all pointers are just addresses in memory, and thus just integers of some width, why do pointers have types associated with them? Addresses vary at the byte level, but the data stored there very likely occupies multiple bytes. The pointer corresponds to the first address in memory where something resides, but to get the actual data item we need to know how many additional bytes should also be considered.

The integer n resides in memory starting at the address printed by the program, but occupies an additional 3 bytes after this too (4 bytes total).

There are a few other observations we should make here:

  • Line 28 creates another double variable, and sets it equal to q through the pointer pq

  • Similarly, line 32 modifies the value of q through the pointer pq

  • There can be multiple pointers to one address, for instance line 37 also modifies q through a pointer, but now using po instead of pq

    • Note that either way, the variable q has changed value

Remark: Does this last point remind you of anything from Python?

Remark: On line 16 the pointer pn is declared but not initialized. We’ve seen before that using uninitialized values can lead to unintended effects. What happens if we try to dereference this uninitialized pointer? For instance, try assigning n = *pn before pn is set. This might give you a segmentation fault, it might fill n with random garbage. The behavior is undefined, and typically not reproducible.

One way to avoid bugs, or at least hard to understand bugs, is to give your pointers some initial address, like on line 17. You may not have a specific address ready where you declare the pointer, and in this case you can use NULL. This is a special address, typically 0x0, that will always give a segmentation fault when dereferenced. It seems odd that you would want this, but de-bugging reproducible errors is much easier than dealing with random or silent errors.

Exercise: Try assigning n from *pn before pn is set. Observe the claimed behavior above, and try running the program through Valgrind or GDB.

Passing by reference

So, we can see the memory addresses where variables live, and we can interact with those variables through these pointers, but what can we actually do with pointers that we couldn’t do before?

For one thing, we can pass variables to functions by reference instead of by value. Consider this small example:

 1/* File: refsAndVals.c
 2 * Author: Ian May
 3 * Purpose: Demonstrate passing arguments by reference and by value
 4 */
 5
 6#include <stdlib.h>
 7#include <stdio.h>
 8
 9void modify1(int n)
10{
11  printf("Inside modify1, n = %d\n",n);
12  n += 10; /* Make n larger by 10 */
13  printf("Inside modify1, n = %d\n",n);
14}
15
16void modify2(int *pn)
17{
18  printf("Inside modify2, n = %d\n",*pn);
19  *pn = 10; /* Set to a value of 10 */
20  printf("Inside modify2, n = %d\n",*pn);
21}
22
23int main()
24{
25  /* Declare a variable in the main scope */
26  int n = 5;
27  printf("In main, n = %d\n",n);
28
29  /* Call modify1 and print value again */
30  modify1(n);
31  printf("In main, n = %d\n",n);
32  
33  /* Call modify2 and print value again */
34  modify2(&n);
35  printf("In main, n = %d\n",n);
36  return 0;
37}

Download this code

This can be compiled by calling:

gcc refPassing.c -o refPassing.ex

Some questions to ponder:

  • Why precisely does the first method not propagate the value out to the variable n in the main program? The variable in modify1 is called n isn’t it?

  • If C passes by value, what is being passed when calling the second method? Is a copy still happening?

Exercise: Make the print statements in the modify functions also report out the address of what they are modifying, and observe the results. Does that help to demonstrate the above questions?

Pointer arithmetic and arrays

Another huge use of pointers is to support the array types we’ve already looked at. Before we examine arrays in detail, let’s look at another operation you can do on pointers: arithmetic.

Just like you can add numbers together, or increment loop counters, you can add, subtract, increment, and decrement pointers. What does this actually mean though?

  • Incrementing a pointer sets it to look further ahead in memory, that is, it shifts the address that it looks at forward.

    • In particular, it shifts it forward by an amount matching the width of the data type it points to

  • Adding a number larger than 1 corresponds to shifting the address by that many steps of the data width.

  • Decrementing a pointer does the opposite, and again respects the width of the data type it points to

In fact, the square brackets we’ve already been using to access elements of an array are actually a fancy operator that combines pointer addition and dereferencing. This deserves an example:

 1/* File: pointerArith.c
 2 * Author: Ian May
 3 * Purpose: Demonstrate basic pointer arithmetic and arrays
 4 */
 5
 6#include <stdlib.h>
 7#include <stdio.h>
 8
 9
10int main()
11{
12  /* Declare an array to work with */
13  double u[] = {1.2,3.7,-13.2,66.5,15.0,1.2e2};
14
15  /* See that u is actually a pointer to the first array element */
16  printf("u = %p, and &u[0] = %p\n",u,&u[0]);
17
18  /* Set a pointer to the second element, and increment */
19  double *pu = &u[1];
20  pu++;
21  printf("pu = %p, and points to the value *pu = %f\n",pu,*pu);
22
23  /* Decrement pu by 2, where does it point now? */
24  pu -= 2;
25  printf("pu = %p, and points to the value *pu = %f\n",pu,*pu);
26
27  /* Print all values in u, can you do this only using pu? */
28  printf("u contains:");
29  for (int i=0; i<6; i++) {
30    printf(" %1.2f |", u[i]);
31  }
32  printf("\n");
33
34  return 0;
35}

Download this code

This can be compiled by calling:

gcc pointerArith.c -o pointerArith.ex

Exercise: Print values from the last loop without using any square brackets. How exactly are the square brackets equivalent to a pointer sum and dereference? Does this explain why C uses zero-based indexing?

Solution: The following code snippet shows what square brackets really are underneath:

int n = 3; /* Some index, value doesn't matter too much */
double q;
double u[] = {1.2, 3.7, -13.2, 66.5, 15., 1.2e2};
double *pu = &u[0];
/* The following four lines are completely equivalent */
q = u[n];
q = *(pu+n);
q = *(u+n);
q = pu[n];

Remark: You should absolutely still use square brackets when indexing into an array. These demonstrations are just to build some confidence with how pointers work and some of the things we can do with them.

Let’s look at another example. We could set a pointer to look at a location inside an array, and then treat that pointer as if it were an array itself. This might be useful if you have a problem where negative indices are meaningful. Consider this sample program:

 1/* File: pointerAlias.c
 2 * Author: Ian May
 3 * Purpose: Use a pointer to generate an array where negative indices are allowed
 4 */
 5
 6#include <stdlib.h>
 7#include <stdio.h>
 8
 9
10int main()
11{
12  /* Declare an array to work with */
13  double u[] = {1.2, 3.7, -13.2, 66.5, 15., 1.2e2};
14  
15  /* Create a pointer that aliases to the third element of u */
16  double *v = &u[2];
17  
18  /* v has the same values, but at different indices */
19  printf("v contains:\n");
20  for(int i=-2; i<4; i++) {
21    printf("v[%d] =  %1.2f\n", i, v[i]);
22  }
23
24  return 0;
25}

Download this code

This can be compiled by calling:

gcc pointerAlias.c -o pointerAlias.ex

Initially, writing a negative index into an array seems like it is just asking for trouble. However, consider what the square brackets are really doing from the perspective above.

Exercise: Re-write line 16 without using any square brackets. Consider re-writing the print statement without using them either. Does this make the negative indexing more clear?

Pointers to structs

So far we’ve seen how to create a pointer to a variable, and how arrays are really just pointers to the start of a block of memory holding a contiguous sequence of values of the same type.

If you recall from before, we can also define our own derived types, and create instances of these types. Here we’ll look at what happens when we create pointers to these instances, and what capability we can gain by doing so.

Consider again the small example struct given at the start of that section:

struct S {
  int n;
  char *s;
  double q;
};

You may recall that when we create an instance of this struct we can access its member variables by using a period. What happens if we only have a pointer to the instance? We do precisely what we did above to access the value of a primitive variable behind a pointer, we dereference it. Consider this small example:

 1/* File: structPointers.c
 2 * Author: Ian May
 3 * Purpose: Demonstrate pointers to structs
 4 */
 5
 6#include <stdlib.h>
 7#include <stdio.h>
 8
 9struct S {
10  int n;
11  char *s;
12  double q;
13};
14
15int main()
16{
17  /* Declare an instance of the struct */
18  struct S a = {2,"Hello",2.3e-4};
19
20  /* Do something with the struct */
21  printf("From the struct instance:\n");
22  for (int i=0; i<a.n; i++) {
23    printf("%s %e\n",a.s,a.q);
24  }
25
26  /* Create a pointer to this struct */
27  struct S *pa = &a;
28
29  /* and do the same thing, using only pa */
30  printf("From the pointer:\n");
31  for (int i=0; i<(*pa).n; i++) {
32    printf("%s %e\n",(*pa).s,(*pa).q);
33  }
34
35  return 0;
36}

Download this code

This can be compiled by calling:

gcc structPointers.c -o structPointers.ex

Now hold on, why do we have all of these parenthesis around? This is due to operator precedence. Just like in math, there is an established order of operations that the language obeys. Now though, there are a bunch of additional operators to account for. Again, let’s try to say in full sentences what each of these lines mean:

struct S a = {2,"Hello",2.3e-4};
struct S *pa = &a;

/* Valid */
int m = (*pa).n;

/* Invalid */
int m = *pa.n;

The valid form says “dereference pa and get field n from that”, while the second says “dereference field n from struct pa”. The second one fails because pa is not a struct. To make matters more confusing, the second could be valid if you have a struct which internally has a data field that is a pointer.

Fortunately, there is a way out of this mess. Taking pointers to structs is so common that there is a special operator for dereferencing them, just like we had the square brackets above for array access. This is the arrow operator ->, and we can use it like this:

 1/* File: arrowOperator.c
 2 * Author: Ian May
 3 * Purpose: Demonstrate pointers to structs, now with the arrow operator
 4 */
 5
 6#include <stdlib.h>
 7#include <stdio.h>
 8
 9struct S {
10  int n;
11  char *s;
12  double q;
13};
14
15int main()
16{
17  /* Declare an instance of the struct */
18  struct S a = {2,"Hello",2.3e-4};
19
20  /* Create a pointer to this struct */
21  struct S *pa = &a;
22
23  /* Do something to a, using only pa */
24  for (int i=0; i<pa->n; i++) {
25    printf("%s %e\n",pa->s,pa->q);
26  }
27
28  return 0;
29}

Download this code

This can be compiled by calling:

gcc arrowOperator.c -o arrowOperator.ex

How would you turn the arrow operator into part of an English sentence? Perhaps you could say pa->n as “go to where pa points, and get the member n”. This is a lot to take in. Perhaps an exercise will help.

Exercise: Modify the above program such that the for loop sits in its own function. Try writing this function to take the struct by value, and by reference. Which makes more sense? Is it problem dependent?

Exercise: Re-write the column major matrix code so that the structs are all passed by reference.

Dynamic memory

So far everything we’ve done has used a fixed amount of memory, and never too much of it at one time. We aren’t going to talk too much about the stack versus the heap, but it will suffice to say that at some large size we can’t rely on variable length arrays any longer. We looked briefly at how Fortran handles this problem through the allocate and deallocate intrinsics, as well as the property allocatable. Similarly, in C we will need some way to allocate and deallocate memory at run time if we want to handle large problems, especially if we don’t want to re-compile for every new size.

This dynamically allocated memory is obtained by the malloc and free functions (malloc being an abbreviation of “memory allocation”). The general format of these calls are:

<a pointer> = malloc(<number of bytes>);

/* Use that memory for something */
/* ... */

free(<a pointer>);

This seems a little daunting. Does this mean we need to account for how many bytes wide every data type is before we even use them? What if we want to allocate an array of structs without counting the width of every field? How do we know for sure the size of an integer on one system or another? Fortunately, like most times something seems too tedious to be true, there is a better way to do this. The sizeof operator, as its name might suggest tells you the size of whatever you apply it to. This operator can act on variables, types, even functions, and can be used like this:

 1/* File: sizeofOperator.c
 2 * Author: Ian May
 3 * Purpose: Demonstrate what the sizeof operator does
 4 */
 5
 6#include <stdlib.h>
 7#include <stdio.h>
 8
 9struct S {
10  int n;
11  char *s;
12  double q;
13};
14
15int main()
16{
17  int n = 12;
18  double q = 2.6e2;
19
20  /* Applied to a variable */
21  printf("int n is %lu bytes wide\n",sizeof n);
22
23  /* Parentheses are optional, but can help readability */
24  printf("double q is %lu bytes wide\n",sizeof(q));
25
26  /* Applied to a type, parentheses not optional here */
27  printf("the int type is %lu bytes wide\n",sizeof(int));
28
29  /* Applied to an expression */
30  double *pq = &q;
31  printf("double q is %lu bytes wide\n",sizeof(*pq));
32  return 0;
33}

Download this code

This can be compiled by calling:

gcc sizeofOperator.c -o sizeofOperator.ex

In particular this makes dynamically allocating arrays much easier, as demonstrated in this example:

 1/* File: dynamicMem.c
 2 * Author: Ian May
 3 * Purpose: Demonstrate basic usage of malloc/free
 4 */
 5
 6#include <stdlib.h>
 7#include <stdio.h>
 8
 9struct S {
10  int n;
11  const char *s;
12  double q;
13};
14
15int main(int argc, char **argv)
16{
17  /* Let's set an array size from the command line */
18  int N = argc>1 ? atoi(argv[1]) : 5;
19  
20  /* Allocate an array of doubles */
21  double *u = malloc(N*sizeof(*u));
22
23  /* Fill with something */
24  for (int i=0; i<N; i++) {
25    u[i] = 1./(1.+i*i);
26  }
27
28  /* Allocate an array of structs */
29  struct S *data = malloc(N*sizeof(*data));
30
31  /* Fill with something */
32  for (int i=0; i<N; i++) {
33    data[i].n = i+1;
34    data[i].s = "Some string";
35    data[i].q = u[i]*u[i];
36  }
37
38  /* Print out one of these structs like we did before */
39  int idx = N-1;
40  for (int i=0; i<data[idx].n; i++) {
41    printf("%s %e\n",data[idx].s,data[idx].q);
42  }
43
44  /* Don't forget to clean up after yourself!!! */
45  free(u);
46  free(data);
47  
48  return 0;
49}

Download this code

This can be compiled by calling:

gcc dynamicMem.c -o dynamicMem.ex

There is a lot going on in this example. We should stop and gather our thoughts by making a few observations.

  • The assignment for int N looks pretty strange, what’s that all about?

    • This is a ternary operator, similar to what we saw briefly in Python

  • After allocating u and data we can interact with them just like any other array

    • Note that for the array of structs we can combine the square brackets and the dot operator naturally

    • Just like any other array (without a brace-closed initializer), declaring one will not initialize the entries to anything in particular. There is a similar function calloc that allocates, and zeros out a block of memory to address this.

  • The malloc function requests space in memory to the resolution of bytes. On its own malloc has no idea what these bytes are for, hence we need to sizeof to allocate the appropriate number.

  • Accordingly, we need to match every call to malloc with a call to free

    • A word of advice: Every time you write malloc, skip ahead and write the corresponding free statement. This won’t catch every bug, but prevents a few common ones.

Remark: You may notice that above we made a big deal about pointers having types. Why is it that malloc can assign to pointers of whatever type? This requires a discussion about a very special type, void*. We won’t get into much detail here, but it suffices to say that this acts as a go-between for pointer types.

A full example

Let’s return to our column-major matrix struct, and address some of the issues that code had. Above, we re-wrote part of it to start passing the structs by reference to functions that need them. The next thing to fix is the way we obtain memory for the internal data array.

Here we are going to use a design pattern inspired by the object oriented approach. C is not an object oriented language, but you can see that packaging this data together into a struct and interacting with it as one cohesive thing has an OOP feel to it. This involves quite a bit of pointer manipulation, and provides a wealth of examples to study.

First, we’ll re-write the header to look like this:

 1/* File: ColMajorMat.h
 2 * Author: Ian May
 3 * Purpose: Another iteration on the previous example, now modeling the OOC
 4 *          approach
 5 */
 6
 7#ifndef COLMAJORMAT_H
 8#define COLMAJORMAT_H
 9
10/* We will only use a pointer to the struct, so make that the typedef */
11typedef struct _p_ColMajorMat *ColMajorMat;
12
13/* Define a structure that holds the matrix dimensions and a flattened array */
14struct _p_ColMajorMat{
15  int m, n;
16  double *data;
17};
18
19/* Functions to create and destroy the matrix struct */
20ColMajorMat CreateMatrix(int,int);
21void DestroyMatrix(ColMajorMat);
22
23/* Functions to interact with this matrix */
24void FillMatrix(ColMajorMat);
25void MatVecMult(ColMajorMat,double[*],double[*]);
26
27#endif

Download this code

then change the source file to match:

 1/* File: ColMajorMat.c
 2 * Author: Ian May
 3 * Purpose: Another iteration on the previous example, now modeling the OOC
 4 *          approach
 5 */
 6
 7#include <stdlib.h>
 8
 9#include "ColMajorMat.h"
10
11/* Matrix creation handles allocation */
12ColMajorMat CreateMatrix(int m, int n)
13{
14  /* Allocate and fill struct */
15  ColMajorMat mat = malloc(sizeof(*mat));
16  mat->m = m;
17  mat->n = n;
18  mat->data = malloc(m*n*sizeof(*mat->data));
19  /* Pass this struct back out */
20  return mat;
21}
22
23/* Destruction frees everything creation allocated */
24/* Can you see a possible bug here? */
25void DestroyMatrix(ColMajorMat A)
26{
27  /* Free the data array inside the struct */
28  free(A->data);
29  /* Free the struct */
30  free(A);
31}
32
33void FillMatrix(ColMajorMat A)
34{
35  for (int i=0; i<A->m; i++) {
36    for (int j=0; j<A->n; j++) {
37      A->data[j*A->m + i] = ((double) i*j+1);
38    }
39  }
40}
41
42void MatVecMult(ColMajorMat A, double x[A->m], double b[A->m])
43{
44  for (int i=0; i<A->m; i++) {
45    b[i] = 0;
46    for (int j=0; j<A->n; j++) {
47      b[i] += A->data[j*A->m + i]*x[j];
48    }
49  }
50}

Download this code

Finally, we’ll need to modify the main file to match the new conventions:

 1/* File: Main.c
 2 * Author: Ian May
 3 * Purpose: Another iteration on the previous example, now modeling the OOC
 4 *          approach
 5 */
 6
 7#include <stdlib.h>
 8#include <stdio.h>
 9
10#include "ColMajorMat.h"
11
12int main(int argc, char **argv)
13{
14  /* Matrix sizes, easily set from command line */
15  int m = argc>1 ? atoi(argv[1]) : 4;
16  int n = argc>2 ? atoi(argv[2]) : 3;
17
18  /* Arrays to hold vectors we are using */
19  double b[m];
20  double x[n];
21  for (int j=0; j<n; j++) {
22    x[j] = 1.0/(1.0 + j*j);
23  }
24
25  /* Create and fill a matrix */
26  ColMajorMat A =  CreateMatrix(m,n);
27  FillMatrix(A);
28
29  /* Store A*x into b, interpreted as matrix-vector product */
30  MatVecMult(A,x,b);
31
32  /* Print out result */
33  printf("b =");
34  for (int i=0; i<m; i++) {
35    printf(" %1.2f |",b[i]);
36  }
37  printf("\n");
38
39  /* Don't forget to clean up! */
40  DestroyMatrix(A);
41  
42  return 0;
43}

Download this code

The makefile remains unchanged:

 1CC = gcc
 2CFLAGS = -g -Wall -Wextra -pedantic -Wno-vla-parameter
 3
 4OBJ = Main.o ColMajorMat.o
 5
 6ColMajor.ex: $(OBJ)
 7	$(CC) $(CFLAGS) -o $@ $(OBJ)
 8
 9%.o: %.c
10	$(CC) $(CFLAGS) -c $<
11
12.PHONY: clean
13
14clean:
15	rm -f ColMajor.ex *.o *~

Download this code

As before, download these to a new directory and compile them by calling make. The program is run by calling ./ColMajor.ex. There is a lot going on here, so let’s make some observations for each file.

The header ColMajorMat.h has changed slightly:

  • Consider the typedef and struct declarations:

    typedef struct _p_ColMajorMat *ColMajorMat;
    struct _p_ColMajorMat{
      /* ... */
    };
    
    • The typedef declares ColMajorMat as a pointer to struct _p_ColMajorMat. This means that any time we use ColMajorMat we are really using a pointer to the actual struct.

    • The structure containing the actual data has the suggestive name struct _p_ColMajorMat, where the prefix indicates that this should be kept private, and not referred to directly by user code. Note: this privacy is purely stylistic, and not enforced by the language.

  • The typedef now makes it easier to use pointers to the struct directly, and discourages ever passing the struct around by value. This means that ColMajorMat is now a pointer type.

  • The FillMatrix and MatVecMult functions now just take ColMajorMat arguments. These are always by reference since ColMajorMat is a pointer type.

The matching source file ColMajorMat.c has also changed slightly:

  • The FillMatrix and MatVecMult functions now use the arrow operator on A since it is a pointer type.

  • The CreateMatrix function is responsible for doing all of the required memory allocation.

    • Since ColMajorMat is now a pointer type, we need to actually create an instance of the base struct for this thing to point to. The first malloc does this. Then we can fill this struct accordingly and hand it back.

  • The DestroyMatrix function is responsible for deallocating all memory that the creation function took. Using this pattern means that the struct can hold many pieces of dynamically allocated memory, but the end user need only call one function to clean up.

    • Question: What happens if the user calls the destroy function multiple times? Can we plan around this possibility?

The main file now shows why we went through all of this effort:

  • Lines 15-23 just set up the vectors to multiply against and store into

  • Line 26 allocates all memory and fills in the fields of the struct

    • Now, when you declare a matrix you don’t need to think about all the struct fields. Consider creating multiple matrices through this approach compared to the previous one

  • Lines 27 and 30 now use references by default, and copying is avoided without any special action.

  • Line 40 handles all deallocation in one call

Hopefully you can see the merits of this pattern. However, as always, there is room for improvement. To see a mature and polished code base using this pattern please take a look at PETSc.

Exercise: Try calling the DestroyMatrix function twice. What happens? Try re-writing the function to be robust against this possibility.