| 1.124 Lecture 3 | 9/17/1998 |
Local variables are objects that are only accessible within a single function (or a sub-block within a function.) Global variables, on the other hand, are objects that are generally accessible to every function in a program. It is possible, though potentially confusing, for a local object and a global object to share the same name. In following example, the local object x shadows the object x in the global namespace. We must therefore use the global scope operator, ::, to access the global object.
main_file.C
float x; // A global object.
int main () {
float x;
// A local object with the same name.
x = 5.0; //
This refers to the local object.
::x = 7.0; // This refers
to the global object.
}
What happens if we need to access the global object in another file? The object has already been defined in main_file.C, so we should not set aside new memory for it. We can inform the compiler of the existence of the global object using the extern keyword.
another_file.C
extern float x; // Declares the existence of a global object external to this file.
void do_something() {
x = 3;
// Refers to the global object defined in main_file.C.
}
Reference types are a convenient alternative way to use the functionality that pointers provide. A reference is just a nickname for existing storage. The following example defines an integer object, i, and then it defines a reference variable, r, by the statement
int& r = i;
Be careful not to confuse this use of & with the address of operator. Also note that, unlike a pointer, a reference must be initialized at the time it is defined.
#include <stdio.h>
int main() {
int i = 0;
int& r = i; // Create a
reference to i.
i++;
printf("r = %d\n", r);
}
Arguments can be passed to functions in two ways. These techniques are known as
// Pass by value.
void increment (int i) {
i++;
// Modifies a local variable.
}
// Pass by reference.
void decrement (int& i) {
i--;
// Modifies storage in the calling function.
}
#include <stdio.h>
int main () {
int k = 0;
increment(k);
// This has no effect on k.
decrement(k);
// This will modify k.
printf("%d\n", k);
}
Passing a large object by reference can improve efficiency since it avoids the overhead of creating an extra copy. However, it is important to understand the potentially undesirable side effects that can occur. If we want to protect against modifying objects in the calling program, we can pass the argument as a constant reference:
// Pass by reference.
void decrement (const int& i) {
i--;
// This statement is now illegal.
}
A function may return a reference to an object, as long as the object is not local to the function. We may decide to return an object by reference for efficiency reasons (to avoid creating an extra copy). Returning by reference also allows us to have function calls that appear on the left hand side of an assignment statement. In the following contrived example, select_month() is used to pick out the month member of the object today and set its value to 9.
struct date {
int day;
int month;
int year;
};
int& select_month(struct date &d) {
return d.month;
}
#include <stdio.h>
int main() {
struct date today;
select_month(today) = 9;
// This is equivalent to: today.month = 9;
printf("%d\n", today.month);
}
C++ allows us to specify default values for function arguments. Arguments with default values must all appear at the end of the argument list. In the following example, the third argument of move() has a default value of zero.
void move(int dx, int dy, int dz = 0) {
// Move some object in 3D space. If dz
= 0, then move the object in 2D space.
}
int main() {
move(2, 3, 5);
move(2, 3); //
dz assumes the default value, 0.
}
In C++, two functions can share the same name as long as their signatures are different. The signature of a function is another name for its parameter list. Function overloading is useful when two or more functionally similar tasks need to be implemented in different ways. For example:
void draw(double center, double radius) {
// Draw a circle.
}
void draw(int left, int top, int right, int bottom) {
// Draw a rectangle.
}
int main() {
draw(0, 5);
// This will draw a circle.
draw(0, 4, 6, 8);
// This will draw a rectangle.
}
Every function call involves some overhead. If a small function
has to be called a large number of times, the relative overhead can be
high. In such instances, it makes sense to ask the compiler to expand
the function inline. In the following example, we have used the inline
keyword to make swap() an inline function.
inline void swap(int& a, int& b) {
int tmp = a;
a = b;
b = tmp;
}
#include <stdio.h>
main() {
int i = 2, j = 3;
swap(i, j);
printf("i = %d j = %d\n", i, j);
}
This code will be expanded as
main() {
int i = 2, j = 3;
int tmp = i;
i = j;
j = tmp;
printf("i = %d j = %d\n", i, j);
}
Whenever the compiler needs to expand a call to an inline function,
it needs to know the function definition. For this reason, inline
functions are usually placed in a header file that can be included where
necessary. Note that the inline specification is only a recommendation
to the compiler, which the compiler may choose to ignore. For example,
a recursive function cannot be completely expanded inline.
C++ provides three predefined objects for basic input and output
operations: cin, cout and cerr. All three objects
can be accessed by including the header file iostream.h.
#include <iostream.h>
// Provides access to cin and cout.
#include <stdio.h>
/* Provides access to printf and scanf. */
int main() {
int i;
cin >> i;
// Uses the stream input object, cin, to read data into i.
scanf("%d", &i);
/* Equivalent C-style statement. */
float a;
cin >> i >> a;
// Reads multiple values from standard input.
scanf("%d%f", &i, &a);
/* Equivalent C-style statement. */
}
#include <iostream.h>
// Provides access to cin and cout.
#include <stdio.h>
/* Provides access to printf and scanf. */
int main() {
cout << "Hello World!\n";
// Uses the stream output object, cout, to print out a string.
printf("Hello World!\n");
/* Equivalent C-style statement. */
int i = 7;
cout << "i = " << i << endl;
// Sends multiple objects to standard output.
printf("i = %d\n", i);
/* Equivalent C-style statement. */
}
#include <iostream.h>
int main() {
int i = 7;
cout << i << endl;
// This is real data.
cerr << "A warning message" << endl;
// This is a warning.
}
We could separate the data from the warning by redirecting the standard output to a file, while allowing the standard error to be printed on our console.
athena% foo > temp
A warning message
athena% cat temp
7
Let's take a closer look at how constructors and destructors work.
Point();
// The default constructor.
Point(float fX, float fY);
// A constructor that takes two floats.
Point(const Point& p);
// The copy constructor.
~Point();
// The destructor.
These constructors can be respectively invoked by object definitions such as
Point a;
Point b(1.0, 2.0);
Point c(b);
The default constructor, Point(), is so named because it can be invoked without any arguments. In our example, the default constructor initializes the Point to (0,0). The second constructor creates a Point from a pair of coordinates of type float. Note that we could combine these two constructors into a single constructor which has default arguments:
Point(float fX=0.0, float fY=0.0);
The third constructor is known as a copy constructor since it creates one Point from another. The object that we want to clone is passed in as a constant reference. Note that we cannot pass by value in this instance because doing so would lead to an unterminated recursive call to the copy constructor. In this example, the destructor does not have to perform any clean-up operations. Later on, we will see examples where the destructor has to release dynamically allocated memory.
Constructors and destructors can be triggered more often than you may
imagine. For example, each time a Point is passed to a function
by value, a local copy of the object is created. Likewise, each time
a Point is returned by value, a temporary copy of the object is
created in the calling program. In both cases, we will see an extra
call to the copy constructor, and an extra call to the destructor.
You are encouraged to put print statements in every constructor and in
the destructor, and then carefully observe what happens.
point.h
// Declaration of class Point.
#ifndef _POINT_H_
#define _POINT_H_
#include <iostream.h>
class Point {
// The state of a Point object. Property
variables are typically
// set up as private data members, which are
read from and
// written to via public access methods.
private:
float mfX;
float mfY;
// The behavior of a Point object.
public:
Point();
// The default constructor.
Point(float fX, float fY);
// A constructor that takes two floats.
Point(const Point& p);
// The copy constructor.
~Point();
// The destructor.
void print() {
// This function will be made inline by default.
cout << "(" <<
mfX << "," << mfY << ")" << endl;
}
void set_x(float fX);
float get_x();
void set_y(float fX);
float get_y();
};
#endif // _POINT_H_
point.C
// Definition class Point.
#include "point.h"
// A constructor which creates a Point object at (0,0).
Point::Point() {
cout << "In constructor Point::Point()"
<< endl;
mfX = 0.0;
mfY = 0.0;
}
// A constructor which creates a Point object from two
// floats.
Point::Point(float fX, float fY) {
cout << "In constructor Point::Point(float
fX, float fY)" << endl;
mfX = fX;
mfY = fY;
}
// A constructor which creates a Point object from
// another Point object.
Point::Point(const Point& p) {
cout << "In constructor Point::Point(const
Point& p)" << endl;
mfX = p.mfX;
mfY = p.mfY;
}
// The destructor.
Point::~Point() {
cout << "In destructor Point::~Point()" <<
endl;
}
// Modifier for x coordinate.
void Point::set_x(float fX) {
mfX = fX;
}
// Accessor for x coordinate.
float Point::get_x() {
return mfX;
}
// Modifier for y coordinate.
void Point::set_y(float fY) {
mfY = fY;
}
// Accessor for y coordinate.
float Point::get_y() {
return mfY;
}
point_test.C
// Test program for the Point class.
#include "point.h"
void main() {
Point a;
Point b(1.0, 2.0);
Point c(b);
// Print out the current state of all objects.
a.print();
b.print();
c.print();
b.set_x(3.0);
b.set_y(4.0);
// Print out the current state of b.
cout << endl;
b.print();
}