Previous Lecture |
Next Lecture |
Exercises |
Top Level
Typing and Access Control
Contents
Now that you have had some programming experience, you will have
noticed that correcting compiler errors is a lot easier than finding
run-time errors.
Good programming languages therefore provide tools to make the
compiler smarter and enable it to find logical mistakes in your
program.
This is achieved through typing and access
control. We not only define the structure of the data and how we
manipulate it, but we also define the meaning of the data and
who is allowed to do what with it.
Next Section |
Contents
Content Dependent Access Control
"You never compare apples and oranges". But the following grocery
store program will compile and run without problems, even though it
doesn't do what the programmer intended:
int price_of_apples(int apples)
{
return 120*apples; // 120 Yen per apple - cheap eh?
}
main()
{
int oranges;
oranges = 3;
price_of_apples(oranges);
}
Now, if we could convey to the compiler the information that apples
and oranges are two different things, even though both are represented
by an integer, then we could detect this error at compile time.
By using classes, the compiler will tell us that we are doing
something strange:
class Oranges {public: int amount; };
class Apples {public: int amount; };
int price_of_apples(Apples apples)
{
return 120*apples.amount;
}
main()
{
Oranges oranges = {3};
price_of_apples(oranges);
}
The compiler will whine about a missing conversion
function. If we wanted to, we could describe a conversion function
from apples to oranges like this:
class Oranges {
public:
// For the price of one apple, you can get 2 oranges
Oranges(Apples apples) { amount = 2*apples.amount; }
int amount;
};
class Apples {
public:
Apples(Oranges oranges) { amount = oranges.amount/2; }
int amount;
};
int price_of_apples(Apples apples)
{
return 120*apples.amount;
}
main()
{
Oranges oranges = {3};
price_of_apples(oranges);
}
Now the program would compile, and the meaning would be something like
"How many apples could I buy for the price of 3 oranges?".
Using a constructor this way is called defining a conversion
between apples and oranges.
In C++, many conversions are definied implicitly. All base types
(int, float, double, char) convert freely, although some
might generate a warning.
Implicit conversion is both good and bad. It's good because it saves
us a lot of typing, and makes "obvious" equivalences work. It's bad
because we lose the effect of using the same data representation for
distinct purposes (which is why we couldn't use typedef in
the example above).
When using classes, conversions have to be defined explicitly. These
conversions document the relationship between classes. If no
conversion is defined, we get a compiler error.
Another way to express relationships between classes is via
inheritance. The following would be a more interesting way to
code out grocery store program:
class Fruit {
public:
virtual int unit_price() {return -1; // unknown};
int amount;
};
class Apple: public Fruit {
public:
virtual int unit_price() { return 120; }
};
class Orange: public Fruit {
public:
virtual int unit_price() { return 60; }
};
int price(Fruit fruit) { return fruit.amount * fruit.price(); }
main()
{
Oranges oranges;
// ... code ...
price(oranges);
// ... more code ...
}
There is a trivial conversion between a class and it's
ancestors. Since every derivation adds stuff to a class, we
simply ignore the excess and just use whatever the base
class knows about.
This conversion is not possible the other way. We would have to guess
what features the child will have. The only way for parent classes to
communicate with their children is via virtual functions, like in the
example.
One way to force a conversion is to use a cast. Here is a
simple example of using a cast:
double round(double number)
{
return (int)(number + 0.5);
}
This function rounds a floating point number to the nearest
integer. The cast forces a conversion from floating point to
integer. That conversion simply chops off the fractional part, which
is why we add 0.5 to achieve the correct rounding to the nearest
integer. Since the return type is again double, we get an
automatic conversion of the integer back into a floating point
number.
Next Section |
Previous Section
User Dependent Access Control
Another way to prevent accidental or negligent abuse of classes is by
controlling the use of class components, or, more exactly,
who gets to use which class component.
C++ knows three kinds of protections:
- public:
- Everybody may access and use public components.
- private:
- Nobody but the member functions of the class itself are permitted
to use private components.
- protected:
- Member functions of the class itself and it's
descendents may use protected components.
Contents |
Previous Section
Method Dependent Access Control
One of the most important acces control features is
read-protecting object components. This is done with the
const keyword.
There are actually three usages of const:
- We can declare member functions to be const. This means
that the member functions will not modify the class of which they are
a member.
- We can declare parameters to a function to be
const. This means that the function will not attempt to
change the value of the parameter.
- We can declare variables to be const. This means we
cannot change the value of the variable. This is intended as a
replacement for the #define preprocessor macros.
Click here to see a program with
all three cases in action.
Previous Lecture |
Next Lecture |
Exercises |
Contents
Christian Goetze