Coming Soon

This lesson is currently being developed

Member selection with pointers and references

Access struct members through pointers and references.

Compound Types: Enums and Structs
Chapter
Beginner
Difficulty
50min
Estimated Time

What to Expect

Comprehensive explanations with practical examples

Interactive coding exercises to practice concepts

Knowledge quiz to test your understanding

Step-by-step guidance for beginners

Development Status

In Progress

Content is being carefully crafted to provide the best learning experience

Preview

Early Preview Content

This content is still being developed and may change before publication.

13.12 — Member selection with pointers and references

In this lesson, you'll learn how to access struct members when working with pointers and references to structs, including the arrow operator and best practices for pointer-based member access.

Review: Member selection with the dot operator

So far, you've learned to access struct members using the dot operator (.) with struct objects:

#include <iostream>

struct Point
{
    int x{0};
    int y{0};
};

int main()
{
    Point p{3, 5};
    
    std::cout << "Point: (" << p.x << ", " << p.y << ")" << std::endl;  // Using dot operator
    
    return 0;
}

Output:

Point: (3, 5)

But what happens when you have a pointer to a struct or work with references?

Member selection with pointers using dereference and dot

When you have a pointer to a struct, you need to dereference it first, then use the dot operator:

#include <iostream>

struct Rectangle
{
    double width{0.0};
    double height{0.0};
};

int main()
{
    Rectangle rect{5.0, 3.0};
    Rectangle* pRect = &rect;  // Pointer to the rectangle
    
    // Method 1: Dereference the pointer, then use dot operator
    std::cout << "Width: " << (*pRect).width << std::endl;
    std::cout << "Height: " << (*pRect).height << std::endl;
    
    // Calculate area using pointer
    double area = (*pRect).width * (*pRect).height;
    std::cout << "Area: " << area << std::endl;
    
    return 0;
}

Output:

Width: 5
Height: 3
Area: 15

The parentheses around *pRect are necessary because the dot operator has higher precedence than the dereference operator.

The arrow operator (->) - A more convenient syntax

C++ provides the arrow operator (->) as a convenient shorthand for dereferencing a pointer and accessing a member:

#include <iostream>
#include <string>

struct Employee
{
    int id{0};
    std::string name{"Unknown"};
    double salary{0.0};
};

int main()
{
    Employee emp{1001, "Alice Johnson", 75000.0};
    Employee* pEmp = &emp;
    
    // These are equivalent:
    std::cout << "Using (*ptr).member syntax:" << std::endl;
    std::cout << "ID: " << (*pEmp).id << std::endl;
    std::cout << "Name: " << (*pEmp).name << std::endl;
    std::cout << "Salary: $" << (*pEmp).salary << std::endl;
    
    std::cout << "\nUsing ptr->member syntax:" << std::endl;
    std::cout << "ID: " << pEmp->id << std::endl;
    std::cout << "Name: " << pEmp->name << std::endl;
    std::cout << "Salary: $" << pEmp->salary << std::endl;
    
    return 0;
}

Output:

Using (*ptr).member syntax:
ID: 1001
Name: Alice Johnson
Salary: $75000

Using ptr->member syntax:
ID: 1001
Name: Alice Johnson
Salary: $75000

The arrow operator is much more readable and is the preferred way to access members through pointers.

Practical example: Dynamic memory allocation

The arrow operator is especially useful when working with dynamically allocated structs:

#include <iostream>
#include <string>

struct Student
{
    int id{0};
    std::string name{"Unknown"};
    double gpa{0.0};
};

void printStudent(const Student* student)
{
    if (student != nullptr)  // Always check for null pointers!
    {
        std::cout << "Student #" << student->id << ": " << student->name 
                  << " (GPA: " << student->gpa << ")" << std::endl;
    }
    else
    {
        std::cout << "Invalid student pointer" << std::endl;
    }
}

void updateGPA(Student* student, double newGPA)
{
    if (student != nullptr && newGPA >= 0.0 && newGPA <= 4.0)
    {
        student->gpa = newGPA;
        std::cout << "Updated " << student->name << "'s GPA to " << newGPA << std::endl;
    }
}

int main()
{
    // Dynamically allocate a Student
    Student* pStudent = new Student{1001, "Bob Smith", 3.2};
    
    printStudent(pStudent);
    updateGPA(pStudent, 3.5);
    printStudent(pStudent);
    
    // Don't forget to free the memory
    delete pStudent;
    pStudent = nullptr;  // Good practice: set to nullptr after delete
    
    printStudent(pStudent);  // This will print "Invalid student pointer"
    
    return 0;
}

Output:

Student #1001: Bob Smith (GPA: 3.2)
Updated Bob Smith's GPA to 3.5
Student #1001: Bob Smith (GPA: 3.5)
Invalid student pointer

Member selection with references

References act like aliases to objects, so you use the dot operator with references just like with regular objects:

#include <iostream>
#include <string>

struct Book
{
    std::string title{"Unknown"};
    std::string author{"Unknown"};
    int pages{0};
    bool isAvailable{true};
};

void borrowBook(Book& book)  // Reference parameter
{
    if (book.isAvailable)
    {
        book.isAvailable = false;
        std::cout << "Borrowed: \"" << book.title << "\" by " << book.author << std::endl;
    }
    else
    {
        std::cout << "Book is not available: \"" << book.title << "\"" << std::endl;
    }
}

void returnBook(Book& book)  // Reference parameter
{
    book.isAvailable = true;
    std::cout << "Returned: \"" << book.title << "\" by " << book.author << std::endl;
}

void printBookStatus(const Book& book)  // Const reference parameter
{
    std::cout << "\"" << book.title << "\" by " << book.author 
              << " (" << book.pages << " pages) - " 
              << (book.isAvailable ? "Available" : "Checked out") << std::endl;
}

int main()
{
    Book novel{"1984", "George Orwell", 328, true};
    
    printBookStatus(novel);
    borrowBook(novel);
    printBookStatus(novel);
    borrowBook(novel);  // Try to borrow again
    returnBook(novel);
    printBookStatus(novel);
    
    return 0;
}

Output:

"1984" by George Orwell (328 pages) - Available
Borrowed: "1984" by George Orwell
"1984" by George Orwell (328 pages) - Checked out
Book is not available: "1984"
Returned: "1984" by George Orwell
"1984" by George Orwell (328 pages) - Available

Working with nested structs and pointers

When dealing with nested structs through pointers, you can chain the member access operators:

#include <iostream>
#include <string>

struct Address
{
    std::string street{"Unknown"};
    std::string city{"Unknown"};
    std::string state{"Unknown"};
    int zipCode{0};
};

struct Person
{
    std::string name{"Unknown"};
    int age{0};
    Address* homeAddress{nullptr};  // Pointer to Address
};

void printPersonInfo(const Person* person)
{
    if (person == nullptr)
    {
        std::cout << "Invalid person pointer" << std::endl;
        return;
    }
    
    std::cout << "Name: " << person->name << std::endl;
    std::cout << "Age: " << person->age << std::endl;
    
    if (person->homeAddress != nullptr)
    {
        std::cout << "Address: " << person->homeAddress->street << std::endl;
        std::cout << "         " << person->homeAddress->city << ", " 
                  << person->homeAddress->state << " " 
                  << person->homeAddress->zipCode << std::endl;
    }
    else
    {
        std::cout << "No address on file" << std::endl;
    }
}

int main()
{
    Address address{"123 Oak Street", "Springfield", "IL", 62701};
    Person person{"Alice Johnson", 25, &address};
    
    printPersonInfo(&person);
    
    // Example with no address
    Person person2{"Bob Smith", 30, nullptr};
    std::cout << std::endl;
    printPersonInfo(&person2);
    
    return 0;
}

Output:

Name: Alice Johnson
Age: 25
Address: 123 Oak Street
         Springfield, IL 62701

Name: Bob Smith
Age: 30
No address on file

Arrays of struct pointers

You can create arrays of pointers to structs, which is useful for collections where objects might be optional or dynamically allocated:

#include <iostream>
#include <string>

struct Car
{
    std::string make{"Unknown"};
    std::string model{"Unknown"};
    int year{0};
    double price{0.0};
};

void printInventory(Car* cars[], int size)
{
    std::cout << "=== Car Inventory ===" << std::endl;
    for (int i = 0; i < size; ++i)
    {
        if (cars[i] != nullptr)
        {
            std::cout << i + 1 << ". " << cars[i]->year << " " 
                      << cars[i]->make << " " << cars[i]->model 
                      << " - $" << cars[i]->price << std::endl;
        }
        else
        {
            std::cout << i + 1 << ". [Empty slot]" << std::endl;
        }
    }
}

int main()
{
    // Create some cars
    Car car1{"Toyota", "Camry", 2022, 28500.0};
    Car car2{"Honda", "Civic", 2023, 25200.0};
    Car car3{"Ford", "Mustang", 2022, 35000.0};
    
    // Array of pointers to cars
    Car* inventory[5] = {
        &car1,      // Point to car1
        &car2,      // Point to car2
        nullptr,    // Empty slot
        &car3,      // Point to car3
        nullptr     // Empty slot
    };
    
    printInventory(inventory, 5);
    
    return 0;
}

Output:

=== Car Inventory ===
1. 2022 Toyota Camry - $28500
2. 2023 Honda Civic - $25200
3. [Empty slot]
4. 2022 Ford Mustang - $35000
5. [Empty slot]

Common patterns and best practices

1. Always check for null pointers

void processEmployee(Employee* emp)
{
    if (emp == nullptr)  // Always check!
    {
        std::cout << "Error: null employee pointer" << std::endl;
        return;
    }
    
    // Safe to use emp->member now
    std::cout << "Processing employee: " << emp->name << std::endl;
}

2. Use const correctness with pointers

void displayInfo(const Employee* emp)  // Const pointer to const data
{
    if (emp != nullptr)
    {
        std::cout << "Employee: " << emp->name << std::endl;
        // emp->name = "New Name";  // ERROR: can't modify through const pointer
    }
}

3. Prefer references over pointers when possible

// Better: use reference (no null checking needed)
void updateSalary(Employee& emp, double newSalary)
{
    emp.salary = newSalary;  // Use dot operator with references
}

// Less preferred: use pointer (requires null checking)
void updateSalary(Employee* emp, double newSalary)
{
    if (emp != nullptr)  // Need to check for null
    {
        emp->salary = newSalary;  // Use arrow operator with pointers
    }
}

4. Set pointers to nullptr after delete

Employee* emp = new Employee{1001, "Alice", 50000.0};

// Use the pointer...
std::cout << emp->name << std::endl;

// Clean up
delete emp;
emp = nullptr;  // Important: prevent accidental reuse

// Safe: this won't crash
if (emp != nullptr)
{
    std::cout << emp->name << std::endl;  // Won't execute
}

Member access operator precedence

Understanding operator precedence is important when combining member access with other operations:

#include <iostream>

struct Counter
{
    int value{0};
};

int main()
{
    Counter counter{5};
    Counter* pCounter = &counter;
    
    // These are different due to operator precedence:
    std::cout << "counter.value: " << counter.value << std::endl;          // 5
    std::cout << "pCounter->value: " << pCounter->value << std::endl;      // 5
    std::cout << "(*pCounter).value: " << (*pCounter).value << std::endl; // 5
    
    // Increment operations:
    counter.value++;           // Increment value
    pCounter->value++;         // Increment value through pointer
    ++pCounter->value;         // Pre-increment value through pointer
    
    std::cout << "After increments: " << counter.value << std::endl;  // 8
    
    return 0;
}

Output:

counter.value: 5
pCounter->value: 5
(*pCounter).value: 5
After increments: 8

Real-world example: Linked list node

Here's a practical example showing how pointers and member access work together:

#include <iostream>
#include <string>

struct ListNode
{
    std::string data{"Empty"};
    ListNode* next{nullptr};
};

void printList(const ListNode* head)
{
    const ListNode* current = head;
    
    while (current != nullptr)
    {
        std::cout << current->data;
        current = current->next;  // Move to next node
        
        if (current != nullptr)
        {
            std::cout << " -> ";
        }
    }
    std::cout << std::endl;
}

int main()
{
    // Create nodes
    ListNode node1{"First"};
    ListNode node2{"Second"};
    ListNode node3{"Third"};
    
    // Link them together
    node1.next = &node2;
    node2.next = &node3;
    // node3.next is already nullptr
    
    // Print the linked list
    std::cout << "Linked list: ";
    printList(&node1);
    
    return 0;
}

Output:

Linked list: First -> Second -> Third

Key concepts to remember

  1. Use the dot operator (.) with struct objects and references.

  2. Use the arrow operator (->) with pointers to structs - it's equivalent to (*ptr).member.

  3. Always check for null pointers before dereferencing them.

  4. References use the dot operator just like regular objects.

  5. Const correctness applies to both pointers and the data they point to.

  6. Prefer references over pointers when null values aren't needed.

  7. Set pointers to nullptr after delete to prevent accidental reuse.

Summary

Member selection with pointers and references is essential for working with struct data in various contexts. The arrow operator provides a clean, readable way to access members through pointers, while references allow natural member access using the dot operator. Understanding these concepts is crucial for dynamic memory management, function parameters, and advanced data structures like linked lists. Always remember to check for null pointers and follow const-correctness principles to write safe, maintainable code.

Quiz

  1. What's the difference between using the dot operator and arrow operator for member access?
  2. How do you access a struct member through a pointer using the dereference operator?
  3. Why should you always check for null pointers before accessing members?
  4. How do you access struct members when using references?
  5. What's the relationship between ptr->member and (*ptr).member?

Practice exercises

Try these exercises with pointer and reference member access:

  1. Create a function that takes a pointer to a Rectangle struct and calculates its area, with proper null pointer checking.
  2. Create a simple linked list of Student structs using pointers, and write functions to add students and print the list.
  3. Write functions that take const references to structs and demonstrate that you can read but not modify the data.
  4. Create a program with nested structs where some members are pointers, and practice accessing deeply nested members.

Continue Learning

Explore other available lessons while this one is being prepared.

View Course

Explore More Courses

Discover other available courses while this lesson is being prepared.

Browse Courses

Lesson Discussion

Share your thoughts and questions

💬

No comments yet. Be the first to share your thoughts!

Sign in to join the discussion