- •Table of Contents
- •Introduction
- •What Is C++?
- •Conventions Used in This Book
- •How This Book Is Organized
- •Part I: Introduction to C++ Programming
- •Part III: Introduction to Classes
- •Part IV: Inheritance
- •Part V: Optional Features
- •Part VI: The Part of Tens
- •Icons Used in This Book
- •Where to Go from Here
- •Grasping C++ Concepts
- •How do I program?
- •Installing Dev-C++
- •Setting the options
- •Creating Your First C++ Program
- •Entering the C++ code
- •Building your program
- •Executing Your Program
- •Dev-C++ is not Windows
- •Dev-C++ help
- •Reviewing the Annotated Program
- •Examining the framework for all C++ programs
- •Clarifying source code with comments
- •Basing programs on C++ statements
- •Writing declarations
- •Generating output
- •Calculating Expressions
- •Storing the results of expression
- •Declaring Variables
- •Declaring Different Types of Variables
- •Reviewing the limitations of integers in C++
- •Solving the truncation problem
- •Looking at the limits of floating-point numbers
- •Declaring Variable Types
- •Types of constants
- •Special characters
- •Are These Calculations Really Logical?
- •Mixed Mode Expressions
- •Performing Simple Binary Arithmetic
- •Decomposing Expressions
- •Determining the Order of Operations
- •Performing Unary Operations
- •Using Assignment Operators
- •Why Mess with Logical Operations?
- •Using the Simple Logical Operators
- •Storing logical values
- •Using logical int variables
- •Be careful performing logical operations on floating-point variables
- •Expressing Binary Numbers
- •The decimal number system
- •Other number systems
- •The binary number system
- •Performing Bitwise Logical Operations
- •The single bit operators
- •Using the bitwise operators
- •A simple test
- •Do something logical with logical calculations
- •Controlling Program Flow with the Branch Commands
- •Executing Loops in a Program
- •Looping while a condition is true
- •Using the for loop
- •Avoiding the dreaded infinite loop
- •Applying special loop controls
- •Nesting Control Commands
- •Switching to a Different Subject?
- •Writing and Using a Function
- •Divide and conquer
- •Understanding the Details of Functions
- •Understanding simple functions
- •Understanding functions with arguments
- •Overloading Function Names
- •Defining Function Prototypes
- •Variable Storage Types
- •Including Include Files
- •Considering the Need for Arrays
- •Using an array
- •Initializing an array
- •Accessing too far into an array
- •Using arrays
- •Defining and using arrays of arrays
- •Using Arrays of Characters
- •Creating an array of characters
- •Creating a string of characters
- •Manipulating Strings with Character
- •String-ing Along Variables
- •Variable Size
- •Address Operators
- •Using Pointer Variables
- •Comparing pointers and houses
- •Using different types of pointers
- •Passing Pointers to Functions
- •Passing by value
- •Passing pointer values
- •Passing by reference
- •Limiting scope
- •Examining the scope problem
- •Providing a solution using the heap
- •Defining Operations on Pointer Variables
- •Re-examining arrays in light of pointer variables
- •Applying operators to the address of an array
- •Expanding pointer operations to a string
- •Justifying pointer-based string manipulation
- •Applying operators to pointer types other than char
- •Contrasting a pointer with an array
- •Declaring and Using Arrays of Pointers
- •Utilizing arrays of character strings
- •Identifying Types of Errors
- •Choosing the WRITE Technique for the Problem
- •Catching bug #1
- •Catching bug #2
- •Calling for the Debugger
- •Defining the debugger
- •Finding commonalities among us
- •Running a test program
- •Single-stepping through a program
- •Abstracting Microwave Ovens
- •Preparing functional nachos
- •Preparing object-oriented nachos
- •Classifying Microwave Ovens
- •Why Classify?
- •Introducing the Class
- •The Format of a Class
- •Accessing the Members of a Class
- •Activating Our Objects
- •Simulating real-world objects
- •Why bother with member functions?
- •Adding a Member Function
- •Creating a member function
- •Naming class members
- •Calling a Member Function
- •Accessing a member function
- •Accessing other members from a member function
- •Defining a Member Function in the Class
- •Keeping a Member Function After Class
- •Overloading Member Functions
- •Defining Arrays of and Pointers to Simple Things
- •Declaring Arrays of Objects
- •Declaring Pointers to Objects
- •Dereferencing an object pointer
- •Pointing toward arrow pointers
- •Passing Objects to Functions
- •Calling a function with an object value
- •Calling a function with an object pointer
- •Calling a function by using the reference operator
- •Returning to the Heap
- •Comparing Pointers to References
- •Linking Up with Linked Lists
- •Performing other operations on a linked list
- •Hooking up with a LinkedListData program
- •A Ray of Hope: A List of Containers Linked to the C++ Library
- •Protecting Members
- •Why you need protected members
- •Discovering how protected members work
- •Protecting the internal state of the class
- •Using a class with a limited interface
- •Creating Objects
- •Using Constructors
- •Why you need constructors
- •Making constructors work
- •Dissecting a Destructor
- •Why you need the destructor
- •Working with destructors
- •Outfitting Constructors with Arguments
- •Justifying constructors
- •Using a constructor
- •Defaulting Default Constructors
- •Constructing Class Members
- •Constructing a complex data member
- •Constructing a constant data member
- •Constructing the Order of Construction
- •Local objects construct in order
- •Static objects construct only once
- •Global objects construct in no particular order
- •Members construct in the order in which they are declared
- •Destructors destruct in the reverse order of the constructors
- •Copying an Object
- •Why you need the copy constructor
- •Using the copy constructor
- •The Automatic Copy Constructor
- •Creating Shallow Copies versus Deep Copies
- •Avoiding temporaries, permanently
- •Defining a Static Member
- •Why you need static members
- •Using static members
- •Referencing static data members
- •Uses for static data members
- •Declaring Static Member Functions
- •What Is This About, Anyway?
- •Do I Need My Inheritance?
- •How Does a Class Inherit?
- •Using a subclass
- •Constructing a subclass
- •Destructing a subclass
- •Having a HAS_A Relationship
- •Why You Need Polymorphism
- •How Polymorphism Works
- •When Is a Virtual Function Not?
- •Considering Virtual Considerations
- •Factoring
- •Implementing Abstract Classes
- •Describing the abstract class concept
- •Making an honest class out of an abstract class
- •Passing abstract classes
- •Factoring C++ Source Code
- •Defining a namespace
- •Implementing Student
- •Implementing an application
- •Project file
- •Creating a project file under Dev-C++
- •Comparing Operators with Functions
- •Inserting a New Operator
- •Overloading the Assignment Operator
- •Protecting the Escape Hatch
- •How Stream I/O Works
- •The fstream Subclasses
- •Reading Directly from a Stream
- •Using the strstream Subclasses
- •Manipulating Manipulators
- •Justifying a New Error Mechanism?
- •Examining the Exception Mechanism
- •What Kinds of Things Can I Throw?
- •Adding Virtual Inheritance
- •Voicing a Contrary Opinion
- •Generalizing a Function into a Template
- •Template Classes
- •Do I Really Need Template Classes?
- •Tips for Using Templates
- •The string Container
- •The list Containers
- •Iterators
- •Using Maps
- •Enabling All Warnings and Error Messages
- •Insisting on Clean Compiles
- •Limiting the Visibility
- •Avoid Overloading Operators
- •Heap Handling
- •Using Exceptions to Handle Errors
- •Avoiding Multiple Inheritance
- •Customize Editor Settings to Your Taste
- •Highlight Matching Braces/Parentheses
- •Enable Exception Handling
- •Include Debugging Information (Sometimes)
- •Create a Project File
- •Customize the Help Menu
- •Reset Breakpoints after Editing the File
- •Avoid Illegal Filenames
- •Include #include Files in Your Project
- •Executing the Profiler
- •System Requirements
- •Using the CD with Microsoft Windows
- •Using the CD with Linux
- •Development tools
- •Program source code
- •Index
Chapter 23
A New Assignment Operator, Should You Decide to Accept It
In This Chapter
Introduction to the assignment operator
Why and when the assignment operator is necessary
Similarities between the assignment operator and the copy constructor
The intrinsic data types are those that are built in the language, such as int, float, double, and so on, plus the various pointer types. Chapter 3
and Chapter 4 describe the operators that C++ defines for the intrinsic data types. C++ enables the programmer to define the operators for classes that the programmer has created in addition to these intrinsic operators. This is called operator overloading.
Normally, operator overloading is optional and not attempted by beginning C++ programmers. A lot of experienced C++ programmers (including me) don’t think operator overloading is such a great idea either. However, you must figure out how to overload one operator: the assignment operator.
Comparing Operators with Functions
An operator is nothing more than a built-in function with a peculiar syntax. The following addition
a + b
could be understood as though it were written
operator+(a, b)
306 Part V: Optional Features
C++ gives each operator a function-style name. The functional name of an operator is the operator symbol preceded by the keyword operator and fol lowed by the appropriate argument types. For example, the + operator that adds an int to an int generating an int is called int operator+(int, int).
Any operator can be defined for a user-defined class. Thus, I could create a
Complex operator*(Complex&, Complex&) that would allow me to multi ply two objects of type Complex. The new operator may have the same semantics as the operator it overloads, but it doesn’t have to. The following rules apply when overloading operators:
The programmer cannot overload the ., ::, * (dereference), and & operators.
The programmer cannot invent new operators. You cannot invent the operation x $ y.
The format of the operators cannot be changed. Thus, you cannot define an operation %i because % is a binary operator.
The operator precedence cannot change. A program cannot force operator+ to be evaluated before operator*.
The operators cannot be redefined when applied to intrinsic types — you can’t change the meaning of 1 + 2. Existing operators can be over loaded only for newly defined types.
Overloading operators is one of those things that seems like a much better idea than it really is. In my experience, operator overloading introduces more problems than it solves, with two notable exceptions that are the subject of this chapter.
Inserting a New Operator
The insertion and extraction operators << and >> are nothing more than the left and right shift operators overloaded for a set of input/output classes. These definitions are found in the include file iostream (which is why every program includes that file). Thus, cout << “some string” becomes operator<<(cout, “some string”). Our old friends cout and cin are predefined objects that are tied to the console and keyboard, respectively.
I discuss this relationship in Chapter 24.
Chapter 23: A New Assignment Operator, Should You Decide to Accept It 307
Creating Shallow Copies
Is a Deep Problem
No matter what anyone may think of operator overloading, you will need to overload the assignment operator for many classes that you generate. C++ provides a default definition for operator=() for all classes. This default def inition performs a member-by-member copy. This works great for an intrinsic type like an int.
int i;
i = 10; // “member by member” copy
This same default definition is applied to user-defined classes. In the follow ing example, each member of source is copied over the corresponding member in destination.
void fn()
{
MyStruct source, destination; destination = source;
}
The default assignment operator works for most classes; however, it is not correct for classes that allocate resources, such as heap memory. The pro grammer must overload operator=() to handle the transfer of resources.
The assignment operator is much like the copy constructor. In use, the two look almost identical:
void fn(MyClass &mc) |
|
{ |
|
MyClass newMC(mc); |
// of course, this uses the |
|
// copy constructor |
MyClass newerMC = mc;// less obvious, this also invokes |
|
|
// the copy constructor |
MyClass newestMC; |
// this creates a default object |
newestMC = mc; |
// and then overwrites it with |
} |
// the argument passed |
|
|
|
|
The creation of newMC follows the standard pattern of creating a new object as a mirror image of the original using the copy constructor MyClass(MyClass&). Not so obvious is that newerMC is also created using the copy constructor.
308 Part V: Optional Features
MyClass a = b is just another way of writing MyClass a(b) — in particular, this declaration does not involve the assignment operator despite its appear ance. However, newestMC is created using the default (void) constructor and then overwritten by mc using the assignment operator.
Like the copy constructor, an assignment operator should be provided when ever a shallow copy is not appropriate. (Chapter 18 discusses shallow versus deep constructors.) A simple rule is to provide an assignment operator for classes that have a user-defined copy constructor.
The rule is this: The copy constructor is used when a new object is being cre ated. The assignment operator is used if the left-hand object already exists.
Overloading the Assignment Operator
The DemoAssignmentOperator program demonstrates how to provide an assignment operator. The program also includes a copy constructor to pro vide a comparison.
//DemoAssignmentOperator - demonstrate the assignment
// operator on a user defined class #include <cstdio>
#include <cstdlib> #include <iostream> #include <string> using namespace std;
//Name - a generic class used to demonstrate
//the assignment and copy constructor
//operators
class Name
{
public:
Name(char *pszN = 0)
{
copyName(pszN, “”);
}
Name(Name& s)
{
copyName(s.pszName, “ (copy)”);
}
~Name()
{
deleteName();
}
//assignment operator Name& operator=(Name& s)
{
Chapter 23: A New Assignment Operator, Should You Decide to Accept It 309
//delete existing stuff...
deleteName();
//...before replacing with new stuff copyName(s.pszName, “ (replaced)”); //return reference to existing object return *this;
}
// very simple access function char* out() { return pszName; }
protected:
void copyName(char* pszN, char* pszAdd); void deleteName();
char *pszName;
};
//copyName() - allocate heap memory to store name void Name::copyName(char* pszN, char* pszAdd)
{
pszName = 0; if (pszN)
{
pszName = new char[strlen(pszN) + strlen(pszAdd) + 1];
strcpy(pszName, pszN); strcat(pszName, pszAdd);
}
}
//deleteName() - return heap memory void Name::deleteName()
{
if (pszName)
{
delete pszName; pszName = 0;
}
}
int main(int nNumberofArgs, char* pszArgs[])
{
//create two objects Name n1(“Claudette”); Name n2(“Greg”);
cout << n1.out() << “ and “
<<n2.out() << “ are newly created objects”
<<endl;
//now make a copy of an object
Name n3(n1);
cout << n3.out() << “ is a copy of “ << n1.out() << endl;
310 Part V: Optional Features
//create a new object using the “=” format
//for accessing the copy constructor
Name n4 = n1;
cout << n4.out() << “ is also a copy of “
<<n1.out() << endl;
//overwrite n2 with n1 n2 = n1;
cout << n1.out() << “ was assigned to “
<<n2.out() << endl;
//wait until user is ready before terminating program
//to allow the user to see the program results system(“PAUSE”);
return 0;
}
The class Name contains a pointer to a person’s name, which it allocates from the heap in the constructor. The constructors and destructor for class Name are similar to those presented in Chapters 17 and 18. The constructor Name(char*) copies the name given it to the pszName data member. This constructor also serves as the default constructor. The copy constructor Name(&Name) copies the name of the object passed to the name stored in the current object by calling copyName(). The destructor returns the pszName character string to the heap by calling deleteName().
The assignment operator=() is a method of the class. It looks to all the world like a destructor immediately followed by a copy constructor. This is typical. Consider the assignment in the example n2 = n1. The object n2 already has a name associated with it (“Greg”). In the assignment, the memory that the orig inal name occupies must be returned to the heap by calling deleteName(), just like a destructor. The assignment operator then invokes copyName() to copy the new information into the object, much like a copy constructor.
The copy constructor did not need to call deleteName() because the object didn’t already exist. Therefore, memory had not already been assigned to the object when the constructor was invoked. The destructor didn’t perform the copy function.
There are two more details about the assignment operator. First, the return type of operator=() is Name&. Expressions involving the assignment opera tor have a value and a type, both of which are taken from the final value of the left-hand argument. In the following example, the value of operator=() is 2.0, and the type is double.
double d1, d2; void fn(double );
d1 = 2.0; // the value of this expression is 2.0