Skip to content

Instantly share code, notes, and snippets.

@remzmike
Last active March 28, 2023 20:07
Show Gist options
  • Save remzmike/482f25fcf366bf13763993d3a5ae8d0c to your computer and use it in GitHub Desktop.
Save remzmike/482f25fcf366bf13763993d3a5ae8d0c to your computer and use it in GitHub Desktop.
Hello C : 05 : Growing code with functions

Part 5: Growing code with functions

We're going to write our own functions and learn how to grow code in small steps.

Now is probably a good time to start using a text editor designed for programming.

You can use whatever you like.

I use Sublime Text for miscellaneous editing, and the sample code in this series.

https://www.sublimetext.com/

But, I also use Visual Studio Code for more serious projects.

https://code.visualstudio.com/

It's not part of the Visual Studio you installed. It just shares the name.

But, like I said, you should use whatever you like.

Define a function

I minimally described function definition in part 3, using this made-up syntax:

<return-value-type> <function-name>(<param-type> <param-name>, ...) <block>

Here is an example definition and a call to the function from main:

#include <stdio.h>
#include <Windows.h>

void main_a() {
    int note_duration = 200;
    int note_frequencies[3] = { 220, 139, 165 };

    for (int i = 0; i < 3; i++) {
        int note_frequency = note_frequencies[i];
        Beep(note_frequency, note_duration);
    }
}

void main() {
    main_a();
}

In this code the main_a function is both declared and defined before it is used in the file.

In your working folder, create 5.c in it with the above code, then compile and run.

Also, prepare to hear Beeps.

Function Definition vs Declaration

Defining a function also declares it.

But... you can also declare a function without defining it, which is required once your source files start using functions that you defined in another file.

This is one of the main problems header files solve; they declare functions that are defined elsewhere.

This allows the compiler to know about functions that are defined outside the file it is compiling, or even functions defined later in that same file, if you need or want that.

In fact, the compiler doesn't really care about the definition of functions you call as long as they are declared.

For example, if you declare a nonsense function, and call it, the compiler will compile it, but the linker will fail.

void nonsense_function();

void main() {
    nonsense_function();
}    

You could compile this code with cl /c, to disable automatic linking, and it will compile without a problem.

But, if you try to link it you will see an error like this:

05-nonsense.obj : error LNK2019: unresolved external symbol nonsense_function referenced in function main
05-nonsense.exe : fatal error LNK1120: 1 unresolved externals

The linker needs the definition, in compiled form, and it gets that definition from a lib or obj file, which are themselves created by a prior compile at some point.

So, if you provided the linker an obj or lib file which included a definition of nonsense_function, then it would link and your program would run fine.

How to write a function declaration

Again, here is the simplified grammatical description I gave for a function definition:

<return-value-type> <function-name>(<param-type> <param-name>, ...) <block>

Function declaration is almost exactly the same, except in a statement and without the definition block.

<return-value-type> <function-name>(<param-type> <param-name>, ...);

This is also called the 'signature' of the function, since it uniquely defines a function.

Example:

void play_notes();    

Grow the code

I think the most natural way to develop new code is to explore in small steps.

Here's the code you should have from above:

#include <stdio.h>
#include <Windows.h>

void main_a() {
    int note_duration = 200;
    int note_frequencies[3] = { 220, 139, 165 };

    for (int i = 0; i < 3; i++) {
        int note_frequency = note_frequencies[i];
        Beep(note_frequency, note_duration);
    }
}

void main() {
    main_a();
}

We're going to modify this code in multiple steps, in the same file.

For convenience and clarity, we're going to do that by creating multiple versions of the same functions. Then we will change main so it's easier to switch between the different versions of the functions we write.

Copy main_a to main_b, then add an if statement to main so you can set which function to call.

void main() {
    char which_function = 'b';

    if (which_function == 'a') {
        main_a(); 
    } else if (which_function == 'b') {
        main_b();
    }
}

When you add more functions, modify this if statement and update the which_function variable so the new function is called.

Now let's modify main_b. We want to play the 3 notes twice in a row, but we want to change their frequencies by powers of two, which moves the notes up one octave for each power of two.

First, do the obvious thing of copying the for loop 3 times, then multiply the note_frequency in the 2nd and 3rd loops by 2 and 4.

void main_b() {
    int note_duration = 200;
    int note_frequencies[3] = { 220, 139, 165 };

    for (int i = 0; i < 3; i++) {
        int note_frequency = note_frequencies[i];
        Beep(note_frequency, note_duration);
    }

    for (int i = 0; i < 3; i++) {
        int note_frequency = note_frequencies[i];
        Beep(note_frequency * 2, note_duration);
    }

    for (int i = 0; i < 3; i++) {
        int note_frequency = note_frequencies[i];
        Beep(note_frequency * 4, note_duration);
    }   
}

When we run this it will run main_b since we set which_function in main. Try it now.

You should now hear the 3 notes repeated two more times at higher frequencies.

Write a function to do the for loop

The next obvious step, is for us to write a function to do what the for loop does, so we don't have to copy it.

To start that, define a function named play_notes_c, and copy the first for loop into it.

void play_notes_c() {
    for (int i = 0; i < 3; i++) {
        int note_frequency = note_frequencies[i];
        Beep(note_frequency, note_duration);
    }
}

This won't compile, because the function cannot see the variables note_frequencies or note_duration. All the variables in this copied code that are not in scope need to be passed as parameters to the function.

So change the function definition so it takes the note_frequencies array and the note_duration as parameters.

void play_notes_c(int note_frequencies[], int note_duration) {

This will work, but it won't do the frequency multiply anymore, unless you passed different notes. I don't think passing different notes is what I want, because I could just multiply the frequencies dynamically.

So, add frequency_multiplier as another parameter, and multiply note_frequency by it.

void play_notes_c(int note_frequencies[], int note_duration, int frequency_multiplier) {
    for (int i = 0; i < 3; i++) {
        int note_frequency = note_frequencies[i];
        Beep(note_frequency * frequency_multiplier, note_duration);
    }   
}

Now write main_c by copying main_b and replacing the three for loops with three calls to play_notes_c.

void main_c() {
    int note_duration = 200;
    int note_frequencies[3] = { 220, 139, 165 };
    play_notes_c(note_frequencies, 200, 1);
    play_notes_c(note_frequencies, 200, 2);
    play_notes_c(note_frequencies, 200, 4);
}

Update the actual main function so it calls main_c, compile and run.

It should sound the same as before. What we did is semantic compression. The code does the same thing, but now we have encoded our desired model of understanding into it through the function call. We have introduced the concept of play_notes_c, because it matches how we decided to think about the problem.

Modify the function so it can repeat the sequence

Right now the notes passed will play once, but if we want to repeat them we can add a new parameter to the function, repeat_count, and change the function to use it.

But, you may also notice that the loop currently only plays 3 notes, since it has i < 3 in the for loop's test-expression.

I wrote 3 there originally because I knew the note_frequencies array had 3 items in it. Now we will just pass in the number of notes to play from the array, so we can change that as desired.

So, copy play_notes_c to play_notes_d, and main_c to main_d.

Then we will add repeat_count and note_count to play_notes_d.

However, we should think about where the new parameters should go in the function, so they are ordered with some kind of reason, if possible.

We should probably add the note_count after the note_frequencies array, then add repeat_count as the final parameter, since it's new and that seems like a fine place.

void play_notes_d(int notes[], int note_count, int note_duration, int frequency_multiplier, int repeat_count) {

Now modify the for loop to use note_count instead of 3.

for (int i = 0; i < note_count; i++) {

Then, for repeat_count, add a new for loop wrapping around the one that is there, to do the repeat.

    for (int j = 0; j < repeat_count; j++)  {
        for (int i = 0; i < note_count; i++) {

And, add some prints so we can see note progress, as a convenience.

void play_notes_d(int notes[], int note_count, int note_duration, int frequency_multiplier, int repeat_count) {
    for (int j = 0; j < repeat_count; j++)  {
        for (int i = 0; i < note_count; i++) {
            int note = notes[i];
            Beep(note * frequency_multiplier, note_duration);
            printf(".");        
        }   
    }
    printf("\n");
}

Now, modify main_d so that you pass the number of notes, 3, and a repeat_count at the end, 2.

Also, since we're repeating the notes now, we can lower the note_duration to 105 so it plays faster.

I also added a longer beep of the first note at the end.

void main_d() {
    int note_duration = 105;
    int notes_frequencies[3] = { 220, 139, 165 };   
    play_notes_d(notes_frequencies, 3, note_duration, 1, 2);
    play_notes_d(notes_frequencies, 3, note_duration, 2, 2);
    play_notes_d(notes_frequencies, 3, note_duration, 4, 2);
    Beep(notes_frequencies[0], note_duration * 6);
}    

Don't forget to change main so it calls main_d now.

Compile and run. You should see and hear the final result.

Now, the point of all these copied functions is so you can look back at them.

By comparing them, you can better understand the small steps taken over multiple iterations.

These are the core actions taken in straightforward programming.

SAMPLE CODE: 05d.c

#include <stdio.h>
#include <Windows.h>
void main_a() {
int note_duration = 200;
int note_frequencies[3] = { 220, 139, 165 };
for (int i = 0; i < 3; i++) {
int note_frequency = note_frequencies[i];
Beep(note_frequency, note_duration);
}
}
void main_b() {
int note_duration = 200;
int note_frequencies[3] = { 220, 139, 165 };
for (int i = 0; i < 3; i++) {
int note_frequency = note_frequencies[i];
Beep(note_frequency, note_duration);
}
for (int i = 0; i < 3; i++) {
int note_frequency = note_frequencies[i];
Beep(note_frequency * 2, note_duration);
}
for (int i = 0; i < 3; i++) {
int note_frequency = note_frequencies[i];
Beep(note_frequency * 4, note_duration);
}
}
void play_notes_c(int note_frequencies[], int note_duration, int frequency_multiplier) {
for (int i = 0; i < 3; i++) {
int note_frequency = note_frequencies[i];
Beep(note_frequency * frequency_multiplier, note_duration);
}
}
void main_c() {
int note_duration = 200;
int note_frequencies[3] = { 220, 139, 165 };
play_notes_c(note_frequencies, 200, 1);
play_notes_c(note_frequencies, 200, 2);
play_notes_c(note_frequencies, 200, 4);
}
void play_notes_d(int notes[], int note_count, int note_duration, int frequency_multiplier, int repeat_count) {
for (int j = 0; j < repeat_count; j++) {
for (int i = 0; i < note_count; i++) {
int note = notes[i];
Beep(note * frequency_multiplier, note_duration);
printf(".");
}
}
printf("\n");
}
void main_d() {
int note_duration = 105;
int notes_frequencies[3] = { 220, 139, 165 };
play_notes_d(notes_frequencies, 3, note_duration, 1, 2);
play_notes_d(notes_frequencies, 3, note_duration, 2, 2);
play_notes_d(notes_frequencies, 3, note_duration, 4, 2);
Beep(notes_frequencies[0], note_duration * 6);
}
void main() {
char which_function = 'd';
if (which_function == 'a') {
main_a();
} else if (which_function == 'b') {
main_b();
} else if (which_function == 'c') {
main_c();
} else if (which_function == 'd') {
main_d();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment