본문으로 건너뛰기

Classes

Data Abstraction: Separates a class interface from its implementation.

Interface: The operations available to users of a class.

Implementation: The data members and internal functions that implement the class.

Encapsulation: Hides implementation details from users.

Abstract Data Type: A type used through its public interface without requiring knowledge of its internal representation.

A class combines data and operations into a new type.

class Counter
{
public:
void increment()
{
++value;
}

int get() const
{
return value;
}

private:
int value = 0;
};

Usage:

Counter counter;

counter.increment();

std::cout << counter.get();

The user interacts with the public interface without directly modifying value.


7.1 Defining Abstract Data Types

An abstract data type hides its representation and exposes operations through an interface.

A Sales_data class can represent information about book sales.

Important operations include:

  • isbn()
  • combine()
  • read()
  • print()
  • add()

1) Designing the Sales_data Class

The interface can be designed around operations rather than direct access to data.

class Sales_data
{
public:
std::string isbn() const;

Sales_data& combine(
const Sales_data& rhs);

private:
std::string book_no;
unsigned units_sold = 0;
double revenue = 0.0;
};

The data representation is hidden.

User code works through member functions.

Sales_data item1;
Sales_data item2;

item1.combine(item2);

Member and Nonmember Functions

isbn() and combine() are member functions.

item1.isbn();

item1.combine(item2);

Functions such as read, print, and add can be ordinary nonmember functions.

read(std::cin, item1);

print(std::cout, item1);

Sales_data result =
add(item1, item2);

Functions closely related to a class should normally be declared in the same header as the class.


Class Designer and Class User

The class designer defines:

  • representation
  • invariants
  • constructors
  • member functions
  • interface

The class user relies only on that interface.

For example:

Sales_data item;

read(std::cin, item);

std::cout << item.isbn();

The user does not need to know how the ISBN is stored internally.


2) Defining the Revised Sales_data Class

A more complete class definition is:

#include <string>

class Sales_data
{
public:
std::string isbn() const
{
return book_no;
}

Sales_data& combine(
const Sales_data& rhs);

private:
double average_price() const;

std::string book_no;
unsigned units_sold = 0;
double revenue = 0.0;
};

Data Members

Data Member: Stores data belonging to each class object.

std::string book_no;
unsigned units_sold = 0;
double revenue = 0.0;

Each object has its own copies of non-static data members.

Sales_data item1;
Sales_data item2;

item1 and item2 contain independent state.


Member Functions

Member Function: A function declared inside a class.

std::string isbn() const
{
return book_no;
}

A member function defined inside the class body is implicitly inline.


Introducing this

Every non-static member function has access to an implicit pointer named this.

Consider:

std::string isbn() const
{
return book_no;
}

Conceptually, accessing:

book_no

means:

this->book_no

When:

item.isbn();

is called, this points to item.

Conceptually:

this


item object
└─ book_no

Explicit Use of this

Usually this does not need to be written explicitly.

Sales_data& Sales_data::combine(
const Sales_data& rhs)
{
units_sold += rhs.units_sold;
revenue += rhs.revenue;

return *this;
}

The data members refer to:

this->units_sold
this->revenue

The return expression:

*this

refers to the current object itself.


const Member Functions

A const member function promises not to modify the object's ordinary data members.

Syntax:

std::string isbn() const;

The const appears after the parameter list.

std::string Sales_data::isbn() const
{
return book_no;
}

Inside a const member function, this behaves as a pointer to a const object.

Therefore:

std::string isbn() const
{
// Error:
// book_no = "changed";

return book_no;
}

A const object may call const member functions.

const Sales_data item;

item.isbn();

but cannot call ordinary non-const member functions.

// Error:
// item.combine(other);

Why const Member Functions Matter

Without:

std::string isbn() const;

this would not work:

void print_isbn(
const Sales_data& item)
{
std::cout << item.isbn();
}

Because item is const, it can call only const-qualified member functions.

Use const on member functions whenever they do not modify the logical state of the object.


Class Scope

All class members belong to class scope.

A member function can use members declared later in the class definition.

class Example
{
public:
int get() const
{
return value;
}

private:
int value = 0;
};

Even though value appears after get(), it is visible in the function body.


Defining Member Functions outside the Class

A member function can be declared inside the class and defined outside.

class Sales_data
{
public:
Sales_data& combine(
const Sales_data& rhs);

private:
unsigned units_sold = 0;
double revenue = 0.0;
};

Definition:

Sales_data& Sales_data::combine(
const Sales_data& rhs)
{
units_sold += rhs.units_sold;
revenue += rhs.revenue;

return *this;
}

Sales_data:: tells the compiler that combine belongs to Sales_data.

The declaration and definition must agree in:

  • return type
  • function name
  • parameter types
  • const qualification

Returning *this

Returning *this by reference allows a member function to return the current object.

Sales_data& Sales_data::combine(
const Sales_data& rhs)
{
units_sold += rhs.units_sold;
revenue += rhs.revenue;

return *this;
}

This enables chaining:

item1.combine(item2)
.combine(item3);

The first call returns item1 itself, allowing the second call to operate on the same object.


Some operations logically belong to a class interface but do not need to be members.

For Sales_data:

Sales_data add(
const Sales_data& lhs,
const Sales_data& rhs);

std::istream& read(
std::istream& input,
Sales_data& item);

std::ostream& print(
std::ostream& output,
const Sales_data& item);

read()

std::istream& read(
std::istream& input,
Sales_data& item)
{
double price = 0.0;

input
>> item.book_no
>> item.units_sold
>> price;

item.revenue =
price * item.units_sold;

return input;
}

The stream is passed by reference because stream objects cannot be copied.

It is non-const because reading changes the stream state.

Returning the stream allows:

if (read(std::cin, item))
{
// input succeeded
}

print()

std::ostream& print(
std::ostream& output,
const Sales_data& item)
{
output
<< item.isbn() << ' '
<< item.units_sold << ' '
<< item.revenue << ' '
<< item.average_price();

return output;
}

The Sales_data argument is a reference to const because printing does not modify it.

The function itself normally does not add a newline.

Caller code decides formatting:

print(std::cout, item)
<< '\n';

add()

Sales_data add(
const Sales_data& lhs,
const Sales_data& rhs)
{
Sales_data result = lhs;

result.combine(rhs);

return result;
}

lhs is copied into result.

The copy is modified.

The original objects remain unchanged.

Sales_data result =
add(item1, item2);

4) Constructors

Constructor: A special member function used to initialize objects.

A constructor:

  • has the same name as the class
  • has no return type
  • may be overloaded

Example:

class Point
{
public:
Point() = default;

Point(double x_value, double y_value)
: x(x_value),
y(y_value)
{
}

private:
double x = 0.0;
double y = 0.0;
};

Usage:

Point p1;
Point p2(10.0, 20.0);

Sales_data Constructors

class Sales_data
{
public:
Sales_data() = default;

Sales_data(
const std::string& s)
: book_no(s)
{
}

Sales_data(
const std::string& s,
unsigned count,
double price)
: book_no(s),
units_sold(count),
revenue(count * price)
{
}

Sales_data(std::istream& input);

private:
std::string book_no;
unsigned units_sold = 0;
double revenue = 0.0;
};

These constructors support several initialization forms.

Sales_data item1;

Sales_data item2(
"0-201-82470-1");

Sales_data item3(
"0-201-82470-1",
5,
19.99);

Sales_data item4(std::cin);

Default Constructor

A constructor that can be called with no arguments is a default constructor.

Sales_data item;

If a class defines no constructors, the compiler may synthesize a default constructor.

However, once a class defines its own constructors, it should explicitly provide a default constructor when default construction is needed.

Sales_data() = default;

= default

= default asks the compiler to generate the normal defaulted implementation.

class Example
{
public:
Example() = default;

private:
int value = 0;
};

A defaulted constructor defined inside the class is implicitly inline.


Constructor Initializer List

Members should be initialized using the constructor initializer list.

Sales_data(
const std::string& s,
unsigned count,
double price)
: book_no(s),
units_sold(count),
revenue(count * price)
{
}

Initialization occurs before the constructor body executes.

The initializer list:

: book_no(s),
units_sold(count),
revenue(count * price)

initializes the members directly.


Initialization vs. Assignment

Prefer:

class Point
{
public:
Point(int x_value, int y_value)
: x(x_value),
y(y_value)
{
}

private:
int x;
int y;
};

over:

class Point
{
public:
Point(int x_value, int y_value)
{
x = x_value;
y = y_value;
}

private:
int x;
int y;
};

In the second version, members are initialized before the body and then assigned new values.

Constructor initializer lists perform the intended initialization directly.


Constructor Defined outside the Class

Declaration:

class Sales_data
{
public:
Sales_data(std::istream& input);

// ...
};

Definition:

Sales_data::Sales_data(
std::istream& input)
{
read(input, *this);
}

*this refers to the object currently being constructed.


5) Copy, Assignment, and Destruction

Classes also control what happens when objects are:

  • copied
  • assigned
  • destroyed

Example:

Sales_data item1;

Sales_data item2 = item1;

This performs copy initialization.

Assignment:

item2 = item1;

replaces the state of an existing object.


Synthesized Operations

When appropriate, the compiler generates operations that work member by member.

For a class such as:

class Record
{
private:
std::string name;
std::vector<int> values;
};

the generated copy operation copies:

  • name
  • values

The standard library members already manage their own resources correctly.

Therefore classes built from types such as:

std::string
std::vector

can often rely on compiler-generated copy, assignment, and destruction.


Resource-Managing Classes

A class directly managing resources may need custom copy-control operations.

For example, a class containing a raw pointer to dynamically allocated memory:

class Buffer
{
private:
int* data = nullptr;
};

cannot automatically assume that simply copying the pointer produces the intended ownership semantics.

Copy control is covered in detail later.


7.2 Access Control and Encapsulation

Access specifiers control which parts of a class are visible to users.

The two main access specifiers are:

public
private

Example:

class Counter
{
public:
void increment()
{
++value;
}

int get() const
{
return value;
}

private:
int value = 0;
};

public

Public members form the interface.

counter.increment();

std::cout << counter.get();

User code can access them directly.


private

Private members form the implementation.

private:
int value = 0;

External code cannot access them directly.

Counter counter;

// Error:
// counter.value = 100;

Only permitted code such as member functions and friends can access private members.


Encapsulated Sales_data

class Sales_data
{
friend Sales_data add(
const Sales_data&,
const Sales_data&);

friend std::istream& read(
std::istream&,
Sales_data&);

friend std::ostream& print(
std::ostream&,
const Sales_data&);

public:
Sales_data() = default;

Sales_data(const std::string& s)
: book_no(s)
{
}

std::string isbn() const
{
return book_no;
}

Sales_data& combine(
const Sales_data&);

private:
double average_price() const
{
return units_sold
? revenue / units_sold
: 0.0;
}

std::string book_no;
unsigned units_sold = 0;
double revenue = 0.0;
};

Users cannot directly place the object into an invalid state through its private data.


class vs. struct

For member access:

struct members are public by default.

struct Point
{
int x;
int y;
};

class members are private by default.

class Point
{
int x;
int y;
};

Both can explicitly use:

public:
private:
protected:

Use struct commonly for simple data aggregates and class when emphasizing encapsulation, though either keyword can implement class types.


1) Friends

Friend: A nonmember function or another class granted access to nonpublic members.

Example:

class Sales_data
{
friend std::istream& read(
std::istream&,
Sales_data&);

private:
std::string book_no;
unsigned units_sold = 0;
double revenue = 0.0;
};

Now read() can access:

item.book_no
item.units_sold
item.revenue

even though those members are private.


A Friend Is Not a Member

A friend function remains a nonmember function.

read(std::cin, item);

not:

// item.read(std::cin);

unless read is separately defined as a member.


Benefits of Encapsulation

Encapsulation provides two important benefits.

First, class users cannot directly depend on or corrupt internal representation.

Second, implementation details can change without changing user code.

For example, the class could change from:

double revenue;

to another internal representation while keeping:

double average_price() const;

unchanged.

User code does not need to know.


Friend Declarations

A friend declaration grants access.

For ordinary visibility, interface functions are also normally declared outside the class.

class Sales_data
{
friend std::istream& read(
std::istream&,
Sales_data&);

// ...
};

std::istream& read(
std::istream&,
Sales_data&);

These declarations usually belong in the same header.


7.3 Additional Class Features

Classes can also define:

  • type aliases
  • overloaded member functions
  • mutable members
  • inline members
  • functions returning *this
  • friendships with other classes

1) Class Members Revisited

Type Members

A class may define type aliases.

class Screen
{
public:
using position =
std::string::size_type;

private:
position cursor = 0;
};

Usage:

Screen::position position;

The type belongs to class scope.

Type aliases should normally appear before members that use them.


A Screen Class

#include <string>

class Screen
{
public:
using position =
std::string::size_type;

Screen() = default;

Screen(
position height,
position width,
char character)
: height(height),
width(width),
contents(
height * width,
character)
{
}

char get() const
{
return contents[cursor];
}

char get(
position row,
position column) const
{
return contents[
row * width + column
];
}

Screen& move(
position row,
position column)
{
cursor =
row * width + column;

return *this;
}

private:
position cursor = 0;
position height = 0;
position width = 0;

std::string contents;
};

Overloaded Member Functions

Member functions can be overloaded.

char get() const;

char get(
position row,
position column) const;

Usage:

screen.get();

screen.get(2, 4);

The compiler selects the overload from the argument list.


Inline Members

A function defined inside the class is implicitly inline.

char get() const
{
return contents[cursor];
}

A function defined outside may also be explicitly declared inline.

inline
Screen& Screen::move(
position row,
position column)
{
cursor =
row * width + column;

return *this;
}

mutable Data Members

A mutable member can be changed even in a const member function.

class Screen
{
public:
void some_member() const
{
++access_count;
}

private:
mutable std::size_t access_count = 0;
};

Even:

const Screen screen;

may call:

screen.some_member();

and access_count may change.

mutable is useful for implementation state that does not change the logical value of the object, such as:

  • cache state
  • usage counters
  • lazy-computation metadata

In-Class Initializers

Class-type members may use = or braces.

class Window
{
private:
std::string title = "Untitled";

std::vector<int> values{
1,
2,
3
};
};

2) Functions That Return *this

A member can return the current object by reference.

Screen& move(
position row,
position column)
{
cursor =
row * width + column;

return *this;
}

This enables chained calls.

screen
.move(1, 2)
.set('#');

Each operation works on the same screen object.


Returning a Copy vs. Returning *this

If the function returned by value:

Screen move(...);

then the next operation could operate on a copy.

Returning:

Screen&

means the original object is returned.


Returning *this from a const Member

Inside a const member function, *this is const.

Therefore:

const Screen& display() const
{
return *this;
}

is appropriate.


Overloading on const

Member functions can be overloaded by const qualification.

class Screen
{
public:
Screen& display()
{
do_display();

return *this;
}

const Screen& display() const
{
do_display();

return *this;
}

private:
void do_display() const
{
std::cout << contents;
}

std::string contents;
};

For:

Screen screen;

the non-const overload is preferred.

For:

const Screen screen;

the const overload is used.


Private Utility Functions

Common implementation can be placed in a private helper.

private:
void do_display() const
{
std::cout << contents;
}

Both public overloads can reuse it.

This keeps duplicated implementation out of the interface.


3) Class Types

Every class definition creates a distinct type.

struct A
{
int value;
};

struct B
{
int value;
};

Despite identical members:

A and B are different types.

Therefore:

A a;
B b;

// Error:
// a = b;

Forward Declaration

A class can be declared without being defined.

class Screen;

At this point, Screen is an incomplete type.

You can declare:

Screen* pointer;

Screen& get_screen();

void process(Screen&);

because these do not require knowing the object's complete layout.


Complete Type Requirement

You cannot create an object before the class is fully defined.

class Screen;

// Error:
// Screen object;

The compiler does not yet know how much memory a Screen requires.

Likewise, member access requires the full definition.


Self-Referential Classes

A class cannot contain an object of its own type directly.

Invalid:

class Node
{
// Error:
// Node next;
};

That would require an infinitely large object.

A class may contain a pointer to its own type.

class Node
{
public:
int value = 0;

Node* next = nullptr;
};

This is the basic idea behind linked data structures.


4) Friendship Revisited

Friendship can be granted to:

  • an ordinary function
  • an entire class
  • one member function of another class

Friendship is not transitive.


Friend Class

Suppose WindowManager must modify private members of Screen.

class WindowManager;

class Screen
{
friend class WindowManager;

private:
std::string contents;

std::size_t height = 0;
std::size_t width = 0;
};

All member functions of WindowManager can access private members of Screen.


Example WindowManager

class WindowManager
{
public:
using ScreenIndex =
std::vector<Screen>::size_type;

void clear(ScreenIndex index);

private:
std::vector<Screen> screens{
Screen(24, 80, ' ')
};
};

Because the class is a friend:

void WindowManager::clear(
ScreenIndex index)
{
Screen& screen =
screens[index];

screen.contents =
std::string(
screen.height *
screen.width,
' ');
}

Without friendship, direct access to:

screen.contents
screen.height
screen.width

would be prohibited.


Friendship Is Not Transitive

If:

Screen
friends with
WindowManager

and:

WindowManager
friends with
AnotherClass

then:

AnotherClass