OO Encapsulation  «Prev  Next»
Lesson 8 Using a member function
Objective Add a member function to the ch_stack struct.

Using C++ Member Functions (ch_stack and Rectangle Examples Explained)

Member functions are written just like any other functions. One difference, though, is they can use the data member names as is. Thus, the member functions in 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.

Unqualified Data Member Access


How Member Functions Access Data Members

Inside a member function, data members are accessed by name without any object prefix. In the 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.

The this Pointer — Implicit Qualification

The mechanism that makes unqualified data member access work is the 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.


Why This Makes Member Functions More Natural

The practical benefit of unqualified data member access is readability. Compare the two implementations of 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.

Invoking Member Functions on Objects

The Dot Operator — Object or Reference

When invoked on a particular object of type 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
}


The dot operator (.) 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.

The Arrow Operator — Pointer to Object

If a pointer to 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')

This invokes the member function 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.

Dot vs Arrow — Complete Picture

The choice between dot and arrow depends on how the object is held. Use . 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.


The ch_stack Member Invocation Example

Two Objects, Independent State

The data and operands objects in the example each receive their own copy of top and s[] when instantiated. Calling data.reset() affects only data.topoperands.top is untouched. Calling data.push('X') stores a character in data.s[] and increments data.topoperands 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.

The top_of() Naming Conflict — Resolved

One last observation: the member function 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.

Implicit Inline — Functions Defined in the struct Body

What Implicit Inline Means

Member functions that are defined within the 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.

The Short/Heavily-Used Rule

As a rule, only short, heavily used member functions should be defined within the 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.

External Member Functions — Preview of Module 3

Other member functions should be defined external to the 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.

Default Private Access — The class Keyword

Members of a Class

By default, all members of a class declared with the 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
}


This class contains four members: two data members of type 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.

Class Name vs Object Name

Notice the difference between the class name and the object name. In the previous example, 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.

Accessing Public Members — The Dot Notation

After the declarations of 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.

Multiple Objects — Independent State in Practice

Creating multiple 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
}


At the level of programming, a class is a data structure with data members for representing the various properties of the different object instances of the class, and member functions for representing the behavior of the class. The three 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.


C++ Member Function - Exercise

At the level of programming, a class is a data structure with
  1. data members for representing the various properties of the different object instances of the class, and
  2. with member functions for representing the behavior of the class.

Click the Exercise link to add a member function to the ch_stack struct.
C++ Member Function - Exercise

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.


SEMrush Software