Most Things You Need to Know About C++

Jian Huang

We will discuss the following topics:

  1. The class type
  2. Access Privileges
  3. Data members
  4. Member functions
  5. Memory storage of a class
  6. this pointer
  7. static members
  8. Constructors
  9. Destructors
  10. Creating objects
  11. Constants
  12. Reference types
  13. Copy constructor
  14. Operator Methods
  15. Inheritance (another lecture)
  16. Template

The Class Type

The class type replaces structs as the dominant means for declaring a user-defined data type. Like a struct a class has a set of named data elements. These data elements are called its instance variables. Unlike a C struct, a class also has a set of operations, called methods, which manipulate the class's data elements. The data elements and methods that make up a class are called its members. We will use the following class definition of an array throughout this section to illustrate the various facets of the class type:

 

  Const int ArraySize = 12;      // say, this is the default size

      class IntArray

{

      public:

          // operations performed on arrays

          IntArray( int size = ArraySize );

          ~IntArray();

          int getValue( int index );

          void setValue( int index, int value );

          int getSize();

 

      protected:

          void checkBounds( int index );

 

          // internal data representation

          int size;

          int *data;

      };

 

This array class provides several services that the normal array type does not provide:

  1. The size of the array can be determined at run-time
  2. The class range-checks the array index

Access Privileges

-       public: The class member is accessible from anywhere within an application. Typically only methods should be public.

-       protected: A protected member may be inherited by a subclass and may be accessed by any of the class's methods. However, it may not be accessed anywhere else in the application. Data members should typically be declared protected.

-       private: A private member may only be accessed by the class's member functions. The member is not inherited by any subclasses.

Prefer protected to private: Protected makes inheritance possible. Private kills inheritance. It is very rare that you would want a class to contain an instance variable but not want a subclass to contain the same instance variable.

Class members are private by default.

Information hiding: By declaring data members as protected, you hide the class's implementation from the application. Consequently, the class's implementation can be altered without requiring any alterations to the application.

If desired, access to protected data members can be efficiently provided via inline access methods. IntArray's GetSize method is an example of an inline, access method.

friend: The friend keyword gives an outside class access to a class's protected and private members. For example:

     class Dlist;
     class Dlnode {
       friend Dlist;
       protected:
         ...
     };
     

The friend declaration gives Dlist access to all of Dlnode's variables and methods. You use a friend declaration to selectively give outsiders access to a class's variables/method. Typically this is done when the outsider needs to work closely with the class or even "own" the class. For example, the only place a Dlnode will presumably be used is in a Dlist so in effect the Dlist class "owns" the Dlnode class.

Data Members

  1. Like a variable declaration, but initializers are not allowed.
  2. A class object can be declared as a data member only if its class definition has already been seen. For example, the following class definition is illegal:
     class Stack 
     {
         int topStack;
         Stack stack; // illegal--class definition not yet complete
     };
  1. A pointer to a class object can be declared as a data member as long as a forward declaration of the class has been seen. For example, the following class definition is legal:
     class Stack;   //forward declaration
     class Stack {
         int topStack;
         Stack *stack; // legal--a forward class declaration has been seen
     };

The following is also legal because a class is considered declared as soon as its class name is seen:

     class tree_node {
         tree_node *left_child;
         tree_node *right_child;
     };

Member Functions

  1. The public member functions of a class are called the class interface.
  2. Member functions defined outside the class body must be prefixed by the class's name and two colons (::). For example:

     IntArray::IntArray( int sz )

     {

         // allocate an integer array of 'size' elements.

      // new returns a pointer to this array or 0

      // 0 indicates the program has exhausted its

      // available memory: a generally fatal error

    size = sz;

    data = new int[size];

 

    for ( int ix=0; ix < sz; ix++ )

        data[ix] = 0;

     }

 

     void IntArray::checkBounds (int index)

     {

       if (index < 0) {

      printf("An array index is out of bounds. Its value is %d\n", index);

      exit(1);

    }

    if (index >= size) {

      printf("An array index exceeds the bounds of its array. The size\n");

      printf("of the array is %d but the value of the index is %d\n",

                 size, index);

      exit(1);

         }

     }

 

     int IntArray::getValue (int index)

     {

       checkBounds(index);

       return data[index];

     }

 

     void IntArray::setValue (int index, int value)

     {

       checkBounds(index);

       data[index] = value;

     }

 

     int IntArray::getSize() { return size; }

 

     IntArray::~IntArray() { delete [] data; }

     
  1. Member functions have full access privilege to all the members of the class, be they public, protected, or private.

Memory Storage of Object

After you define a class, such as IntArray, of course, you will use it. You can treat IntArray just as a normal type, like float, and declare:

IntArray myintarray1, myintarray2;

IntArray * myintarrayptr;

 

Doing so gives you two instances and a pointer to an instance of IntArray. From the aspect of memory arrangement, myintarray1 and myintarray2 have two distinct area in memory to store the data members of each of the two instances, however, there is only a single common area in memory where the methods (and functions) of IntArray are stored at. Indeed, no matter how many instances of IntArray there are, there is only one copy of executable text of IntArray’s methods in the main memory. Therefore, you can view an instance of a class as a piece of storage representing a unique state. The methods to manipulate these states (instances) only have one copy. This is why my program “yeha” works.

 

As a reminder to those who forgot, yeha breaks the encapsulation of a class that I created. Since it’s a “bad” influence to new C++ programmers, all you need to know is that it can be done and why it can be done. You’d best forget how I did it.

 

Now, we have a problem. There are multiple states (instances) in memory, how does the methods know which instance to work on?

The Implicit this Pointer

Passed as an implicit argument to every member method.

this is a pointer to the object through which this method was invoked. Consequently, the following two statements are equivalent:

     int getSize() { return size; }
     int getSize() { return this->size; }

Common uses for this

To call a method in another object and pass a reference to this object as an argument. For example, if this object is to be added to a hash table object called object_table, one might add it with the call:

       object_table.add(this);

To return a pointer to this object. We will see later why returning a pointer to oneself is sometimes useful.

Actually, there is a simple way to find out the value of this. Who can impress me?

Constructors

You have already seen how constructors are used to initialize an object when it is first created. Here are some specifics to remember about constructors:

-       They are automatically invoked when an object is created.

-       They have the same name as the class name.

-       They may be overloaded (i.e., there may be multiple constructors with different signatures).

-       They must not specify a return type or explicitly return a value.

 

Example constructor declaration:

 

      class Stack {

        protected:

          int *data;

         int top;

        public:

                 Stack( int maxSize );

          Stack( );

         ...

       };

 

Example constructor definition:

Stack::Stack( int maxSize ) {

      top = 0;

      data = new int[maxSize];

    }

 

Stack::Stack( ) {

      top = 0;

      data = new int[20];  // default maximum size

    }

 

If you do not specify any constructor functions, the C++ compiler will create an implicit one for you that takes no arguments and that does nothing.

Destructor Functions

Here are some specifics to remember about destructors:

-       They are automatically invoked when an object is destroyed.

-       They have the same name as the class name, except that they are preceded by a ~.

-       They take no arguments. Hence they may not be overloaded (i.e., there may be only one destructor per class).

-       They must not specify a return type or explicitly return a value.

 

Example destructor declaration and definition:

 

~Stack() { delete [] data; }

 

A destructor does not deallocate memory for its own object. However, it should delete any memory that the constructor explicitly allocated for member objects using the new operator. If you do not specify any destructor functions, the C++ compiler will create an implicit one for you that takes no arguments and that does nothing.

Creating and Destroying Objects

An object can be statically allocated by simply declaring it:

 

     Stack a(5);

     Stack b;

     Stack c(a);

 

A statically allocated object cannot be explicitly destroyed. However, if the block or procedure to which the object belongs goes out of scope, then the object will be automatically destroyed.

An object can be dynamically allocated using the new operator:

 

     Stack *x = new Stack(4);

     Stack *y;

     Stack *z = new Stack[5]; // allocate an array of 5 Stacks

 

     y = new Stack(*x);

 

A dynamically allocated object can be freed using the delete operator:

     delete x;

     delete [] z;

 

If an array is allocated using the new command, it must be deleted by placing [] after the delete command. If you fail to do this, the compiler will think that the object being deleted is a singleton, and it will call the destructor on only the first object.

Data Sharing

One problem with C is there is virtually no control on global variables in two aspects:

-       Who can access a global variable? Using static, only limit access to a file (translation unit) or a single function.

-       What types of operations can be performed on a global variable? For instance, today’s date can be incremented but probably should not be multiplied.

 

These two problems that cannot be addressed in C are solved in C++, with static members. Inside a class, you can declare:

-       A variable to be static. This limits the scope (visibility) of the variable to be within a class and the term static gives the variable static duration (global lifetime). All instances share the static variable. Because a class really is an abstraction of a segment of a program, somewhere else in the program implementation, static data members must be initialized. Actually, due to this exact reason (you don’t need an instance to exist before you initialize a static data member), static data members, however, are not really considered as a member of any instance (object).

-       A method to be static. This helps to encapsulate the global variable and have strong control over what operations can be performed on the global variable. Static methods are not considered member of a class, either. Therefore, static methods can not access any other non-static data members within each class.

 

For example:

class Goods

{

public:

     Goods(int w);

     ~Goods();

     int Weight();

     static int TotalWeight();

private:

     int weight;

     static int totalWeight;

};

 

Goods::Goods(int w)

{

   weight = w;

   totalWeight += w;

}

 

Goods::~Goods()

{

   totalWeight -= weight;

}

 

int Goods::Weight()

{

   return weight;

}

 

int Goods:: TotalWeight()

{

   return totalWeight;

}

 

////////////////the main function///////////////////

 

#include <iostream.h>

int Goods::totalWeight = 0;

 

int main()

{

   Goods g1(20);

   Goods g2(50);

   cout<<Goods::TotalWeight()<<endl;

   return 0;

}

Reference Types

We will try to avoid using reference types in this class because they are confusing. However, they appear in examples in both the Weiss and Schildt books and therefore you need to have at least a basic understanding of what they are. For example, if you have a class named string, you will often see a method with the following signature:

 

string::string(const string &objToBeCopied);

 

This method is a copy constructor. The thing to focus on for now is the declaration of the parameter objToBeCopied:

 

objToBeCopied is declared to be a reference of type string. It is also declared to be constant, which means that its contents cannot be altered by the method.

 

A reference type is defined by following the type specifier with an address-of (&) operator:

 

You can think of a reference type as being the same as a pointer. The only difference is that when you refer to the contents of the object pointed to by the reference type, you use the dot (.) operator rather than the arrow (->) operator. For example, if the string class contains a method called length, then the copy constructor declared above could access the length method in objToBeCopied via the notation objToBeCopied.length().

 

Why does C++ confuse the issue by introducing what is in essence a second type of pointer? No, it’s not. It really is an alias created for reasons that are subtle. The details will be covered in the revised CS365 and are not important in this course. But here are a few places what the reference types are most commonly used for:

-       A different way of passing function arguments (as opposed to pass by value)

void allocateMem (char * & mystring, int len)

{

     mystring = new char [len];

}

-       Efficient passing of object variables as argument

void dumpToFile (IntArray & intarray1)

{

     // output everything in intarray1

     …..

}

-       Functions with reference typed return values can appear on the left side of “=”

int & whichOne(int * arr1, …)

{

   //do all kinds of interesting index calculation

   //say you found out it should be element k in arr1

   int & thisone = arr1[k];

   return thisone;

}

 

whichOne(myarray, …) = 33;

 

Note: using return values in reference types can also improve program efficiency, but please make sure it’s not a reference to a local variable.

Constants

In C you declared a constant using the #define statement. For example, to specify that the boiling point of water is 212 degrees, you might have written:

 

#define WATER_BOILING_POINT 212

 

In C++ you declare a constant using the const keyword. For example:

 

const int WATER_BOILING_POINT = 212;

 

Notice that a constant declaration requires five elements:                  

-       the keyword const

-       the type of the constant (e.g., int)

-       the name of the constant (e.g., WATER_BOILING_POINT)

-       the value of the constant (e.g., 212)

-       the '=' sign and the ';'.

 

The constant keyword can be used to declare local variables, global variables, or parameters to be constant. For example, the function declaration:

 

float compute_avg(const int scores [], const int size);

 

declares that both the scores array and the size of the scores array are constant within the function. That means that within the function you promise not to modify either the contents of the scores array or the size variable. If you attempt to do so, the compiler will give you an error message. For example:

 

#include <stdio.h>
 
float compute_avg(const int scores[], int size) {
  int i, sum;
  sum = 0;
  for (i = 0; i < size; i++) {
    sum += scores[i];
    scores[i] = sum;     // can't do this--scores is a constant
  }
  return ((float)sum / size);
}
 
main() {
  int s[5] = {72, 68, 79, 81, 65};
 
  printf("%6.2f\n", compute_avg(s, 5));
}

If you try to compile this program, you get the following error message from the compiler:

UNIX> g++ temp.cc
avg.cc: In function `float compute_avg(const int *, int)':
avg.cc:8: assignment of read-only location

 

Of course, you need to apply const to class to really get to know C++. When dealing with class, if an object is passed as an argument to a function, it’s quite expensive because a constructor needs to be called each time. So, in most cases, programmers will instead pass either a pointer or a reference of an object, e.g. IntArray * iptr or IntArray & i.

 

While this approach being efficient, it creates a problem. If the function changes anything in the IntArray object, all changes take place on the original copy directly. This is how it’s done shown with an example.

class Location

{

public:

  Location (int x = 0, int y = 0) {X = x; Y = y;};

  void Move(int x, int y) {X = x; Y = x;};

  void Print() const;

private:

  int X, Y;

};

 

void Location::Print() const

{

  cout<<X<<" "<<Y<<endl;

};

 

int main()

{

  const Location A (1,2);

  //A.Move(3,4); this is illegal

  A.Print(); // this is fine

 

  Location B(2,4);

  B.Move(3,4);  //this is fine

  B.Print();

  return 0;

}

 

When A is declared as const, you can only call methods of Location class that are also const. In this case, Print is the only one you can call on A. Further, in Print, no non-const member functions can be called, as well as no data members except static ones can be modified. Now, if I have the following function:

void lookAtLocation (const & Location loc);

lookAtLocation can not call loc.move(), but can call loc.print().

Copy Constructor

The copy constructor copies the contents of one object to another object. The two objects must be instances of the same class. The copy constructor is called when:

 

-       One class object is initialized with another class object. For example:

 

string a;

string c(a);  //copy constructor called

            string b = a; //copy constructor called

 

-       A class object is passed by value as an argument to a function.

-       A class object is returned as the return value from a function.

 

The form of the copy constructor is classname(const classname&) where classname is the name of the class. The argument to the copy constructor is the object to be copied.

 

The copy constructor should copy the values of each of the parameter object's data members to each of the class's data members.

 

IMPORTANT: If a data member is a pointer, the copy constructor should not copy the pointer. Instead, it should:

1.    Allocate a new block of memory large enough to hold the contents of the object being pointed to by the pointer,

2.    Copy the object to the newly allocated memory, and

3.    Make the data member point to the new block of memory

 

As an example, suppose you have a string class:

 

     class string {

         public:

       string(const string&);

         protected:

       char *str;

       int length;

     }

Then the copy constructor should look something like:

 

     string::string(const string & s) {

       length = s.length;

       str = new char [ length + 1 ];

       

       if ( str == 0 ) {

         fprintf(stderr, "string::string: could not allocate a string of length %d\n",

                   length);

         exit(1);

         }

       strcpy( str, s.str );

     }

    

 

The problem with copying the pointer rather than the contents of the object to which the pointer points is that you end up with two objects pointing to the same block of memory. If either of the objects goes out of scope or is destroyed using the delete operator, then the other object will be left with a dangling pointer.

 

Important: If your class contains pointers to dynamically allocated memory, define a copy constructor for that class that adheres to the above convention of allocating memory for the object pointed to by a pointer and copying the object to that memory. If you fail to define a copy constructor, C++ will define a default copy constructor that will do a bit-wise copy on all the data members. This means that pointers will be copied and you can end up with dangling pointers as described above.

 

Any class must have a copy constructor. If and only if a class doesn’t have one, then compiler will generate a default one for you, which essentially mirrors the value of data members.

Operator Methods

C++ allows most of its operators (e.g., =, ==, +, -, ++, ->) to be redefined by a class. This redefinition is called overloading, because the operator has different definitions depending on which object it is being invoked on. To fully understand operators, you must be smart enough to understand these operators are really functions.

 

Examples:

 

     class string {

       public:

          string& operator=(const string &);

          string& operator+=(const string&);

          string& operator+=(const char*);

       ...

     }

 

     string& string::operator=(const string &s)

       // don't do anything if the object to be copied is the same as

       // the current object

     {

       if (*this != s) //something is tricky here J

       {

         length = s.length;

         delete [] str;    // free the memory for the old string

         str = new char [ length + 1 ]; // allocate memory for the new string

       

         if ( str == 0 )

         {

           fprintf(stderr, "string=: couldn’t allocate string of length %d\n",length);

           exit(1);

         }

         strcpy( str, s.str );

       }

     }

    

     string& operator+=(const string& s) {

         len += s.len;

         char *p = new char[len+1];

         assert( p != 0);

         strcpy(p, str);

         strcat(p, s.str);

         delete [] str;   // free up old memory!

         str = p;

         return *this;

     }

 

     string& operator+=(const char *s) {

       len += strlen(s);

       char *p = new char[len+1];

       assert( p != 0);

       strcpy(p, str);

       strcat(p, s);

       delete [] str;   // free up old memory!

       str = p;

       return *this;

     }    

 

Operators should take care to release memory if they are replacing one piece of dynamically allocated memory with another (see the assignment operator for string and the concatenate operator for string as examples).

 

Template

Let’s look at the following example:

int abs(int x)

{

  return x < 0 ? -x : x;

}

 

double abs(double x)

{

  return x < 0 ? -x : x;

}

 

The function abs itself doesn’t change much whether it’s for double or int, however, you need separate functions for it. This is why a lot of programmers use macros in their code. In C++, there is a better solution, that is, template.

 

template <typename T>

T abs (T x)

{

  return x < 0 ? -x : x;

}

 

“template” and “typename” are both reserved key words. Besides “typename”, you can use “class” key word in its place. Both would work, however, since in C++, “class” is also used to define real classes, so, in newer versions of C++ standard, “typename” key word is advocated. I personally prefer “typename”. “T” is the identifier used to represent the actual type, to be determined in a real program. Of course, you don’t have to use “T”. Put your favorite word in there. You call the function just like a normal call:

double d = -50.9999;

d = abs(d);

 

What about classes in templates? Here is an example:

template <typename T>

class Array {

  public:

         Array(int s);

         ~Array();

         Array & operator = (const Array & val);

  protected:

         int size;

         T * elements;

}

 

template<typename T>

Array <T>::Array(int s)

{

  size = s;

  elements = new T [s];

}

 

template <typename T>

Array <T> & Array <T> :: operator = (const Array<T> & val)

{

  delete [] elements;

  elements = new T [val.size];

  for (int i = 0; i < val.size; i++)

         elements[i] = val.elements[i];

  size = val.size;

  return *this;

}

 

......

 

Obviously, this Array template lets you create whatever typed array you want, being it int, float, double, or your self-defined complex number, matrix, even, if you want, 3D graphics objects, etc. For example, you can define an object:

Array<int> myarray(50);

myarray = …;

 

Similarly, class templates can be used as function arguments, as well as base class. The rules of thumb are: template itself is simple but quite flexible. However, it’s quite tricky to use other people’s template libraries to get good quality code (speed, storage, …) Don’t think using STL can solve all your problems. Often times the contrary is true, unfortunately.

 

Homework

Using the Goods class in the notes, tell me what’s wrong with the following program without actually running it? What is the simplest way to fix it?

 

void Display (Goods gd)

{

   cout<<gd.Weight()<<endl;

}

int Goods::totalWeight = 0;

 

int main()

{

   Goods g1(50);

   Goods g2(30);

   Display(g1);

   Display(g2);

   cout<<Goods::TotalWeight()<<endl;

   return 0;

}