Columns


Questions & Answers

Pointers and Multi-Dimensional Arrays

Kenneth Pugh


Kenneth Pugh, a principal in Pugh-Killeen Associates, teaches C and C++ language courses for corporations. He is the author of C Language for Programmers and All On C, and was a member of the ANSI C committee. He also does custom C programming for communications, graphics, image databases, and hypertext. His address is 4201 University Dr., Suite 102, Durham, NC 27707 You may fax questions for Ken to (919) 489-5239. Ken also receives email at kpugh@allen.com (Internet) and on Compuserve 70125,1142.

Q

Thank you for your help on my questions concerning allocating memory for multi-dimensional char arrays. Your explanation of the array-of-pointers and the pointer-to-arrays approaches to the second dimension not only cleared up my misunderstanding but also served as a valuable lesson in pointers. I would like to mention a couple of things and then ask for your thoughts on my overall approach.

I am using a 486, Borland C/C++ 3.01, and the large memory mode. I tested both methods using Borland's Turbo Profiler tprof, and although there was a very slight difference in the time for fgets, the time was exactly the same for cputs using:

for( i = 0; i < screen_rows; i++)
   cputs(array_p_char[i]);
                 time = 0.0114
and

for( i = 0; i < screen_rows; i++)
   cputs( p_char_array[i]);
                 time = 0.0114
I have an application that uses 15 to 18 screen displays for data input to a database (Borland Paradox Engine 2.0). The screens are in an ASCII file which I read into the p_screen_array using an fgets (scrnarray, ascii file, 82). The array is defined as

char far scrnarray[MAX_ROWS][ROW_LEN];
I use far to force a new data segment because the _DATA segment is nearing the 64KB limit. Tests did not indicate any benefit in using the far heap, as you suggested, over using a far data segment:

char_far*p_char;
p_char=new_far char[NUM_CHARS];
When a screen format needs to be displayed, I search the scrnarray to find the correct screen and then display it as follows:

while( i < MAX_ROWS)
   {
   if(!strncmpi
     (scrnarray[i++],
     scrn_ident,3))
     // find correct screen
      {
      for(j = i;
         j < SCRN_ROWS; j++)
         {
         cputs(scrnarray[i]);
         // display screen rows
         }
      break;
         // break out of while
      }
   }
Using strncmpi is not as efficient as using int equality, but the latter would require that I convert ASCII char to int before testing, which is not very appealing. Other than quick and snappy screen display, I am somewhat concerned about hardware independence.

I would be very interested in your and my fellow CUJ readers' thoughts about my overall approach to snappy screen displays. And again, thank you for your response to my first question.

Tom Parke
Sparks, Nevada

A

If you understood the double pointers in my last column, then you are ready for my answer to your follow-up question. What I suggest is using another array of pointers.

char *p_screen_chars
    [NUMBER_SCREENS][NUMBER_OF_ROWS];
p_screen_chars is a doubly-dimensioned array of pointers, each element of which is a pointer to char. To allocate blocks of memory NUMBER_OF_CHARS long and assign the addresses to the elements in p_screen_chars, you code:

for (screen = 0;
    screen < NUMBER_OF_SCREENS;
    screen++)
   {
   for (row = 0;
       row < number_rows; row++)
      {
      p_screen_chars[screen]
        [row]= malloc
        (NUMBER_OF_COLUMNS);
      }
   }
It appears that your screens are stored on disk in the form:

S01
First row
Second row
...
S02
First row
Second row
...
When reading from the file, use strncmpi to identify the screen number and write the rows to the appropriate elements of p_screen_chars. You can display any one of the screens with:

screen = desired_screen;
for (row = 0;
    row < NUMBER_OF_ROWS; row++)
   {
   cputs(p_screen_chars
        [screen][row]);
   }
To allocate memory for the screens from data segments rather than from the heap, you could declare:

char screen1[NUMBER_OF_ROWS]
           [NUMBER_OF_COLUMNS];
char screen2[NUMBER_OF_ROWS]
           [NUMBER_OF_COLUMNS];
   ...

char (*p_screen_chars
     [NUMBER_SCREENS])
   [NUMBER_OF_ROWS]
    [NUMBER_OF_COLUMNS]
    = { screen1, screen2, ....};
As I mentioned earlier, p_screen_chars is an array of pointers, with each element pointing to an object which is a doubly-dimensioned array of chars. The code for the display will look exactly as above whether you use the far heap or place the arrays in different data segments. Both require far four-byte pointers.

The main issue here is which part of memory should be used for allocating the data. For protected-mode applications, I recommend using static memory. For real-mode applications, where the issue is more critical, I suggest placing the screens in extended memory and using a package of XMS routines to bring them into real memory. If you do not have control over the environment (e.g., over how much extended memory a machine has), leave the screens in a file with pointers to the initial character of every screen. The file could be on RAM disk, if it is available. The access time would be minimal.

The difference in execution time between using pre-computed indices to the beginning of each row and computing them every time is small. For a small number of accesses on a fast machine, such as a 486, the difference may not even show up in your timing routines. You might want to examine the actual machine instructions — I don't have a spec sheet on the 486, but I would imagine that an integer multiply probably requires little more time than an addition.

C++ Operators

Q

The accompanying sample code (see Figure 1) has been haunting me for a while and I have not been able to get an accurate answer from Borland or Microsoft.

C++ does not allow you to inherit the overloaded operator= function, so derived classes need to repeat this function. However, you may inherit all other overloaded operator functions.

With the assignment

b3 = b2 - b1;
b2's CBase operator- is called. It accepts a const reference to a CBase object as an argument (b1), and returns a CBase object. b3 expects a CBase object. No problem so far.

However, with the assignment:

d3 = d2 - d1;
d2's inherited CBase operator- is called. It expects a const reference to a CBase object as an argument. However, I'm passing a CDerived object instead (d1). Also, d3 expects a CDerived object. However, d2's inherited CBase operator- function returns a CBase object instead.

Question 1: d2's operator-function expects a const reference to a CBase object as an argument. However, I'm passing d1, which is not a CBase object but a CDerived object, instead. What happens here? Do I have to rewrite the operator-function for the CDerived class?

Question 2: The inherited operator-function returns a CBase object. However, d3 expects a CDerived object. Again, what happens here? Do I have to rewrite the operator-function for the CDerived class?

Question 3: In general, when I derive a class D from a base class B, do I have to rewrite all functions that either take a B object as an argument or return a B object?

I enjoy reading your magazine articles, and I wonder if you would be able to help me. Also, I would appreciate it if you could recommend a good book that reflects the latest C++ ANSI draft.

Jorge Padron
Florida

A

I'll take your questions in order.

Question 1: The CBase operator- expects a single parameter of type const CBase &. References and pointers to derived classes can be implicitly converted to references and pointers to a base class. The expression:

d2 - d1;
is the same as:

d2.operator-(d1);
The CDerived class inherits an operator-(CBase &) member function from CBase. Since there is no CDerived::-operotor-(CDerived &) function, the function-argument matching algorithm uses the simple conversion from CDerived & to CBase & and matches the inherited function. If you want d2 - d1 to do something different, then you should declare and code a CDerived::-operator-(CDerived &) member function.

Question 2: The assignment operator is the one operator function that cannot be inherited. The reason for this prohibition is similar to the reason why constructors cannot be inherited: the additional members in the derived class may not necessarily be assigned properly. The expression

d3 = d2 - d1;
does translate into

d3.operator=(d2.operator-(d1));
The innermost expression returns a const CBase &. Thus the outermost expression is attempting to find a function of

CDerived::operator=(const CBase &).
You have defined such a function with

const CDerived& operator
      =(const CDerived& derived);
A reference to a base class cannot be converted to a reference to a derived class, because there is no way to determine how the additional members in the derived class should be set. If you specify an assignment operator as

const CDerived& operator
        =(const CBase& derived);
then the expression will compile.

Question 3: When you derive a class Derived from a base class Base, you do not have to rewrite functions that take a Base object as an argument. For functions that return a Base class, you must implement either a copy-type constructor or the assignment function with a Base class argument. The copy-type constructor prototype would look like:

Derived::Derived(Base &);
This would provide a conversion from the Base class to the Derived class, with appropriate default values being filled in for any additional Derived members.

It's generally best to avoid these, as constructing upwards (or downwards, depending on how you look at it) from a Base to a Derived class reverses the normal construction. A Derived class knows about the members of the Base class, but the opposite is not true. (Multiple inheritance can complicate this explanation. I concur with several other C++ writers in urging you to avoid multiple inheritance, but I will not belabor these issues.)

Readers should note that the operator- function for CBase will still be used for the expression d2 - d1 even if it is declared as

CBase operator-(CBase base) const;
The reason is that every class is given an implicit copy constructor, e.g.,

CBase(const CBase &);
This constructor takes a reference to the CBase class, so references to CDerived objects can be converted by a standard conversion to a reference to the CBase class. When the operator-function is called, the copy constructor will be invoked first to make a temporary CBase variable. That temporary variable will be copied onto the stack and passed to the operator- function. To minimize this needless copying, make the parameters to functions of the reference type. (For a copy of the C++ Draft Standard, contact ANSI at X3 Secretariat Computer and Business Equipment Association, 311 1st St. NW, Suite 500, Washington, DC 20001-2178, PH: (202) 737-8888.)

Auto-Increment

This note is a follow-up to a previous column concerning the auto-increment. The ANSI C Standard gives the example

i = 5;
i = ++i + 1;
as having undefined results. At a glance, it is difficult to see how the result could be other than 7. The variable i is preincremented to 6, 1 is added, and the result is stored in i.

Consider, however, the possibility of parallel processing. It could be the case that the increment and the assignment will be performed by two separate processors in parallel. Depending on which processor stores its result back in memory first, the value of i could be either 6 or 7.