Chapter 13_Basic Object-oriented Programming
13.1 — Welcome to object-oriented programming
Object-oriented programming (OOP) provides us with the ability to create objects that tie together both properties and behaviors into a self-contained, reusable package. This leads to code that looks more like this:
you.driveTo(work);
This not only reads more clearly, it also makes it clearer who the subject is (you) and what behavior is being invoked (driving somewhere). Rather than being focused on writing functions, we’re focused on defining objects that have a well-defined set of behaviors. This is why the paradigm is called “object-oriented”.
Note that OOP doesn’t replace traditional programming methods. Rather, it gives you additional tools in your programming tool belt to manage complexity when needed.
13.2 — Classes and class members
Classes
In the world of object-oriented programming, we often want our types to not only hold data, but provide functions that work with the data as well. In C++, this is typically done via the class keyword. The class keyword defines a new program-defined type called a class.
Warning
Just like with structs, one of the easiest mistakes to make in C++ is to forget the semicolon at the end of a class declaration. This will cause a compiler error on the next line of code. Modern compilers like Visual Studio 2010 will give you an indication that you may have forgotten a semicolon, but older or less sophisticated compilers may not, which can make the actual error hard to find.
A reminder
Initialize the member variables of a class at the point of declaration.
Member Functions
So when we call “today.print()”, the compiler interprets m_day as today.m_day, m_month as today.m_month, and m_year as today.m_year. If we called “tomorrow.print()”, m_day would refer to tomorrow.m_day instead.
In this way, the associated object is essentially implicitly passed to the member function. For this reason, it is often called the implicit object.
Best practice
Name your classes starting with a capital letter.
With member functions, this limitation doesn’t apply:
class foo
{
public:
void x() { y(); } // okay to call y() here, even though y() isn't defined until later in this class
void y() { };
};
Member types
In addition to member variables and member functions, classes can have member types or nested types (including type aliases).
A note about structs in C++
Best practice
Use the struct keyword for data-only structures. Use the class keyword for objects that have both data and functions.
Conclusion
The class keyword lets us create a custom type in C++ that can contain both member variables and member functions. Classes form the basis for Object-oriented programming, and we’ll spend the rest of this chapter and many of the future chapters exploring all they have to offer!
13.3 — Public vs private access specifiers
Public members are members of a struct or class that can be accessed directly by anyone, including from code that exists outside the struct or class.
The code outside of a struct or class is sometimes called the public: the public is only allowed to access the public members of a struct or class, which makes sense.
class DateClass // members are private by default
{
int m_month {}; // private by default, can only be accessed by other members
int m_day {}; // private by default, can only be accessed by other members
int m_year {}; // private by default, can only be accessed by other members
};
int main()
{
DateClass date;
date.m_month = 10; // error
date.m_day = 14; // error
date.m_year = 2020; // error
return 0;
}
Private members are members of a class that can not be accessed by the public. Private members can only be accessed by other members of the class (or by friends of the class).
Access specifiers
Mixing access specifiers
Best practice
Make member variables private, and member functions public, unless you have a good reason not to.
The group of public members of a class are often referred to as a public interface.
Access controls work on a per-class basis
Consider the following program:
#include <iostream>
class DateClass // members are private by default
{
int m_month {}; // private by default, can only be accessed by other members
int m_day {}; // private by default, can only be accessed by other members
int m_year {}; // private by default, can only be accessed by other members
public:
void setDate(int month, int day, int year)
{
m_month = month;
m_day = day;
m_year = year;
}
void print()
{
std::cout << m_month << '/' << m_day << '/' << m_year;
}
// Note the addition of this function
void copyFrom(const DateClass& d)
{
// Note that we can access the private members of d directly
m_month = d.m_month;
m_day = d.m_day;
m_year = d.m_year;
}
};
int main()
{
DateClass date;
date.setDate(10, 14, 2020); // okay, because setDate() is public
DateClass copy {};
copy.copyFrom(date); // okay, because copyFrom() is public
copy.print();
std::cout << '\n';
return 0;
}
One nuance of C++ that is often missed or misunderstood is that access control works on a per-class basis, not a per-object basis. This means that when a function has access to the private members of a class, it can access the private members of any object of that class type that it can see.
In the above example, copyFrom() is a member of DateClass, which gives it access to the private members of DateClass. This means copyFrom() can not only directly access the private members of the implicit object it is operating on (copy), it also means it has direct access to the private members of DateClass parameter d! If parameter d were some other type, this would not be the case.
This can be particularly useful when we need to copy members from one object of a class to another object of the same class. We’ll also see this topic show up again when we talk about overloading operator<< to print members of a class in the next chapter.
Structs vs classes revisited
Quiz time
Question #3
Now let’s try something a little more complex. Let’s write a class that implements a simple stack from scratch. Review lesson 12.2 – The stack and the heap if you need a refresher on what a stack is.
13.4 — Access functions and encapsulation
For similar reasons, the separation of implementation and interface is useful in programming.
Encapsulation
Benefit: encapsulated classes are easier to use and reduce the complexity of your programs
Benefit: encapsulated classes help protect your data and prevent misuse
Benefit: encapsulated classes are easier to change
Benefit: encapsulated classes are easier to debug
Access functions
Best practice
Getters should return by value or const reference.
Access functions concerns
Summary
As you can see, encapsulation provides a lot of benefits for just a little bit of extra effort. The primary benefit is that encapsulation allows us to use a class without having to know how it was implemented. This makes it a lot easier to use classes we’re not familiar with.
13.5 — Constructors
When all members of a class (or struct) are public, we can use aggregate initialization to initialize the class (or struct) directly using list-initialization:
class Foo
{
public:
int m_x {};
int m_y {};
};
int main()
{
Foo foo { 6, 7 }; // list-initialization
return 0;
}
However, as soon as we make any member variables private, we’re no longer able to initialize classes in this way. It does make sense: if you can’t directly access a variable (because it’s private), you shouldn’t be able to directly initialize it.
So then how do we initialize a class with private member variables? The answer is through constructors.
Constructors
Value-initialization
In the above program, we initialized our class object using value-initialization:
Fraction frac {}; // Value initialization using empty set of braces
We can also initialize class objects using default-initialization:
Fraction frac; // Default-initialization, calls default constructor
Best practice
Favor value-initialization over default-initialization for class objects.
Direct- and list-initialization using constructors with parameters
Best practice
Favor brace initialization to initialize class objects.
An implicitly generated default constructor
Best practice
If you have constructors in your class and need a default constructor that does nothing (e.g. because all your members are initialized using non-static member initialization), use = default.
Constructor notes
Best practice
Always initialize all member variables in your objects.
13.6 — Constructor member initializer lists
This produces code similar to the following:
const int m_value; // error: const vars must be initialized with a value
m_value = 5; // error: const vars can not be assigned to
Assigning values to const or reference member variables in the body of the constructor is clearly not possible in some cases.
Member initializer lists
Best practice
Use member initializer lists to initialize your class member variables instead of assignment.
Initializing const member variables
Rule
Const member variables must be initialized.
Initializing array members with member initializer lists
Initializer list order
Perhaps surprisingly, variables in the initializer list are not initialized in the order that they are specified in the initializer list. Instead, they are initialized in the order in which they are declared in the class.
For best results, the following recommendations should be observed:
- Don’t initialize member variables in such a way that they are dependent upon other member variables being initialized first (in other words, ensure your member variables will properly initialize even if the initialization ordering is different).
- Initialize variables in the initializer list in the same order in which they are declared in your class. This isn’t strictly required so long as the prior recommendation has been followed, but your compiler may give you a warning if you don’t do so and you have all warnings turned on.
Summary
Member initializer lists allow us to initialize our members rather than assign values to them. This is the only way to initialize members that require values upon initialization, such as const or reference members, and it can be more performant than assigning values in the body of the constructor. Member initializer lists work both with fundamental types and members that are classes themselves.
13.7 — Non-static member initialization
When writing a class that has multiple constructors (which is most of them), having to specify default values for all members in each constructor results in redundant code. If you update the default value for a member, you need to touch each constructor.
It’s possible to give normal class member variables (those that don’t use the static keyword) a default initialization value directly:
#include <iostream>
class Rectangle
{
private:
double m_length{ 1.0 }; // m_length has a default value of 1.0
double m_width{ 1.0 }; // m_width has a default value of 1.0
public:
void print()
{
std::cout << "length: " << m_length << ", width: " << m_width << '\n';
}
};
int main()
{
Rectangle x{}; // x.m_length = 1.0, x.m_width = 1.0
x.print();
return 0;
}
This program produces the result:
length: 1.0, width: 1.0
However, note that constructors still determine what kind of objects may be created. Consider the following case:
#include <iostream>
class Rectangle
{
private:
double m_length{ 1.0 };
double m_width{ 1.0 };
public:
// note: No default constructor provided in this example
Rectangle(double length, double width)
: m_length{ length },
m_width{ width }
{
// m_length and m_width are initialized by the constructor (the default values aren't used)
}
void print()
{
std::cout << "length: " << m_length << ", width: " << m_width << '\n';
}
};
int main()
{
Rectangle x{}; // will not compile, no default constructor exists, even though members have default initialization values
return 0;
}
Even though we’ve provided default values for all members, no default constructor has been provided, so we are unable to create Rectangle objects with no arguments.
Note that initializing members using non-static member initialization requires using either an equals sign, or a brace (uniform) initializer – the parenthesis initialization form doesn’t work here:
class A
{
int m_a = 1; // ok (copy initialization)
int m_b{ 2 }; // ok (brace initialization)
int m_c(3); // doesn't work (parenthesis initialization)
};
Rule
Favor use of non-static member initialization to give default values for your member variables.
13.8 — Overlapping and delegating constructors
Constructors with overlapping functionality
When you instantiate a new object, the object’s constructor is called implicitly. It’s not uncommon to have a class with multiple constructors that have overlapping functionality. Consider the following class:
class Foo
{
public:
Foo()
{
// code to do A
}
Foo(int value)
{
// code to do A
// code to do B
}
};
This class has two constructors: a default constructor, and a constructor that takes an integer. Because the “code to do A” portion of the constructor is required by both constructors, the code is duplicated in each constructor.
As you’ve (hopefully) learned by now, having duplicate code is something to be avoided as much as possible, so let’s take a look at some ways to address this.
The obvious solution doesn’t work
class Foo
{
public:
Foo()
{
// code to do A
}
Foo(int value)
{
Foo(); // use the above constructor to do A (doesn't work)
// code to do B
}
};
Delegating constructors
Constructors are allowed to call other constructors from the same class. This process is called delegating constructors (or constructor chaining)
.To have one constructor call another, simply call the constructor in the member initializer list. This is one case where calling another constructor directly is acceptable. Applied to our example above:
class Foo
{
private:
public:
Foo()
{
// code to do A
}
Foo(int value): Foo{} // use Foo() default constructor to do A
{
// code to do B
}
};
This works exactly as you’d expect. Make sure you’re calling the constructor from the member initializer list, not in the body of the constructor.
A few additional notes about delegating constructors. First, a constructor that delegates to another constructor is not allowed to do any member initialization itself. So your constructors can delegate or initialize, but not both.
Second, it’s possible for one constructor to delegate to another constructor, which delegates back to the first constructor. This forms an infinite loop, and will cause your program to run out of stack space and crash. You can avoid this by ensuring all of your constructors resolve to a non-delegating constructor.
Best practice
If you have multiple constructors that have the same functionality, use delegating constructors to avoid duplicate code.
Using a normal member function for setup
Constructors are allowed to call non-constructor member functions (and non-member functions), so a better solution is to use a normal (non-constructor) member function to handle the common setup tasks, like this:
#include <iostream>
class Foo
{
private:
const int m_value { 0 };
void setup() // setup is private so it can only be used by our constructors
{
// code to do some common setup tasks (e.g. open a file or database)
std::cout << "Setting things up...\n";
}
public:
Foo()
{
setup();
}
Foo(int value) : m_value { value } // we must initialize m_value since it's const
{
setup();
}
};
int main()
{
Foo a;
Foo b{ 5 };
return 0;
}
In this case, we’ve created a setup() member function to handle various setup tasks that we need, and both of our constructors call setup(). We’ve made this function private so we can ensure that only members of our class can call it.
Of course, setup() isn’t a constructor, so it can’t initialize members. By the time the constructor calls setup(), the members have already been created (and initialized if an initialization value was provided). The setup() function can only assign values to members or do other types of setup tasks that can be done through normal statements (e.g. open files or databases). The setup() function can’t do things like bind a member reference or set a const value (both of which must be done on initialization), or assign values to members that don’t support assignment.
Resetting a class object
While this works, it violates the DRY principle, as we have our “default” values in two places: once in the non-static member initializers, and again in the body of reset(). There is no way for the reset() function to get the default values from the non-static initializer.
However, if the class is assignable (meaning it has an accessible assignment operator), we can create a new class object, and then use assignment to overwrite the values in the object we want to reset:
#include <iostream>
class Foo
{
private:
int m_a{ 5 };
int m_b{ 6 };
public:
Foo()
{
}
Foo(int a, int b)
: m_a{ a }, m_b{ b }
{
}
void print()
{
std::cout << m_a << ' ' << m_b << '\n';
}
void reset()
{
// consider this a bit of magic for now
*this = Foo{}; // create new Foo object, then use assignment to overwrite our implicit object
}
};
int main()
{
Foo a{ 1, 2 };
a.reset();
a.print();
return 0;
}
13.9 — Destructors
A destructor is another special kind of class member function that is executed when an object of that class is destroyed. Whereas constructors are designed to initialize a class, destructors are designed to help clean up.
A class can only have a single destructor.
Destructor naming
However, destructors may safely call other member functions since the object isn’t destroyed until after the destructor executes.
A destructor example
A reminder
In lesson 11.17 – An introduction to std::vector, we note that parentheses based initialization should be used when initializing an array/container/list class with a length (as opposed to a list of elements). For this reason, we initialize IntArray using IntArray ar ( 10 );.
Constructor and destructor timing
RAII
RAII (Resource Acquisition Is Initialization) is a programming technique whereby resource use is tied to the lifetime of objects with automatic duration (e.g. non-dynamically allocated objects). In C++, RAII is implemented via classes with constructors and destructors. A resource (such as memory, a file or database handle, etc…) is typically acquired in the object’s constructor (though it can be acquired after the object is created if that makes sense). That resource can then be used while the object is alive. The resource is released in the destructor, when the object is destroyed. The primary advantage of RAII is that it helps prevent resource leaks (e.g. memory not being deallocated) as all resource-holding objects are cleaned up automatically.
The IntArray class at the top of this lesson is an example of a class that implements RAII – allocation in the constructor, deallocation in the destructor. std::string and std::vector are examples of classes in the standard library that follow RAII – dynamic memory is acquired on initialization, and cleaned up automatically on destruction.
A warning about the std::exit() function
Note that if you use the std::exit() function, your program will terminate and no destructors will be called. Be wary if you’re relying on your destructors to do necessary cleanup work (e.g. write something to a log file or database before exiting).
Summary
As you can see, when constructors and destructors are used together, your classes can initialize and clean up after themselves without the programmer having to do any special work! This reduces the probability of making an error, and makes classes easier to use.
13.10 — The hidden “this” pointer
The hidden *this pointer
The good news is that all of this happens automatically, and it doesn’t really matter whether you remember how it works or not. All you need to remember is that all non-static member functions have a “this” pointer that refers to the object the function was called on.
“this” always points to the object being operated on
Because “this” is just a function parameter, it doesn’t add any memory usage to your class (just to the member function call, since that parameter needs to be passed to the function and stored in memory).
Explicitly referencing “this”
Although this is acceptable coding practice, we find using the “m_” prefix on all member variable names provides a better solution by preventing duplicate names altogether!
Chaining member functions
class Calc
{
private:
int m_value{};
public:
Calc& add(int value) { m_value += value; return *this; }
Calc& sub(int value) { m_value -= value; return *this; }
Calc& mult(int value) { m_value *= value; return *this; }
int getValue() { return m_value; }
};
#include <iostream>
int main()
{
Calc calc{};
calc.add(5).sub(3).mult(4);
std::cout << calc.getValue() << '\n';
return 0;
}
Summary
The “this” pointer is a hidden parameter implicitly added to any non-static member function. Most of the time, you will not need to access it directly, but you can if needed. It’s worth noting that “this” is a const pointer – you can change the value of the underlying object it points to, but you can not make it point to something else!
By having functions that would otherwise return void return *this instead, you can make those functions chainable. This is most often used when overloading operators for your classes (something we’ll talk about more in chapter 14).
13.11 — Class code and header files
Doesn’t defining a class in a header file violate the one-definition rule?
It shouldn’t. If your header file has proper header guards, it shouldn’t be possible to include the class definition more than once into the same file.
Types (which include classes), are exempt from the part of the one-definition rule that says you can only have one definition per program. Therefore, there isn’t an issue #including class definitions into multiple code files (if there was, classes wouldn’t be of much use).
Libraries
Having your own files separated into declaration (header) and implementation (code file) is not only good form, it also makes creating your own custom libraries easier. Creating your own libraries is beyond the scope of these tutorials, but separating your declaration and implementation is a prerequisite to doing so.
13.12 — Const class objects and member functions
Const classes
Const member functions
Best practice
Make any member function that does not modify the state of the class object const, so that it can be called by const objects.
Const objects via pass by const reference
Can you figure out what’s wrong with the following code?
#include <iostream>
class Date
{
private:
int m_year {};
int m_month {};
int m_day {};
public:
Date(int year, int month, int day)
{
setDate(year, month, day);
}
void setDate(int year, int month, int day)
{
m_year = year;
m_month = month;
m_day = day;
}
int getYear() { return m_year; }
int getMonth() { return m_month; }
int getDay() { return m_day; }
};
// note: We're passing date by const reference here to avoid making a copy of date
void printDate(const Date& date)
{
std::cout << date.getYear() << '/' << date.getMonth() << '/' << date.getDay() << '\n';
}
int main()
{
Date date{2016, 10, 16};
printDate(date);
return 0;
}
The answer is that inside of the printDate function, date is treated as a const object. And with that const date, we’re calling functions getYear(), getMonth(), and getDay(), which are all non-const. Since we can’t call non-const member functions on const objects, this will cause a compile error.
The fix is simple: make getYear(), getMonth(), and getDay() const:
Const members can not return non-const references to members
Const members can not return non-const references to members
#include <string>
class Something
{
private:
std::string m_value {};
public:
Something(const std::string& value=""): m_value{ value } {}
const std::string& getValue() const { return m_value; } // getValue() for const objects (returns const reference)
std::string& getValue() { return m_value; } // getValue() for non-const objects (returns non-const reference)
};
Summary
Because passing objects by const reference is common, your classes should be const-friendly. That means making any member function that does not modify the state of the class object const!
13.13 — Static member variables
Static members are not associated with class objects
Best practice
Access static members by class name (using the scope resolution operator) rather than through an object of the class (using the member selection operator).
Defining and initializing static member variables
Because static member variables are not part of the individual class objects (they are treated similarly to global variables, and get initialized when the program starts), you must explicitly define the static member outside of the class, in the global scope.
In the example above, we do so via this line:
int Something::s_value{ 1 }; // defines the static member variable
This line serves two purposes: it instantiates the static member variable (just like a global variable), and optionally initializes it. In this case, we’re providing the initialization value 1. If no initializer is provided, C++ initializes the value to 0.
Note that this static member definition is not subject to access controls: you can define and initialize the variable even if it’s declared as private (or protected) in the class.
If the class is defined in a .h file, the static member definition is usually placed in the associated code file for the class (e.g. Something.cpp). If the class is defined in a .cpp file, the static member definition is usually placed directly underneath the class. Do not put the static member definition in a header file (much like a global variable, if that header file gets included more than once, you’ll end up with multiple definitions, which will cause a linker error).
Inline initialization of static member variables
class Whatever
{
public:
static const int s_value{ 4 }; // a static const int can be declared and initialized directly
};
#include <array>
class Whatever
{
public:
static constexpr double s_value{ 2.2 }; // ok
static constexpr std::array<int, 3> s_array{ 1, 2, 3 }; // this even works for classes that support constexpr initialization
};
Finally, as of C++17, we can also initialize non-const static members in the class definition by declaring them inline:
class Whatever
{
public:
static inline int s_value{ 4 }; // a static inline int can be declared and initialized directly (C++17)
};
Best practice
Prefer initializing static constexpr members at the point of definition.
Prefer making static non-constexpr members inline and initializing them at the point of definition.
An example of static member variables
Why use static variables inside classes? One useful example is to assign a unique ID to every instance of the class.
Static member variables can also be useful when the class needs to utilize an internal lookup table (e.g. an array used to store a set of pre-calculated values). By making the lookup table static, only one copy exists for all objects, rather than making a copy for each object instantiated. This can save substantial amounts of memory.
13.14 — Static member functions
class Something
{
private:
static int s_value;
};
int Something::s_value{ 1 }; // initializer, this is okay even though s_value is private since it's a definition
int main()
{
// how do we access Something::s_value since it is private?
}
Like static member variables, static member functions are not attached to any particular object. Here is the above example with a static member function accessor:
#include <iostream>
class Something
{
private:
static int s_value;
public:
static int getValue() { return s_value; } // static member function
};
int Something::s_value{ 1 }; // initializer
int main()
{
std::cout << Something::getValue() << '\n';
}
Like static member variables, they can also be called through objects of the class type, though this is not recommended.
Static member functions have no *this pointer
Another example
A word of warning about classes with all static members
Be careful when writing classes with all static members. Although such “pure static classes” (also called “monostates”) can be useful, they also come with some potential downsides.
C++ does not support static constructors
And while some modern languages do support static constructors for precisely this purpose, C++ is unfortunately not one of them.
If your static variable can be directly initialized, no constructor is needed: you can initialize the static member variable at the point of definition (even if it is private). We do this in the IDGenerator example above. Here’s another example:
class MyClass
{
public:
static std::vector<char> s_mychars;
};
std::vector<char> MyClass::s_mychars{ 'a', 'e', 'i', 'o', 'u' }; // initialize static variable at point of definition
Summary
Static member functions can be used to work with static member variables in the class. An object of the class is not required to call them.
Classes can be created with all static member variables and static functions. However, such classes are essentially the equivalent of declaring functions and global variables in a globally accessible namespace, and should generally be avoided unless you have a particularly good reason to use them.
13.15 — Friend functions and classes
Friend functions
A friend function is a function that can access the private members of a class as though it was a member of that class.
Here’s another example:
#include <iostream>
class Value
{
private:
int m_value{};
public:
Value(int value)
: m_value{ value }
{
}
friend bool isEqual(const Value& value1, const Value& value2);
};
bool isEqual(const Value& value1, const Value& value2)
{
return (value1.m_value == value2.m_value);
}
int main()
{
Value v1{ 5 };
Value v2{ 6 };
std::cout << std::boolalpha << isEqual(v1, v2);
return 0;
}
Multiple friends
A function can be a friend of more than one class at the same time.
For example, consider the following example:
#include <iostream>
class Humidity;
class Temperature
{
private:
int m_temp {};
public:
Temperature(int temp=0)
: m_temp { temp }
{
}
friend void printWeather(const Temperature& temperature, const Humidity& humidity);
};
class Humidity
{
private:
int m_humidity {};
public:
Humidity(int humidity=0)
: m_humidity { humidity }
{
}
friend void printWeather(const Temperature& temperature, const Humidity& humidity);
};
void printWeather(const Temperature& temperature, const Humidity& humidity)
{
std::cout << "The temperature is " << temperature.m_temp <<
" and the humidity is " << humidity.m_humidity << '\n';
}
int main()
{
Humidity hum{10};
Temperature temp{12};
printWeather(temp, hum);
return 0;
}
There are two things worth noting about this example. First, because printWeather is a friend of both classes, it can access the private data from objects of both classes. Second, note the following line at the top of the example:
class Humidity;
However, unlike functions, classes have no return types or parameters, so class prototypes are always simply class ClassName, where ClassName is the name of the class.
Friend classes
A few additional notes on friend classes. First, even though Display is a friend of Storage, Display has no direct access to the *this pointer of Storage objects (because *this is a function parameter of Storage member functions, not a member of Storage). Second, just because Display is a friend of Storage, that does not mean Storage is also a friend of Display. If you want two classes to be friends of each other, both must declare the other as a friend. Finally, if class A is a friend of B, and B is a friend of C, that does not mean A is a friend of C.
Be careful when using friend functions and classes, because it allows the friend function or class to violate encapsulation. If the details of the class change, the details of the friend will also be forced to change. Consequently, limit your use of friend functions and classes to a minimum.
Friend member functions
Instead of making an entire class a friend, you can make a single member function a friend. This is done similarly to making a normal function a friend, except using the name of the member function with the className:: prefix included (e.g. Display::displayItem).
Fortunately, this is also fixable in a couple of simple steps. First, we can add class Storage as a forward declaration. Second, we can move the definition of Display::displayItem() out of the class, after the full definition of Storage class.
Here’s what this looks like:
#include <iostream>
class Storage; // forward declaration for class Storage
class Display
{
private:
bool m_displayIntFirst {};
public:
Display(bool displayIntFirst)
: m_displayIntFirst { displayIntFirst }
{
}
void displayItem(const Storage& storage); // forward declaration above needed for this declaration line
};
class Storage // full definition of Storage class
{
private:
int m_nValue {};
double m_dValue {};
public:
Storage(int nValue, double dValue)
: m_nValue { nValue }, m_dValue { dValue }
{
}
// Make the Display::displayItem member function a friend of the Storage class (requires seeing the full definition of class Display, as above)
friend void Display::displayItem(const Storage& storage);
};
// Now we can define Display::displayItem, which needs to have seen the full definition of class Storage
void Display::displayItem(const Storage& storage)
{
if (m_displayIntFirst)
std::cout << storage.m_nValue << ' ' << storage.m_dValue << '\n';
else // display double first
std::cout << storage.m_dValue << ' ' << storage.m_nValue << '\n';
}
int main()
{
Storage storage(5, 6.7);
Display display(false);
display.displayItem(storage);
return 0;
}
If this seems like a pain – it is. Fortunately, this dance is only necessary because we’re trying to do everything in a single file. A better solution is to put each class definition in a separate header file, with the member function definitions in corresponding .cpp files. That way, all of the class definitions would have been visible immediately in the .cpp files, and no rearranging of classes or functions is necessary!
Summary
A friend function or class is a function or class that can access the private members of another class as though it was a member of that class. This allows the friend function or friend class to work intimately with the other class, without making the other class expose its private members (e.g. via access functions).
Friending is commonly used when defining overloaded operators (which we’ll cover in the next chapter), or less commonly, when two or more classes need to work together in an intimate way.
Note that making a specific member function a friend requires the full definition for the class of the member function to have been seen first.
13.16 — Anonymous objects
An anonymous object is essentially a value that has no name. Because they have no name, there’s no way to refer to them beyond the point where they are created. Consequently, they have “expression scope”, meaning they are created, evaluated, and destroyed all within a single expression.
Here is the add() function rewritten using an anonymous object:
#include <iostream>
int add(int x, int y)
{
return x + y; // an anonymous object is created to hold and return the result of x + y
}
int main()
{
std::cout << add(5, 3) << '\n';
return 0;
}
When the expression x + y is evaluated, the result is placed in an anonymous object. A copy of the anonymous object is then returned to the caller by value, and the anonymous object is destroyed.
#include <iostream>
void printValue(int value)
{
std::cout << value;
}
int main()
{
printValue(5 + 3);
return 0;
}
In this case, the expression 5 + 3 is evaluated to produce the result 8, which is placed in an anonymous object. A copy of this anonymous object is then passed to the printValue() function, (which prints the value 8) and then is destroyed.
Note how much cleaner this keeps our code – we don’t have to litter the code with temporary variables that are only used once.
Anonymous class objects
Although our prior examples have been with built-in data types, it is possible to construct anonymous objects of our own class types as well. This is done by creating objects like normal, but omitting the variable name.
Cents cents{ 5 }; // normal variable
Cents{ 7 }; // anonymous object
In fact, because cents1 and cents2 are only used in one place, we can anonymize this even further:
#include <iostream>
class Cents
{
private:
int m_cents{};
public:
Cents(int cents)
: m_cents { cents }
{}
int getCents() const { return m_cents; }
};
Cents add(const Cents& c1, const Cents& c2)
{
return { c1.getCents() + c2.getCents() }; // return anonymous Cents value
}
int main()
{
std::cout << "I have " << add(Cents{ 6 }, Cents{ 8 }).getCents() << " cents.\n"; // print anonymous Cents value
return 0;
}
Summary
In C++, anonymous objects are primarily used either to pass or return values without having to create lots of temporary variables to do so. Memory allocated dynamically is also done so anonymously (which is why its address must be assigned to a pointer, otherwise we’d have no way to refer to it).
It is also worth noting that because anonymous objects have expression scope, they can only be used once (unless bound to a constant l-value reference, which will extend the lifetime of the temporary object to match the lifetime of the reference). If you need to reference a value in multiple expressions, you should use a named variable instead.
13.17 — Nested types in classes
Nesting types
#include <iostream>
class Fruit
{
public:
// Note: we've moved FruitType inside the class, under the public access specifier
// We've also changed it from an enum class to an enum
enum FruitType
{
apple,
banana,
cherry
};
private:
FruitType m_type {};
int m_percentageEaten { 0 };
public:
Fruit(FruitType type) :
m_type { type }
{
}
FruitType getType() const { return m_type; }
int getPercentageEaten() const { return m_percentageEaten; }
};
int main()
{
// Note: we access the FruitType via Fruit now
Fruit apple { Fruit::apple };
if (apple.getType() == Fruit::apple)
std::cout << "I am an apple";
else
std::cout << "I am not an apple";
return 0;
}
First, note that FruitType is now defined inside the class. Second, note that we’ve defined it under the public access specifier, so the type definition can be accessed from outside the class.
Note that because enum classes also act like namespaces, if we’d nested FruitType inside Fruit as an enum class instead of an enum, we’d access the enumeration via a Fruit::FruitType:: scope qualifier. This double-scoping is unnecessary, so we’ve used a normal enum.
Other types can be nested too
Although enumerations are probably the most common type that is nested inside a class, C++ will let you define other types within a class, such as typedefs, type aliases, and even other classes!
Defining nested classes isn’t very common, but the C++ standard library does do so in some cases, such as with iterator classes.
13.18 — Timing your code
The good news is that we can easily encapsulate all the timing functionality we need into a class that we can then use in our own programs.
Here’s the class:
#include <chrono> // for std::chrono functions
class Timer
{
private:
// Type aliases to make accessing nested type easier
using Clock = std::chrono::steady_clock;
using Second = std::chrono::duration<double, std::ratio<1> >;
std::chrono::time_point<Clock> m_beg { Clock::now() };
public:
void reset()
{
m_beg = Clock::now();
}
double elapsed() const
{
return std::chrono::duration_cast<Second>(Clock::now() - m_beg).count();
}
};
A few caveats about timing
Timing is straightforward, but your results can be significantly impacted by a number of things, and it’s important to be aware of what those things are.
Finally, note that results are only valid for your machine’s architecture, OS, compiler, and system specs. You may get different results on other systems that have different strengths and weaknesses.
13.x — Chapter 13 comprehensive quiz
Chapter 14_Operator overloading
14.1 — Introduction to operator overloading
In C++, operators are implemented as functions. By using function overloading on the operator functions, you can define your own versions of the operators that work with different data types (including classes that you’ve written). Using function overloading to overload operators is called operator overloading.
Operators as functions
Consider the following example:
int x { 2 };
int y { 3 };
std::cout << x + y << '\n';
When you see the expression x + y, you can translate this in your head to the function call operator+(x, y)
(where operator+ is the name of the function).
Resolving overloaded operators
What are the limitations on operator overloading?
First, almost any existing operator in C++ can be overloaded. The exceptions are: conditional (?
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)