HSHimanshu Sharma
Basic C & C++

C++ for beginners: classes, the STL, references and smart pointers

C++ for beginners: classes, the STL, references and smart pointers Advertisement

C++ began as "C with Classes", created by Bjarne Stroustrup at Bell Labs in the early 1980s, and was renamed C++ in 1983. The name is a joke from C's ++ operator: "C, incremented". It keeps nearly all of C (so C code usually still works) and adds classes, templates, a large standard library, exceptions and tools for managing resources automatically. It is used where speed and control matter: games and game engines, browsers, databases, trading systems, embedded software. Modern C++ (C++11 and later, now standardised every three years) feels very different from the C++ of the 1990s: you rarely write new, delete or raw loops. This tour covers the core ideas with programs that were compiled with clang++ -std=c++17 -Wall -Wextra and run. If C is new to you, read the C language guide first, since pointers and memory work the same way.

Your first program

#include <iostream>

int main() {
    std::cout << "Hello, C++!" << std::endl;
    return 0;
}

Compile with c++ hello.cpp -o hello (or g++/clang++) and run ./hello. std::cout is the standard output stream and << sends values to it. The std:: prefix names the namespace of the standard library. Beginners are often taught using namespace std;, but in real projects it is better to write the prefix, or import only the names you need (using std::cout;), to avoid clashes.

Input and output with streams

cin reads and cout prints, with automatic conversion for each type, so there are no format characters to match as in C's printf:

#include <iostream>
#include <iomanip>
#include <string>

int main() {
    std::string name;
    int age;
    std::cout << "Name and age: ";
    std::cin >> name >> age;
    std::cout << "Hello " << name << ", next year you will be " << age + 1 << "\n";
    std::cout << std::fixed << std::setprecision(2) << 3.14159 << " " << std::setw(6) << 42 << "\n";
    return 0;
}

With Asha 30 as input it printed Name and age: Hello Asha, next year you will be 31 followed by 3.14 42. cin >> name stops at whitespace; use std::getline(std::cin, line) to read a whole line. Always check that input succeeded (if (!(std::cin >> age)) ...).

References

A reference is another name for an existing variable. It is like a pointer that is always valid and needs no *:

#include <iostream>
#include <string>

void add_bonus(int &salary) { salary += 500; }        // changes the caller's variable
void show(const std::string &s) { std::cout << s << "\n"; }   // no copy, no change

int main() {
    int pay = 1000;
    add_bonus(pay);
    std::cout << pay << "\n";
    show("passed without copying");
    int &alias = pay;
    alias = 2000;
    std::cout << pay << "\n";
    return 0;
}

It printed 1500, passed without copying, 2000. Passing large objects by const & is the standard way to avoid expensive copies, while passing by plain & lets the function modify the original. This replaces most of the pointer-passing you needed in C.

Strings, vectors and maps

The standard library (often called the STL) gives you safe, growable containers, so you rarely need raw arrays or char* strings.

#include <iostream>
#include <string>
#include <vector>
#include <map>
#include <algorithm>

int main() {
    std::string s = "Hello";
    s += ", World";
    std::cout << s << " " << s.size() << " " << s.substr(7) << " " << (s.find("World") != std::string::npos) << "\n";

    std::vector<int> v = {5, 2, 9, 1};
    v.push_back(7);
    std::sort(v.begin(), v.end());
    for (int x : v) std::cout << x << " ";
    std::cout << "| size=" << v.size() << " max=" << *std::max_element(v.begin(), v.end()) << "\n";

    std::map<std::string, int> marks = {{"Asha", 82}, {"Ravi", 67}};
    marks["Meena"] = 91;
    for (const auto &[name, m] : marks) std::cout << name << "=" << m << " ";
    std::cout << "\n";
    std::cout << marks.count("Ravi") << marks.count("Zed") << "\n";
    return 0;
}
Hello, World 12 World 1
1 2 5 7 9 | size=5 max=9
Asha=82 Meena=91 Ravi=67 
10

vector is a dynamic array; map stores key-value pairs sorted by key (unordered_map is the hash-table version, faster for lookups when order does not matter). auto lets the compiler deduce the type, and for (x : container) is the range-based loop. The structured binding [name, m] unpacks each pair. Containers manage their own memory, so there is nothing to free.

Algorithms and lambdas

The <algorithm> header supplies sorting, searching, counting and transforming; lambdas are small unnamed functions you pass to them:

#include <iostream>
#include <vector>
#include <algorithm>
#include <numeric>

int main() {
    std::vector<int> v = {5, 2, 9, 1, 7, 4};
    std::sort(v.begin(), v.end(), [](int a, int b) { return a > b; });   // descending
    int evens = std::count_if(v.begin(), v.end(), [](int x) { return x % 2 == 0; });
    int total = std::accumulate(v.begin(), v.end(), 0);
    int threshold = 5;
    auto big = std::count_if(v.begin(), v.end(), [threshold](int x) { return x > threshold; });
    for (int x : v) std::cout << x << " ";
    std::cout << "| evens=" << evens << " total=" << total << " bigger than " << threshold << ": " << big << "\n";
    return 0;
}

It printed 9 7 5 4 2 1 | evens=2 total=28 bigger than 5: 2. The [threshold] part is the lambda's capture list: it copies the local variable into the lambda.

Classes

A class bundles data and the functions that work on it. In C++ you choose what is visible with public, protected and private. A constructor sets up an object; a destructor (~Name) runs when it is destroyed.

#include <iostream>
#include <stdexcept>
#include <string>
#include <utility>

class Account {
public:
    Account(std::string owner, double balance = 0) : owner_(std::move(owner)), balance_(balance) {}

    void deposit(double amount) {
        if (amount <= 0) throw std::invalid_argument("deposit must be positive");
        balance_ += amount;
    }
    bool withdraw(double amount) {
        if (amount > balance_) return false;
        balance_ -= amount;
        return true;
    }
    double balance() const { return balance_; }
    const std::string &owner() const { return owner_; }

private:
    std::string owner_;
    double balance_;
};

int main() {
    Account a("Asha", 1000);
    a.deposit(250.5);
    std::cout << a.owner() << ": " << a.balance() << "\n";
    std::cout << std::boolalpha << a.withdraw(5000) << " " << a.withdraw(200) << " " << a.balance() << "\n";
    return 0;
}
Asha: 1250.5
false true 1050.5

Notice the member initialiser list (: owner_(...), balance_(...)), which is the correct way to initialise members; the const after balance() promises the method does not change the object. The same ideas as object-oriented programming in Python apply, with static types and explicit access control.

Inheritance and virtual functions

#include <iostream>
#include <memory>
#include <vector>

class Shape {
public:
    virtual ~Shape() = default;
    virtual double area() const = 0;                 // pure virtual: makes Shape abstract
    virtual const char *name() const { return "shape"; }
};
class Rect : public Shape {
    double w_, h_;
public:
    Rect(double w, double h) : w_(w), h_(h) {}
    double area() const override { return w_ * h_; }
    const char *name() const override { return "rectangle"; }
};
class Circle : public Shape {
    double r_;
public:
    explicit Circle(double r) : r_(r) {}
    double area() const override { return 3.14159265 * r_ * r_; }
    const char *name() const override { return "circle"; }
};

int main() {
    std::vector<std::unique_ptr<Shape>> shapes;
    shapes.push_back(std::make_unique<Rect>(3, 4));
    shapes.push_back(std::make_unique<Circle>(1));
    for (const auto &s : shapes) std::cout << s->name() << " " << s->area() << "\n";
    return 0;
}
rectangle 12
circle 3.14159

virtual makes the call go to the real object's version at run time (polymorphism). Mark overrides with override so the compiler checks you really are overriding, and give a base class with virtual functions a virtual destructor so deleting through a base pointer works properly. Code that never calls new or delete is the goal, which brings us to smart pointers.

Memory management: RAII and smart pointers

In C you free what you allocate. In C++ the idiom is RAII (resource acquisition is initialisation): wrap a resource in an object whose destructor releases it, so cleanup happens automatically, even when an exception is thrown. Smart pointers do this for heap memory:

  • std::unique_ptr<T>: sole owner; freed when it goes out of scope; cannot be copied, only moved.
  • std::shared_ptr<T>: shared ownership with a reference count; freed when the last owner goes.
  • std::weak_ptr<T>: a non-owning view of a shared_ptr, to break cycles.
#include <iostream>
#include <memory>

struct Resource {
    explicit Resource(int id) : id_(id) { std::cout << "acquire " << id_ << "\n"; }
    ~Resource() { std::cout << "release " << id_ << "\n"; }
    int id_;
};

int main() {
    {
        auto a = std::make_unique<Resource>(1);
        std::cout << "using " << a->id_ << "\n";
    }                                              // released here automatically
    auto s1 = std::make_shared<Resource>(2);
    {
        auto s2 = s1;
        std::cout << "owners: " << s1.use_count() << "\n";
    }
    std::cout << "owners: " << s1.use_count() << "\n";
    return 0;
}
acquire 1
using 1
release 1
acquire 2
owners: 2
owners: 1
release 2

Resource 1 was released at the closing brace; resource 2 was released only when the last owner (s1) went away at the end of main. Prefer make_unique and make_shared over new, and use raw pointers only to observe, never to own.

Templates

A template is code that works for any type, resolved at compile time. The STL is built from them.

#include <iostream>
#include <string>
#include <utility>
#include <vector>

template <typename T>
T maximum(T a, T b) { return a > b ? a : b; }

template <typename T>
class Stack {
    std::vector<T> items_;
public:
    void push(T x) { items_.push_back(std::move(x)); }
    T pop() { T x = std::move(items_.back()); items_.pop_back(); return x; }
    bool empty() const { return items_.empty(); }
};

int main() {
    std::cout << maximum(3, 9) << " " << maximum(2.5, 1.5) << " " << maximum(std::string("apple"), std::string("pear")) << "\n";
    Stack<std::string> s;
    s.push("a"); s.push("b");
    std::cout << s.pop() << s.pop() << " " << s.empty() << "\n";
    return 0;
}

It printed 9 2.5 pear and ba 1. One definition, many types, with no loss of speed.

Exceptions

C++ reports errors with exceptions: throw an object, catch it by reference.

#include <iostream>
#include <stdexcept>
#include <vector>

double safe_div(double a, double b) {
    if (b == 0) throw std::runtime_error("division by zero");
    return a / b;
}

int main() {
    try {
        std::cout << safe_div(10, 4) << "\n";
        std::cout << safe_div(1, 0) << "\n";
    } catch (const std::exception &e) {
        std::cout << "error: " << e.what() << "\n";
    }
    std::vector<int> v = {1, 2, 3};
    try { std::cout << v.at(10); } catch (const std::out_of_range &e) { std::cout << "out of range caught\n"; }
    return 0;
}

It printed 2.5, error: division by zero and out of range caught. Note that v.at(10) checks bounds and throws, while v[10] does not check and is undefined behaviour, the same silent danger as in C.

C versus C++ at a glance

C C++
Style procedural procedural, object-oriented, generic and functional
Strings char arrays std::string
Arrays fixed arrays std::vector, std::array
Memory malloc/free by hand RAII, smart pointers
Errors return codes exceptions (and return codes)
Reuse functions, macros classes, templates, the STL
I/O printf, scanf cout, cin, streams

Common mistakes

  • Using new and delete directly instead of containers and smart pointers.
  • Forgetting a virtual destructor in a base class.
  • Using v[i] out of range (no check) instead of .at(i) while debugging.
  • Returning a reference to a local variable, which no longer exists after the function returns.
  • Copying big objects by accident; pass by const &.
  • Mixing cin >> x and getline and getting an empty line (the newline is left in the buffer).
  • Compiling without -Wall -Wextra, or without -std=c++17.
  • Using using namespace std; in header files.

Where to go next

Write small programs: a contact book with map, a word counter, a bank account class hierarchy. Try the prime number program in C++ with a vector sieve. Then see how the same ideas look in a managed language: the Java series is a good next step, and the sorting technique for huge data sets is in external sort.

Advertisement