Classes are like blueprints, while instances are like the buildings constructed from those blueprints. Similarly, a CAD design is like a class, and the manufactured items are its instances. Instantiation is the process of creating an instance of a class.
Instances are allocated memory to store specific information, and you can have multiple identical instances of the same class type.
For each class instance:
Constructor: A special function that is called when an object is created to set up necessary initializations.
Destructor: A special function that is called when an object is destroyed (freed) to clean up resources.
Using struct
:
struct List { ... };
void Initialize(List* list);
void Destroy(List* list);
int Find(const List& list, int key);
bool Insert(List* list, int key, int pos);
bool Delete(List* list, int key);
List Concat(const List& l1, const List& l2);
Using class
:
class List {
public:
void Initialize();
void Destroy();
int Find(int key) const;
bool Insert(int key, int pos);
bool Delete(int key);
static List Concat(const List& l1, const List& l2);
};
The class implementation encapsulates the operations, preventing accidental omission of Initialize()
or Destroy()
calls.
Constructors are special member functions used to initialize objects.
They have the same name as the class and no return type.
They can take different arguments, allowing for various initialization methods.
Use the syntax : field(value), ...
to initialize member variables.
Example:
class Student {
public:
Student()
: name_(), grade_(), id_(0), midterm_(0),
final_(0), hw1_(0), hw2_(0) {}
Student(const string& name, int id)
: name_(name), id_(id) {
midterm_ = final_ = hw1_ = hw2_ = 0;
}
void SetInfo(const string& name, int id) {
name_ = name, id_ = id;
}
const string& grade() const { return grade_; }
private:
std::string name_, grade_;
int id_, midterm_, final_, hw1_, hw2_;
};
The destructor is a special cleanup member function called when the object is destroyed.
Its name is ~
+ the class name.
It takes no arguments and has no return type.
Example:
class Student {
public:
~Student() {} // Nothing to do
};
class DoubleArray {
public:
DoubleArray() : ptr_(NULL), size_(0) {}
DoubleArray(size_t size)
: ptr_(NULL), size_(0) { Resize(size); }
~DoubleArray() { delete[] ptr_; }
void Resize(size_t size);
int size() const { return size_; }
double* ptr() { return ptr_; }
const double* ptr() const { return ptr_; }
private:
double* ptr_;
size_t size_; // size_t is unsigned int.
};
void DoubleArray::Resize(size_t size) {
double* new_ptr = new double[size];
if (ptr_) {
for (int i = 0; i < size_ && i < size; ++i) {
new_ptr[i] = ptr_[i];
}
delete[] ptr_;
}
ptr_ = new_ptr;
size_ = size;
}
void TestScope(int n) {
assert(n == 10);
for (int i = 0; i < n; ++i) {
int n = 20;
for (int j = 0; j < n; ++j) {
int n = 30;
assert(n == 30);
}
assert(n == 20);
}
assert(n == 10);
}
int main() {
TestScope(10);
return 0;
}
Local variables shadow variables in outer scopes. When a local variable goes out of scope, the variable in the enclosing scope becomes accessible again.
struct TestClass {
int n;
TestClass(int i) : n(i) {
cout << "Constructor " << n << endl;
}
~TestClass() {
cout << "Destructor " << n << endl;
}
};
void TestClassScope(int n) {
TestClass c1(n);
for (int i = 0; i < n; ++i) {
TestClass c2(i);
}
}
int main() {
TestClassScope(3);
return 0;
}
Constructors are called when an object comes into scope.
Destructors are called when an object goes out of scope (in reverse order of construction).
A copy constructor initializes an object using another instance of the same class.
ClassName(const ClassName& obj);
Example:
class Point {
public:
double x, y;
Point(double x, double y) : x(x), y(y) {}
// Copy constructor, most popular form
Point(const Point& p) : x(p.x), y(p.y) {}
// You can also use this form, but...
Point(Point& p) { x = p.x; y = p.y; }
// Compile error (hint: recursion)
//Point(Point p) { x = p.x; y = p.y; }
};
int main(int argc, char* argv[]) {
Point p2(0.1, 0.2);
Point p3 = p2;
Point p4(p2);
return 0;
}
When an object is returned by value.
When an object is passed to a function by value as an argument.
When an object is constructed based on another object of the same class.
Example:
class Point {
public:
double x, y;
Point(double x, double y) : x(x), y(y) {}
Point(const Point& p) : x(p.x), y(p.y) {}
// ^ Copy constructor
};
Point GetScaledPoint(double scale, Point p) {
Point p_new;
p_new.x = p.x * scale, p_new.y = p.y * scale;
return p_new; // 1.
}
int main(int argc, char* argv[]) {
Point p1(0.1, 0.2);
Point p2 = GetScaledPoint(2.0, p1);
// ^ 3. ^ 2.
Point p3 = p1, p4(p2); // 3.
return 0;
}
A default copy constructor is implicitly created by the compiler if no user-defined copy constructor exists.
It performs a member-wise copy between objects, where each member is copied by its own copy constructor.
This works well in general, but not in some cases.
Note: A default constructor is implicitly created by the compiler only if there is no user-defined constructor.
If you define a copy constructor, the compiler does not create the default constructor and default copy constructor.
Be careful when dealing with pointers!
The default copy constructor only copies the pointer values, not the allocated memory.
This can lead to issues like double deletion.
Example:
class MyString {
private:
char* str_;
int len_;
public:
MyString(const char* s = "") {
len_ = strlen(s);
str_ = new char[len_ + 1];
strcpy(str_, s);
}
// No copy constructor defined
~MyString() { delete[] str_; }
void Print() { cout << str_ << endl; }
};
int main(int argc, char* argv[]) {
MyString s1("hello world");
MyString s2(s1); // Default copy constructor
return 0;
}
To avoid issues with pointers, write a proper copy constructor that allocates new memory and copies the contents.
class MyString {
private:
char* str_;
int len_;
public:
MyString(const char* s = "") {
len_ = strlen(s);
str_ = new char[len_ + 1];
strcpy(str_, s);
}
MyString(const MyString& s) {
len_ = s.len_;
str_ = new char[len_ + 1];
strcpy(str_, s.str_);
}
~MyString() { delete[] str_; }
void Print() { cout << str_ << endl; }
};
Compilers like g++ can optimize code to avoid unnecessary copying of objects by default.
For example, omitting temporary object creation when returning an object (return value optimization).
To disable this feature, use -fno-elide-constructors
with g++.
struct Complex {
double real;
double imag;
};
int main() {
Complex c;
c.real = 1.0, c.imag = 0.5; // 1 + 0.5i
Complex d;
d.real = c.real * 2,
d.imag = c.imag * 2; // d = c * 2;
printf("%f + %fi\n", d.real, d.imag);
return 0;
}
struct Complex {
double real;
double imag;
Complex() : real(0.0), imag(0.0) {}
Complex(const Complex& c)
: real(c.real), imag(c.imag) {}
Complex(double r, double i)
: real(r), imag(i) {}
};
int main() {
Complex c(1.0, 0.5); // 1 + 0.5i
Complex c0 = c, c1(c);
Complex d(c.real * 2, c.imag * 2);
// d = c * 2;
printf("%f + %fi\n", d.real, d.imag);
return 0;
}
struct Complex {
double real;
double imag;
Complex() : real(0.0), imag(0.0) {}
Complex(const Complex& c)
: real(c.real), imag(c.imag) {}
Complex(double r, double i)
: real(r), imag(i) {}
Complex Add(const Complex& c) const {
return Complex(real + c.real,
imag + c.imag);
}
Complex Multiply(const Complex& c) const {
return Complex(
real * c.real - imag * c.imag,
real * c.imag + imag * c.real);
}
void Print() const {
printf("%f + %fi", real, imag);
}
};
int main() {
Complex c(1.0, 0.5); // 1 + 0.5i
Complex d = c.Multiply(Complex(2, 0));
// d = c * 2;
d.Print();
return 0;
}
Structures representing various 2D shapes:
struct Point {
double x, y;
};
struct Line {
Point p[2];
};
struct Triangle {
Point p[3];
};
struct Rectangle {
double x0, y0, x1, y1;
// top_left, bottom_right
// Why not Point p[4]; ?
};
#include <math.h>
// Utility function : distance between two points.
inline double Distance(const Point& p0,
const Point& p1) {
const double dx = p1.x - p0.x, dy = p1.y - p0.y;
return sqrt(dx * dx + dy * dy);
}
double Length(const Line& line){
return Distance(line.p[0], line.p[1]);
}
double Perimeter(const Triangle& tri){
return Distance(tri.p[0], tri.p[1]) +
Distance(tri.p[1], tri.p[2]) +
Distance(tri.p[2], tri.p[0]);
}
double Perimeter(const Rectangle& rect){
return 2 * (fabs(rect.x1 - rect.x0) +
fabs(rect.y1 - rect.y0));
}
double Area(const Triangle& tri){
const double dx10 = tri.p[1].x - tri.p[0].x;
const double dy10 = tri.p[1].y - tri.p[0].y;
const double dx20 = tri.p[2].x - tri.p[0].x;
const double dy20 = tri.p[2].y - tri.p[0].y;
return 0.5 * fabs(dx10 * dy20 - dx20 * dy10);
}
double Area(const Rectangle& rect) {
return fabs((rect.x1 - rect.x0) *
(rect.y1 - rect.y0));
}
Use member functions:
struct Point {
double x, y;
};
double Distance(const Point& p0, const Point& p1);
struct Line {
Point p[2];
double Length() const;
}
struct Triangle {
Point p[3];
double Perimeter() const;
double Area() const;
};
struct Rectangle {
double x0, y0, x1, y1;
double Perimeter() const;
double Area() const;
};
double Line::Length() const {
return Distance(p[0], p[1]);
}
double Triangle::Perimeter() const {
return Distance(p[0], p[1]) + Distance(p[1], p[2])
+ Distance(p[2], p[0]);
}
double Triangle::Area() const {
return 0.5 * fabs(
(p[1].x - p[0].x) * (p[2].y - p[0].y) -
(p[2].x - p[0].x) * (p[1].y - p[0].y));
}
double Rectangle::Perimeter() const {
return 2 * (fabs(x1 - x0) + fabs(y1 - y0));
}
double Rectangle::Area() const {
return fabs((x1 - x0) * (y1 - y0));
}
Stack: Last In First Out (LIFO)
class Stack {
public:
Stack() : num_data_(0), data_(NULL) {}
~Stack() { delete[] data_; }
void Push(int value);
void Pop() {
if (num_data_ > 0) --num_data_;
}
int Top() const {
return data_[num_data_ - 1];
} // TODO: check NULL.
bool IsEmpty() const {
return num_data_ <= 0;
}
private:
int num_data_;
int* data_;
};
void Stack::Push(int value) {
int* new_data = new int[num_data_ + 1];
for (int i = 0; i < num_data_; ++i) {
new_data[i] = data_[i];
}
delete[] data_;
data_ = new_data;
data_[num_data_] = value;
++num_data_;
}
Push operation with better memory management:
void Stack::Push(int value) {
if (num_data_ >= capacity_) {
const int new_capacity = num_data_ + 1;
int* new_data = new int[new_capacity];
for (int i = 0; i < num_data_; ++i) {
new_data[i] = data_[i];
}
delete[] data_;
data_ = new_data;
capacity_ = new_capacity;
}
data_[num_data_] = value;
++num_data_;
}
Queue: First In First Out (FIFO)
class Queue {
public:
Queue();
~Queue();
void Push(int value);
void Pop();
int Front() const ;
int Back() const ;
bool IsEmpty() const ;
private:
...
};
this
PointerIn member functions, this
can be used to point to the instance itself.
Useful when you need to return the pointer or reference to the instance.
struct Complex {
double real;
double imag;
Complex(double r = 0, double i = 0)
: real(r), imag(i) {}
Complex Add(const Complex& c) const {
return Complex(real + c.real, imag + c.imag);
}
Complex& Scale(double s) {
real *= s, imag *= s;
return *this;
}
void Print() const {
printf("%f + %fi", this->real, this->imag);
}
};
int main() {
Complex c(1.0, 0.5), d(0.5, 0.5);
c.Add(d).Scale(2.0).Print(); // 3.0 + 2.0i
return 0;
}
Static members pertain to the class, not to individual instances.
Static member variables are shared among all instances.
Static member functions do not have an associated instance, so you can only see the static member variables.
They can, however, access private member variables if an instance is passed to them.
Use the static
keyword to specify static members.
In static member functions, this
pointer is not defined.
Static member variables are like global variables, but only for the class.
To access them, use ClassName::MemberName
, obj.MemberName
, or ptr->MemberName
.
struct Complex {
double real;
double imag;
Complex() : real(0.0), imag(0.0) {}
Complex(const Complex& c)
: real(c.real), imag(c.imag) {}
Complex(double r, double i) : real(r), imag(i) {}
...
static Complex Add(const Complex& c1,
const Complex& c2) {
return Complex(c1.real + c2.real,
c1.imag + c2.imag);
}
};
int main() {
Complex c(1.0, 0.5);
Complex d = Complex::Add(c, Complex(2, 1));
d.Print();
return 0;
}
class CountInstance {
public:
CountInstance() {
Print("construct: ", ++count_);
}
~CountInstance() {
Print("destruct: ", --count_);
}
static void Print(const string& msg, int n) {
cout << msg << count_ << endl;
}
private:
static int count_; // declaration
};
int CountInstance::count_ = 0; // definition
int main() {
CountInstance instance;
for (int i = 0; i < 2; ++i) {
CountInstance inner_instance;
// Do nothing.
}
return 0;
}
struct MyClass {
MyClass(double x, double y) : x_(x), y_(y) {}
void DoSomething();
static void Prepare(MyClass* arg);
double x_, y_;
static int iter_;
};
void MyClass::DoSomething() {
for (int i = 0; i < iter_; ++i) {
cout << x_ + y_ << endl;
}
}
void MyClass::Prepare(MyClass* arg) {
if(arg) arg->x_ = arg->y_ = 0.0; // OK.
iter_ = 10;
// OK.
}
int MyClass::iter_ = 10; // definition & init
int main() {
MyClass::Prepare(nullptr);
MyClass a(1.0, 2.0);
MyClass::Prepare(&a);
a.DoSomething();
cout << MyClass::iter_ << ", "
<< a.x_ << endl;
return 0;
}
Singleton ensures only one instance can exist for the entire program.
class Singleton {
public:
static Singleton* GetInstance();
// Some useful member functions here..
private:
Singleton() {}
static Singleton* instance_;
};
Singleton* Singleton::instance_ = NULL;
Singleton* Singleton::GetInstance() {
if (instance_ == NULL) {
instance_ = new Singleton;
}
return instance_;
}
Hide all data members unless absolutely necessary.
Make member functions meaningful and atomic.
Use const
as much as possible.
Make input and output parameters clearly visible (coding style).
Keep initialization in constructors simple.
Make a separate setup function for complex initializations, especially if it might fail.
Use static members only when necessary.
Class utility functions that do not need to access data members are often implemented as static functions.
BSTs allow binary search for fast lookup, addition, and removal of data items.
BST ADT Operations:
Initialize (BST T): constructor
Destroy (BST T): destructor
Search (BST T, Key K): returns whether K is in T
Insert (BST T, Key K): insert K into T
Delete (BST T, Key K): delete K from T
The left subtree of a node contains only nodes with keys smaller than its key.
The right subtree of a node contains only nodes with keys greater than its key.
The left and right subtrees each must also be a BST.
There must be no duplicate nodes.
Operation | Average | Worst |
---|---|---|
Search | O(log n) | O(n) |
Insert | O(log n) | O(n) |
Delete | O(log n) | O(n) |
struct Node {
int value;
Node *left, *right;
Node(int v = 0)
: value(v), left(nullptr), right(nullptr) {}
bool Search(int key) {
if (key == value) return true;
return key < value ?
(left ? left->Search(key) : false) :
(right ? right->Search(key) : false);
}
bool Insert(int key) {
if (key == value) return false;
if (key < value) {
if (left) return left->Insert(key);
left = new Node(key);
} else {
if (right) return right->Insert(key);
right = new Node(key);
}
return true;
}
void Destroy() {
if (left) left->Destroy();
if (right) right->Destroy();
delete this;
}
};
class BinarySearchTree {
public:
void Initialize() { root_ = nullptr; }
void Destroy() { if (root_) root_ ->Destroy(); }
bool Search(int key) const; { return root_ ? root_ ->Search(key) : false; }
bool Insert(int key) {
if (root_) return root_ ->Insert(key);
root_ = new Node(key);
return true;
}
bool Delete(int key);
private:
Node* root_;
};
If the node has no children or one child, simple removal and adjustment of pointers.
If the node has two children, find the smallest node in the right subtree (or largest in the left subtree), replace the node to be deleted with this node, and then delete the inorder successor.
bool BinarySearchTree::Delete(int key) {
if (root_ == nullptr) return false ;
Node** pp = &root_;
while (*pp != nullptr) {
if (key == (*pp)->value) break ;
pp = &(key < (*pp)->value ? (*pp)->left :
(*pp)->right);
}
if (*pp == nullptr) return false ;
Node* p = *pp;
if (p->left && p->right) { // both children
Node** pq = &(p->right);
while ((*pq)->left) pq = &(*pq)->left;
(*pp)->value = (*pq)->value;
p = *pq;
*pq = (*pq)->right;
}
else {
*pp = (p->left ? p->left : p->right);
}
delete p;
return true ;
}
C++ allows you to define same-name functions with different parameters.
Both regular functions and class member functions can be overloaded.
A family of functions that do the same thing but using different argument lists
Exactly same functions except return types are not allowed.
At least one parameter is different, or the const-ness should be different.
void Print(const char* str, int w); // #1
void Print(double d, int width); // #2
void Print(long l, int width); // #3
void Print(int s, int width); // #4
void Print(const char* str); // #5
Print("Pancakes", 15); // Use #1
Print("Syrup"); // Use #5
Print(3.1415, 10); // Use #2
Print(1979, 12); // Use #4
Print(1979l, 15); // Use #3
class MyClass {
public:
MyClass();
MyClass(const MyClass& c);
explicit MyClass(int x);
// ^ Prevent implicit type conversion.
int& x(); // Differ in constness.
const int& x() const;
int DoSomething();
double DoSomething(double c);
void DoSomething(double a, double b);
//double DoSomething(); // Error!
//void DoSomething(double a, double b,
// double c = 0.0); // Error!
};
C++ allows you to redefine built-in operators like +
, -
, *
.
Most commonly overloaded operators are:
Arithmetic operators: +
, -
, *
, /
Assignment operators: =
, +=
, -=
, *=
Comparison operators: <
, >
, <=
, >=
, ==
, !=
Array or containers: []
, ()
struct Point {
double x, y, z;
Point() : x(0), y(0), z(0) {}
Point(double x, double y, double z)
: x(x), y(y), z(z) {}
Point Add(const Point& p) const {
return Point(x + p.x, y + p.y, z + p.z);
}
void Print() const {
cout << x << "," << y << "," << z << endl;
}
};
int main() {
Point p1(1, 1, 1), p2(2, 2, 2);
Point p3 = p1.Add(p2);
p3.Print();
return 0;
}
struct Point {
double x, y, z;
...
Point operator+(const Point& p) const {
return Point(x + p.x, y + p.y, z + p.z);
}
...
};
int main() {
Point p1(1, 1, 1), p2(2, 2, 2);
Point p3 = p1.operator+(p2);
p3.Print();
p3 = p1 + p2;
p3.Print();
return 0;
}
Operator overloading must be used very carefully, as it can seriously degrade readability.
If the first operand is not a class type, use a non-member operator overloaded function!
class Point {
private:
double x, y, z;
...
friend Point operator+(
double v, const Point& p);
};
Point operator+(double v, const Point& p) {
return Point(v + p.x, v + p.y, v + p.z);
}
int main() {
Point p1(1, 1, 1);
Point p2 = operator+(2.0, p1);
p2.Print();
p2 = 2.0 + p1;
p2.Print();
return 0;
}
Constructors can be used to convert some type (the parameter type) to another type (the class belonging the constructor).
This can affect the behavior of overloaded operators.
struct Complex {
double real, imag;
Complex() : real(0.0), imag(0.0) {}
Complex(double r, double i)
: real(r), imag(i) {}
Complex(const Complex& c)
: real(c.real), imag(c.imag) {}
Complex operator+(const Complex& c) const {
return Complex(real + c.real,
imag + c.imag);
}
};
void Test() {
Complex a(1.0, 2.0), b(2.0, 5.0), c;
c = a + b;
// OK.
c = a + 3.0; // Error.
c = 2.0 + b; // Error.
}
struct Complex {
double real, imag;
Complex() : real(0.0), imag(0.0) {}
Complex(double v) : real(v), imag(0.0) {}
Complex(double r, double i)
: real(r), imag(i) {}
Complex(const Complex& c)
: real(c.real), imag(c.imag) {}
Complex operator+(const Complex& c) const;
};
void Test() {
Complex a(1.0, 2.0), b(2.0, 5.0), c;
c = a + b;
// OK.
c = a + 3.0; // OK.
c = 2.0 + b; // Still error.
}
struct Complex {
double real, imag;
Complex() : real(0.0), imag(0.0) {}
// Complex(double v) : real(v), imag(0.0) {}
Complex(double r, double i = 0.0)
: real(r), imag(i) {}
Complex(const Complex& c)
: real(c.real), imag(c.imag) {}
};
Complex operator+(const Complex& a,
const Complex& b);
void Test() {
Complex a(1.0, 2.0), b(2.0, 5.0), c;
c = a + b;
// OK.
c = a + 3.0; // OK.
c = 2.0 + b; // OK.
}
```c++
using namespace