Classes: User-Defined Types
Classes are the foundation of object-oriented programming in C++. They combine data (members) and behavior (methods) into cohesive types with controlled access.
class Point {
private:
double x_, y_; // Data members (by convention, trailing underscore)
public:
Point(double x, double y) : x_(x), y_(y) {} // Constructor
double distance() const { // Const member function
return std::sqrt(x_ * x_ + y_ * y_);
}
void setX(double x) { x_ = x; } // Mutator
double getX() const { return x_; } // Accessor
};
Access Specifiers
C++ provides three access levels:
- private: Accessible only within the class
- protected: Accessible within the class and derived classes
- public: Accessible everywhere
Default access: class defaults to private, struct defaults to public.
class MyClass {
int private_by_default;
public:
int explicitly_public;
};
struct MyStruct {
int public_by_default;
private:
int explicitly_private;
};
Convention: Use class for types with invariants requiring encapsulation; use struct for passive data containers (POD types).
Constructors and Initialization
Constructors initialize objects. C++ provides several kinds:
Default Constructor
class Widget {
public:
Widget() : value_(0) {} // Default constructor
private:
int value_;
};
Widget w; // Calls default constructor
Parameterized Constructor
class Point {
public:
Point(double x, double y) : x_(x), y_(y) {}
private:
double x_, y_;
};
Point p(3.0, 4.0); // Direct initialization
Point q = {5.0, 6.0}; // Aggregate initialization (C++11)
Member Initializer Lists
Always prefer initializer lists over assignment in constructor body:
class Container {
std::string name_;
std::vector<int> data_;
public:
// Good: Direct initialization
Container(std::string n) : name_(n), data_(100) {}
// Bad: Default construction then assignment
// Container(std::string n) { name_ = n; data_.resize(100); }
};
Reasons:
- Initialization is generally more efficient than assignment
- Const and reference members must use initializer lists
- Base classes and members without default constructors require it
Delegating Constructors (C++11)
Constructors can delegate to other constructors:
class Rectangle {
double width_, height_;
public:
Rectangle(double w, double h) : width_(w), height_(h) {}
Rectangle() : Rectangle(0, 0) {} // Delegates to above
};
Destructors and RAII
Destructors execute when objects are destroyed (scope exit, delete, exception). They’re the foundation of RAII (Resource Acquisition Is Initialization): tying resource lifetime to object lifetime.
class FileHandle {
FILE* file_;
public:
FileHandle(const char* path) : file_(fopen(path, "r")) {
if (!file_) throw std::runtime_error("Failed to open");
}
~FileHandle() {
if (file_) fclose(file_); // Always cleanup
}
// Prevent copying (for now)
FileHandle(const FileHandle&) = delete;
FileHandle& operator=(const FileHandle&) = delete;
};
void process() {
FileHandle f("data.txt");
// Use file...
} // Destructor automatically closes file, even if exceptions thrown
Special Member Functions
The compiler auto-generates certain functions if not user-declared:
- Default constructor (if no constructors declared)
- Destructor
- Copy constructor:
T(const T&) - Copy assignment:
T& operator=(const T&) - Move constructor:
T(T&&)(C++11) - Move assignment:
T& operator=(T&&)(C++11)
The Rule of Zero
Best Practice: If you can, don’t declare any of the special members. Let the compiler generate them. Use standard library types that manage resources correctly.
class Good {
std::string name_;
std::vector<int> data_;
// Compiler-generated special members are correct
};
The Rule of Three/Five
If you declare any of destructor, copy constructor, or copy assignment, you should probably declare all three (Rule of Three). In modern C++, also declare move constructor and move assignment (Rule of Five).
class Buffer {
char* data_;
size_t size_;
public:
// Constructor
Buffer(size_t s) : data_(new char[s]), size_(s) {}
// Destructor
~Buffer() { delete[] data_; }
// Copy constructor
Buffer(const Buffer& other) : data_(new char[other.size_]), size_(other.size_) {
std::copy(other.data_, other.data_ + size_, data_);
}
// Copy assignment
Buffer& operator=(const Buffer& other) {
if (this != &other) {
delete[] data_;
size_ = other.size_;
data_ = new char[size_];
std::copy(other.data_, other.data_ + size_, data_);
}
return *this;
}
// Move constructor
Buffer(Buffer&& other) noexcept : data_(other.data_), size_(other.size_) {
other.data_ = nullptr;
other.size_ = 0;
}
// Move assignment
Buffer& operator=(Buffer&& other) noexcept {
if (this != &other) {
delete[] data_;
data_ = other.data_;
size_ = other.size_;
other.data_ = nullptr;
other.size_ = 0;
}
return *this;
}
};
However, prefer std::vector<char> and Rule of Zero!
Why prefer member initializer lists over assignment in constructor bodies?
Interactive Lab
Waiting for signal...