Richard Hale Shaw
Richard Hale Shaw is a contributing editor of MSJ, PC Magazine, and various other computer magazines. He is authoring a book on C to be published by Ziff-Davis Press.
{ewc navigate.dll, ewbutton, /Bcodeview /T"Click to open or copy the code samples from this article." /C"samples_2}
For those of us who loved the power of MicrosoftÒ C Version 5.1 and were less than enamored with C 6.0, C 7.0 is a long-awaited successor. Its most important new feature is of course support for AT&TÒ C++ 2.1, but it also offers new features like precompiled headers, auto-inlining, DPMI-hosted tools and support for DPMI program development, an overlay manager, and packed code. Complete class libraries for the Microsoft WindowsÔ operating system and MS-DOSÒ operating system are also included.
In addition to the class libraries, the Microsoft C/C++ version 7.0 development system for Windows (C 7.0) contains more Windows1 support than any previous Microsoft compiler. The package contains everything necessary for building Windows-based applications: the header files, libraries, dialog, and resource editors, as well as the Windows debug kernel and additional Windows tools. Indeed, the only items in the Microsoft Windows Software Development Kit (SDK) not included in C 7.0 are the additional documentation and the analysis tools. You won’t need the SDK to create real Windows-based applications, nor will you pay extra to get those tools as part of C 7.0.
To use C 7.0, you’ll need an 386 or 486-based PC with a minimum of 4MB of RAM, a hard disk with at least 10MB free, MS-DOS2 3.3, or higher and a CGA, EGA, or VGA monitor. EGA or better is recommended.
(This article is based on the newest beta release that was available at the time of writing--Ed.)
The most exciting new feature in C 7.0 is its C++ support. Just by renaming your existing .C files to .CPP and recompiling, you can get the benefits of stricter type-checking, function overloading, stream I/O, relaxed variable declarations, and type-safe linkage. All of this is available even if you never actually write C++ code. If you’re already a C++ programmer, you’ll find that C 7.0 supports nested classes (the ability to define a class within a class definition) and offers a template generator for creating C++ code from templates as defined in AT&T C++ 3.0.
For programmers developing Windows-based applications with C++, the Microsoft Foundation Classes (MFC) offer full support for Windows objects such as windows, dialogs, messages, controls, and GDI objects (pens, brushes, fonts, and bitmaps). There are classes to support the Multiple Document Interface (MDI) and Object Linking and Embedding (OLE). In addition, MFC contains general purpose classes for managing strings, files, and exceptions that can be used in both MS-DOS and Windows-based applications. The 60 classes are small and fast: they consume only 39KB of RAM when compiled for small model, and add only about 5 percent to the execution time of applications.
As to the C compiler itself, all Microsoft C/C++ 7.0 tools are now DPMI-hosted. That is, they use the DOS Protected Mode Interface to run as 32-bit applications: they can make full use of extended memory and are generally faster than their 16-bit counterparts. Windows serves up DPMI to the compiler when its running in an MS-DOS window under Windows. You cannot run the command-line compiler under MS-DOS itself unless you first install a DPMI host, such as QualitasÔ 386MaxÔ, which is supplied with C 7.0. You can use C 7.0 to develop your own DPMI and VCPI applications, and the new CodeViewÒ debugger 4.0 included with C 7.0 can be used to debug these applications.
C 7.0 contains new command-line switches (see Figure 1), pragmas, keywords, and functions. For instance, you can use the _ _inline keyword to designate functions for which the compiler should generate inline code, rather than a function call. The _commit and _dos_commit functions let you flush the MS-DOS file buffers directly. The QuickWin functions (see "QuickCÒ for Windows, A Complete Graphical Environment for Easier Windows Programming," MSJ, Vol. 6, No. 6), let you use STDIO functions like printf in a Windows-based program. There’s a set of functions for implementing virtual memory in your MS-DOS programs. A new global variable, _cpumode, is set to _REAL_MODE or _PROTECT_MODE to indicate the operating mode of your program.
Figure 1 New Command-line Options
Precompiled Header Control |
/Fpfilename |
/Ycfilename |
/Yd |
/Yu |
Code Generation |
/G3 |
/G4 |
/Obnumber |
/Oo[-] |
/Gq |
/Gx |
/Gy |
P-code Generation and Control |
/Oq |
/Ov[-] |
/Of[-] |
/Gn |
/Gpnumber |
/NQpcodeseg |
Other Optimization Options |
/GA |
/GD |
/GEstring |
Linker Control |
/Ld |
/Lw |
/Lu |
Miscellaneous Options |
/Bmmemavail |
/F |
/Zf |
/Zn |
/NV |
/Tp |
Warnings and errors have been improved. For a given warning level, more warnings are generated by C 7.0 than by C 6.0. I wrote a program that used inline assembler and an assortment of ints. Below is a sample line.
int dow;
.
.
.
_asm mov dow, al; // dow = AL
C 6.0 didn’t generate any warnings or errors--yet the code it generated did not handle the transfer of the byte in AL correctly: AL was not promoted to an int, as called for by C. When I compiled the program under C 7.0, a warning was generated, giving me the option of changing the variable to an unsigned char.
Some older features of Microsoft C have changed, while still others have been eliminated. For instance, the same_seg pragma, which used to direct the compiler to assume that the external far variables are allocated in the same segment, is no longer supported, and the loop_opt pragma has been replaced with a version of the optimize pragma.
Command-line options such as /MD, /MT, /Lp, /Lc, /Li, /Gi, /B1, /B2, and /B3 are no longer supported, and the OS/2 functions (_beginthread, _cwait, _endthread, _pclose, _pipe, _popen, and _wait) are now obsolete. Finally, the object module format and LINK have changed, so you cannot link C 7.0 object modules using the linker supplied with C 6.0.
Although C 7.0 conforms to AT&T C++ 2.1, it does offer some limited support for templates, as found in AT&T C++ 3.0. Templates, as you may know, are also called parameterized types or generics. They let you create generic classes that are capable of handling any data type. When you define a template, you can delay specifying the data types used in a class definition until compile time; the compiler generates source code appropriate to handling the data types you specified when you instantiated the template class in your program.
Unlike BorlandÒ C++ 3.0, C 7.0 does not offer built-in support for templates. In other words, Borland C++ will perform the substitution for you automatically, whereas C 7.0 will not. In C 7.0, a template-generation tool called TEMPLDEF is supplied. TEMPLDEF is a text substitution tool that understands enough of the C++ syntax to pattern-match and substitute data types in a piece of source code.
You run TEMPLDEF from the MS-DOS command line, passing it the name of a file containing a template definition, data types to use in place of the generic data types, and the name of a source file on which to perform the substitutions. TEMPLDEF generates the appropriate source files from this information.
There’s no question that templates will be standard in C++ eventually. But there’s still some debate among C++ aficionados--both at AT&T and on the ANSI C++ committee--about how to implement them. For instance, if you tried to use a template class like a conventional C++ class, you’d define the template class in a header file and place any out-of-line member functions in a source file. But templates are instantiated in the application: how does the compiler resolve the differences between the generic template member functions in one source file with the function calls from the instantiated template class object in the application code? This has yet to be resolved.
Microsoft’s position is that when there is final definition of templates, Microsoft will implement them in the compiler, completely, correctly, and robustly. Microsoft didn’t include TEMPLDEF in the compiler itself, because it’s not compiler technology. It’s just simple text substitution, and not of the quality you would expect from a C++ compiler. Indeed, the documentation supplied with C 7.0 makes it clear that TEMPLDEF is just for generating useful sample code, and that it’s not an officially supported tool.
I disagree with this approach. Sure, you can use TEMPLDEF to generate templates. But Microsoft might as well have built it into the compiler: once the debates over template implementation have been resolved, Microsoft will have to change its C++ compiler to implement them anyway. In the meantime, developers will want to use templates, but will have to add the extra step of generating them with TEMPLDEF instead of the compiler’s substituting them automatically.
Inlining--particularly inlining class member functions in a C++ program--is a problem for some compilers. Borland’s C++ 2.0 and 3.0 compilers do not allow the C++ inline designation on functions containing switch, while, or for statements. C 7.0 can generate inline code for functions of any type or any mix of instructions. You can inline the most speed-critical functions (including seldom-used class member functions or constructors), allowing you to generate faster code.
A new compiler switch,/Ob, lets you stress inlining as you compile a program. When /Ob is used, the compiler inserts a copy of the function inline, in place of a function call. The compiler does this only for smaller, frequently called functions, minimizing the impact on a program’s size. This feature lets you pick up 5 to 10 percent increases in execution speed, especially with programs that contain many small functions or functions repeated in code blocks.
The /Ob1 switch enables inlining of all C++ member functions defined in a class definition, as well as functions marked with the C++ keyword inline or the new _ _inline designation. The /Ob2 auto-inlining switch inlines these plus any others the compiler deems appropriate. The Programmer’s Workbench ("PWB") offers a dialog box setting to enable this option. The default for this switch, /Ob0, disables all inlining.
The Microsoft CodeView debugger has been improved. CodeView3 4.0 has a new (albeit still character-based) interface, with CUA-compliant, overlapping windows that can be tiled, cascaded, minimized, or maximized. You can open up to 16 moveable, sizable windows. I found the interface peppier and easier to use. This new CodeView also loads programs faster than previous versions.
As you’d expect, CodeView 4.0 supports C++ values and expressions fully. There are the usual abilities to set breakpoints on and trace into constructors and destructors, and trace into inline functions and virtual functions (even if they’re defined in header files). It also offers class- browsing features that let you view information on a program’s objects: its class, member functions, friends, and the public, private, and protected status of each element.
CodeView 4.0 offers remote debugging. You can debug a C or C++ program running on a remote system via the RS-232 port. It can even transfer an executable to the remote system if it’s not already there or not up-to-date. CodeView 4.0 can run in extended memory, leaving conventional memory to run the target program. You can also use it to debug p-code and code that contains a VCPI- or DPMI-compliant MS-DOS extender.
PWB has also gotten a facelift. Like CodeView, it offers a CUA-compatible interface with moveable, sizeable, overlapping windows (see Figures 2 and 3). The interface is easier and more intuitive to use: some of the more insulting features of the old PWB, such as having to change settings in a file, have been updated to use dialogs. Plus, you can now change environment variables from inside the PWB itself.
Figure 2 The PWB runs as a character-based application under Windows.
Figure 3 Selecting C++ compiler otions in the PWB.
C 7.0 also features a new class browser, making it easier to manage C++ projects. You can use it to view class hierarchies, class data members, and member functions, and display a list of classes and members in a tree format. Plus, a browser API is available for accessing the browser from any environment.
Because many people were dismayed by the lack of hardcopy documentation in C 6.0, Microsoft dramatically expanded the documentation in C 7.0. There’s the usual Getting Started, Programming Techniques, C Language Reference--and the Run-Time Library Reference, which wasn’t even included with C 6.0. C 7.0 also comes with a 150-page C++ tutorial for C programmers, and a C++ Language Reference. The Class Libraries User’s Guide explains how to build object-oriented Windows-based programs using the MFC libraries, and a Microsoft Foundation Class Libraries Reference is also included. Microsoft C 7.0 Environment and Tools is a complete guide to the PWB, CodeView, and other tools; Programming Techniques has been expanded to include documentation on precompiled headers and p-code. In short, Microsoft C now comes with complete and comprehensive documentation; the only drawback is that you may need to build a new room onto your home or office for it.
Precompiled headers aren’t new. They first appeared in Borland C++ 2.0. But Microsoft has come up with a unique, more powerful way to implement them.
When you create a precompiled header with C 7.0, you don’t just precompile the header files included in a given source file. Precompilation saves the state of compilation up to a given point. A relationship is established between a source file and the precompiled header--not between the #included headers and the precompiled header as is the case with Borland C++ 3.0. You can create more than one precompiled header file per source file, which can be very powerful.
Precompiled headers are useful if you are changing code in your application’s source files more frequently than the headers, or when the headers comprise a significant portion of the code for given modules (as is often the case in C++ programs). Precompiled headers are also handy when you’re working on an application with multiple program modules that all use a standard set of header files. These can be made into one precompiled header if all the modules use the same compilation options.
C 7.0 offers a number of new compiler options for creating and using precompiled headers. These are summarized in Figure 1. All precompiled header files have the PCH extension.
Since PCH files are a "snapshot" of the state of compilation at a certain point in a source file, there are a number of ways to tell the compiler where in the source file to take the picture. You can specify this via the placement of certain #include and #pragma directives, or you can let precompilation continue to the end of a source file. To use the pragma, place
#pragma hdrstop
at the point where you want to save the state of compilation. You can specify a fully qualified path to a PCH file.
#pragma hdrstop("c:\\c700\\pch\\winapp.pch")
Because the argument to hdrstop is a C/C++ string, you must use double backslashes instead of single backslashes when specifying paths. However, this also means you can use ANSI token pasting:
#define PATH "c:\\c700\\pch\\"
#define PCH "winapp.pch"
o
o
o
#pragma hdrstop(PATH PCH)
You can place the hdrstop pragma anywhere in a source file (not a header file), as long as it appears outside any data or function declarations or definitions.
To create a PCH file, you use the /Yc command-line option. This option causes the compiler to save the state of compilation at the end of the source file. The compiler will save it to a PCH file with the same name as the source file. If the compiler encounters a hdrstop pragma before the end of the source file, it will save the state of compilation at the point that it found the #pragma. If the #pragma contains a filename, that name is used for the PCH file.
You can also use /Yc to save the state of compilation at the point that the compiler encounters a particular included header. Thus,
/YcWINAPP.H
causes the compiler to save the state of compilation to WINAPP.PCH at the point that WINAPP.H is included in the source.
Alternatively, you can use the /Fp option to specify the PCH filename, and still use /Yc to save compilation when a particular header file is included.
/YcWINAPP.H /FpDWINAPP
Here, compilation is saved to DWINAPP.PCH when WINAPP.H is included. This way you can create different PCH files--accounting for differences in compiler options and environment--for a single source file. In theory, you could have a different PCH file for each memory model and each set of compilation options used to develop an application.
To tell the compiler to use a particular PCH file, use the /Yu option. By itself, /Yu causes the compiler to scan the source file until it encounters a hdrstop pragma. Up to that point, everything--including preprocessor directives--is ignored by the compiler. Upon encountering the hdrstop pragma, the compiler restores the state of compilation from a PCH file specified in the pragma. If the pragma doesn’t specify a PCH file, the compiler derives the PCH filename from the source filename. You can also use the /Fp option to designate the PCH filename.
Alternatively, you can use
/Yufilename
and the compiler skips to the #include of filename and restores the state of compilation at that point from filename.PCH. Thus,
/YuWINAPP.H
causes the compiler to find the #include directive for WINAPP.H, and restore compilation from WINAPP.PCH from that point in the source file. These options even let you create one PCH from another. You can use /Yc to create a PCH, then /Yc and /Yu to create another PCH based on the first. Consequently, you can have more than one hdrstop pragma in a source file.
If you use /Yu and the compiler cannot find the appropriate header file, PCH file, or the #pragma hdrstop, it will generate a compilation error.
You can use precompiled headers to store information from the Microsoft CodeView debugger. The /Yd option (used in conjunction with the /Zi option to generate debugging information) puts the debugging symbol tables into the PCH file, rather than the OBJ.
This feature has a several benefits. First, you can compile other OBJs from this PCH without replicating debugging information in the OBJs. So when you use a C++ class in more than one module, the debugging information for that class is stored once, in the PCH file. This saves disk space, and makes a debug compilation much faster.
When you use precompiled headers, the C 7.0 compiler assumes that the compilation environment is the same as when you created them. If C 7.0 detects an inconsistency, it issues a warning. It’s largely up to you to make sure the environment is unchanged.
A number of factors can alter the compilation environment from when a PCH file was created, including the selection of memory models, the state of defined constants, the use of debugging options, and the use of other code generation options such as those used for compiling Windows applications. All these factors must be the same to reuse a precompiled header.
Two compiler options, /E and /EP (to preserve and remove #line directives), do not work with precompiled headers. And if you use the /Fr or /FR options to generate information for the Microsoft Source Browser, you must use them at the same time that you use /Yu.
Another caution: PCH files do not contain an INCLUDE path. Thus, if you switch between different sets of #include files, you’ll need to build new PCH files accordingly.
Finally, C 7.0 does retain certain pragmas in a PCH file. If defined and stored in a PCH, the following pragmas affect compilation that follows restoration from the PCH file:
alloc_text | intrinsic | |
auto_inline | native_caller | |
code_seg | pack | |
data_seg | same_seg | |
function | segment |
I’m quite impressed with the power and flexibility of the C 7.0 precompiled headers. They’re going to be particularly valuable to C++ developers, because you can store class definitions, member function definitions--indeed, everything--in a precompiled header.
The only objection I have is with the name: since you’re really precompiling a program up to a certain point, it’s silly to have called them precompiled headers. "Precompiled source files" would have been more accurate.
The most noteworthy new feature in C 7.0--with the obvious exception of C++ itself--is the Microsoft Foundation Class library (MFC). A collection of more than 60 reusable C++ classes, MFC encompasses all the features of the Microsoft Windows GUI and is fully compatible with Windows 3.0 and 3.1. It features classes for managing Windows objects and processes including Object Linking and Embedding (OLE), and offers a number of general-purpose classes that can be used in both Windows and MS-DOS-based applications. These include classes for creating and managing collections, files, persistent storage, exceptions, strings, and time. All 16,000 lines of source code that comprise MFC are provided with C 7.0, along a number of sample programs and a tutorial.
Unlike other class libraries for Windows such as Borland’s, ObjectWindows Library (OWL), MFC does not force you to learn a new Windows programming paradigm nor does it attempt to hide or duplicate the Windows API. The classes are a closely coupled, thin C++ wrapping of the Windows API. Consequently, MFC is flexible enough to simplify writing Windows-based applications that are nearly as small and as fast as their C counterparts.
Where MFC shines is in the way it reduces the programming "surface area" of Windows relative to the standard C approach. Using C++ and MFC, you don’t have to know and understand nearly as many functions or steps as you would to create a comparable program using C and the Windows API. MFC offers a wide array of useful window classes that can be used as is, and from which you can easily derive new classes of your own. There are classes for managing ordinary frame windows, MDI windows, edit controls, list boxes, combo boxes, push buttons, and so on.
MFC offers an efficient message-mapping system for processing Windows messages and associating them with class member functions. This lets you eliminate the standard, window-procedure approach to Windows programming. Instead, you specify which function will process which message via the message map.
The non-Windows classes in MFC can be used in both MS-DOS and Windows-based applications. They include collection classes that let you create ordered lists, indexed arrays and keyed maps; and store strings, void pointers, object pointers, bytes, words, and doublewords. As mentioned, a template tool, TEMPLDEF, is provided for using C++ templates to generate custom collection classes.
MFC also includes a string class that can manage dynamically allocated strings and manipulate strings with a Basic-like syntax; a time class that offers date/time arithmetic and automatic conversions of binary time values into readable date formats; a file class that offers a C++ interface to several low-level, buffered, and in-memory files; and an exception-handling classes. All the MFC classes (and their derivatives) support archiving or the creation of persistent objects and have built-in, optimal debugging and diagnostic features.
Like many well-designed class hierarchies, MFC’s classes are derived from a single base class, in this case CObject. CObject implements both type checking and persistency or serialization. These features let you query an object’s type at run time and store them to disk or some other persistent medium. Consequently, any classes derived from CObject--including the entire MFC library--offer these facilities, too.
To give you a feel for using the Foundation Classes in a Windows-based application, I’ve written a simple program called MFCHELLO (see Figure 4). This program opens a standard window with title bar, system icon, minimize/maximize boxes, and a simple menu. The program displays the message "Hello Windows, from MFC!" in its client area and offers a single menu item, which invokes the program’s About box.
A little over 60 lines of C++, MFCHELLO is shorter than a comparable Windows-based program written in C. It includes two locally defined C++ classes, both of which are derived from classes provided by MFC. Every Windows-based program written using MFC must have an application class, derived from CWinApp. This class provides a WinMain and a message loop, as well as most of the setup and initialization code usually found in WinMain. To make MFCHELLO work, the program also creates a new class based on CFrameWnd, an MFC class that provides for the creation and management of a standard frame window in applications that use the Windows single document interface.
The program begins with the inclusion of AFXWIN.H. This header file includes the MFC Windows-specific classes, WINDOWS.H, and AFX.H, an MFC header file that defines the general-purpose classes. Using a single header file facilitates the use of precompiled headers and means you have to remember to include only one file in your program.
Next is the first class definition. The HelloWindow class is derived from CFrameWnd, and contains the declarations for three member functions and a message map. The member functions are the class constructor and two message-response member functions. In C++, a class constructor is always called when an object of the class is instantiated. The two member functions, designated by afx_msg, are called when a window managed by a member of this class receives certain messages from Windows. (You’ll see how these are specified below.) Finally, the message map declaration tells the C++ compiler that a message map will be defined for this class.
The function definitions follow immediately after the class definition. The constructor, HelloWindow, calls the Windows API function LoadAccelTable to load an accelerator table. It will also call the Create member function (inherited from CFrameWnd) to create a standard overlapping window, using the menu "MainMenu" and the window caption "MFCHello." Based on the parameters passed to it, it’s obvious that Create will internally call CreateWindow.
The OnPaint member function handles WM_PAINT messages. When it’s called, OnPaint displays the string "Hello Windows, from MFC!" in the center of the window’s client area. To display the string, OnPaint needs a device context for the window. Rather than calling BeginPaint (as is done in a traditional Windows-based program), OnPaint creates a CPaintDC object, which provides a device context for use during WM_PAINT processing. CPaintDC is one of three MFC functions (including CClientDC and CWindowDC) that provide device contexts for specific uses.
Since the CPaintDC object is created by a window object derived from CFrameWnd, you don’t have to pass CPaintDC’s constructor a window handle as you would with BeginPaint. Instead, you can pass it a pointer to the window object that created it. The C++ keyword "this" always refers to the object that’s calling a member function. Thus, "this" represents a HelloWindow object, and CPaintDC’s constructor internally retrieves the HelloWindow object’s window handle. HelloWindow inherits the window handle data member from CFrameWnd, which in turn inherits the member from CWnd. Thus, CPaintDC’s constructor receives a pointer to a CWnd object, from which it retrieves the window handle data member and calls BeginPaint. Upon returning from CPaintDC’s constructor, the dc variable represents a device context, and you can call its member functions, SetTextAlign and SetBkMode, to set it up for centered text display and set its background color.
Next, OnPaint creates a CRect object called rect. C++ lets you create a variable anywhere in a function--not just at its inception.
OnPaint passes rect to the GetClientRect member function to retrieve the coordinates of the window’s client rectangle. (HelloWindow inherits GetClientRect from one of its ancestors.) You don’t pass the address of rect to GetClientRect: this function takes a C++ reference that lets a function access an object as if it had been passed a pointer to that object and is just as efficient. References don’t require you to use the address-of (&) operator in the function call, and the code to the called function does not have to dereference the object with *, since it’s not a pointer.
After this, OnPaint creates the string that will be displayed by initializing an object of class CString, which can manage dynamic strings while behaving like an ordinary character pointer. CString is one of the many general-purpose classes in MFC that you can use in either Windows or MS-DOS-based applications. Since CString overloads the C++ assignment operator (=), you can create a CString object through simple assignment, as shown here (although you can optionally pass a string to CString’s constructor if you wish). The CString class also overloads + and += so you can concatenate strings using operators. CString provides its own GetLength member function, used in the next line of the program, to return the length of the string.
Finally, OnPaint calls CPaintDC::TextOut to display the string at the center of the client area. Although the function ends here, EndPaint has not been called--or at least, not explicitly. C++ guarantees that a class’ destructor is automatically called when an instance of the class goes out of scope. CPaintDC’s destructor, called for the dc variable when the function ends, automatically calls EndPaint and releases the device context.
Next in the program is the definition of the HelloWindow::OnAbout member function. This function creates and displays a modal dialog box, defined in the About dialog template, and then returns. The function is called when a window managed by a HelloWindow object receives a WM_COMMAND with its wParam parameter set to IDM_ABOUT.
You may be wondering how the OnPaint and OnAbout member functions of HelloWindow are connected to WM_PAINT and WM_COMMAND messages. This is where the message map, declared in the class definition for HelloWindow, comes in. The first line of the message map definition shows which C++ class the message map is defined for (HelloWindow) and the name of that class’ parent class in MFC (CFrameWnd). The ON_WM_PAINT function always maps WM_PAINT messages to the class’ OnPaint function, so just entering ON_WM_PAINT in the message map creates the relationship between OnPaint and WM_PAINT. The ON_COMMAND function lets you specify what function (OnAbout) will be called when the wParam parameter to WM_COMMAND has a certain value (in this case, IDM_ABOUT). Thus, the OnAbout function is called when a window associated with a HelloWindow object receives a WM_COMMAND message with wParam set to IDM_ABOUT.
The remaining code simply derives an application class and creates an application object (the beginning of an MFC program). The MFCHello class is derived from CWinApp (which provides a WinMain), and at the end of the listing, an object of type MFCHello is declared.
MFCHello has only one member, a member function that overrides the InitInstance function it inherits from CWinApp. The InitInstance in CWinApp is a virtual function; it’s a default function that’s called if it’s not overridden in a derived class. Since MFCHello overrides the virtual function CWinApp::InitInstance, a call to InitInstance--even by the CWinApp constructor--calls MFCHello::InitInstance. The function, declared in MFCHello and defined immediately after, creates an instance of the HelloWindow class and calls that instance’s ShowWindow and UpdateWindow member functions. And thus, the HelloWindow window appears.
To start an MFC program, you only need to instantiate an object of the class that you derived from CWinApp. The CWinApp constructor automatically invokes the InitInstance function of the derived class--MFCHello::InitInstance--and the program begins. The CWinApp-derived object is created on the last line of the listing.
The combination of C++ and MFC not only shortens the program, it removes the necessity for the ubiquitous WinMain and a window procedure. You spend your time writing the application code--not the routine code that’s required in every Windows-based program.
Another powerful new capability supplied with C 7.0 is the p-code technology. Used by Microsoft in the creation of its own commercial applications, p-code is a flexible and easy-to-use facility for reducing executable file sizes and therefore, a program’s memory requirements. Using p-code properly, you can significantly reduce a program’s size, sometimes by as much as 40 percent.
Traditionally, a gain in speed has been associated with increased code size. But as a program increases in size, there’s a greater chance nowadays that new PC enhancements such as virtual memory and caching will kick in and counteract any improvement in speed obtained with larger code. In environments like Windows, segment swapping--which affects performance--is reduced with smaller code.
The philosophy behind p-code is simple: reduce the size of the program without significantly reducing its execution speed. P-code is a high-level, very compact language where each p-code instruction translates into several machine code instructions. You can turn p-code generation on via the /Oq compiler switch or toggle it on and off via source code pragmas:
#pragma optimize("q",on) // turns on p-code generation
#pragma optimize("q",off) // turns off p-code generation
These pragmas must be placed outside a function definition; you cannot toggle p-code generation inside a function.
You can translate individual functions, modules, or entire programs into p-code, although the latter isn’t always advisable. If any part of your program includes p-code, the linker invokes the Make P-Code utility, MPC.EXE. MPC binds a p-code interpreter into your program, adding about 9KB to the executable file. Obviously, generating p-code is not for the smallest applications: the portion you’re converting to p-code should be bigger than 25KB if you want to recoup the space occupied by the p-code interpreter in the executable file.
At run time, the p-code interpreter is invoked to translate and execute p-coded sections of your program. While there’s definitely some loss of performance associated with calling the interpreter and having it translate and execute p-code, you can minimize this performance degradation by restricting p-code generation to noncritical sections of the application.
Although you don’t have to understand how p-code works to use it in your programs, you’ll have difficulty debugging a p-coded program without a basic understanding of its operation.
The p-code interpreter is a simple engine that processes a series of high-level opcodes. It’s strictly stack-based, using the stack to store p-code operands. P-code instructions are more compact than assembly-language instructions since it’s not always necessary to specify source and destination addresses for each instruction.
For instance, the following assembly-language instructions,
pop CX ; pop 1st operand into CX
pop DI ; pop 2nd operand into DI
add DI,CX ; add and store result in DI
push DI ; push result onto stack
can be p-coded into
AddW
Each p-code instruction implicitly pops its operands off the stack and pushes its result back onto the stack. Thus, the single AddW opcode encapsulates the locations of the operands and the result on the stack, as well as the mechanics of adding them and accessing them. Since much of the action takes place by default, p-code requires fewer instructions.
In cases where a p-code instruction must modify the value of a nonstack variable, the source and destination addresses are specified in the opcode; however, most p-code instructions use the stack for at least one argument.
The p-code stack can store different data types including bytes, words, and longs. It stores floating-point types on a separate stack called the coprocessor stack. The p-code stack replaces the need for registers AX through DX, but DS, SS, CS, IP, SP, and BP are still accessed by the p-code engine. In addition, the engine maintains two pseudoregisters, PQ and TL.
P-code instructions (see Figure 5) have the following form,
operation[mode][qualifier]data_type[operand_data_type]
where
operator | Operation being performed |
mode | Addressing mode |
qualifier | Conventions used by instruction |
data_type | Data type operation is performed on |
operand_data_type | An operand not on the stack |
P-code can further reduce the size of programs by using assumed values, where opcodes have alternate forms that assume a particular value for an operand. For instance,
JneWb 05
pops two words off the stack, compares them, and if not equal, jumps a length of 5. But
JneW5
does the same thing and doesn’t require an operand, saving one byte. Only the most common values are encoded like this, but the advantage is that they can be used to reduce the code size further.
Since the p-code engine uses implied addressing, most opcodes average less than 2 bytes in size and come in two flavors. The standard set of p-code opcodes consist of the 225 most commonly used opcodes, which require a single byte. The extended set of 256 opcodes is used less frequently.
Figure 5 P-code Instructions
P-code Instruction Modes |
Mode |
n |
f |
no |
fo |
P-code Instruction Qualifiers |
Qualifier |
p |
t |
s |
u |
fc |
nc |
fp |
np |
P-code Instruction Data Types |
Qualifier |
V |
B |
Q |
W |
S |
L |
N |
A |
F |
H |
R |
D |
T |
P-code Instruction Operand Data Types |
Operand Data Type |
0-9,ml |
b |
w |
l |
The p-code generator also uses "quoting" to reduce the size of the code. Similar to routines in a high-level language, a quote is a single block of code used throughout a program. Using conventional optimization techniques, the compiler examines the p-code it generates and looks for repeated sequences of instructions. When it finds a repetition, it replaces all but one of the occurrences with a jump instruction, which directs the flow of execution to the beginning of the single retained occurrence. This can compress an executable file an additional 5 or 10 percent.
For example, suppose two lines of source in the same module contain a common subexpression. At the first occurrence of the expression, the end of the code is marked with an EQUOTE instruction. When the code is used again, the p-code generator issues a QUOTE instruction instead of reproducing the code. Like common subexpression elimination (used by optimizing compilers), this converts a duplicated sequence of code to a reusable routine.
You can control the use of quoting at the global or program level by enabling quoting with /Of, and you can disable quoting with /Of-. Since quoting often results in numerous jumps to labels, you’re better off turning quoting off until the application is complete; otherwise you may find it difficult to debug.
Unlike a function call, a quote does not have arguments or a return value: only the path of execution changes when a QUOTE is encountered. The QUOTE instruction takes a one- or two-byte offset as an argument. When it’s executed, a QUOTE saves the address of the next instruction in the p-code engine’s PQ register as a return address, and performs a jump to the specified offset.
When the engine encounters the subsequent EQUOTE, it checks to see whether PQ contains an address. If so, the engine jumps back to that address; if not, it does nothing. This allows the quoted code to be executed both as a sequence of code and as a quote call.
The quoting facility can quote function calls inside code blocks as well as generate interprocedural quotes. Nested quotes are not supported. But a quoted section can contain a procedure call that in turn executes a different section of quoted code. This is possible because the stack frame of each p-code procedure has its own PQ register.
In the event that a machine code function calls a p-code function (which can happen if p-code is turned on and off throughout a program), the program stops executing machine code and turns over control to the p-code engine. To make this possible, each p-code function contains a native entry point: a sequence of 6 bytes of machine code instructions that transfer control to the p-code engine.
The compiler automatically generates this entry sequence at the beginning of each p-code function. You can use the /Gn switch to turn off native-entry-point generation. Or use
#pragma native_caller(off)
and follow it with the definitions of functions that are only called from other p-code functions. You can turn native-entry-point generation back on with
#pragma native_caller(on)
Optionally, you can reset the native_caller setting to that specified on the command line with
#pragma native_caller()
All p-code functions that are exported, called via function pointers, or defined as _loadds require native entry points. This includes Windows callback functions.
You can use the p-code compiler switch and pragmas to turn it on and off like any other optimization option. You can tune the speed/size ratio by p-coding some portions of a program and not others. If you want to try a quick transition to p-code, set the /Oq switch to p-code your entire application. You can then use the pragmas to turn off p-coding in specific functions or portions of a module.
The best applications to p-code entirely are programs that leave the CPU idle a great deal of the time or that rely heavily on a user interface: that is, any application where differences in speed are largely negligible. Word processors, editors, interactive calendars, time-planners and schedulers, as well as batch programs that are not time-critical are all good candidates for global p-coding. Code associated with error-handling and other rarely used operations are also good p-code candidates.
If you’d prefer to p-code your application selectively, don’t waste time experimenting: use the Microsoft Source Profiler (included in C 7.0) to generate a function-level profile. (For more information, see "Microsoft Source Profiler Helps Tune DOS, Windows, and OS/2Ò Applications," MSJ, Vol. 6, No. 5.) The resulting profile shows which functions are used most heavily, how they perform, and the degree to which the application’s performance relies on them. Functions that are good p-code candidates include infrequently used routines, error-handling procedures, and user-interface routines. You should refrain from p-coding functions that are speed-critical, are called frequently, or used heavily in loops (that is, either the function is called frequently in a loop or the loop code itself is exercised heavily in the application). Use the p-code pragmas to toggle p-coding at the function level.
You can use p-code for any application that supports C 7.0’s C or C++ syntax, and that’s targeted for either MS-DOS or Windows. Both the Microsoft Source Profiler and the CodeView debugger can properly profile or debug p-coded applications.
For instance, CodeView 4.0 lets you debug a p-coded application at both the assembly-language and source-code level. In assembly mode, CodeView disassembles the p-code sections of the program to show the p-code instructions rather than the native machine code. (You can use the Options Native command to disable p-code support and display only assembly language.) When a p-coded program hits a breakpoint in p-code, the register window changes to show the state of the p-code stack and the interpreter. The usual CodeView commands (such as break, step, watch) work identically for both p-coded and non-p-coded applications.
Many commercial Microsoft applications employ p-code to some degree. Indeed, when the C 7.0 team recompiled Microsoft Project with full-size optimizations on, the resulting executable was 932KB. The program shrunk to 556KB using p-code.
There are no guarantees: you must be judicious in your choices of functions to be p-coded. P-code may be smaller, but it’s definitely slower.
At some point, every software developer is faced with the problem of trying to wedge a program’s code and data into a limited memory address space. Overlay management systems offer a solution. They let an application keep only those portions of the program that are needed in memory at any given time.
With an overlay manager, a program is divided logically into a root section and one or more overlays. The root contains an overlay manager, plus routines, variables, and constants that must remain in memory at all times (because they’re used by all other parts of the program). The overlay manager can then swap pieces of an application from disk into RAM on demand, and lay them over other pieces (effectively discarding them). You don’t need to use overlay management in environments like Windows (which offer some form of virtual memory), but they’re often necessary under MS-DOS.
Previous versions of Microsoft C offered a rudimentary overlay facility. C 7.0 features a new full-fledged overlay manager called MOVE. MOVE has some features other overlay managers don’t, including support for pointers to functions, XMS or EMS caching, and the ability to keep more than one overlay in memory at a time. In addition, MOVE offers run-time configuration and performance tuning, as well as compatibility with the Microsoft Source Profiler and CodeView 4.0.
In a MOVE application, the root includes 5KB of overlay management routines, a table of information about the overlays, and all the program segments that you do not designate overlay-resident. At run time, MOVE allocates a heap from conventional memory and (if possible) a cache from XMS or EMS memory. The heap can be as small as the largest overlay and as large as the total size of the three largest overlays, but you can use the _moveinit routine to change these defaults.
When a MOVE application calls a function that resides in an overlay that’s not yet been loaded, the MOVE overlay manager reads the overlay from the executable file into the overlay heap. If necessary, the overlay manager discards existing overlays (using an LRU algorithm to determine which ones to discard) to make room in the heap for the new overlay. If the cache was allocated, the overlay manager may place the discarded overlays in the cache (again, it uses an LRU to determine which overlays remain). When it needs to reload an overlay, the overlay manager may reload it from the cache rather than from disk.
If an overlay in the cache is reloaded into main memory, MOVE does not delete the overlay from the cache. Deletion unnecessarily consumes CPU cycles and it can cause recaching. If the overlay is later discarded from main memory, MOVE can determine that the overlay is already in the cache, and therefore it will not have to recache it.
MOVE maintains the LRU on overlays stored in the heap and the cache via a chain of doubly linked lists--one for the heap and one for the cache. Each list node is an overlay data structure. When the application references an overlay or the overlay is loaded from the cache or the disk, the corresponding node is moved to the front of the list.
Each MOVE overlay includes a header of 512 bytes; while this increases the size of overlaid applications, the difference is usually a small fraction of the total executable size. You can define up to 65,535 overlays in a single application, and each overlay can be up to 64KB long. The optimal overlay size is 4KB: the smaller the overlay, the less swapping is required.
In the event that a MOVE application spawns a child program that uses XMS or EMS memory, the MOVE parent can use the _movepause routine to release the cache. The _moveresume routine can be used to restore the cache when the child exits.
C 7.0 offers some simple extensions to the module definition file (DEF) format that you can use to define the overlays in a MOVE application. For instance, the FUNCTIONS statement lets you specify a list of functions that should be placed in an overlay, followed by an overlay number. (An overlay number of zero is used to specify the root.) Alternatively, you can use the SEGMENTS statement to identify named segments and the overlay to which they belong. An INCLUDE statement lets you insert a text file at the point of the INCLUDE statement in the DEF file. The included file can contain a list of valid SEGMENTS and FUNCTIONS statements. While you can also overlay data with MOVE, it’s not recommended.
Two MOVE features let you fine-tune the performance of an overlaid program: tracing and dynamic configuration.
The biggest performance problem with overlays is thrashing--when an overlay is being swapped too frequently. Thrashing is usually caused if calls to a function in the swapped overlay are not optimally distributed among the other overlays. This causes frequent jumps between the same overlays, and the overlays are unnecessarily swapped too often.
Tracing lets you study the performance of an overlaid application by producing a history of overlay activity. You turn on tracing via a call to _movetraceon, causing the MOVE kernel to generate performance records on each overlay. These records are written to a file, where they can later be read via a provided utility. Once you’ve identified the culprit function, you can redistribute the overlays to swap more efficiently.
The _movetraceoff function turns tracing off. Since you can place these functions anywhere in a program, you can institute tracing over all or just a part of the program.
Another problem concerns overlay managers themselves. Most are statically configured at link-time; that is, the allocation of memory and system resources must be performed before you know exactly how large those resources will be.
A MOVE facility called dynamic configuration enables system resources to be automatically configured at run time. When you run a MOVE application, the MOVE kernel calls _moveinit to query the system about available conventional, XMS, and EMS memory, and to calculate the optimal heap and cache sizes. While _moveinit’s default settings are often sufficient, you can use environment variables to configure them (see Figure 6).
Figure 6 Environment Variables of _moveinit
MOVE_XMS | Amount of available extended memory |
MOVE_EMS | Amount of available expanded memory |
MOVE_HEAP | Amount of heap space for overlays |
MOVE_COVL | Maximum number of overlays |
Microsoft C/C++ 7.0 brings the compiler into the world of object-oriented programming. For many people, features like p-code, MOVE, precompiled headers and auto-inlining make it well worthwhile to upgrade. Both Microsoft C/C++ 7.0 and Borland C++ 3.0 are great C++ compilers.
Each compiler offers something different in Windows and C++ support. Borland C++ comes with a Windows-based IDE: TurboC++Ô for Windows. The C/C++ 7.0 Programmer’s Workbench can run in an MS-DOS window under Windows, but it isn’t a Windows-based IDE. It’s pretty amazing that the company that brought us Windows itself hasn’t come up with a true Windows-based environment for programmers.
The MFC libraries for C++ are cleaner, simpler, and easier to use than Borland ObjectWindows (or Borland TurboVisionÔ for MS-DOS--which is incompatible with ObjectWindows). Plus, you’ll have to pay extra to get Borland’s class libraries. Without them, Borland C++ is the same price as Microsoft C 7.0 with MFC. If you’re a new C++ programmer, you’ll love the MFC libraries, and I predict that it will become a new standard for programming Windows, replacing the older, C-based, SDK approach.
1For ease of reading, "Windows" refers to the Microsoft Windows operating system. "Windows" is a trademark that refers only to this Microsoft product.
2For ease of reading, "MS-DOS" refers to the Microsoft MS-DOS operating system. "MS-DOS" is a trademark that refers only to this Microsoft product.
3For ease of reading, "CodeView" refers to the Microsoft CodeView for Windows debugger. "CodeView" is a trademark that refers only to this Microsoft product.