Masters of the Void [The Void]
10. Stupid CD Database

Previous | Next

Doing the work

Now that we have the framework for our database program, let's fill it with meaning. Instead of the printf()s, we'll need to actually do some work. Since it would get kind of unreadable if we stuffed all of this into main(), we'll write our own functions, DoNewCommand() and DoListCommand() for doing that. I already wrote about the benefits of extracting code that is used repeatedly into a function of its own, but it's also useful for just making the program easier to read. You'll spend a lot of time reading your source code - definitely more time than writing it - so do what you can to make your code more readable - it'll pay off in the thousands. But now, let's get back to the code:

#include <stdio.h>      // declares printf(), scanf() and fpurge().
#include <stdbool.h> // declares bool.
#include <string.h> // declares strcmp().
#include <stdlib.h> // We'll need that later for malloc() and realloc().

// Data structure:
struct CDDatabaseEntry
{
char artist[40];
char composer[40];
char albumName[40];
int trackCount;
bool isSampler;
};

// Global variables:
int gNumDatabaseEntries = 0;
struct CDDatabaseEntry* gDatabase = NULL;

// Main event loop:
// Fetches user input and calls our DoXXX functions to do the work.
int main()
{
bool keepRunning = true;
char userInput[11];

while( keepRunning == true )
{
printf( "Type NEW , LIST, or QUIT:\n> " );
scanf( "%10s", userInput );
fpurge( stdin );

if( strcmp( userInput, "NEW" ) == 0 )
DoNewCommand();
else if( strcmp( userInput, "LIST" ) == 0 )
DoListCommand();
else if( strcmp( userInput, "QUIT" ) == 0 )
keepRunning = false; // We're finished.
else
printf( "ERROR: Unknown command \"%s\"!\n\n", userInput );
}

DoCleanUp();

return 0;
}

One thing we'll need to be able to write these functions, though, is a place to keep our database. That's what gDatabase above is. It's a pointer-to-CDDatabaseEntry, which we'll be abusing as a dynamic array. You may be surprised that, so far, we only defined variables at the top of our functions. gDatabase is outside our functions. What gives? Well, when you define a variable at the top of a function, it's only visible at the top of that function. When you define a variable outside any functions, it's visible to all code below that. That's what we call a global variable.

Global variables are good when you have one piece of data that needs to be accessed from lots of places. When you have a variable inside a function (called a local variable), only this function can mess with it (unless you hand a pointer to that variable to another function, of course). So, why don't we always use global variables? Well, we'll later see additional advantages of local variables, but the main point is that a local variable's name only has to be unique inside the function you declare it in, while a global variable's name has to be unique across the whole program, because the whole program can see and use it! So, if you used a global for every x you use, you'd eventually end up with names like x123466.

In addition, if a global can be changed from any function, you'd have to be very careful when calling another function. I'm currently using x27, what if the author of that other function also uses and changes x27 and overwrites my number? Admitted, with a tiny project like this, you can just scroll up two lines and check, but as soon as you do a project that's actually useful, or you work on a project together with someone else, you'll love the peace of mind you get from knowing nobody can screw up your local variables.

Note the NULL that we assign to gDatabase. NULL is simply a fancy way of saying "0 as a pointer". When your computer starts up, it usually loads the operating system somewhere right at the start of your memory. So, the address 0 is guaranteed to lie in the operating system's code, and can't be used by your program for anything. So, as a convention, programmers use the memory address 0 to mean "I'm not using that memory yet".

Okay, so now we have to write those two functions. DoNewCommand() is supposed to create a new array element in gDatabase and fill it out with info it gets from the user, and DoListCommand() is supposed to use a while() loop to print the info for each array element, and DoCleanUp() is supposed to get rid of the memory we malloced for our array. Now, three hints:

  1. There's no way to find out the size of a pointer created using malloc(), so we'll also have to keep track of the number of items so we know where our array ends.
  2. Make sure you put the three functions in the source file above main(). The C compiler reads your source file from top to bottom, so you'll get odd error messages if you try to call a function in main() that the compiler hasn't seen yet because it's defined below main().
  3. If the user starts up our application and immediately types in QUIT, we will never have malloc()ed the memory to go in gDatabase (because you can't malloc() a memory block of size 0, and we never created a database entry). So, be sure that DoCleanUp() can cope with this situation.
Want to give it a try yourself?

Below, I'll provide my versions of the two functions we need.

Caution! scanf() will only read the first word of what you type in. So, you have two options: You can just not write any spaces in the names (e.g. write "Simon_and_Garfunkel"), or you could use the getchar() function in a loop to get the whole line out, until you encounter the '\n' character. I'm leaving that as an exercise to the reader.
void DoNewCommand()
{
char yesOrNo;

// First, create a new array element (or a new array if we don't have one yet):
if( gDatabase == NULL )
{
gDatabase = malloc( sizeof(struct CDDatabaseEntry) ); // size of 1 element.
if( gDatabase == NULL ) // Still NULL? malloc() must have returned NULL due to error.
{
printf( "ERROR: Couldn't create a new entry!\n" );
return;
}
}
else
{
struct CDDatabaseEntry* newPtr = NULL;
newPtr = realloc( gDatabase, (gNumDatabaseEntries +1) *sizeof(struct CDDatabaseEntry) );
if( newPtr == NULL ) // Error! Out of memory?
{
// We just keep the old pointer in gDatabase.
printf( "ERROR: Couldn't create a new entry!\n" );
return;
}
// newPtr is our new ptr, gDatabase is no longer valid!
gDatabase = newPtr; // Remember newPtr in gDatabase.
}

// Make sure we remember we have one more entry:
gNumDatabaseEntries += 1;

// Now replace the garbage data in the new, last entry with data the user entered:
printf( "Artist Name: " );
scanf( "%39s", gDatabase[ gNumDatabaseEntries -1 ].artist );
fpurge( stdin );

printf( "Composer: " );
scanf( "%39s", gDatabase[ gNumDatabaseEntries -1 ].composer );
fpurge( stdin );

printf( "Album Name: " );
scanf( "%39s", gDatabase[ gNumDatabaseEntries -1 ].albumName );
fpurge( stdin );

printf( "No. of Tracks: " );
scanf( "%d", &gDatabase[ gNumDatabaseEntries -1 ].trackCount );
fpurge( stdin );

printf( "Sampler? (y/n): " );
scanf( "%c", &yesOrNo );
fpurge( stdin );

gDatabase[ gNumDatabaseEntries -1 ].isSampler = (yesOrNo == 'y' || yesOrNo == 'Y');
}

Not much special in this function. We're pretty much just applying what we learned in earlier chapters. Only two things to point out, and they're all in the lines that mess with gDatabase:

scanf( "%d", &gDatabase[ gNumDatabaseEntries -1 ].trackCount );
The easy one here is that we need to say gNumDatabaseEntries -1 because the number of entries is always 1 bigger than our highest index (our indices start at 0, while a 0 count means no items). And here, we want the number of our newest, last element, which always has the index gNumDatabaseEntries -1.

The other thing to watch out for is called precedence. When you use several operators in a row, there's a certain order they are evaluated in. Just like

5 + 6 * 4
is evaluated as
5 + (6 * 4)
(because the * and / operators have precedence over the + and - operators), the other operators have an order. In the line above, the critical ones are the &, [] and . operators. The way the compiler will read the above is:
&((gDatabase[gNumDatabaseEntries -1]).trackCount)
I.e. it will first get our last entry, then it will get the trackCount field from that, and only then will it get the address. This will not get the address of gDatabase and then try to use that as an array, and it will not get the last element's address and try to get a field from that pointer. Obviously, both wouldn't make sense, but C wouldn't know that. If you're in doubt what operator has precedence, you'll want to either get a good C reference book where you can look it up, or use brackets to make sure C uses the right order. Don't worry about "unnecessarily" using brackets. Brackets don't generate any additional code, they simply control the order code is generated in. And they make things more readable, and you know that that's a Good Thing(tm).

On to our listing function:

void DoListCommand()
{
int x = 0;

if( gDatabase == NULL )
{
printf("There are no CDs in the database.\n");
return;
}

while( x < gNumDatabaseEntries )
{
printf( "Artist Name: %s\n", gDatabase[ x ].artist );
printf( "Composer: %s\n", gDatabase[ x ].composer );
printf( "Album Name: %s\n", gDatabase[ x ].albumName );
printf( "No. of Tracks: %d\n", gDatabase[ x ].trackCount );
if( gDatabase[ x ].isSampler )
printf( "\tThis CD is a sampler.\n" );
printf( "\n" ); // Add an empty line for space to the next CD.

x += 1;
}
}

This is a pretty common thing, and you'll probably write lots of loops like this. You'll always have some sort of counter variable with an initial value (here x = 0), a termination condition that controls when the loop will end (when x < gNumDatabaseEntries is no longer true), and a statement that adds one to the counter (x += 1, a shorter form of writing x = x +1).

Loops like this are actually so common, that C has added a few things to save you some time typing: the for loop and the ++ increment operator. Usually, you'll be using them in cases like:

    int    x;

for( x = 0; x < gNumDatabaseEntries; ++x )
{
// actual code goes here.
}

When I started out, this was unreadable gibberish to me. Not only was it pretty much unlabeled and I had no idea what goes where, no, it's also one command that contains semicolons, so it looked like three commands on one line. And strictly spoken, that's what it is. If you feel more comfortable using while(), feel free to stick to that. I introduced you to while() first because it can do everything you can do with any of C's other loop constructs. Everything else is just syntactic sugar. The advantage of for() is that you write the looping stuff in one go. The start value, the end value, the step. When I originally wrote the while() loop above, I forgot to add the x +=1; line, and when I tested my program it got stuck in an endless loop and I had to abort it.

A few more words about the ++ prefix increment operator: If you want to, you can replace ++x above with x = x +1 or with x += 1. That's fine. You can have loops that take bigger steps than 1 that way. You can also use ++x anywhere you use the others, it will work exactly the same. There's just one thing you rarely want to do: Don't write x++ unless you know what you're doing (i.e. put the ++ after the variable instead of before it). You see, every operation in C has a return value. Yes, even = and +=. Usually, it's the same as the result of the operation. So, if you write

foo = bar +=1;
This will add 1 to bar, and then assign that value to foo. The same will happen if you write:
foo = bar = bar +1;
foo = ++bar;
But when you write
foo = bar++;
It will first remember bar's current value, then add 1 to bar, then use bar's old value as the result of the operation and assign that to foo. Confused? Let's say bar was 20. The three statements above will result in both foo and bar containing 21. The line above, with the postfix increment operator on the other hand will result in foo containing 20, and bar 21. So, whenever you use the ++ operator, be mindful of this difference.

And yes, there's also a -- operator in both prefix and postfix varieties that you can use to subtract 1 from a variable, too.

Now, let's quickly cover our clean-up function:

void    DoCleanUp( void )
{
if( gDatabase != NULL ) // We have allocated memory?
{
free( gDatabase );
gDatabase = NULL; // Not really necessary, but good style.
gNumDatabaseEntries = 0;
}
}

Not much happening here. gDatabase starts out being NULL if we never created an item, so to cover the instant-quit situation, we check for that and do nothing in that case (if we don't malloc(), we don't need to free() anything). Otherwise, we free the database and, just to be nice, we set gDatabase back to NULL and gNumDatabaseEntries to 0. In this program that's pretty unnecessary, but if we were in a bigger program, someone could call DoCleanUp() at some other time to empty the array. This way, we make sure that the rest of the code can still work and won't crash trying to talk to a pointer that has already been freed (and maybe reused).

Previous | Next

Reader Comments: (RSS Feed)
stijnschoor writes:
Dear Uli Thanks for the tutorials, they have been a great help to me. I am also creating a database(not CD's) and your article have helped me alot. Thanks. Maybe you can help me with a question about C, in my database program I want the user to edit the data. Is there a way of printing the data from a file(In my case the FILE pointer file) and let the user edit it? I was thinking about some sort of scanf function where there's already some text. Thank you Stijn
Ryan Stonebraker writes:
These Last two lesson have been very confusing... but after reading them twice I somewhat get it... Thanks for the tutorials though!!!
gsoli writes:
Very cool! This tutorial clarifies a lot of things for me. I especially enjoyed the explanation on the stack and heap in memory. These last few pages have been a little heavy, so I will have to come back again to try to force these concepts into my stubborn brain. It has been fun. Thanks!
Uli Kusterer replies:
stijnschoor, it is definitely possible to do that: There are variants of the functions you already know that work on files instead of the screen. Google for information on the "C standard library", particularly the functions fprintf(), fscanf(), fopen(), fwrite(), fread(), fseek(), ftell() and fclose().
Uli Kusterer replies:
Ryan, gsoli, I hope the new animations will make re-reading this tutorial much more pleasant and understandable than the old graphics with their forests of arrows. Let me know what you think!
tómppu writes:
Thanks for the great tutorials Uli, some parts have been really hard to comprehend but that problem can be solved by trying harder. Learning new things is never easy, but you have managed to simplify this one and most of all made it flow so its interesting and fun! This chapter however has one obvious flaw in the sample code; Artist name, composer or album name can't contain any spaces or at least they're not printed out in the LISTing. One would think that it doesn't matter what goes inside "%39s" since it waits for a string and gets one but apparently it does... Any way to correct this? Thanks!
Richard Howell writes:
I think I copied the code word for word, and at first it worked fine. But now I get this error, and to be fair, I don't understand it at all. Is there something wrong with one of the library files? Internal error occurred while creating dependency graph: _registerUndoObject:: NSUndoManager 0x2016497a0 is in invalid state, must begin a group before registering undo
Richard Howell writes:
I just just down the compiler and restarted it. The program now works fine, lol. ????
John writes:
Hi, First of all, thanks for writing these tutorials. I've never found other tutorials intuitive to follow, but this definitely helps! I'm just confused about two points in this chapter: 1. Inside the function DoNewCommand() with this line in particular: scanf( "%d", &gDatabase[ gNumDatabaseEntries -1 ].trackCount ); is the reason there's a "&" character for trackCount because it's still an integer inside the CDDatabase structure and not a pointer like the character arrays? I just assumed that since I made a pointer to the entire CDDatabase structure that you wouldn't need that "&" character so I guess I'm wrong? 2. My other question is somewhat related to the above, but it deals with the code in the DoListCommand() function and the following lines of code: printf( "Artist Name: %s\n", gDatabase[ x ].artist ); printf( "Composer: %s\n", gDatabase[ x ].composer ); printf( "Album Name: %s\n", gDatabase[ x ].albumName ); printf( "No. of Tracks: %d\n", gDatabase[ x ].trackCount ); Why isn't there a "*" character in front of the pointers for the artist, compose, and albumName character arrays? I thought it would be written as follows: printf( "Artist Name: %s\n", *gDatabase[ x ].artist ); printf( "Composer: %s\n", *gDatabase[ x ].composer ); printf( "Album Name: %s\n", *gDatabase[ x ].albumName ); printf( "No. of Tracks: %d\n", gDatabase[ x ].trackCount ); with trackCount not having a "*" in front since it's still a variable within the CDDatabase structure unlike the character arrays inside which are pointers themselves.
Martin Baker writes:
Missing backslash in DoNewCommand code? printf( "ERROR: Couldn't create a new entry!n" );
Ardeshir Sepahsalar writes:
Here is a DoNewCommand and a helper function to check pointers, with some improvements, it lets you create names of artists and albums with spaces in them using getchar() : int PtrNotOk( void *check ) { bool status = false; if (check == NULL) { printf("Error: Couldn't create entry!\n"); status = true; return status; } else { return status; } } void doNewCommand() { printf("Ok, lets create a new entry in the database.\n"); char yesOrNo; // this is to check if the entry is Sampler or not. int c = 0; // array counter, for getchar() // first create a new array element (or a new array if we don't have one if (gDB == NULL) { gDB = malloc( sizeof(struct dbCD) ); // size of 1 database if ( PtrNotOk(gDB) ) { return; } } else { struct dbCD* newPtr = NULL; newPtr = realloc( gDB, (gNumDbEntries+1) * sizeof( struct dbCD ) ); if( PtrNotOk(newPtr) ) { return; } // newPtr is our new ptr, gDB is no longer valid gDB = newPtr; } // make sure we remember we added one more entry gNumDbEntries += 1; printf("Enter Artist Name: "); while((gDB[gNumDbEntries-1].artist[c++] = getchar()) != '\n') ; fpurge(stdin); gDB[gNumDbEntries-1].artist[--c] = 0; c=0; // remove the new line printf("Enter Composer: "); while((gDB[gNumDbEntries-1].composer[c++] = getchar()) != '\n') ; fpurge(stdin); gDB[gNumDbEntries-1].composer[--c] = 0; c=0; printf("Enter Album Name: "); while((gDB[gNumDbEntries-1].albumName[c++] = getchar()) != '\n') ; fpurge(stdin); gDB[gNumDbEntries-1].albumName[--c] = 0; c=0; printf("No. of Tracks: "); scanf("%d", &gDB[ gNumDbEntries-1].tracks ); // get the address of the Int for scanf fpurge(stdin); printf("Sampler? (y/n) "); scanf("%c", &yesOrNo ); fpurge(stdin); gDB[ gNumDbEntries-1].isSampler = (yesOrNo == 'y' || yesOrNo == 'Y'); }
Uli Kusterer replies:
Martin, thank you, corrected. Sometimes the server eats backslashes because it's trying to be helpful and keep people from injecting code...
Comment on this article:
Name:
E-Mail: (not shown, hashed for Gravatar)
Web Site URL: (optional)
Comment: (plain text only)
Please Enter the following word:
Or E-Mail Uli privately.