- •Introduction
- •Saving Time with This Book
- •Conventions Used in This Book
- •Part II: Working with the Pre-Processor
- •Part III: Types
- •Part IV: Classes
- •Part V: Arrays and Templates
- •Part VI: Input and Output
- •Part VII: Using the Built-in Functionality
- •Part VIII: Utilities
- •Part IX: Debugging C++ Applications
- •Part X: The Scary (or Fun!) Stuff
- •Icons Used in This Book
- •Creating and Implementing an Encapsulated Class
- •Creating a Mailing-List Application
- •Testing the Mailing-List Application
- •Customizing a Class with Polymorphism
- •Testing the Virtual Function Code
- •Why Do the Destructors Work?
- •Delayed Construction
- •The cDate Class
- •Testing the cDate Class
- •Creating the Header File
- •Testing the Header File
- •The Assert Problem
- •Fixing the Assert Problem
- •Using the const Construct
- •Identifying the Errors
- •Fixing the Errors
- •Fixing What Went Wrong with the Macro
- •Using Macros Appropriately
- •Using the sizeof Function
- •Evaluating the Results
- •Using sizeof with Pointers
- •Implementing the Range Class
- •Testing the Range Class
- •Creating the Matrix Class
- •Matrix Operations
- •Multiplying a Matrix by a Scalar Value
- •Multiplying a Matrix by Scalar Values, Take 2
- •Testing the Matrix Class
- •Implementing the Enumeration Class
- •Testing the Enumeration Class
- •Implementing Structures
- •Interpreting the Output
- •Defining Constants
- •Testing the Constant Application
- •Using the const Keyword
- •Illustrating Scope
- •Interpreting the Output
- •Using Casts
- •Addressing the Compiler Problems
- •Testing the Changes
- •Implementing Member-Function Pointers
- •Updating Your Code with Member-Function Pointers
- •Testing the Member Pointer Code
- •Customizing Functions We Wrote Ourselves
- •Testing the Default Code
- •Fixing the Problem
- •Testing the Complete Class
- •Implementing Virtual Inheritance
- •Correcting the Code
- •Rules for Creating Overloaded Operators
- •Using Conversion Operators
- •Using Overloaded Operators
- •Testing the MyString Class
- •Rules for Implementing new and delete Handlers
- •Overloading new and delete Handlers
- •Testing the Memory Allocation Tracker
- •Implementing Properties
- •Testing the Property Class
- •Implementing Data Validation with Classes
- •Testing Your SSN Validator Class
- •Creating the Date Class
- •Testing the Date Class
- •Some Final Thoughts on the Date Class
- •Creating a Factory Class
- •Testing the Factory
- •Enhancing the Manager Class
- •Implementing Mix-In Classes
- •Testing the Template Classes
- •Implementing Function Templates
- •Creating Method Templates
- •Using the Vector Class
- •Creating the String Array Class
- •Working with Vector Algorithms
- •Creating an Array of Heterogeneous Objects
- •Creating the Column Class
- •Creating the Row Class
- •Creating the Spreadsheet Class
- •Testing Your Spreadsheet
- •Working with Streams
- •Testing the File-Reading Code
- •Creating the Test File
- •Reading Delimited Files
- •Testing the Code
- •Creating the XML Writer
- •Testing the XML Writer
- •Creating the Configuration-File Class
- •Setting Up Your Test File
- •Building the Language Files
- •Creating an Input Text File
- •Reading the International File
- •Testing the String Reader
- •Creating a Translator Class
- •Testing the Translator Class
- •Creating a Virtual File Class
- •Testing the Virtual File Class
- •Using the auto_ptr Class
- •Creating a Memory Safe Buffer Class
- •Throwing and Logging Exceptions
- •Dealing with Unhandled Exceptions
- •Re-throwing Exceptions
- •Creating the Wildcard Matching Class
- •Testing the Wildcard Matching Class
- •Creating the URL Codec Class
- •Testing the URL Codec Class
- •Testing the Rot13 Algorithm
- •Testing the XOR Algorithm
- •Implementing the transform Function to Convert Strings
- •Testing the String Conversions
- •Implementing the Serialization Interface
- •Creating the Buffer Class
- •Testing the Buffer Class
- •Creating the Multiple-Search-Path Class
- •Testing the Multiple-Search-Path Class
- •Testing the Flow Trace System
- •The assert Macro
- •Logging
- •Testing the Logger Class
- •Design by Contract
- •Adding Logging to the Application
- •Making Functions Inline
- •Avoiding Temporary Objects
- •Passing Objects by Reference
- •Choosing Initialization Instead of Assignment
- •Learning How Code Operates
- •Testing the Properties Class
- •Creating the Locking Mechanism
- •Testing the Locking Mechanism
- •Testing the File-Guardian Class
- •Implementing the Complex Class
- •Creating the Conversion Code
- •Testing the Conversion Code
- •A Sample Program
- •Componentizing
- •Restructuring
- •Specialization
- •Index
52 Avoiding Memory
Overwrites
Technique
Save Time By
Understanding memory overwrites
Preventing memory overwrites in arrays and allocated blocks
Protecting your data
Interpreting the output
Memory overwrites are a bane of the C++ programmer. A memory overwrite occurs when you write to an area outside an allocated block. This is bad, since you are writing to a block of memory
that you may or may not own, but certainly did not intend to modify. For example, if we have a C++ statement that says
char line[10];
and we then write something that says
line[11] = ‘a’;
we have overwritten a valid part of memory and might cause problems in the application later on. Unfortunately, memory overwrites like this are somewhat hard to track down without specialized tools and software. Alternatively, of course, you can simply avoid writing outside the valid bounds of an array or allocated block, but that is a bit easier said than done. The real problem here is that when we write to position 11 of the ten-block array, we have overwritten something in memory. That something could be another variable, it could be the return address for a function, or it could be a pointer that was previously allocated. None of these are good things. You need to stay within the memory allotment that you have requested for a memory block.
This technique looks at a way to use the C++ coding concepts to protect the data we are working with from being overwritten. This is a basic concept of encapsulation in C++: If you have data, you need to make sure that the data is used properly. That means not assigning values outside a valid range; it also means not overwriting bounds that have been established.
Creating a Memory Safe Buffer Class
The most common memory overwrite case occurs when strings are copied and assigned values. This error, which typically shows up with the use of the C functions strcpy and memcpy, occurs because the functions do not “know” how big a string is, so they cannot protect against
308 Technique 52: Avoiding Memory Overwrites
the string boundaries being overwritten. In order to fix this problem, we will create a class that understands its own boundaries and only allows the correct number of characters to be written to or copied into the object. In this way, we will create a generic buffer class that can be safely used to store strings in all of your applications, saving you time in implementing the class and in debugging memory overwrites. Here’s how:
1.
2.
In the code editor of your choice, create a new file to hold the code for the technique.
In this example, the file is named ch52.cpp, although you can use whatever you choose. This file will contain the source code for our classes.
Type the code from Listing 52-1 into your file.
Better yet, copy the code from the source file on this book’s companion Web site.
LISTING 52-1: THE BUFFER CLASS
#include <iostream> #include <fstream>
using namespace std;
class Buffer
{
private:
char *_buffer; long _length;
virtual void Init()
{
_buffer = NULL; _length = 0;
}
virtual void Clear()
{
if ( _buffer )
delete [] _buffer; Init();
}
public:
Buffer(void)
{
Init();
}
Buffer( const char *buffer, int length )
{
Init();
SetBuffer( buffer, length );
}
Buffer ( int length )
{
_buffer = new char[length]; _length = length;
set( 0 );
}
Buffer( const Buffer& aCopy )
Creating a Memory Safe Buffer Class |
309 |
{
Init();
SetBuffer( aCopy._buffer, aCopy._length );
}
virtual ~Buffer()
{
if ( _buffer )
delete [] _buffer;
}
Buffer operator=(const Buffer& aCopy )
{
Clear();
SetBuffer( aCopy._buffer, aCopy._length ); return *this;
}
Buffer operator=(const char *buffer )
{
Clear();
SetBuffer( buffer, strlen(buffer) ); return *this;
}
char& operator[]( int index )
{
if ( index < 0 || index >= _length ) |
|
4 |
||
throw |
“Buffer: Index out of range”; |
|
||
return _buffer[ index ]; |
|
|
||
} |
|
|
|
|
Buffer operator()(int st, int end) |
|
|
|
|
{ |
|
|
|
|
// Validate the pieces. |
|
|
|
|
if ( st < |
0 || st >= _length ) |
|
|
|
throw |
“Buffer: Start index |
out of range”; |
|
|
if ( end < 0 || end >= _length |
) |
|
|
|
throw |
“Buffer: End index out of range”; |
|
|
|
Buffer b( _buffer+st, end-st+1 |
); |
|
|
|
return b; |
|
|
|
|
} |
|
|
|
|
void set( char c )
{
for ( int i=0; i<_length; ++i ) _buffer[i] = c;
}
virtual void SetBuffer( const char *buffer, int length )
{
_buffer = new char[ length ]; for ( int i=0; i<length; ++i )
_buffer[i] = buffer[i]; _length = length;
}
void empty()
(continued)
310 Technique 52: Avoiding Memory Overwrites
LISTING 52-1 (continued)
{
set( 0 );
}
int Length() const
{
return _length;
}
};
ostream& operator <<( ostream& out, Buffer &b )
{
for ( int i=0; i<b.Length(); ++i ) out << b[i];
return out;
}
void func1()
{
char *buffer = new char[10];
strcpy( buffer, “This is a really long string”); cout << “Func1: [1]” << buffer << endl;
memset( buffer, 0, 11 );
cout << “Func1: [2]” << buffer << endl; strcpy( buffer, “This is a short string”); buffer[ 12] = 0;
cout << “Func1: [3]” << buffer << endl;
}
void func2()
{
Buffer b(10); try
{
b = “This is a really long string”; cout << “Func2: [1]” << b << endl; b.set( 0 );
cout << “Func2: [2]” << b << endl; b[12] = 0;
cout << “Func2: [3]” << b << endl;
}
catch ( ... )
{
printf(“Exception caught\n”);
}
}
int main()
{
func1();
func2();
}
1
2
3
Creating a Memory Safe Buffer Class |
311 |
The two functions shown above illustrate the memory overwrite problem, first as addressed by the standard C++ allocation of arrays (func1, shown at 1) and then using our Buffer class to handle the problem (func2, shown at 2). In each case, we allocate a character buffer of ten characters. In the func1 case, we then copy a string that is much longer than ten characters into the buffer. This causes a memory overwrite and could easily crash your program. However, because standard C++ has no way of detecting this, you will not see the problem immediately. In the second case, func2, we are using our Buffer class, which detects the problem as soon as you try to copy the larger string into the small allocated buffer and report it.
3. Save the source code in your code editor and close the editor application.
4. Compile the application, using your favorite compiler on your favorite operating system.
5. Run the application in the console window.
If you have done everything right, you should see something similar to this output from the application:
$ ./a.exe
Func1: [1]This is a re1 Func1: [2]
Func1: [3]This is a sh
Func2: [1]This is a really long string Func2: [2]
Func2: [3]
Note that you may see different output on different operating systems, depending on how the error occurs and whether or not it is visible. For func1, we see that the string does not get set to what we expect it to. You might see the string “This is a really long string” which would be even worse. In the func2 case, however, we never assign the strings that are too long, because an exception is thrown and the code properly handles it.
As you can see, in the “pure” C-style access code (shown at 1), there are no checks to see whether the programmer overwrites the buffer that is allocated. Obviously, in your own code it would not be quite as straightforward to find the problem. There do not appear to be any problems with the code here; everything prints out and continues processing properly. However, damage has been done to structures in memory — and these might or might not show up as program crashes. Worse, you might give the user incorrect output upon which they may make invalid decisions.
In the second case, C++ style (shown at 2), you can
see that the code protects against |
overwrites and |
|
|||
|
|
|
|
||
throws an exception (shown at the line marked |
|
4 |
|||
and caught at the line marked |
|
3 |
invalid |
||
|
|
) when an |
|
|
|
programmer to imme- |
|||||
write occurs. This allows the |
|
|
|
|
|
diately find where the problem happened and to fix it quickly and easily.