Grouping data: records and objects

Beginner50 min

Bundle related values into one named thing, attach behaviour to it, and tell apart two things that are equal from one thing with two names.

What you will learn
  • Model a real-world thing as a record with named fields
  • Add methods to turn a record into an object that carries its own behaviour
  • Explain encapsulation and why a type protects its own data
  • Distinguish identity (the same object) from equality (the same contents)
On this page

A library book has a title, an author, a year and a flag saying whether it is on the shelf. You could keep those in four parallel lists and hope the positions always line up, or in a map with string keys and hope nobody misspells one. Neither is a good fit. What you want is a single value that groups the pieces, gives each a name, and has a recognisable shape. That is a record.

Records: named fields

A record (also called a struct, or a data class) is a value made of several fields, each with a name and often its own type. You make one by filling in all the fields, and you read or change a field by its name, usually with a dot: book.title.

Unlike a list, whose elements normally share one meaning (“a score, another score”), a record’s fields are different things that belong together: a title, a year, a flag. Unlike a map, a record’s fields are fixed in advance, so a typo in a field name is caught early instead of silently creating a new key.

Book
+-----------+-----------------+
| title | "Dune" |
| author | "Frank Herbert" |
| year | 1965 |
| available | true |
+-----------+-----------------+

From records to objects

A record only holds data. Sooner or later you write functions that work on that data: describe a book, lend it out. The step from record to object is to keep those functions with the data they operate on, as methods. You call a method on a particular object: book.lend(). Inside, the method has access to that object’s fields (Python calls it self, JavaScript and Java call it this; in C there are no methods, so you pass a pointer to the struct as the first parameter instead).

A class (or, in C, a struct type plus its functions) is the blueprint: it says which fields every book has and which methods it supports. Each book you create from it is an instance.

Python

book_demo.py
from dataclasses import dataclass
@dataclass
class Book:
title: str
author: str
year: int
available: bool = True
def describe(self):
status = "available" if self.available else "on loan"
return f"{self.title} by {self.author} ({self.year}) - {status}"
def lend(self):
self.available = False
dune = Book("Dune", "Frank Herbert", 1965)
print(dune.describe())
dune.lend()
print(dune.describe())

JavaScript

book_demo.js
class Book {
constructor(title, author, year) {
this.title = title;
this.author = author;
this.year = year;
this.available = true;
}
describe() {
const status = this.available ? "available" : "on loan";
return `${this.title} by ${this.author} (${this.year}) - ${status}`;
}
lend() {
this.available = false;
}
}
const dune = new Book("Dune", "Frank Herbert", 1965);
console.log(dune.describe());
dune.lend();
console.log(dune.describe());

Java

BookDemo.java
class Book {
String title;
String author;
int year;
boolean available = true;
Book(String title, String author, int year) {
this.title = title;
this.author = author;
this.year = year;
}
String describe() {
String status = available ? "available" : "on loan";
return title + " by " + author + " (" + year + ") - " + status;
}
void lend() {
available = false;
}
}
public class BookDemo {
public static void main(String[] args) {
Book dune = new Book("Dune", "Frank Herbert", 1965);
System.out.println(dune.describe());
dune.lend();
System.out.println(dune.describe());
}
}

C

book_demo.c
#include <stdio.h>
typedef struct {
char title[32];
char author[32];
int year;
int available;
} Book;
/* No methods in C: functions take a pointer to the struct instead. */
void book_describe(const Book *b) {
printf("%s by %s (%d) - %s\n", b->title, b->author, b->year,
b->available ? "available" : "on loan");
}
void book_lend(Book *b) {
b->available = 0;
}
int main(void) {
Book dune = {"Dune", "Frank Herbert", 1965, 1};
book_describe(&dune);
book_lend(&dune);
book_describe(&dune);
return 0;
}

All four print:

Dune by Frank Herbert (1965) - available
Dune by Frank Herbert (1965) - on loan

A few things to notice:

  • The Python version is a dataclass, a shortcut that writes the constructor for you from the field list. JavaScript and Java spell the constructor out.
  • Java and C declare the type of every field. Python’s annotations (title: str) document the type; JavaScript has none at all.
  • The C function receives a pointer (b->title is the field of the struct b points at). It needs a Book *, not a Book, so that book_lend changes the original instead of a copy. Pointers are covered in memory and references.

Encapsulation: protecting the data

If any code anywhere may set any field, then any code anywhere may break your rules. A library member, for instance, may borrow at most two books at once. If other code can simply append to the member’s list of books, that rule will be broken sooner or later.

Encapsulation means a type keeps its fields private and offers a small set of methods as the only way to change them. The methods enforce the rules. Callers then depend on what the object does, not on how it stores things, so you are free to change the inside later.

Languages support this differently. Java has private fields. JavaScript has #name private fields. Python has no enforcement, only a convention: a leading underscore (_borrowed) means “internal, please leave alone”. C can hide a struct’s layout by keeping its definition out of the header file. The idea is the same everywhere. In each program below, Ada borrows two books, is refused a third, returns one, and then borrows the third.

Python

encapsulation_demo.py
class Member:
MAX_BOOKS = 2
def __init__(self, name):
self.name = name
self._borrowed = [] # internal: use borrow() and give_back()
def borrow(self, title):
if len(self._borrowed) >= self.MAX_BOOKS:
raise ValueError(f"{self.name} already has {self.MAX_BOOKS} books")
self._borrowed.append(title)
def give_back(self, title):
self._borrowed.remove(title)
def books(self):
return list(self._borrowed) # a copy, so callers cannot change ours
ada = Member("Ada")
ada.borrow("Dune")
ada.borrow("Emma")
try:
ada.borrow("Ulysses")
except ValueError as err:
print("refused:", err)
ada.give_back("Dune")
ada.borrow("Ulysses")
print("books:", ", ".join(ada.books()))

JavaScript

encapsulation_demo.js
class Member {
static MAX_BOOKS = 2;
#borrowed = []; // private: only code inside the class can see it
constructor(name) {
this.name = name;
}
borrow(title) {
if (this.#borrowed.length >= Member.MAX_BOOKS) {
throw new Error(`${this.name} already has ${Member.MAX_BOOKS} books`);
}
this.#borrowed.push(title);
}
giveBack(title) {
this.#borrowed.splice(this.#borrowed.indexOf(title), 1);
}
books() {
return [...this.#borrowed]; // a copy, so callers cannot change ours
}
}
const ada = new Member("Ada");
ada.borrow("Dune");
ada.borrow("Emma");
try {
ada.borrow("Ulysses");
} catch (err) {
console.log("refused:", err.message);
}
ada.giveBack("Dune");
ada.borrow("Ulysses");
console.log("books:", ada.books().join(", "));

Java

EncapsulationDemo.java
import java.util.ArrayList;
import java.util.List;
class Member {
static final int MAX_BOOKS = 2;
private final String name;
private final List<String> borrowed = new ArrayList<>(); // private: other classes cannot touch it
Member(String name) {
this.name = name;
}
void borrow(String title) {
if (borrowed.size() >= MAX_BOOKS) {
throw new IllegalStateException(name + " already has " + MAX_BOOKS + " books");
}
borrowed.add(title);
}
void giveBack(String title) {
borrowed.remove(title);
}
List<String> books() {
return new ArrayList<>(borrowed); // a copy, so callers cannot change ours
}
}
public class EncapsulationDemo {
public static void main(String[] args) {
Member ada = new Member("Ada");
ada.borrow("Dune");
ada.borrow("Emma");
try {
ada.borrow("Ulysses");
} catch (IllegalStateException err) {
System.out.println("refused: " + err.getMessage());
}
ada.giveBack("Dune");
ada.borrow("Ulysses");
System.out.println("books: " + String.join(", ", ada.books()));
}
}

C

encapsulation_demo.c
#include <stdio.h>
#include <string.h>
#define MAX_BOOKS 2
/* In a real project this struct would be declared in a header as an incomplete
type, so other files could not see the fields at all. */
typedef struct {
const char *name;
const char *borrowed[MAX_BOOKS];
int count;
} Member;
/* Returns 0 on success, -1 when the member already has the maximum. */
static int member_borrow(Member *m, const char *title) {
if (m->count >= MAX_BOOKS) {
return -1;
}
m->borrowed[m->count++] = title;
return 0;
}
static void member_give_back(Member *m, const char *title) {
for (int i = 0; i < m->count; i++) {
if (strcmp(m->borrowed[i], title) == 0) {
m->borrowed[i] = m->borrowed[m->count - 1]; /* fill the gap with the last book */
m->count--;
return;
}
}
}
static void member_print_books(const Member *m) {
printf("books: ");
for (int i = 0; i < m->count; i++) {
printf("%s%s", i > 0 ? ", " : "", m->borrowed[i]);
}
printf("\n");
}
int main(void) {
Member ada = {"Ada", {NULL}, 0};
member_borrow(&ada, "Dune");
member_borrow(&ada, "Emma");
if (member_borrow(&ada, "Ulysses") != 0) {
printf("refused: %s already has %d books\n", ada.name, MAX_BOOKS);
}
member_give_back(&ada, "Dune");
member_borrow(&ada, "Ulysses");
member_print_books(&ada);
return 0;
}

All four print:

refused: Ada already has 2 books
books: Emma, Ulysses

The rule lives in one place, borrow. Returning a copy from books() stops callers from reaching in and changing the internal list.

Identity versus equality

Two questions that sound alike are different:

  • Equality: do these two things hold the same contents?
  • Identity: are these two names for the very same object in memory?

Two copies of the same paperback are equal in contents, but they are two separate objects. Lend one and the other stays on the shelf. If instead two variables name one object, lending through either changes what both see.

a ---> [ "Dune", 1965 ] b ---> [ "Dune", 1965 ] a and b: equal, not identical
c ---> (same object as a) a and c: identical

Python

identity_demo.py
from dataclasses import dataclass
@dataclass
class Book:
title: str
year: int
a = Book("Dune", 1965)
b = Book("Dune", 1965)
c = a
print("equal contents:", "yes" if a == b else "no")
print("same object (a, b):", "yes" if a is b else "no")
print("same object (a, c):", "yes" if a is c else "no")
c.year = 1966
print("a.year after changing c:", a.year)
print("b.year:", b.year)

JavaScript

identity_demo.js
class Book {
constructor(title, year) {
this.title = title;
this.year = year;
}
// JavaScript does not compare contents for us, so we say what "equal" means.
equals(other) {
return this.title === other.title && this.year === other.year;
}
}
const a = new Book("Dune", 1965);
const b = new Book("Dune", 1965);
const c = a;
console.log("equal contents:", a.equals(b) ? "yes" : "no");
console.log("same object (a, b):", a === b ? "yes" : "no");
console.log("same object (a, c):", a === c ? "yes" : "no");
c.year = 1966;
console.log("a.year after changing c:", a.year);
console.log("b.year:", b.year);

Java

IdentityDemo.java
class Book {
String title;
int year;
Book(String title, int year) {
this.title = title;
this.year = year;
}
// Without this, equals() would fall back to identity.
@Override
public boolean equals(Object o) {
return o instanceof Book other && title.equals(other.title) && year == other.year;
}
@Override
public int hashCode() {
return java.util.Objects.hash(title, year);
}
}
public class IdentityDemo {
public static void main(String[] args) {
Book a = new Book("Dune", 1965);
Book b = new Book("Dune", 1965);
Book c = a;
System.out.println("equal contents: " + (a.equals(b) ? "yes" : "no"));
System.out.println("same object (a, b): " + (a == b ? "yes" : "no"));
System.out.println("same object (a, c): " + (a == c ? "yes" : "no"));
c.year = 1966;
System.out.println("a.year after changing c: " + a.year);
System.out.println("b.year: " + b.year);
}
}

C

identity_demo.c
#include <stdio.h>
#include <string.h>
typedef struct {
const char *title;
int year;
} Book;
/* C has no built-in == for structs, so equality is a function we write. */
static int book_equals(const Book *x, const Book *y) {
return strcmp(x->title, y->title) == 0 && x->year == y->year;
}
int main(void) {
Book a = {"Dune", 1965};
Book b = {"Dune", 1965};
Book *c = &a; /* a second name for a: a pointer to it */
printf("equal contents: %s\n", book_equals(&a, &b) ? "yes" : "no");
printf("same object (a, b): %s\n", &a == &b ? "yes" : "no");
printf("same object (a, c): %s\n", &a == c ? "yes" : "no");
c->year = 1966;
printf("a.year after changing c: %d\n", a.year);
printf("b.year: %d\n", b.year);
return 0;
}

All four print:

equal contents: yes
same object (a, b): no
same object (a, c): yes
a.year after changing c: 1966
b.year: 1965

c = a did not make a new book. It gave the same book a second name, so changing the year through c is visible through a, while b, a separate object, is untouched. Python’s is, JavaScript’s === on objects and Java’s == on objects all test identity; in C you compare addresses. For equality, Python’s dataclass writes == for you, while JavaScript, Java and C need a method or function you define yourself (and Java’s hashCode must agree with equals, so maps and sets that use it keep working).

C structs are plain values, copied byte by byte on assignment, so Book c = a; would make a true copy there. That is why the C program makes c a pointer when it wants a second name for the same book.

Common pitfalls

  • Parallel lists (titles[i], authors[i]) that drift out of step. Use one record per item and a single list of records.
  • Public fields everywhere, so any code can put an object into an invalid state. Funnel changes through methods.
  • Confusing == with identity. In Java, a == b on two objects asks “same object?”, not “same contents?”. Use equals.
  • Aliasing surprises. After c = a, a change through c shows up in a. Copy deliberately when you need independence.
  • Putting everything in one giant object that does ten unrelated jobs. A good type has one clear purpose.
ExerciseA library book and a member

Model a Book (title, author, available flag) and a Member (name, list of borrowed books). A member borrows a book with member.borrow(book). It must fail with a clear error if the book is already on loan or the member already holds two books. Returning a book makes it available again. Demonstrate each case with this script: Ada borrows Dune and Emma; Ada then tries Ulysses; Grace tries Dune; Ada returns Dune; Grace borrows Dune. Expected output:

refused: Ada already has 2 books
refused: 'Dune' is already on loan
Ada: Emma
Grace: Dune
Show solution

The member checks both rules before changing anything, so a refused request leaves everything as it was.

Python

library.py
class Book:
def __init__(self, title, author):
self.title = title
self.author = author
self.available = True
class Member:
MAX_BOOKS = 2
def __init__(self, name):
self.name = name
self._borrowed = []
def borrow(self, book):
if not book.available:
raise ValueError(f"{book.title!r} is already on loan")
if len(self._borrowed) >= self.MAX_BOOKS:
raise ValueError(f"{self.name} already has {self.MAX_BOOKS} books")
book.available = False
self._borrowed.append(book)
def give_back(self, book):
self._borrowed.remove(book)
book.available = True
def titles(self):
names = []
for book in self._borrowed:
names.append(book.title)
return ", ".join(names)
dune = Book("Dune", "Frank Herbert")
emma = Book("Emma", "Jane Austen")
ulysses = Book("Ulysses", "James Joyce")
ada = Member("Ada")
grace = Member("Grace")
ada.borrow(dune)
ada.borrow(emma)
for member, book in [(ada, ulysses), (grace, dune)]:
try:
member.borrow(book)
except ValueError as err:
print("refused:", err)
ada.give_back(dune)
grace.borrow(dune)
print(f"Ada: {ada.titles()}")
print(f"Grace: {grace.titles()}")

JavaScript

library.js
class Book {
constructor(title, author) {
this.title = title;
this.author = author;
this.available = true;
}
}
class Member {
static MAX_BOOKS = 2;
#borrowed = [];
constructor(name) {
this.name = name;
}
borrow(book) {
if (!book.available) {
throw new Error(`'${book.title}' is already on loan`);
}
if (this.#borrowed.length >= Member.MAX_BOOKS) {
throw new Error(`${this.name} already has ${Member.MAX_BOOKS} books`);
}
book.available = false;
this.#borrowed.push(book);
}
giveBack(book) {
this.#borrowed.splice(this.#borrowed.indexOf(book), 1);
book.available = true;
}
titles() {
return this.#borrowed.map((book) => book.title).join(", ");
}
}
const dune = new Book("Dune", "Frank Herbert");
const emma = new Book("Emma", "Jane Austen");
const ulysses = new Book("Ulysses", "James Joyce");
const ada = new Member("Ada");
const grace = new Member("Grace");
ada.borrow(dune);
ada.borrow(emma);
for (const [member, book] of [[ada, ulysses], [grace, dune]]) {
try {
member.borrow(book);
} catch (err) {
console.log("refused:", err.message);
}
}
ada.giveBack(dune);
grace.borrow(dune);
console.log(`Ada: ${ada.titles()}`);
console.log(`Grace: ${grace.titles()}`);

Java

Library.java
import java.util.ArrayList;
import java.util.List;
class Book {
final String title;
final String author;
boolean available = true;
Book(String title, String author) {
this.title = title;
this.author = author;
}
}
class Member {
static final int MAX_BOOKS = 2;
private final String name;
private final List<Book> borrowed = new ArrayList<>();
Member(String name) {
this.name = name;
}
void borrow(Book book) {
if (!book.available) {
throw new IllegalStateException("'" + book.title + "' is already on loan");
}
if (borrowed.size() >= MAX_BOOKS) {
throw new IllegalStateException(name + " already has " + MAX_BOOKS + " books");
}
book.available = false;
borrowed.add(book);
}
void giveBack(Book book) {
borrowed.remove(book);
book.available = true;
}
String titles() {
List<String> names = new ArrayList<>();
for (Book book : borrowed) {
names.add(book.title);
}
return String.join(", ", names);
}
}
public class Library {
public static void main(String[] args) {
Book dune = new Book("Dune", "Frank Herbert");
Book emma = new Book("Emma", "Jane Austen");
Book ulysses = new Book("Ulysses", "James Joyce");
Member ada = new Member("Ada");
Member grace = new Member("Grace");
ada.borrow(dune);
ada.borrow(emma);
Member[] members = {ada, grace};
Book[] books = {ulysses, dune};
for (int i = 0; i < members.length; i++) {
try {
members[i].borrow(books[i]);
} catch (IllegalStateException err) {
System.out.println("refused: " + err.getMessage());
}
}
ada.giveBack(dune);
grace.borrow(dune);
System.out.println("Ada: " + ada.titles());
System.out.println("Grace: " + grace.titles());
}
}

C

library.c
#include <stdbool.h>
#include <stdio.h>
#define MAX_BOOKS 2
typedef struct {
const char *title;
const char *author;
bool available;
} Book;
typedef struct {
const char *name;
Book *borrowed[MAX_BOOKS];
int count;
} Member;
/* Returns NULL on success, or a message that says why the loan was refused. */
static const char *member_borrow(Member *m, Book *book) {
static char message[80];
if (!book->available) {
snprintf(message, sizeof message, "'%s' is already on loan", book->title);
return message;
}
if (m->count >= MAX_BOOKS) {
snprintf(message, sizeof message, "%s already has %d books", m->name, MAX_BOOKS);
return message;
}
book->available = false;
m->borrowed[m->count++] = book;
return NULL;
}
static void member_give_back(Member *m, Book *book) {
for (int i = 0; i < m->count; i++) {
if (m->borrowed[i] == book) {
m->borrowed[i] = m->borrowed[m->count - 1];
m->count--;
book->available = true;
return;
}
}
}
static void member_print(const Member *m) {
printf("%s: ", m->name);
for (int i = 0; i < m->count; i++) {
printf("%s%s", i > 0 ? ", " : "", m->borrowed[i]->title);
}
printf("\n");
}
int main(void) {
Book dune = {"Dune", "Frank Herbert", true};
Book emma = {"Emma", "Jane Austen", true};
Book ulysses = {"Ulysses", "James Joyce", true};
Member ada = {"Ada", {NULL}, 0};
Member grace = {"Grace", {NULL}, 0};
member_borrow(&ada, &dune);
member_borrow(&ada, &emma);
Member *members[] = {&ada, &grace};
Book *books[] = {&ulysses, &dune};
for (int i = 0; i < 2; i++) {
const char *problem = member_borrow(members[i], books[i]);
if (problem != NULL) {
printf("refused: %s\n", problem);
}
}
member_give_back(&ada, &dune);
member_borrow(&grace, &dune);
member_print(&ada);
member_print(&grace);
return 0;
}

All four print the four lines above.

ExerciseSame or equal?

Make two lists a and b that both hold 1 and 2, and a third name c for the same list as a. Print whether a and b have equal contents, whether a and b are the same list, and whether a and c are the same list. Then set the first element through c to 99 and print a and b. Expected output:

equal contents (a, b): yes
same list (a, b): no
same list (a, c): yes
a: [99, 2]
b: [1, 2]
Show solution

a and b match in contents but are two separate lists; a and c are one list with two names, so a change through c shows in a and leaves b alone.

Python

same_or_equal.py
a = [1, 2]
b = [1, 2]
c = a
print("equal contents (a, b):", "yes" if a == b else "no")
print("same list (a, b):", "yes" if a is b else "no")
print("same list (a, c):", "yes" if a is c else "no")
c[0] = 99
print("a:", a)
print("b:", b)

JavaScript

same_or_equal.js
const a = [1, 2];
const b = [1, 2];
const c = a;
const sameContents = a.length === b.length && a.every((value, i) => value === b[i]);
console.log("equal contents (a, b):", sameContents ? "yes" : "no");
console.log("same list (a, b):", a === b ? "yes" : "no");
console.log("same list (a, c):", a === c ? "yes" : "no");
c[0] = 99;
console.log("a:", `[${a.join(", ")}]`);
console.log("b:", `[${b.join(", ")}]`);

Java

SameOrEqual.java
import java.util.ArrayList;
import java.util.List;
public class SameOrEqual {
public static void main(String[] args) {
List<Integer> a = new ArrayList<>(List.of(1, 2));
List<Integer> b = new ArrayList<>(List.of(1, 2));
List<Integer> c = a;
System.out.println("equal contents (a, b): " + (a.equals(b) ? "yes" : "no"));
System.out.println("same list (a, b): " + (a == b ? "yes" : "no"));
System.out.println("same list (a, c): " + (a == c ? "yes" : "no"));
c.set(0, 99);
System.out.println("a: " + a);
System.out.println("b: " + b);
}
}

C

same_or_equal.c
#include <stdio.h>
#include <string.h>
int main(void) {
int a[2] = {1, 2};
int b[2] = {1, 2};
int *c = a; /* a second name for the same array */
printf("equal contents (a, b): %s\n", memcmp(a, b, sizeof a) == 0 ? "yes" : "no");
printf("same list (a, b): %s\n", &a[0] == &b[0] ? "yes" : "no");
printf("same list (a, c): %s\n", &a[0] == &c[0] ? "yes" : "no");
c[0] = 99;
printf("a: [%d, %d]\n", a[0], a[1]);
printf("b: [%d, %d]\n", b[0], b[1]);
return 0;
}

All four print the five lines above.

ExerciseSpot the design problem

A Temperature object stores celsius as a public field. Another part of the program sets t.celsius = -500. What is wrong, and how would you fix the design? Then check your fix: setting 20 should be accepted, and setting -500 should be refused with the message rejected: -500 is below absolute zero.

Show solution

Nothing stops an impossible value: nothing can be colder than absolute zero (about -273.15 degrees Celsius). Make the field private and offer a setter method that checks the value and refuses it when it is out of range. Then the object can never be in an invalid state, whichever code uses it.

Python

temperature.py
class Temperature:
def __init__(self):
self._celsius = 0.0
def set_celsius(self, value):
if value < -273.15:
raise ValueError(f"{value} is below absolute zero")
self._celsius = value
def celsius(self):
return self._celsius
t = Temperature()
t.set_celsius(20)
print("accepted:", t.celsius())
try:
t.set_celsius(-500)
except ValueError as err:
print("rejected:", err)

JavaScript

temperature.js
class Temperature {
#celsius = 0;
setCelsius(value) {
if (value < -273.15) {
throw new RangeError(`${value} is below absolute zero`);
}
this.#celsius = value;
}
celsius() {
return this.#celsius;
}
}
const t = new Temperature();
t.setCelsius(20);
console.log("accepted:", t.celsius());
try {
t.setCelsius(-500);
} catch (err) {
console.log("rejected:", err.message);
}

Java

Temperature.java
public class Temperature {
private double celsius = 0;
void setCelsius(double value) {
if (value < -273.15) {
throw new IllegalArgumentException((int) value + " is below absolute zero");
}
celsius = value;
}
double celsius() {
return celsius;
}
public static void main(String[] args) {
Temperature t = new Temperature();
t.setCelsius(20);
System.out.println("accepted: " + (int) t.celsius());
try {
t.setCelsius(-500);
} catch (IllegalArgumentException err) {
System.out.println("rejected: " + err.getMessage());
}
}
}

C

temperature.c
#include <stdio.h>
typedef struct {
double celsius; /* callers should use temperature_set instead of writing this */
} Temperature;
/* Returns 0 if the value was accepted, -1 if it is below absolute zero. */
static int temperature_set(Temperature *t, double value) {
if (value < -273.15) {
return -1;
}
t->celsius = value;
return 0;
}
int main(void) {
Temperature t = {0};
if (temperature_set(&t, 20) == 0) {
printf("accepted: %g\n", t.celsius);
}
if (temperature_set(&t, -500) != 0) {
printf("rejected: -500 is below absolute zero\n");
}
return 0;
}

C cannot force callers to go through temperature_set; the protection comes from hiding the struct’s layout in a separate file.

All four print:

accepted: 20
rejected: -500 is below absolute zero

Summary

  • A record groups named fields into one value; methods turn it into an object that carries its own behaviour.
  • Encapsulation keeps fields private and enforces rules in one place.
  • Equality asks whether contents match; identity asks whether two names refer to the same object.

Next: Stacks, queues and linked lists, the classic ways of ordering a collection by when things arrive.