Life without Objects: Remembering Structured Programming in C

 < Free Open Study > 



I'm quite sure that your daily programming tasks involve using objects at some level, and I'd bet you're happy about this. However, it is important to know exactly what problems object technology (and therefore COM and ATL) attempt to solve in the first place. In short, we need to "remember our roots" and see why procedural programming languages fall short in the current state of software engineering. To begin, let's journey back in time to a world without objects and tackle a simple development task using the structured programming (SP) language C.

Programmers using the C language (or any structured language for that matter) tend to work in the realm of global atoms. From a very high level, programs built using structured programming techniques operate something like this: A given application consists of some number of data points (e.g., variables), dwelling either in the program's global namespace (e.g., out of a function scope) or locally within some function scope.

Along with the program's internal data, SP applications are also composed of a number of global functions that operate on various portions of the application's data set. If you can't tell by now, the operative word in these last few paragraphs is "global." Structured languages such as C move data into and out of global functions to control the execution and flow of the program, as illustrated in Figure 1-1.

click to expand
Figure 1-1: Internal organization of an SP application.

Here, MySPApp.exe consists of three pieces of global data, collectively operated on by four global functions. These data points may be any of C's intrinsic data types. For example:

/* A signed and unsigned data point. */ short gruntBonus; unsigned long managerBonus; /* A pointer to an intrinsic C data type (our friend the C-style string). */ char* pLastName; /* Arrays of intrinsic types. */ int myLightBulbs[20]; char firstName[MAX_LENGTH];

While single data points are central to any programming language, it is always helpful to have the ability to assemble a collection of related data as a named entity, or user-defined types (UDTs). In C, this is achieved with the use of the struct keyword.

Programming with User-Defined Types (UDTs)

To illustrate the virtues of named custom types, imagine you are asked to develop a very simple C program modeling the creation, operation, and ultimate demise of a simulated automobile. After gathering the initial client requirements, you discover the program must support the following functionality:

  1. Allow the end user to create a car, specifying a pet name and maximum speed.

  2. Ensure the maximum speed is never greater than 500 MPH.

  3. Display the max speed and pet name when necessary.

  4. Accelerate the car in 10-mile increments.

  5. Determine if the engine block cracks due to excessive speed (defined as the first 10-mile increment over the car's maximum speed).

Note 

Throughout the course of this book, we will port the above requirements from C into C++, into an object supporting multiple interfaces, and finally into a "real DCOM" application. Furthermore, the automobile in question will be extended into the ATL framework to support COM error handling, aggregation, tear-off interfaces, connection points, and finally reincarnated into a full web-savvy ActiveX control. Just thought you'd like to know where we are heading.

Your first task as a C developer may be to determine the data types necessary for the project at hand, using a UDT to represent the concept of a car in the system. UDTs are very useful when creating program data types composed of a number of variables that seem to naturally work together as a whole. As you recall, the struct keyword allows us to refer to a collection of logically related data as a named type. Each member of the structure is called a field. Here is our simple CAR structure, which contains three fields:

/* The CAR structure. */ #define MAX_LENGTH     100 struct CAR {      char          petName[MAX_LENGTH];                  /* Pet name */      int           maxSpeed;                             /* Maximum speed */      int           currSpeed;                            /* Current speed */ };

Using this new UDT is straightforward. You may declare a CAR type just as you would declare any intrinsic C data type (recall that formal C syntax requires you to use the struct keyword when declaring UDT variables). To access the fields of this structure, you may use the dot operator (.) or arrow operator (->). Here is our CAR struct in action:

/* Creating a CAR variable. */ #include <string.h>      void main(void) {       struct CAR myCar;       myCar.maxSpeed = 300;       myCar.currSpeed = 60;       strcpy(myCar.petName,"Fred"); } 

Populating the Global Namespace

The next step in our simple C application is to identify the global functions that will manipulate CAR data types. Having thought about it for a moment, assume you write the following three global function prototypes. Notice how each function takes an existing CAR type variable (two by reference and one by value):

/* Global function prototypes. */ void DisplayCarStats(struct CAR c);          /* Pump CAR info to a Win32 console */ void CreateACar(struct CAR* c);              /* Prompt for user input */ void SpeedUp(struct CAR* c);                 /* Add 10 to CAR currSpeed field */

When using UDTs, we can avoid more cumbersome (but logically equivalent) functions such as:

/* UDTs help avoid overbearing function signatures. */ void DisplayCarStats (char petName, int maxSpeed, int currSpeed); void CreateACar(char* petName, int* maxSpeed, int* currSpeed); void SpeedUp (char* petName, int* maxSpeed, int* currSpeed);

Hopefully you agree the proper use of UDTs lends itself to more robust and readable code. As we will see, object-oriented languages (such as C++) extend the notion of a simple UDT by providing a tight coupling between data and the functions that act on that data. So then, with the CAR structure and a set of global functions, you are free to flesh out the programmatic details of the application. To get the ball rolling, let's dust off our structured programming skills and put together a minimal and complete C program that meets the initial design requirements.

Lab 1-1: Structured Programming in C

It is likely that the information presented in this first lab will be nothing new to you— understood. However, we will be porting the ideas presented in this lab from SP to OOP, OOP to an interface-based solution, and finally to a DCOM server usable from C++, Java, and Visual Basic clients. Furthermore, the car in question will be reworked into various ATL-based solutions (including an animated ActiveX control) full of numerous bells and whistles. Therefore, at the very least completing this lab will pave the way to "real COM" development with ATL and refresh some C details which may have fallen to the wayside.

 On the CD   The solution for this lab can be found on your CD-ROM under:
Labs\Chapter 01\CarInC

Step One: Prepare the Project Workspace

In this lab, we will create a simple automobile simulation written in the C programming language, appropriately called CarInC. Fire up Visual C++, select File | New... and create a Win32 Console Application project workspace (Figure 1-2). Select OK and from the resulting dialog select empty project.

click to expand
Figure 1-2: Creating a new Win32 Console Application.

Next, insert a new empty text file (using the File | New... menu selection) named main.c, and define an empty main loop at the top of the file:

/* main.c */ void main(void) { }

Step Two: Prototype the Global Functions and CAR Structure

We will be using the same CAR structure and function prototypes as defined earlier in this chapter. First, be sure to include the standard IO <stdio.h> and string manipulation <string.h> header files to your program, then add the following:

/* main.c */ /* Defines and includes. */ #define MAX_LENGTH         100 #define MAX_SPEED          500 #include <stdio.h>                     /* C input and output functions */ #include <string.h>                    /* String manipulation functions */ /* The CAR structure. */ struct CAR {      char           petName[MAX_LENGTH];      int            maxSpeed;      int            currSpeed; }; /* Global function prototypes. */ void DisplayCarStats(struct CAR c);  void CreateACar(struct CAR* c); void SpeedUp(struct CAR* c); /* Program entry point. */ void main(void) { } 

Step Three: Implement the Global Car Functions

We now have three global functions to implement in our application. To keep things simple, implement each function within the same main.c file (somewhere below the main loop). The DisplayCarStats() function examines an incoming CAR variable and pumps the field data to a Win32 console using printf(). Notice we send in a copy of an existing CAR variable to ensure that this function cannot change the caller's original CAR data type:

/* Dump current stats about a given CAR. */ void DisplayCarStats(struct CAR c) {      printf("***********************************\n");      printf("Your car is called: %s\n", c.petName);      printf("It does up to %d\n", c.maxSpeed );      printf("***********************************\n\n"); }

The CreateACar() function takes a pointer to an existing CAR variable and populates the fields based off user input. We could use scanf() in this function's implementation, however scanf() will misbehave if the buffer contains additional empty characters before the terminating NULL. Thus, if the pet name for the car is "My rusty clunker," we are out of luck. To rectify this, we will make use of the gets() function, which will read a string up until the first "\n" and append a NULL terminator. Here is the implementation of CreateACar()— without excessive error checking:

/* Fill the fields of a new CAR variable. */ void CreateACar(struct CAR* c) {       char  buffer[MAX_LENGTH];       int   spd = 0;       memset(c, 0, sizeof(struct CAR));       /* Read in a string and set petName field. */       printf("Please enter a pet-name for your car:");       gets(buffer);        /* Could check strlen() against MAX_LENGTH... */       strcpy(c->petName, buffer);       /* Be sure speed isn't beyond MAX_SPEED. */       do       {              printf("Enter the max speed of this car:");              scanf("%d", &spd);       }while(spd > MAX_SPEED);       /* Set remaining fields. */       c->maxSpeed = spd;       c->currSpeed=0; }

The implementation of SpeedUp() is very simple. In keeping with our design notes, increase the current speed by 10 MPH and print to the console:

/* Increment the currSpeed field of a CAR by 10 and display new speed. */ void SpeedUp(struct CAR* c) {      if(c->currSpeed <= c->maxSpeed)      {            c->currSpeed = c->currSpeed + 10;            printf("\tSpeed is: %d\n", c->currSpeed);      } } 

Step Four: Implement the main() Loop

Here, we will create a CAR data variable, gather user input using CreateACar(), and display the new car information within a Win32 console. We will then accelerate the automobile until its engine block snaps into pieces, defined as 10 miles over the maximum:

/* Program entry point. */ void main(void) {      /* C syntax requires the struct keyword at declaration */      struct CAR myCar;      printf("***********************************\n");      printf("The Amazing Car Application via C\n");      printf("***********************************\n\n");      CreateACar(&myCar);            /* Go create a car. */      DisplayCarStats(myCar);        /* Display new car information */      /* Speed up until engine block cracks */      while(myCar.currSpeed <= myCar.maxSpeed)            SpeedUp(&myCar);      /* Final message... */      printf("\n%s has blown up! Lead foot!\n", myCar.petName); }    /* End of main */

There you have it—a simple structured programming example written in C. We have assembled a UDT with three fields and implemented a set of global functions to operate on variables of this type, all in keeping with our program specifications. Figure 1-3 shows some possible output of your structured programming application:

click to expand
Figure 1-3: The CarInC application.

Problems with Structured Programming

The C program runs and meets all design specifications, so what is the problem with the application as it stands? To be honest, numerous full-blown applications have been written using the C language, so a partial answer is "nothing." However, imagine that this trivial application has been expanded beyond belief, and is now being used by a major vehicle manufacturer. To keep up with the new expanded design specifications, we may swell our application to define numerous UDTs and hundreds of global functions.

The first problem that creeps up using traditional structured programming is that the data and functions that are logically grouped together are not physically grouped together. Consider again the CAR structure. The first iteration of our program had three functions that seem to be related to the CAR data type based off the loose semantics of the function signatures. For example, we can gather that the DisplayCarStats() function operates on a CAR structure given its physical name and signature (a CAR variable type). However, what about a new function prototyped as the following:

/* The mystery parameter 'sp'. */ void ChangeSpeed(int sp);

This could be a function that operates on the maxSpeed field of a CAR type, or perhaps some other notion of "speed" in the revised program (speed of production, speed in a crash test, and so forth). Ambiguities of this nature show up often in traditional SP, as functions and the data they operate upon are only loosely bound together.

Another shortcoming of SP is that when you change your application's data, functions using that data must often change as well. Again, assume the CAR structure has undergone a redesign that changes the currSpeed field from an int to a float to allow for a finer grain of precision in the program:

/* Changing a field from an int to a float... */ struct CAR {      char           petName[MAX_LENGTH];      float          maxSpeed;             /* was an int */      int            currSpeed; };

In our current application, we have three functions that use the CAR structure directly. Upon recompiling, we are issued an explicit conversion warning (C4244 to be exact). Moreover, other run-time bugs show up as well, as seen in Figure 1-4:

click to expand
Figure 1-4: Altering data can result in logical errors.

As you can see, we have just broken the DisplayCarStats() method, as the printf() statement is looking for an incoming integer, but we just passed in a float:

/* Oops! maxSpeed is now a float. */ printf("It does up to %d\n", c.maxSpeed );

Obviously, we can easily modify this particular printf() statement to handle the change, but remember that the new iteration of our program may have hundreds of functions using this CAR structure. That sounds like hundreds of edits, endlessly searching source code for every function operating on the maxSpeed field of the CAR structure. That's no fun at all, even with a source code search utility!

A final headache with traditional SP is the almighty notion of code reuse. Recall that in SP, functions and the data they operate on are loosely coupled. If we wish to reuse a handful of our automobile functions in a new system, it is up to us to understand how discrete pieces interact. For example, assume you have a given function named Show() which operates internally on three pieces of global data:

/* This global function operates on three pieces of global data. */ void Show() {      gDatumA += 90;      gDatumB = gDatumA + gSomeOtherGlobalDataPoint;      printf("Data point B is currently %d\n", gDatumB); }

Now, just for the sake of argument, if we wish to use the Show() function in a new program, we must also reuse the three pieces of global data that are referenced within the function's scope. If we don't grab the correct data definitions (and are unsure where to find them), we can't recompile—and thus can't reuse the code.

To sum up our quick remembrance of a world without objects, remember that structured programming works with (typically global) data and global functions. Because the two are only logically grouped together, not physically, we must have a keen understanding of our entire system to understand the impact of changing data types, editing functions, and reusing code.

Hopefully most of you out there do not have to work with traditional SP too much in your day-to-day programming tasks. However, it is important to remember what the C language has given us, as the next pit stop on the road to COM is that of classical object orientation using the C++ programming language.



 < Free Open Study > 



Developer's Workshop to COM and ATL 3.0
Developers Workshop to COM and ATL 3.0
ISBN: 1556227043
EAN: 2147483647
Year: 2000
Pages: 171

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net