03.03 - Selected C Topics

Started by
2 comments, last by Teej 22 years, 11 months ago
This topic is reserved for articles on selected C topics that I write. If you have any questions/comments on this material, please don't post them here -- use 03.06 - Reader Comments. Thank you! Edited by - teej on April 23, 2001 3:16:16 PM
Advertisement
C vs. C++



Cousins or Twins?

Without knowing a single thing about these two languages, you can already guess that there''s some relationship between them just from their names. Unfortunately, the guesswork doesn''t end there, and a lot of confusion has resulted. Are they different languages? What''s the difference between C and C++? Which one is better?

Industry resources aren''t helping much either. If it''s deemed that there''s a demand for C++ literature, publishers are more than happy to oblige. As such, the C language has been relegated to the position of `old'' and C++ resides as the new popular programming language, making matters even worse.

The purpose therefore of this article is to `clear the air'' on any issues concerning C and C++, their similarities, differences and intended uses.

In a Nutshell

C is a modular programming language.

C++ is an object-oriented programming language.

The two languages are practically the same – the difference lies in the way you use them. Take the C language, add a few new keywords, and you’ve got C++. This is one of the main reasons why people exhibit some level of confusion when they ask, “Which language should I learn?” – they don’t realize that they are two different ‘styles’ of programming but using the same language.

The key to understanding what C and C++ are is in understanding what modular and object-oriented programming are. Most popular languages can be classified into one of these two main categories – it’s their method of implementation that sets them apart.

Modular Programming

Modular design is the classic programming model, and consists of creating units of code called procedures or functions. One such unit of code serves as the entry point of the program, and program flow consists of calling these procedures/functions, which in turn can call other procedures/functions. These units of code can be grouped into modules which serve to segregate (read: organize) all of the procedures/functions that make up a program.

Technically, procedures are code units (groups of program statements) that perform some task and don’t return a value to the code that called the procedure. Functions, similar to the way the term is used in mathematics, refers to code units who’s purpose is to return some value based on some calculation. For the remainder of this article, I’ll merely refer to them both as functions (most people don’t use the term procedure anymore in C).

In the C language, the entry point is a function called main(). Whatever your program wants to do (code statements, calls to other functions), it takes place in here. Here’s a quick example (I’m sure that the vast majority of you don’t need an introduction to this stuff):

void SayHello(){    printf(“Hello”);} void SayGoodbye(){} void main(){    SayHello();     SayGoodbye();}  


The main() function is automatically called by the operating system when the program starts. In it, two functions are called that display some words on the screen. Right now we’re not interested in the program’s syntax; just note the fact that there are three functions to the program in total.

When writing large programs, it’s desirable to group functions by their purpose. For instance, I could write a whole bunch of functions that do mathematical calculations for me and place them in a file called math.c. This type of organization is what gives modular programming its meaning. Programs utilizing hundreds of functions can be broken down into modules. Modules consist of functions (and other things). Functions are responsible for doing the actual grunt-work of a program.

Languages such as BASIC (not to be confused with Visual Basic), PASCAL, MODULA(-II) and C are modular languages.

Object-Oriented Programming

OOP (Object-Oriented Programming) involves some new concepts and a different way of thinking. There’s still a starting point, and there’s still functions (called methods in ‘OO-speak’), but they’re organized differently.

For an example, we first need to take a look at what an object is. I’ll just go ahead and show you a simple example:

class Wheel{ public:    Wheel();    ~Wheel();     void Inflate();   private:     int tirePressure;};  class Car{  public:    Car();    ~Car();     void StartEngine();    void StopEngine();    void AddGasoline();    void Accelate();    void ApplyBrakes();    void ReleaseBrakes();    void TurnLeft();    void TurnRight();   private:     int currentSpeed;    bool bBrakesOn;     Wheel FrontLeft, FrontRight, RearLeft, RearRight;}; 


Once again, we’re not here to learn the language itself, so I’ve made the example as simple as possible. Here two entities (‘classes’) are described, the first for a wheel and the second for a car (that has four wheels). A class typically contains variables, but also contains methods (functions) that use the variables. The general idea here is that in order for someone to change the currentSpeed member of the Car class, they would have to use the Accelerate(), ApplyBrakes() and ReleaseBrakes() methods of the class. Take a look at this:

void main{    Car theCar;    TheCar.StartEngine();    TheCar.Accelerate();    TheCar.TurnLeft();    TheCar.ApplyBrakes();    TheCar.StopEngine();} 


This is a small program that creates an object of type Car, and uses the object by calling some of its methods. Keep in mind that I haven’t shown the actual code for each of the methods (functions) – the class declaration is just a template for the compiler to follow. I’ll try and clear up some points with some uhh, points:
  • Once you define a class, you can create objects based on that type. In our program, we create an object (like any other variable) called theCar which is actually an instance of the Car class.

  • The first two methods of the Car class are called the constructor and destructor, respectively. These are special class methods that are automatically called for us when an instance (object) of a class is created in a program. Seeing as the constructor Car() is called before the object is used, it would be a good place to initialize our variables (e.g. currentSpeed = 0). If we allocated any memory in our methods, we could destroy that memory in the destructor (~Car()) before the program is over (before the object dies).

  • See the keywords public and private in the class declaration? Anything in private is just that – private. You can’t for instance try this in your program:

    void main(){    Car theCar;     TheCar.currentSpeed = 10;  // ILLEGAL} 


  • The class methods are responsible for manipulating the variables in the class. For instance, you could check in StartEngine() to make sure that the engine isn’t already started.

  • One class, Wheel, is used inside of another class. Although technically my example isn’t correct, it does demonstrate that methods of the Car class could call methods of the wheel class by using one of the four Wheel objects it owns.


There is much more to OOP than I’m letting on, but the important idea is that objects are instances of classes that represent organized ways of thinking. A prime example is the MFC (Microsoft Foundation Classes) Library, which provides classes that represent windows, controls and other entities used in Windows programming. Here’s a few examples of classes that you can create objects from in MFC:
  • Application

  • Window

  • Button

  • Scrollbar

  • Dialog

  • Socket


You can imagine an application object using a window object that contains other objects representing screen elements, perhaps communicating over the Internet via a socket object. There may be hundreds of methods involved, but they’re organized by what they’re used for (or belong to).

Other examples of OOP languages include Visual Basic, SmallTalk and Java.

So, what goes in these class methods?

Why, C code, of course.

Some Other Considerations

We can see that C and C++ are in fact different. What about the pros and cons of each?

C
  • Easier to learn (more straightforward method of programming)

  • slightly faster

  • still in wide use today


C++
  • better way of approaching programming (more intuitive)

  • more powerful language features

  • simplifies some types of programming (e.g. Windows applications)


As with any language comparison, you’re interested in the following criteria:
  • ease of use

  • intended applications

  • level of type-checking

  • programming model

  • portability

  • learning curve

  • overall popularity

  • language level (low, high, etc.)


It’s C For Us

In closing, I’d like to emphasize that we’ll be using the C language for our game development tutorial. In my opinion, there are two reasons for this decision: speed and learning ability. There’s simply too much C++/object-specific overhead involved with using C++ as a programming language – we’re here to concentrate on the game development itself, and not the idiosyncrasies of C++ or object-oriented design itself. With C, what you see is exactly what you get…there’s no other magic taking place in the background.

Looking on the bright side, catering to C programmers allows for the widest audience, seeing as one can’t be a C++ programmer without being able to write code in C.

I realize that my attempt at outlining the relationship (and contrast) of C and C++ is a rather pathetic one, but at least you get somewhat of an overview, and the relationship between C and C++ is more clearly defined in your mind. I personally think of the two in terms of how a program is designed. When it comes to implementation, you’re almost doing the same thing – writing code statements.

Please post any questions or comments in 03.06 - Reader Comments.
Preprocessor Melodrama

This article addresses the use of preprocessor statements in the template code, as well as the global structure ‘G ’. Incidentally, it takes us through a discussion on scope (code visibility, not the mouthwash) and overall program organization.

Trust me, I understand the disadvantage inherent in picking up someone else’s code and trying to make heads or tails from it. One of my chief complaints in learning something new in programming is that code seems complicated for no apparent reason – sure, the original author probably had some motive at the time, but that whisper in his/her ear has long since been forgotten. So, let’s take a little time out to learn about code visibility from the ground-up, so that we might perhaps re-discover what the heck it was I was thinking when I threw that ‘G thang’ in there.

Meet Mr. MaGoo, the C Compiler

If you don’t remember Mr. MaGoo, he’s the blind old man in cartoons that’s always getting into crazy situations because he can’t see anything (oh, and he’s very absent-minded). To a certain extent, our loveable C compiler doesn’t perform its duties with the leniency and foresight we’d expect – there are many things in your code that will cause the compiler to complain, and often enough it’s because it seems to play ‘dumb’ when trying to figure out what you’ve coded. By knowing what the (strict) rules are that the compiler follows, you can beat it at its own game and give it nothing to complain about.

What actually happens when you try and build your program? The first step in the build process consists of the preprocessor going through each source file and resolving any macro definitions and control statements. It’s important to realize that this happens before the compiler ever gets to see the code, which is the whole reason for the preprocessor in the first place. If it comes across an #include directive, the contents of the included file are added in-place, as if it were a copy/paste operation.

Then, the compiler begins. Each source file is ‘read’ from top to bottom, and its statements are translated to machine code (in OBJ files). Line by line, statement by statement, the compiler tries to evaluate everything it sees. Naturally, the compiler is aware of all C language keywords and evaluation rules. For everything else, it makes no assumptions whatsoever. If it comes across a variable or function name that hasn’t been declared before, you’ll have an error on your hands.

A variable must be declared before it can be used. You can’t have something like


num = 5;


without having declared the variable num somewhere earlier in your program like so:


int num;


When the compiler sees declarations such as this, it remembers the identifier and knows what to do in the future (i.e. later in the source file).

The same is true with functions. A function prototype (or definition) is a statement which tells the compiler what a proper function call needs to look like. For instance,


int DoSomething(int aNumber);


tells the compiler that a function called DoSomething exists that accepts an integer and returns an integer. If, later on in your code, you have


Result = DoSomething(5);


the compiler can verify that (a) the function exists, and (b) the calling syntax has been followed properly. The rule here is that a function must be declared before it is used in a program – either with a prototype (as above, a ‘preview’ of the function), or with the actual implementation. So, this would be incorrect:

int main(){    DoSomething();} void DoSomething(void){    printf(“Hello there”);}  


The compiler sees the call to DoSomething() , but hasn’t yet seen the function itself, so an error results. If you place the DoSomething() function before main() , everything would be fine because the compiler has prior knowledge of the function’s existence. As well, this would work fine:

void DoSomething(void); int main(){    DoSomething();} void DoSomething(void){    printf(“Hello there”);}  


The essence of what I’m trying to get across is that the compiler doesn’t necessarily need to know what the function does – it just needs to know what the proper method of calling it is (i.e. number of parameters, parameter types and return value). The same is true with variables.

Scoping Rules

It’s almost as if the compiler starts at the top of your source file and ingests everything that it reads, but that’s not quite the case. There are rules that determine what the compiler remembers, and when it decides to forget what it’s seen. These rules are referred to as scoping rules because they dictate the scope of visibility (to the compiler) of an identifier (variable or function).

You should already be familiar with the majority of these rules. For instance, any variable declared inside of a function is only visible to that function. If a variable is declared outside of any function, it is considered global. Functions and global variables both have one thing in common – they are accessible to any other source file.

Okay folks, now read carefully – this part throws a lot of people off. Functions and global variables are global, which means that they can be used anywhere in any source file of your project. BUT, and here’s the thing, they aren’t automatically visible to other source files! Let me rephrase that: A function or global variable can be used at any point from when it’s declared, up to the end of the current source file. After that, the compiler forgets about these declarations when starting on the next source file. Every source file is treated as a new entity, as if it was the only source file in the project. Let me repeat that:

Every source file is treated as a new entity, as if it was the only source file in the project.

Don’t worry; I know I haven’t described the issue clearly yet. Let’s start with functions:

// ONE.CPP #include  void main(){    DoSomething();} //////////////////////////////////////// // TWO.CPP void DoSomething(){    printf(“Hello there”);}  


Let’s assume that the compiler starts with ONE.CPP. Note that the order doesn’t matter one bit because as I’m trying to convince you of, the compiler starts fresh every source file anyhow. In ONE.CPP, DoSomething() is undefined. The compiler is going to see that function call and have no idea what it is because it wasn’t declared or defined first. Even if the compiler had chosen TWO.CPP first, the error would still be the same because it doesn’t retain any information on functions or variables from one source file to the next. Also, note that TWO.CPP would also fail to compile because printf() is not defined. Sure, we all know what it is, but as far as the compiler is concerned it may as well be called SomeDamnedCrazyThing() – it’s never seen it before.

Here’s the correction:

// ONE.CPP void DoSomething(); void main(){    DoSomething();} ////////////////////////////////// // TWO.CPP #include  void DoSomething(){    printf(“Hello there”);}  


In a way, it’s funny that the compiler confuses us with its errors, when all we have to do to understand why it complains is just act ‘stupid’ and go from the top of our source file down. Heck, if you were really enthusiastic you could get out some paper and a pencil, writing down each identifier as you see it, ensuring that everything used is declared.

You might be wondering about the function prototype in ONE.CPP, and how it knows that we’re talking about a function in TWO.CPP. Don’t worry about that for the moment.

We’re not done yet; there’s still global variables to consider. In almost the same way, we take something like this (that doesn’t compile):

// ONE.CPP // Function prototype (from TWO.CPP)void DoSomething(); // Global variableint i; void main(){    i = 5;     DoSomething();} //////////////////////////////// // TWO.CPP #include  void DoSomething(){    i = i + 4;     printf(“The number is %d”, i);}  


…and fix it by declaring the global in TWO.CPP like so:

// TWO.CPP #include  // This variable is declared in ONE.CPPextern int i; void DoSomething(){    i = i + 4;     printf(“The number is %d”, i);}  


You may be tempted to place a line like this:


Int i;


Inside of TWO.CPP, but watch out! A global variable can only be declared once. If you declare it again, the compiler will treat it as a new variable and not the one used in other files. No, you need a way to tell the compiler that we want to use the same variable as in ONE.CPP, so we use the extern keyword. Simply put, it tells the compiler that the variable has already been declared somewhere else. With function prototypes, you’re basically doing the same thing…

So, what’s the basic rule here? For global variables, declare them once in your program (in any source file), and use the extern keyword in any other source file that uses it. For functions, just make sure that a function prototype appears in every source file it’s being called in.

Header Files

You can imagine with large programs consisting of many files how much of a pain it would be to have to add prototypes and externs at the top of every source file. This is where header files come in. Imagine you had fifty functions that are used in different source files. You could place all of the prototypes in a header file, and just add an #include to the top of each one. What would happen is that for every source file read by the compiler, the contents of the header file are ‘pasted’ in the top of the source file, and now the compiler knows what the heck you’re talking about when making all of these function calls. The same is true with global variables – just remember to use the extern </i> keyword.<br><br>Oops. Throwing all of your global variables in a header file using <i>extern </i> s is going to give you a big problem. You see, if every source file inherits the header file, that means that your global variables are declared as external in each of them. So, where’s the actual declaration? Somebody’s got to own each global variable in order for your program to work! You’d need each one declared in one file, and declared external in all of the other files that use it…<br><br><font size="+1" color="#66FF66">Back to G</font><br><br>Now go ahead and bring up GLOBALS.H. Pretend that we’re the compiler and we’re at the start of one of our source files. The first thing we’re asked to do is include GLOBALS.H, so we paste it right into our source file. The first <i>#ifndef </i> that we see is there to make sure that this file, GLOBALS.H, doesn’t get included more than once per source file. I know what you were thinking – ‘more than once per project’, but you’re wrong. Every new source file that the compiler translates gets this header file anew. Sometimes there might be a tricky situation where one source file includes a header that includes a header and so on… and somehow the same header makes it into this web of <i>#include </i> s more than once. That is what the <i>#ifndef </i> is trying to avoid.<br><br>Let me show you a simple form of GLOBALS.H:<br><br><pre><br>#ifndef _H_GLOBALS<br>#define _H_GLOBALS<br> <br>// …<br>// Contents of GLOBALS.H<br>// …<br> <br>#endif // _H_GLOBALS<br> </pre> <br><br>What this says is “If _H_GLOBALS is not defined yet, define it and read the rest of this file in”. If somehow this was the second pass through GLOBALS.H in the same source file, the entire file would be skipped (avoiding re-definition errors). The result is that GLOBALS.H is included in every source file once and only once.<br><br>Take a look at the G structure near the bottom of GLOBALS.H. Does it now make sense to you? <i>Here’s a hint: only one source file defines GLOBALS_OWNERSHIP… </i> <br><br>Every source file in our project includes GLOBALS.H. If the source file has GLOBALS_OWNERSHIP defined in it, the entire G structure is added to that source file like so:<br><br><b><br>struct<br>{<br> // Members…<br>} G;<br> </b> <br><br>If not, the extern keyword gets thrown in, giving us:<br><br><b><br>extern<br>Struct<br>{<br> // Members…<br>} G;<br> </b> <br><br>which is exactly the same as:<br><br><b>extern struct {…} G; </b> <br><br>which is an external declaration. All source files but one get this line added to the top of their source file.<br><br><font size="+1" color="#66FF66">One More: ‘Static’</font><br><br>This damned keyword has many different meanings, depending on the context you use it in, so I’m going to offer a direct quote from the Visual C++ Programmer’s Guide:<br><br><i>When modifying a variable, the static keyword specifies that the variable has static duration (it is allocated when the program begins and deallocated when the program ends) and initializes it to 0 unless another value is specified. When modifying a variable or function at file scope, the static keyword specifies that the variable or function has internal linkage (its name is not visible from outside the file in which it is declared). </i> <br><br>In plain English, static variables inside functions don’t lose their value between function calls, and global variables and functions are limited to the current file. We’ll definitely be using static variables, but don’t worry yourself over any of this now.<br><br><font size="+1" color="#66FF66">In Closing</font><br><br>Hey, what can I tell you? Our programs are going to get larger and more complicated, and we’re going to need some ‘clean’ way of organizing our globals and functions. With GLOBALS.H, I pulled out a little trick I use to aid in making these globals automatically available to any source file (that includes the header). We’ll be playing with preprocessor directives and other tricks when we start learning some debugging skills later on.<br><br>Well… this whole article to answer one simple question; “Why are all of your globals in a structure?”<br><br>Simple – <i>I didn’t feel like typing <b>extern</b> on every line </i> . <img src="smile.gif" width=15 height=15 align=middle><br><br><br><font color="#FF6666">Comments? Questions? Please reply in <u>03.06 – Reader Comments</u></font><br><br><br>Edited by - teej on May 7, 2001 1:57:07 PM
Pointers, Pointers, Pointers

Without question, pointers is the number one C topic that can take people''s brains, thrash them around and dump them in an alleyway confused and disoriented. I''ve seen chapters on pointers in many C books, and most do a terrible job of describing them in a way that clicks with people. What ends up happening most of the time is that people read through it, catch the general jist of what they''re reading, but don''t actually retain a complete knowledge of what pointers really are. I can say this because although almost everyone knows that a pointer has to do with addresses, many people fall apart when it comes to using pointers in more complicated scenarios.

Alright, so this article is my opportunity to do a better job than the average book, and fix that crazy notion of pointers in your head once and for all. After you''re done reading this, some of you might have a revelation and suddently everything will make perfect sense -- that''s what we''re shooting for.

Picturing Memory

We need to start by coming up with a way to think of memory in a computer, how it''s arranged, and how it''s accessed. This is very important, because if you have the wrong picture in your head, many things will confuse you. Trust me.

Memory is the ability to ''remember'' states. The smallest unit of memory is called a bit, and is technically the current level of voltage in a conduit (wire, etching, whatever). If the voltage is at one level, we call the bit ''on'', ''1'', ''true''. If the voltage is at another level, the bit is considered ''off'', ''0'', ''false''. It''s not important to us how the computer''s memory really works -- just that it can get and set the ''value'' of a bit.

Okay, so let''s learn how to properly visualize memory. Do you know what a ticker tape is? It''s a long narrow piece of paper, kind of like a roll of scotch tape, a roll of film or a ribbin. Let''s pretend that the left end of the ticker tape is the beginning of memory, and that memory continues to the right, far, far away. Every bit in the computer has a value of 0 or 1, so picture a long sequence of 0''s and 1''s starting at the beginning of memory and continuing off into the distance along the ticker tape:

01011011101011011101101011011110110101101101101101...


Now, in order for the computer to be able to get or set the value of a bit, it has to be able to access it, which means that it needs the address of a bit. If all of the bits were numbered (so for example the seventh bit above would be ''1''), we could tell the computer "What is the value of bit seven?" or "Set the value of bit seven to zero". Since the poor computer needs to be able to access memory by address, and addresses can get quite large if you numbered every individual bit, it was long ago decided to group the bits together in sections of eight, called bytes, and address memory by byte rather than by bit.

In other words, take our mental ticker tape and group the bits like so:

01011011 10101101 11011010 11011110 11010110 11011011 01...0        1        2        3        4        5        6 


Here I''ve labelled each byte with their address in memory, starting at zero. Today''s typical computers can access up to 4,294,967,296 (4 GB) of memory, which means that bytes are addressed from 0 to 4,294,967,295. This limit exists because the computer has 32 address lines (think 32 bits) that it uses to access memory. Of course, you need to actually have that much memory in your system in order to be able to use it all.

In reality, the operating system takes control of all the available memory in the computer, and dishes it out to programs that need it. Also, it assigns us virtual memory instead of physical memory, which means that the memory addresses it gives our program aren''t the true addresses, but it doesn''t matter -- to our program, a virtual memory address is the same as a real memory address. Don''t worry about this -- memory is memory as far as we''re concerned.

Now for the fun part. Forget everything you know about data types -- there aren''t any. The computer doesn''t know an integer from a float from Ghandi or Barbeque Doritos. It only knows bytes. So, in the above example, what''s the value of the second byte (in decimal)? 173? -83? Both. Memory has no concept of positive and negative numbers, let alone number systems. We decide how the eight bits in question are read, not the computer.

Fundamental Data Types

Let''s rebuild, from scratch, the whole idea of data types, and then we''ll be in proper position to tackle pointers.

So, you''ve got all these bytes to play with...since our ticker tape only deals with bytes in sequence, we need to figure out how to get numbers, letters, words, structures, etc. into this memory and back again.

All data types in C derive from the following atomic (basic) defined types:
  • char: 1 byte

  • int: 4 bytes

  • float: 4 bytes

  • double: 8 bytes


Characters consist of letters, numbers, and punctuation. All of these characters are numbered, and as it turns out there''s less than 256 of them all together, so a single byte is sufficient to store one. Each character is numbered in a table (called the ASCII table), and it''s this numerical value that''s placed in the byte, using standard binary. So, for instance, the letter A is value #65 in the ASCII table, so the value 65 (or in binary, 01000001) is placed into the byte. Since there were empty slots in the ASCII table, companies (such as IBM) filled some of them in with other non-printable and control characters...it''s not important though.

Integers are whole numbers, either positive or negative. The number of bytes that are needed to store a number depends on how large the number is. Because integers can be negative, the compiler needs some way of indicating this, and therefore uses the highest (leftmost) bit as a sign flag -- 0 for positive and 1 for negative. The remaining bits are used to store the value; positive values are stored in the normal way, and negative numbers use a special two''s complement notation in binary to determine the actual value to store.

Floats and doubles are floating-point values (i.e. those that contain a decimal place), and the compiler has its own special way of converting a number like 4.3732 into the four (for float) or eight (for double) bytes in memory. If you''re really interested in what values are actually stored in memory, consult your compiler''s online help -- it''s not necessary to know for this article...just let the compiler handle it for you.

The C language also provides modifiers that ''adjust'' the way a data type is used and/or it''s storage capacity:
  • unsigned: Used for char or int. Tells the compiler that we won''t be using any negative values, so use the highest (leftmost) bit as part of the value. This effectively doubles the range of the maximum value. for instance, a char can hold a value between -128 and +127, but an unsigned char can hold values from 0 to 255.

  • long: Used for int and double. For int, does nothing (int used to be a 2-byte data type, and back then long would have doubled it''s capacity to 4 bytes, but these days ints are 4 bytes anyways). For double, enlarges the capacity by 2 bytes (from 8 to 10) for increased accuracy.

  • short: Used for int. Shortens the capacity from 4 bytes to 2. In other words, short ints have a smaller range.


Folks, those are the only data types defined for the C language. Every other data type in C must originate from one of these. Here it is again -- "Every other data type in C must originate from one of these: char, int, float, double". More on this in a moment.

So, what it all boils down to now is getting these data types onto our ticker tape (i.e. memory). When you declare a variable like this:

int i;


...the compiler seeks out four bytes of memory and assigns the address of the first byte of that memory to the identifier ''i''. When you use I in a program:

i = 5;


...the compiler looks up the address for the variable i and deposits the value 5 into it. So what is i, exactly? It''s the address that the value 5 was put into. If the address of i was 12 (think of the ticker tape), then the compiler would look up the address of i (which is location 12), and use the next four bytes (i.e. locations 12, 13, 14 and 15) by assigning a value of 5 to the memory. The compiler has everything it needs to do this: it has the variable name (from which it can look up the address internally), and the variable type (so that it knows how many consecutive bytes are involved and how to ''prepare'' the value for our program). Do you see the importance of a variable''s type? Check this out:

int i = 10;
float f = 2.6;
char c = 51;
double d = 5.57335;


Using our ticker tape analogy and starting at location 0, the above code would allocated locations 0-16 for these variables (int = 4 + float = 4 + char = 1 + double = 8). So, what''s at location 5? Obviously there''s something at location 5 -- every byte from location 0 to location 16 is currently in-use. But, to the computer it''s just byte after byte of memory. If you didn''t look at the code sample above and read your ticker tape from left to right, you''d have no idea what you were looking at. If, however, I told you to go to location 8 and I also told you that it''s a single-byte signed value, you''d be able to tell me that the value is 51. Every byte has an address, but you also need some information on what you''re supposed to be looking at for that address in order to decide if maybe the next few bytes are also part of the data type.

So, if we were to pretend that we are the compiler and the ticker tape is our memory, we should be able to step through a simple program together. We''ll need a piece of scrap paper as well so that we can write down the variable names and addresses too. Let''s try an example:

int a, b, c;

a = 5;
b = a * 3;
c = a + b;


So here''s our layout:

Variable: a
Type: int
Location: 0
Size: 4

Variable: b
Type: int
Location: 4
Size: 4

Variable: c
Type: int
Location: 8
Size: 4

And here''s our ticker tape at the end of the execution of the above statements:

00000000 00000000 00000000 00000101 00000000 00000000 00000000 00001111 00000000 00000000 00000000 00010100 


or, in decimal,

0, 0, 0, 5, 0, 0, 0, 15, 0, 0, 0, 20----a-----  -----b-----  -----c------ 


One thing I didn''t mention before was that if you take a value like 5 and convert it to binary 101, placing it into memory involves not only padding it to 32 bits, but placing the bytes in memory in reverse order (this is an architectural design ''effect'' of Intel CPUs called reverse-endian ordering). That''s why the bytes look backwards in that last example. So, the ticker tape reads like this: 4th_byte_a, 3rd_byte_a, 2nd_byte_a, 1st_byte_a, 4th_byte_b, 3rd_byte_b, 2nd_byte_b, 1st_byte_b, 4th_byte_c, 3rd_byte_c, 2nd_byte_c, 1st_byte_c.

I do believe that we can finally tackle pointers.

Pointers Unmasked

Addresses are numbers too, just like with our ticker tape. It turns out that being able to store and use actual addresses is an important, if not the most important powerful aspect of the C language. Let''s start out with a simple definition of a pointer:

A pointer is a variable that contains an address as its value.

Today''s computers can address 4GB of memory, which means that an address is a 32-bit value. In other words, addresses are 4-byte values. Pointers are declared almost the same as regular variable, except that an indirection-operator (''*'') is used to declare a variable as a pointer, like so:

char *p;


Here, p is a pointer. It takes up four bytes of memory because its value is an address. Don''t get confused by the data type char -- that just tells the compiler what type of address p will hold -- the address of a char. You can also write it like this if it helps make the distinction clearer:

(char*) p;


The compiler needs to know what type of address is being stored so that when you want to access the memory that the pointer ''points to'', it knows what type of data we''re talking about. We''re telling the compiler that the address that p holds refers to a character. This way, the compiler can correctly use the address in p. Here''s an example:

char c;
char *p;

c = ''H'';
p = &c


The ampersand (''&'') is called the address-of operator, and does exactly what it sounds like -- gives the address of a variable instead of its value. Here, c hold the letter ''H'', which is the value 72. The variable p holds the address of c, which is some address in memory. Let''s look at the ticker tape:

..., 58, 126, 221, 8, 72, 0, 0, 0, 44, 3, 11, 252, ...

Assume that the first address in this piece of ticker tape is address 40, the address of c is 44, and the address of p is 45. You can see that the value at location 44 is 72 (i.e. the letter ''H''), and that the four bytes beginning at location 45 is the value 44 (0, 0, 0, 44), which is the address of c.

There is also a way to get the value that p points to -- use the indirection-operator again, like so:

*p = ''R'';


The ''*'' is used in two different contexts in C; when declaring a variable of type ''pointer'', and in getting the value that a pointer points to. The line above will actually set the value of c to ''R'' because p holds the address of c, and the indirection-operator tells the compiler that we want to change the memory pointed to by p and not the value (address of) p itself.

Just resist the temptation to think that a pointer is anything other than an address value. Pointers are four-byte variables, that''s all. The address value can be the address of another variable, or any other address at all. By the way, when the value of a pointer is an invalid address (i.e. an address that your program doesn''t know) and you try to access that address, you crash the program (or in NT, get a General Protection Fault (GPF)).

Arrays and Pointers

Arrays are a lot like pointers. Okay, they are pointers. Check this out:

char szArray[10] = "Hello";


This declares an array of ten characters, and fills the first 6 in (5 for the letters and the 6th for an end-of-string character (value ''0'')). Let''s say that our ticker tape starts at location 0, with the variable szArray:

72, 101, 108, 108, 111, 0, 0, 0, 0, 0, ...

The variable szArray is just a pointer that points to the first character in the array at location 0. In other words, the value of szArray is 0. If you use szArray in a string function, bytes are read continually until the end-of-string character (value 0) is reached. szArray could have been declared as

char szArray[10000];


...but the variable szArray would still be only four bytes long -- a pointer that contains the address of the first element in the array. You''ll see a lot in code where an array is used directly where a char-pointer is called for, because the two are the same thing.

The Benefits and Pitfalls of Pointers

A pointer is an address variable. All pointers are four bytes wide. Pointers must have a type that describes the memory at the address they point to. int pointers point to int variables, and int variables only, for instance. They need to be of a certain type because pointers only point to a byte, which could be the only byte, or it could be the first byte or a longer data type.

Here''s an example of one benefit of using pointers:

struct RECORD{    char szFirstName[80];    char szLastName[80];    char szAddress1[80];    char szAddress2[80];    char szPhone[20];    char szNotes[200];} RECORD r; 


Here, r is a variable of type RECORD. The variable r requires 460 bytes of memory. If you had a function like this:

void PrintName(RECORD r){    printf("First Name = %s", r.szFirstName);    printf("Last Name = %s", r.szLastName);} 


...then the entire record r has to be copied into PrintName(), all 460 bytes of it. If, however, you just sent a pointer to the record''s data like so:

void PrintName(RECORD *pRec){    printf("First Name = %s", pRec->szFirstName);    printf("Last Name = %s", pRec->szLastName);} 


...then only four bytes is transferred to the function. Notice also that when accessing structure fields with pointers you need to use the ''->'' notation instead of ''.'', because szFirstName and szLastName aren''t members of pRec (which is just a four-byte pointer), but of the structure pointed at by pRec. Also notice that the compiler knows that szLastName is 80 bytes past the address in pRec, because the pointer has a type (RECORD). So long as the computer has a four-byte address to memory, and an idea of what''s supposed to be in that memory, it can access it properly.

Another benefit is that with the first example of PrintName(), the record r is copied into the function, and therefore any changes to r are lost when the function ends. After all, it''s only a local copy -- not the original record r. When using a pointer however, we have the actual address of the real record r, so we can make changes to the original memory.

The single largest pitfall to using pointers is that you are responsible for making sure that the address it holds is valid when you try to use it. That means that the memory must belong to you, and the type needs to be valid.

Stack and Heap Memory

If your program needs memory, it can either allocate memory on the stack (i.e. create a local variable), or ask the operating system for some more ''permanent'' memory from the main supply -- called the heap. Whenever we ask the operating system for memory (by using malloc(), calloc() or new), we''re given back a pointer to the first byte of that memory. This pointer needs a proper type in order to be used. For malloc(), you get back a pointer of type VOID. VOID is a special type which denotes an absense of a real type. Therefore, you can''t actually use a void pointer -- you need to cast (i.e. change the type of) the pointer to something that you can use. If you are asking the operating system for space for 100 integers, then you''d use a line like this:

int *p;
p = (int *)malloc(100 * sizeof(int));


Now, the compiler knows that you''re talking about ints, and can let you properly access the memory with something like this:

p[8] = 4;


...which sets the 9th element (start counting at 0!) to 4. If it wasn''t for the pointer type, the compiler would have no idea how many bytes to move over from the starting address to get to the 9th element. With the new operator (a C++ keyword), you specify the type at the time you ask for the memory, and supply the proper pointer directly like so:

int *p;
p = new int[100];


Both do the same thing. Remember that in either case, you need to give the memory back to the OS when you''re done with it, or else the memory is permanently lost (until you reboot).

And now, for a very important note. Stack memory is only valid as long as the variable it''s attributed to doesn''t go out of scope. Here''s a classic error:

char *GetName(void){    char szName[80];     scanf("Enter your first name: %s", szName);     return szName;} 


Here, you''re allocating 80 characters on the stack (local memory) and asking the user for their first name. Then, you''re returning the pointer to the character buffer. Unfortunately for you, the szName array is destroyed when the function is finished, which means that the address you returned is no longer valid. The first time another part of your program tries to access szName, BOOM!

Another favorite is the use of a pointer that hasn''t been set to any address at all. Something like this is just plain bad:

char *p;

p[5] = ''A'';


The variable p is a pointer, and like I''ve said, is a four-byte variable that holds an address. Does it point to anything here? No. Therefore, it doesn''t have any memory either. Using pointers means accessing memory that it points to. The initial value of p is just like the initial value of any local variable -- garbage. And when you try to access garbage? BOOM!

Pointers to Pointers and Other Craziness

Do you remember me saying that pointers and array names are the same thing? Keep that in mind when you see something like this:

char **p;


Let''s take this thing apart. First, note that the following two declarations are equal:

char *pArray = new char[20];
char szArray[20];


Both have allocated 20 characters of memory, the first on the heap, and the second on the stack. Now, since a pointer is a data type like any other, wouldn''t it be possible to create an array of them? Why not! Here:

char **pArray = new char*[20];


This is an array of twenty char pointers. Here''s how you''d access them:

pArray[0] = NULL;
pArray[1] = NULL;
pArray[2] = NULL;


It''s just like any other array, except that each element is a char pointer. And since each element is just an address variable, you need to point it at something before you can use it. In this next example, I create a 3x20 array using both methods:

/////////////////////////////////////////////////////////// 3x20 using pointers char **pArray = new char*[3]; pArray[0] = new char[20];pArray[1] = new char[20];pArray[2] = new char[20]; strcpy(pArray[0], "Hello");strcpy(pArray[1], "There");strcpy(pArray[2], "Everyone");  printf("%s %s %s", pArray[0], pArray[1], pArray[2]); // remember to delete heap memory!delete[] pArray[0];delete[] pArray[1];delete[] pArray[2];delete[] pArray; /////////////////////////////////////////////////////////// 3x20 using arrays char szArray[3][20]; strcpy(szArray[0], "Hello");strcpy(szArray[1], "There");strcpy(szArray[2], "Everyone"); printf("%s %s %s", szArray[0], szArray[1], szArray[2]); // No memory to delete here; it''s all local (stack) memory 


As you can surmize by looking at the above example, a double array and a double pointer have a lot in common. Pointers are great for managing dynamic lists because each element (in this case a string) can have any length you wish, and the entire memory used for the structure is on the heap, so you can pass it around your program without any hassles.

Technically, ''**'' really means ''a pointer to a pointer'', or ''the address of a pointer''. Is there another use for this? Sure there is. What if you wanted to write a function that allocated a buffer of memory from the heap and returned the pointer to the caller? Here''s a solution:

char *CreateCharBuffer(int size)
{
char *temp = new char[size];

return temp;
}


In this case, it''s okay to pass temp back because the memory it points to (in other words, it''s address value) is valid even after the function completes -- the memory lives on. Someone that uses the function like this:

char *pMem = CreateCharBuffer(100);


...is going to get the address of the heap memory back, because temp held the address of the memory, and the value of temp was copied over to pMem with the return statement. After that, temp died because it was a local variable, but the address it held is still valid. Just remember to free the memory at some point!

Perhaps here''s a better illustration of ''pointers to pointers'':

void CreateCharBuffer(char **ppBuffer, int size)
{
*ppBuffer = new char[size];
}


This function accepts a pointer to a pointer, and creates a buffer for it. Why a pointer to a pointer? Well, for the same reason you''d pass a pointer to anything...If you pass a pointer to a data type, the function can alter the value of the data type. The same goes here -- we''re passing a pointer to our buffer pointer so that the function can alter the value of the buffer pointer, which in this case assigns it the address of the newly created data. Now, when it comes to the use of the ''*'' indirection operator in the function itself, it''s not my fault if it''s a little confusing...the designers of the C language use the ''*'' for two different things, one to declare a pointer type, and another to dereference (get the value of) a pointer. The function body statement reads, "The value of ppBuffer is equal to the address of the new character memory". Just think "the value of" whenever you see it in action and you should be alright.

Now here''s how you''d use the function above:

char *pBuf;

CreateCharBuffer(&pBuf, 100);


This is another one of those ''say it out loud'' things... If pBuf is a pointer, then &pBuf is ''the address of pBuf''. It''s the address of the pointer itself -- the address of the four-byte memory, not the address that pBuf points to. Get it?

Okay, Enough Already!

Wow, this has been one long article. Looking back, I realize why books have such a hard time with the subject, and I don''t think I did any better, unfortunately. Perhaps it''s still a help to some of you though, as the important thing about pointers was emphasized enough times in the last while. Most importantly, you should be able to work out for yourself any pointer-related code you come accross, and that''s the important thing. Eventually, this material becomes second-nature through constant use, and the only hard part becomes trying to explain it all to someone else

Repeat after me:

  • A pointer is a variable that contains a memory address as its value.

  • ''&p'' means ''the address of p''. The address of a value is it''s location in memory. The address of a pointer is the pointer''s location in memory, not its value (which is another address).

  • ''*p'' means ''the value of p''. Used with pointers, this means ''the location p points to''.

  • ''**p'' means the adress of the pointer p. It''s the memory address of p, not p''s value.

  • Pointers make more sense the more you use them (even I''m confused reading the above!)


Whew, the article is over (read: I''ve given up trying to explain it).

Questions? Comments? Head over to READER COMMENTS and post ''em there!

This topic is closed to new replies.

Advertisement