Classes, Objects, Friend Functions and Buggy Code Snippets
Priority: MEDIUM — likely as warm-up questions and "spot the bug" exercises Spoken answer target: under 45 seconds each
1. Class
Definition:
A class is a user-defined blueprint or template that defines the data (attributes) and behavior (methods) of objects. It is a logical construct — it occupies no memory by itself.
Key Point:
- Data members are private by default (vs struct: public by default)
- Class defines WHAT the object looks like; the object IS an instance at runtime
- In C++, class and struct differ only in default access: class = private, struct = public
class Dog {
private:
string name;
string breed;
public:
Dog(string n, string b) : name(n), breed(b) {}
void bark() { cout << name << " says: Woof!"; }
string getName() const { return name; }
};Likely follow-up:
"Struct vs Class in C++?" The ONLY technical difference is default access: struct members are public by default, class members are private. Convention: use struct for plain data holders (no behavior, no encapsulation), class for objects with behavior and protected state.
2. Object
Definition:
An object is an instance of a class — a concrete realization of the blueprint that occupies actual memory. Each object has its own copy of data members but shares the class's methods.
Key Point:
- Stack: fast, automatic cleanup (RAII), limited size
- Heap (
new): flexible lifetime, must explicitly delete (or use smart pointers) - Modern C++: prefer
unique_ptrover rawnew/delete
// Stack allocation — automatic cleanup when scope ends
Dog d1("Buddy", "Labrador");
d1.bark();
// Heap allocation — manual (dangerous — prefer smart pointers!)
Dog* d2 = new Dog("Max", "Shepherd");
d2->bark();
delete d2; // Must manually free!
// Smart pointer — automatic cleanup, no manual delete
auto d3 = make_unique<Dog>("Rex", "Husky");
d3->bark();
// d3 auto-deleted when it goes out of scopeLikely follow-up:
"What is RAII?" Resource Acquisition Is Initialization — tie a resource's lifetime to an object's lifetime. Acquire in constructor, release in destructor. Destructors run automatically when scope ends, so resources cannot leak — even if exceptions are thrown.
unique_ptr,lock_guard,fstreamare all RAII types.
3. Friend Function
Definition:
A friend function is a non-member function (or another class) that is explicitly granted access to the private and protected members of a class by declaring it inside the class with the
friendkeyword.
Key Point:
- Friend is explicitly granted — the class remains in control
- Does NOT break encapsulation — it extends it in a controlled way
- Most common use: operator overloading (especially
<<,>>, and symmetric operators like3 * vec)
class BankAccount {
private:
double balance;
string owner;
public:
BankAccount(string o, double b) : owner(o), balance(b) {}
// Declare friend — grants access to private members
friend void audit(const BankAccount& acc);
friend class Auditor;
// friend needed for << because ostream is left operand
friend ostream& operator<<(ostream& os, const BankAccount& acc) {
os << acc.owner << ": $" << acc.balance; // Accesses private directly!
return os;
}
};
// This function is NOT a member — but can access private fields
void audit(const BankAccount& acc) {
cout << "Audit: " << acc.owner << " has $" << acc.balance;
// ^^^^^^^^^^^^^^ private — OK via friend!
}
int main() {
BankAccount acc("Alice", 1000);
audit(acc); // Non-member accessing private
cout << acc << endl; // friend operator<<
}Likely follow-up:
"Is friend breaking encapsulation?" No — the class EXPLICITLY grants access. Only specific functions/classes are friends. If anything, it is a deliberate, documented extension of trust — not a bypass.
Common mistake: Overusing friend. If you are making a dozen unrelated classes friends, you have lost the point of encapsulation. Friend should be reserved for tightly coupled pairs (like a class and its operator<< implementation).
4. Getters and Setters — Encapsulation Link
Definition:
Getters (accessors) and setters (mutators) are public methods that provide controlled read/write access to private data members. The key is the VALIDATION and SIDE EFFECTS they can add.
Key Point: A getter/setter pair without validation is just a struct with extra steps — no real encapsulation. Real value comes from: validation, computed properties, triggering side effects (logging, notifications, caching), and the ability to change internal representation later without breaking callers.
class Temperature {
private:
double celsius;
public:
// Getter — read-only access
double getCelsius() const { return celsius; }
// Computed property — derived from internal state
double getFahrenheit() const { return celsius * 9.0 / 5.0 + 32; }
// Setter WITH VALIDATION
void setCelsius(double value) {
if (value < -273.15)
throw invalid_argument("Below absolute zero!");
celsius = value;
}
// Setter that reuses validation logic
void setFahrenheit(double f) {
setCelsius((f - 32) * 5.0 / 9.0); // Converts then calls validated setter
}
};BAD example (anemic class — no real encapsulation):
class User {
string name;
string email;
public:
string getName() { return name; }
void setName(string n) { name = n; } // No validation
string getEmail() { return email; }
void setEmail(string e) { email = e; } // No validation
// This is just a struct in disguise!
};5. Buggy Code Snippets — Spot the Bug (Interview Favorites)
Bug 1: Missing virtual — Polymorphism Silently Broken
class Base {
public:
void speak() { cout << "Base"; } // BUG: not virtual!
};
class Derived : public Base {
public:
void speak() { cout << "Derived"; }
};
int main() {
Base* p = new Derived();
p->speak(); // Outputs "Base" -- NOT "Derived"!
delete p;
}Bug: speak() is not virtual. Through a Base pointer, static binding always calls Base::speak. No dynamic dispatch happens.
Fix: Add virtual to Base::speak(). Also add override in Derived.
Bug 2: Missing Virtual Destructor — Memory Leak
class Base {
public:
~Base() { cout << "Base destroyed"; } // BUG: not virtual!
};
class Derived : public Base {
int* data = new int[100];
public:
~Derived() {
delete[] data; // This NEVER runs!
cout << "Derived destroyed";
}
};
int main() {
Base* p = new Derived();
delete p; // Only ~Base() runs. ~Derived() is skipped. MEMORY LEAK!
}Bug: Non-virtual destructor. delete p calls Base's destructor only. Derived's destructor (which frees data) is never called.
Fix: virtual ~Base() { ... }. Rule: any class with virtual functions MUST have a virtual destructor.
Bug 3: Shallow Copy — Double Delete Crash
class Resource {
int* data;
int size;
public:
Resource(int s) : size(s), data(new int[s]) {}
// BUG: No copy constructor defined!
// Compiler generates a SHALLOW copy — both objects share same pointer!
~Resource() { delete[] data; } // Both r1 and r2 will try to delete the same pointer!
};
int main() {
Resource r1(10);
Resource r2 = r1; // Shallow copy! r1.data == r2.data
// r2 destroyed: delete[] data -- OK
// r1 destroyed: delete[] data -- CRASH! Already freed! Double-free undefined behavior.
}Bug: Compiler-generated copy constructor does shallow copy. Both objects share the same raw pointer. Double-delete on destruction.
Fix: Write a deep copy constructor: Resource(const Resource& other) : size(other.size), data(new int[other.size]) { ... copy values ... }
Bug 4: Calling Virtual Function in Constructor
class Base {
public:
Base() {
init(); // BUG: virtual dispatch does NOT work in constructors!
}
virtual void init() { cout << "Base init"; }
};
class Derived : public Base {
public:
void init() override { cout << "Derived init"; }
};
int main() {
Derived d; // Output: "Base init" — NOT "Derived init"!
}Bug: During Base's constructor execution, the Derived part of the object has not been constructed yet. The vptr points to Base's vtable, so init() resolves to Base::init() always.
Fix: Don't call virtual functions in constructors or destructors. Initialize in the constructor body using non-virtual calls, or use a factory method.
Bug 5: Object Slicing — Losing Derived Data
void makeSpeak(Animal a) { // BUG: pass by VALUE — slicing!
a.speak(); // Always calls Animal::speak, even if you pass a Dog
}
int main() {
Dog d;
makeSpeak(d); // "..." — Dog's override is LOST
}Bug: The function takes Animal by value. When d is passed, the Dog portion (including the vtable pointer and breed) is sliced off. Only the Animal part is copied.
Fix: void makeSpeak(const Animal& a) — pass by reference (or pointer) to preserve polymorphism.
Bug 6: Diamond Problem Without Virtual Inheritance
class A { public: int x = 10; };
class B : public A {}; // BUG: should be virtual public A
class C : public A {}; // BUG: should be virtual public A
class D : public B, public C {};
int main() {
D d;
d.x = 5; // ERROR: ambiguous! d.B::A::x or d.C::A::x?
// Workaround (ugly):
d.B::x = 5;
d.C::x = 5; // Two separate copies!
}Bug: Without virtual inheritance, B and C each have their own copy of A. D has two copies, making d.x ambiguous.
Fix: class B : virtual public A and class C : virtual public A. Then D must initialize A directly: D() : A() {}.
Bug 7: Overload Ambiguity with Default Arguments
void print(int x, int y = 0) { cout << "A"; }
void print(int x) { cout << "B"; }
int main() {
print(5); // ERROR: ambiguous! Both match!
print(5, 0); // OK — only first overload matches
}Bug: print(5) could match both print(int) and print(int, int=0) since the default argument makes y optional. Compiler cannot resolve this.
Fix: Remove one of the overloads, or change the default argument so they are no longer ambiguous.
Bug 8: Using Moved-From Object
int main() {
string s1 = "hello";
string s2 = move(s1); // s1 is now in a valid but empty state
cout << s1; // BUG: undefined behavior! s1 has been moved from.
// In practice with std::string: outputs "" (empty), but relying on this is wrong.
s1 = "world"; // OK — reassigning a moved-from object is safe
cout << s1; // "world"
}Bug: Using s1 after it has been moved. The moved-from object is in a valid but unspecified state. You should only destroy or reassign it.
Fix: Do not access a moved-from object. If you must reuse it, reassign it first.
Quick Self-Test — Classes, Friends, Bugs
- What is the only difference between class and struct in C++?
- What is a friend function — does it break encapsulation?
- Why should
operator<<be a friend (or non-member)? - What is object slicing and how do you prevent it?
- What is the bug when a virtual function is called in a constructor?
- What causes a double-free crash in C++?
- What is the correct signature for post-increment operator++?
- Can a function be a friend of multiple classes?
- What is the difference between passing by value and by reference for polymorphic objects?
- After
string s2 = move(s1), what is the state of s1?
Answers
- Default access: struct = public, class = private. That is the ONLY difference.
- A non-member function explicitly granted access to private members. It does NOT break encapsulation — the class grants it intentionally.
- The left operand of << is ostream, not your class. Member functions require your class as the left operand. So it must be a free function (friend to access private data).
- Derived-specific data is lost when a derived object is copied into a base object by value. Prevent with pointers or references.
- During construction, the vptr points to the current class's vtable (not derived). Virtual dispatch resolves to the base version, not the override.
- Shallow copy — two objects share the same raw pointer. When both are destroyed, delete is called twice on the same memory.
ClassName operator++(int)— takes a dummy int parameter to distinguish from pre-increment.- Yes — a function can be declared as friend inside multiple class declarations.
- By value causes slicing — only the base portion is copied and polymorphism is lost. By reference or pointer preserves the actual type and enables virtual dispatch.
- Valid but unspecified state. With std::string it is usually empty. You should only destroy or reassign it — do NOT read from it.