Arrays
There are only two way of having a collection or array of elements in C.
One of them is declaring it as an array, which can be done as such:
int a[] = {1,2,3,4}; /*the empty [] has the comliler able to generate a count*/
int b[4] = {1,2,3,4}; /*you can define the size yourself, and if you have more elements, then the compiler will error*/
int c[1024]; /*uninitialised areay of 1024 ints*/
This way of building a collection of elements allows you to get a size (in bytes) and also a count of elements, as such:
int total_element_count = sizeof(a) / sizeof(*a); /*this will be 4, the total count of elements*/
int total_bytes = sizeof(a); /*this will be 16, the total count of elements times total size of one, so 4 elements times 4 bytes per element, 16*/
int bytes_size_of_one_element = sizeof(*a); /*this will be 4 bytes, the size of an int*/
Notice the syntax for getting the size of one element:
int first_element = *a;
int also_first_element = a[0];
int still_first_element = *(a+0);
int second_element = a[1];
int still_second_element = *(a+1);
int weird_indexing_second_element = 1[a];
You can conclude that these notations are equivalent: (not valid C-code, just showing that these are all equal)
a[n] == *(a + n) == n[a] == *(n + a);
To understand this better, I will show a pointer version. We will assume there exists some allocator named “alloc” that allocates bytes as requested.
int *p = alloc(4);
This allocates 4 bytes and p is the base address of this allocation. Notice that the size of one int is also 4 bytes, which means that this pointer only “contains” one integer. I put the quotations there because in C you can index out of bounds without any issue, but you should always avoid allowing that to happen. For example
int *p = alloc(4);
p[1] = 60; /*this writes past the pointer’s valid address, which could be garbage memory or otherwise. This is undefined behaviour*/
You can also take a pointer into an array instead of allocating memory for that pointer. This is called aliasing.
int a[] = {1,2,3,4};
int *p = a; /* Now p and a are the same address. Any changes made to elements of p are changes made to elements of a because they are truly the same memory space*/
There is, however, a difference between these two:
int a[] = {1,2,3,4};
int *p = a;
/*This is called pointer arithmetic, because we modify the memory address being pointed to directly.*/
/*All math operations would work, but it would be very unusual to use multiplication or division.*/
a++; /* this is illegal*/
p++; /* this is legal */
printf("they are the same value at same memory location: %d\n", *p == a[1]); /* p moved one element ahead, so it is no longer at a[0] but at a[1].*/
The reason the increment is allowed for p is because a is an array while p is a pointer. However, arrays decay into pointers when passed to functions, so you could increment the copy of a if passed to a function. I say the copy of a, because in C, arguments are passed by value. We’ll have a function called f that returns nothing and takes a point to an int.
void f(int *ptr)
{
ptr++; /*only increments the copy of the paraneter*/
*ptr = 9; /*this modifies the actual value, of the pointer argument, not just the local copy*/
}
int a[] = {1,2,3,4};
int *p = a;
f(a);
printf("is equal %d\n", a[1] == 9); /* the value at index 1 in a has been modified inside the function*/
f(p);
/*
* This repeated the same thing a did, which is, it incremented inside the function and then assigned 9 to the new position.
* It's worth nothing that both a and p increment inside the function from a[0] and p[0] to a[1] and p[1], because they've never been incremented beforehand.
* Because of this, after the increment, the value will be assigned at a[1] and p[1] respectively.
*/
printf("also equals 9\n", p[1] == 9;
printf("is equal as memory address: %d\n", p == a); /* they are still the same memory address, because the a and p outside the functions have not been incremented.*/
p++;
printf("these are not equal anymore: %d\n", p != a); /*no longer the same address, p is now pointing at a[1] and not at the base a[0]*/
Pointer arithmetic automatically increments by the size of the type. For example:
int a[] = {1,2,3,4};
int *p = a;
p++; /*advances 4 bytes*/
char *p2 = (char*)a; /* the compiler may let you do this cast implicitly, but you should always avoid implicit casting between different types*/
p2++; /*advances by 1 byte, which means it is now pointing at the second byte of the first int*/
/* since the first int is 1, the bytes are as follows (for little-endina which is what x86 uses):
0000 0001 first byte
0000 0000 second byte <- p2 points here after the increment
0000 0000 third byte
0000 0000 fourth byte
We incremented by one byte only, so we are now pointing at the second byte, whixh means that the value of p2, if we were to use it as a 1 byte char, would be 0
*/
I will continue with pointer arithmetic in the next post, to clarify more of the things happening in here.
Found this useful? Support the writing on Ko-fi.