Episode 15 — Types (Part 7: Practical Examples)
Welcome back!
Our journey into C types enters its final stage: after basic types and enumerated types, it is now the turn of derived types. This category contains arrays, structures, unions, functions, pointers, and atomic types. We have just scratched the surface of most of them in these first episodes, but now it is the time to delve deep into them.
Let’s get started with array types.
Array types
This is a most interesting topic, and one of the things that make first-comers to programming raise an eyebrow. Ever since our infancy, we are used to count starting from 1
, such as “That bag contains some apples, let’s count them: 1, 2, 3, …”. As you will have already realised, arrays are a “collection” type, in that they group a certain number of objects together, but they have this peculiarity that makes them count from 0
. Yes, the first element on an array is element number zero! This may look as a lot of fun to start with, but, in practice, you will soon join the family of those who made their program crash because they didn’t remember this specific feature of arrays.
Trying to give arrays a definition, we can say that an array is a type made up of a sequence of objects with a specific element type. Digging further one notch, this sequence must allocate contiguous spaces in memory, as if each of its elements were holding each other’s hand. As said, every element inside the array must belong to the same type and the size of the array never changes during the array lifetime.
Syntax
Precisely as all other types we have encountered so far, arrays in C start by stating the type of the elements contained in their sequence, followed by an identifier and, normally, a size in square brackets. For example:
int myArray[10];
Declares an array capable of containing ten integers. At this stage the array is empty, that is it contains no object, but if we would go look for its address in memory, we would see how it is occupying about 40 bytes of space (4 bytes for each of the ten integers). We would also see that these addresses are contiguous. To do so, though, we would need to get comfortable with the concept of pointers first, and this will come in a future episode.
Most element-containing types in C have a curious syntax for their contents: they put it inside braces { }
. If you have ever looked at arrays in Swift, for example, they are much simpler: square brackets in declarations, square brackets for content. In C, to create an array to hold the numbers 1, 2, 3
, we need to write:
int m[3] = { 1, 2, 3 };
Once more, I prefer to separate the elements with spaces as it makes it easier to read for me, but I am aware that this is not the way most people write C.
Music-wise, we looked at enumerated types as a good type to store elements which are not bound to change. The elements contained in an array, instead, have the possibility of being replaced by other elements (of the same type), or to be entirely removed, according to the type of array we are looking at. They are not the best object type to be used for note classification, for example, but it may be for storing data where the order of the elements matter, for example the order of pages in a score, the order of strings in a cello (even if storing the first string of an instrument at place zero doesn’t sound completely right, or does it?)
Explanation
There are several kinds of array types: arrays of known constant size, variable-length arrays, and arrays of unknown size.
Known constant size
When declaring an array, the part contained in the square brackets is called the expression, and if it is an integer constant expression (an integer number or an object containing an integer), then the resulting array is fixed and of known size.
The example we looked at earlier
int myArray[10];
Has a literal value of 10
as its expression, and this is therefore a known constant size array. A more abstract approach may be to initialise an array with the size of another type by using the sizeof
function, like this:
char o[sizeof(double)];
This line declares a character array called o
, capable of storing 8
characters, as this is what the sizeof(double)
function would return. The sizeof
function returns the size, in bytes, of the object representation of type passed as argument. In this case, a double
occupies 8 bytes, and therefore we will be able to store up to eight characters in our array.
enum { MAX_SIZE = 100 };
int p[MAX_SIZE];
Raising the complexity bar a tiny bit, we can draw upon our existing knowledge of enumeration’s cases (or enumerators), being integers under the hood, therefore making the above example possible. We declare an enumeration without identifier but with a single enumerator called MAX_SIZE
initialised as 100
(its default value would be 0
). We then create a new array called p
with size equal to MAX_SIZE
and configured to store integers.
It is also possible to provide some or all of an array’s initial values in the same line of the declaration:
int a[5] = { 1, 2, 3 };
This example declares an integer array identified as a
, capable of containing up to 5
elements. Inside the braces, though, we find only three elements. What is happening here? We can use a for
loop to inspect it:
for (int i = 0; i < 5; i++) {
printf("Array element at index %d: %d\n", i, a[i]);
}
This will print out the following result in the console:
Array element at index 0: 1
Array element at index 1: 2
Array element at index 2: 3
Array element at index 3: 0
Array element at index 4: 0
Thus, we can see that the first three slots are occupied by the elements 1
, 2
, and 3
, while the fourth and fifth slots have been assigned a default value of 0
. In C, it is therefore impossible to create a fixed-size array and leave some elements unassigned, as the compiler will fill those gaps in for us.
Another example concerns character arrays, or strings, such as this:
char str[] = "abc";
Theoretically, we could think that the array being created is of undefined size, yet it isn’t. A second guess would propose a size of 3
characters, but, alas, that would also be wrong, as the program needs to have a way of knowing where a string is ending. That is the \0
character, also known as the “null” character. Thus, the size of this array is 4
. With this line of code:
printf("The size of the str array is %lu bytes\n", sizeof(str));
…coupled with the knowledge that every character is exactly one byte wide, we will successfully get this as a result:
The size of the str array is 4 bytes
Variable-length arrays
If the expression is not an integer constant, then the declared array has variable size. This is a massive thing, as it allows us to create an array that changes the amount of allocated memory every time the flow of control passes over the declaration, that is, every time the program uses the array. Thus, the expression is evaluated and, assuming it ends up being greater than zero, the necessary memory is allocated and the lifetime of the array ends only when the declaration goes out of scope. Trying to simplify all this: the size of a variable-length array (VLA) will not change during its lifetime, but on another pass over the same code it may be allocated with a different size.
From what I could learn, it is advised not to use VLAs in production code, so I will not spend too many words on them. This is a working example, but I have been expressively advised not to use it in production code:
int q = 1;
while (q < 10) {
int a[q];
printf( "The array has %zu elements\n", sizeof a / sizeof *a);
q++;
}
In this example, we declare an integer q
and initialise it with the value of 1
. We then create a while
loop which will initialise an array of size equal to q
every time it is run, print the array’s size, and increment q
’s value by 1
. While this works, I understand why this kind of code is not too reliable. In the end we are using a fixed-size array every time, and for the compiler this is a new array every time the loop runs, and has no memory of what was before.
Arrays of unknown size
For the sake of completion, we are covering this as well. When declaring the str
array of characters, we saw the identifier accompanied just by open and close brackets []
. In this case, when the expression in the array declarator is omitted, an array of unknown size is declared. The problem with this is that we cannot stop there: if we want to keep the expression empty, we need to use the array as a function parameter list (where such an array will be transformed into a pointer to the array’s address), or provide an initialiser such as { 1, 2, 3 }
. If we do not do that, the result will be an incomplete type.
An exception to this is when defining a struct
in which an array of unknown size may appear as the last member and there is at least another named one. In this case, it becomes a flexible array member. We will get deeper into this when facing structures in details.
Usage
More modern languages, such as Swift, let you store an array as a variable or constant property, and while every object not preceded by the const
qualifier in C is a variable, arrays do not behave like this (I had told you this subject was a tough one!). Here we learn a new term: lvalue and its counterpart rvalue. An lvalue is shorthand for “left-value” and represents what is found to the left of the equal sign =
during the assignment operation. Conversely, a rvalue is what stays to the right of the equal sign during that same operation. A simple example would be this:
int tuning[4] = { 415, 435, 440, 442 };
If you are curious, these are some of the frequencies in Herz (Hz) used to tune the note A in classical music. There are many more, and even more theories, just take my word for it. In this example, the lvalue is int tuning[4]
while the rvalue is { 415, 435, 440, 442 };
.
All this to say the objects of the array type are not modifiable lvalues, and even if their address in memory may be taken and used, they cannot appear on the left side of an assignment operator, unless they are members of a structure.
The address of an array is taken using the address of operator &
. Here is a quick example:
int c[3] = { 1, 2, 3 }, d[3] = { 4, 5, 6 };
int (*r)[3] = &c;
c = d;
This declares, on the same line, two arrays of integers of size 3
and initialises them with some values (you can put whatever you want in there, I decided to go simple with it). In the second line, I create a pointer array of the same size and use the address of operator to assign it the address of c
. The third line tries to assign d
to c
. This returns the error “c is not assignable”, which corresponds to what we would expect.
Now, I could not resist, and I am now showing how the address of items work. We will have plenty of time to delve deeper into this at a later stage, but let’s start with this:
printf("The address of c is %p, and the address of r is %p\n", &c, &r);
The %p
conversion specifier wants a pointer, and we are feeding it the appropriate value with the address of operator. On my machine, at the moment of writing, this was the result:
The address of c is 0x7ff7bfeff064, and the address of r is 0x7ff7bfeff020
They are close enough, but they are not the same, as they differ only in the last three digits. Now, I wonder if this is because the address of an array may not be always its first element. This for
loop helps shed some light on all this:
for (int i = 0; i < 3; i++) {
printf("Array c element %d: %d, with address %p\n", i, c[i], &c[i]);
printf("Array r element %d: %d, with address %p\n", i, *r[i], &r[i]);
printf("=====\n");
}
Which prints:
Array c element 0: 1, with address 0x7ff7bfeff064
Array r element 0: 1, with address 0x7ff7bfeff064
=====
Array c element 1: 2, with address 0x7ff7bfeff068
Array r element 1: 415, with address 0x7ff7bfeff070
=====
Array c element 2: 3, with address 0x7ff7bfeff06c
Array r element 2: 442, with address 0x7ff7bfeff07c
=====
Nice, we are happier with this: element 0
is now the same and placed at the same address in memory. Very interestingly, though, array r
diverges from this behaviour and gets different addresses and values. So far, I cannot explain why this is happening, but I can’t help but be in awe at how fascinating all this is.
Multidimensional arrays
Yes, it is as Sci-Fi as it sounds! This allows us to create matrices of n
by m
size, where n, m
are both integers. This is possible when the element of an array is another array. The syntax here is not so nice with us, look at an example of a 2 x 3
array.
int a[2][3] = {{1,2,3},{4,5,6}};
Here we have an array of integers of size 2
, each element containing an array of integers of size 3
. This can be viewed as a matrix of two columns and three rows.
Before you ask, yes, we can create 3D arrays!
int b[3][3][3];
I will let you add elements to it for your personal fun!
Last, know that you can edit a multidimensional array in every dimension, like so:
int size = 5;
int cuboid[size][size*2][size*3];
What’s next?
I hope you enjoyed this quite deep introduction to arrays. We will use them a lot in the exercises later on. In the next episode, we will look at structures, gradually getting closer to our goal of checking every type in C.
Bottom Line
Thank you for reading today’s article.
If you have any question or suggestion, please leave a comment below or contact me using the dedicated contact form. Assuming you do not already do so, please subscribe to my newsletter on Gumroad, to receive exclusive discounts and free products.
I hope you found this article helpful, if you did, please like it and share it with your friends and peers. Don’t forget to follow me on this blog and to let me know what you think.
If you are interested in my music engraving services and publications don’t forget to visit my Facebook page and the pages where I publish my scores (Gumroad, SheetMusicPlus, ScoreExchange and on Apple Books).
You can also support me by buying Paul Hudson’s Swift programming books from this Affiliate Link or BigMountainStudio’s books from this Affiliate Link.
Thank you so much for reading!
Until the next one, this is Michele, the Music Designer.
One thought on “Learning the C Programming Language as a Classical Musician [15]”