C Multidimensional Arrays at Run Time

Using C functions to create two- and three-dimensional arrays at run time helps you organize the heap and write more powerful programs.


August 01, 1989
URL:http://www.drdobbs.com/embedded-systems/c-multidimensional-arrays-at-run-time/184408185

Figure 1


Copyright © 1989, Dr. Dobb's Journal

Figure 2


Copyright © 1989, Dr. Dobb's Journal

Figure 5


Copyright © 1989, Dr. Dobb's Journal

Figure 4


Copyright © 1989, Dr. Dobb's Journal

Figure 6


Copyright © 1989, Dr. Dobb's Journal

AUG89: C MULTIDIMENSIONAL ARRAYS AT RUN TIME

C MULTIDIMENSIONAL ARRAYS AT RUN TIME

Organizing the heap for run-time multidimensional arrays isn't easy, but it is possible

Paul Anderson

Paul is a consultant and co-author of Advanced C Tips and Techniques, published by Howard W. Sams, from which this article was adapted. Paul can be reached at 1212 Eolus Ave., Leucadia, CA 92024.


When you declare arrays in C, their sizes remain fixed. You have to decide before your program runs how large an array will be, regardless of whether you declare it as automatic or static. What about declaring arrays at run time? In this case, you need to call the C library routine malloc( ) or calloc( ) to build dynamic arrays from memory (called the heap). This approach eliminates anticipating array bounds at compile time and makes programs allocate only as much memory as they need.

One-dimensional arrays are easy because a call to malloc( ) or calloc( ) returns a pointer to a chunk of heap memory that you may use as a one-dimensional array. You cast the heap pointer to an appropriate data type and use either array notation or pointers to reference the allocated elements.

Multidimensional arrays, however, are more difficult. There's no standard C library routine that sets up the heap so that you may use it as a multidimensional array. To the heap manager, heap storage is merely a block of consecutive bytes with no notion of rows, columns, and so on. You need a way to organize the heap for run-time multidimensional arrays. Moreover, it would be nice to retain the concepts of rows and columns so that heap memory appears like a multidimensional array to your programs.

In this article, I'll discuss C functions that create run-time two-dimensional and three-dimensional arrays of any data type (including arrays of structures and unions). I'll also discuss a technique to mimic the subroutine calling conventions of Basic and Fortran with two-dimensional arrays. Along the way, I'll review two-dimensional and three-dimensional arrays and the relationships between pointer expressions and array references. This should give you the information you need to implement these concepts in your own C programs.

The Basic Rule

Let's start with how C views arrays. For a one-dimensional array of any data type, the following equivalence exists between an array reference and a pointer expression:

  a[i] = (*(a + i))

I call this the basic rule because it applies to many complicated pointer expressions, as you will see later on. Note that you may omit the outer parentheses most of the time except for expressions where C's precedence rules require them. The basic rule helps explain simple relations such as &a[i] = a +i, where I apply C's address operator (&) to both sides of the relation and use C's precedence rules to simplify the result. Using the same method, the basic rule helps derive the relations a[0] = *a and &a[0] = a.

One of the surprising things about C arrays is that array references don't really exist. If you don't believe this, try running the portable program in Example 1 , which compiles without error. You'll discover the program displays 5 (the sixth element of the array) four times. Despite the fact that the last two array references must be the ultimate in job security, C translates the array references to pointer expressions according to the basic rule. Although no one (hopefully) would use expressions such as these in programs, they're conclusive proof that C handles arrays differently from the way other languages do.

Example 1: A portable program that compiles without error

  #include <stdio.h>
  main ()
  {
     static char a[] = "0123456789";
     int i = 5;

     printf ("%c %c %c %c\n", a[i], a[5], i[a], 5[a]);
  }

Now let's move on to multidimensional arrays. Two-dimensional arrays are easy because you can visualize them as grids with rows and columns. (By the way, I'll use the term grid for a two-dimensional array from now on. Think of it as a checkerboard in which rows and columns locate unique elements.) In C, the declaration double mint[3][5]allocates storage for 15 doubles, arranged as 3 rows by 5 columns. Figure 1 shows how to visualize the array mint. Array references have the format mint[row][col]. Once again, C allows you to use expressions with pointer indirection and array reference notation that are not immediately obvious. mint[1], for instance, is a pointer to a double located in the first column of the second row. Likewise, *mint is a pointer to a double in the first row and first column. The name of the array (mint) is a pointer to an array of five doubles, located in the first row. The expression mint + 1, therefore, is a pointer to the array of five doubles in the second row.

The basic rule comes in handy for deciphering two-dimensional array references. The notation mint[1][2], for example, is equivalent to *(mint[1] + 2), according to the following derivation.

Let:

  p = mint[1]

Then:

  mint[1][2] = p[2]

Marking mint[1]as p makes it easier to apply the basic rule:

  p[2] = (*(p + 2))

Now substitute mint[1] for p in both sides of the previous expression:

  mint[1][2] = (*(mint[1] + 2))
  mint[1][2] = *(mint[1] + 2)

(Here I drop the outer parentheses because they're unnecessary.)

mint[1][2] is also equivalent to (*(mint + 1))[2], although this is considerably more obscure. Here are the substitutions with the basic rule:

  mint[1] = (*(mint + 1))
  mint[1][2] = (*(mint + 1))[2]

This time, however, you cannot remove the outer parentheses because C's precedence rules require them ([] has higher precedence than *). Without the parentheses, the expression *(mint + 1)[2] evaluates to **(mint + 3). (Can you derive this with the basic rule?) Both expressions are wrong because they locate an element outside the bounds of the mint array.

Three-dimensional arrays add another level to the same concept. The C declaration double vision[2][3][5], for example, allocates storage for 30 doubles, arranged as two grids of 3 rows and 5 columns. Think of it as a stack of two-dimensional arrays on top of each other. Figure 2 shows how to visualize the array vision. Array references have the format vision[grid][row][col]. C permits pointer expressions for arrays in three dimensions as well as it does for two dimensions, but their meanings are different. vision[1] is now a pointer to an array of five doubles located at the first row of the second grid. Likewise, vision[1][1] is a pointer to a double in the second grid, second row, first column. The name of the array (vision) is a pointer to a 3 by 5 array of doubles (the first grid), and vision + 1 is a pointer to the second grid. I'll return to these types of pointer expressions when I apply them later on to run-time arrays.

Storage Map Equations

Viewing multidimensional arrays as grids with rows and columns is for our benefit. Unfortunately, the compiler has a harder job than we do making the connection between arrays and grids. Physical memory is accessed as a one-dimensional array, so the compiler maps multidimensional arrays to blocks of memory. Each time you reference a multidimensional array element, the compiler calculates an address in the memory block.

Figure 3 shows the memory layout for the declaration double mint[3][5]. C stores two-dimensional arrays in memory by rows (this is called row major form). The second subscript varies faster than the first one. Therefore, mint[0] points to the first double in the first row, mint[1] points to the first double in the second row, and so forth. The compiler calculates the address for an array reference by locating the appropriate row and accessing the correct column within that row.

When you use the array reference mint[i][j] in a C program, the compiler implements the following pointer expression: *(&mint[0][0] + 5 * i + j). Subscript i is the row number, subscript j is the column number, and &mint[0][0] is the base address of the array. I call this above expression a storage map equation. Every multidimensional array declaration in a program has one. This particular storage map equation is for the two-dimensional array double mint[3][5].

Note that the number of rows in a two-dimensional array declaration is not used in the storage map equation. This helps explain why the declarations in Example 2 are legal in C.

Example 2: The number of rows in a two-dimensional array declaration is not used in the storage map equation

  main ()
  {
     extern double mint [][5]; /* OK */
     . . . mint [i][j] . . .
     f(mint);
  }
  f(a)
  double a [][5];              /* OK */
  {
    . . . a[i][j] . . .
  }

To generate the storage map equation for a[i][j] inside f( ), the compiler requires the number of columns but not the number of rows. Likewise, the extern statement provides the compiler with the necessary information to reference mint[i][j], even though the array is declared in another file.

Storage map equations apply to three-dimensional arrays as well. Figure 4 shows the memory layout for the declaration double vision[2][3][5]. The second 3 by 5 grid follows the first. Memory layout within each grid has the same organization as in the two-dimensional example.

When you use the array reference vision[i][j][k], the C compiler uses the following storage map equation: *(&vision[0][0][0] + 15 * i + 5 * j + k). Subscripts i, j, and k are the grids, rows, and columns, respectively, and &vision[0][0][0] is the base address of the array. The number 15 is the product of the number of rows (3) and the number of columns (5). The storage map equation for a three-dimensional array reference does not use the number of grids.

Why be concerned with storage map equations, anyway? It turns out that many C compilers generate multiply instructions in assembly code for multidimensional array references. Depending on the compiler you use, this may affect performance of a running C program. Suppose, for example, you declare mint as follows: double mint[3][4]; The storage map equation for mint[i][j] is now *(&mint[0][0] + 4 * i + j). Some compilers generate shift instructions in place of multiplies. Multiplying an integer by a power of 2, for instance, is the same as shifting the bits left by the value of the power. In this example, a compiler could shift i left by 2 bits instead of multiplying it by 4. If the number of columns is not a power of 2 (like our original declaration of mint[3][5]), some compilers may even generate a set of "shift-and-add" instructions. In other words, shifting i left by 2 bits and adding i is the same as multiplying i by 5. In the following sections, I'll create two- and three-dimensional arrays that don't use storage map equations to access array elements. In many cases, this improves a program's performance.

Two-Dimensional Arrays at Run Time

Now let's put all this information to work and create a function that allocates two-dimensional arrays at run time. Suppose you need a 2 by 3 array of integers in a program. Instead of declaring int a[2][3], let's allocate memory from the heap with the statements in Example 3.

Example 3: Allocating memory from the heap

  int *p;
  int **a;

  p = (int *) calloc(2 * 3, sizeof(int));     /* pointer to data */
  a = (int **) malloc(2 * sizeof(int *));     /* pointer to rows */

  a[0] = p;                                      /* first row */
  a[1] = p + 3;                                  /* second row */

Figure 5 shows the memory arrangement. There are two pointers to heap storage. The first pointer (p) points to the array of data (six integers). The second pointer (a) points to a pointer array containing the addresses of the rows of the data array. Note that p is a pointer to an int and a is a pointer to a pointer to an int. I use calloc( ) to zero fill the data array and malloc( ) to allocate heap storage for the row array.

With the pointer array a, you may now use two-dimensional array references in a program. a[1][2], for instance, refers to the last data item, or the third column of the second row of the two-dimensional array. From the basic rule, it's as if you typed *(a[1] + 2). However, a[1] is now a pointer to the first integer in the second row, and a[1] + 2 is a pointer to the third column in the same row. The compiler uses pointer indirection in place of a storage map equation to access array elements.

Listing One contains the C source code for two functions based on this technique. Function dim2( ) creates two-dimensional arrays of any data type at run time and function free2( ) frees them. Example 4 shows how to create a 3 by 5 array of integers and a 4 by 6 array of structures. The statements free2(a) and free2(s) release the heap memory allocated for each array.

Example 4: Creating two-dimensional arrays of integers and structures

  int **a;                              /* 2D array of integers */
  struct something **s;       /* 2D array of structures */

  a = (int **) dim2 (3, 5, sizeof(int));
  s = (struct something **) dim2 (4, 6, sizeof(struct something));

Function dim2( ) allocates heap memory for the row pointer array (pointed to by prow) and the data array (pointed to by pdata). The forloop connects the row pointer array to the data array. Function free2( ) makes two calls to free( ) to release heap memory. The first call frees the data array and the second call releases the row pointer array. Note that the order for the calls to free( ) is significant!

Three-Dimensional Arrays at Run Time

Three-dimensional arrays at run time extend the same concepts. Implementing the compile time declaration of int a[2][3][2], for instance, requires two pointer arrays in addition to the data array. One pointer array contains pointers to the rows of heap data and the other one contains pointers to the grids. Figure 6 shows the memory arrangement. Pointer a points to the grid pointer array, p2 points to the row pointer array, and p points to the data. Once you initialize the grid and row pointer arrays, you access the data with a. Here are the declarations for the three pointers: int ***a, **p2, and *p. Note that three-dimensional array references with a use pointers with triple indirection. The array reference a[0][2][1], for example, becomes *(*(*a + 2) + 1), according to the basic rule. As with the two-dimensional technique, three-dimensional array references use pointer indirection in place of a storage map equation with multiplies or shifts and adds.

Listing Two contains the C source code for two functions based on the three-dimensional technique. Function dim3( ) creates the three-dimensional arrays of any data type at run time and free3( ) frees them. Example 5 shows how to create a 3 by 4 by 5 array of integers and a 4 by 6 by 8 array of structures. Function calls free3(a) and free3(s) release the heap memory allocated for each array.

Example 5: Creating three-dimensional arrays of integers and of structures

  int ***a;                                 /* 3D array of integers */
  struct something ***s;                    /* 3D array of structures */

  a = (int ***) dim3 (3, 4, 5, sizeof(int));
  s = (struct something ***) dim3 (4, 6, 8, sizeof(struct something));

Function dim3( ) allocates heap memory for the grid pointer array (pointed to by pgrid), the row pointer array (pointed to by prow), and the data array (pointed to by pdata). Two for loops connect the grid pointer array to the row pointer array and to the data array. Function free3( ) makes three calls to free( ) to release heap memory for the data array, the row pointer array, and the grid pointer array, respectively.

Function Calling Conventions

Languages such as Fortran and Basic allow you to pass different-size multidimensional arrays as arguments to the same function and use array notation to reference elements. In Fortran, for example, the statements:

  SUBROUTINE FUNC(A, M, N)
  DIMENSION A(M, N)
  . . .
  END

allow a subroutine called FUNC( ) to access a two-dimensional array called A with run-time values for M rows and N columns. Programs call FUNC( ) with different-size arrays. Fortran libraries use subroutines such as FUNC( ) to invert matrices or calculate mathematical items, such as determinants and eigenvalues. This convention is useful because subroutines may reference elements by rows and columns.

C doesn't provide such a built-in feature. Consider what happens, for example, when you pass the address of a multidimensional array to a function. Inside the function, the compiler uses a storage map equation with fixed sizes. To illustrate, suppose you declare the following two-dimensional array of integers: int data[5][9].

The statements in Example 6 call a C function, func( ), to pass the address of the two-dimensional array along with the number of rows and columns. The compiler requires 9 in the declaration for a to properly address elements of the array with the storage map equation.

Example 6: Passing the address of a two-dimensional array

       func(data, 5, 9);

  Inside func(), you have the following:

  func(a, rows, cols)
  int a[][9];
  int rows, cols;
  {
    register int i, j;

    for (i = 0; i < rows; i++)
       for (j = 0; j < cols; j++)
           . . . a[i][j] . . .    /* array references OK */

Suppose you have another two-dimensional array called int moredata [4][10]; You can't use func( ) to access data in this array. The function call func(more data, 4, 10) won't work because func( ) is compiled with the number of columns set to 9 and not 10. Attempts to use it make func( ) reference memory incorrectly.

Another alternative is to pass the address of the first element of the two-dimensional array. The statements:

  func(&data[0][0], 5, 9);
  func(&moredata[0][0], 4, 10);

make func( ) access the two-dimensional arrays as a one-dimensional array. The code inside func( ) changes, however. Now you have the code shown in Example 7.

Example 7: The effect of passing the first address of an element of a two-dimensional array

  func(a, rows, cols)
  int *a;
  int rows, cols;
  {
    register int *p = a, *end = a + row * cols;
    while (p < end) {
       . . . *p++ . . .      /* Can't use a[i][j] references */
    }
  }

Here, parameter a is a pointer to an integer (previously, it was a pointer to an array of integers). This allows you to use compact pointer expressions such as *p++ to access memory as a series of rows*cols integers. Although this is relatively fast, the concept of rows and columns disappears. Functions that compute matrix inversions and determinants need this information, though.

In situations such as this, we'd like to have C behave like Fortran or Basic. This would help us translate Fortran and Basic subroutines to C more easily. Let's apply the previous techniques to solve this problem.

Suppose you want to pass the address of different-sized, two-dimensional arrays to a function that calculates a mathematical quantity called a determinant. The details of how you calculate determinants do not concern us here, but this problem serves as a good example of an algorithm that requires rows and columns for the calculation. Listing Three is a C program that calculates determinants for two-dimensional arrays of doubles.

Function det( ) calls sdim2( ) to construct pointer arrays to the data using the two-dimensional technique presented earlier. Function sdim2( ) is similar to dim2( ), except that it doesn't have to create the data array. Instead, it allocates heap memory for the row pointer array, connects it to the data array (pointed to by pdata) and returns the heap memory address. This allows the det( ) function to access the array elements of the different size arrays passed to it using array notation with rows and columns. Before det( ) returns, it calls free( ) to release heap memory for the pointer array.

Performance Pointers

What about performance issues? The techniques I've shown you for run-time multidimensional arrays substitute pointer indirections for storage map equations. (Remember, storage map equations typically generate integer multiplies or shifts and adds in assembly code.) I've benchmarked these programs and others using Microsoft's C compiler (under DOS and Xenix for 286 and 386 machines) and discovered that pointer indirections are no worse in execution time than integer multiplies (and often execute faster). In the two-dimensional case, the overhead of allocating heap memory for the row pointer array isn't too bad, but remember that three-dimensional arrays require two pointer arrays. This approach uses more heap memory and can take more time to set up. It's also possible to eliminate function call overhead by using macros to generate the arrays (the solutions are in the bibliography). Ultimately, you'll have to judge whether the overhead of setting up run-time multidimensional arrays is worth the effort for your application.

Bibliography

Anderson, Paul and Anderson, Gail; Advanced C: Tips and Techniques. Indianapolis, Ind.: Howard W. Sams & Company, 1988.

Acknowledgments

I'd like to thank Gail Anderson, Marty Gray, and Tim Dowty for their assistance.

Availability

All source code for articles in this issue is available on a single disk. To order, send $14.95 (Calif. residents add sales tax) to Dr. Dobb's Journal, 501 Galveston Dr., Redwood City, CA 94063; or call 800-356-2002 (from inside Calif.) or 800-533-4372 (from outside Calif.). Please specify the issue and format (MS-DOS, Macintosh, Kaypro).

C MULTIDIMENSIONAL ARRAYS AT RUN TIME by Paul Anderson

[LISTING ONE]




#include <stdio.h>
#include <malloc.h>

char **dim2(row, col, size)          /* creates 2D array */
int row, col;
unsigned size;
{
   int i;
   char **prow, *pdata;

   pdata = (char *) calloc(row * col, size);
   if (pdata == (char *) NULL) {
      fprintf(stderr, "No heap space for data\n");
      exit(1);
   }
   prow  = (char **) malloc(row * sizeof (char *));
   if (prow == (char **) NULL) {
      fprintf(stderr, "No heap space for row pointers\n");
      exit(1);
   }

   for (i = 0; i < row; i++) {
     prow[i] = pdata;             /* store pointers to rows */
     pdata += size * col;         /* move to next row */
   }
   return prow;                   /* pointer to 2D array */
}

void free2(pa)                   /* frees 2D heap storage */
char **pa;
{
   free(*pa);                    /* free the data */
   free(pa);                     /* free pointer to row pointers */
}






[LISTING TWO]



#include <stdio.h>
#include <malloc.h>

char ***dim3(grid, row, col, size)            /* creates 3D array */
int grid, row, col;
unsigned size;
{
   int i;
   char ***pgrid, **prow, *pdata;

   pdata = (char *) calloc(grid * row * col, size);
   if (pdata == (char *) NULL) {
      fprintf(stderr, "No heap space for data\n");
      exit(1);
   }
   prow  = (char **) malloc(grid * row * sizeof (char *));
   if (prow == (char **) NULL) {
      fprintf(stderr, "No heap space for row pointers\n");
      exit(1);
   }
   pgrid  = (char ***) malloc(grid * sizeof (char **));
   if (pgrid == (char ***) NULL) {
      fprintf(stderr, "No heap space for grid pointers\n");
      exit(1);
   }

   for (i = 0; i < grid * row; i++) {
     prow[i] = pdata;                    /* store pointers to rows */
     pdata += col * size;                /* move to next row */
   }

   for (i = 0; i < grid; i++) {
     pgrid[i] = prow;                    /* store pointers to grid */
     prow += row;                        /* move to next grid */
   }
   return pgrid;                         /* pointer to 3D array */
}

void free3(pa)                          /* frees 3D heap storage */
char ***pa;
{
   free(**pa);                  /* free the data */
   free(*pa);                   /* free the row pointers */
   free(pa);                    /* free the grid pointers */
}






[LISTING THREE]



/* det.c - find determinant of a two-dimensional array of doubles */

#include <stdio.h>
#include <malloc.h>

main()
{
    double det();

    static double f[4][4] = {
        1, 3, 2, 1,
        4, 6, 1, 2,
        2, 1, 2, 3,
        1, 2, 4, 1
    };
    static double g[5][5] = {
        1, 3, 2, 1, 7,
        4, 6, 1, 2, 6,
        2, 1, 2, 3, 5,
        1, 2, 4, 1, 4,
        8, 5, 4, 1, 3
    };

    printf ("determinant of f = %g\n", det(f, 4));
    printf ("determinant of g = %g\n", det(g, 5));
}

double det(arg, n)      /* calculate determinant for n by n matrix */
char *arg;
int n;
{
    register int i, j, k;
    double **a;          /* this is the array name */
    char **sdim2();
    double ret;          /* determinant */
    double x;            /* temp */

    /* dynamically create 2 dimensional "array" a from arg */
    a = (double **) sdim2(arg, n, n, sizeof(double));

    /* determinant algorithm using rows and columns */
    for (k = 0; k < n - 1; k++)
        for (i = k + 1; i < n; i++){
            x = a[i][k]/a[k][k];
            for (j = k; j < n; j++)
                a[i][j] = a[i][j] - x * a[k][j];
            }

    for (ret = 1, i = 0; i < n; i++)
        ret *= a[i][i];

    free(a);               /* free heap storage */

    return ret;
}

char **sdim2(pdata, row, col, size)          /* "creates" 2D array */
char *pdata;
int row, col;
unsigned size;
{
   int i;
   register char **prow;

   prow  = (char **) malloc(row * sizeof (char *));
   if (prow == (char **) NULL) {
      fprintf(stderr, "No heap space for row pointers\n");
      exit(1);
   }

   for (i = 0; i < row; i++) {
     prow[i] = pdata;             /* store pointers to rows */
     pdata += size * col;         /* move to next row */
   }
   return prow;                   /* pointer to 2D array */
}












Copyright © 1989, Dr. Dobb's Journal

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.