Examples On Classes and Objects
Examples On Classes and Objects
| HOME
C++ Programming
1.1 throw, try and catch
1.2 Example: The Time Class
1.3 Class exception and its subcl
Language 1.4 Creating Your Own exception
2. Storage Duration, Scopes and Li
PositiveInteger.h
1 /* Header for the PositiveInteger class (PositiveInteger.h) */
2 #ifndef POSITIVE_INTEGER_H
3 #define POSITIVE_INTEGER_H
4
5 class PositiveInteger {
6 private:
7 int value; // positive integer (>0) only
8
9 public:
10 PositiveInteger(int value = 1);
11 void setValue(int value);
12 int getValue() const;
13 };
14
15 #endif
PositiveInteger.cpp
1 /* Implementation for the PositiveInteger Class (PositiveInteger.cpp) */
2 #include <iostream>
3 #include <stdexcept> // Needed for exception handling
4 #include "PositiveInteger.h"
5 using namespace std;
6
7 // Constructor with input validation
8 PositiveInteger::PositiveInteger(int value) {
9 // Call setter to perform input validation
10 setValue(value);
11 }
12
13 // Setter with input validation
14 void PositiveInteger::setValue(int v) {
15 if (v > 0) {
16 value = v;
17 } else {
18 throw invalid_argument("value shall be more than 0.");
19 // need <stdexcept>
20 }
21 }
22
23 // Getter
24 int PositiveInteger::getValue() const {
25 return value;
26 }
Program Notes:
The constructor calls the setter setValue(), which validates the input value.
In setValue(), if the input is invalid, instead of print an error message or terminate the program (via
abort() or exit()), we throw an invalid_argument exception with an appropriate message. The
invalid_argument is one of the system-defined exception classes in header <stdexcept>.
TestPositiveInteger.cpp
1 /* Test Driver for the PositiveInteger class (TestPositiveInteger.cpp) */
2 #include <iostream>
3 #include <stdexcept> // Needed for exception handling
4 #include "PositiveInteger.h"
5 using namespace std;
6
7 int main() {
8 // Valid input
9 PositiveInteger i1(8);
10 cout << i1.getValue() << endl;
11
12 // Invalid input without try-catch
13 // PositiveInteger i2(-8); // Program terminate abruptly
14
15 // Graceful handling of exception with try-catch
16 try {
17 cout << "begin try 1..." << endl;
18 PositiveInteger i3(-8);
19 // Exception thrown.
20 // Skip the remaining statements in try and jump to catch.
21 cout << i3.getValue() << endl;
22 cout << "end try 1..." << endl;
23 // Continue to the next statement after try-catch, if there is no exception
24 } catch (invalid_argument & ex) { // need <stdexcept>
25 cout << "Exception: " << ex.what() << endl;
26 // Continue to the next statement after try-catch
27 }
28 cout << "after try-catch 1..." << endl;
29
30 // Graceful handling of exception with try-catch
31 try {
32 cout << "begin try 2..." << endl;
33 PositiveInteger i4(8); // no exception thrown
34 cout << i4.getValue() << endl;
35 cout << "end try 2..." << endl;
36 // Continue to the next statement after try-catch, if there is no exception
37 } catch (invalid_argument & ex) { // need <stdexcept>
38 cout << "Exception: " << ex.what() << endl;
39 // Continue to the next statement after try-catch
40 }
41 cout << "after try-catch 2..." << endl;
42 }
Program Notes:
Without the try-catch statement, the program abnormally terminated, when a throw statement is
encountered.
With try-catch, the execution skips the rest of the try-clause, when a throw statement is encountered. It
jumps into the catch-clause; and continues to the next statement after the try-catch. Take note that the
program is not abnormally terminated.
If no exception is encountered, the execution completes the try clause, skip the catch-clause, and
continues to the next statement after the try-catch.
The catch takes a parameter of a reference to exception class (in header <exception>) or its subclass
(such as invalid_argument in header <stdexcept>).
The exception class has a member function what(), which prints the exception message.
You can have multiple catch-clauses, each catching a exception type. If an exception is thrown, the catch-
clauses are matched in sequential manner. The catch clause catches an exception also catches its subclass.
If an exception is thrown, but no catch-clause matches the exception type. The program returns to its caller
(unwinding the function stack), and repeat the exception handling process in the caller.
MyException.h
1 /* Header for the MyException class (MyException.h) */
2 #ifndef MY_EXCEPTION_H
3 #define MY_EXCEPTION_H
4
5 #include <stdexcept>
6
7 class MyException : public std::logic_error {
8 public:
9 // Constructor
10 MyException() : std::logic_error("my custom error") { };
11 };
12
13 #endif
Program Notes:
Your custom exception shall subclass exception or its subclass, in this case, logic_error.
Provide a constructor with a custom what-message.
TestMyException.cpp
1 /* Test Driver for the MyException class (TestMyException.cpp) */
2 #include <iostream>
3 #include "MyException.h"
4 using namespace std;
5
6 void fun() {
7 throw MyException();
8 }
9
10 int main() {
11 try {
12 fun();
13 } catch (MyException & ex) {
14 cout << ex.what() << endl;
15 }
16 }
Compiler typically allocates 3 chunks of storage for static variables, automatic variables, and dynamically
allocated variables, respectively.
Scope - Local or Global
The scope of a variable determines which parts of the program can reference the variable, i.e., the visibility.
Some variables can be referenced throughout the program (file-scope or global-scope); while others can only
be referenced in a limited part of the program (block-scope or local-scope). For example, a local automatic
variable defined inside a function (or a block) is visible by that function (or block), and not outside the function
(or block).
Take note that a variable might exist in memory (determined by its storage duration) but not visible by a certain
part of the program (determined by its scope and linkage). For examples, a local variable created within a
function still exists when the function calls another function, but it is not visible by the second function. A
static variable declared inside a function exists throughout the program duration, but visible only by that
function. A static variable with internal linkage exists throughout the program duration, but visible only to the
file in which it is defined.
Automatic storage is used to converse memory, as these variables does not exist throughout the entire
program, but created when needed (and destroyed).
You can use the auto specifier to explicitly specify automatic storage class for a variable. However, it is rarely
used as auto is the default for local variables. In C++11, the keyword "auto" is assigned a new meaning to
indicates automatic type deduction. The old meaning of automatic storage class is no longer valid in C++11.
For example,
Take that that C++ allocates local variable when the execution enters the block, but the local variable is only
visible (i.e., scope begins) after its declaration statement. The variable's scope ends and is deallocated when the
execution leaves the block. You can verify that local variables are allocated when execution enters the block
using a graphic debugger (on CodeBlock or Eclipse) - Try it out!
Automatic variables are not initialized. You MUST assign an initial value. Compiler may not issue a warning/error
if you use uninitialized local variable.
Automatic variables are typically allocated in a function stack (a Last-in-First-out LIFO queue), where the new
data is stacked on top of the existing data. When a function is called, the caller pushes the arguments onto the
stack. The function's local variables are also pushed (allocated) onto the stack. When the function exits, the top-
of-stack pointer is reset and all variables are freed.
If the inner block has a variable with the same name of the outer block (e.g., rename blockVar to localVar in
the above example), we say that the inner block's variable hides the outer block's variable. The outer block's
variable is temporarily out of scope, until the inner block exits.
Static variables are initialized to zero (all its bits set to 0), if no initial values are provided. All elements of static
array and structures are initialized to zero too. On the other hand, automatic local variables are not initialized.
Static variables are allocated at fixed memory locations (instead of function stack) as they last for the entire
program duration.
For example,
Try using a graphic debugger (CodeBlocks or Eclipse) to check the duration (created/destroyed) and scope
(visible) of the variables.
The static variable blockVar, which is declared inside the function fun(), has local scope and no linkage. It
can only visible (scope) inside the block in which it is defined, just like an automatic variable. But unlike
automatic variable, static variable retains its memory (and value) across multiple function calls. Both the
static variables allFileVar and thisFileVar are visible (scope) immediately after their declarations.
thisFileVar, with "static" specifier, has internal linkage and can be used by all functions in this file. On the
other hand, allFileVar, without the "static" specifier, has external linkage and can be used in other files with
"extern" specifier (which will be described later).
// File1.cpp
extern int globalVar; // Declare that this variable is defined in another file (external variable).
// Cannot assign an value.
// Need to link to the other file.
// File2.cpp
int globalVar = 88; // Definition here
// or
extern int globalVar = 88; // The "extern" specifier is optional.
// The initialization indicates definition
C/C++ has the so-called "one definition rule", which states that a variable can only be defined once. In the
above example, the statement "int globalVar" in File2.cpp is called the defining declaration (or simply
definition), which causes memory to be allocated; the statement "extern int globalVar" in File1.cpp is
called referencing declaration (or simply declaration), which does not allocate memory but links to an existing
memory.
2.4 Summary
Recap that the duration determines when the variable is created and destroyed; scope determines which part of
the program can access the variable; and linkage determines whether the variable is available in other source
files.
The "volatile" qualifier indicates that the content of the storage location could be altered outside your program,
e.g., by an external hardware. This qualifier is needed to tell compiler not to optimize this particular location
(e.g., not to store in register, not to re-order the statement, or collapse multiple statements).
The mutable specifier can be used in struct or class to indicate that a particular data member is modifiable
even though the instance is declared const.
You use use the "static" specifier to confine the function to internal linkage (accessible in this file only). The
"one definition only" rule applies to all non-inline functions. That is, there is only one function definition. Each
file shall have the function prototype (declaration). Since inline functions are often placed in the header, which
will be included in all file, the "one definition rule" makes an exception. However, all copies of inline function
shall be identical.
If a function is declared static in its prototype, the C++ compiler/linker search the current file only for the
function definition. Otherwise, C++ compiler/linker searches all the program files. It issues an error if it finds
more than one definitions. If the function is not found in all the program files, it then searches the libraries.
extern "C" void function1 (int, int); // Use C function naming protocol
// without name manglind
extern "C++" void function2 (double, double); // Use C++ naming mangling
extern void function (double, double); // Same as above
2.7 Other Scopes
Besides the block-scope (local-scope), file-scope (global-scope) with internal or external linkage, there are:
Function Scope: A label (identified by an identifier followed by a colon, e.g., loop:) can be referenced
within the entire function in which it is defined.
Function Prototype Scope: The optional identifiers defined in the function prototype is confined to the
function prototype only. There are not bind to the function definition.
Class Scope: Class members (data and function) have class scope and are visible inside the class definition.
You can use the same identifier in two different classes. You cannot access a class member directly outside
the class definition, even for public members (which is accessed via the dot operator in the form of
objectName.memberName).
Namespace Scope: Name defined under a namespace are visible within the namespace definition only. You
need to use the scope resolution operator to reference the name outside the namespace definition in the
form of namespace::memberName. With the introduction of namespace in C++, the global scope is
changed to global namespace scope, identified by a nameless namespace or ::memberName.
static_cast
static_cast is used for force implicit conversion. It throws a type-cast error if the conversion fails.
You can use static_cast to convert values of various fundamental types, e.g., from double to int, from
float to long.
[TODO] Example
dynamic_cast
dynamic_cast can be used to verify the type of an object at runtime, before performing the type conversion. It
is primarily used to perform "safe downcasting"
dynamic_cast<Type *>(ptr)
It converts the pointer ptr to a pointer of the type Type, in runtime, if ptr is pointing to an object of Type, or
its direct or indirect subclass. Otherwise, it returns 0, or null pointer.
You can use dynamic_cast in a condition to ascertain the type of the object, before performing certain
operations.
[TODO] Example
const_cast
The const_cast can be used to drop the const label, so as to alter its contents (i.e., cast away the const-ness
or volatile-ness). This is useful if you have a variable which is constant most of the time, but need to be
changed in some circumstances. You can declare the variable as const, and use const_cast to alter its value.
The syntax is:
const_cast<Type>(expression)
[TODO] Example
reinterpret_cast
Used for low-level casts that yield implementation-dependent result, e.g., casting a pointer to an int.
int main() {
const int ROWS = 8;
......
}
Function's Parameters
In C++, objects are pass-by-value into function by default, which has no side effect but involves calling the copy
constructor to make a clone copy (an expensive operation for huge objects). Objects should be passed into
function by reference as far as possible for performance. However, in pass-by-reference, changes made inside
the function have side effect of modifying the caller's object. We could use keyword const to enforce
immutability, if we do not wish to change the object inside the function. Instead of using pass-by-value to
prevent side-effect, it is better to use pass-by-reference-to-const.
You can also use const for array to prevent it from being modified inside the function (as array is an pointer).
It is recommended to leave out the const for fundamental types (int, double), as they are passed by value.
Although you can use const keyword to prevent modification of these local parameters (which is rarely
necessary), the keyword can be confusing. If needed, you may include it in the implementation, but leave them
out from the header.
const function parameters = not modifying the caller's copy (in pass-by-reference).
MyComplex operator+ (const MyComplex & lhs, const MyComplex & rhs);
// non-const return type can be used as lvalue
Function return value of fundamental types can never be the lvalue, and trigger compilation error.
Class
A const data member cannot be modified (as usual).
A const member function (with const keyword at the end of the function) cannot modify data members.
Object
A const object can only invoke const member function. It cannot invoke non-const member function. A non-
const object can invoke both const and non-const member function.
If a function is overloaded with a const and a non-const version, a const object will match with a const
member function. A non-const object will match with a non-const function. For example, the at() function of
string class has two versions:
A const string object will invoke the const version, which returns a const char & that cannot be used a
lvalue.
const string str1("hello"); // const string object
cout << str1.at(1); // okay
str1.at(1) = 'x'; // error - returned const char & cannot be lvalue
Pointer
1. Non-constant pointer to constant data: Data pointed to CANNOT be changed; but pointer CAN be
changed to point to another data. For example,
int i1 = 8, i2 = 9;
const int * iptr = &i1; // non-constant pointer pointing to constant data
// *iptr = 9; // error: assignment of read-only location
iptr = &i2; // okay
2. Constant pointer to non-constant data: Data pointed to CAN be changed; but pointer CANNOT be
changed to point to another data. For example,
int i1 = 8, i2 = 9;
int * const iptr = &i1; // constant pointer pointing to non-constant data
*iptr = 9; // okay
// iptr = &i2; // error: assignment of read-only variable
3. Constant pointer to constant data: Data pointed to CANNOT be changed; and pointer CANNOT be
changed to point to another data. For example,
int i1 = 8, i2 = 9;
const int * const iptr = &i1; // constant pointer pointing to constant data
// *iptr = 9; // error: assignment of read-only variable
// iptr = &i2; // error: assignment of read-only variable
4. Non-constant pointer to non-constant data: Data pointed to CAN be changed; and pointer CAN be
changed to point to another data. For example,
int i1 = 8, i2 = 9;
int * iptr = &i1; // non-constant pointer pointing to non-constant data
*iptr = 9; // okay
iptr = &i2; // okay
If the keyword const appears before (to the left) of the *, what is pointed-to is a constant. If it appears after (to
the right) of *, the pointer itself is a constant.
6. C++ Keywords
Operators: sizeof.
Compound Types: enum, struct, union.
Feedback, comments, corrections, and errata can be sent to Chua Hock-Chuan (ehchua@ntu.edu.sg) |
HOME