Understanding Binding in Programming
Prerequisite: Memory and Its Role in Binding
Before diving into the concept of binding, it’s crucial to understand the role of memory in a computer program. Memory is where data and instructions are stored and accessed during program execution. In most systems, memory is divided into several segments, including:
- Stack Segment: Used for static memory allocation. Function parameters, local variables, and return addresses are stored here.
- Heap Segment: Used for dynamic memory allocation. Objects and data structures that need to persist beyond the scope of a function are stored here.
- Data Segment: Divided into initialized and uninitialized segments. Global and static variables are stored here.
When we compile a program, variables and functions are assigned specific memory addresses. These addresses are then used for referencing the variables and functions throughout the program, a process known as binding.
How Memory Is Utilized in Binding
Binding associates variables and functions with their respective memory addresses. Understanding how this works can give us insights into the efficiency and flexibility of our code. Let’s look at how this memory usage unfolds during both early binding and late binding.
Early Binding (Static Binding)
Early binding occurs when the compiler resolves the function calls and variable references at compile-time. This means that the memory addresses are known before the program runs, making the function calls direct and efficient.
Example of Early Binding:
#include <iostream>
class Dog {
public:
void bark() {
std::cout << "Woof!" << std::endl;
}
};
int main() {
Dog myDog;
myDog.bark(); // Early binding
return 0;
}Here, the compiler knows the exact address of the bark function during compilation. Let’s say the memory address for bark is 0x00401000. The compiler replaces the bark function call with a machine instruction that directly jumps to this address.
Memory Address | Content
-------------------|-------------------
0x00401000 | Function bark() {
| std::cout << "Woof!" << std::endl;
| }
0x002fffd4 | myDog instanceWhen myDog.bark() is called, the execution jumps to 0x00401000 where the bark function is located.
Late Binding (Dynamic Binding)
Late binding, or dynamic binding, occurs when the function calls are not resolved until runtime. This is typical with virtual functions and function pointers, allowing more flexibility but requiring additional memory lookups.
Get Aarthi_Honguthi’s stories in your inbox
Join Medium for free to get updates from this writer.
Example of Late Binding with Function Pointers:
#include <iostream>
void greet() {
std::cout << "Hello, World!" << std::endl;
}
void farewell() {
std::cout << "Goodbye, World!" << std::endl;
}
int main() {
void (*message)();
message = greet; // Assigning function to pointer
message(); // Late binding
message = farewell; // Reassigning function to pointer
message(); // Late binding
return 0;
}In this case, the function pointer message stores the address of the function to call. Suppose greet is at 0x00402000 and farewell is at 0x00403000. The exact function to call is determined at runtime, making the binding dynamic.
Memory Address | Content
-------------------|-------------------
0x00402000 | Function greet() {
| std::cout << "Hello, World!" << std::endl;
| }
0x00403000 | Function farewell() {
| std::cout << "Goodbye, World!" << std::endl;
| }
0x002fff80 | Function pointer messageWhen message = greet; is executed, message points to 0x00402000. When message() is called, the execution jumps to 0x00402000. The same happens when message is reassigned to farewell.
Example of Late Binding with Virtual Functions:
#include <iostream>
class Animal {
public:
virtual void sound() {
std::cout << "Some generic animal sound" << std::endl;
}
};
class Dog : public Animal {
public:
void sound() override {
std::cout << "Woof!" << std::endl;
}
};
class Cat : public Animal {
public:
void sound() override {
std::cout << "Meow!" << std::endl;
}
};
int main() {
Animal* myAnimal;
Dog myDog;
Cat myCat;
myAnimal = &myDog;
myAnimal->sound(); // Late binding
myAnimal = &myCat;
myAnimal->sound(); // Late binding
return 0;
}With virtual functions, the exact function to be called is determined by the object’s actual type at runtime. This involves using a vtable (virtual table) that holds pointers to the virtual functions.
Memory Address | Content
-------------------|-------------------
0x00404000 | Animal::sound() {
| std::cout << "Some generic animal sound" << std::endl;
| }
0x00405000 | Dog::sound() {
| std::cout << "Woof!" << std::endl;
| }
0x00406000 | Cat::sound() {
| std::cout << "Meow!" << std::endl;
| }
0x002fffa0 | vtable for Animal
0x002fffb0 | vtable for Dog
0x002fffc0 | vtable for CatWhen myAnimal->sound() is called, the program looks up the function pointer in the vtable of the object type (Dog or Cat), and then jumps to the corresponding function address.
Function Overloading and Overriding
Function overloading and function overriding are two concepts that relate closely to binding, particularly in object-oriented programming.
Function Overloading
Function overloading allows multiple functions to have the same name but differ in parameters (type, number, or both). The correct function to call is determined by the argument list at compile-time, making it an example of early binding.
Example of Function Overloading:
#include <iostream>
class Printer {
public:
void print(int i) {
std::cout << "Printing int: " << i << std::endl;
}
void print(double f) {
std::cout << "Printing float: " << f << std::endl;
}
void print(const std::string &s) {
std::cout << "Printing string: " << s << std::endl;
}
};
int main() {
Printer printer;
printer.print(5); // Calls print(int)
printer.print(5.5); // Calls print(double)
printer.print("Hello"); // Calls print(const std::string&)
return 0;
}In this example, the print function is overloaded with different parameter types. The compiler resolves the correct function to call based on the argument types at compile-time.
Function Overriding
Function overriding occurs when a derived class provides a specific implementation for a function already defined in its base class. This requires the function to be marked as virtual in the base class, enabling late binding.
Example of Function Overriding:
#include <iostream>
class Animal {
public:
virtual void sound() {
std::cout << "Some generic animal sound" << std::endl;
}
};
class Dog : public Animal {
public:
void sound() override {
std::cout << "Woof!" << std::endl;
}
};
class Cat : public Animal {
public:
void sound() override {
std::cout << "Meow!" << std::endl;
}
};
int main() {
Animal* myAnimal;
Dog myDog;
Cat myCat;
myAnimal = &myDog;
myAnimal->sound(); // Calls Dog::sound() at runtime
myAnimal = &myCat;
myAnimal->sound(); // Calls Cat::sound() at runtime
return 0;
}In this case, the sound function is overridden in both Dog and Cat classes. The correct function to call is determined at runtime based on the actual object type, demonstrating late binding.









