C/C++ Arrays, Pointers, Memory, References

Key Word(s): C++, Arrays, Pointers, Memory, References




Key: :large_orange_diamond: - Code Example :large_blue_diamond: - Code Exercise :red_circle: - Code Warning
Previous: Operators, Loops, Functions
Next: Function Pointers


Arrays, Pointers, Memory, References

We touched a little bit on arrays in the data structures section, but, let's look at statically and dynamically allocated arrays in detail.

Arrays

Recall, an array is a variable which stores multiple values of the same type contiguously in memory.

int data[12];


Array Declaration

dataType arrayName[arraySize];

Note that arraySize must be a fixed constant known at compile time.
The compiler automatically allocates and deallocates the array memory.

:large_orange_diamond: Memory Addresses Example

:large_orange_diamond: [Deepnote: Array Memory Demo](https://deepnote.com/project/fdeed75f-9b4a-428c-8bb7-3766103008ee): We can check to see if the memory is actually contiguous.

#include <stdio.h>

int main(void){
    int ndata = 12;
    int data[ndata];

    for (int i = 0; i < ndata; ++i) {
        printf("data[%2d] address %p\n",i,&data[i]);
    }
    return 0;
}
Result

data[ 0] address 0x7ffc29e1fbe0
data[ 1] address 0x7ffc29e1fbe4
data[ 2] address 0x7ffc29e1fbe8
data[ 3] address 0x7ffc29e1fbec
data[ 4] address 0x7ffc29e1fbf0
data[ 5] address 0x7ffc29e1fbf4
data[ 6] address 0x7ffc29e1fbf8
data[ 7] address 0x7ffc29e1fbfc
data[ 8] address 0x7ffc29e1fc00
data[ 9] address 0x7ffc29e1fc04
data[10] address 0x7ffc29e1fc08
data[11] address 0x7ffc29e1fc0c


Array Initialization

We can initialize arrays during the declaration:

int arr1[4] = {4, 234, 22, -1};
int arr2[]  = {45, 107, 207, 4, 0};

Array Access

We can simply access arrays by using []. For example,

int b = data[2];

:red_circle: Be careful when accessing data elements! :red_circle:

Example:

int data[4] = {0, 0, 0, 0};

printf("data[4] = %d\n",data[4]); // index 4 is out of bounds
>>> data[4] = -585229896

Multidimensional Arrays

We can also create multidimensional arrays, e.g.

int dim2d[20][8];
int dim3d[10][3][44];

Array Memory Location and Limitations


Pointers

:large_orange_diamond: Print Address

int var;
printf("var's address: %p\n",&var);
>>> var's address: 0x7ffc29e1fbe0

:large_orange_diamond: Pointer * Placement

- The declaration of pointers is flexible in terms of the `*` placement relative to **spaces**.

int *p1;
int* p2;
int * p3;

:large_orange_diamond: Suggested Pointer * Placement

By placing `*` next to the variable name, it helps provide an implicit local assignment.

int *p1, v1;  // p1 is an integer pointer, v1 is an integer
int* p2, v2;  // p2 is an integer pointer, v2 is an integer (NOT a pointer)
int *p3, *p4; // p3 is an integer pointer, p4 in an integer pointer


Getting the Address and Value

:large_orange_diamond: Assigning and Accessing Pointers

/* assign pointer */
int v1;
int *p1 = &v1;  // assign address of v1 to p1
int v2 = *p1;   // assign v2 to the value pointed by p1
int v3 = p1[0]; // assign v3 to the value pointed at the 0-index pointed by p1

:large_orange_diamond: Pass By Address

void my_function(int *ad){
  int v0 = *ad; // access the value stored at ad
  ad[0] = 46;   // set the value
  *ad = 46;     // or equivalently
}

void main(void){
  int var = 6;  
  my_function(&var);  // pass by address
}

Working Example

#include <stdio.h>
int main(){
   int *pc, c;

   c = 22;
   printf("Address of c: %p\n", &c);
   printf("Value of c: %d\n\n", c);  // 22

   pc = &c;
   printf("Address of pointer pc: %p\n", pc);
   printf("Content of pointer pc: %d\n\n", *pc); // 22

   c = 11;
   printf("Address of pointer pc: %p\n", pc);
   printf("Content of pointer pc: %d\n\n", *pc); // 11

   *pc = 2;
   printf("Address of c: %p\n", &c);
   printf("Value of c: %d\n\n", c); // 2
   return 0;
}

Pointer Arithmetic

Pointers simply point to memory address.
Nothing is stopping us from actually performing arithmetic on the pointer itself to access other elements stored beyond the base pointer.

int array[2] = {212, 1054};
int ind2 = *(array + 1); // := array[1] = 1054

:large_orange_diamond: Deepnote: Pointer Arithmetic Demo


Memory

As we saw above, the array sizes must be known at compile time and the maximum number of elements is limited system-assigned stack size (and the amount of stack already occupied). To get around these limitations, we introduce Dynamic Memory Allocation.


C Dynamic Allocation and Deallocation

Dynamic memory allocation in C is achieved by malloc and calloc.

:large_orange_diamond: Allocating Memory: malloc

- `malloc()` stands for memory allocation - It is a function which is used to allocate a block of memory dynamically - It reserves memory space of specified size and returns the null pointer pointing to the memory location - The pointer returned is usually of type `void` which means that we can assign malloc function to any pointer **Syntax:**

 void * malloc(byte_size);
- The values found within the the allocated memory is *garbage* until you fill in the values **
:large_orange_diamond: `malloc` Example**

int main(void){
  int *my_ints = (int *) malloc(16 * size(int)); // allocate a block of memory the size of 16 integers (64 bytes)
  if(my_ints != NULL) free(my_ints);             // deallocate the block of memory
  my_ints = NULL;

  return 0;
}

:large_orange_diamond: Allocating Memory: calloc

- `calloc` declared in `` allocates memory dynamically but also **initializes the bytes to `0`**. **Syntax:**

void *calloc(size_t nelements, size_t element_size);
**
:large_orange_diamond: `calloc` Example**

int main(void){
  int *my_ints = (int *) calloc(16, size(int));  // allocate a block of memory the size of 16 integers (64 bytes) and initialize to 0
  if(my_ints != NULL) free(my_ints);             // deallocate the block of memory
  my_ints = NULL;

  return 0;
}


Double, Triple, and Beyond Pointers

Recall that we call have double pointers (pointers containing pointers), triple pointers, and more.
Note that when do allocate pointers containing pointers, the data is only contiguous within their individual memory blocks and the momory block containing the pointer addresses.

:large_orange_diamond: Double Pointers Example (Pointers Containing Pointers)

**Example:**

// allocate pointer of pointers memory
int **pp = (int **) malloc(9*sizeof(int *));

// allocate each block
pp[0] = (int *) malloc(10*sizeof(int));
pp[1] = (int *) malloc(11*sizeof(int));
pp[2] = (int *) malloc( 5*sizeof(int));
pp[3] = (int *) malloc(12*sizeof(int));
pp[4] = (int *) malloc(11*sizeof(int));
pp[5] = (int *) malloc( 8*sizeof(int));
pp[6] = (int *) malloc( 9*sizeof(int));
pp[7] = (int *) malloc(11*sizeof(int));
pp[8] = (int *) malloc( 7*sizeof(int));
...
// free each block
for (int i = 0; i < 9) {
  free(pp[i]); pp[i] = NULL;
}

//free pointer of pointers
free(pp); pp = NULL;
![]({attach}doublepointer.png)


Recommended Array Arithmetic

In the previous example, we saw that using data structures containing pointers of pointers fragnants the memory space.
This may result in very poor performance if we are trying to perform matrix-based calculations.

Suppose we wish to perform a matrix-vector product:

where i and j represent the indexes into the matrix and vector.
We need to allocate and access the memory in a contiguous manner. To do so, we will allocate a single block of contiguous memory.

:large_orange_diamond: Contiguous Memory Matrix Multiplication

// Naive Matrix-Vector Multiplication
const int M = 32;
const int N = 64;

double *A = (double *) malloc(M*N*sizeof(double));
double *b = (double *) malloc(  N*sizeof(double));
double *x = (double *) malloc(M  *sizeof(double));

... // fill in A and b
... // zero out x

// version 0
for (int j = 0; j < N; ++j) {
  for (int i = 0; i < M; ++i) {
      x[i] += A[j*M + i] * b[j];
  }
}

... // zero out x 
// version 1
for (int j = 0; j < N; ++j) {
  const double *Aj = A[j*M];
  const double bj = b[j];
  for (int i = 0; i < M; ++i) {
      x[i] += Aj[i] * bj;
  }
}

// free memory
free(A); A = NULL;
free(b); b = NULL;
free(x); x = NULL;


Pass by Value, Pointer, Reference (C++)

Now that we have command of pointers, we can examine how to pass variables to functions by three approaches.


1. Pass By Value: Variable Copied

:large_orange_diamond: Pass By Value Example

#include <stdio.h>

void try_resetting(int a){
  a = 0;
}

int main(void){
  int b = 10;
  try_resetting(b); // pass by value -- copy of b passed to function

  printf("Value of b = %d\n",b); // value of b = 10
  return 0;
}
````
</p>
</details>

---
#### 2. Pass by Pointer
If we wish to modify the value inside the function or send an object, e.g. `struct` or `class`, we can **pass by pointer**.
- Pass the address of the variable as the fucntion argument

**<details><summary><b>:large_orange_diamond: Pass by Pointer Example</b></summary>**
<p>

```C
#include <stdio.h>

void try_resetting(int *a){
  *a = 0;
}

int main(void){
  int b = 10;
  try_resetting(&b); // pass by pointer -- send address of b to function

  printf("Value of b = %d\n",b); // value of b = 0
  return 0;
}


3. Pass by Reference (C++ only)

C++ introduced pass by reference.

:large_orange_diamond: Pass by Reference Example

#include <stdio.h>

// pass by reference (C++ only)
void try_resetting(int &a){
  a = 0;
}

int main(void){
  int b = 10;
  try_resetting(b); // pass b directly

  printf("Value of b = %d\n",b); // value of b = 0
  return 0;
}
If we want pass by reference but make sure we do not modify the value or object, we use the `const` modifier.
void try_resetting(const int &a);


Let's look at all three approaches when a struct is the object passed to a function.
:large_orange_diamond: Deepnote: Passing Structs By Value, Pointer, and Reference


Next: Function Pointers