We began with the following example:
Example Write a program which defines and initializes a string, and then prints it to the screen.
//File: string1.cpp
#include <iostream>
using namespace std;
int main()
{
char s[2] = "HI";
// Error Message:
// initializer-string for array of chars is too long
//char s[3] = "HI";
// or
//char s[3]={'H','I','\0'};
//CORRECTS the problem.
cout << s << endl;
return 0;
}
The source of the above error is the requirement (derived from the C programming language) that strings be terminated with the "null" character, '\0'. So, simply increasing the capacity of the array s to 3 corrects the problem. So either of the following declarations will suffice:
char s[3] = "HI";
or
char s[3] = {'H', 'I', '\0'};
We followed with the following:
Example. Write a program which takes a line of text and echoes it to the screen.
#include <iostream>
using namespace std;
int main()
{
VERSION 1
VERSION 2
//File: string2.cpp
//File: string22.cpp
Array of Characters
Pointer to char
char line[80];
char * line;
cin.getline (line, 80);
cin.getline(line, 80);
cout << line << endl;
cout << line << endl;
return 0;
return 0;
}
VERSION 2 will compile but generates a run-time error using Visual C++ 6.0. The difficulty arises because the pointer 'line' has not been initiallized. That is, as we type in a line of text, the computer doesn't know where to put it. This can be remedied by explicitly asking for free space using the keyword new as follows:
#include <iostream>
using namespace std;
int main()
{
char * line;
line = new char[80];
//dynamically allocates 80 new memory locations capable
//of storing characters and assigns the pointer 'line' the address
//of the first of those memory locations
cin.getline (line, 80);
cout << line << endl;
delete[] line;
//releases the memory locations allocated by new
return 0;
}
Note that in C, the word string usually means an array of characters which is null-terminated. That is, the last character is the null character (ASCII code zero), '\0'. For example, the string literal "HELLO" is really composed of the six characters 'H', 'E', 'L', 'L', 'O', '\0'. The following are equivalent statements:
char s[] = "HELLO"; <========> char s[] = {'H', 'E', 'L', 'L', 'O', '\0'};
Some of the functions included with C++ automatically add the null character; others do not. For example, in the above program, getline(line, 80), does not put the end of line character in 'line', but it will add a null character at the end.
We next explored the string class which is part of the newly established C++ standard. It resembles the vector class in that it extends arrays of chars while vectors extend arrays of arbitrary typed elements. One significant difference between the two is that strings can be of arbitrary length. That is, the subscript operator, [], does not provide range checking for strings. A version of the program that inputs and outputs a line of text might look as follows using the C++ string class:
//File: string3.cpp
#include <iostream>
#include <string>
using namespace std;
//The following program takes a line of text and echoes it to the screen
int main()
{
string line;
getline (cin, line); //new version of getline
specific to new string class
cout << line << endl;
return 0;
}
Lab Exercise. Write a program with two swap() functions, one that swaps C strings and one that swaps a pair of new C++ strings. Don't peek at solution!.
In situations requiring traditional C strings (or char *), a string cannot be used. However, the string class provides a member function which converts the string to a null-terminated C string. The following code segment illustrates this in the context of providing a filename interactively:
cout << "Filename please: ";
string s;
cin >> s;
ifstream fin (s.c_str()); // 's' alone will give error since expects
null-terminated C string
Example. A version of the program (from last class) which inputs and outputs a line of text from a file might look as follows using the C++ string class:
//File: file_echo.cpp
#include <fstream>
#include <string>
using namespace std;
int main()
{
string filename,
line;
cout << "What is the name of your input file? ";
cin >> filename;
ifstream fin(filename.c_str()); //try ifstream fin
(filename) and see what happens
getline (fin, line);
cout << line << endl;
}
It's perfectly legal to have several functions with the same name (and different parameter lists) in the same program.
Example
//File: overload.cpp
#include <iostream>
#include <string>
using namespace std;
void swap (string &a, string &b)
{
string temp = a;
a = b;
b = temp;
}
void swap (int &a, int &b)
{
int temp = a;
a = b;
b = temp;
}
int main()
{
int x = 25, y = 60;
string s("HI"), t("THERE");
cout << x << endl;
//==> 25
cout << y << endl;
//==> 60
cout << s << endl
//==> HI
<< t << endl; //==> THERE
cout << endl;
swap (x, y);
swap (s, t);
cout << x << endl;
//==> 60
cout << y << endl;
//==> 25
cout << s << endl
//==> THERE
<< t << endl; //==> HI
return 0;
}
25
60
HI
THERE
60
25
THERE
HI
Here we have two functions with the same name (swap()). The compiler is able to cope because the arguments for each differ in type. The number and type of arguments for a function are called the function's signature. So, even though the functions have the same name, the signatures differ and the compiler can resolve which function is intended.
Even though overloading (many functions with the same name) is possible, it is not an ideal solution. You will notice that the code for all of the swap functions is almost identical. This violates the spirit of the software engineering principle of reusable code. It would clearly be advantageous if some mechanism existed which would allow us to write "generic" routines; i.e., algorithms which would apply regardless of the type of the parameters. In fact, C++ provides just this mechanism. It is called a template.
Here, we want to write a function that determines the type of its arguments at run time. It offers a "one size fits all" solution.
Example.
template<class T> void
swap (T &a, T &b)
{
T temp = a;
a = b;
b = temp;
}
Insertion of this template in place of the two swap functions in the above program will accomplish the same task. There's nothing special about choosing the letter T, except that it represents an arbitrary type.
Actually, when you attempt to execute the following program, you get a compile-time error pointing to the invocation of swap:
//File: template.cpp
#include <iostream>
#include <string>
using namespace std;
template<class T> void swap (T
&a, T &b)
{
T temp = a;
a = b;
b = temp;
}
int main ()
{
int x=12, y=5;
string s("HI"),
t("THERE");
cout << "First number
is: " << x
<< endl;
cout << "Second number
is: " << y <<
endl;
cout << "First string
is: " <<
s << endl;
cout << "Second string
is: " << t
<< endl;
// in Visual C++ 6.0
swap (x, y); //<== none of
2 overload have a best conversion
swap (s, t); //<==
ambiguous call to overloaded function
cout << endl;
cout << "First number
is: " << x
<< endl;
cout << "Second number
is: " << y <<
endl;
cout << "First string
is: " <<
s << endl;
cout << "Second string
is: " << t
<< endl;
return 0;
}
The compiler is confused as to which version of swap it should use, the version defined in namespace std or the template included in our program. The ambiguity is resolved in either of two ways. Either rename the template (say swap1()) or place our definition in a new namespace. The second option results in the following implementation:
//File: template2.cpp
...
namespace bill
{
template< class
T> void swap (T &a, T &b)
{
T temp = a;
a = b;
b = temp;
}
}
...
--->bill::swap(x, y); <------------
--->bill::swap(s, t); <------------
...
The scope resolution operator :: determines which version of swap to use.
Lab Exercise. Write a template that calculates the average of a list of numbers, either int or double. If you want to check my version after you struggle for awhile, just click the following: average.cpp