Instructor: David Goldschmidt, Ph.D.
Office Hours: after class
Email: click here to email me

Example 7: C++ Strings

// echoname.cpp  <== click here for file

// ask for a person's name, then generate a framed greeting

#include <iostream>
#include <string>
using namespace std;

int main()
{
  cout << "Please enter your first name and last name: ";
  string name;
  cin >> name;
  string lname;
  cin >> lname;

  // build the message that we intend to write
  const string greeting = "Hello, " + name + "!";

  // build the second and fourth lines of the output
  const string spaces( greeting.size(), ' ' );
  const string second = "* " + spaces + " *";

  // build the first and fifth lines of the output
  const string first( second.size(), '*' );

  // write it all
  cout << endl;
  cout << first << endl;
  cout << second << endl;
  cout << "* " << greeting << " *" << endl;
  cout << second << endl;
  cout << first << endl;
  cout << endl;

  cout << "Thanks, " << lname << endl;

  return 0;
}

C++ Strings:

  • Here is example output of the program above:
  • Please enter your first name and last name: David Goldschmidt
    
    *****************
    *               *
    * Hello, David! *
    *               *
    *****************
    
    Thanks, Goldschmidt
    
  • A string is an object type defined in the standard library to contain a sequence of characters.
  • The string type, like all types (including int, double, char, float, etc.), defines an interface, which includes object construction (initialization), operations, and functions (a.k.a. methods).
  • When an object is created, a special function called a constructor is executed. The constructor initializes the object. The example code above illustrates three ways to construct string objects:
    1. By default, creating an empty string.
    2. With a specified number of instances of a single character.
    3. By using (copying) another string.
  • The notation greeting.size() is a call to the size() function defined as a member function of the string class.
  • Input to string objects through streams (e.g. cin) includes the following steps:
    1. Input and discard whitespace characters, one at a time, until a non-whitespace character is found.
    2. Input a sequence of non-whitespace characters, storing each character in the string. This overwrites anything that was already in the string.
    3. Reading stops either at the end of the input or upon reaching the next whitespace character (without reading it in).
  • The (overloaded) operator + is defined on strings. It concatenates two strings to create a third string (without changing either of the original two strings).
  • The assignment operator = on strings overwrites the current contents of the string with whatever the resulting r-value is.
  • Strings behave like arrays when using the subscript operator [].
    • Provides access to individual characters in the string.
    • Subscript 0 corresponds to the first character.
  • Strings define a special type called size_type, which is the type returned by the string function size().
    • Use string::size_type to use this type. The :: notation means that size_type is defined within the scope of the string type.
    • The string::size_type type is generally equivalent to unsigned int.
    • You may have compiler warnings (e.g. using -Wall with g++) and potential compatibility problems if you compare an int variable to a.size().

Example 8: More C++ Strings

// echonamediag.cpp  <== click here for file

// ask for a person's name, then generate a diagonal greeting

#include <iostream>
#include <string>
using namespace std;

int main()
{
  cout << "Please enter your first name and last name: ";
  string name;
  cin >> name;
  string lname;
  cin >> lname;

  const string star_line( name.size() + 4, '*' );
  cout << star_line << '\n';

  const string blanks( name.size() + 2, ' ' );
  cout << '*' << blanks << "*\n";

  for ( int i = 0 ; i < name.size() ; i++ )
  {
    cout << "* ";

    for ( int j = 0 ; j < i ; j++ ) {
      cout << ' ';
    }
    cout << name[i];

    for ( int j = i ; j < name.size() ; j++ ) {
      cout << ' ';
    }

    cout << "*\n";
  }

  cout << '*' << blanks << "*\n";
  cout << star_line << '\n';

  cout << "Thanks, " << lname << endl;

  return 0;
}

More C++ Strings:

  • Here is example output of the program above:
  • Please enter your first name and last name: David Goldschmidt
    
    *********
    *       *
    * D     *
    *  a    *
    *   v   *
    *    i  *
    *     d *
    *       *
    *********
    
    Thanks, Goldschmidt
    
  • There are two ways to end a line of output in a C++ program, each of which are shown below:
  • cout << '\n';
    cout << endl;
    
  • What's the difference? C++ streams store their output in an output buffer. This buffer is not immediately written to a file or displayed on your screen. The reason is that the writing process is much slower than other computations. Overall, program execution is much faster when output is buffered, then done in large chunks when the buffer is full.
  • Outputting '\n' (the end-of-line character) just adds one more character to the buffer.
  • Outputting endl has two effects:
    1. Outputting the '\n' character.
    2. Forcing the buffer to actually be output to the file or screen (i.e. flushed).
  • Why should you care?
    • When your program crashes, the contents of the output buffer are lost and not actually output. As a result, when looking at your output, it often appears that your program crashed much earlier than it actually did. Using endl helps you debug your code.
    • Using endl can substantially slow down a program. When a program is fully debugged (and needs to run at a reasonable speed), endl should be replaced by '\n'.

L-Values and R-Values:

  • Consider the simple code:
  • string a = "Kim";
    string b = "Tom";
    b[1] = a[1];
    
  • String b is now "Tim" (something you cannot do in Java).
  • What's the difference between the use of b[1] on the left-hand side of the assignment statement and a[1] on the right-hand side? Syntactically, they look the same. But there are important differences:
    • The expression a[1] gets the character value 'i' from string location 1 in a. This is an r-value.
    • The expression b[1] gets a reference to the memory location associated with string location 1 in b. This is an l-value.
  • The assignment operator stores the value in the referenced memory location.

Example 9: Vectors

// vec.cpp  <== click here for file

#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;

int main()
{
  // A vector is a "container class"
  // Unlike an array, a vector can grow/shrink as necessary
  // Vectors can store any type of data

  int grade;
  vector<int> grades;

  cout << "Enter grades as integers (CTRL-D to end): " << endl;
                           // or CTRL-Z on Windows

  while ( cin >> grade )  // returns 0 when CTRL-D is input
  {
    grades.push_back( grade );
  }

  cout << "Size of vector is: " << grades.size() << endl;

  sort( grades.begin(), grades.end() );

  for ( int i = 0 ; i < grades.size() ; i++ )
  {
    cout << grades[i] << endl;
  }

  return 0;
}

Vectors:

  • A vector is a standard library container class to hold sequences of elements.
  • A vector acts like a dynamically-sized one-dimensional array. It holds objects of any type and starts empty (unless otherwise specified).
  • Any number of objects may be added to the end of a vector. There is no limit on size.
  • A vector can be treated like an ordinary array using the subscripting operator []. Note that there is no automatic checking of subscript bounds.
  • The following line of code creates an empty vector of integers:
  • vector<int> scores;
    
  • Vectors are an example of a templated container class. Angle brackets < and > are used to specify the type of object that will be stored in the vector.
  • The push_back() function appends a value to the end of the vector, increasing its size by one. This is an O(1) operation on average.
  • There is no corresponding push_front() operation.
  • The size() function returns the number of items stored in the vector.

Initializing a Vector through the use of Constructors:

  • Below are examples of several different ways to initialize a vector:
  • vector<int> a;  // #1
    vector<double> b( 100, 3.14 ); // #2
    int n = 100;
    vector<int> c( n * n ); // #3
    vector<double> d( b ); // #4
    vector<int> e( b ); // #5
    
  • Line #1 constructs an empty vector of integers. Values must be placed in the vector using the push_back() function.
  • Line #2 constructs a vector of 100 elements of type double, each entry storing the value 3.14. New entries can be created using the push_back() function, but these will create entries 100, 101, 102, etc.
  • Line #3 constructs a vector of 10,000 integers, but provides no initial values for these integers. Again, new entries can be created for the vector using the push_back() function. These will create entries 10,000, 10,001, etc.
  • Line #4 constructs a vector that is an exact copy of b. This is called a copy constructor.
  • Line #5 produces a compilation error because no constructor exists to create a vector of integers from vector. These are different and incompatible types.

Sorting:

  • The standard library has a series of algorithms that apply to container classes. The prototypes for these algorithms are in the header file algorithm.
  • One of the most useful algorithm is the sort() function. It is accessed by providing the beginning and end of the container's interval to be sorted. As an example, the following code reads, sorts, and outputs a vector of values of type double:
  • double x;
    vector<double> a;
    
    while ( cin >> x ) {
      a.push_back(x);
    }
    
    sort( a.begin(), a.end() );
    
    for ( unsigned int i = 0 ; i < a.size() ; i++ ) {
      cout << a[i] << '\n';
    }
    
  • Note that a.begin() obtains an iterator that references the first location in the vector, while a.end() obtains an iterator that references one index past the last location in the vector.
  • The ordering of values by the sort() function is lowest to highest (technically, non-decreasing order).

Example 10: More Vectors

// mediangrades.cpp  <== click here for file

// Reads from a file (e.g. <a href="docs/csci1200/grades.txt" target="_blank">grades.txt</a>), then calculates
//   and displays average, standard deviation, and median.

#include <algorithm>
#include <cmath>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <vector>
using namespace std;

void
read_grades( vector<int> &grades, ifstream &grade_stream )
{
  int grade;
  while ( grade_stream >> grade )
  {
    grades.push_back( grade );
  }
}

      // (we're passing grades by reference to avoid a COPY)
void                     // const means grades cannot change
compute_avg_and_std_dev( const vector<int> &grades,
                         double &avg, double &std_dev )
{
  // Compute the average
  int sum=0;
  for ( unsigned int i = 0; i < grades.size(); i++ )
  {
    sum += grades[i];
  }
  avg = double(sum) / grades.size();

  // Compute the standard deviation
  double sum_sq = 0.0;
  for ( unsigned int i = 0; i < grades.size(); i++ )
  {
    sum_sq += ( grades[i] - avg ) * ( grades[i] - avg );
  }
  std_dev = sqrt( sum_sq / ( grades.size() - 1 ) );
}

double
compute_median( const vector<int> &grades )
//compute_median( vector<int> grades )
{
  // Create a copy of the vector
  vector<int> grades_to_sort( grades );

  // Sort the values in the vector (default is increasing order)
  sort( grades_to_sort.begin(), grades_to_sort.end() );

  // Compute median
  unsigned int n = grades_to_sort.size();

  if ( n % 2 == 0 ) // even number of grades
    return double( grades_to_sort[ n / 2 ] + grades_to_sort[ n / 2 - 1] ) / 2.0;
  else
    return double( grades_to_sort[ n / 2 ] );
}

int
main( int argc, char* argv[] )
{
  cout << "argc is " << argc << endl;

  if ( argc != 2 )
  {
    cerr << "Usage: " << argv[0] << " <grades-file>\n";
    return 1;
  }

  // Open the grades.txt file (i.e. argv[1])
  //   (bail if an error occurs)
  ifstream grades_stream( argv[1] );
  if ( !grades_stream )
  {
    cerr << "Cannot open grades file " << argv[1] << endl;
    return 1;
  }

  vector<int> grades;
  read_grades( grades, grades_stream );

  // Quit with an error message if no grades loaded
  if ( grades.size() == 0 )
  {
    cout << "No grades entered." << endl;
    return 1;
  }

  // Compute average, standard deviation, and median
  double average, std_dev;
  compute_avg_and_std_dev( grades, average, std_dev );
  double median = compute_median( grades );

  cout << "From " << grades.size() << " grades: \n"
       << "  average is " << setprecision(3) << average << '\n'
       << "  std_dev is " << std_dev << '\n'
       << "   median is " << median << endl;

  return 0;  //  Everything ok
}

Passing Vectors (and Strings) As Parameters:

The following bullet items outline rules for passing vector objects as parameters. The same rules apply to passing string objects.

  • If you are passing a vector as a parameter to a function and you want to make a (permanent) change to the vector, then you should pass it by reference.
    • This is illustrated by the function read_grades() in the program above.
    • This is very different from the behavior of arrays as parameters.
  • What if you don't want to make changes to the vector or don't want these changes to be permanent? The answer we've learned so far is to pass by value. The problem is that the entire vector is copied when this happens!
  • The solution is to pass by constant reference: pass it by reference, but make it a constant so that it cannot be changed in the function. This is illustrated by the compute_avg_and_std_dev() and compute_median() functions in the code above.
  • As a general rule, you should not pass a container object such as a vector or a string by value because of the cost of copying.