| Lesson 8 | Using a member function |
| Objective | Add a member function to the ch_stack struct. |
ch_stack use top and s in an unqualified manner. When invoked on a particular object of type ch_stack, the member functions act on a specified member of that object. Member functions that are defined within the struct are implicitly inline. As a rule, only short, heavily used member functions should be defined with the struct. Other member functions should be defined external to the struct. We will look at defining external member functions in the next module. Lesson 7 showed how to declare member functions inside a struct. This lesson examines what happens when those functions are called — how data members are accessed without qualification, how the dot and arrow operators invoke member functions on objects, and how the Rectangle class illustrates default private access in a class definition.
ch_stack struct, the push() function writes directly to s[top] and increments top — not stack.s[top] and stack.top. This unqualified access is what distinguishes member functions from free functions: a free function that operates on a ch_stack must receive the struct as an explicit parameter and qualify every data member access through that parameter. A member function receives the object implicitly and accesses its members directly.
this pointer — a hidden parameter that the compiler passes to every non-static member function. this is a pointer to the specific object on which the function was called. When push() accesses top, the compiler silently reads it as this->top. When it writes to s[top], the compiler reads it as this->s[this->top]. The programmer writes unqualified names; the compiler inserts the qualification. This is why the same member function code correctly operates on any number of different objects — this changes with each call to point to the specific object being operated on.
push():
// C-style free function — explicit parameter, qualified access
void push(ch_stack* st, char c) {
assert(st->top != FULL);
st->s[++st->top] = c;
}
// C++ member function — implicit this, unqualified access
void ch_stack::push(char c) {
assert(top != FULL);
s[++top] = c;
}
The member function version is shorter, reads more naturally, and makes the relationship between the operation and the data it operates on explicit in the code structure rather than in a naming convention.
ch_stack, the member functions act on the specified member in that object. The following example illustrates these ideas. If two ch_stack variables are declared and their reset() member functions called:
#include <cassert>
static constexpr int MAX_LEN = 40;
struct ch_stack {
char s[MAX_LEN];
int top = -1;
void reset() { top = -1; }
void push(char c) { assert(top != MAX_LEN - 1); s[++top] = c; }
char pop() { assert(top != -1); return s[top--]; }
char top_of() const { assert(top != -1); return s[top]; }
bool empty() const { return top == -1; }
bool full() const { return top == MAX_LEN - 1; }
};
int main() {
ch_stack data, operands;
data.reset(); // sets data.top to -1
operands.reset(); // sets operands.top to -1 independently
data.push('X'); // data.top becomes 0, data.s[0] = 'X'
data.push('Y'); // data.top becomes 1, data.s[1] = 'Y'
// operands is still empty — independent state
}
.) accesses a member of an object or a reference to an object. data.reset() calls reset() with this pointing to data — so top inside reset() refers to data.top. operands.reset() calls the same function with this pointing to operands — so the same top inside reset() now refers to operands.top. The two objects are completely independent despite sharing the same member function code.
ch_stack is used instead of a direct object, the arrow operator (->) invokes the member function:
ch_stack* ptr_operands = &operands;
ptr_operands->push('A'); // equivalent to: (*ptr_operands).push('A')
push(), which has the effect of incrementing operands.top and setting operands.s[top] to 'A'. The arrow operator is shorthand for dereferencing the pointer and applying the dot operator: ptr->member is exactly equivalent to (*ptr).member. Both expressions pass the same this pointer to the member function — the address of the object being pointed to.
. when you have an object or a reference; use -> when you have a pointer. In modern C++, references are strongly preferred over raw pointers for member access — a reference cannot be null and cannot be accidentally left uninitialized, eliminating an entire class of runtime errors. The arrow operator remains important to understand because it appears throughout legacy code, in smart pointer usage (unique_ptr->push('A') calls push() on the managed object), and when working with low-level system APIs.
data and operands objects in the example each receive their own copy of top and s[] when instantiated. Calling data.reset() affects only data.top — operands.top is untouched. Calling data.push('X') stores a character in data.s[] and increments data.top — operands remains empty. This independence is the direct consequence of the object model established in lesson 6: each object receives its own copy of all non-static data members.
top_of() had its name changed from the previous implementation because of a naming conflict. In an earlier C-style version of the stack, there was a data member named top and a function also named top. C++ does not allow a member function and a data member to share the same name in the same scope — the names would be ambiguous, and the compiler cannot determine from context whether top refers to the integer or the function. Renaming the function top_of() resolves the conflict immediately. This is a common issue when converting C structs to C++ classes: function names that mirror data member names must be made distinct.
struct are implicitly inline. The inline keyword is a hint to the compiler suggesting that calls to the function be expanded directly at the call site rather than generating a function call instruction. For a function like empty() — which consists of a single comparison — the overhead of a function call (stack frame setup, parameter passing, return) can exceed the cost of the comparison itself. Inline expansion eliminates this overhead. The compiler is not required to honor the inline hint; it may choose not to inline a function if the body is too large or if the optimization settings do not support it.
struct. The tradeoff is binary size vs call overhead: every call site where an inline function is expanded receives its own copy of the function body. For a large function called in many places, this significantly increases binary size without proportionate performance benefit. For a small function like empty() or full() — a single comparison — the binary size impact is negligible and the call overhead savings are meaningful, especially in tight loops common in stack-intensive algorithms.
struct. We will look at defining external member functions in the next module. An external definition uses the scope resolution operator :: to identify which struct or class the function belongs to — void ch_stack::push(char c) { ... } defines push() as a member of ch_stack outside the struct body. External definitions allow larger, more complex functions to be placed in separate source files, keeping the struct declaration concise and readable.
class keyword have private access. Therefore, any member declared before the first access specifier automatically has private access. The following Rectangle class demonstrates this — x and y are private because they appear before the public: specifier:
class Rectangle {
int x_, y_; // private by default — no specifier needed
public:
void set_values(int w, int h); // declaration only
int area() const; // declaration only
}; // note: modern practice — no object after }
// Out-of-line definitions using the scope resolution operator ::
void Rectangle::set_values(int w, int h) { x_ = w; y_ = h; }
int Rectangle::area() const { return x_ * y_; }
int main() {
Rectangle rect;
rect.set_values(3, 4);
int myarea = rect.area(); // myarea = 12
// rect.x_ = 5; // compile error: x_ is private
}
int (member x_, member y_) with private access (because private is the default access level for class), and two member functions with public access: set_values() and area(). The declarations appear inside the class body; the definitions appear outside using :: — the pattern that module 3 covers in full.
Rectangle is the class name (the type), whereas rect is an object of type Rectangle. It is the same relationship int and a have in the following declaration:
int a;
where int is the type name (the class) and a is the variable name (the object). The class definition specifies the structure and behavior; the object declaration creates storage with that structure and behavior attached.
Rectangle and rect, we can refer within the body of the program to any of the public members of the object rect as if they were normal functions or normal variables, by putting the object's name followed by a dot and then the name of the member — all very similar to what we did with plain data structures before:
rect.set_values(3, 4);
int myarea = rect.area();
The dot operator works identically for structs and classes — the only difference is what is accessible. For a struct with all-public members, every member is reachable through the dot. For a class with private data, only the public member functions are reachable through the dot from outside the class.
Rectangle objects demonstrates that each has independent state — the same principle established with ch_stack data, operands:
int main() {
Rectangle r1, r2, r3;
r1.set_values(3, 4); // r1: 3×4
r2.set_values(5, 6); // r2: 5×6
r3.set_values(10, 2); // r3: 10×2
// Each object has its own x_ and y_
// All three share the same set_values() and area() code
int total = r1.area() + r2.area() + r3.area(); // 12 + 30 + 20 = 62
}
Rectangle objects each hold their own x_ and y_ values — calling r1.set_values(3,4) has no effect on r2.x_ or r3.y_. The single copy of area() in the binary serves all three objects, with this pointing to the correct object at each call.
ch_stack struct.
In the next lesson, you will learn about private and public access specifiers — how adding private: and public: to the ch_stack struct transforms it from a transparent data structure into a fully encapsulated ADT.