0% found this document useful (0 votes)
51 views

Case Study

The document discusses Samsung's experience using Parasoft's C++test tool to improve code quality on a mobile software project by enforcing coding standards. Key points: - Using C++test throughout development reduced coding standard violations compared to sporadic use of a previous tool. This directly improved code quality. - C++test was chosen for its customizable rules, integrated development environment support, unified C/C++ rules, and ability to check header files. - Over time, violations decreased by about 1/8th initially and then 6.6% for violations only impacting the current module. This showed direct benefits, while integrated development support provided indirect benefits to developers.

Uploaded by

stell66
Copyright
© Attribution Non-Commercial (BY-NC)
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
51 views

Case Study

The document discusses Samsung's experience using Parasoft's C++test tool to improve code quality on a mobile software project by enforcing coding standards. Key points: - Using C++test throughout development reduced coding standard violations compared to sporadic use of a previous tool. This directly improved code quality. - C++test was chosen for its customizable rules, integrated development environment support, unified C/C++ rules, and ability to check header files. - Over time, violations decreased by about 1/8th initially and then 6.6% for violations only impacting the current module. This showed direct benefits, while integrated development support provided indirect benefits to developers.

Uploaded by

stell66
Copyright
© Attribution Non-Commercial (BY-NC)
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
You are on page 1/ 33

CASE STUDY

Code Quality Improvement

To overcome critical software development challenges and reduce development


costs, the software engineering field has developed practices such as requirements
engineering, analysis and design technologies, process engineering, and the like.
Many of these practices apply to the implementation phase of development; for
example, coding standards, refactoring, inspection, and static analysis. Among
them, coding standards are the fundamental way to improve code readability, help
individual developers produce consistent code, and prevent error-prone coding
styles.

Samsung Electronics has focused on improving code quality by defining and


enforcing internal coding standards. We—the QA team—used a coding standards
conformance checker for this purpose, but we did not regularly apply this initial
checking tool to our software development. Because of tool weaknesses, we used it
only sporadically during final audits. As a result, we saw only minimal improvement
to code quality.

More recently, we evaluated Parasoft's C++test and applied it throughout an on-


going project ("Mobile"). In this article, we describe our experiences and share what
we learned. Here, a "coding standards item" is a guideline described in the
company's coding standards or conformance documents; a "coding standards rule"
(or just "rule") is a codified programming rule in an automated coding standards
tool.

Samsung Electronics is a major consumer electronics company. Although Samsung


started as a hardware-centric vendor, software is quickly becoming a major concern
for us, as it is for most other consumer electronics vendors. The Mobile project is a
Samsung Electronics C/C++ development project designed to develop a reusable
and extensible object-oriented software framework mainly targeting mobile devices.
Our QA team is an independent group responsible for testing software produced by
Samsung Electronics. We invest considerable time evaluating automated tools in an
effort to minimize routine work.

C/C++ Coding Standards

The Mobile project uses a set of project-specific coding standards derived from
general Samsung coding standards. The Mobile project standards could be rewritten
by taking into account the language variances and other development constraints.
For example, some compilers used in the Mobile project do not support exception
handling. Therefore, it is impossible to detect resource allocation failures while an
object's constructors are called. The Mobile project solves this problem by
employing the well-known two-phase object construction technique described in the
Mobile project coding standards: Divide an object initialization into the object
allocation phase and the resource allocation phase to return the exception as a
value (see Listing One).

class ResourceManager
{
ResourceManager(); // allocate only object
result Construct(); // allocate resources
// 'result' contains error
code
};
int main()
{
// Two phase construction
ResourceManager aObject;
if (aObject.Construct() == FAIL)
printf("Resource allocation is failed");
}
Listing One

Additionally, the Mobile project requires rigid conformance to coding standards. This
project mainly targets a software framework to be used by other developers; it
must be consistent and well organized to facilitate software development on this
framework. The more project constraints there are, the greater the necessity for an
automated tool. That's why the Mobile project adopted a coding standards checker.

C++test from Parasoft (www.parasoft.com) provides automated C/C++ unit


testing, as well as automated coding standards checking. We chose C++test as our
coding standards checker because it was the most effective solution for the
majority of our considerations.

One of C++test's distinct features is its GUI-based rules. Figure 1 shows the GUI
rule description for the rule "Each global variable must be initialized." Whenever a
"global variable" is found while parsing the source code, the rule evaluates logic
components. A violation is reported if any of the following conditions are not met:

 The detected global variable is an external declaration.


 It does not have an initializer.
 Its type is not an array or a class.
Figure 1: The GUI-based coding standard rules in Parasoft C++test.

The GUI interface simplifies rule creation. Most C++ code checkers require scripting
for rule creation; this is difficult and requires more C++ programming knowledge.

GUI-based rules can be easily understood and implemented because the available
conditions are shown graphically. Having GUI-based rules could compromise
extensibility because only predefined nodes and condition sets can be selected in a
GUI. However, C++test provides Python scripting support to ensure extensibility.

Code quality improvement

Choosing a Coding Standards Checker

Our selection criteria included product usability features, but not specific details
(rule selection, rule execution, and scalability). We established these criteria from
our experiences with previous coding standards checkers, feedback from project
development teams, and product evaluations.

Ability to modify rules. Most coding standards checkers include preimplemented


rules. Having built-in rules reduces the rule implementation effort. However, these
rules usually do not fully match our coding style. Moreover, because most rules are
implemented simply, false positives are common, making the results unreliable. We
needed an easy way to customize the rules to exclude exceptional cases, add new
rules, or modify the existing rules. Lint-like tools can check some coding standards
items, but lack rule customization features. Though some checkers provide
parameterization techniques for rule customization, more flexibility is required for
modifying rules in detail.

Ability to report coding standards compliance at various levels. Many coding


standards checkers support file-level reporting. However, for management
purposes, package- and project-level reporting is desirable. For example, project
managers often want to review coding standards violations by packages or by
projects to identify trends and prioritize the correction of coding standards
violations—especially when deadlines are approaching.

Ability to integrate with development environments. Many rule violations can


be easily corrected. For instance, violations like "Use tabs instead of spaces to
indent" can be corrected by simply replacing the spaces with tabs. In such
situations, having a coding standards checker with direct access to the violation
source (through tight integration with the development environment) significantly
reduces the time required for correction.

Additionally, as a project evolves, it includes more files and more "include/directive"


settings. If the checker does not run inside the IDE, the checker should create
project files by importing or synchronizing with IDE project files, such as makefiles,
dsp/dsw files, and so on. This saves time configuring the environment for running
the checker.

Ability to make unified rules for C/C++. Our major programming languages, C


and C++, have a similar structure—except that C++ has more features for object-
oriented and generic programming. Maintaining two different rules for the common
items in these languages would require extra resources.

Ability to recognize language variation. Compared to C, C++ has a short


history. Compiler vendors produced their own C++ compilers before the ISO C/C++
was established and research shows that many C++ implementations do not yet
perfectly support C++ ISO Standards (http://www.ddj.com/184405483). Since
coding standards checkers usually parse the source code, the supported language
variations are significant.

Ability to check unprecompiled header files. Some coding standards checkers


lack direct checking for header files. Instead, violations in header files are indirectly
reported by checking header file parts in the precompiled implementation files. In
this case, violations related to preprocessor directives and comments in header
files, which usually contain important information used by other developers, are
ignored. Therefore, direct checking of header files is desired.

Experiences Using the Checker

Applying a coding standards checker reduces coding standards violations during


implementation. However, the number of violations detected and eliminated
depends on the characteristics of the target project and the quality of the coding
standards rules. We found that each project has similar overall trends but many
different details that impact coding standards checking. Thus, the following results
should be viewed as a trend, not in a deterministic manner.

Direct benefits refer to how reducing the number of violations improved code
quality. Indirect benefits are other unexpected developer benefits.

Figure 2 shows the overall trend for the number of coding standards violations. To
eliminate the effects of rule evolution, we use the latest rule set for all checking.
After a relatively steady number of violations between Nov-04 and Jan-05, there
was a violation reduction of approximately 1/8 in Feb-05. There was an unexpected
increase in violations between Feb-05 and Mar-05, but this was acceptable because
the target project was evolving continuously.
Figure 2: The overall number of violations/KLOC.

Figure 3 shows the trend for the number of violations whose corrections impact only
the current module. Violations decreased 6.6 percent since the date of checker
deployment. This data does not cover comment rules, which usually don't have
large change impacts but do require significant change efforts.

Figure 3: The number of violations with small change impacts/KLOC.

Figure 4 shows the trend for the number of violations whose corrections impact
more than the current module. Violations decreased 19.6 percent since the date of
checker deployment.

Figure 4: The number of violations with large change impacts/KLOC.


One of the unexpected benefits of coding standards checking was the developer
education it promoted. Beginners, and even some experienced developers, initially
did not understand the meaning and importance of some coding standards items.
For example, consider the coding standards item "Prefer initializations to
assignments," which is known as an efficient way to initialize member variables in a
constructor (see Effective C++, by Scott Meyers; Addison-Wesley, 1992). Some
developers did not understand why initializing members in a constructor's
initialization list is better than assigning the initial values in a constructor's body.
They discussed these issues when reviewing the checker's results. This eventually
helped the developers understand C++ features thoroughly, and demonstrates how
coding standards checking helps developers learn from their mistakes.

Another indirect benefit was the removal of unrealistic coding standards items.
When we applied the coding standards rules to the Mobile project, we found that
most developers did not obey the rule "Do not exceed 80 columns per line." This
rule was developed because some old development environments had 80-column
displays. We examined our development environments and concluded that in our
situation, such limited development environments are rarely used and longer
columns are preferred. Consequently, we changed the column limitation from 80 to
150 columns per line. This kind of coding standards modification has improved
developer buy-in to coding standards compliance.

Lessons Learned

Until recently, our checking involved only the QA team checking coding standards
compliance at the project level. This was effective for tracking defect trends and
maintaining the rule set. However, there were several drawbacks.

First, the reported violation sources do not always match the code in the
development environment. Developers don't usually work on a centralized source-
code repository; they write and modify code in a private area, then copy or check-
in the source code to the centralized repository. If the development source code
differs from the code that QA is testing, it can be difficult for developers to identify
and repair the source of reported violations.

Second, developers prefer to immediately verify that a violation correction has


eliminated the problem.

For these reasons, we decided to have the coding standards checker run by
developers as well as the QA team.

Developers who do not carefully follow standards may produce code with many
violations, and are often reluctant to correct the violations. This is especially
problematic with violations from identifier- or design-related rules, which are
difficult to correct in later development phases because making such corrections
can have a large, project-wide impact. We recommend checking coding standards
conformance from the early coding phase so that the violated code is corrected
before it is propagated out of the module.

One reason why the coding standards checking effort with our previous tool failed
was the lack of rule maintenance at the organization level.

After the initial rule set is established, rules should be customized continuously
because some rules might not apply to particular projects and development style
might vary across development domains. A rule set should be maintained
throughout each project's operations as the project constraints evolve.

Automatic coding standards checking should complement code reviews. In our


development process, code review is mandatory because it effectively catches
developers' logical errors and mistakes. However, developers nevertheless skip
code reviews due to schedule constraints. Our experiences and research show that
some developers are easily distracted by style issues and focus on them during
code reviews (D. Kelly and T. Shepard, "Qualitative Observations from Software
Code Inspection Experiments"; CASCON, 2002). Consequently, some developers
view the code review process as being time-consuming with little benefit, and then
skip it. This can be prevented by removing coding standards violations with
automatic coding standards checking before the review, which allows the review to
focus on finding logical and serious errors. Moreover, because automatic checking
covers only about 50 percent of the items in our coding standards (some items are
complicated to implement with an automated tool), code reviews are needed to
check the remaining items.

Conclusion

We have applied the checker to several projects in our organization and achieved
significant code-quality improvements in each case. We plan to apply it to
additional projects and analyze the effects on code quality.
Case Study: Histograms
Suppose that you are presented with a large body of text. Is it possible to tell interesting things
about the authors from the typical length of their sentences, their words, and so on? Some
literary detectives think so; in this case study, your job is to collect these statistics and present
them as a bar graph, otherwise known as a histogram. You already have one tool for the job—
C++ input streams, which read in strings separated by spaces. C++ input streams are well
suited to this task because they are not line based; text is naturally parsed into tokens, which
are usually (but not always) words. For example, "He said, let's go." is read as {"He"
"said," "let's" "go."}. So when you read in a token, you need to look at the end of each
word for punctuation. The basic strategy is to read each word and punctuation character and to
increment word and sentence frequency tables. These tables don't have to be very long, so
arrays will work well.

First, the arrays need to be initialized to zero (never assume that this is already true!). The
standard algorithm fill_n() is easier to use in this case than an explicit for loop:

const int MAXWORD = 30, MAXSENTENCE = 120;

int word_lengths[MAXWORD];
int sentence_lengths[MAXSENTENCE];

void init_arrays()
{
fill_n(word_lengths,MAXWORD,0);
fill_n(sentence_lengths,MAXSENTENCE,0);
}

Here is collect_file_stats(), which reads each word of the text using >>. Then it looks at
the last character of the word, which is word[len-1] because strings are like arrays. If this
character is punctuation, it must be removed from the string. The word length frequency table
can be updated with a single statement: ++word_lengths[len]. The next few lines count
words in sentences.

bool collect_file_stats(string file, int word_lengths[],


int sentence_lengths[])
{
ifstream in(file.c_str());
if (! in) return false; // sorry, can't open file!
int wcount = 0; // will count no. of words in each sentence
string word;
while (in >> word) {
int len = word.length();
char ech = word[len-1]; // last character of word
if (ech == `.' || ech == `,' || ech == `;') {
word = word.substr(0,len-1); // chop off punctuation
—len;
}
++word_lengths[len];
++wcount;
if (ech == `.') {
++sentence_lengths[wcount];
wcount = 0;
}
} // end of while(...)
} // end of collect_file_stats
;> init_arrays();
;> collect_file_stats("chap1.txt",word_lengths,sentence_lengths);
;> show_arr(word_lengths,15);
0 266 854 866 806 472 312 316 232 106 104 56 62 30 4
;> show_arr(sentence_lengths,15);
0 4 0 10 0 4 12 8 6 6 4 4 6 4 8

The result of using this code is two frequency


tables, word_lengths and sentence_lengths.word_lengths[1] shows the number of words
with one character, word_lengths[2] shows the number of words with two characters, up to
about 15 or so characters. I have used the useful function show_arr() from earlier in the
chapter to show you the values when the first chapter of The Hound of the Baskervilles is
analyzed.
Two Approaches to Histograms
The easiest way to print out a histogram is to print it on its side. You can fill strings or character
arrays with a chosen character, but the easiest method is just to write a character out to cout.
(You can make the bars thicker by nesting another loop inside the i loop.)

void histo_1 (int values[], int sz)


{
for(int i = 0; i < sz; i++) {
for(int k = 0; k < values[i]; k++) cout << `*';
cout << endl;
}
}
;> histo_1(sentence_lengths,10);
0
1 ****
2
3 **********
4
5 ****
6 ************
7 ********
8 ******
9 ******

Note that this method is just not going to work with word_lengths, since the values are very
large. These values need to be scaled by choosing a maximum value for the display (say 60
characters across) and generating an array within those bounds. The maximum value of the
input data is found as before, and the input data is multiplied by the scaling factor, which is just
the ratio of the desired maximum value to the actual maximum value.

void scale_data(int in_data[], int out_data[], int sz, int imax)


{
int maxval = *max_element(in_data,in_data+sz);
for(int i = 0; i < sz; i++)
out_data[i] = (imax*in_data[i])/maxval;
}

;> int scaled_lengths[16];


;> scale_data(word_lengths,scaled_lengths,15,60);
;> show_arr(scaled_lengths,15);
0 18 59 60 55 32 21 21 16 7 7 3 4 2 0
;> histo_1(scaled_lengths,15);
0
1 ******************
2 ***********************************************************
3 ************************************************************
4 *******************************************************
5 ********************************
6 *********************
7 *********************
8 ****************
9 *******
10 *******
11 ***
12 ****
13 **
14
;>

How can you create a histogram that is upright? You start with the maximum value as a level,
and then you run along the array, comparing values to the level. If the values are greater than or
equal to the level, you write out a string, and if they are less than the level, you write out spaces:

void histo_2 (int values[], int sz)


{
int level = max_arr(values,sz); // std method to do this?
for(; level > 0; level—) { // leave out 1st part of for-loop
for(int i = 0; i < sz; i++)
if (values[i] >= level) cout << "**** ";
else cout << " ";
cout << endl;
\ }
}
;> scale_data(word_lengths,scaled_lengths,10,20);
;> histo_2(scaled_lengths,10);
****
**** ****
**** **** ****
**** **** ****
**** **** ****
**** **** ****
**** **** ****
**** **** ****
**** **** ****
**** **** ****
**** **** **** ****
**** **** **** ****
**** **** **** ****
**** **** **** **** **** ****
**** **** **** **** **** **** ****
**** **** **** **** **** **** **** ****
**** **** **** **** **** **** **** ****
**** **** **** **** **** **** **** ****
**** **** **** **** **** **** **** **** ****
**** **** **** **** **** **** **** **** ****
**** **** **** **** **** **** **** **** **** ****

This example hardly exploits the supercharged graphics of modern machines, of course. You
can also create histograms by using the Turtle Graphics interface of UnderC for Windows.
Appendix B, "A Short Library Reference" gives you all the information you need about using
Turtle Graphics for now. I will be discussing it further in Chapter 4, "Programs and Libraries."
Containers
Arrays have several disadvantages. Earlier in this chapter we discussed their lack of size
information, which means you must use two arguments to pass an array to a function. It also
means that you cannot check an array index at runtime to see whether it's out of bounds. It is
easy to crash a program by using the wrong index; what is perhaps worse—because the
program seems to work—is that memory can be silently overwritten. All C programmers will tell
you that these are some of the worst bugs to solve. Built-in arrays are also inflexible in that they
have a fixed size that must be a constant. Although it is very fast to access array data randomly,
insertions and deletions are slow.

The standard library defines a number of container types. A container holds a number of
elements, like an array, but it is more intelligent. In particular, it has size information and is
resizable. We will discuss three kinds of standard containers in the following sections: vector,
which is used like a built-in array, but is resizeable; list, which is easy to insert elements into;
and map, which is an associative array. That is, it associates values of one type with another
type.

Resizable Arrays: std::vector
You use the vector container type the same way you use an ordinary array, but a vector can
grow when required. The following is a vector of 10 ints:

;> vector<int> vi(10);


;> for(int i = 0; i < 10; i++) vi[i] = i+1;
;> vi[5];
(int&) 6
;> vector<int> v2;
;> v2.size();
(int) 0
;> v2 = vi;
;> v2.size();
(int) 10

vector is called a parameterized type. The type in angle brackets (<>) that must follow the
name is called the type parameter. vector is called parameterized because each specific type
(vector<int>, vector<double>, vector<string>, and so on) is built on a specific base type,
like a built-in array. In Chapter 10, "Templates," I will show you how you can build your own
parameterized types, but for now it's only important that you know how to use them.

vi is a perfectly ordinary object that behaves like an array. That is, you can access any element
very quickly using an index; this is called random access. Please note that the initial size (what
we would call the array dimension) is in parentheses, not in square brackets. If there is no size
(as with v2) then the vector is initially of size zero. It keeps its own size information, which you
can access by using the size() method. You cannot initialize a vector in the same way as an
array (with a list of numbers), but you can assign them to each other. The statement v2 =
vi actually causes all the elements of vi to be copied into v2. A vector variable behaves just
like an ordinary variable, in fact. You can pass the vi vector as an argument to a function, and
you won't need to pass the size, as in the following example:

void show_vect(vector<int> v)
{
for(int i = 0; i < v.size(); i++) cout << v[i] << ` `;
cout << endl;
}
;> show_vect(vi);
1 2 3 4 5 6 7 8 9 10

You can resize the vector vi at any point. In the following example the elements of vi are
initialized to random numbers between 0 and 99. (n % 100 will always be in that range). vi is
then resized to 15 elements:

;> for(int i = 0; i < 10; i++) vi[i] = rand() % 100;


;> show_vect(vi);
41 67 34 0 69 24 78 58 62 64
;> vi.resize(15);
show_vect(vi);
41 67 34 0 69 24 78 58 62 64 0 0 0 0 0

You can resize the vi vector without destroying its values, but this can sometimes be quite a
costly operation because the old values must be copied. Note that vectors are passed to
functions by value, not by reference. Remember that passing by value involves making a copy
of the whole object. In the following example, the function try_change() tries to modify its
argument, but doesn't succeed. Earlier in this chapter ("Passing Arrays to Functions") you saw a
similar example with built-in arrays, which did modify the first element of its array argument.

;> vector<int> v2 = vi;


;> v2.size();
(int) 15
;> v2[0];
(int&) 41
;> void try_change(vector<int> v) { v[0] = 747; }
;> try_change(v2);
;> v2[0];
(int&) 41

At this point, you may be tired of typing vector<int>. Fortunately, C++ provides a shortcut.
You can create an alias for a type by using the typedef statement. The form of
the typedef statement is just like the form of a declaration, except the declared names are not
variables but type aliases. You can use these typedef names wherever you would have used
the original type. Here are some examples of how to use typedef, showing how the resulting
aliases can be used instead of the full type:

;> typedef unsigned int uint;


;> typedef unsigned char uchar;
;> typedef vector<int> VI;
;> typedef vector<double> DV;
;> uint arr[10];
;> DV d1(10),d2(10);
;> VI v1,v2;
;> int get(VI v, int i) { return v[i]; }

Think of typedef names as the equivalent of constants. Symbolic constants make typing easier
(typing pi to 12 decimal places each time is tedious) and make later changes easier because
there is only one statement to be changed. In the same way, if I consistently use VI throughout
a large program, then the code becomes easier to type (and to read). If I later decide to use
some other type instead of vector<int>, then that changes becomes straightforward.

As you have learned, passing a vector (or any standard container) to a function involves a full
copy of that vector. This can make a noticeable difference to a program's performance if the
function is called enough times. You can mark an argument so that it is passed by reference, by
using the address operator (&). You can further insist that it remains constant, as we did earlier
in the chapter for arrays and as shown in the following example:

void by_reference (vector<int>& vi)


{ vi[0] = 0; }
void no_modify (const vector<int>& vi)
{ cout << vi[0] << endl; }

Generally, you should pass vectors and other containers by reference; if you need to make a
copy, it's best to do it explicitly in the function and make such reference arguments const,
unless you are going to modify the vector. When experienced programmers see something
passed by reference, they assume that someone is going to try to change it. So the preferred
way of passing containers is by const reference, as in the preceding example. You can always
use the typedefnames to make things look easier on the eye, as shown here:

int passing_a_vector (const VI& vi) { return vi[0]; }

The standard string is very much like a vector<char>, and it is considered an "almost
container." Strings can also be indexed like arrays, so if s is a string, then s[0] would be the
first character (not substring), and s[s.size()-1] would be the last character.

Linked Lists: std::list
vectors have strengths and weaknesses. As you have seen, any insertion requires moving
elements, so if a vector contained several million elements (and why not?), insertion could be
unacceptably slow. Although vectors grow automatically, that process can also be slow
because it involves copying all the elements in the vector.

Lists are also sequences of elements, but they are not accessed randomly, and they are
therefore not like arrays. Starting with an empty list, you append values by using push_back(),
and you insert values at the front of the list by using push_front(). back() and front() give
the current values at each end. To remove values from the ends, you
use pop_front() and pop_back(). The following is an example of creating a list:

;> list<int> li;


;> li.push_back(10);
;> li.push_front(20);
;> li.back();
(int) 10
;> li.front();
(int) 20
;> li.size();
(int) 2
;> li.pop_back();
;> li.back();
(int) 20

You can remove from a list all items with a certain value. After the remove operation, the list
contains only "two":

;> list<string> ls;


;> ls.push_back("one"); ls.push_back("two"); ls.push_back("one");
;> ls.remove("one");

Associative Arrays: std::map
In mathematics, a map takes members of some input set (say 0..n-1) to another set of values;
a simple example would be an array. The standard C++ map is not restricted to contiguous (that
is, consecutive) values like an array or a vector, however. Here is a simple map from int to int:

;> map<int,int> mii;


;> mii[4] = 2;
(int&) 2;
;> mii[88] = 7;
(int&) 7
;> mii.size();
(int) 2
;> mii[4];
(int&) 2
;> mii[2];
(int&) 0

You access maps the same way you access arrays, but the key values used in the subscripting
don't have to cover the full range. To create the map in the preceding example by using arrays,
you would need at least 89 elements in the array, whereas the map needs only 2. If you consider
a mapof phone numbers and contact names, you can see that an ordinary array is not an
option. maps become very interesting when the key values are non-integers; we say that they
associate strings with values, and hence they are often called associative arrays. Typically,
a map is about as fast as a binary search.

;> map<int,string> mis;


;> mis[6554321] = "James";
(string&) "James";
;> mis.size();
(int) 1
;> map<string,int> msi;
;> msi["James"] = 6554321;
(int&) 6554321
;> msi.size();
(int) 1
;> msi["Jane"];
(int&) 0
;> msi.size();
(int) 2

Something that is important to note about maps is that they get bigger if you are continuously
querying them with different keys. Say you are reading in a large body of text, looking for a few
words. If you are using array notation, each time you look up a value in the map, the map gets
another entry. So a map of a few entries can end up with thousands of entries, most of which
are trivial. Fortunately, there is a straightforward way around this: You can use the
map's find()method. First, you can define some typedef names to simplify things:

;> typedef map<string,int> MSI;


;> typedef MSI::iterator IMSI;
;> IMSI ii = msi.find("Fred");
;> ii == msi.end();
(bool) true

The find() method returns a map iterator, which either refers to an existing item or is equal to
the end of the map.

Maps are some of the most entertaining goodies in the standard library. They are useful tools,
and you can use them to write very powerful routines in just a few lines. Here is a function that
counts word frequencies in a large body of text (testing this case, the first chapter of Conan
Doyle's Hound of the Baskervilles, courtesy of the Gutenberg Project):

int word_freq(string file, MSI& msi) {


ifstream in(file.c_str());
string word;
while (in >> word) msi[word]++;
return msi.size();
}
;> word_freq("chap1.txt",msi);
(int) 945
;> msi["the"];
(int&) 94

This example uses the shorthand for opening a file, and it assumes that the file will always exist.
The real fun happens on the fourth line in this example. For each word in the file, you increment
the map's value. If a word is not originally present in the map, msi[word] is zero, and a new
entry is created. Otherwise, the existing value is incremented. Eventually, msi will contain all
unique words, along with the number of times they have been used. This example is the first bit
of code in this book that really exercises a machine. The UnderC implementation is too slow for
analyzing large amounts of text, but Chapter 4, "Programs and Libraries," shows how to set up
a C++ program that can be compiled into an executable program.

Stacks and Queues


Sometimes it's useful to build up a vector element by element. This works exactly like adding
to the end of a list. You can add new elements at the end with push_back(); back() gives
the value of the last value of the vector, and pop_back() removes the last value, decrementing
the size.

;> typedef vector<int> VI;


;> VI vs;
;> vs.push_back(10);
;> vs.push_back(20);
;> show_vect(vs);
10 20
;> vs.size();
(int) 2
;> vs.back();
(int) 20
;> vs.pop_back();
;> vs.back();
(int) 10
;> vs.size();
(int) 1

void read_some_numbers(VI& vi, string file) {


int val;
ifstream in(file.c_str());
while (in >> val) vi.push_back(val);
}

Often you are given input without any idea of how many numbers to expect. If you
usepush_back(),the vector automatically increases in size to accommodate the new numbers.
So the function read_some_numbers() will read an arbitrary number of integers and add them
to the end of the vector.

There is no push_front() method because that would potentially be an expensive operation. If


you really need to do it, you can use vi.insert(vi.begin(),val).

The operations push and pop define a stack. A stack is similar to the spring-loaded device often


used for dispensing plates in cafeterias. As you remove plates from the top of the device (that
is, "pop the stack"), more plates rise and are ready to be taken. You can also push plates onto
the pile. A stack operates in first-in, last-out (FILO) fashion: if you push 10, 20, and 30, then you
will pop 30, 20, and 10. Stacks are one of the basic workhorses of computer science, and you
see them all over the place. A common use is to save a value, as in the following example:

void push(int val) {


vi.push_back(val);
}
int pop() {
int val = vi.back();
vi.pop_back();
return val;
}
;> int val = 1;
;> push(val); // save val;
;> val = 42; // modify val;
(int) 42
.... do things with val.......
;> val = pop(); // restore val;

A queue, on the other hand, operates in first-in, first-out (FIFO) fashion, similarly to a line of
waiting people, who are served in first come, first served order. A vector is not a good
implementation of a queue because inserting at the front causes all entries to shuffle
along. lists, however, are good candidates for queuing. You add an item to a queue by
using push_front(), and you take an item off the end by using pop_back(). Queues are
commonly used in data communications, where you can have data coming in faster than it can
be processed. So incoming data is buffered—that is, kept in a queue until it is used or the buffer
overflows. The good thing about a list is that it never overflows, although it can underflow,
when someone tries to take something off an empty queue; therefore, it is important to check
size. Graphical user interface systems such as Windows typically maintain a message queue,
which contains all the user's input. So it is possible to type faster than a program can process
keystrokes.
CASE STUDY

1.8 Case Study: Array Class

Pointer-based arrays have a number of problems. For example, a program can easily “walk off”
either end of an array, because C++ does not check whether subscripts fall outside the range of
an array (the programmer can still do this explicitly though). Arrays of sizen must number their
elements 0, ..., n – 1; alternate subscript ranges are not allowed. An entire non-char array
cannot be input or output at once; each array element must be read or written individually. Two
arrays cannot be meaningfully compared with equality operators or relational operators (because
the array names are simply pointers to where the arrays begin in memory and, of course, two
arrays will always be at different memory locations). When an array is passed to a general-
purpose function designed to handle arrays of any size, the size of the array must be passed as
an additional argument. One array cannot be assigned to another with the assignment
operator(s) (because array names are const pointers and a constant pointer cannot be used on
the left side of an assignment operator). These and other capabilities certainly seem like
“naturals” for dealing with arrays, but pointer-based arrays do not provide such capabilities.
However, C++ does provide the means to implement such array capabilities through the use of
classes and operator overloading.

In this example, we create a powerful array class that performs range checking to ensure that
subscripts remain within the bounds of the Array. The class allows one array object to be
assigned to another with the assignment operator. Objects of the Array class know their size, so
the size does not need to be passed separately as an argument when passing an Array to a
function. Entire Arrays can be input or output with the stream extraction and stream insertion
operators, respectively. Array comparisons can be made with the equality operators == and !=.

This example will sharpen your appreciation of data abstraction. You will probably want to
suggest other enhancements to this Arrayclass. Class development is an interesting, creative
and intellectually challenging activity—always with the goal of “crafting valuable classes.”

11.8 Case Study: Array Class (Continued)

The program of Figs. 11.6—11.8 demonstrates class Array and its overloaded operators. First
we walk through main (Fig. 11.8). Then we consider the class definition (Fig. 11.6) and each of
the class’s member-function and friend-function definitions (Fig. 11.7).

1 // Fig. 11.6: Array.h


2 // Array class for storing arrays of integers.
3 #ifndef ARRAY_H
4 #define ARRAY_H
5
6 #include <iostream>
7 using std::ostream;
8 using std::istream;
9
10 class Array
11 {
12 friend ostream &operator<<( ostream &, const Array & );
13 friend istream &operator>>( istream &, Array & );
14 public:
15 Array( int = 10 ); // default constructor
16 Array( const Array & ); // copy constructor
17 ~Array(); // destructor
18 int getSize() const; // return size
19
20 const Array &operator=( const Array & ); // assignment operator
21 bool operator==( const Array & ) const; // equality operator
22
23 // inequality operator; returns opposite of == operator
24 bool operator!=( const Array &right ) const
25 {
26 return ! ( *this == right ); // invokes Array::operator==
27 } // end function operator!=
28
29 // subscript operator for non-const objects returns modifiable lvalue
30 int &operator[]( int );
31
32 // subscript operator for const objects returns rvalue
33 int operator[]( int ) const;
34 private:
35 int size; // pointer-based array size
36 int *ptr; // pointer to first element of pointer-based array
37 }; // end class Array
38
39 #endif

Fig. 11.6 Array class definition with overloaded operators.

1 // Fig 11.7: Array.cpp


2 // Member-function definitions for class Array
3 #include <iostream>
4 using std::cerr;
5 using std::cout;
6 using std::cin;
7 using std::endl;
8
9 #include <iomanip>
10 using std::setw;
11
12 #include <cstdlib> // exit function prototype
13 using std::exit;
14
15 #include "Array.h" // Array class definition
16
17 // default constructor for class Array (default size 10)
18 Array::Array( int arraySize )
19 {
20 size = ( arraySize > 0 ? arraySize : 10 ); // validate arraySize
21 ptr = new int[ size ]; // create space for pointer-based array
22
23 for ( int i = 0; i < size; i++ )
24 ptr[ i ] = 0; // set pointer-based array element
25 } // end Array default constructor
26
27 // copy constructor for class Array;
28 // must receive a reference to prevent infinite recursion
29 Array::Array( const Array &arrayToCopy )
30 : size( arrayToCopy.size )
31 {
32 ptr = new int[ size ]; // create space for pointer-based array
33
34 for ( int i = 0; i < size; i++ )
35 ptr[ i ] = arrayToCopy.ptr[ i ]; // copy into object
36 } // end Array copy constructor
37
38 // destructor for class Array
39 Array::~Array()
40 {
41 delete [] ptr; // release pointer-based array space
42 } // end destructor
43
44 // return number of elements of Array
45 int Array::getSize() const
46 {
47 return size; // number of elements in Array
48 } // end function getSize
49
50 // overloaded assignment operator;
51 // const return avoids: ( a1 = a2 ) = a3
52 const Array &Array::operator=( const Array &right )
53 {
54 if ( &right != this ) // avoid self-assignment
55 {
56 // for Arrays of different sizes, deallocate original
57 // left-side array, then allocate new left-side array
58 if ( size != right.size )
59 {
60 delete [] ptr; // release space
61 size = right.size; // resize this object
62 ptr = new int[ size ]; // create space for array copy
63 } // end inner if
64
65 for ( int i = 0; i < size; i++ )
66 ptr[ i ] = right.ptr[ i ]; // copy array into object
67 } // end outer if
68
69 return *this; // enables x = y = z, for example
70 } // end function operator=
71
72 // determine if two Arrays are equal and
73 // return true, otherwise return false
74 bool Array::operator==( const Array &right ) const
75 {
76 if ( size != right.size )
77 return false; // arrays of different number of elements
78
79 for ( int i = 0; i < size; i++ )
80 if ( ptr[ i ] != right.ptr[ i ] )
81 return false; // Array contents are not equal
82
83 return true; // Arrays are equal
84 } // end function operator==
85
86 // overloaded subscript operator for non-const Arrays;
87 // reference return creates a modifiable lvalue
88 int &Array::operator[]( int subscript )
89 {
90 // check for subscript out-of-range error
91 if ( subscript < 0 || subscript >= size )
92 {
93 cerr << "\nError: Subscript " << subscript
94 << " out of range" << endl;
95 exit( 1 ); // terminate program; subscript out of range
96 } // end if
97
98 return ptr[ subscript ]; // reference return
99 } // end function operator[]
100
101 // overloaded subscript operator for const Arrays
102 // const reference return creates an rvalue
103 int Array::operator[]( int subscript ) const
104 {
105 // check for subscript out-of-range error
106 if ( subscript < 0 || subscript >= size )
107 {
108 cerr << "\nError: Subscript " << subscript
109 << " out of range" << endl;
110 exit( 1 ); // terminate program; subscript out of range
111 } // end if
112
113 return ptr[ subscript ]; // returns copy of this element
114 } // end function operator[]
115
116 // overloaded input operator for class Array;
117 // inputs values for entire Array
118 istream &operator>>( istream &input, Array &a )
119 {
120 for ( int i = 0; i < a.size; i++ )
121 input >> a.ptr[ i ];
122
123 return input; // enables cin >> x >> y;
124 } // end function
125
126 // overloaded output operator for class Array
127 ostream &operator<<( ostream &output, const Array &a )
128 {
129 int i;
130
131 // output private ptr-based array
132 for ( i = 0; i < a.size; i++ )
133 {
134 output << setw( 12 ) << a.ptr[ i ];
135
136 if ( ( i + 1 ) % 4 == 0 ) // 4 numbers per row of output
137 output << endl;
138 } // end for
139
140 if ( i % 4 != 0 ) // end last line of output
141 output << endl;
142
143 return output; // enables cout << x << y;
144 } // end function operator<<

Fig. 11.7 Array class member- and friend-function definitions.

1 // Fig. 11.8: fig11_08.cpp


2 // Array class test program.
3 #include <iostream>
4 using std::cout;
5 using std::cin;
6 using std::endl;
7
8 #include "Array.h"
9
10 int main()
11 {
12 Array integers1( 7 ); // seven-element Array
13 Array integers2; // 10-element Array by default
14
15 // print integers1 size and contents
16 cout << "Size of Array integers1 is "
17 << integers1.getSize()
18 << "\nArray after initialization:\n" << integers1;
19
20 // print integers2 size and contents
21 cout << "\nSize of Array integers2 is "
22 << integers2.getSize()
23 << "\nArray after initialization:\n" << integers2;
24
25 // input and print integers1 and integers2
26 cout << "\nEnter 17 integers:" << endl;
27 cin >> integers1 >> integers2;
28
29 cout << "\nAfter input, the Arrays contain:\n"
30 << "integers1:\n"<< integers1
31 << "integers2:\n"<< integers2;
32
33 // use overloaded inequality (!=) operator
34 cout << "\nEvaluating: integers1 != integers2" << endl;
35
36 if ( integers1 != integers2 )
37 cout << "integers1 and integers2 are not equal" << endl;
38
39 // create Array integers3 using integers1 as an
40 // initializer; print size and contents
41 Array integers3( integers1 ); // invokes copy constructor
42
43 cout << "\nSize of Array integers3 is "
44 << integers3.getSize()
45 << "\nArray after initialization:\n" << integers3;
46
47 // use overloaded assignment (=) operator
48 cout << "\nAssigning integers2 to integers1:" << endl;
49 integers1 = integers2; // note target Array is smaller
50
51 cout << "integers1:\n"<< integers1
52 << "integers2:\n"<< integers2;
53
54 // use overloaded equality (==) operator
55 cout << "\nEvaluating: integers1 == integers2" << endl;
56
57 if ( integers1 == integers2 )
58 cout << "integers1 and integers2 are equal" << endl;
59
60 // use overloaded subscript operator to create rvalue
61 cout << "\nintegers1[5] is " << integers1[ 5 ];
62
63 // use overloaded subscript operator to create lvalue
64 cout << "\n\nAssigning 1000 to integers1[5]" << endl;
65 integers1[ 5 ] = 1000;
66 cout << "integers1:\n"<< integers1;
67
68 // attempt to use out-of-range subscript
69 cout << "\nAttempt to assign 1000 to integers1[15]" << endl;
70 integers1[ 15 ] = 1000; // ERROR: out of range
71 return 0;
72 } // end main
Size of Array integers1 is 7
Array after initialization:
0 0 0 0
0 0 0
Size of Array integers2 is 10
Array after initialization:
0 0 0 0
0 0 0 0
0 0
Enter 17 integers:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17

After input, the Arrays contain:


integers1:
1 2 3 4
5 6 7
integers2:
8 9 10 11
12 13 14 15
16 17

Evaluating: integers1 != integers2


integers1 and integers2 are not equal

Size of Array integers3 is 7


Array after initialization:
1 2 3 4
5 6 7
Assigning integers2 to integers1:
integers1:
8 9 10 11
12 13 14 15
16 17
integers2:
8 9 10 11
12 13 14 15
16 17

Evaluating: integers1 == integers2


integers1 and integers2 are equal

integers1[5] is 13

Assigning 1000 to integers1[5]


integers1:
8 9 10 11
12 1000 14 15
16 17

Attempt to assign 1000 to integers1[15]

Error: Subscript 15 out of range

Fig. 11.8 Array class test program.

Creating Arrays, Outputting Their Size and Displaying Their Contents

The program begins by instantiating two objects of class Array—integers1 (Fig. 11.8, line 12)
with seven elements, and integers2(Fig. 11.8, line 13) with the default Array size—10
elements (specified by the Array default constructor’s prototype in Fig. 11.6, line 15). Lines 16–
18 use member function getSize to determine the size of integers1 and output integers1,
using the Array overloaded stream insertion operator. The sample output confirms that
the Array elements were set correctly to zeros by the constructor. Next, lines 21–23 output the
size of Array integers2 and output integers2, using the Array overloaded stream insertion
operator.
Using the Overloaded Stream Insertion Operator to Fill an Array

Line 26 prompts the user to input 17 integers. Line 27 uses the Array overloaded stream
extraction operator to read these values into both arrays. The first seven values are stored
in integers1 and the remaining 10 values are stored in integers2. Lines 29–31 output the two
arrays with the overloaded Array stream insertion operator to confirm that the input was
performed correctly.

Using the Overloaded Inequality Operator

Line 36 tests the overloaded inequality operator by evaluating the condition

integers1 != integers2

The program output shows that the Arrays indeed are not equal.

Initializing a New Array with a Copy of an Existing Array’s Contents

Line 41 instantiates a third Array called integers3 and initializes it with a copy of Array


integers1. This invokes the Array copy constructor to copy the elements
of integers1 into integers3. We discuss the details of the copy constructor shortly. Note that
the copy constructor can also be invoked by writing line 41 as follows:

Array integers3 = integers1;

The equal sign in the preceding statement is not the assignment operator. When an equal sign
appears in the declaration of an object, it invokes a constructor for that object. This form can be
used to pass only a single argument to a constructor.

Lines 43–45 output the size of integers3 and output integers3, using the Array overloaded


stream insertion operator to confirm that the Array elements were set correctly by the copy
constructor.

Using the Overloaded Assignment Operator

Next, line 49 tests the overloaded assignment operator (=) by


assigning integers2 to integers1. Lines 51—52 print both Arrayobjects to confirm that the
assignment was successful. Note that integers1 originally held 7 integers and was resized to
hold a copy of the 10 elements in integers2. As we will see, the overloaded assignment
operator performs this resizing operation in a manner that is transparent to the client code.

Using the Overloaded Equality Operator

Next, line 57 uses the overloaded equality operator (==) to confirm that
objects integers1 and integers2 are indeed identical after the assignment.

Using the Overloaded Subscript Operator

Line 61 uses the overloaded subscript operator to refer to integers1[ 5 ]—an in-range
element of integers1. This subscripted name is used as an rvalue to print the value stored
in integers1[ 5 ]. Line 65 uses integers1[ 5 ] as a modifiable lvalue on the left side of an
assignment statement to assign a new value, 1000, to element 5 of integers1. We will see
that operator[] returns a reference to use as the modifiable lvalue after the operator confirms
that 5 is a valid subscript for integers1.

Line 70 attempts to assign the value 1000 to integers1[ 15 ]—an out-of-range element. In


this example, operator[] determines that the subscript is out of range, prints a message and
terminates the program. Note that we highlighted line 70 of the program in red to emphasize
that it is an error to access an element that is out of range. This is a runtime logic error, not a
compilation error.

Interestingly, the array subscript operator [] is not restricted for use only with arrays; it also
can be used, for example, to select elements from other kinds of container classes, such as
linked lists, strings and dictionaries. Also, when operator[] functions are defined, subscripts no
longer have to be integers—characters, strings, floats or even objects of user-defined classes
also could be used. In Chapter 23, Standard Template Library (STL), we discuss
the STL map class that allows noninteger subscripts.

Array Class Definition

Now that we have seen how this program operates, let us walk through the class header (Fig.
11.6). As we refer to each member function in the header, we discuss that function’s
implementation in Fig. 11.7. In Fig. 11.6, lines 35–36 represent the private data members of
class Array. Each Array object consists of a size member indicating the number of elements in
the Array and an intpointer—ptr—that points to the dynamically allocated pointer-based array
of integers managed by the Array object.

Overloading the Stream Insertion and Stream Extraction Operators


as friends

Lines 12–13 of Fig. 11.6 declare the overloaded stream insertion operator and the overloaded
stream extraction operator to befriends of class Array. When the compiler sees an expression
like cout << arrayObject, it invokes global function operator<< with the call

operator<<( cout, arrayObject )

When the compiler sees an expression like cin >> arrayObject, it invokes global
function operator>> with the call

operator>>( cin, arrayObject )

We note again that these stream insertion and stream extraction operator functions cannot be
members of class Array, because theArray object is always mentioned on the right side of the
stream insertion operator and the stream extraction operator. If these operator functions were to
be members of class Array, the following awkward statements would have to be used to output
and input anArray:

arrayObject << cout;


arrayObject >> cin;

Such statements would be confusing to most C++ programmers, who are familiar
with cout and cin appearing as the left operands of<< and >>, respectively.
Function operator<< (defined in Fig. 11.7, lines 127—144) prints the number of elements
indicated by size from the integer array to which ptr points. Function operator>> (defined in
Fig. 11.7, lines 118–124) inputs directly into the array to which ptr points. Each of these
operator functions returns an appropriate reference to enable cascaded output or input
statements, respectively. Note that each of these functions has access to
an Array’s private data because these functions are declared as friends of class Array. Also,
note that class Array’s getSize and operator[] functions could be used
by operator<< and operator>>, in which case these operator functions would not need to
be friends of class Array. However, the additional function calls might increase execution-time
overhead

Array Default Constructor

Line 15 of Fig. 11.6 declares the default constructor for the class and specifies a default size of
10 elements. When the compiler sees a declaration like line 13 in Fig. 11.8, it invokes
class Array’s default constructor (remember that the default constructor in this example actually
receives a single int argument that has a default value of 10). The default constructor (defined
in Fig. 11.7, lines 18–25) validates and assigns the argument to data member size, uses new to
obtain the memory for the internal pointer-based representation of this array and assigns the
pointer returned by new to data member ptr. Then the constructor uses a for statement to set
all the elements of the array to zero. It is possible to have an Array class that does not initialize
its members if, for example, these members are to be read at some later time; but this is
considered to be a poor programming practice. Arrays, and objects in general, should be
properly initialized and maintained in a consistent state.

Array Copy Constructor

Line 16 of Fig. 11.6 declares a copy constructor (defined in Fig. 11.7, lines 29–36) that initializes
an Array by making a copy of an existing Array object. Such copying must be done carefully to
avoid the pitfall of leaving both Array objects pointing to the same dynamically allocated
memory. This is exactly the problem that would occur with default memberwise copying, if the
compiler is allowed to define a default copy constructor for this class. Copy constructors are
invoked whenever a copy of an object is needed, such as in passing an object by value to a
function, returning an object by value from a function or initializing an object with a copy of
another object of the same class. The copy constructor is called in a declaration when an object
of class Array is instantiated and initialized with another object of class Array, as in the
declaration in line 41 of Fig. 11.8.

Software Engineering Observation 11.4

The argument to a copy constructor should be a  const  reference to allow a  const  object
to be copied.

Common Programming Error 11.6

Note that a copy constructor must receive its argument by reference, not by value.


Otherwise, the copy constructor call results in infinite recursion (a fatal logic error) because
receiving an object by value requires the copy constructor to make a copy of the argument
object. Recall that any time a copy of an object is required, the class’s copy constructor is
called. If the copy constructor received its argument by value, the copy constructor would
call itself recursively to make a copy of its argument!

The copy constructor for Array uses a member initializer (Fig. 11.7, line 30) to copy the size of
the initializer Array into data membersize, uses new (line 32) to obtain the memory for the
internal pointer-based representation of this Array and assigns the pointer returned by new to
data member ptr. Then the copy constructor uses a for statement to copy all the elements of
the initializer Arrayinto the new Array object. Note that an object of a class can look at
the private data of any other object of that class (using a handle that indicates which object to
access).

Common Programming Error 11.7

If the copy constructor simply copied the pointer in the source object to the target object’s
pointer, then both objects would point to the same dynamically allocated memory. The first
destructor to execute would then delete the dynamically allocated memory, and the other
object’s  ptr  would be undefined, a situation called a  dangling pointer—this would likely
result in a serious runtime error (such as early program termination) when the pointer was
used.

Array Destructor

Line 17 of Fig. 11.6 declares the destructor for the class (defined in Fig. 11.7, lines 39–42). The
destructor is invoked when an object of class Array goes out of scope. The destructor
uses delete [] to release the memory allocated dynamically by new in the constructor.

getSize Member Function

Line 18 of Fig. 11.6 declares function getSize (defined in Fig. 11.7, lines 45–48) that returns
the number of elements in the Array.

Overloaded Assignment Operator

Line 20 of Fig. 11.6 declares the overloaded assignment operator function for the class. When
the compiler sees the expressionintegers1 = integers2 in line 49 of Fig. 11.8, the compiler
invokes member function operator= with the call

integers1.operator=( integers2 )

The implementation of member function operator= (Fig. 11.7, lines 52–70) tests for self


assignment (line 54) in which an object of classArray is being assigned to itself. When this is
equal to the address of the right operand, a self-assignment is being attempted, so the
assignment is skipped (i.e., the object already is itself; in a moment we will see why self-
assignment is dangerous). If it is not a self-assignment, then the member function determines
whether the sizes of the two arrays are identical (line 58); in that case, the original array of
integers in the left-side Array object is not reallocated. Otherwise, operator= uses delete (line
60) to release the memory originally allocated to the target array, copies the size of the source
array to the size of the target array (line 61), uses new to allocate memory for the target array
and places the pointer returned by new into the array’s ptr member.2 Then the for statement at
lines 65—66 copies the array elements from the source array to the target array. Regardless of
whether this is a self-assignment, the member function returns the current object (i.e., *this at
line 69) as a constant reference; this enables cascaded Array assignments such as x = y = z.
If self-assignment occurs, and function operator=

1. Note that new could fail to obtain the needed memory. We deal with new failures in Chapter
16, Exception Handling.

2. Once again, new could fail. We discuss new failures in Chapter 16.

did not test for this case, operator= would delete the dynamic memory associated with
the Array object before the assignment was complete. This would leave ptr pointing to memory
that had been deallocated, which could lead to fatal runtime errors.

Software Engineering Observation 11.5

A copy constructor, a destructor and an overloaded assignment operator are usually


provided as a group for any class that uses dynamically allocated memory.

Common Programming Error 11.8

Not providing an overloaded assignment operator and a copy constructor for a class when
objects of that class contain pointers to dynamically allocated memory is a logic error.

Software Engineering Observation 11.6

It is possible to prevent one object of a class from being assigned to another. This is done by
declaring the assignment operator as a  private  member of the class.

Software Engineering Observation 11.7

It is possible to prevent class objects from being copied; to do this, simply make both the
overloaded assignment operator and the copy constructor of that class  private.

Overloaded Equality and Inequality Operators

Line 21 of Fig. 11.6 declares the overloaded equality operator (==) for the class. When the
compiler sees the expression integers1 == integers2 in line 57 of Fig. 11.8, the compiler
invokes member function operator== with the call

integers1.operator==( integers2 )

Member function operator== (defined in Fig. 11.7, lines 74–84) immediately returns false if


the size members of the arrays are not equal. Otherwise, operator== compares each pair of
elements. If they are all equal, the function returns true. The first pair of elements to differ
causes the function to return false immediately.
Lines 24–27 of the header file define the overloaded inequality operator (!=) for the class.
Member function operator!= uses the overloaded operator== function to determine whether
one Array is equal to another, then returns the opposite of that result. Writing operator!= in
this manner enables the programmer to reuse operator==, which reduces the amount of code
that must be written in the class. Also, note that the full function definition for operator!= is in
the Array header file. This allows the compiler to inline the definition of operator!= to
eliminate the overhead of the extra function call.

Overloaded Subscript Operators

Lines 30 and 33 of Fig. 11.6 declare two overloaded subscript operators (defined in Fig. 11.7 at
lines 88–99 and 103–114, respectively). When the compiler sees the
expression integers1[ 5 ] (Fig. 11.8, line 61), the compiler invokes the appropriate
overloadedoperator[] member function by generating the call

integers1.operator[]( 5 )

The compiler creates a call to the const version of operator[] (Fig. 11.7, lines 103–114) when
the subscript operator is used on aconst Array object. For example, if const object z is
instantiated with the statement

const Array z( 5 );

then the const version of operator[] is required to execute a statement such as

cout << z[ 3 ] << endl;

Remember, a program can invoke only the const member functions of a const object.

Each definition of operator[] determines whether the subscript it receives as an argument is in


range. If it is not, each function prints an error message and terminates the program with a call
to function exit (header <cstdlib>).3 If the subscript is in range, the non-const version
of operator[] returns the appropriate array element as a reference so that it may be used as a
modifiable lvalue(e.g., on the left side of an assignment statement). If the subscript is in range,
the const version of operator[] returns a copy of the appropriate element of the array. The
returned character is an rvalue.

You might also like