C++ Week 2

Using objects. Conditionals.

As well as being something simple like an int or a double a variable can also be an object. Typically an object holds structured data and exhibits certain behaviour. The data access can be restricted; the data inside the object is said to be encapsulated. The definition of what data an object will contain and what functionality it will have is specified in a class. An object variable is said to be an instance of a class. A string is an example of an object. the string library contains the definition of the string class.

Classes

Consider a possible Student class, which defines objects that contain the following three data items:

Assuming that the class Student has been defined (we see later in the course how to do that), we can create an instance of the Student class using either of the following definitions:

Student st(1234, "Mary Smith", 2420);
Student st = Student(1234, "Mary Smith", 2420);

We can also use the default constructor (if there is one defined for the class) as follows to create an instance of Student that contains default data:

Student st;

Note that we do not write Student st();.

You can use assignment with objects. If st1 and st2 were both objects of type Student, we could do the following:

st1 = st2;

The following statement changes the contents of the already-existing object:

st = Student(5678, "Bill Brown", 1320);

Encapsulation

The data stored in an object is not generally immediately accessible to the outside world. Access is defined by the object's member functions which expose the data through the object's interface. For example the Student class could have an accessor member function get_number() that defines access to the student's ID number as follows:

st.get_number(); // returns the ID number of the Student object st

reg_no = st.get_number(); // assigns the ID number to the variable reg_no.

To access the properties and member functions of an object, use the dot operator as shown above. Objects can also have mutator member functions that allow the properties to be modified. For example, the Student class might have a set_name() function which you could use to change the name property of a student:

st.set_name("Betty Jones");

changes the name of st from Bill Brown to Betty Jones.

What you can do with an object depends on how the class has been defined. For example, whether you can say cout << st; or if (st1 == st2) (where st1 and st2 are objects of type Student), depends on whether Student has been defined in such a way as to allow you to do this. If it has, you can; if it hasn't, you can't.

Non-standard libraries

You include the standard C++ libraries by using a #include<library> statement. However, if you want to include non-standard libraries (Horstmann's for example), you use a statement of the form #include "filename". If the libraries contain names defined in the std namespace, you may need to put the #include after the using namespace std; statement.

Conditional statements

Conditions take the form:

if (condition)
   {statements executed if the condition is true
   }
or
if (condition)
   {statements executed if the condition is true
   }
else
   {statements executed if the condition is not true
   }
Execution resumes here

Note that the first line does not end with a semicolon. The curly brackets are necessary only if there are several statements. If you are only executing one statement as a result of the condition, you can place it on the same line or the next. For example:

double first, second;	// first and second serves, in tennis
cin >> first >> second;
if (first /second < 1.2)
   cout << "Practise more serves"; // Statement on the next line, but indented
else cout << "Serve is OK"; // Statement on the same line

if (first / second  < 1.2)
{  cout << "How many double faults?";
   double dfault;
   cin >> dfault;
   if (dfault / second > 0.25)
      cout << "Could do better";
} // curly brackets to enclose the compound statement after the if

Relational operators

Relational operators allow you to compare data items.

== is equal to
!= is not equal to
> is greater than
<

is less than

>= is greater than or equal to
<= is less than or equal to

The < and > operators can be used with strings to determine alphabetical order. For example, if s has the value "abc" and t has the value "def"

(s < t)

is true. When dealing with non-alphanumeric characters, you need to know which character set you are using before you can determine the collating order of a pair of strings. The most common character set is the ASCII (American Standard Code for Information Interchange). The EBCDIC (Extended Binary Coded Decimal Interchange Code) is used in IBM mainframes. The first 32 characters of the ASCII character set (numbered 0 to 31) are non-printable control codes. Features of ASCII worth remembering are that the space character (number 32) comes before all the printable characters, the digits come before the letters, and the upper-case alphabet comes before the lower-case alphabet.

Boolean operators

Boolean operators are used to compare true (1 or non-zero) and false (0) values

pre-'98 1998 standard
! not
&& and
|| or

The order of precedence is not, then and, then or. For example A or not B and C means A or ((not B) and C). The and operator evaluates to true only if both operands are true; or evaluates to true if either operand is true; not evaluates to the complement of the operand (if A is true, not A is false and vice versa).

Common pitfalls

Be very careful to write "==" rather than "=". If you write if (x = 0), this is an assignment, not an equality test, and the effect will be to assign 0 to x. How does it evaluate an assignment as true or false? If the value assigned is zero, it evaluates it as false; if it is anything else, it evaluates it as true.

When expressing "If x or y is greater than zero", you must write:

if (x > 0 or y > 0)

rather than

if (x or y > 0)

which are both correct code but logically different. The second expression literally means "if x is true or y is greater than zero", and x is always true for non-zero values of x.

When expressing "If x is between 0 and 10 inclusive", you must write:

if (x >= 0 and x <= 10)

rather than

if (0 <= x <= 10)

To evaluate the second, it first decides whether zero is less than or equal to x. Either it is (so the expression is true) or it isn't (so the expression is false). It then decides whether this result (true, or false) is less than or equal to 10. How can true (or false) be less than or equal to 10? It makes no sense. But it resolves the problem by treating true as 1 and false as 0. Both of these are less than 10, so the expression is true regardless of the value of x.

Order of evaluation of operands

In general, in C++, there is no guarantee of the order in which the operands of an expression will be evaluated. For example, given the expression (x + 2) * (y - 1), does it first evaluate the (x + 2) and then the (y - 1) and then multiply them together, or does it first evaluate the (y - 1) and then the (x + 2) and then do the multiplying? The answer is that it might do either - the language does not specify.

Note that this is a different matter from deciding where the brackets go in an unbracketed expression; we have already seen that that is decided by the precedence and associativity of the operators. Precedence and associativity decide where the implied brackets go - they decide what the expression means. The above expression is fully bracketed; operator precedence is not an issue here - there is no doubt about what the expression means. What we are talking about now is the temporal order in which the different operands of an expression are evaluated.

Most of the time, it does not matter which way round it does it, but it might. If the evaluation of one operand affected the value of the other, it might make a difference. Suppose an integer variable x had the value 3, and you had ++x * (10 - x). Doing the left side first changes the value of x from 3 to 4, so the right-hand side evaluates to 6 and the result is 24. But if it did the right side first, (10 - x) comes to 7, ++x comes to 4, so the result is 28.

This would apply also to a cout statement. For example, in

cout << expressionA << expressionB << endl;
while you can be sure that the value of expressionA will be output before the value of expressionB, you cannot be sure that expressionA will be evaluated before expressionB. There is no guarantee that the following will output 1234:
int	x = 0;
cout << ++x << ++x << ++x << ++x << endl;

In general you should be careful not to write expressions that rely on a particular order of evaluation.

The boolean operators, however, are an exception to this general rule. The operands of and and or are always evaluated left to right. For example, given the expression x < 3 and y > 10, x < 3 will always be evaluated first.

Lazy evaluation

Given a pair of conditional expressions joined by a boolean operator, C++ always evaluates the left-hand one first. It also uses lazy evaluation, in which boolean expressions are evaluated only until their result is clear. Since false and anything is always false, then, if it is evaluating an and expression and it evaluates the left side to false, it can (and does) stop evaluating since it already knows the result for the whole expression. Likewise with evaluating the left side of an or expression to true. This can be useful. For example:

if (second > 0 and first / second > 1.2)
{...
}

In this case if second is indeed zero, then the potentially fatal calculation of first / second is never executed.

Boolean variables

A boolean variable is a variable that can hold only the values true and false. Suppose we are processing applications for theatre tickets and suppose that a discount applies if the performance is on a Monday or the customer is under 15 or the ticket is for one of the back rows. Rather than evaluate this condition repeatedly in the program, we might evaluate it just once and store the outcome in a boolean variable:

bool half_price = day == "Mon" or age < 15 or row >= "w";
You might like to put the boolean expression in brackets, but you don't have to:
bool half_price = (day == "Mon" or age < 15 or row >= "w");
Then, when we want to say "If the discount is applicable," we just say if (half_price). (For "If the discount is not applicable," we just say if (not half_price).) Note that there is no need to say if (half_price == true). Having established whether half_price is true, it already has the result it needs. Asking it now to test whether true is equal to true (or whether false is equal to true) is redundant.

Languages that don't have boolean variables have to make do with some other type of variable for this job, such as:

string half_price;
if (day == "Mon" or age < 15 or row >= "w")
	half_price = "T";
else	half_price = "F";
And then later to say if (half_price == "T"). But it's all too easy to write if (half_price == "t") or if (half_price == "true") or if (half_price == "yes") or ...

Conditional operator, ? :

It is possible to abbreviate some condition statements using the ? : operator as follows:

conditional_expression ? expression_1 : expression_2

in which, if conditional_expression is true, expression_1 is returned, otherwise expression_2 is returned. Note that the expressions must be of the same type.

max = x > y ? x : y; // Assigns to max the larger of x and y
cout << (s.length() < t.length() ? s : t); // prints the shorter of s and t

The conditional operator doesn't do anything that an if statement couldn't do, but it sometimes enables you to express something more succinctly.

The switch statement

The switch statement is C++'s multi-way branch, allowing you to specify a number of different cases, rather than simply true or false. Here is an example:

int x = 12;  int  z;  cin >> z;
switch (z)
{  case 1: cout << x + 1;
   case 2: cout << x - 2;
   case 3: cout << x * 3;
   case 4: cout << x / 4;
   default: cout << "Invalid value";
}

The switch statement requires an expression after the word switch and then it jumps to the statement whose case matches the expression. All subsequent statements are executed as well, so that if z were 3 in this example, the output would not just be 36, but 363Invalid value. You can avoid this state of affairs by placing a break keyword at the end of each case statement, to go to the statement after the curly brackets, thus:

switch (z)
{  case 1: cout << x + 1; break;
   case 2: cout << x - 2; break;
   case 3: cout << x * 3; break;
   case 4: cout << x / 4; break;
   default: cout << "Invalid value";
}

switch statements are of limited use: the expression must always evaluate to an integral value, e.g. int or char (not double or string); you must specify constant values after the case - you can't have expressions here - and you can have only one value per case and not, for example, a range of values, though you can implement a (small) range rather clumsily as follows:

switch (z)
{  case 1:
   case 2:
   case 3: cout << "One, two or three"; break;

which will output "One, two or three" in the event that z has the value 1 or 2 or 3.


Notes on R. Mitton's lectures by S.P. Connolly, edited by R. Mitton, 2000