This bit is probably the easiest bit to grasp. C functions are broken up into procedures, which are short segments of code. Each function is supposed to do one thing, or act as a gateway to another set of functions.
The idea is that if you assign one function to one procedure, it’s easy to drop it or modify the code without harming the rest of the project. Kind of like replacing just the CD player in your stereo setup, rather than having to replace the whole thing.
You should be grouping your functions together in .c files so that all the functions that are part of one process are located in the same file. For instance, putting all your file access routines in one c file called fileaccess.c, or putting all you animation routines in a file called anim.c.
Now since you break all your functions up by procedures, there must be a way for procedures to talk to each other right? For them to know of each others existence, and for them to share and transfer variables. And there is. And here’s where it gets a little complicated.
Input and output of procedures, Scope and Local Variables.
Procedures have three sets of data they can operate on. The first is local variables. Here are a couple of small procedures.
Void whatever (void)
int a, b, c;
a = 10;
b = 20;
c = a * b;
void evenMoreWhatever (void)
int d, e, f;
d = 10;
e = d * 20;
f = 205;
Now these procedures demonstrate local variables. The variables a, b and c are defined inside the procedure whatever (that means within the curly brackets that define the procedure) and while they can be used by any code within it, they are NOT accessible by any code within the procedure evenMoreWhatever.
They are considered local to the procedure whatever. In fact, you could even name the variables the same in both functions. You will see this done a lot for variables used for ForNext counters, like i, x and z;
There’s more about local variables. They are defined anew every time the procedure is called when the program is running. You should not expect them to still hold data from the last time you ran the procedure.
Every time you come in, you should re-initialize them. (Actually that’s not quite true, if you declare your variables as Static when you define them, they will be around next time you come in to the procedure, but this can lead to problems, and is not generally a recommended habit. I bet I get emails on this one:) ).
Ok, so what’s the next set of data we can talk about?
Well, if you look at the above functions, while they are syntactically correct, they don’t do anything useful. Who cares if in the process we do some math? We can never see the result outside of that function.
Procedural input & output
That’s where the in and output of a process comes in.
Here’s a slightly different procedure.
int whatever (int var1, int var2)
a = var1 * var2;
a -= 20;
It starts getting more interesting here. We start to see data coming in and out of the procedure. The first int declaration is what we expect to come out of the procedure. You can only ever have one data element coming out of a procedure – sort of. There are ways around this, but we’ll get into pointers to memory later.
So anyway, we are expecting an integer to come back from this procedure. The next part of the definition line is the name of the procedure – whatever. Then after that, in brackets, is the data elements we can feed into the procedure. And you can have as many as you want there:), just separated by comma’s. You can mix and match different types to your hearts content too.
Incidentally, if you don’t want data to either be coming in of the procedure, or going out, you replace the variable definitions with the keyword void. This means “don’t expect anything here.”
We have two integers being fed into the procedure here, and we are expecting one out. Since we have already defined the variables coming in inside the function definition, we don’t need to declare them again.
We only need to define the integer that’s coming out. In fact, if we were clever, we wouldn’t even need this, since it’s perfectly legal to use the incoming variables as the out going ones. You could write
int whatever (int var1, int var2)
var1 *= var2;
var1 -= 20;
and it would be the same as the above, only using less variables.
It’s important to point out that unless you are using pointers (of which more later) then the data you have passed into the procedure is a ‘copy’ of the original data. When you change it within this procedure, it remains unaffected out side of the procedure.
void whatever ( int var1, int var2)
var1 = var2 * 20;
void caller (void)
int a, b;
a = 20;
b = 30;
When the process thread in the procedure caller returns from calling the function whatever, a and b will remain unchanged, even though you messed with var1 inside of the function whatever.
If you did want to mess with the value of a, the example would look more like this.
int whatever ( int var1, int var2)
var1 = var2 * 20;
void caller (void)
int a, b;
a = 20;
b = 30;
a = whatever(a,b);
Note – the function whatever will be called with whatever is in a before a is assigned the new value by the function returning.
Notice also the new keyword RETURN. This keyword signals the end of the code thread for this function. If you are expecting to return data from the procedure, then you need this keyword in the function at some point, and definitely at the end. This keyword can also be used in a procedure that does NOT return data, just to prematurely end the code thread inside. For instance
Void whatever(int var1, int var2)
if (var1 <100)
var1 = var2 * 200;
not a really useful function, but you get the point.
The third way to operate on data is to use GLOBAL variables.
There is a split debate on global variables. They are extremely useful, there’s no doubt of that, but they are also very seductive and do sometimes destroy the modularity of code in a project. If all you variables you ever use are global, then the code you produce cannot be called replaceable. Since it is operating on data that is defined at the highest level, the code is bound to the data, and so you can’t just dump code and replace modules easily.
Global variables are effectively the same as local variables, except they are local to the entire program, rather than just one function.
To explain all that better, we need to quickly describe scope, and then show an example.
Scope is the term used to describe the expanse of code that a variable can be seen in. The scope of a local variable is the procedure it is defined in. The scope of a global variable is every file of source code that it’s defined in. Again, an example will better demonstrate this.
Imagine this code is in file main.c
int main( void)
file_count = cant_find_count = 0;
for (i = 0; i < 100; i++)
and imagine this code is in file files.c
int file_access(int file_count)
we are looking at a different file here depending on the value of I
if we can access a file, the we do this code
else, if we can’t find the file, we do
Now, before we get started, I am very aware of how dumb the code above is. I know it’s really stupid code, but it shows the point I’m trying to make, which is all that matters.
The scope of variable I inside of the main function is just the main function itself, and no where else, not in file_access or anywhere.
The scope of the variable file_count and can_find_count is every function in the file main.c You can use them inside of both main and setCantFindCount as you wish, but not in file_access, since that’s outside scope. Right now anyway – we’ll deal with making global files truly global in a second.
One other thing you might notice is that one function in main calls a function called file_access which isn’t in the same file as main. How does the compiler know what that function it’s calling is? Aha. In the example above, it wouldn’t. And in fact the compiler would give us an error. Which leads us nicely into..
Prototyping and Externs.
Prototyping is the method by which you inform the compiler about functions in other files that you want to call. Since the compiler works on one file at a time, compiling as it goes, linking them all together at the end of the process, it needs a way to determine what the function you are calling in one file looks like in the other file. It can’t just open the other file to look, because it doesn’t know where it is. It could be in any of a 100 files you may have in your project. So you have to prototype the function parameters for the compiler.
To prototype a function, you basically duplicate the first line of the function.
So, given our example above, in order for that to actually compile, we’d have to add this line above the procedure main.
extern void file_access (int file_count);
The extern keyword tells the compiler “this function is actually not here, but this is what it’s going to look like when you do find it”.
You don’t actually need the extern keyword, but I always use it, since it denotes to me that the function is in another file, rather than in the same file, only lower down. Oh yeah, we should talk about that quickly.
When you call a function from another function, the compiler MUST have already encountered either that function in that file, or a prototype for it. Imagine it this way. If a book had all the chapters pulled out of order, and you were to try and read it, all of a sudden in one chapter you’d be reading about things that hadn’t happened yet, and you’d get all confused. Compilers are the same. If you try talking to a function, then you must have defined what it looks like (i.e. prototyped it) before you use it.
The eagle eyed amongst us may have already noticed that we do exactly the opposite in the example above.
So, we need to add yet another line to make this compile completely. We need to define the function setCantFindCount before we use it. We do this the same way as we prototype the file_access function, except personally I don’t use the extern keyword, just to remind me that this function is in this file.
Some extern stuff to notice.
You would have thought that having two definitions in a file would be bad, but it isn’t because the first definition has no content.
There are no curly brackets that denote “here is the code”. There can only be one of those in the code anywhere, but you can have prototypes all over the place.
Now we can also use the extern keyword to pass variables between files, so they can be truly global like the name suggest. For example, if we needed to use the variable cant_find_count inside of the file files.c we would place this command at the top of the file, before the variable is referenced.
extern int cant_find_count;
Now there are some caveats with this. Firstly, you can’t use local variables with the same name as global variables. This is a good thing, and stops confusion in the code.
Secondly, you can prototype and extern variables that don’t exist. Now this can lead to real confusions. The compiler won’t flag the error, but the linker will, so at least your mistakes can be caught. The reason this occurs is that your prototyping is saying to the compiler when it builds each file “Hey, there is a routine out there somewhere, this is what it looks like, when you come to link, you’d better figure out where it is, but for now, use this”. And then the linker comes along, and says “Ok, where is this routine then?” and it doesn’t find it, so it pukes.
Now it’s worth mentioning that while externing in the .c file works, it ends up making the code look extremely messy. And so we come to the whole .h file reason for existence. Inside of .h (or Header) files, you can place all the externs that a particular file will need from another file. In fact, better yet, you can place pretty much all the externs you will ever need from one file inside an .h file, and just include that in other .c files. But we are getting ahead of ourselves.
Mostly, the .h file structure tends to follow the paradigm of one .h file for each .c file. Inside of main.h for instance, we would do this.
Extern int file_count;
Extern int cant_find_count;
Extern void setCantFindCount(void);
This prototypes all callable and useable variables inside of main.c for other .c files to use. NOTE – you shouldn’t be prototyping the main procedure. This is a C peculiar thing. Since this is your starting point, you shouldn’t be messing around with this.
However, just generating main.h is not enough to get this information across to other files. You have to indicate to the compiler when it is compiling that particular file that you want to use the prototypes declared inside of main.h. We do this using another one of those clever # compiler directives.
If we wish to use some of the global variables declared inside of main.h inside of files.c, we would put at the top of the code
This would allow us to include the file main.h inside of files.c, and you would have access to the procedures and variables declared within. You’ll notice that almost always these includes are at the top of .c files, since the variables and processes have to be declared before they are used.
Nesting include files.
One really cool thing you can do inside of .h files is include more .h files. Actually, I’ll let you in on a secret. .h files and .c files are almost always treated by the compiler exactly the same. You can even put code in the .h files if you wish. In fact C++ makes you do it. Anyway, I’m personally not a fan of this. It’s hard enough keeping track of code in .c files without messing around inside of .h files, so my advice to you is Don’t Do It.
Anyway, back to nesting. Since you can include one .h file within another, it stands to reason that you can keep doing this ad infinitum. And you can. And it can get real scary, so be careful. However, you should do it once your project gets beyond a certain size. Once we get into defining your our data structures, you’ll begin to see how you can set up .h files for certain structures, and have them automatically included where they need to be used.
Misc notes on using .h files.
Firstly, it doesn’t matter if you declare variables and procedures inside of a .h file that you never use in other files. The compiler just ignores them. However, they do take some processing. For the kind of programs you will be building it’s pretty immaterial. But once you start getting into 100,000 line programs, with nest include files, it can all start getting out of hand, and start affecting compilation times. It’s worth keeping track of exactly what’s being compiled here.
Other notes – input from the calling program.
One thing I’ll deal with here that seems appropriate is the way to get data in from the calling program. When you run most dos programs, you give it parameters. For instance
Copy stuff.c otherstuff.c
Will copy stuff.c to otherstuff.c. Both stuff.c and otherstuff.c are called parameters, since they are fed to the program by the user in order for the program to run. You get the idea.
Well, how do you get this information within the program we are writing? Well, we use the normal procedural input/output to get at this information. C as a language provides us with this data in the form of an array of strings called arg and a number of strings passed counter called argc.
int main (int argc, char *arg)
for(loop =1; loop < argc; loop++)
printf (“incoming arg %d %s\n”, loop, arg[I]);
uses some commands we haven’t covered yet, but that doesn’t matter. The idea is that you can call this program and it will print out all the parameters you fed it. There is no limit on the parameters you can feed it, it just depends on what OS you are using, and how much it will let you stick on a line. Parameters are separated by spaces on the command line when you are calling the program by the way.
Anyway, you can see in this example how the argc integer comes in with a count of the number of parameters on the command line (and note also that the first one, arg will ALWAYS be the program name itself. That’s why the for next loop starts at 1 and not 0) and that the actual string array for each entry is pointed at by the string array arg;
It’s as simple as that. We will deal with for next loops in the next lesson, and then the printf statement in the lesson after that.