pPage


Teach Yourself Visual C++ 6 in 21 Days


- D -
Understanding and Exception Handling

  • Using Exceptions
    • Running Code and Catching the Errors
    • Throwing Exceptions
    • Deleting Exceptions
  • MFC Exception Types
    • Using the CException Base Class
    • Using the Memory Exception
    • Using the Resource Exceptions
    • Using the File and Archive Exceptions
    • Using the Database Exceptions
    • Using OLE Exceptions
    • Using the Not Supported Exception
    • Using the User Exception
    • Generating Your Own Custom Exception Classes



by Jon Bates

Using Exceptions

An exception is an object that holds details about something that has gone wrong. The clever thing about exception handling is that you can create an exception when something goes wrong in a low-level function and have it automatically bubble back up to a calling function that can deal with all such exceptions in one place.

Running Code and Catching the Errors

The system automatically detects certain error conditions and generates exceptions for them. If you don't deal with them in your application, they will bubble back out of your code and be handled by Windows's own exception-catching mechanisms. If you want to see this in action, just add the following two lines to any of your code and run it:

CDC* pNullDC = 0; pNullDC->SetPixel(0,0,0); 

The first line declares a device context pointer pNullDC and sets it to point the memory address to zero (which isn't a good place for any object to be). Obviously there isn't a valid object at this address, so when the following SetPixel() function is called, the system tries to find the object at address zero. The memory management hardware and software know that the program has no right being at this memory address and raise a memory access violation exception.

If you run these lines of code from outside the Visual C++ debugger, you'll see a dialog box familiar to all Windows users, as shown in Figure D.1.

FIGURE D.1. The Windows memory access violation Exception dialog box.

However, if you run the application from the Visual C++ debugger, the debugger first catches the exception for you and displays the Developer Studio dialog box instead, as shown in Figure D.2.

FIGURE D.2. The Developer Studio-handled memory access violation exception.

Memory access violations are very severe exceptions that will crash your program without any chance to catch them. There are many less severe exceptions, such as the file-handling exception CFileException. This exception is thrown when erroneous file operations occur, such as attempting to seek to the beginning of an unopened file:

CFile fileNotOpen; fileNotOpen.SeekToBegin(); 

This results in a system-generated dialog box (see Figure D.3). If you click OK, your program will continue as usual.

FIGURE D.3. The Windows default dialog box for a file exception.

Rather than letting the system catch the exception, you can catch the exception and deal with it yourself in a more graceful manner. To do this, you must use the C++ try and catch keywords. You can use these by defining a block of code to try; then, when a specified exception is raised, an action is defined in the catch block (see Listing D.1).

LISTING D.1. LST29_1.CPP--USING A try AND catch BLOCK TO CATCH CFileExceptions.

1: // ** Try a block of code 2: try 3: { 4:     CFile fileNotOpen; 5:     fileNotOpen.SeekToBegin(); 6: } 7: catch(CFileException* e)    // Catch File Exceptions 8: { 9:     // ** Check the cause of the exception 10:     if (e->m_cause == CFileException::fileNotFound) 11:        AfxMessageBox("Oops, forgot to open the file!"); 12:     e->Delete(); 13: }  

In Listing D.1, a try block is defined around the file operations at lines 4 and 5. If these lines don't raise an exception, the code will continue as normal. However, if a CFileException is raised, it will be caught by the catch keyword in line 7, and the variable e will point to the new exception. The CFileException object has an m_cause code that defines exactly why the exception was raised. This is checked on line 10, and if this was a CFileException::fileNotFound code, the message box on line 11 is displayed.

Notice that the Delete() member function of the CException class (the base class of CFileException) in line 12 will delete the exception for you. You must ensure that exceptions are always deleted when you are finished with them.

The try block can include calls to other functions and could be used to catch any specified exceptions raised in a large portion of the application, as shown in Listing D.2.

LISTING D.2. LST29_2.CPP--A try BLOCK CAN INCLUDE MANY FUNCTION CALLS AND CALLS FROM THOSE FUNCTIONS.

1: try 2: { 3:     // ... Lots of code 4:     DoLotsOfFileHandling(); 5:     // ... More code 6:     EvenMoreFileHandling(); 7:     // ... And more Code 8: } 9: catch(CFileException* e)    // Catch File Exceptions 10: { 11:     // ** Check the cause of the exception 12:     if (e->m_cause == CFileException::fileNotFound) 13:        AfxMessageBox("Oops, forgot to open the file!"); 14:     e->Delete(); 15: } 

In Listing D.2 the DoLotsOfFileHandling() function on line 4 could implement some file handling itself, as well as calls to other functions, as with EvenMoreFileHandling() on line 6. Should a file exception arise through any of these file operations, the exception will bubble back so that the same catch block will be executed in lines 9 through 13 with e pointing to the CFileException object. Finally the exception is deleted in line 14.

If you want to catch two different exceptions from the try block, you can add catch blocks to handle each different exception, as shown in Listing D.3.

LISTING D.3. LST29_3.CPP--HANDLING TWO DIFFERENT EXCEPTIONS WITH THE EXCEPTION- SPECIFIC catch BLOCKS.

1: try 2: { 3:     // ** This file operation is ok 4:     CMemFile fileMemFile; 5:     fileMemFile.SeekToBegin(); 6:  7:     // ** But you can't have two different system  8:     // ** resources with the same name. 9:     CMutex        mutex1(0,"Same Name"); 10:     CSemaphore    semaphore1(1,1,"Same Name"); 11: } 12: catch(CFileException* e)    // Catch File Exceptions 13: { 14:     if (e->m_cause == CFileException::fileNotFound) 15:        AfxMessageBox("Oops, forgot to open the file!"); 16:     e->Delete(); 17: } 18: catch(CResourceException* e)      Â// Catch Resource Exceptions 19: { 20:     // ** Report the Resource exception error 21:     AfxMessageBox("Oops, duplicate resource name"); 22:     e->Delete(); 23: }  

In Listing D.3, the memory file is automatically created in line 4, so line 5 won't cause a file exception. However, naming two different system resources (a mutex and a semaphore) with the same name does cause a CResourceException in line 10 that is then caught by the second catch block in line 18, which displays the message box in line 21. If you try this code yourself, remember to add an #include <afxmt.h> line for the CMutex and CSemaphore definitions.

If you want to do a blanket exception catch, you don't need to have a catch block for each type of exception; instead, you can catch the CException base class exception from which all the other more specific exception classes are derived (see Listing D.4).

LISTING D.4. LST29_4.CPP--USING THE catch BLOCK TO CATCH ALL TYPES OF EXCEPTIONS.

1: // ** Try a block of code 2: try 3: { 4:     // ** Lots of code ... 5: } 6: catch(CException* e) 7: { 8:     // ** General Error message, details in e 9:     AfxMessageBox("Oops, something went wrong!"); 10:     e->Delete(); 11: } 

Notice that on line 6 the CException base class is used rather than a specific exception such as CFileException or CResourceException. You can test which type of exception was raised using the IsKindOf() function inside the catch block. For example, to test whether a file exception has been raised, you might use the following lines:

if (e->IsKindOf(RUNTIME_CLASS(CFileException))) AfxMessageBox("File Exception"); 

Because exceptions are derived from CObject, they support the MFC runtime class information. By using DECLARE_DYNAMIC and IMPLEMENT_DYNAMIC, the class information is bundled into the derived exception object so that the IsKindOf() function can be used to check for a specific class type. The RUNTIME_CLASS macro turns class names into a pointer to a CRuntimeClass object for the specified object. The IsKindOf() member function will then return TRUE if the object being called is of that runtime class.

The "MFC Exception Types" section later in this chapter covers how you can determine exception-specific information from each type of MFC exception caught in a catch block.


FREEING SYSTEM RESOURCES

This exception-catching principle becomes very useful when you want to detect and handle errors arising from large portions of code. It can save coding lots of individual error-checking lines, but you must still free up any system resources that you've allocated in lines before the exception was raised.


Throwing Exceptions

You can throw exceptions yourself from code embedded in any enclosing try block when an error condition arises. The corresponding catch block will then handle the exception. Or you can throw the exception again from within a catch section to a higher-level catch section, enclosing the first.

Several AfxThrow... functions will automatically generate and throw various types of MFC exceptions up to the next catch level, such as AfxThrowFileException() or AfxThrowMemoryException(). These are covered in detail in the "MFC Exception Types" section. However, these functions create a new instance of a specific CException-derived object for you--using the C++ new keyword and then the throw keyword to raise an exception, as shown in the code fragment in Listing D.5.

LISTING D.5. LST29_5.CPP--RAISING AN EXCEPTION WITH THE throw KEYWORD.

1: try 2: { 3:     DoSomeFileHandling(); 4: } 5: catch(CFileException* e) 6: { 7:     e->ReportError(); 8:     e->Delete(); 9: } 10:  11: return TRUE; 12: } 13:  14: BOOL bSomeThingWentWrong = TRUE; 15:  16: void CExceptionalDlg::DoSomeFileHandling() 17: { 18:     // ** ... File handling functions 19:     if (bSomeThingWentWrong == TRUE) 20:     { 21:         CFileException* pException =  22:            new CFileException(CFileException::generic); 23:         throw(pException); 24:     } 25:  26: // ** ... Yet More file handling 27: }  

In Listing D.5 the try block encloses a call to the DoSomeFileHandling() function in line 16. This function may implement some file-handling procedures and raises an exception when the error condition on line 19 is found to be TRUE. Line 22 creates a new CFileException object passing the CFileException::generic flag to its constructor and then throws the new object in line 23 to be caught by the catch section in line 5.

This process of newing a CException-derived object and then using the throw keyword is the basis of the exception-raising mechanism. The specific details indicating the cause of the error can be attached to the CException object, or extra information can be added by deriving a class from the CException base class and adding extra variables to store more specific information.

Your catch block can then determine whether the error is too severe to be handled at that level. If so, you might want to throw the exception out to a higher-level enclosing catch block. You can use the throw keyword (with no parameters) from within the catch block to rethrow the exception before you delete it. Instead of deleting the exception, you could rethrow it to a higher level catch block by changing the catch block shown in Listing D.5 to add the throw keyword like this:

e->ReportError(); throw; 

Then after reporting the error, the exception will be thrown again for an enclosing try block to catch. If you haven't implemented this nesting, the overall MFC outside the catch block will catch it. You can use this nesting mechanism to determine the error severity and implement appropriate recovery mechanisms at various hierarchical levels in your program.

Deleting Exceptions

As you've seen, you are fundamentally responsible for new-ing exceptions and must also delete these objects when you've handled them. If you delete one of the MFC exceptions, you shouldn't use the normal C++ delete keyword (as you've seen) because the exception might be a global object or a heap object. Instead, the CException base class has a Delete() function that first checks to see whether the exception should be deleted. The creator of the exception can specify whether the exception should be deleted or not by passing TRUE into the b_AutoDelete parameter of the CException class's constructor (which is the only parameter).

MFC Exception Types

The Microsoft Foundation Classes have several predefined CException-derived classes that are used during different types of MFC operations. You've already seen CFileException and CResourceException in use. The following section covers each of these various classes and how it is raised in more detail. Each class is based on the CException class and extends the functionality of CException for different types of exception handling. You can also derive your own exception classes from CException, and a generic CUserException is used for user-oriented application exceptions.

Using the CException Base Class

CException itself has a constructor that takes an AutoDelete flag as discussed earlier, and is defined like this:

CException( BOOL b_AutoDelete ); 

If you new a CException or derived class, you should ensure that this is set to TRUE so that it will be deleted with the C++ delete keyword. Otherwise, a global or stack-based exception should pass TRUE so that it is deleted only when it goes out of scope (at the end of a function or program that declares it).

The base class contains the Delete()function and two error-reporting functions. GetErrorMessage() can be used to store the error message into a predefined buffer and specify the ID of a help message to show the user context-specific help pertinent to the error. Its first parameter is the address of a destination buffer to hold the associated error message. The second parameter specifies the maximum size of the buffer so that messages stored in the buffer don't over-spill outside the buffer area. The third optional parameter can specify the context help ID as a UINT value.

You might use this function to help format an error message more relevant to your application:

char msg[512]; e->GetErrorMessage(msg,sizeof(msg)); CString strMsg; strMsg.Format("The following error occurred in  ÂMyApp: %s",msg); AfxMessageBox(strMsg); 

The sizeof() C++ operator in the GetErrorMessage() function returns the size of an array or variable, so if the msg array is changed, you don't have to change any other code. The message is then formatted into the strMsg CString object and displayed in a message box.

The ReportError()function displays the message text directly in the familiar exception message box and would be used from the catch block:

e->ReportError(); 

Using the Memory Exception

The CMemoryException is raised automatically when a C++ new keyword fails. You can also raise it yourself using the AfxThrowMemoryException(); function. The meaning of this exception is exclusively that Windows can't allocate any more memory via its GlobalAlloc() or other memory allocation functions. This is a pretty dire situation for any program; you would usually handle this exception by writing code that lets your program die gracefully, freeing up memory and system resources as it goes. There are rare cases in which you could take recovery action if you had a large block of memory allocated and could free it without too much detriment to the users' activities.

Due to the exclusivity of this exception, no other cause attributes or specific functions extend the CException class's functionality.

You can watch new automatically raise a CMemoryException with these lines:

MEMORYSTATUS mem; GlobalMemoryStatus(&mem); BYTE* pBig = new BYTE[mem.dwAvailVirtual+1]; 

The mem.dwAvailVirtual structure member of MEMORYSTATUS will hold the total available memory after the GlobalMemoryStatus() function retrieves the details. The new on the next line requests one more byte than it could possibly have, thus throwing the exception.

Using the Resource Exceptions

CResourceException is thrown in many places where system resources are compromised, as you saw in the mutex and semaphore example in Listing D.3. If you want to throw these exceptions yourself, use the corresponding AfxThrowResourceException() function.

Windows can't find or allocate the requested resource and doesn't give any more specific guidance; hence it has no other functions or attributes.

Using the File and Archive Exceptions

You already looked at CFileException in Listing D.5. This is probably one of the more sophisticated MFC exceptions because of the number of things that can go wrong with file access. You can throw these yourself using the AfxThrowFileException() function, which takes three parameters, one mandatory and the other two optional. The first mandatory parameter, cause, is a cause code for the exception. This will be placed in the file exception's m_cause member variable for interrogation in a catch block.

Table D.1 shows a list of the various cause codes. The second parameter, lOsError, can be used to specify an operating system error code to be placed in the file exception's m_lOsError member variable. This long value can help clarify an error in more detail by drawing on the operating system's own list of file access errors. The third parameter, strFileName, is placed into the file exception's m_strFileName member string variable to indicate the filename of the file that was being accessed when the error occurred.

TABLE D.1. THE CFileException m_cause CODES.

Cause Code Meaning
CFileException::none There was no error.
CFileException::generic No error code specified.
CFileException::tooManyOpenFiles Too many concurrently open files.
CFileException::fileNotFound Can't find the specified file.
CFileException::badPath The path name specified is invalid.
CFileException::invalidFile An attempt was made to use an invalid file handle.
CFileException::badSeek The seek operation failed.
CFileException::endOfFile The end of the file was reached.
CFileException::diskFull There is no spare disk space.
CFileException::hardIO A hardware error occurred.
CFileException::accessDenied Permissions deny access to the file.
CFileException::directoryFull The directory has too many files and can't add another.
CFileException::removeCurrentDir Can't remove the current working directory.
CFileException::lockViolation Can't lock an already locked region of the file.
CFileException::sharingViolation A shared region is locked or can't be shared.

There is also a ThrowOsError()static member function that throws and configures a file exception based on an operating system error code. You must pass ThrowOsError() the operating system error code as its first parameter and an optional filename as its second parameter. Another member function, ThrowErrno(), does the same thing but uses the UNIX-style errno error codes as its only parameter (from the Errno.h header file). Because these are static functions, you would use them with static scope to raise exceptions with lines like this:

CFileException::ThrowOsError(ERROR_BAD_PATHNAME);  Â// Invalid Path CFileException::ThrowErrno (ENOSPC); // Disk Full 

Another static member function, OsErrorToException(), automatically converts between operating system error codes and CFileException cause codes. By passing an OS error code, it will return the corresponding cause code. A corresponding function ErrnoToException() does the same when passed an errno error code.

When using archives with the CArchive class, you normally handle both CFileExceptions and CArchiveException cases in conjunction: Many of the CArchive operations are tied in with their underlying file and file access functions. CArchiveException has its own m_cause member to hold archive-specific cause codes, as shown in Table D.2. You can raise archive exceptions yourself through the AfxThrowArchiveException() function, which requires a cause code parameter and a lpszArchiveName string pointer for the archive object throwing the exception.

TABLE D.2. THE CArchiveException m_cause CODE VALUES.

Cause Code Meaning
CArchiveException::none No error occurred.
CArchiveException::generic The specific cause wasn't specified.
CArchiveException::badSchema The wrong version of an object was read.
CArchiveException::badClass The class of the object being read was unexpected.
CArchiveException::badIndex The file format is invalid.
CArchiveException::readOnly Attempt to write on an archive opened for loading.
CArchiveException::writeOnly Attempt to read on an archive opened for storing.
CArchiveException::endOfFile The end of the file was reached unexpectedly while reading.

Using the Database Exceptions

There are two database exception classes: CDBException is used for ODBC-based database access, and CDAOException is used for DAO-based database access. You can throw these exceptions yourself with the AfxThrowDBException() function, which needs three parameters. The first, nRetCode, specifies one of a huge number of database return codes to define the type of error (you should look in the ODBC documentation for these). The second parameter, pDB, is a pointer to the database associated with the exception, and the third parameter, hstmt, is an ODBC handle to the SQL statement object that was executed, causing the exception.

The RETCODE type is available from the CDBException object via its m_nRetCode member. You can also access a human-readable piece of error text from the m_strError member string and the error text returned from the ODBC driver itself in the m_strStateNativeOrigin member.

The CDAOException class has a corresponding AfxThrowDaoException() function that can throw the DAO exception objects. This function needs just two optional parameters. The first, nAfxDaoError, is a DAO-specific error code that indicates problems with DAO itself (see Table D.3). The second parameter is an OLE SCODE value that is the return code from a DAO-based OLE call (see the section "Using OLE Exceptions" for a definition of SCODEs).

TABLE D.3. DAO COMPONENT-SPECIFIC ERROR CODES FROM nAfxDaoError.

Error Code Meaning
NO_AFX_DAO_ERROR The exception was due to a DAO-specific problem; you should check the supplied CDaoErrorInfo object and SCODE value.
AFX_DAO_ERROR_ENGINE_INITIALIZATION The Microsoft Jet Engine database engine failed during initialization.
AFX_DAO_ERROR_DFX_BIND A DAO record set field exchange address is invalid.
AFX_DAO_ERROR_OBJECT_NOT_OPEN
The queried table hasn't been opened.

The CDAOException class has three member attributes: m_scode, which holds an asso-ciated OLE SCODE value with the attempted operation; or S_OK, if the OLE operation was successful. The m_nAfxDaoError member holds one of the DAO-specific values from Table D.3. The m_pErrorInfo is a pointer to a CDaoErrorInfo structure that holds an error code, descriptive error strings, and a help context ID that is defined like this:

struct CDaoErrorInfo {    long m_lErrorCode;    CString m_strSource;    CString m_strDescription;    CString m_strHelpFile;    long m_lHelpContext; }; 

By interrogating this structure, you can find most of the specific database error details pertaining to the DAO exception.

DAO exceptions can describe more than one error at a time, so you can use the GetErrorCount() member function to find out how many are being referenced. These other errors can then be obtained by passing the GetErrorInfo() function a zero-based index to the specific error. After calling GetErrorInfo() with a specific index in the range returned by the GetErrorCount() function, m_pErrorInfo will be updated to point to the specified object, and thus you can retrieve those values.

Using OLE Exceptions

There are two types of OLE exceptions, represented by two classes: the COleException class, which is normally used for server-side or OLE-specific operations, and the COleDispatchException class, which is used when dealing with client-side IDispatch-based operations such as calling ActiveX object functions.

The simpler of the two is the COleException class, which can be generated by calling the AfxThrowOleException() function passing an OLE SCODE value. An OLE SCODE is a 32-bit error code that is used to represent any kind of error arising from an OLE function.

This value would probably arise from the return code of a function call to a function on one of the interfaces of an OLE object. This SCODE value will then be stored in the exception's m_sc member for analysis from within a catch block.

There is also a Process() static member function that is passed an exception object and will turn that exception into an SCODE value to represent that exception.

The COleDispatchException class is used in conjunction with OLE IDispatch interfaces and is thrown by the AfxThrowOleDispatchException() function. This function has two forms, both with two mandatory parameters and an optional parameter. The first parameter for both forms is a wCode WORD value that is an application-specific error code. The second parameter is an lpszDescription string pointer in one form, or nDescriptionID for a UINT resource code; both types represent either a verbal string or a string resource code for a verbal string describing the error. The last optional parameter is a help context ID.

These values are then available as member variables of the COleDispatchException object via m_wCode, m_strDescription, and m_dwHelpContext. If a help context is specified and a help file available, the framework will fill in an m_strHelpFile string identifying the help file. The name of the application producing the error can also be sought from the m_strSource attribute.

If you raise this exception from an OLE object such as an ActiveX control, Visual Basic or any other application using the control or object will display these exception details.

Using the Not Supported Exception

The CNotSupportedException class represents exception objects that are generated when an unsupported MFC, operating system, or user-application-specific feature is requested. If you want to raise this exception, use AfxThrowNotSupportedException(), which doesn't required any parameters. There are also no extended members or functions associated with this exception--it just means unsupported.

Using the User Exception

You can use the CUserException class to generate application-specific exception objects. You might want to do this when your program is interacting with the user to halt the process should she choose a certain option. For example, when you are using the AppWizard, you can press Esc at any time to cancel the whole process. Microsoft might have used CUserException to do this by detecting the Esc key and then raising a user exception object.

This exception can be raised by a call to the AfxThrowUserException() function and then caught in the usual try and catch blocks. There are some places in the MFC where this exception is raised, such as during dialog box validation or if the file is too big for an edit view.

Generating Your Own Custom Exception Classes

You can derive your own exception classes from CException and add your specific extended functionality. Listing D.6 shows the class definition for such a custom exception class that extends the normal functionality by adding a m_strMessage CString variable to the exception, enabling you to specify your own message when constructing the exception.

LISTING D.6. LST29_6.CPP--CLASS DEFINITION FOR CCustomException IMPLEMENTED IN CustomException.h.

1:  // ** CustomException.h 2:  // ** Header file for CCustomException 3:  4:  class CCustomException : public CException 5:  { 6:      DECLARE_DYNAMIC(CCustomException); 7:  8:  public: 9:      CCustomException(CString strMessage); 10:  11:      CString m_strMessage; 12:  }; 

In Listing D.6 the class is implemented in its own CustomException.h header file and derives from CException in line 4. The DECLARE_DYNAMIC macro in line 6 supplies the MFC CObject-derived runtime class information required for you to decide the exception type in a catch-all catch block. The constructor definition in line 9 takes a CString strMessage parameter to let you create the custom exception with the message that will be stored in the m_strMessage CString variable declared in line 11.

The corresponding CCustomException class implementation is shown in Listing D.7.

LISTING D.7. LST29_7.CPP--IMPLEMENTATION OF THE CCustomException CLASS.

1:  // ** CustomException.cpp 2:  // ** Implementation for CCustomException exception 3:  4:  #include "stdafx.h" 5:  #include "CustomException.h" 6:  7:  IMPLEMENT_DYNAMIC(CCustomException,CException); 8:  9:  CCustomException::CCustomException(CString strMessage) 10:      : m_strMessage(strMessage) 11:  { 12:  } 

In Listing D.7 the usual header files are included, and the IMPLEMENT_DYNAMIC macro is used in line 7 to implement the MFC runtime class information functions. The constructor in line 9 takes the strMessage parameters and initializes the m_strMessage member variable with this string value in line 10.

You can then use the custom exception class in your application, as shown in Listing D.8.

LISTING D.8. LST29_8.CPP--USING THE NEW CCustomException CLASS.

1: try 2: { 3:     // ** Something goes wrong 4:     CCustomException* pCustomEx =  5:         new CCustomException("My custom error occured"); 6:     throw(pCustomEx); 7: }     8: catch(CCustomException* e) 9: { 10:     // ** Access the extended m_strMessage string 11:     AfxMessageBox(e->m_strMessage); 12:     e->Delete(); 13: } 

In Listing D.8 a new CCustomException object is created with the application-specific error text in lines 4 and 5 and is thrown in line 6. This is then caught by the catch keyword in line 8 and the custom information used by the message box in line 11. The exception is then deleted in line 12.

If you try this, remember that the implementation code must also have an #include for the CustomException.h header file to retrieve the class definition like this:

#include "CustomException.h"




© Copyright, Macmillan Computer Publishing. All rights reserved.



Sams teach yourself Visual C++ 6 in 21 days
Sams Teach Yourself C in 21 Days (6th Edition)
ISBN: 0672324482
EAN: 2147483647
Year: 1998
Pages: 31

Similar book on Amazon

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