CSE202: OBJECT ORIENTED PROGRAMMING - Unit II: Functions
This unit delves deeper into the concept of functions in C++, exploring various specialized types and related concepts essential for structured and object-oriented programming.
1. Functions with Default Parameters/Arguments
Functions can be defined with parameters that have default values. If an argument is not provided for such a parameter during a function call, the default value is used.
-
Concept: Allows a function to be called with fewer arguments than it is defined to take, providing flexibility and often reducing the need for multiple overloaded functions for similar tasks.
-
Syntax: Default values are specified in the function declaration (or definition if there's no separate declaration) using the assignment operator (
=
).return_type function_name(parameter1_type param1, parameter2_type param2 = default_value2, ...) { // function body }
-
Rules:
- Once a parameter has a default value, all subsequent parameters to its right must also have default values.
- Default values should be specified only once, typically in the function declaration in a header file.
-
Example:
#include <iostream> // Function declaration with default parameters int add(int a, int b = 10, int c = 20); int main() { std::cout << "add(5): " << add(5) << std::endl; // Uses default b=10, c=20 std::cout << "add(5, 15): " << add(5, 15) << std::endl; // Uses default c=20 std::cout << "add(5, 15, 25): " << add(5, 15, 25) << std::endl; // No defaults used return 0; } // Function definition int add(int a, int b, int c) { return a + b + c; }
-
Output:
add(5): 35 add(5, 15): 40 add(5, 15, 25): 45
-
Advantages:
- Simplifies function calls.
- Reduces code redundancy compared to using multiple overloaded functions for variations in parameter lists.
- Makes functions more user-friendly.
-
Disadvantage: Can make the function signature less immediately clear about the minimum required arguments without looking at the declaration.
2. Inline Functions
Inline functions are a performance optimization technique where the compiler is requested to replace the function call with the body of the function at the point of call.
-
Concept: Reduces the overhead associated with a function call (saving context, jumping to function address, returning, restoring context). It's a trade-off: faster execution time at the cost of increased code size (due to code duplication).
-
Keyword:
inline
-
Syntax:
inline return_type function_name(parameters) { // function body }
-
Placement: The
inline
keyword is a request to the compiler. The compiler can choose to ignore it for various reasons (e.g., function is too large, contains loops, recursion, static variables). For theinline
request to be effective, the function definition (not just the declaration) must be visible at the point of the call. This usually means defining inline functions directly in header files. -
Example:
#include <iostream> inline int square(int x) { return x * x; } int main() { int num = 5; int result = square(num); // Compiler *may* replace this call with 'num * num;' std::cout << "Square of " << num << " is " << result << std::endl; return 0; }
-
Considerations:
- Best suited for small, simple functions.
- Using
inline
on large functions can lead to significant code bloat. - Recursive functions are generally not inlined.
- The decision to inline is ultimately up to the compiler, often influenced by optimization settings.
-
Difference from Macros: While similar in effect (code substitution), inline functions are type-safe, evaluate arguments only once, and respect scope, unlike preprocessor macros.
3. Manipulator Functions
Manipulators are functions used with insertion (<<
) and extraction (>>
) operators on stream objects (like std::cout
, std::cin
, std::cerr
) to modify the state of the stream, affecting how data is formatted or handled.
-
Concept: Provide control over input/output formatting without needing to call member functions of the stream objects explicitly.
-
Header: Most standard manipulators are found in
<iostream>
and<iomanip>
. -
Syntax: Used directly in the stream expression chain.
std::cout << manipulator << data; std::cin >> manipulator >> variable;
-
Common Manipulators (from
<iostream>
):std::endl
: Inserts a newline character and flushes the output buffer.std::flush
: Flushes the output buffer.std::ws
: Extracts whitespace characters from the input stream.
-
Common Manipulators (from
<iomanip>
):std::setw(int width)
: Sets the minimum field width for the next output item. The field is padded if the item is smaller.std::setprecision(int precision)
: Sets the floating-point precision. The interpretation depends on the floating-point format flags (e.g.,fixed
,scientific
).std::fixed
: Displays floating-point numbers in fixed-point notation (e.g., 123.45). Precision set bysetprecision
determines the number of digits after the decimal point.std::scientific
: Displays floating-point numbers in scientific notation (e.g., 1.23e+02). Precision set bysetprecision
determines the total number of significant digits.std::showpoint
: Always shows the decimal point and trailing zeros for floating-point numbers.std::noshowpoint
: Does not always show the decimal point and trailing zeros.std::setfill(char fill_char)
: Sets the character used for padding fields whensetw
is used.std::left
,std::right
,std::internal
: Sets the alignment within a field.std::boolalpha
,std::noboolalpha
: Displays boolean values as "true"/"false" or 1/0.std::hex
,std::oct
,std::dec
: Sets the integer base for output.
-
Example:
#include <iostream> #include <iomanip> // Required for setw, setprecision, fixed, etc. int main() { double pi = 3.1415926535; std::cout << "Default: " << pi << std::endl; // Using manipulators std::cout << std::fixed << std::setprecision(2); std::cout << "Fixed (2): " << pi << std::endl; std::cout << std::scientific << std::setprecision(4); std::cout << "Scientific(4): " << pi << std::endl; std::cout << std::fixed << std::setprecision(6) << std::showpoint; std::cout << "Fixed (6+show):" << 12.0 << std::endl; std::cout << std::setw(10) << std::setfill('*') << std::right << 123 << std::endl; std::cout << std::setw(10) << std::setfill('*') << std::left << 123 << std::endl; std::cout << std::setw(10) << std::setfill('*') << std::internal << -123 << std::endl; std::cout << std::boolalpha << true << " " << false << std::endl; std::cout << std::noboolalpha << true << " " << false << std::endl; std::cout << std::hex << 255 << std::endl; std::cout << std::dec << 255 << std::endl; return 0; }
-
Note: Stream format flags (like
fixed
,scientific
,showpoint
,hex
,oct
,dec
,left
,right
,internal
,boolalpha
) often remain set until explicitly changed.setw
andsetfill
typically apply only to the next output item.
4. Function Overloading
Function overloading allows multiple functions with the same name to exist within the same scope, provided they have different parameter lists (different number of parameters, different types of parameters, or different order of parameter types).
-
Concept: Enables using a single function name for a group of functions that perform similar tasks but operate on different data types or require different inputs. The compiler determines which overloaded function to call based on the arguments provided during the call (signature matching).
-
Requirement: Overloaded functions must differ in their parameter list (signature). The return type alone is not sufficient to distinguish overloaded functions.
-
Syntax:
return_type function_name(parameter_list1); return_type function_name(parameter_list2); // Different parameter list // ...
-
Example:
#include <iostream> #include <string> // Overloaded function: sum integers int sum(int a, int b) { std::cout << "Using int sum(int, int): "; return a + b; } // Overloaded function: sum doubles double sum(double a, double b) { std::cout << "Using double sum(double, double): "; return a + b; } // Overloaded function: concatenate strings std::string sum(std::string s1, std::string s2) { std::cout << "Using string sum(string, string): "; return s1 + s2; } // Overloaded function: sum three integers int sum(int a, int b, int c) { std::cout << "Using int sum(int, int, int): "; return a + b + c; } int main() { std::cout << sum(5, 10) << std::endl; std::cout << sum(5.5, 10.1) << std::endl; std::cout << sum("Hello", " World") << std::endl; std::cout << sum(1, 2, 3) << std::endl; // Ambiguity example (would cause error if uncommented and a suitable match isn't found) // std::cout << sum(5, 10.1) << std::endl; // Might be ambiguous or require implicit conversion rules return 0; }
-
Output:
Using int sum(int, int): 15 Using double sum(double, double): 15.6 Using string sum(string, string): Hello World Using int sum(int, int, int): 6
-
Overload Resolution: The compiler follows a specific process to determine the best match for a function call:
- Exact match: Looks for a function with parameters that exactly match the types of the arguments.
- Match with trivial conversions: Looks for a function requiring trivial conversions (e.g.,
int
tolong
,const T
toT
). - Match with standard conversions: Looks for a function requiring standard conversions (e.g.,
int
todouble
,double
toint
). - Match with user-defined conversions: Looks for a function where user-defined conversions (via constructors or conversion operators) can be applied.
- Match with ellipsis: If a function with
...
is available. If multiple viable functions are found at the same conversion level, or if no match is found, a compilation error occurs (ambiguity or no matching function).
-
Advantages:
- Improves code readability and usability by using intuitive names for related operations.
- Reduces the need for distinct function names like
sumInt
,sumDouble
,concatenateString
.
-
Relation to OOP: Overloading is a form of polymorphism (specifically, ad hoc polymorphism or static polymorphism), allowing a single interface (the function name) to handle different data types or argument lists.
5. Scope Rules
Scope refers to the region of a program where a declared name (variable, function, class, etc.) is valid and can be used. C++ has several types of scope.
-
Concept: Determines the visibility and lifetime of identifiers within a program, helping prevent naming conflicts and managing memory.
-
Types of Scope:
- Local Scope (Block Scope): An identifier declared within a block
{}
. It is visible only from its point of declaration to the end of the block. Variables declared within a function body or inside loops/conditional statements have local scope. Their lifetime is tied to the execution of the block. - Function Scope: Labels (used with
goto
) have function scope. They are visible throughout the function in which they are declared. (Note:goto
is generally discouraged in modern C++). - Function Prototype Scope: Names of parameters in a function prototype (declaration) have function prototype scope. They are only visible within the prototype itself and don't affect visibility elsewhere.
- File Scope (Global Scope): An identifier declared outside of any function or class, at the top level of a source file. It is visible from its point of declaration to the end of the file. If declared with the
static
keyword, its visibility is limited to that file (internal linkage). - Namespace Scope: An identifier declared within a
namespace
. It is visible within the namespace. To access it from outside the namespace, you need to use the scope resolution operator (::
) or ausing
declaration/directive. - Class Scope: An identifier declared within a class definition (member variables, member functions, nested types). It is visible within the class definition. Access from outside depends on the access specifiers (
public
,protected
,private
). Member functions are in the class scope.
- Local Scope (Block Scope): An identifier declared within a block
-
Scope Resolution Operator (
::
): Used to access names that are hidden by a local scope or to refer to members of a namespace or class.::variableName
: Accesses a global variable when a local variable with the same name exists.namespaceName::name
: Accesses a name within a specific namespace.ClassName::memberName
: Accesses a static member of a class or specifies which class a member function definition belongs to.
-
Example:
#include <iostream> int global_var = 10; // File scope (global) namespace MyNamespace { int namespace_var = 20; } class MyClass { public: int class_var = 30; // Class scope (member variable) static int static_class_var; // Class scope (static member) void displayScopes(int param) { // param has function prototype scope in declaration, local scope in definition int local_var = 40; // Local scope (block scope) std::cout << "Inside displayScopes:" << std::endl; std::cout << " Parameter: " << param << std::endl; // Accessing parameter (local) std::cout << " Local: " << local_var << std::endl; // Accessing local variable std::cout << " Class Member: " << class_var << std::endl; // Accessing class member std::cout << " Static Class Member: " << static_class_var << std::endl; // Accessing static class member std::cout << " Global (hidden): " << global_var << std::endl; // Accessing global variable (local var doesn't hide it here) std::cout << " Global (explicit): " << ::global_var << std::endl; // Explicitly accessing global variable std::cout << " Namespace: " << MyNamespace::namespace_var << std::endl; // Accessing namespace variable } }; // Definition for static class member (required outside class) int MyClass::static_class_var = 50; int main() { int global_var = 100; // Local variable, hides the global one std::cout << "Inside main:" << std::endl; std::cout << " Local (hiding global): " << global_var << std::endl; // Accessing local variable std::cout << " Global (explicit): " << ::global_var << std::endl; // Explicitly accessing global variable std::cout << " Namespace: " << MyNamespace::namespace_var << std::endl; // Accessing namespace variable // std::cout << local_var << std::endl; // ERROR: local_var is not in scope here MyClass obj; obj.displayScopes(5); // Calling member function std::cout << " Accessing class member from main (public): " << obj.class_var << std::endl; std::cout << " Accessing static class member from main: " << MyClass::static_class_var << std::endl; return 0; }
-
Output:
Inside main: Local (hiding global): 100 Global (explicit): 10 Namespace: 20 Accessing class member from main (public): 30 Accessing static class member from main: 50 Inside displayScopes: Parameter: 5 Local: 40 Class Member: 30 Static Class Member: 50 Global (hidden): 10 Global (explicit): 10 Namespace: 20
-
Lifetime: The duration for which a variable exists in memory. It is closely related to scope but not identical. Local variables exist during the execution of their block. Global and static variables exist for the entire duration of the program. Dynamic variables (allocated with
new
) exist until explicitly deallocated withdelete
.
6. Friend of a Class (Friend Function and Friend Class)
In C++, private
and protected
members of a class are typically only accessible by the member functions of that class. However, sometimes it's useful to allow an external function or another class to access these private/protected members. This can be achieved using the friend
keyword.
- Concept: A
friend
declaration grants special access privileges to a function or class, allowing it to access the private and protected members of the class that declared it as a friend. - Keyword:
friend
- Located: The
friend
declaration must be placed inside the class definition that is granting the access. It can be in any access specifier section (public
,protected
, orprivate
), but its location within the class definition does not affect the access rights granted (friends can access anything), only potentially how the friend declaration itself is documented. Conventionally, it's often placed in thepublic
section for clarity.
6.1 Friend Function
A function declared as a friend
of a class can access the private and protected members of that class.
-
Syntax:
class MyClass { private: int private_data; public: MyClass(int data) : private_data(data) {} // Declare an external function as a friend friend void displayPrivateData(const MyClass& obj); }; // Definition of the friend function (outside the class) void displayPrivateData(const MyClass& obj) { // This function can access private_data of MyClass objects std::cout << "Private data: " << obj.private_data << std::endl; }
-
Example:
#include <iostream> class MyClass { private: int value; public: MyClass(int val) : value(val) {} // Declare a non-member function as a friend friend void showValue(const MyClass& obj); }; // Definition of the friend function void showValue(const MyClass& obj) { // Friend function can access private members std::cout << "Value from friend function: " << obj.value << std::endl; } int main() { MyClass obj(100); showValue(obj); // Call the friend function // std::cout << obj.value; // ERROR: Cannot access private member directly return 0; }
-
Use Cases:
- Overloading stream insertion (
<<
) and extraction (>>
) operators for a class. These operators are typically non-member functions but need access to the class's internal data. - Functions that operate on objects of two different classes, needing access to private members of both.
- Utility functions closely related to a class but not conceptually part of its interface.
- Overloading stream insertion (
6.2 Friend Class
If Class B is declared as a friend
of Class A, then all member functions of Class B can access the private and protected members of Class A.
-
Syntax:
class ClassA { private: int secretA; public: ClassA(int s) : secretA(s) {} // Declare ClassB as a friend friend class ClassB; }; class ClassB { public: void accessClassA(const ClassA& objA) { // ClassB member functions can access private members of ClassA std::cout << "Accessing ClassA's secret from ClassB: " << objA.secretA << std::endl; } };
-
Example:
#include <iostream> class ClassA { private: int private_a; public: ClassA(int val) : private_a(val) {} // Declare ClassB as a friend class friend class ClassB; }; class ClassB { public: void displayA(const ClassA& objA) { // Member function of ClassB can access private members of ClassA std::cout << "ClassB accessing ClassA's private_a: " << objA.private_a << std::endl; } }; int main() { ClassA objA(123); ClassB objB; objB.displayA(objA); // ClassB's member function accessing ClassA's private data return 0; }
-
Use Cases:
- When two classes are tightly coupled and their member functions frequently need access to each other's private data (e.g., a Container class and an Iterator class).
- Building complex data structures where helper classes need deep access.
-
Important Considerations for Friends:
- Breaks Encapsulation: Friends violate the principle of encapsulation by allowing external entities to access internal details. Use them judiciously and only when necessary.
- Not Reciprocal: If Class B is a friend of Class A, Class A is not automatically a friend of Class B. Friendship must be explicitly granted by each class.
- Not Inherited: Friendship is not inherited. If BaseClass is a friend of AnotherClass, DerivedClass (inheriting from BaseClass) is not automatically a friend of AnotherClass.
7. Reference Variables
A reference variable is an alias, or an alternative name, for an existing variable. Once a reference is initialized to a variable, it cannot be changed to refer to another variable.
-
Concept: Provides a way to pass arguments to functions by "reference" and to create aliases for variables, often used for convenience or efficiency (avoiding copying large objects).
-
Syntax: Declared using the ampersand symbol (
&
).type& reference_name = existing_variable;
-
Rules:
- A reference must be initialized when it is declared.
- Once initialized, a reference cannot be reseated (made to refer to a different variable).
- References cannot be null (unlike pointers).
- You cannot have references to references, arrays of references, or pointers to references.
-
Example:
#include <iostream> int main() { int original = 10; // reference_var is an alias for original int& reference_var = original; std::cout << "Original value: " << original << std::endl; std::cout << "Reference value: " << reference_var << std::endl; // Modifying through the reference modifies the original variable reference_var = 20; std::cout << "Original value after modifying reference: " << original << std::endl; std::cout << "Reference value after modifying reference: " << reference_var << std::endl; // Attempting to reseat (this does NOT reseat, it assigns the value) int another = 30; reference_var = another; // This assigns the VALUE of 'another' (30) to 'original', NOT makes reference_var refer to 'another'. std::cout << "Original value after 'reseating' attempt: " << original << std::endl; // original is now 30 std::cout << "Reference value after 'reseating' attempt: " << reference_var << std::endl; // reference_var is still an alias for original, so it's also 30 std::cout << "Another value: " << another << std::endl; // another is still 30 return 0; }
-
Output:
Original value: 10 Reference value: 10 Original value after modifying reference: 20 Reference value after modifying reference: 20 Original value after 'reseating' attempt: 30 Reference value after 'reseating' attempt: 30 Another value: 30
-
References as Function Parameters (Call by Reference): This is a primary use case (see next section). Passing a reference allows the function to modify the original argument.
-
References as Function Return Types: A function can return a reference. This allows the caller to modify the object that was returned by reference. Be cautious: don't return a reference to a local variable that will go out of scope when the function returns.
-
References vs. Pointers:
- References must be initialized; pointers can be null.
- References cannot be reseated; pointers can point to different objects.
- References are generally safer and easier to use for simple aliasing and pass-by-reference. Pointers are more flexible for dynamic allocation, pointer arithmetic, and representing optional values (via null).
8. Differences between Call by Value, Call by Address, and Call by Reference
These are different ways of passing arguments to functions, affecting whether the function can modify the original variable passed from the caller.
Feature | Call by Value | Call by Address (using Pointers) | Call by Reference (using References) |
---|---|---|---|
Mechanism | A copy of the argument's value is passed to the function. | The memory address of the argument is passed to the function. | An alias (reference) to the original argument is passed to the function. |
Function Parameter | type param_name |
type* param_name |
type& param_name |
Function Call | function(variable) |
function(&variable) |
function(variable) |
Modification | Function works on a copy; changes inside the function do not affect the original variable. | Function works on the original variable via its address (dereferencing the pointer); changes do affect the original. | Function works directly on the original variable via its alias; changes do affect the original. |
Overhead | Copying the value (can be significant for large objects). | Copying the address (usually small). | No copy of the object itself is made (only the reference alias). Generally efficient. |
Null/Invalid | Not applicable. | Pointer can be nullptr (or invalid), requiring checks. |
Reference must refer to a valid object; cannot be null. |
Syntax in Function | Use param_name directly. |
Use *param_name to access/modify the value. |
Use param_name directly (like a local variable). |
Safety/Ease | Safest (original is protected), simple syntax. | Requires careful handling of pointers, including null checks and dereferencing. | Relatively safe (cannot be null, cannot be reseated), simpler syntax than pointers for modifying original. |
-
Example:
#include <iostream> // Call by Value: Swaps copies, original variables unchanged void swap_value(int x, int y) { int temp = x; x = y; y = temp; std::cout << "Inside swap_value: x = " << x << ", y = " << y << std::endl; } // Call by Address: Swaps values at the addresses pointed to, originals changed void swap_address(int* x, int* y) { int temp = *x; *x = *y; *y = temp; std::cout << "Inside swap_address: *x = " << *x << ", *y = " << *y << std::endl; } // Call by Reference: Swaps values of the original variables using aliases, originals changed void swap_reference(int& x, int& y) { int temp = x; x = y; y = temp; std::cout << "Inside swap_reference: x = " << x << ", y = " << y << std::endl; } int main() { int a = 10, b = 20; std::cout << "Before swap_value: a = " << a << ", b = " << b << std::endl; swap_value(a, b); std::cout << "After swap_value: a = " << a << ", b = " << b << std::endl; // a=10, b=20 (unchanged) std::cout << "--------------------" << std::endl; a = 10, b = 20; // Reset values std::cout << "Before swap_address: a = " << a << ", b = " << b << std::endl; swap_address(&a, &b); // Pass addresses std::cout << "After swap_address: a = " << a << ", b = " << b << std::endl; // a=20, b=10 (changed) std::cout << "--------------------" << std::endl; a = 10, b = 20; // Reset values std::cout << "Before swap_reference: a = " << a << ", b = " << b << std::endl; swap_reference(a, b); // Pass variables directly, treated as references std::cout << "After swap_reference: a = " << a << ", b = " << b << std::endl; // a=20, b=10 (changed) std::cout << "--------------------" << std::endl; return 0; }
-
Output:
Before swap_value: a = 10, b = 20 Inside swap_value: x = 20, y = 10 After swap_value: a = 10, b = 20 -------------------- Before swap_address: a = 10, b = 20 Inside swap_address: *x = 20, *y = 10 After swap_address: a = 20, b = 10 -------------------- Before swap_reference: a = 10, b = 20 Inside swap_reference: x = 20, y = 10 After swap_reference: a = 20, b = 10 --------------------
-
When to Use:
- Call by Value: When you only need to use the argument's value and do not want to modify the original. Safe and simple.
- Call by Reference (using
&
): When you need to modify the original argument, or when passing large objects and want to avoid the copy overhead, without allowing the argument to be null or requiring pointer arithmetic. Often preferred over call by address in C++ for modifying arguments. Useconst&
if you want to avoid copying but not modify the original (efficient constant access). - *Call by Address (using `
):** When you need to modify the original argument, or when passing large objects to avoid copy overhead, *and* you need the flexibility of pointers (e.g., the possibility of passing
nullptr`, pointer arithmetic, working with C APIs).
9. Recursion (Function, Member Function)
Recursion is a programming technique where a function calls itself to solve a problem. A recursive solution typically involves breaking down a problem into smaller, self-similar subproblems.
- Concept: A function is recursive if its definition includes a call to itself. To prevent infinite recursion, a recursive function must have one or more base cases that stop the recursion.
- Components of a Recursive Function:
- Base Case(s): A condition where the function does not call itself and returns a result directly. This is essential to terminate the recursion.
- Recursive Step: The part where the function calls itself with a modified input, moving towards the base case.
- How it Works: Each recursive call creates a new stack frame for the function, with its own set of local variables and parameters. When a base case is reached, the calls start returning, unwinding the stack.
9.1 Recursive Function
A standalone function that calls itself.
-
Example: Factorial Calculation
- Problem: Calculate $n!$ (n factorial). $n! = n \times (n-1) \times \dots \times 1$. $0! = 1$.
- Base Case: If $n = 0$, return 1.
- Recursive Step: If $n > 0$, return $n \times \text{factorial}(n-1)$.
#include <iostream>
// Recursive function to calculate factorial long long factorial(int n) { // Base Case if (n == 0 || n == 1) { return 1; } // Recursive Step else if (n > 1) { return n * factorial(n - 1); } else { // Handle negative input (optional, depending on problem definition) std::cerr << "Factorial not defined for negative numbers." << std::endl; return -1; // Indicate error } }
int main() { int num = 5; long long result = factorial(num); if (result != -1) { std::cout << "Factorial of " << num << " is " << result << std::endl; // Output: 120 }
num = 0; result = factorial(num); if (result != -1) { std::cout << "Factorial of " << num << " is " << result << std::endl; // Output: 1 } return 0;
}
-
Example: Fibonacci Sequence
- Problem: Calculate the $n$-th Fibonacci number. $F_n = F_{n-1} + F_{n-2}$. Sequence starts 0, 1, 1, 2, 3, 5, 8...
- Base Cases: $F_0 = 0$, $F_1 = 1$.
- Recursive Step: For $n > 1$, return $\text{fibonacci}(n-1) + \text{fibonacci}(n-2)$.
#include <iostream>
// Recursive function for Fibonacci int fibonacci(int n) { // Base Cases if (n <= 1) { return n; } // Recursive Step else { return fibonacci(n - 1) + fibonacci(n - 2); } }
int main() { int num = 10; std::cout << "Fibonacci(" << num << ") is " << fibonacci(num) << std::endl; // Output: 55 return 0; }
* **Note:** This simple recursive Fibonacci is inefficient due to repeated calculations. Iterative solutions or recursive solutions with memoization are preferred for performance.
9.2 Recursive Member Function
A member function of a class that calls itself.
-
Example: Traversing a Tree Structure (Conceptual) Imagine a simple node class for a binary tree. A member function could recursively traverse the tree.
#include <iostream> struct Node { int data; Node* left; Node* right; Node(int val) : data(val), left(nullptr), right(nullptr) {} }; class BinaryTree { private: Node* root; // Private recursive helper function for inorder traversal void inorderTraversal(Node* current_node) const { // Base Case: If the node is null, stop if (current_node == nullptr) { return; } // Recursive Step: inorderTraversal(current_node->left); // Traverse left subtree std::cout << current_node->data << " "; // Visit current node inorderTraversal(current_node->right); // Traverse right subtree } // Private recursive helper function for tree deletion void destroyTree(Node* current_node) { if (current_node == nullptr) { return; } destroyTree(current_node->left); destroyTree(current_node->right); // std::cout << "Deleting: " << current_node->data << std::endl; // Optional: show deletion order delete current_node; } public: BinaryTree() : root(nullptr) {} // Public function to start the traversal (calls the recursive helper) void displayInorder() const { std::cout << "Inorder Traversal: "; inorderTraversal(root); std::cout << std::endl; } // Method to insert nodes (simplified, no balancing) void insert(int val) { Node* newNode = new Node(val); if (root == nullptr) { root = newNode; return; } Node* current = root; Node* parent = nullptr; while (current != nullptr) { parent = current; if (val < current->data) { current = current->left; } else { current = current->right; } } if (val < parent->data) { parent->left = newNode; } else { parent->right = newNode; } } // Destructor to clean up memory using recursion ~BinaryTree() { destroyTree(root); root = nullptr; // Good practice to set root to nullptr after deletion } }; int main() { BinaryTree tree; tree.insert(50); tree.insert(30); tree.insert(70); tree.insert(20); tree.insert(40); tree.insert(60); tree.insert(80); tree.displayInorder(); // Should print 20 30 40 50 60 70 80 // Tree is automatically destroyed when main ends (destructor called) return 0; }
-
Output:
Inorder Traversal: 20 30 40 50 60 70 80
-
Advantages of Recursion:
- Can lead to elegant and concise code for problems that have a naturally recursive structure (e.g., tree traversals, fractal generation, certain mathematical functions).
- Closely mirrors mathematical definitions for some problems (like factorial, Fibonacci).
-
Disadvantages of Recursion:
- Can be harder to understand and debug than iterative solutions.
- Can be less efficient due to function call overhead.
- Can lead to stack overflow errors if the recursion depth is too large (each call consumes stack space). Iteration is often preferred for very deep problems.
- Recursive solutions might perform redundant computations (as seen in the naive Fibonacci example).
This concludes the detailed notes for Unit II, covering various aspects of functions, parameter passing, scope, and recursion in C++. These concepts are building blocks for understanding and applying Object-Oriented Programming principles in C++.