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:
- The compiler won't stop you from accessing elements out-of-bounds (can set a compiler flag to check though)
- Can accidently access elements beyond allocated memory
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
- Static arrays have limitations on how many elements can be allocated
- Arrays are allocated on the memory stack: computer's memory which stores temporary variables created by a function
- The actual size limit of the stack depends on how much space your CPU thread has allocated, e.g. 2 megabytes
Pointers
- We can obtain the memory address of a variable and store it in a pointer variable.
- If we have a variable
var
, then we can access its address in memory by&var
. - We can assign a pointer to the address in memory:
int *pvar = &var
. - To print the address in the C language, we use the format
%p
.
: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
- If we wish to assign the address to a variable or pass the variable by address, we use the
&
operator. - To get the value from a pointer, we can either dereference the value by using
*pvar
orvar[0]
.
: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);
: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 `
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;
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
- Value that is passed as a function argument is copied into a temporary variable on the stack by the compiler
- Cannot modify the value of the variable passed into the function as an argument
- Passing a
struct
or object is very slow
: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.
- Looks similar to passing by value but the value can be modified and the original object will reflect those changes.
- Function input argument declaration uses
&var
- Much faster for passing objects
: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;
}
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