Object Construction and Destruction
Contents
- Local and Global Variables
- Reference Types
- Functions in C++
- Basic Input and Output
- Creating and Destroying Objects - Constructors and Destructors
1. Local and Global Variables
(Ref. Lippman 8.1-8.3)
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.
}
2. Reference Types
(Ref. Lippman 3.6)
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);
}
3. Functions in C++
Argument Passing
(Ref. Lippman 7.3)
Arguments can be passed to functions in two ways. These techniques are known as
Pass by value.
Pass by reference.
When an argument is passed by value, the function gets its own local copy of the object that was passed in. On the other hand, when an argument is passed by reference, the function simply refers to the object in the calling program.
// 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.
}
Return by Reference
(Ref. Lippman 7.4)
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);
}
Default Arguments
(Ref. Lippman 7.3.5)
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.
}
Function Overloading
(Ref. Lippman 9.1)
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.
}
Inline Functions
(Ref. Lippman 3.15, 7.6)
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.
4. Basic Input and Output
(Ref. Lippman 1.5)
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.
Reading from Standard Input: cin
cin is an object of type istream that allows us to read in a stream of data from standard input. It is functionally equivalent to the scanf() function in C. The following example shows how cin is used in conjunction with the >> operator. Note that the >> points towards the object into which we are reading data.
#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. */
}
Writing to Standard Output: cout
cout is an object of type ostream that allows us to write out a stream of data to standard output. It is functionally equivalent to the printf() function in C. The following example shows how cout is used in conjunction with the << operator. Note that the << points away from the object from which we are writing out data.
#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. */
}
Writing to Standard Error: cerr
cerr is also an object of type ostream. It is provided for the purpose of writing out warning and error messages to standard error. The usage of cerr is identical to that of cout. Why then should we bother with cerr? The reason is that it makes it easier to filter out warning and error messages from real data. For example, suppose that we compile the following program into an executable named foo:
#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
5. Creating and Destroying Objects - Constructors and Destructors
(Ref. Lippman 14.1-14.3)
Let's take a closer look at how constructors and destructors work.
A Point Class
Here is a complete example of a Point class. We have organized the code into three separate files:
point.h contains the declaration of the class, which describes the structure of a Point object.
point.C contains the definition of the class i.e. the actual implementation of the methods.
point_test.C is a program that uses the Point class.
Our Point class has three constructors and one destructor.
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();
}