Learning the C Programming Language as a Classical Musician [13]

Episode 13 — Types (Part 5: Practical Examples)

Welcome back!

Today, we continue our journey through practical examples of types in the C programming language. We dedicated the whole of the last episode to integers, saw how the C compiler handles them, and met the modulo % operator, used to return the remainder of a division. An important topic was the distinction between sizes of integers as while for us humans this may not look that crucial, for a computer, it is vital to know just how much space it needs to allocate for a certain object. When using int as type specifier, we are guaranteed to have at least 16 bits in width, even if modern compilers such as Xcode tend to offer 32 bits by default already. We specify the need for 64 bits with the long long type specifier, with some interesting quirks when analysing unsigned integers.

Finally, we introduced the concept of number conversions, or how to pass from decimal to binary, to octal, to hexadecimal representations. It was a funny episode to write, and I hope you enjoyed it.

Today we are going to look at how C represents decimal numbers, technically called floating numbers.

Let’s get started.

Arithmetic types (Part 3)

Floating types: an introduction

Back in primary school we learned of the difference between whole numbers (1, 2, 3, …) and decimal numbers (1.25, 3.14, 7.6665, …). The nomenclature of decimal numbers is though flawed, as it takes its name from the decimal separator (a dot in English-speaking countries, a comma in Italian and possibly other countries) between the integer and the fractional part of a number written in decimal form. Possibly, the main issue with this kind of numbers is their precision: if we divide 3 by 2 we all know this gives us 1.5, or one and a half. This is nice because while the result of the division is not exact, i.e., it is not a whole number, we can still see a precise separation. The story would change if we tried to divide 1 by 3, as we would get 0.3333... and, sadly, those 3s would never end (and with never I mean, never!). This poses a problem: how are we going to tell the computer that such a number’s representation would just never end? Long story short, we cannot, and here is where floating-point arithmetic comes into play. This kind of arithmetic represents real numbers as an approximation to balance between range and precision. In mathematics, it is used to represent very large and very small numbers made of a predetermined amount of significant digits (labelled, the significand), multiplied by an exponent of a fixed base. For example, we could write 1.2345 as 12345 x 10^-4.

But why are they called floating-point numbers? Let’s start from their name, which already contains the solution: a decimal number is composed of an integer part (to the left of the point) and a fractional part (to the right of the point). By changing the exponent of the fixed base shown above, we can have more or less digits after the point, which means that this point “floats” between digits without altering their meaning. Thus, 12345 x 10^-4 is equivalent to 1234.5 x 10^-3. You see? The dot “floated” leftwards by one digit, as we changed the exponent of the fixed base.

In computing, this translates to the fact that, for each type of floating-point numbers, we have a certain precision, that is, how many digits we can accept after the dot. Modern computers all have a floating-point unit (FPU, sometimes referred to as math coprocessor) specifically dedicated to carrying out operations with floating-point numbers. You may have heard the term “teraflops” when looking at the performance of discrete GPUs; well, the terms FLOPS means FLoating-point OPerations Speed.

Floating-point in practice

If you try this in Xcode:

float a = 3 / 2;
float b = 1 / 3;
printf("3 divided by 2 as a float is %f, 1 divided by 3 as a float is %f\n", a, b); 

The output will be:

3 divided by 2 as a float is 1.000000, 1 divided by 3 as a float is 0.000000

I scratched my head for a good 10 minutes trying to understand what I was missing and, as almost always in programming, the answer was basic: I was trying to store a division between two integers inside a floating-point object. In integer math, 3 / 2 is 1; 1 represented as a float is 1.000000. Thus, I need to specify that I want to perform a full floating-point operation, by either specifying the precision I want, for example float a = 3.0 / 2.0, or by adding a dot and an f after the integer, such as float b = 1.f / 3.f.

Our new code is:

float c = 3.0 / 2;
float d = 1.f / 3.f;
printf("3 divided by 2 as a float is %f, 1 divided by 3 as a float is %f\n", c, d);

Which outputs to:

3 divided by 2 as a float is 1.500000, 1 divided by 3 as a float is 0.333333

There are three types of floating-point values as of C17, while three more will be introduced in C23. float is a single-precision floating-point type, double is a double-precision floating-point type, and long double is an extended precision floating-point type. They respectively have a 32, 64, and 128 bit in width, or at least try to have according to their implementation in the compiler. But what is “single”, “double”, and “extended” precision? It refers to how many digits after the decimal point they can support.

There is a problem, though: the printf function only supports up to 6 significant digits after the decimal point so, if we want to specify a higher amount, we need to increase it manually by using the formula %.nf where n is equal to the number of digits we want to show.

If we want to represent the constant PI as a decimal number and show it in our output, we will get 3.141593, regardless of whether we use float, double, or long double. I have performed the necessary experiments for you and found that the maximum number of decimal digits we can get with float is 22, thus:

// This constant requires the #include <math.h> header
float f_pi = M_PI;
printf("The pi constant as a float is %.22f\n", f_pi);

Outputs to:

The pi constant as a float is 3.1415927410125732421875

Now pay attention to the 7th digit, 7, which is the reason we get the 3 with 6 digits instead of the 2. Moreover, this is an approximation, though, as with double we can go much further, up to 48 digits:

double d_pi = M_PI;
printf("The pi constant as a double is %.48lf\n", d_pi);

The lf is how printf accepts a double. This outputs to:

The pi constant as a double is 3.141592653589793115997963468544185161590576171875

As you see, the 7th digit is not a 7 but a 6 and many other digits afterwards do not correspond. Obviously, this is due to how the FPU processes the calculation of the cosine trigonometric function of 1, but something that I find interesting is how the last four digits are always 1875. I wonder if there may be a clue there. Trying this with long double gives the same result as double does, which simply means modern usage of double and long double have merged or that the compiler doesn’t support more digits than this, which are already a lot.

In music, we do not have too much to compare with floating-point numbers, as we tend to reduce everything to fractions, regardless of how complex. Irregular groups of notes are often expressed as a ratio between the number of notes we want and the number of notes we should normally have in that rhythmic space, for example 5 quarter notes in the space of 4, expressed as 5:4.

More floating-point types

So far, we have only looked at real floating-point types, that is, those that represent real numbers. If one would be interested in them, complex and imaginary floating-point types are also supported by the C programming language. I am no mathematician, so I will not risk it by venturing into such a complex ground.

The last thing I would like to cover is how to express floating-point numbers in other number systems, exactly as we did in the last episode with integer types.

Decimal system

We saw at the beginning of this episode how the decimal system can express floating-point numbers also as a significand multiplied by a base raised by an exponent. Thus, the number 1200.0 can be expressed by 1.2 x 10^3 in this way:

double e = 1.2e3

Where e is “exponent”.

Binary system

There is not an included way to print binary in C, and one has to code their own way, which we will possibly look at in the future, but let’s just say that, precisely how an integer binary is expressed as powers of 2 that are greater than, or equal to 0, we can easily deduce that fractional part will be expressed with negative powers of 2. For example, the decimal number 3.25 would become 11.01 in binary because:

When we pass 2^0 that is where the decimal point goes.

Octal system

To convert decimal numbers to octal it is luckily straightforward in C because there is a dedicated conversion specifier %o that allows us to get the output immediately, for example:

int f = 42;
printf("%o", f); // prints 52

For fractional numbers, C doesn’t have any direct way to show it in the printf function.

printf("%d in octal is %o, and %f in octal is not representable\n", f, f, g);
// prints
// 42 in octal is 52, and 4.267194 in octal is not representable

Here is our usual calculation:

Thus, 3.25 as octal is 3.2.

Hexadecimal system

As long as the source is an integer, we can use the %x or %X conversion specifier:

printf("%x or %X", f); // prints 2a or 2A

For fractional numbers, C allows the %a or %A specifier.

printf("%d in hexadecimal is %x or %X, while %f is %a or %A\n", f, f, f, g, g, g);
// prints
// 42 in hexadecimal is 2a or 2A, while 4.267194 is 0x1.1119b4dcebfecp+2 or 0X1.1119B4DCEBFECP+2

Here is, instead, our usual calculation:

Thus, 3.25 as hexadecimal is 3.4.

It is clear that representing fractional numbers using the decimal system is, by far, the most optimal way, while hexadecimal is the best for very big numbers.

What’s next?

In the next episode we will look at a few practical examples of the other types C supports and that do not belong to the realm of arithmetic and, at least apparently, to mathematics. We will start with enumerated types and see where this leads us.

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.

Published by Michele Galvagno

Professional Musical Scores Designer and Engraver Graduated Classical Musician (cello) and Teacher Tech Enthusiast and Apprentice iOS / macOS Developer Grafico di Partiture Musicali Professionista Musicista classico diplomato (violoncello) ed insegnante Appassionato di tecnologia ed apprendista Sviluppatore iOS / macOS

One thought on “Learning the C Programming Language as a Classical Musician [13]

Leave a comment