Model your domain with classes: data plus methods, encapsulation, constructors and destructors, structs versus classes, and the Rule of Zero / Three / Five.
A class bundles data (members) with the functions that operate on them (methods) and controls access: private members are hidden, public ones form the interface. A struct is the same thing with public default access — use struct for plain data, class when you enforce invariants.
#include <iostream>
class Counter {
int value_ = 0; // private: invariant is "never negative"
public:
void increment() { ++value_; }
void reset() { value_ = 0; }
int get() const { return value_; } // const: doesn't modify state
};
int main() {
Counter c;
c.increment();
c.increment();
std::cout << c.get() << "\n"; // 2
// c.value_ = -1; // error: private
return 0;
}A constructor initializes an object; the member initializer list (: a_(a)) sets members before the body runs and is the preferred way. The destructor (~Name) runs when the object dies and is where RAII cleanup lives. explicit stops unwanted implicit conversions.
#include <iostream>
#include <string>
class Account {
std::string owner_;
double balance_;
public:
Account(std::string owner, double opening)
: owner_(std::move(owner)), balance_(opening) {} // init list
~Account() { std::cout << "closing " << owner_ << "\n"; }
void deposit(double amt) { balance_ += amt; }
double balance() const { return balance_; }
};
int main() {
Account a("Ada", 100.0);
a.deposit(50);
std::cout << a.balance() << "\n"; // 150
return 0; // "closing Ada" prints here
}If your class manages a resource by hand, and you define a destructor, copy constructor, or copy assignment, you almost certainly need all three (Rule of Three) — plus the move constructor and move assignment (Rule of Five). The best option is the Rule of Zero: hold resources in members like std::string and std::unique_ptr that already manage themselves, and write none of them.
#include <string>
#include <vector>
// Rule of Zero: members manage their own memory, so the compiler-generated
// copy, move, and destructor are all correct. Write nothing.
class Profile {
std::string name_;
std::vector<int> scores_;
public:
Profile(std::string n) : name_(std::move(n)) {}
void add(int s) { scores_.push_back(s); }
};
// If you DID hold a raw owning pointer you'd owe all five:
// ~T(), T(const T&), T& operator=(const T&),
// T(T&&) noexcept, T& operator=(T&&) noexcept
// ... which is exactly the bookkeeping unique_ptr removes.
int main() {
Profile p("Ada");
Profile copy = p; // compiler-generated copy just works
return 0;
}