Objects as Function Arguments in C++

Introduction

In C++, objects can be passed as arguments to functions, just like fundamental data types (int, char, etc.). This is a powerful feature that enables you to work with complex data structures in a modular way.

Ways to Pass Objects to Functions

There are three main ways to pass objects to functions in C++:

  1. Pass by Value
  2. Pass by Reference
  3. Pass by Pointer

1. Passing Objects by Value

When you pass an object by value, a copy of the object is created and passed to the function. The original object remains unchanged regardless of what happens inside the function.

Syntax:

void functionName(ClassName objectName);

Example:

#include <iostream>
using namespace std;

class Rectangle {
private:
    int length;
    int width;
    
public:
    // Constructor
    Rectangle(int l = 0, int w = 0) {
        length = l;
        width = w;
    }
    
    int getArea() {
        return length * width;
    }
    
    void setDimensions(int l, int w) {
        length = l;
        width = w;
    }
    
    void display() {
        cout << "Length: " << length << ", Width: " << width << endl;
        cout << "Area: " << getArea() << endl;
    }
};

// Function that takes Rectangle object by value
void modifyRectangle(Rectangle rect) {
    // This changes only the local copy of the rectangle
    rect.setDimensions(20, 30);
    cout << "Inside function (after modification):" << endl;
    rect.display();
}

int main() {
    Rectangle myRect(10, 5);
    
    cout << "Original rectangle:" << endl;
    myRect.display();
    
    modifyRectangle(myRect);
    
    cout << "Original rectangle (after function call):" << endl;
    myRect.display();  // Original remains unchanged
    
    return 0;
}

Pros and Cons of Pass by Value:

Pros:

  • Original object is protected from changes
  • Simpler to understand (no pointers or references)

Cons:

  • Creates a copy, which can be expensive for large objects
  • Copy constructor is called, which might be a performance concern
  • Cannot modify the original object

2. Passing Objects by Reference

When you pass an object by reference, the function receives a reference to the original object. No copy is created, and any changes made inside the function affect the original object.

Syntax:

void functionName(ClassName& objectName);

Example:

#include <iostream>
using namespace std;

class Rectangle {
    // Same as previous example
    // ...
};

// Function that takes Rectangle object by reference
void modifyRectangle(Rectangle& rect) {
    // This changes the original rectangle
    rect.setDimensions(20, 30);
    cout << "Inside function (after modification):" << endl;
    rect.display();
}

int main() {
    Rectangle myRect(10, 5);
    
    cout << "Original rectangle:" << endl;
    myRect.display();
    
    modifyRectangle(myRect);
    
    cout << "Original rectangle (after function call):" << endl;
    myRect.display();  // Original is modified
    
    return 0;
}

Pros and Cons of Pass by Reference:

Pros:

  • No copy is created (efficient)
  • Can modify the original object
  • No need to use pointers or dereferencing

Cons:

  • Original object can be modified inadvertently
  • May be less clear that the function modifies the original object

Const References

If you want to pass an object efficiently (without making a copy) but don’t want to modify it, you can use a const reference:

void displayRectangle(const Rectangle& rect) {
    // Cannot modify rect here because it's const
    cout << "Rectangle area: " << rect.getArea() << endl;
    
    // This would cause a compilation error:
    // rect.setDimensions(20, 30);
}

3. Passing Objects by Pointer

You can also pass the address of an object (a pointer to the object) to a function.

Syntax:

void functionName(ClassName* objectPointer);

Example:

#include <iostream>
using namespace std;

class Rectangle {
    // Same as previous example
    // ...
};

// Function that takes a pointer to a Rectangle object
void modifyRectangle(Rectangle* rectPtr) {
    // The arrow operator -> is used with pointers
    rectPtr->setDimensions(20, 30);
    cout << "Inside function (after modification):" << endl;
    rectPtr->display();
}

int main() {
    Rectangle myRect(10, 5);
    
    cout << "Original rectangle:" << endl;
    myRect.display();
    
    // Pass the address of the object
    modifyRectangle(&myRect);
    
    cout << "Original rectangle (after function call):" << endl;
    myRect.display();  // Original is modified
    
    return 0;
}

Pros and Cons of Pass by Pointer:

Pros:

  • No copy is created (efficient)
  • Can modify the original object
  • Can pass NULL to indicate “no object”

Cons:

  • More complex syntax with arrow operator (->)
  • Need to check for NULL pointers
  • More prone to errors like dereferencing NULL

Passing Objects to Constructors

Objects can also be passed to the constructors of other objects:

class Point {
private:
    int x, y;
    
public:
    Point(int xVal = 0, int yVal = 0) : x(xVal), y(yVal) {}
    
    int getX() const { return x; }
    int getY() const { return y; }
};

class Circle {
private:
    Point center;
    double radius;
    
public:
    // Constructor that takes a Point object
    Circle(const Point& c, double r) : center(c), radius(r) {}
    
    void display() {
        cout << "Circle with center (" << center.getX() << ", " 
             << center.getY() << ") and radius " << radius << endl;
    }
};

int main() {
    Point p(5, 10);
    Circle c(p, 7.5);
    c.display();
    
    return 0;
}

Returning Objects from Functions

Just as you can pass objects to functions, you can also return objects from functions:

Example:

#include <iostream>
using namespace std;

class Complex {
private:
    double real;
    double imag;
    
public:
    Complex(double r = 0, double i = 0) : real(r), imag(i) {}
    
    void display() const {
        cout << real;
        if (imag >= 0) cout << " + " << imag << "i" << endl;
        else cout << " - " << -imag << "i" << endl;
    }
    
    // Function to add two Complex numbers
    Complex add(const Complex& other) const {
        Complex result;
        result.real = real + other.real;
        result.imag = imag + other.imag;
        return result;  // Returns an object
    }
};

// Function that returns a Complex object
Complex multiplyByTwo(const Complex& c) {
    return Complex(c.getReal() * 2, c.getImag() * 2);
}

int main() {
    Complex c1(3, 4), c2(1, -2);
    
    cout << "c1: ";
    c1.display();
    
    cout << "c2: ";
    c2.display();
    
    // Using member function to add and return an object
    Complex sum = c1.add(c2);
    cout << "Sum: ";
    sum.display();
    
    // Using global function to multiply and return an object
    Complex doubled = multiplyByTwo(c1);
    cout << "c1 doubled: ";
    doubled.display();
    
    return 0;
}

Passing Arrays of Objects to Functions

You can also pass arrays of objects to functions:

#include <iostream>
using namespace std;

class Student {
private:
    int id;
    string name;
    float gpa;
    
public:
    Student(int i = 0, string n = "", float g = 0.0) : id(i), name(n), gpa(g) {}
    
    void display() const {
        cout << "ID: " << id << ", Name: " << name << ", GPA: " << gpa << endl;
    }
    
    float getGPA() const { return gpa; }
};

// Function that takes an array of Students
void displayAllStudents(Student students[], int size) {
    for (int i = 0; i < size; i++) {
        students[i].display();
    }
}

// Function that finds the student with the highest GPA
Student findTopStudent(Student students[], int size) {
    if (size <= 0) return Student();
    
    int topIndex = 0;
    for (int i = 1; i < size; i++) {
        if (students[i].getGPA() > students[topIndex].getGPA()) {
            topIndex = i;
        }
    }
    
    return students[topIndex];
}

int main() {
    // Array of Student objects
    Student classList[3] = {
        Student(101, "Alice", 3.9),
        Student(102, "Bob", 3.5),
        Student(103, "Charlie", 4.0)
    };
    
    cout << "Class List:" << endl;
    displayAllStudents(classList, 3);
    
    Student topStudent = findTopStudent(classList, 3);
    cout << "\nTop Student:" << endl;
    topStudent.display();
    
    return 0;
}

Friend Functions and Objects as Arguments

Friend functions can take objects as arguments and access their private members:

class Box {
private:
    double length, width, height;
    
public:
    Box(double l = 0, double w = 0, double h = 0) 
        : length(l), width(w), height(h) {}
    
    // Friend function declaration
    friend double calculateVolume(const Box& box);
};

// Friend function definition - can access private members of Box
double calculateVolume(const Box& box) {
    return box.length * box.width * box.height;
}

int main() {
    Box myBox(3, 4, 5);
    cout << "Volume: " << calculateVolume(myBox) << endl;
    return 0;
}

Common Pitfalls When Passing Objects

  1. Slicing Problem: When a derived class object is passed by value to a parameter of base class type, only the base class part is copied.
class Base {
protected:
    int baseData;
public:
    Base(int d = 0) : baseData(d) {}
    virtual void display() { cout << "Base: " << baseData << endl; }
};

class Derived : public Base {
private:
    int derivedData;
public:
    Derived(int b, int d) : Base(b), derivedData(d) {}
    void display() override { 
        cout << "Base: " << baseData << ", Derived: " << derivedData << endl; 
    }
};

// This function causes object slicing when called with a Derived object
void processObject(Base obj) {  // Pass by value
    obj.display();  // Always calls Base::display() due to slicing
}

int main() {
    Derived d(1, 2);
    processObject(d);  // Object is sliced - only the Base part is copied
    
    // To avoid slicing, use reference or pointer:
    // void processObject(Base& obj) or void processObject(Base* obj)
    
    return 0;
}
  1. Copy Constructor Not Called: When passing by reference or pointer, the copy constructor is not called.

  2. Resource Management: Be careful with objects that manage resources (like dynamic memory) when passing by value.

Best Practices

  1. Prefer References for Efficiency: For most cases, pass by const reference const ClassName& for objects you don’t need to modify, and non-const reference ClassName& for objects you do need to modify.

  2. Use Value Parameters Judiciously: Only use pass-by-value when making a copy is explicitly needed or for small objects.

  3. Be Explicit About Modifications: If a function modifies an object, make it clear in the function name or documentation.

  4. Consider Move Semantics for Return Values: In modern C++ (C++11 and later), return values can often be optimized with move semantics.

  5. Avoid Returning References to Local Objects: Never return references to objects created within the function.

Summary

Passing objects to functions in C++ can be done by value, reference, or pointer, each with its own advantages and disadvantages. Reference parameters are generally preferred for efficiency, especially for larger objects. Understanding how objects are passed and returned is crucial for writing efficient and correct C++ code.