Coming Soon
This lesson is currently being developed
Member selection with pointers and references
Access struct members through pointers and references.
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
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 = ▭ // 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
-
Use the dot operator (.) with struct objects and references.
-
Use the arrow operator (->) with pointers to structs - it's equivalent to
(*ptr).member
. -
Always check for null pointers before dereferencing them.
-
References use the dot operator just like regular objects.
-
Const correctness applies to both pointers and the data they point to.
-
Prefer references over pointers when null values aren't needed.
-
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
- What's the difference between using the dot operator and arrow operator for member access?
- How do you access a struct member through a pointer using the dereference operator?
- Why should you always check for null pointers before accessing members?
- How do you access struct members when using references?
- What's the relationship between
ptr->member
and(*ptr).member
?
Practice exercises
Try these exercises with pointer and reference member access:
- Create a function that takes a pointer to a
Rectangle
struct and calculates its area, with proper null pointer checking. - Create a simple linked list of
Student
structs using pointers, and write functions to add students and print the list. - Write functions that take const references to structs and demonstrate that you can read but not modify the data.
- Create a program with nested structs where some members are pointers, and practice accessing deeply nested members.
Explore More Courses
Discover other available courses while this lesson is being prepared.
Browse CoursesLesson Discussion
Share your thoughts and questions