Chapter 28. From C to C++

No distinction so little excites envy as that which is derived from ancestors by a long descent.

François de Salignac de la Mothe Fénelon

C++ was built on the older language C, and there’s a lot of C code still around. That’s both a blessing and a curse. It’s a curse because you’ll probably have to deal with a lot of ancient code. On the other hand, there will always be work for you. This chapter describes some of the differences between C and C++, as well as how to migrate from one to the other.

K&R-Style Functions

Classic C (also called K&R C after its authors, Brian Kernighan and Dennis Ritchie) uses a function header that’s different from the one used in C++. In C++ the parameter types and names are included inside the ( ) defining the function. In Classic C, only the names appear. Type information comes later. The following code shows the same function twice, first as defined in C++, followed by its K&R definition:

int do_it(char *name, int function)   // C++ function definition
{
    // Body of the function

int do_it(name, function)             // Classic C definition
char *name;
int function;
{
    // Body of the function

When C++ came along, the ANSI C committee decided it would be a good idea if C used the new function definitions. However, because there was a lot of code out there using the old method, C accepts both types of functions. C++ does not.

Prototypes

Classic C does not require prototypes. In many cases, prototypes are missing from C programs. A function that does not have a prototype has an implied prototype of:

int funct(  );    // Default prototype for Classic C functions

The ( ) in C does not denote an empty argument list. Instead it denotes a variable length argument list with no type checking of the parameters. Also, Classic C prototypes have no parameter lists. The only “prototype” you’ll see consists merely of “( )”, such as:

int do_it(  );    // Classic C function prototype

This tells C that do_it returns an int and takes any number of parameters. C does not type-check parameters, so the following are legal calls to do_it:

i = do_it(  );
i = do_it(1, 2, 3);
i = do_it("Test", 'a'),

C++ requires function prototypes, so you have to put them in. There are tools out there such as the GNU prototize utility that help you by reading your code and generating function prototypes. Otherwise, you will have to do it manually.

struct

In C++, when you declare a struct, you can use the structure as a type name. For example:

struct sample {
    int i, j;    // Data for the sample
};
sample sample_var;  // Last sample seen

C is more strict. You must put the keyword struct before each variable declaration:

struct sample sample_var;  // Legal in C
sample sample_var;          // Illegal in C

malloc and free

In C++, you use the new operator to get memory from the heap and use delete to return the memory. C has no built-in memory-handling operations. Instead, it makes use of two library routines: malloc and free.

The C malloc function

The function malloc takes a single parameter—the number of bytes to allocate—and returns a pointer to them (as a char * or void *). But how do we know how big a structure is? That’s where the sizeof operator comes in. It returns the number of bytes in a structure. To allocate a new variable of type struct foo in C, we use the code:

foo_ptr = (struct foo *)malloc(sizeof(struct foo));

Note that we must use a cast to turn the pointer returned by malloc into something useful. The C++ syntax for the same operation is much cleaner:

foo_ptr = new foo;

Suppose we want to allocate an array of three structures. We need to multiply our allocation size by three, resulting in the following C code:

foo_ptr = (struct foo *)malloc(sizeof(struct foo) * 3);

The much simpler C++ equivalent is:

foo_ptr = new foo[3];

Programs can freely mix C-style malloc and C++ new calls. The C memory allocators are messy, however, and should be converted to their C++ version whenever possible.

There are a number of traps concerning C-style memory allocation. Suppose we take our structure foo and turn it into a class. We can, but shouldn’t, use the C memory routines to allocate space for the class:

class foo {...};
foo_var = (struct foo *)malloc(sizeof(struct foo)); // Don't code like this

Because C++ treats struct as a special form of class, most compilers won’t complain about this code. The problem is that our malloc statement allocates space for foo and that’s all. No constructor is called, so it’s quite possible that the class will not get set up correctly. The C++ new operator not only allocates the memory, but also calls the constructor so that the class is properly initialized.

The C free function

C uses the function free to return memory to the heap. The function free takes a single character pointer as a parameter (thus making a lot of casting necessary):

free((char *)foo_var);
foo_var = NULL;

In C++ you delete a foo_var that points to a simple value this way:

delete foo_var;
foo_var = NULL;

If foo_array is an pointer to an array, you delete it with the code:

delete []foo_array;
foo_array = NULL;

Again, you must be careful when turning foo into a class. The free function just returns the memory to the heap. It does not call the destructor for foo, while the delete operator calls the destructor and then deletes the class’s memory.

C-style memory allocation is messy and risky. When converting code to C++ you probably should get rid of all malloc, calloc, and free calls whenever possible.

Warning

According to the ANSI C standard, memory allocated by malloc must be deallocated by free. Similarly, memory allocated by new must be deallocated by delete. However, most of the compilers I’ve seen implement new as a call to malloc and delete as a call to free. In other words, mixing new/free or malloc/free calls will usually work. To avoid errors, you should follow the rules and avoid mixing C and C++ operations.

Turning Structures into Classes

Frequently when examining C code you may find a number of defined struct statements that look like they should be objects defined as C++ classes. Actually, a structure is really just a data-only class with all the members public.

C programmers frequently take advantage of the fact that a structure contains only data. One example of this is reading and writing a structure to a binary file. For example:

a_struct struct_var;	   // A structure variable

// Perform a raw read to read in the structure
read_size = read(fd, (char *)&struct_var, sizeof(struct_var));

// Perform a raw write to send the data to a file
write_size = write(fd, (char *)&struct_var, sizeof(struct_var));

Turning a structure like this into a class can cause problems. C++ keeps extra information, such as virtual function pointers, in a class. When you write the class to disk using a raw write, you are outputting all that information. What’s worse, when you read the class in, you overwrite this bookkeeping data.

For example, suppose we have the class:

class sample {
    public:
        const int sample_size;     // Number of samples
        int cur_sample;            // Current sample number
        sample(  ) : sample_size(100) {} // Set up class
        virtual void get_sample(  ); // Routine to get a sample
};

Internally, this class consists of three member variables: a constant, sample_size (which C++ won’t allow you to change); a simple variable, cur_sample; and a pointer to the real function to be used when get_sample is called. All three of these are written to disk by the call:

sample a_sample;
// ...
write_size = write(fd, (char *)&a_sample, sizeof(a_sample));

When this class is read, all three members are changed. That includes the constant (which we aren’t supposed to change) and the function pointer (which now probably points to something strange).

C programmers also make use of the memset function to set all the members of a structure to zero. For example:

struct a_struct { ... }
a_struct struct_var;
// ...
memset(&struct_var, '', sizeof(struct_var));

Be careful when turning a structure into a class. If we had used the class a_sample in the previous example instead of the structure struct_var, we would have zeroed the constant sample_size as well as the virtual function pointer. The result would probably be a crash if we ever tried to call get_sample.

setjmp and longjmp

C has its own way of handling exceptions through the use of setjmp and longjmp. The setjmp function marks a place in a program. The longjmp function jumps to the place marked by setjmp.

Normally setjmp returns a zero. This tells the program to execute normal code. When an exception occurs, the longjmp call returns to the location of the setjmp function. The only difference the program can see between a real setjmp call and a fake setjmp call caused by a longjmp is that normally setjmp returns a zero. When setjmp is “called” by longjmp, the return value is controlled by a parameter to longjmp.

The definition of the setjmp function is:

#include <setjmp.h>

int setjmp(jmp_buf env);

where env is the place where setjmp saves the current environment for later use by longjmp.

The setjmp function return values are as follows:

0

Normal call

Nonzero

Non-zero return codes are the result of a longjmp call.

The definition of the longjmp call is:

void longjmp(jmp_buf env, int return_code);

where env is the environment initialized by a previous setjmp call, and return_code is the return code that will be returned by the setjmp call.

Figure 28-1 illustrates the control flow when using setjmp and longjmp.

setjmp/longjmp control flow
Figure 28-1. setjmp/longjmp control flow

There is one problem here, however. The longjmp call returns control to the corresponding setjmp. It does not call the destructors of any classes that are “destroyed” in the process.

In Figure 28-1 we can see that in the subroutine we define a class named a_list. Normally we would call the destructor for a_list at the end of the function or at a return statement. However, in this case we use longjmp to exit the function. Since longjmp is a C function, it knows nothing about classes and destructors and does not call the destructor for a_list. So we now have a situation where a variable has disappeared but the destructor has not been called. The technical name for this situation is a “foul-up.”

When converting C to C++, change all setjmp/longjmp combinations into exceptions.

Mixing C and C++ Code

It is possible for C++ code to call a C function. The trick is that you need to tell C++ that the function you are calling is written in C and not C++. This is accomplished by declaring the function prototypes inside an extern "C" block. For example:

extern "C" {
    extern int the_c_function(int arg);
}

Summary

What you must do to get C to compile with a C++ compiler:

  • Change K&R-style function headers into standard C++ headers.

  • Add prototypes.

  • Rename any functions or variables that are C++ keywords.

  • Change setjmp/longjmp calls into catch / throw operations.

Once you’ve done these tasks, you have a C+1/2 program. It works, but it’s really a C program in C++’s clothing. To convert it to a real C++ program, you also need to do the following:

  • Change malloc to new.

  • Change free to delete or delete [] calls.

  • Turn printf and scanf calls into cout and cin.

  • When turning struct declarations into class variables, be careful of read, write, and memset functions that use the entire structure or class.

Programming Exercise

Exercise 28-1: There are a lot of C programs out there. Turn one into C++.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.188.190.175