Class: User-Defined Data Types
Classes and structs are essentially the same except structs' default access modifier is public.
The struct is a carry-over from the C. In C++, classes are generally used.
Class | Struct |
---|---|
class Point { public: double x,y; } | Struct Point { double x,y; } |
private by default | public by default |
#include <iostream> using namespace std; class Point { public: double x, y; }; class Vector { public: Point start, end; }; int main() { Vector vec1; vec1.start.x = 1.0; vec1.start.y = 2.0; vec1.end.x = 3.0; vec1.end.y = 4.0; Vector vec2; vec1.end.x = 5.0; vec1.end.y = 6.0; vec2.end.x = 7.0; vec2.end.y = 8.0; }
Let's add a function to help the Point class so that it can use offset() function and build the second point:
#include <iostream> using namespace std; class Point { public: double x, y; }; class Vector { public: Point start, end; }; // pass by value void offset(Point p , double dx, double dy) { p.x += dx; p.y += dy; } int main() { Point p; p.x = 1.0; p.y = 2.0; offset(p, 3.0, 4.0); cout << "p(" << p.x << "," << p.y << ")" << endl; // unchanged (1,2) return 0; }
As we see from the result, passing by value passes a copy of the class instance to the function, and the changes made in the function aren't preserved.
We need to pass the class using a reference to the class so that the changes are reflected in the original:
#include <iostream> using namespace std; class Point { public: double x, y; }; class Vector { public: Point start, end; }; // pass by reference void offset(Point &p;, double dx, double dy) { p.x += dx; p.y += dy; } int main() { Point p; p.x = 1.0; p.y = 2.0; offset(p, 3.0, 4.0); cout << "p(" << p.x << "," << p.y << ")" << endl; // changed (4,6) return 0; }
In the previous example, the offset() function is closely related to the Point class, and there could be many more functions that we can tie them to the class. The following code demonstrates how we link the functions to the class. We call the function a member function or a method:
#include <iostream> using namespace std; class Point { public: void offset(double offsetX, double offsetY) { x += offsetX; y += offsetY; } void print() { cout << "(x,y)=" << "(" << x << "," << y << ")" << endl; } double x, y; }; class Vector { public: void offset(double offsetX, double offsetY) { start.x += offsetX; start.y += offsetY; end.x += offsetX; end.y += offsetY; } void print() { cout << "start=(" << start.x << "," << start.y << ")" << endl; cout << "end=(" << end.x << "," << end.y << ")" << endl; } Point start, end; }; int main() { Point p1; p1.x = 1.0; p1.y = 2.0; Point p2; p2.x = 3.0; p2.y = 4.0; p1.offset(5.0, 6.0); p2.offset(7.0, 8.0); p1.print(); p2.print(); Vector v1; v1.start = p1; v1.end = p2; Vector v2; v2 = v1; v2.offset(10.0, 20.0); v1.print(); v2.print(); return 0; }
Output from the run:
(x,y)=(6,8) (x,y)=(10,12) start=(6,8) end=(10,12) start=(16,28) end=(20,32)
In the example, we made two functions as member functions (methods): offset() and print().
Manually initializing our fields of a class can get tedious. So, we need to initialize them when we create an instance. A method called when we are creating an instance is a constructor.
class Point { public: Point() { x = 0.0; y = 0.0; } double x, y; };
#include <iostream> using namespace std; class Point { public: Point(double xi, double yi) { x = xi; y = yi; } void print() { cout << "(x,y)=" << "(" << x << "," << y << ")" << endl; } double x, y; }; int main() { Point p1(1.0, 2.0); Point p2(3.0, 4.0); p1.print(); p2.print(); return 0; }
Output is:
(x,y)=(1,2) (x,y)=(3,4)
A class can have multiple constructors:
#include <iostream> using namespace std; class Point { public: // default constructor Point() { x = 0.0; y = 0.0; cout << "default constructor" << endl; } // constructor with two parameters Point(double xi, double yi) { x = xi; y = yi; cout << "two-parameter constructor" << endl; } void print() { cout << "(x,y)=" << "(" << x << "," << y << ")" << endl; } double x, y; }; int main() { Point p(3.0, 4.0); Point q = p; // q.x = 3.0, q.y = 4.0 return 0; }
When we assign one class instance to another, it copies all fields from one class and constructs a new instance. Actually, behind the scene, we are invoking a default copy constructor provided by our compiler.
In the previous example, when we do:
int main() { Point p(3.0, 4.0); Point q = p; // q.x = 3.0, q.y = 4.0 return 0; }
we are invoking a copy constructor.
We can have our own copy constructor:
class Point { public: ... Point(Point &p;) { x = p.x; y = p.y; cout << "customized copy constructor" << endl; } ... };
Let's look at the following example:
#include <iostream> using namespace std; class Student { public: int id; char *name; Student() { id = 0; name = ""; } }; int main() { Student student1; student1.id = 10; char nm[] = "Clinton"; student1.name = nm; Student student2 = student1; student2.name[0] = 'P'; cout << student1.name; // Plinton return 0; }
As shown in the example, we made another instance of Student class which is student2 from an existing instance student1. However, later we modified an element of the name array. It turned out that modifying a field of one instance can affect the other instance, and that's not we wanted.
So, to control the assigning operation, we need our own copy constructor not the default copy constructor that compiler provides:
#include <iostream> using namespace std; class Student { public: int id; char *name; Student() { id = 0; name = ""; } Student(Student &s;) { id = s.id; name = strdup(s.name); } }; int main() { Student student1; student1.id = 10; char nm[] = "Clinton"; student1.name = nm; Student student2 = student1; student2.name[0] = 'P'; cout << student1.name << endl; // Clinton cout << student2.name << endl; // Plinton return 0; }
Note that strdup() in our own copy constructor does dynamic memory allocation for the character array including the end character '\0' and returns the address of the heap memory. In other words, the new instance has its own memory to store the name not shared one with which it was instantiated from.
Access modifier defines where our fields/methods can be accessed from.
- public: can be accessed from anywhere
- private: can only be accessed within the class
#include <iostream> using namespace std; class Point { public: Point(double xi, double yi) { x = xi; y = yi; } void print() { cout << "(x,y)=" << "(" << x << "," << y << ")" << endl; } double x, y; }; int main() { Point p1(1.0, 2.0); // allowed p1.x = 111; p1.y = 222; return 0; }
The member x and y are accessible from outside of a class since their access modifier is public. However, in the example below, it's private, and cannot be accessed from outside.
#include <iostream> using namespace std; class Point { public: Point(double xi, double yi) { x = xi; y = yi; } void print() { cout << "(x,y)=" << "(" << x << "," << y << ")" << endl; } private: double x, y; }; int main() { Point p1(1.0, 2.0); // access not allowed p1.x = 111; p1.y = 222; return 0; }
There is a way to access the private members using member functions of the class:
#include <iostream> using namespace std; class Point { public: Point(double xi, double yi) { x = xi; y = yi; } void print() { cout << "(x,y)=" << "(" << x << "," << y << ")" << endl; } double getX() { return x;} double getY() { return y;} void setX(double xi) { x = xi;} void setY(double yi) { y = yi;} private: double x, y; }; int main() { Point p1(1.0, 2.0); p1.setX(111); p1.setY(222); cout << "(" << p1.getX() << "," << p1.getY() << ")" << endl; return 0; }
#include <iostream> using namespace std; class Double { public: Double() { dval = 0.0; cout << "default constructor" << endl; } double dval; }; int main() { Double d; return 0; }
Output from the run:
default constructor
When making an array of objects, default constructor is invoked on each object.
#include <iostream> using namespace std; class Double { public: Double() { dval = 0.0; cout << "default constructor" << endl; } double dval; }; int main() { Double d[3]; return 0; }
Output should look like this:
default constructor default constructor default constructor
When making a class instance, the default constructor of its fields are invoked.
#include <iostream> using namespace std; class Double { public: Double() { dval = 0.0; cout << "default constructor" << endl; } double dval; }; class DoubleWrapper { public: DoubleWrapper() { cout << "Wrapper default constructor" << endl; } // field member Double m_Double; }; int main() { DoubleWrapper dw; return 0; }
Output from the run:
default constructor Wrapper default constructor
As we see from the output, when we make a DoubleWrapper object dw, the line:
// field member Double m_Double;
calls the default constructor for Double class to make m_Double which is a field member variable of the DoubleWrapper class.
We already know that a constructor can take parameters. Here is a constructor that takes one parameter.
#include <iostream> using namespace std; class Double { public: Double(double d) { dval = d; cout << "constructor with parameter " << d << endl; } double dval; }; int main() { Double d1(12.3); Double d2 = 45.6; return 0; }
Output is:
constructor with parameter 12.3 constructor with parameter 45.6
C++ constructors that have just one parameter automatically perform implicit type conversion. As shown in the example above, we can invoke single-parameter constructor via assignment to the appropriate type.
To block this kind of implicit conversion, we use the keyword explicit:
#include <iostream> using namespace std; class Double { public: explicit Double(double d) { dval = d; cout << "constructor with parameter " << d << endl; } double dval; }; int main() { Double d1(12.3); Double d2 = 45.6; // error return 0; }
If a constructor with parameters is defined, the default constructor is no longer available.
#include <iostream> using namespace std; class Double { public: explicit Double(double d) { dval = d; cout << "constructor with parameter " << d << endl; } double dval; }; int main() { Double d1(12.3); // OK Double d2; // Error: default constructor is not available return 0; }
Without a default constructor, we cannot declare arrays without initializing them.
#include <iostream> using namespace std; class Double { public: explicit Double(double d) { dval = d; cout << "constructor with parameter " << d << endl; } double dval; }; int main() { // OK Double d1[2] = { Double(12.3), Double(45.6) }; // Error: default constructor is not available Double d2[3]; return 0; }
If a constructor with parameters is defined, the default constructor is no longer available. However, we can create a separate 0-argument constructor:
#include <iostream> using namespace std; class Double { public: Double() { dval = 0.0; } explicit Double(double d) { dval = d; cout << "constructor with parameter " << d << endl; } double dval; }; int main() { Double d1; // OK Double d2(9.87); // OK return 0; }
Or we can use default arguments:
#include <iostream> using namespace std; class Double { public: // one parameter constructor with default value explicit Double(double d = 0.0) { dval = d; cout << "constructor with parameter " << d << endl; } double dval; }; int main() { Double d1; // OK Double d2(9.87); // OK return 0; }
For more detailed info, visit Memory Allocation.
Using the new is one of the ways to allocate memory, where the memory will remain allocated until we manually de-allocate it. The new returns a pointer to the newly allocated memory:
int *p = new int;
- If using int x; the allocation occurs on a region of memory called the stack
- If using new int; the allocation occurs on a region of memory called the heap
The delete operator de-allocates memory that was previously allocated using new. It takes a pointer to the memory location:
int *p = new int; ... delete p;
Here are couple of things to remember regarding the memory allocation and de-allocation:
- Don't use the memory after deletion
- Don't delete the memory twice
- Use delete if memory was allocated by new
- When allocating arrays on the stack int arr[SIZE], size must be a constant
- If we use new[] to allocate arrays, they can have variable size
- De-allocate arrays with delete[] if we used new[] to allocate arrays.
Let's use dynamic memory allocation to make an instance of our Point class.
#include <iostream> using namespace std; class Point { public: Point() { x = 0.0; y = 0.0; cout << "default constructor" << endl; } Point(double xi, double yi) { x = xi; y = yi; cout << "two args constructor" << endl; } void print() { cout << "(" << x << "," << y << ")" << endl; } double x,y; }; int main() { Point *p = new Point; Point *q = new Point(3.0, 4.0); p->print(); q->print(); delete p; delete q; return 0;
Output from the run:
default constructor two args constructor (0,0) (3,4)
Destructor is called when the class instance gets de-allocated
The following example shows the case when the destructor is called. In this case, it is triggered by delete.
#include <iostream> using namespace std; class Point { public: Point() { x = 0.0; y = 0.0; cout << "default constructor" << endl; } ~Point() { cout << "destructor is called" << endl; } double x,y; }; int main() { Point *p = new Point; delete p; return 0; }
Output put should look like this:
default constructor destructor is called
int main() { if(true) { Point p; } // p gets out of scope return 0; }
Output is the same:
default constructor destructor is called
When we call default copy constructor, the pointer is pointing to the original data. The result can be critical since when the copied object can go away with the original when the copied object gets out of scope. This can trigger a non-desirable destructor behavior: trying to delete an already deleted pointer.
#include <iostream> using namespace std; class IntegerArray { public: IntegerArray(int size) { pArray = new int[size]; this->size = size; cout << "default constructor" << endl; } ~IntegerArray() { cout << "destructor" << endl; delete[] pArray; } int *pArray; int size; }; int main() { IntegerArray ia(2); ia.pArray[0] = 10; ia.pArray[1] = 20; cout << "ia.pArray[0] = " << ia.pArray[0] << endl; cout << "ia.pArray[1] = " << ia.pArray[1] << endl; if(true) { cout << "copy ia to ib" << endl; IntegerArray ib = ia; cout << "ib.pArray[0] = " << ib.pArray[0] << endl; cout << "ib.pArray[1] = " << ib.pArray[1] << endl; cout << "ib is getting out of scope" << endl; // 1st destructor } cout << "ia.pArray[0] = " << ia.pArray[0] << endl; cout << "ia.pArray[1] = " << ia.pArray[1] << endl; return 0; // 2nd destructor }
Output from the run should look like this:
default constructor ia.pArray[0] = 10 ia.pArray[1] = 20 copy ia to ib ib.pArray[0] = 10 ib.pArray[1] = 20 ib is getting out of scope destructor ia.pArray[0] = -17891602 ia.pArray[1] = -17891602 destructor
As we see from the output, destructor was called twice:
- When ib goes out of scope, destructor is called (deallocates array), ia.pArray now a dangling pointer.
- when ia goes out of scope (main()), its destructor tries to delete the already-deleted array.
By not depending on the default copy constructor, a newly copied object can have its own memory space and does not interfere with the object from which it is copied.
#include <iostream> using namespace std; class IntegerArray { public: IntegerArray(int size) { pArray = new int[size]; this->size = size; cout << "default constructor" << endl; } ~IntegerArray() { cout << "destructor" << endl; delete[] pArray; } // copy constructor IntegerArray(IntegerArray &arr;) { pArray = new int[arr.size]; size = arr.size; for (int i = 0; i < size; ++i) pArray[i] = arr.pArray[i]; } int *pArray; int size; }; int main() { IntegerArray ia(2); ia.pArray[0] = 10; ia.pArray[1] = 20; cout << "ia.pArray[0] = " << ia.pArray[0] << endl; cout << "ia.pArray[1] = " << ia.pArray[1] << endl; if(true) { cout << "copy ia to ib" << endl; IntegerArray ib = ia; cout << "ib.pArray[0] = " << ib.pArray[0] << endl; cout << "ib.pArray[1] = " << ib.pArray[1] << endl; cout << "ib is getting out of scope" << endl; } cout << "ia.pArray[0] = " << ia.pArray[0] << endl; cout << "ia.pArray[1] = " << ia.pArray[1] << endl; return 0; }
The output is different from the previous example:
default constructor ia.pArray[0] = 10 ia.pArray[1] = 20 copy ia to ib ib.pArray[0] = 10 ib.pArray[1] = 20 ib is getting out of scope destructor ia.pArray[0] = 10 ia.pArray[1] = 20 destructor
This is from Google C++ Style Guide.
Use the specified order of declarations within a class: public: before private:, methods before data members (variables), etc.
Our class definition should start with its public: section, followed by its protected: section and then its private: section. If any of these sections are empty, omit them.
Within each section, the declarations generally should be in the following order:
- Typedefs and Enums
- Constants (static const data members)
- Constructors
- Destructor
- Methods, including static methods
- Data Members (except static const data members)
Friend declarations should always be in the private section, and the DISALLOW_COPY_AND_ASSIGN macro invocation should be at the end of the private: section. It should be the last thing in the class.
Method definitions in the corresponding .cc file should be the same as the declaration order, as much as possible.
Do not put large method definitions inline in the class definition. Usually, only trivial or performance-critical, and very short, methods may be defined inline.
In the example, we have a class B which inherits from a class A. When we try to construct an instance of B, it needs to construct its base object of A. So, it calls A ctor. Then, in its constructor, the class B has an initialization list which also needs to construct two A instance members, A x and A y. So, it calls A ctor twice with different m member for each instance of A. After that, it finally executes the body of B ctor.
- A ctor at B(int n)
- Construct A obj x at x(m+1)
- Construct A obj y at y(n)
- B ctor and execute its body
#include <iostream> using namespace std; class A { public: A(int n = 7) : m(n) { cout << "A() n = " << n << " m = " << m << endl; } ~A() { cout << "~A() m = " << m << endl; } protected: int m; }; class B : public A { public: // (1) A ctor at B(int n) // (2) Construct A obj x at x(m+1) // (3) Construct A obj y at y(n) // (4) B ctor and execute its body B(int n) : x(m + 1) , y(n) { cout << "B() n = " << n << endl; } public: // (1) dtor of B // (2)-(4) dtor of A ~B() { cout << "~B() m = " << m << endl; --m; } private: A x; A y; }; int main() { { B b(10); } return 0; }
Output:
A() n = 7 m = 7 A() n = 8 m = 8 A() n = 10 m = 10 B() n = 10 ~B() m = 7 ~A() m = 10 ~A() m = 8 ~A() m = 6
As we see from the output, when we finished the bock of code { B b(10); } in main(), destructors are called. First, B dtor, then A dtor three times since we construct A instances three times.
Compiler should know the size of the type!
In C++, classes can be forward-declared if we only need to use the pointer to the class type.
This is because all object pointers are the same size, and this is what the compiler cares about!
This is why a class contains a member that is a pointer to forward declared class (note that the usage of a pointer member is to avoid circular references).
When we simply forward-declare a class, we call it an incomplete type: we do not know the structure of it, and as a result, we do not know the actual size of the class.
There are cases when the forward declaration of a class is not sufficient if we need to use the actual class type, for example, if we have a member whose type is that class directly (not a pointer), or if we need to use it as a base class, or if we need to use the methods of the class in a method.
// Forward declaration of class A class A; // class B is OK class B { public: B(A* pa, const A& ra) : pA(pa), rA(ra) {}; A& getRef() const; A* getPtr(); void foo(A b); A getA() const; private: A *pA; const A &rA; }; // class C & D have some issues! // class A cannot be used as a base class class C: public A {} ; // Error! class D { public: void set(A* pa) {pA = pa;} // used as a parameter for prototype void foo(A); // OK // defining a function requires complete type void bar(A a) {} // Error! void method() { // using a point to an incomplete class type pA->getA(); // Error! } private: // cannot be used to declare a member A mObj; //Error! A* pA, // OK };
For further discussions:
http://stackoverflow.com/questions/553682/when-to-use-forward-declaration.
#include <stdlib.h> struct A { int one; int two; }; int main() { // dynamically allocated struct struct A *pA = (struct A *)malloc(sizeof(struct A)); (*pA).one = 1; pA -> two = 2; // statically allocated struct struct A a; a.one = 1; a.two = 2; }
#include <stdlib.h> struct A { int one; int two; }; typedef struct A A_t; int main() { A_t *pA = (A_t *)malloc(sizeof(A_t)); (*pA).one = 1; pA -> two = 2; A_t a; a.one = 1; a.two = 2; }
Visit structs vs classes.
There are some examples, visit Linked List Examples.
The following code shows a very simple linked list.
#include <stdlib.h> #include <stdio.h> typedef struct node { struct node *next; int data; } node_t; node_t *make_node(int d) { node_t *new_node = (node_t *)malloc(sizeof(node_t)); new_node -> data = d; new_node -> next = NULL; return new_node; } // insert a node before the given pointer node_t *insert_node(node_t *ptr, int d) { node_t *new_node = make_node(d); new_node -> next = ptr; return new_node; } // delete a given node node_t *delete_node(node_t *head, int d) { if(head == NULL) return NULL; if(head->data == d) { node_t *new_head = head->next; free(head); return new_head; } node_t *prev = head; node_t *cur = head->next; while(cur) { if(cur->data == d) { prev->next = cur->next; free(cur); return NULL; } cur = cur->next; } } int main() { node_t *head = make_node(1); head = insert_node(head, 2); head = insert_node(head, 3); head = insert_node(head, 4); head = insert_node(head, 5); node_t *ptr = head; while(ptr) { printf("ptr->data=%d\n", ptr->data); ptr = ptr->next; } head = delete_node(head, 5); delete_node(head, 3); ptr = head; while(ptr) { printf("ptr->data=%d\n", ptr->data); ptr = ptr->next; } return 0; }
The output from the run should look like this:
ptr->data=5 ptr->data=4 ptr->data=3 ptr->data=2 ptr->data=1 ptr->data=4 ptr->data=2 ptr->data=1
We can construct a diagram for class hierarchy using Doxygen and Graphviz.
- Go to the source directory
- Run "doxygen -g"
This will generate a configure file "Doxyfile" - Modify the "Doxyfile" to get the diagrams as well as documents.
change the following options of the generated Doxyfile:EXTRACT_ALL = YES HAVE_DOT = YES UML_LOOK = YES
- run "doxygen Doxyfile"
- Navigate to the "html" directory, and run "index.html"
Ph.D. / Golden Gate Ave, San Francisco / Seoul National Univ / Carnegie Mellon / UC Berkeley / DevOps / Deep Learning / Visualization