You are on page 1of 589

Learning C++ MEAP V05

1. Copyright_2023_Manning_Publications
2. welcome
3. 1_C++_is_brilliant!
4. 2_Exploring_C++_fundamentals
5. 3_Smooth_operator_–exploring_C++_operators_and_conditions
6. 4_Let_it_flow_–_Conditions,_iteration,_and_flow_control
7. 5_Hip_hip_array_–_C++_arrays
8. 6_Vectors_–_your_arrays_"on_steroids"
9. 7_With_strings_attached_-_working_with_string_literals
10. 8_Function_in_action
MEAP Edition Manning Early Access Program Learning C++ Version 5

Copyright 2023 Manning Publications


©Manning Publications Co. We welcome reader comments about anything in
the manuscript - other than typos and other simple mistakes. These will be
cleaned up during production of the book by copyeditors and proofreaders.
https://livebook.manning.com/#!/book/learning-c-plus-plus/discussion

For more information on this and other Manning titles go to manning.com


welcome
Thanks for purchasing the MEAP for Learning C++ book. We hope you will
enjoy the ride, and with your help, we can perfect the content and flow of
each chapter, up to the completed book.

We have designed and crafted this book especially for those with no skills in
computer programming, or for programmers with some skills in other
programming languages, wishing to make the move to C++.

C++ is considered to be one of the most powerful and robust programming


languages out there, and as such, teaching C++ might be even more
challenging than learning it – especially when trying to teach C++ to
complete beginners.

This book was born from a notion there should be a better way to teach C++,
while making sure beginners to the language will not miss out on some of the
advanced and fantastic features C++20, which is the new and improved C++
standard, can offer. When we were looking at printed or online guides to
some of the new C++20 features, we could clearly see they were not intended
for beginners, and even if some were, a true beginner might struggle to
understand them. We wanted to change that and offer a true beginner guide to
C++20.

This book was written using a practical approach, so you can get to know
C++ 20, and create practical programs (as opposed to exercises) using your
new skills. When writing this book, we did not take anything for granted –
we explained the ‘how’ and the ‘why’ as much as possible, and we tried our
best to provide our unique perspective and use creative and fun ways, so you
can really understand C++, not just know it. We also wanted to write a book
which gives readers hands-on advice, tips and tricks, which can be useful
when writing real life programs, rather than just “school assignments”.

This book was born after each of us approached C++ 20 from a different
angle: Michael, having over 30 years of experience in programming in C and
C++, trying to make the most of the C++ 20 new features in commercial
products such as a our Anti Malware engine, a new Anti-Virus (which uses
our engine), along with other Cyber related products he was working on, and
Ruth, with over 25 years of experience in the software industry, who just
loves teaching while inventing new teaching methods. We started discussing
the need for such book while juggling among complex software projects we
were working on as part of our software venture, Secured Globe, Inc. in New
York. During that time, the Covid 19 pandemic spread around the world, but
the curse turned to be a blessing: lockdown gave us plenty of time to work on
our book.

Throughout this book, we will be covering basics and advanced features of


C++, as well as some basic concepts in computer science and computer
programming in general. Obviously we could not cover everything, but we
did try to build a solid root foundation for you to continue building your
skills upon. By the end of this book, you will be able to write a very practical
and complex C++ code as part of the book’s final exercise: a code that can
safeguard a computer program from an intellectual property theft by hackers,
by turning a code unintelligible.

We enjoyed writing this book, and we do hope you will enjoy reading it. We
value feedback from our readers and take that feedback very seriously. We
will be both looking forward to hearing your feedback, which you are
welcome to leave in the comments section in the liveBook Discussion forum.
Our book is a dynamic creature which will continue to be finetuned based on
readers’ feedback, and hopefully, together with your help, we can craft this
book even further, turning it into a true asset for beginners.

— Michael Haephrati and Ruth Haephrati

about the authors


Michael Haephrati, Co-Founder of Secured Globe, Inc. is an inventor,
musician, entrepreneur, and a C++ expert, specializing in software
development, vulnerabilities research and Cyber Security. Over the last +30
years, Michael has developed a unique perspective and methodology,
combining core technologies, creativity and functionality with the end user
experience. Back in 1989, Michael developed Rashumon, the first
multilingual graphical word processor for the Amiga. Later, he developed
various software products and technologies, especially in the field of Video
Processing, Cyber Security, Data Cleansing, Data Leak Prevention, DRM,
and high-end solutions, for government agencies. Recently, he developed the
next generation anti-virus and Malware Detection Technology, used by
leading tech companies such as Avaxi LLC. Michael also took part in starting
a Web3 based Social Ecosystem. Most of his work is developed using C++.
Michael regularly writes in Code Project, Microsoft blogs and Pentest
Magazine. He is also a music composer and pianist, who likes traveling and
long-distance jogging.

Ruth Haephrati, Co-Founder of Secured Globe, Inc. is an entrepreneur and


a cyber security and cyber intelligence expert with vast computer forensics
skills. Ruth started her career back in the early 90s and has since taken part in
various projects and emerging technologies, starting with Windows based
applications, Web application, Computer vision, IOT, Deep learning and
more. During her career, Ruth has designed and developed innovative
technologies, including some military grade solutions. She recently also led
the R&D of a new and innovative Cryptocurrency marketplace. She is also a
painter, photographer and loves jogging.

In this book

Copyright 2023 Manning Publications welcome brief contents 1 C++ is


brilliant! 2 Exploring C++ fundamentals 3 Smooth operator –exploring C++
operators and conditions 4 Let it flow – Conditions, iteration, and flow
control 5 Hip hip array – C++ arrays 6 Vectors – your arrays "on steroids" 7
With strings attached - working with string literals 8 Function in action
1 C++ is brilliant!
This chapter covers
Learning why C++ is great
Considering spoken vs computer languages
Discovering the reasons to learn C++
Getting into the correct mindset for programming and C++ in particular
Exploring the book’s approach and expectations

C++ is alive and kicking and is considered by many to be one of the most
powerful and robust programming languages across all platforms. C++ plays
a constant key role in evolving technologies of all sorts – from mobile
applications, web applications, communication and networking, desktop
applications, and up to the most complex and demanding projects, including
the human genome project, fundamental physics research at CERN, and the
Mars Rover. Everywhere you look, C++ fingerprints are all over it. To use
the words of Bjarne Stroustrup, the creator, and designer of C++, " I find it
most gratifying that C++ has been used in science: the human genome
project, the fundamental physics research at CERN and elsewhere, and the
Mars Rovers. It feels great to have contributed, however small".

With its versatility and proven track record of success, it's no wonder that
C++ remains a vital and essential tool for software developers today, and this
book intends to help you start your own journey with C++.

1.1 There’s more to C++ than you can C


Despite being considered one of the most complex programming languages,
C++ is not only highly sought after, but also extremely rewarding to master.
With over 30 years of use and a growing community of more than 5.4 million
C++ programmers worldwide[1], it is considered a stable and enduring
language that continues to grow in popularity. While there may be easier
programming languages available, none of them offer the same level of
robustness, performance, and capabilities simultaneously as C++. This is why
C++ remains a valuable tool for developers seeking to create high-quality,
reliable software. If you're up for the challenge, learning C++ can be an
incredibly rewarding experience that will open up a wealth of career
opportunities.

One of the unique features of C++ is its ability to handle both low-level and
high-level programming tasks. Low-level programming involves working
closely with the hardware, using languages that are close to machine code.
High-level programming, on the other hand, involves writing code that is
more ‘abstract’, simplified, and closer to the natural language we use in
everyday communication. C++ can bridge the gap between these two levels
of programming, allowing developers to work with both low-level and high-
level elements simultaneously. This versatility makes C++ a popular choice
for a wide range of projects, and it is therefore often referred to as a general-
purpose language.

Note

The position of several programming languages in relation to the actual


hardware is explained and illustrated in Appendix A.

With C++ you can craft your code and refine it to the extent of controlling
and micro-managing the computer's performance. C++ code can also be
written in so many styles and approaches to serve almost any use case.
Taking this even further, C++ is like having invented your own programming
language: you can create your own commands, style, and code flow, having
full control, flexibility, and power in your beautiful coding hands.

Good to know

C++ uses 95 keywords. Keywords define one and only one meaning to a used
word within your C++ code; for example, the keyword “break” means exiting
a specific part of your program and moving to the next, while the keyword
“continue” means continuing from a specific part of your program. There are
other examples of keywords in C++, such as "if", "else", "while", "for",
"return", "class" and many more, (and you will learn and know them all
very soon). These keywords have a specific meaning in the language and are
used to perform certain tasks in the program. C uses only 32 keywords, while
Python uses 33, and Golang only uses 25.

1.1.1 The peeled onion principal


Some say that the freedom C++ offers comes with a price: Complex and
flexible programs can be more prone to mistakes and bugs, which can cause
your program to crash or behave unexpectedly. This is something to be
mindful of when working with C++, especially if you are new to the
language. Writing complex code can be a bit like peeling an onion - each
layer you uncover makes you cry a bit more. It will be fair to say that, as
much as it is important to understand how to write code in C++, it is also
important to understand how not to. However, there are tools and resources
available to help you navigate these challenges, and in this book, we try to
point you to best practices and potential pitfalls to avoid. Modern
development environments also offer a range of features and tools that can
help you manage your code, prevent mistakes, and identify problems as they
arise. And of course, practice is always the best way to learn and improve
your skills. Remember: mistakes are your best tutor - don't be afraid to make
mistakes, as they are often the best way for you to learn and grow as a
programmer.

1.1.2 Is C++ actually awesome?

Some experienced C++ programmers, particularly those who have been


working with the language for many years, have expressed their criticism
about modern C++, comparing it to the legendary figure of the ‘Golem’ - a
creature that rose up against its creator (also known as “The Golem effect”).
Some professionals even abandoned C++ and went back to good old C.

This criticism has even spawned its own Wikipedia page[2], which outlines
the various concerns and objections that these programmers have with the
direction that modern C++ has taken.

To understand the criticism, let's go back to the early days of C++, as it is a


programming language with a long and storied history, having been
developed in the 1970s as an extension of the C programming language.
While C++ has undergone many changes and updates over the years, it is still
based on many of the same underlying principles and features that were
present in its predecessor C. This can be both a strength and a weakness, as
the language retains much of its power and flexibility, but it also still includes
older features that may be unsafe or outdated in modern programming
environments.

One way to think about C++ is as an ancient city with old roads and
infrastructure that were never designed for modern vehicles and technology.
The language was created at a time when developers had to work with 1MB
of memory and low computing power, while today computers use no less
than 8GB of memory and robust computing power. Initially, C++ was not
designed to take advantage of the much larger and more powerful computers
that are available today. While C++ has been adapted and updated to meet the
needs of modern programming, it is still, to some extent, based on
foundations that were laid down decades ago. This can make it more complex
and challenging to work with than newer languages that were designed from
the ground up with modern hardware and software in mind.

If we put aside specific complaints, most of the criticism relates to the


complexity and overhead modern C++ may suffer from. While some of these
complaints and criticisms might be true, let's remember three key factors:

1. C++ has stood the test of time and is here to stay, despite some
criticisms that have been leveled against it. While it is true that C++ can
be complex and heavy at times, with a lot of overhead and baggage from
its long history, it is also a language that has evolved to meet the needs
of a wide range of technologies and applications that were not even
imaginable when it was first developed. This adaptability and versatility
are something that should be recognized and appreciated, even as we
acknowledge the challenges that the language presents.
2. Modern C++ was introduced in 2011, more than 25 years after the
original version of the language was first released. While it has been
updated and improved to meet the needs of modern programming
environments, it is still designed to be compatible with older systems
and hardware, as well as less complicated applications. This can
sometimes make it feel cumbersome or awkward to work with and can
be perceived as being overly complex or annoying at times.
3. The newest C++20 brought together some brilliant minds to rethink and
redesign the language, to provide practical answers and solutions to
some fundamental features and issues C++ was carrying since its early
days. The result of their efforts was a set of comprehensive changes and
modifications that represented the most significant overhaul of the
language in its history. These changes are designed to make C++ more
practical, efficient, and user-friendly, and they will continue to evolve
and expand with the release of future standards such as C++23. The way
we see it, C++ is headed in the right direction.

To sum up, even if some programmers feel that modern C++ has strayed too
far from the roots of the language and has become too complex and
unwieldy, it is only natural for any programming language to evolve and
change over time. It is also important to note that these criticisms are by no
means universal, as millions of C++ programmers continue to use and enjoy
modern C++ without any issues.

1.2 Spoken languages Vs programming language


C++ is a human-to-machine language, a second language to over 5.4 million
people worldwide, with commonalities from the world of computer science.
As computers do not understand "humans", we need to use a language which
a computer can "understand". Computers "speak" binary, which is basically
just an input and output constructed of 0s and 1s. Humans, on the other hand,
would carry a heavy burden if they were forced to write complex machine
codes using only the 0s and 1s – it would be extremely difficult to write and
maintain. To better understand, let's look at Figure 1.1, which shows what the
English alphabet looks like in binary.

Figure 1.1 The way the English alphabet is represented in binary.


Now, let's look at Figure 1.2 which illustrates machine code written in
Assembler (on the right), which is hard to understand and maintain, and the
same code written in C++ (on the left). In fact, you don’t need to know C++
to see that the C++ code looks structured more like an understandable set of
instructions compared to the machine code.

Figure 1.2 A small sample of C++ code and how it looks like had it been written as machine code.
Note

The binary assembly code in Figure 1.2 is specific to the machine


architecture and operating system it was compiled for, and might not be
directly executed on other systems without modification.

good to know

Machine code is typically written in assembly language, which is a low-level


programming language that is specific to the architecture of the processor.
Assembly language provides a human-readable representation of machine
code instructions, using mnemonics to represent the individual instructions
and operands.

Moving on, and since it would be impractical and inefficient for humans to
write complex machine code using binary, we use programming languages
such as C++ to write code in a way that is more natural and humanly
readable.
Once our code is written, it is passed through a special program called a
compiler, which converts the code into a form that can be understood by the
computer. This process is necessary because computers are not able to
directly execute code written in a human-readable language such as C++.
Instead, they require the code to be translated into binary instructions the
computer can understand.

1.2.1 Language Evolution: From Grunts to Code


Understandably, as computers become more complex, fast, and strong, and as
our world becomes increasingly reliant on technology to support this
complexity (for better or worst), it’s only natural for a programming language
to evolve in parallel with technology itself. However, this might sometime
lead to unwanted complexity.

In the science of linguistics, language complexity can be characterized as the


number and variety of elements and the elaborateness of their interrelation
structure[3]. You might be surprised that we mention the science of linguistics
in a programming book, but there is, in fact, a strong correlation between
linguistics and computer science, which started back in the 1940s and 1950s,
when computer science just started to emerge. The rule of thumb is that every
language aspires towards simplicity, and so is C++.

In human history, the most ancient spoken languages which are still spoken
today, evolved into a simplified form, often becoming simpler and more
efficient over time. Take ancient Greek for example it evolved into modern
Greek, which is simpler and a more streamlined language. Another good
example is ancient Hebrew, which is considered to be a very complex
language. Ancient (or biblical Hebrew), has evolved into modern Hebrew - a
simpler and easier form of the language.

Compared to ancient languages like Hebrew or Greek, English is relatively


young and less complex. For instance, in English, we only have one pronoun
"you" to refer to both singular and plural subjects, while in Hebrew and
Arabic, there are four different pronouns for each subject, depending on
gender and number.
Essentially, modern languages tend to be simpler and more efficient than
older ones. If you need further proof, look around you: people started
“speaking” using emojis, and according to linguistics, it is a natural evolution
of the way people might be speaking in the future[4].

Programming languages evolve in a remarkably equivalent manner to spoken


languages: they aspire towards simplicity and abstraction. In the most basic
form, you want to "talk" to a machine and control it, you cannot do it in
binary - you need to use "words", expressions, mathematics, and logic. If we
look at the changes C++ had throughout the years, we can clearly see that
abstraction is the major one. Furthermore, C++20 generated an even greater
simplification of the language, turning it into more user (or coder) friendly. In
future versions of C++, it will continue this path.

1.3 All the good reasons to learn C++


Mastering C++ is a great root foundation and a valuable investment. The
knowledge you will gain from learning C++ can be implemented across many
other programming languages. The influence C++, (which was originally
written in C), has on other programming languages is huge. How do you
think C# was written? C++ of course!

Another great benefit is that when you use C++ as a programmer, you have
the power to deliver impressive performance and, at the same time, handle
complexity with grace. Since C++ can run on all hardware types, it gives you
the power to choose your passion and professional path. Do you want to
develop games? Networks? Mobile apps? Financial applications? Medical
applications? Imaging? Artificial intelligence? Wearable devices? The Mars
Rover? The list goes on and on, and C++ is the golden ticket to most of those.
There are, of course, other programming languages around which can also
open up these worlds for you; but, in many cases, C++ gives you more
control and speed over the alternatives.

One of the key strengths of C++ is its ability to be used as both an operating
system-specific language and a platform-independent language. When used
as an operating system-specific language, C++ provides libraries, APIs, and
features that allow developers to interact closely with the underlying
operating system, accessing platform-specific functionalities and resources.
This level of system-level control is particularly beneficial when building
applications that need to directly interface with the operating system,
leverage its unique features, or take advantage of hardware-specific
optimizations. On the other hand, code written in C++ can be designed to be
platform-independent by relying on standard libraries and adhering to cross-
platform coding practices. This flexibility makes C++ an excellent choice for
developing applications that need to run across different operating systems
without significant modifications.

It is also important to mention the emerging popularity of C++ and the


growing demands for C++ professionals. This is one of the reasons for the
growing and growing strong C++ communities, programming forums, and
open source (free source code). GitHub, for example, is an open-source hub
with a lot of free code samples. Stack Overflow offers articles, free code,
Q&A, and much more, and the C++ alliance offers great resources and
support for the growing C++ community. As you start your programming
career you will most definitely encounter these platforms, and you will
always be able to post questions, download code, share, learn, and become an
active valuable member.

1.3.1 C++ == Salary++


If we are talking about your career, C++ dominates almost all industries. The
list of game-changing applications and technologies which were developed in
C++ is long and impressive. Software like Adobe Photoshop and all other
Adobe applications, Amazon AWS, PayPal, Oracle, SAP, Google, Firefox,
MySQL and so much more were written in C++. And the list goes on and on.
Over 1,200 major IT companies are using C++, and this speaks for itself.
Mastering C++ is a real career booster as C++ is literally everywhere. Many
employers consider C++ a great advantage when candidates master more than
one programming language - C++ gives you an edge, as it proves you have a
strong set of capabilities that those who do not know C++ might lack.

As the common saying goes, "C++ == Salary++” because there is indeed a


growing need for C++ programmers, with average salaries ranked among the
ten highest in the industry. Start from +$65K for a C++ beginner, going up to
around $118K a year[5]. And no -C++ is not the highest-paying programming
language, but it is always ranked somewhere on top. A programming
language's ranking usually shifts and fluctuates, but C++ manages to stay
stable on the high end of the salary table. Figure 1.3 shows the most in-
demand programming languages for 2022 according to CodingNomads,
which ranked the most popular and rewarding programming languages based
on LinkedIn job postings in the US and Europe. The graph awards Python the
top spot, followed by Java, JavaScript, C++, C#, C, TypeScript, PHP, Perl,
and Ruby, which closes the top 10 list.

Figure 1.3 C++ has one of the highest paying salaries when it comes to similar options such as C,
Java, and Javascript.
Though there are other programming languages that pay more than C++, bear
in mind that languages such as Python, Java, and Java Script are mostly
designed for high-level programming, while C++ is used for both high- and
low-level programming. It is also important to note that the salaries
demonstrated in Figure 1.3 are average salaries for mid-level programmers,
and experienced C++ programmers are known to earn even twice as much.
Overall, while salary is certainly an important factor to consider when
choosing a programming language to learn, it is only one of many factors,
and it is worth considering the long-term value and potential of a language as
well.

1.3.2 Sometimes C++ is the less appropriate choice


While C++ is a powerful and versatile programming language that is useful
in a wide range of contexts, it may not always be the best choice for certain
types of projects, such as web programming or the development of
lightweight applications. If your primary focus is on web development or
front-end development, you might find it more practical to learn a language
like JavaScript, PHP, Java, Python, or C#, which are specifically designed for
these types of tasks. Some programmers who want to delve even deeper into
the world of programming may choose to learn C instead of C++, as it is a
lower-level language that provides a greater level of control over the
hardware and operating system. However, for the reasons outlined
previously, C++ is an excellent choice.

1.4 C++ 20 - in the name of simplification


As already mentioned, C++20 (and C++ 23 to come) encapsulates some of
the most powerful and key features and modifications are seen so far and is
the next generation of modern C+, representing a major step forward in the
evolution of the language. C++20 includes some fantastic powerful and
innovative features that are designed to simplify and streamline the process of
writing code, while also making it more expressive and easier to read. As old-
timer C and C++ users, we were thrilled with some of the new features,
which drove us to write beautiful codes in half the effort.

If we look at the evolution of modern C++ across all versions (starting from
C++11, C++14, and C++17), we can definitely say that C++ 20 encapsulates
some of the most profound core changes, not only in the sense of
simplification but also in the way it allows you to structure your code, as you
will learn in this book. In a way, C++ 20 is considered a game changer.
In this book, we aim to provide a comprehensive introduction to the new
features and improvements in C++20. We not only explain what these new
features are and how they work, but we also delve into the reasons behind
their inclusion in the language and how they can be used to make your code
more efficient, expressive, and maintainable. To help you understand the full
extent of the changes in C++20, we sometimes provide comparisons with
previous versions of C++, so that you can see how the language has evolved
over time.

While some of the new features in C++20 are quite advanced and require a
deep understanding of the language, we provide additional resources and
references at the end of the book for those who want to learn more about
these more complex features.

1.5 A C++ state of mind


Learning the fundamentals of C++ is relatively straightforward, but some
advanced features can initially appear intimidating. When embarking on your
C++ learning journey, it's essential to grasp the logic behind the syntax,
statements, and structure, and how they work together. Hold on to these
concepts, especially when dealing with complex code that may seem
nonsensical at first.

It's no secret that there is a lot of 'brain power' which needs to be invested not
only when learning C++, but also when using it in real life. Hair pulling is
another common C++ side effect. While other programming languages, such
as C#, JavaScript, or Python are easier to learn and practice, C++ does not
give you any discounts...

In terms of building things, C++ is more like constructing a piece of furniture


from scratch, while programming languages like Python or Java are more like
purchasing a pre-made piece of furniture from IKEA only in need of
assembling. The key difference is that with C++, you have the option to do
either one.

In other programming languages, there is often a tradeoff between simplicity


and control.
If you are into full (or more) control over what is performed at the lowest
level, C++ is one of the best choices, and with that control, you can always
add layers of functionalities on top of that lower level, giving you simplicity
and easiness as well. More simple programming languages might allow you
to write code faster, think less, and worry less, but they have specific limits.
It is like typing using readymade sentences which were pre-prepared for you,
rather than in your own words.

Remember

Programming provides you with a set of capabilities and tools to convert


tasks and ideas into computer programs. These capabilities and tools are wide
and different, allowing you to choose your preferred way of implementing
what you want a computer program to do. C++ gives you an even wider
range of options. This book exposes you to these capabilities and, with a
hands-on approach, teaches you how to become a programmer, how to
choose the path you wish to go when you start working on a task or an idea,
and assures you that you have what it takes to make the right choice, keeping
in mind that the choice is yours.

1.6 Our approach


In this book, we take you on a practical and hands-on journey through the
world of modern C++20. Our goal is to not only teach you the fundamental
concepts and syntax of the language but also to give you a deep
understanding of how C++ works and why it is structured the way it is. We
use a step-by-step approach that starts with the basics and gradually builds up
to more advanced topics so that you can gain a solid foundation in C++ and
be able to apply your new skills to real-life projects.

Throughout the book, we use a variety of examples and exercises to help you
learn by doing, and we also provide explanations and insights into the "why"
behind the "how", so that you can gain a deeper understanding of the
language and its design principles. Whether you are a beginner programmer
or an experienced developer looking to learn C++, this book is an ideal
resource for gaining the skills and knowledge you need to succeed.
Developing strong logical thinking skills is crucial for success in
programming, particularly when working with a versatile language like C++.
The ability to approach problems systematically and find elegant solutions is
a key aspect of effective programming. In C++, you'll encounter various
approaches to problem-solving, but the most effective ones are often the
simplest and easiest to comprehend. Therefore, it's essential to hone your
logical thinking abilities and regularly practice applying them to coding
challenges.

The way we see it, programming is an art form - a creative and expressive
way of using technology to solve problems and build new things. Yet, many
programmers tend to focus solely on the practical aspects of coding and
overlook the artistic and philosophical side of it. In this book, we aim to
highlight the beauty and creativity of programming and encourage you to see
it as an art form in its own right.

In this book, we not only teach you the syntax and concepts of C++, but we
also provide exercises and examples that will help you develop your logical
thinking skills and apply them to real-world problems. By the end of this
book, you will not only have a solid foundation in C++, but you will also
have the critical thinking skills that you need to succeed as a programmer, or,
even “a code artist”.

1.6.1 Building a solid foundation

The truth is that mastering C++ requires time, dedication, and most
importantly, a lot of practice. In this book, we hand-craft your skills, as we
provide the guidance and resources you need to develop a strong foundation
in C++ and grow your skills over time. With our hands-on approach and
emphasis on understanding the concepts behind the code, you will be able to
craft elegant and effective programs that showcase your capabilities as a
programmer. This book will, hopefully, plant the seed from which you will
grow your C++ crafting skills. Once you get the hang of it and gain some
confidence, you will be amazed at the level of freedom and creativity C++
can offer you as a programmer, so you could literally create, innovate, and
build the most amazing programs imaginable.
In this book, we also guide you on how to design, write and develop
interesting and practical projects, which will teach you quite a lot and will
serve as a great foundation that you can use in your portfolio. So, whether
you are just starting on your journey as a C++ programmer or looking to
build upon your existing skills, this book is the perfect guide to help you
grow and succeed in your craft.

1.6.2 What we expect from you


As you embark on your journey to master C++, we encourage you to embrace
the challenge and have fun along the way. Learning a new programming
language can be a rewarding and fulfilling experience, but it is important to
remember that it takes time and practice to develop real expertise. Just as it
takes years to become proficient in a spoken language or to master a musical
instrument, it will take dedication and perseverance to become a skilled C++
programmer.

To ensure that you get the most out of this book and your learning journey, it
is important to stay curious and ask questions when something is not clear.
Ask "why" a lot - this will lead you to search for the answers which will,
eventually, lead you to a better understanding. Don't be afraid to dig deeper
and seek out the answers to your questions – this will help you gain a deeper
understanding of the concepts and techniques you are learning. And above
all, remember to enjoy the process and have fun as you grow and develop
your skills in C++.

1.6.3 What you should expect from this book


In this chapter, we introduced the basics of C++, a programming language for
both low- and high-level programming. We also laid out all the good reasons
to learn C++, along with the difference between C++ and other programming
languages. Moving forward, rest assured we have used our combined
experience to construct a great learning program that will help you
understand and practice C++ from the very basics up to solid skills. If you
never wrote code before, or have little to no knowledge of computer science,
worry not – we will explain every little concept, term, or methodology, and
will never take it for granted that you know or understand something unless
we thoroughly explain it. It is enough if you have elementary school math
skills to gain all the benefits from this book, and we are sure you will enjoy
reading and learning from it.

1.6.4 Making the best out of this book


Note that this book is aimed at those with NO previous knowledge or
experience with any programming language, or even knowledge of the basics
of computer science. This book is also suitable for those who have some
experience in other programming languages and are new to C++. All you
need to do is to follow this book chapter by chapter and practice a lot. You
can also find many great programming skill development sites online, with
coding exercises and quizzes, which can help you maintain and grow your
knowledge through hands-on experience. Learning C++ using a book is
greatly beneficial only if you practice writing real code, and we encourage
you to do so.

To get the most from this book, focus on the hands-on parts. Spend more
time using your selected development environment (IDE), and, during and
after each chapter, try to not only write the exercises in the book but also try
to invent your own programs based on what you have learned. Try to see the
fun in each ability and concept you will be introduced to - it will give you the
power to turn your ideas into a working program. The best advice we can
give you is to practice, code, and invent. Practice each topic you will be
learning. Exercises and code samples are part of each section; code as many
variations of what you learn, not only what you are asked to do, but more.
The more the better. Then try to invent other ways, other tasks, and other
programs using what you have learned. Try to combine different areas and
topics from different chapters. When you are working on something you have
just learned, try to think: what other things you learned before can be useful
now? The best scenario would be using the accumulated knowledge from all
previous chapters when working and practicing code based on the current
chapter.

This book will provide you with all the basic tools and know-how to start
your path toward becoming a great C++ programmer regardless of your
previous knowledge or experience in C++ and programming, in general.
1.7 Summary
C++ is one of the most powerful programming languages out there and
can be used for both low-level (closer to the computer's hardware) and
high-level programming (the actual visible software, application, or
website). C++ is used by almost every IT giant, and mastering it can be
a real career booster, including higher salaries.
C++ 20 is the most recent evolution of C++, introducing some brilliant
and powerful new features which can simplify your code. It is yet
another evolution in modern computer programming.
There are tremendous advantages to learning and mastering C++, among
which is the fact it is a powerful root foundation and a desirable skill for
any other programming language out there.
There is a tight correlation between spoken languages and computer
science, and the resemblance in aspiration towards simplicity and
abstraction, which C++ 20 brings us closer to.
C++ is not suitable for all programming use cases, and if you are
looking into lightweight programming languages, there are better
options.
Once you master C++, it can become a true form of art, with
unimaginable room for creativity.
Our journey begins here. Let's pull up our sleeves and start learning the
newest and most exciting version of C++!
[1]According to the Developer Economics survey by SlashData. See:
https://adtmag.com/articles/2018/09/24/developer-economics-survey.aspx
[2] https://en.wikipedia.org/wiki/Criticism_of_C%2B%2B
[3]
Rescher, Nicholas (1998). Complexity: A Philosophical Overview. New
Brunswick: Transaction Publishers. ISBN 978-1560003779.
[4]https://www.bbc.com/future/article/20151012-will-emoji-become-a-new-
language
[5] According to Indeed.com.
2 Exploring C++ fundamentals
This chapter covers
Reviewing basic programming concepts
Writing your first C++ program
Understanding principles of variables and memory allocation
Declaring and initializing variables
Learning about constant variables and when to use them
Understanding the concept of local and global variables

In this chapter, you'll learn the fundamental programming concepts, including


the role of punctuation, logic, syntax, indentation, and comments in writing
functional and aesthetically pleasing code in C++. We'll cover the basics of
computer programming and the compiler's function, which transforms your
code into machine language, and the IDE (Integrated Development
Environment). Additionally, we'll assist you in writing your first C++
program and interpreting every part of your code, allowing you to feel at ease
with your first C++ hands-on experience.

This chapter will also introduce you to variables and how they aid in
overcoming the limitations of human memory and computer storage. You'll
learn how to use variables in your code and how to avoid errors by adhering
to best practices. We'll also show you how the location of code statements
can have a significant impact on your program, whether locally (within the
scope of your code block) or globally (outside of the scope of your code
block). Finally, we'll teach you how to handle constant variables, which are
values that never change. Throughout this chapter, you'll write simple code
while gaining confidence in reading and understanding C++ syntax and logic.

2.1 Some basic concepts before we begin


Most if not all of the terms explained in this book were created to simplify
the computer's work, but we need to help the computer "understand" us.
From the computer's point of view, a program is just a very long list of
instructions written by humans, telling them what to do. Such a list can get to
millions of lines of code, and the goal of a programming language is to
structure and simplify these instructions and mediate us humans with the
computer.

This is why, on your path to writing and understanding C++ code, it’s
important to start with some basic programming and computer science
concepts, which are an inseparable part of programming in C++ (as well as
other programming languages). So, before we continue our journey, let’s first
go through some concepts in computer programming in general, and in C++
in particular.

Note

If you are familiar with some or all of these concepts, feel free to move on to
the next section.

2.1.1 Your operating system - The wizard behind the curtain


The operating system has traditionally been a crucial component in
determining the behavior and performance of programs, as programming
languages rely on various system resources like system calls, libraries, and
more to interact with the operating system. The OS serves as a mediator
between software and hardware, offering tailored services to programs for
efficient execution. However, with the advancement of technology, the
dependency on the OS is gradually diminishing. Online compilers are now
available that can compile and execute code without relying on the local OS.
Additionally, C++ 20 has made significant strides toward reducing the
reliance on OS-specific libraries (APIs) by incorporating more functionalities
into the language.

For instance, file handling, a crucial aspect of programming, can now be


achieved with just a single line of code in C++ 20, making it easier for
programmers to work with files without relying on the OS. As time goes on,
the OS's role in programming might become less significant, but it still
remains a vital component of the computing ecosystem.
2.1.2 Your Console / Terminal – because who needs the mouse?

An important term you may find in books, guides, and source code snippets is
“Console”, also referred to as “terminal” (In this book, we will only use the
term “Console”).

In the old days of the DOS operating system, all computers only displayed a
black screen with monospaced text, and all interaction with the computer was
done via typing a command to that screen and waiting for the result to appear.
This is also known as "input" (what you type) and "output" (what the
computer types back). Later, when the first versions of Windows and
Macintosh appeared, the graphics-oriented interface was born.

Today, most users will only use the mouse and keyboard to open windows,
drag, select, copy, and paste. Every child is familiar with these actions;
however, the old "black window" – a console, or terminal, is still used,
especially during the development of a program. Throughout this book, you
will be writing code that uses the console as the platform to display and run
your programs.

2.1.3 Shhh… libraries

In real life, we all know what a library is: a collection of materials, books, or
media that are accessible for use and not just for display purposes[1].
Computer programming libraries have a similar definition: they are
collections of prewritten code that users can use to optimize tasks.

When we program, we often need to perform repetitive or common


operations, such as input and output, calculation, reading and writing to a file,
searching, etc. C++ comes with a large base of prewritten code, called
libraries, which can save you the time and effort of programming these basic
operations yourself.

Libraries in C++ and other programming languages offer a variety of


routines, functionality, and definitions that can greatly benefit your code.
Instead of reinventing the wheel, you can use libraries to quickly and easily
access prewritten code. For example, a library for creating PDF files can
allow a program to generate and save PDF reports without the need to write
the code from scratch.

C++ standard library (STD) – the Crème De La Crème of the C++


libraries

C++ has many useful libraries that developers can use to build applications
for various use cases in the real world, but the Crème De La Crème of C++
libraries must be C++ standard library, also known as std – and you will see
and use std a lot throughout this book, including in the C++ code you are
about to write shortly.

The Standard Library offers a wide set of useful functionalities and methods
which can be helpful, using modern, easy-to-use, and efficient algorithms.
For example, the iostream library, which is part of the C++ Standard
Library, contains all the functionality for input and output streams. If you
include the iostream library in your code, you will not need to handle the
actual properties or any other code for handling input and output streams.

The C++ Standard Library is a huge subject on its own, and in this section,
we only introduce you to the tip of the iceberg.

Note

Another important and useful library in C++ is the Standard Template


Library (STL), which you will learn all about in Chapter 10.

Good to know

C++, and many other programming languages, provide the flexibility of not
only utilizing their built-in libraries but also allowing you to create and
design your own libraries. Developing a library is a skill that requires a good
grasp of programming concepts, and this book aims to equip you with the
necessary skills to create your own libraries further on in your career.

2.1.4 Header files – no need to reinvent the wheel


Header files contain pre-written code such as functions, declarations, and data
structures that are required by your program. These files are pre-processed or
pre-compiled, meaning that instead of writing the code from scratch, you can
simply include the header file in your program.

In fact, libraries in C++ come with several header files. There are framework-
related header files, such as <iostream>, which you just learned about, and
which we are about to use. When it comes to libraries, the header file is like a
map, that shows us which roads we can take in the library. Each element of
the library is declared or defined in the header file (and if you're not sure
what "declare" and "define" is – don't worry, we explain these concepts soon
enough), and each header file contains specific methods, functions, and
helpful capabilities you can use in your code.

For example, the header file <chrono> was added in C++ 20 and is now a
part of the C++ Standard Library. <chrono> offers date and time utilities you
can use in your code, such as clock, time points, and duration, and it was an
important and useful addition to C++.

C++ has many header files, which are part of various libraries, and we teach
about a lot of them in this book, but, obviously, cannot cover all of them.

To work with header files, we must use the #include preprocessor directive,
which tells the compiler to include the contents of a header file in the source
code before it is compiled. When you write your first code, you will use the
#include directive to include the <iostream> header file which will allow us
to print text to the console (output) and handle input – and we demonstrate
and explain it shortly.

Good to know

C++ allows you to use its built-in header files, or use your own header files.
Designing and writing header files is an important skill we cover later in this
book.

Note

Header files have some potential pitfalls, such as naming collisions and
dependencies between different modules of a program – and we discuss all
that in chapter 15. In Chapter 15 you will also learn about Modules, which
were added in C++20, and were designed to replace the way traditional
header files are used. However, it's important to note, that Modules are very
new and still very new, so they are not well embedded in the C++ language
yet, and in any case, understanding how traditional header files work can help
in understanding how C++ modules work, as they build upon and improve
upon the traditional header system.

2.1.5 Namespace – avoid name conflicts

The concept of a namespace is important and will be discussed throughout


this book, but in a nutshell, it's a way to better organize code and avoid name
conflicts. It's like having two kids named Max in the same class – when you
call Max, which Max should answer? but what if we call them Max L, and
Max B – the name conflict is resolved.

Going back to code, let’s say we have two libraries: One is called lib1, and
the other lib2. Both libraries use a function called calculate. If our program
needs to use this function – which library should be used? The compiler will
simply not know unless we define it explicitly. Namespace solved this
problem by attaching each name to a "space" – hence the name "namespace"
– it's literally a space for names and we use the library name as a prefix.
Think of a namespace as a container that holds variables, functions, and other
data types. If we take lib1 and lib2 as examples and use a Prefix, it would be
lib1::calculate, or lib2::calculate.

In our case, we use the C++ Standard Library, which is named std. This
means that to use a function from the standard library, we need to prefix it
with std::. For example, to use the cout (“callout”) function for outputting
text to the console, we need to write std::cout – and we explain more about
that in a second.

2.1.6 keywords
In Chapter 1, we mentioned that C++ uses 95 keywords, which are pre-
defined words that have a specific meaning and purpose in the C++
programming language. These words are reserved by the language and cannot
be used as identifiers for anything else, but the intended meaning is C++
intended. It's also important to spell the keywords correctly. For example, the
keyword int, which represents an integer value, and which you will use in
your first code, will not work if it is spelled as Int. There is no need to
memorize all 95 keywords, and most programmers don’t know them all.

2.1.7 The important role of your compiler


Up to this point, we talked about a programming language, which is in a
more "human" form, and machine language, which is what the computer can
work with, while humans find it harder and non-friendly. What we didn't
really talk about is how programming language converts into machine code.
The process of converting human to machine code is actually a complex
procedure under the responsibility of the compiler.

A compiler is nothing but a program that converts human-readable source


code into machine code, which is a binary code that computers can
understand and execute.

Good to know

The process of converting high-level code into low-level code, or human-


readable code into machine code, involves three main steps: tokenization,
parsing, and code generation. In Appendix B we further explain these
concepts and terms, and we recommend that you will learn them, as
understanding the role of the compiler and how it converts source code into
machine code is essential for any programmer.

2.1.8 Your IDE - your power tool


Your IDE is your kitchen. It's where you boil, spill, steam, and stew some of
your best ideas and brilliant programs. The IDE itself, on the very basic
notion, contains an intelligent text editor, or a software environment you use
to write, edit, debug, test, and run your code. Some IDEs can interpret what is
typed out. Some IDEs display the text in different colors to represent
different components, which makes it easier to maintain the code on a visual
level. For example, Visual Studios offers a feature called IntelliSense, which
predicts what you are typing and auto-completes it. Some other IDEs offer
similar features.

Your IDE will also contain your compiler. Once you want to execute your
code, the IDE’s compiler interprets it and converts it into machine language,
based on the steps you just learned in the previous section.

The role of the linker

Another important part of your IDE is the linker. The linker is what produces
the final compilation output from what the compiler produced. One of the
components the linker links with your program is libraries. As you learned, a
library is a set of pre-written commands, functions, and other methods to
handle various use cases in your code.

Libraries can either be static or dynamic, and it's important to understand the
difference between the two. Static libraries are linked at compile-time,
resulting in a larger executable file that contains the library's code. On the
other hand, dynamic libraries are linked at runtime, allowing the executable
to load the library's code only when needed, resulting in smaller executable
files. However, in C++, only static libraries can be linked, which applies to
all operating systems. The linker ensures that all necessary components,
including static libraries, are linked to the right objects, making it possible to
generate an executable of our program. If the correct component isn't linked,
compilation might be successful, but linking will fail, making it impossible to
generate the final output.

Which IDE is good for me?

It is important to note that we recommend using Visual Studio (VS), a


popular and powerful integrated development environment (IDE) widely
used by most developers, and which supports Windows and MacOS (and if
you use Linux, VS code is the IDE for you). In our experience, Visual Studio
has proven to be very stable and effective with many benefits, such as easy
navigation between files, accurate coding with built-in code assistance,
rigorous testing capabilities, and customizable options.
Additionally, VS supports almost every major programming language,
making it a valuable tool for those interested in learning multiple languages.
By learning how to use Visual Studio from the beginning, you will be able to
easily transition to other programming languages supported by the IDE, such
as C#, Python, JavaScript, Java, and TypeScript. While some programmers
also use Visual Studio Code (VS code), which is also a good choice. You are
welcome to use either one.

Last but not least, using VS can help you develop your skills as a C++
programmer and prepare you for real-world development where many
companies and employers use Visual Studio as their primary IDE.

2.2 Ready, set, code: your first C++ program


The first step in writing your first code is creating a new project. A project
contains all the files you need for your programs (code). We’ll use
Microsoft’s Visual Studio to create the project and to write the code, but you
can use any other IDE, some of which are free IDEs you can find online (just
search for "free C++ IDE"). Another option is to use Visual Studio Code, also
known as VS code, which is widely used.

You can download a free version of Visual Studio[2].

You can download a free version of Visual Studio Code[3].

As you learn, you will start to get used to your IDE, and you will also learn
how to use it to write, edit, view, and run your program. If you need help
setting up your IDE, there are plenty of online guides and instructions to help
you out.

Note

Instructions on how to set up your Visual Studio IDE and open a new
console/terminal project can be found in Appendix C.

2.2.1 Start writing your code – write basic and simple code in
C++
The program you are about to write will ask the user to rate this book. The
user will have to input his/her rating (a number between 1-10), and then the
program will display an output of a message together with the user's rating.
Before we start to write the actual code, let’s look at the program’s flow first
(Figure 2.1):

Figure 2.1 The flow of our program is simple: the user is required to input his rating of the book,
he types a rating between 1-10 and we then print the rating to the console.
Now, let’s look at the code you are about to write (figure 2.2), exactly as you
should type it into your IDE. In the next few sections, we will dissect each
and every component and explain how and why you should write your C++
code this way.

Figure 2.2 This is what your first C++ code will look like. We numbered each part and we go over
each numbered element in the next few sections.

2.2.2 Step 1: Include the <iostream> header file


To enable input and output functionality in our program, we need to include
the <iostream> header file in C++. This file contains all the necessary code to
handle input and output operations. By including this header file, we are
utilizing the built-in features and constructs of the C++ language to simplify
our programming tasks, saving us the hassle of dealing with input and output
streams ourselves.

As you may recall, whenever we want to include a header file, we must first
type #include, so unless the iostream header file is a default part of your
IDE, let's type outside (above) main() :
#include <iostream>
Note

In case your IDE displays a default "hello World" output statement, just
remove it from the code.

Good to know

there are two ways to include a header file: one is using the angled brackets,
for example, #include <iostream>, and in this case, the compiler searches
for the header file in the system directories where the standard C++ libraries
are installed and in addition, the directories you specify. The other way to
include header files is using double quotes, for example, #include
“my_header.h”. In this case, the compiler searches for the header file in the
current directory where the source code is located. In other words, the
difference between using angle brackets <> and double quotes "" is the search
path used by the compiler to locate the header file.

2.2.3 Step 2 – Know the main() function – your programs’


entry point
The main() function is where you place the first instructions you wish to be
executed. Since we normally want to use the help of the IDE and the
Operating System, the main() function is your entry point, and where we
jump in and add our code, but it's not where the program actually starts[4].
What happens behind the scenes stays behind the scenes

It's important to note that when writing a program, certain preparations are
made before the main function is executed. These preparations, which are
done behind the scenes, involve the initialization of various components that
depend on the specific program and IDE being used.

Think of it as turning on your computer - there are many preparations that


happen behind the scenes before you can start using it: the operating system
loads, the drivers for various components (such as the display, keyboard, and
mouse) initialize, and various background processes start running. You don't
need to worry about these preparations - you just need to wait until
everything is ready, and then you can start using your computer.
As a programmer, you don't need to worry about the preparations behind the
scenes of your program and can focus on writing the main function and the
rest of your code.

Once some behind the scene preparations are done, your program calls the
main() function, where your portion of your code and logic are placed.

Once the main() function ends and the program has finished its course, some
additional work is done in the background, which you are blissfully unaware
of as well, as it is just cleaning up and shutting down, freeing resources, and
marking the program as completed. You can think of it like turning off your
computer - the operating system runs some additional tasks in the
background, like closing all open programs and files, saving any unsaved
data, and shutting down all system services.

This entire process is illustrated in Figure 2.3.

Figure 2.3 The flow of events when your program runs. Behind the scenes some preparations are
made, depending on your program and IDE, then main() is called and your code is executed.
Once main() has finished its course, in most cases, some cleanup is done behind the scenes.
Note
Preparations and the way the program work in the background once it
finishes its course, depend on the operating system, as well as your IDE,
which might have some additional overhead.

Step 3 –use a variable as a "storage unit"

The next step in writing your first program is to create a storage location to
store our input or output values. The values will be stored in the computer's
memory, and we use variables to give this memory location a name.

Tip

To understand the concept of variables, think of a room with different shelves


where you can store different items. Each shelf can be assigned a name, like
"books," "clothes," or "electronics." These names serve as identifiers for the
shelves, just like variables in programming serve as identifiers for values that
can be stored.

In our case, the variables will represent the location of the value which the
user will input. If the concept of variables is confusing – don't worry, we
explain all you need to know about variables later in this chapter.

The first variable will be an integer (int) type. Integers (int) handle
numerical values, and since we ask the user to input a rate between 1-10 it’s
best to use a variable of int type.

We also want to give our variable a meaningful name, so let's call it rate.
This name makes sense since our variable rate will represent a rating value
between 1-10.

So let's type inside main() and under the last line (figure 2.4).
int rate;

Figure 2.4 Start writing your code inside main() and declare your variable 'int'
Note

The part in the code that starts with // and is colored in green is a comment.
A comment is not a real part of your code, but rather a reference for your
future self and others who might work on the code. We talk a bit more about
comments later in this chapter when we go through some of the C++
language building blocks.

The next step is to always end a statement with a semicolon ;. The


semicolon; is a statement terminator. It marks the end of a statement,
indicating that the current statement has ended and that the next statement
begins.

If you don't declare the end of the statement, your program will not compile.
Most IDEs will alert whenever there is a punctuation issue, and your code
will not compile. Your code will have a red line indicating a mistake, similar
to how Microsoft Word highlights typos in red. Below (figure 2.5) is an
example of a highlighted punctuation error in the code we are writing:

Figure 2.5 Your IDE will alert in case there is an error of any sort
As you can see in Figure 2.5, the std:: has a red error marker, indicating
there's no; in the statement int rate{}. The compiler thinks that the 2 lines
are 1, and is therefore compiling your code as if it was all 1 line instead of 2.
Think of it as being like a sentence where you forget the full stop. The next
sentence will not make sense.

Tip

Forgetting to place a semicolon is a 'childhood disease' many beginners to


C++ suffer from. Don't worry, soon enough it will become second nature.

Steps 4 and 5 – handling the user's input

Now we want to print something to the console asking the user to type his
book's rating. We already mentioned we are using a pre-built header named
<iostream>, which manages inputs (what the user types in) and outputs
(what the program prints out).

Now we need to use input and output streams. To use these streams, which
are part of the <iostream> library, we must use two statements: cout and
cin. Think of it as a short for "call out" and print something to the console
(cout - output), and "call in" asking the user to input something (cin – input).

Note

There is a certain way to use std::cin and std::cout statements in our code
and we must use the correct punctuation.
Good to know

In C++ cin actually stands for “character input”, while cout stands for
“character output”.

Using std:: before the cin and cout statements

std:: stands for "standard library" you just learned about. As explained,
libraries are a type of program that isn't intended to be used by software
users, but instead to be used by other programs. It's like the engine behind
your code. As explained, a program can use as many libraries as needed,
while each static library is "linked” to the program adding to it the abilities
this library provides using the linker.

As explained, since we are using C++ Standard Library, std, we must add
std:: before the cin and cout statements, as they are part of the std. Don’t
worry about the double colon ::, you will learn why it’s there later in this
book. When we type std::cout for output and std::cin for input, the
compiler will know that the cin and cout we will use are part of the standard
library.

Using '<<' (for output) and '>>' (for input)

Many beginners refer to them as "less than – less than" and "more than –
more than" signs. The << and >> are in fact operators. You will learn more
about operators in the next chapter, but in a nutshell, operators are symbols
that tell the program to perform specific operations. In this case, the << and
>> operators are part of the cin (the syntax for input) and cout (the syntax for
output). Confused? Let’s try and look at these operators from a different,
more figurative perspective: think of the << and >> operators as if they were
a megaphone. When you cout (call out) the megaphone is positioned
outwards, while with cin, it will be positioned inwards (figure 2.6).

Figure 2.6 using cin and cout must be accompanied by the << and >> operators. Using the
megaphone illustration, you might find it easier to remember which operator represents which
stream.
So let's type:
std::cout << "How would you rate this book so far? Please rate from 1-10" <<
std::cin >> rate;

As you can see, we also use quotation marks for the output statement. You
might have also noticed another part of the syntax: std::endl. endl means
end line. Placing std::endl at the end of the statement means the statement
will end in this line, and the next statement will appear in a new line. Don't
forget to use the semicolon sign; at the end of each statement.

We know it all may seem like a lot to learn and remember, and it might even
feel confusing, as it might seem like a strange structure and syntax, after all,
this may be your first attempt at a C++ program, but fear not – you will get
used to the way C++ code is written and soon it will feel natural to write and
read.

Note

there is another method to end a line using \n and we will talk more about
this method in the next chapter. However, throughout this book, we will use
std::endl.

Tip

Most IDEs offer hints and code completion to assist the user with
recollection and features, such as auto-complete, and many more. This can be
extremely valuable for both beginners and novice programmers.

You can see that we are using our variable rate for the input of the user.
When the code is executed, the user will be prompted to enter a value and
that value is assigned to the variable.

Why do we need quotation marks when adding textual output?

It's important to remember that any text that we want to display on the
console must be inside quotation marks. In our code, we wrote: "How would
you rate this book so far? Please rate from 1-10". Why must we
use quotation marks? Well, that’s a great question.

In our program we use strings. You will learn more about strings in Chapter
5, but, in a nutshell, let's refer to strings as a sequence of letters, or words.
Since our code is also written with letters and some words, the compiler
needs to tell the difference between the letters we use as part of our code, and
which are part of C++ and strings of our own making. Wrapping strings in
quotation marks lets the compiler know we are handling strings, rather than a
variable or keyword in the code.

Remember

quotation marks must always be used when we create a string literal output
statement.

Step 6 – let’s print something to the console

The next step after we have the user’s input is to display some sort of output.
The output can be anything we like, and it can also include the user’s input,
as it does in this case:
std::cout << “ You rated this book as “ << rate << “ , thanks for
your feedback!” << std::endl;

Let’s look at the code. We have the std::cout first, so we use an output
statement. Then we have the << operator, leading the actual string literal used
in the output statement, which, as explained, will always be in quotation
marks. Now we have another << operator, leading to the variable rate which
the user already input. It means that the input of the user for the rate will be
displayed. Then we have another << operator, leading to the next output
statement ending with std::endl, meaning this is the end of the output line.

Note that we placed some spaces between the text and the quotation marks.
We do that since C++ doesn't know to place spaces on its own. If we don’t
place the required spaces ourselves, the output can look like this:
You rated this book as 10

Instead of:
You rated this book as 10

Below is your full code as it should look like:

Listing 2.1 Your first C++ program

#include <iostream>
int main()
{
int rate; #A
std::cout << "How would you rate this book so far? Please rate from 1-10
std::cin >> rate; #B
std::cout << "You rated this book as " << rate << ", thanks for your fee
}

Note

Of course, there are different scenarios we did not take into account in this
tiny program: What if the user enters a number smaller or bigger than 1-10?
What if he enters a letter or word instead of numbers? What if he enters
nothing? Computer programs are obviously more complex than the program
you just wrote, and later chapters will teach you how to plan and handle
different input scenarios.

Step 7 – let's run your code!

Ok, you worked hard and now it's time to execute your code. We click the
debugger button (Figure 2.7). If you are using MacOSX, the debugger button
is also a part of VS. If you are using Linux, best if you use VS Code.

Figure 2.7 Click on the "Local Windows Debugger" to compile (build) your code and turn it into
a program.

Now the compiler will compile your code, and this is what we should expect
to see in this console:
Now that you have written your first code, you can try and "play" with it: try
changing the input and output, adding more input and output options. Read
the code, get a better sense of it, and get more familiar and confident, as you
will keep on following this book (which we hope you rated 10…).

Let's recap the process of writing your first program, as illustrated in Figure
2.8:

1. First, include the iostream header.


2. Then create a variable.
3. Then handle input and output streams using cin and cout. Use std:: to
indicate to the compiler you are using the standard library stream.
4. Continue by adding quotation marks to the output statement, and end
your statement with std::endl (for ending line) and then finally with a
semicolon ;.
5. You can now run your code.

Figure 2.8 The process of writing your first program is illustrated in five steps.
To IDE or not to IDE?

As you can see, the IDE is an important power tool, but must we use an IDE
to write a computer program? The answer is no. You can do without an IDE,
and go "guerilla" using nothing but a text editor like Notepad - but why
should you? The modern IDE is so warm and fuzzy, there is absolutely no
reason to do without it (unless it's for research and learning aspects maybe).
However, if you do go guerilla and use a text editor, you will always need to
use a compiler and a linker to perform the required actions for your code to
run.

2.3 Respect your code (it will respect you back)


After writing your first code, let's focus on several good practices in C++
(and programming in general), which may not appear to be crucial at first
glance, but our extensive hands-on experience suggests that they can
significantly enhance your programming skills and help you write a well-
behaved code that will respect you back.

2.3.1 Indentation – appearance is everything


While indentation will not affect the compiler or your program and might
seem meaningless, it will have a great effect on the readability of your code.
Bad indentation can make your eyes bleed, and we are not ashamed to say:
clean and clear code with the right indentation can make a big difference in
your performance as a coder.

There are different approaches to indentation. Ours throughout this book is a


bit different than most, as we like to write a very clear code and use
symmetrical indentation to each and every element. Many coders use K&R
style indentation. K&R stands for Kernighan & Ritchie Style, and is referred
to as "one true brace style". The following code represents the K&R type
indentation. Note: you are not supposed to understand the code at this point –
just look at the visuals of the structure (figure 2.9).

Figure 2.9 The common and most used way to use indentation – personally, we feel it's messy.
We recommend, (and personally use), the Allman style, which is named after
Eric Allman. Here is the way we use indentation for the same code using the
Allman style, and, as you can clearly see, it looks much clearer and the flow
is clear and readable (figure 2.10).:
Figure 2.10 Neat symmetrical indentation – a better way we recommend using
You can see that our code looks far neater than the previous code. You can,
of course, use your own indentation style, or simply follow the style and best
practice used by your team. Just keep in mind that the use of indentation is
extremely important for you, the programmer, not for the machine. Of course,
the indentation style has no impact on the actual code execution. The only
importance is code maintenance and visibility.

In this book, we demonstrate our indentation style in many use cases, and we
also highlight the indentation when needed to make it clear and easy to
follow.

2.3.2 Comments comments comments, (and more comments)


One more crucial component of your code is comments. Think of comments
as "memos" to your future self. Comments have no impact on the execution
of your code or your program. You can write whatever you want, and it will
be part of your code, but not part of your program. In our opinion and years
of experience, it is always a good practice to comment on your code as much
as you can: use comments to explain what you are doing, why, or if there is a
specific reference to your code, and anything else. We already used
comments in the code you wrote in the first section of this chapter, using the
single-line comment:
// This is a single-line comment

And we can also use a multiline comment, which is a comment stretching


across a few lines, using /* at the beginning of the comment and ending with
*/ like the sample below:
/*
This is a multi-line comment
The comment will continue
How long?
Until we mark the end
*/

2.3.3 Dr. Brain and Mr. Google – why you don’t need to over-
memorize things
Programming involves dealing with a vast amount of information and
knowledge that can be challenging to remember or keep track of – especially
in a complex and robust programming language such as C++. Libraries,
keywords (all 95 of them), header files, functions, structures, syntax, etc. are
continuously evolving. It's unrealistic for you to memorize everything by
heart, and you really shouldn't. Don't get us wrong: it's useful to familiarize
yourself and memorize various concepts, elements, libraries, keywords, etc.,
keeping the balance between what you remember by heart, and what you
don't. For everything you don’t remember, you can rely on online resources,
documentation, forums, and tutorials – and there are tons of them, also
allowing you to stay up-to-date with the latest programming trends and
changes.

2.3.4 How the C++ Syntax and your program’s logic are tied
The C++ syntax is constructed with a certain logic and understanding this
logic will help you not only to know how to do things and write good code
but also understand why the syntax is designed in a certain way. However,
sometimes the logic or syntax may seem confusing or not make sense. In
these cases, you may need to memorize certain things, but, as said, there are
always great resources available online to help you with this.

What's important to remember is that each component in the syntax has a


specific role and works together with the other components within the syntax.

Good to know

There is no denying that mastering the syntax of C++ can be a daunting task.
However, with practice and dedication, it is possible to become proficient in
the language. One way to help with this is to use the auto-complete feature
provided by most IDEs, which can save you time and effort in writing correct
syntax.

The logic of the syntax structure and the expression depends on several
things, for example, it is important which element (variables for example) is
on the right side of the syntax and which is on the left. Though we explain
more about the importance of left side values (lvalues) and right side values
(rvalues) later on in this book, but without the need for you to understand this
concept yet, let’s look at a simple example (figure 2.11) illustrating the
importance of where we position elements in our code.

Figure 2.11 Where we place our elements is important. Positioning elements on the left (Left
values) or on the right side (Right values) has a crucial meaning.

Once you understand the logic behind left and right values, you can use your
understanding towards writing code better – and that’s just one example of
many.

At the end of the day, your code, your syntax, and your expressions must
make sense as you "talk" to a machine, so there is a special syntactical
structure that accomplishes that. In some cases, within the flow and the way
the syntax is constructed, there are rules of priorities between the different
components.

We can divide C++ syntax and the way it is used in statements into several
categories. Each type has its own purpose and unique syntactical structure,
and we demonstrate and explain each and every type thoroughly in this book.
Note

Many times, when something doesn't make sense, or seems too complicated
as a syntax or expression, a flow chart can bring a lot of sense into it.

2.3.5 Sweet: Syntactic sugar and C++ 20


We sum up this section on a sweet note: syntactic sugar. In computer science
syntactic sugar means syntax within the language which is much easier to
read, write and understand, so you might say it makes the language
"sweeter".

As C++, evolves and, at the same time, becomes more and more complex, the
need for syntactic sugar turns into a must. There was a need to "sweeten" the
language as it became more and more complex, and abstract certain concepts
behind a more simplified syntax. We like to think of it as a marathon runner
that needs to fuel up some sugar at the 20th mile after depleting his muscle
power– a programmer can feel exhausted after a while as well (sometimes
even more than a marathon runner…). Simplifying C++ gives you more
"muscle power". In the new C++ 20 standard we can find more syntactic
sugar than ever before in any other C++ version, as old complex expressions
and syntax were stripped and re-dressed in a new simple and sweet version,
while, at the same time, new expressions were born, and they are easier and
clearer than ever before. We go over many of these important syntactical
changes as we go along in the book. However, to be honest, not everything in
C++ is sugar and candy – there are a lot of not-so-friendly expressions and
structures, which might seem more challenging.

Now that we have covered these practices and concepts, we can move on and
dive back into learning how to write C++ code, moving directly to memory
allocation and variables.

2.4 Memory is king: Variables and memory


The computer’s memory and the code you write go hand in hand, so it’s only
natural to start your programming journey understanding the basics of
memory management, especially memory allocation. Understanding how
memory really works involves a lot of technicalities, and might leave some
programmers scratching their heads, especially when it comes to how a given
program interacts with the memory. In this section, we will only focus on the
basic concepts of memory allocation and why variables, which are named
storage locations in a computer's memory that can hold a value, play such an
important role in your program and memory.

It's no secret that the computer's memory is where certain information is


stored for later retrieval - this information can be stored permanently until it
is manually deleted, or it can be stored temporarily, and the machine will
delete it for you later on.

Good to know

On the hardware side, all computers are constructed from two core
components: the CPU (Central Processing Unit), which can be considered the
"brain" of the computer and performs various data processing operations, and
the RAM (Random Access Memory), which is the computer's internal
memory. During the execution of a program, it is always loaded (either fully
or partially) into the RAM. There are other memory components, such as
cache memory and virtual memory, and disk memory, but we will not cover
these in this book.

Memory must be managed and handled properly when we write code, to


support our program and allow smooth and fast operation – we don't want to
exhaust resources or use them unwisely. You can write the most amazing
program, but if there are no sufficient memory resources to run it, or if your
code mismanages memory, it won't matter how amazing it is – it will not
work well, or even crash. After all, memory is a finite resource, and it is
necessary to carefully manage its use to ensure the smooth operation of your
program.

Think of memory management and allocation as an Amazon locker system:


When parcels are stored in storage units, they take up space that can later be
used to store new parcels. Similarly, in a computer, when information is
stored in memory, it occupies space that can be used to store new
information. Just as Amazon must free up space in its locker system by
returning uncollected parcels to the store, a computer must also free up space
in its memory by deleting information that is no longer needed. This is
important to ensure smooth operation and maintain free space for new
information.

Memory management involves setting aside sections of memory to be used to


store various values and data, and releasing the occupied memory when it is
no longer needed. C and C++ offer fine-grained control over memory
management, which can be useful in certain situations, and throughout this
book, you will gradually learn more about that.

At the end of the day, the computer's main memory is nothing but a hardware
resource with a physical address - once we store something in memory, we
will probably need to call and use it at some point, which means these
memory addresses must be available for use to use. To be able to call these
memory addresses we must have some sort of abstraction (simplification), as
it is humanly impossible to know and remember the physical addresses of
each component in your program. This is where variables come into the
picture, mitigating our human way of thinking and remembering, with the
way the machine does.

Note

We go back and discuss memory throughout this book, especially in chapter


8 when we explain the role of pointers in memory management and
manipulation.

2.5 Variables – the backbone of your code


Variables are at the very heart of your program and their role is to serve as an
abstraction, or, in other words, simplification of memory allocation,
depending on the type of data we need to store. For example, in the code you
just wrote (Listing 2.1), you asked the user to input his book rate. The value
of rate, which in your program was the user input, must be stored somewhere
in the computer's memory so that later the program can pull the number and
display the output.

Instead of remembering the physical memory storage address (memory


location), which is constructed of values, which in most cases are shown as
hexadecimal (hex) values, we can call our memory storage by name, and
more specifically, use our own pre-defined variable names and reserved pre-
defined variable types. In other words, a variable is a named storage location
in a computer's memory that can hold a value.

In the code, we used the type int since the value was an integer (a number),
and we named it rate since the name made the most sense. Using variables is
simpler than using memory addresses, so in our code, we'll be using
meaningful names instead of memory locations that you would never
remember by heart. You can say that variables are the missing link mitigating
our 'flawed' human memory with the computer's ultra-memory capabilities.

Good to know

Some experiments show that a human's short-term memory can hold a very
limited number of items, with an average of about seven items. Our language
center is better suited to memorizing things written in a terminology that they
are already familiar with and with established patterns, rather than
hexadecimal clusters.

2.5.1 Like a lid to a pot - every variable has a data types


As programmers, we use various values of various types in our code. For
example, let's say we run a small program that runs the daily sale of a shop.
We might need to use the following values:

Was the shop open? (yes/no)


The number of clients who visited the shop today? (93)
The number of clients who bought an item? (54)
Daily revenue in USD? (5679.35)
Initials of the shop manager (D)

As you can see, our sale program contains different types of data – from a
yes/no value, an integer with a small value, an integer with a long value, a
value with a floating point, and a character. Each of these types has a
different size in bytes, and each requires a designated memory allocation to
support its size.

In programming, every variable must be associated with a specific data type


that will reflect the value the variable holds and will allow the value to be
stored in the appropriate memory location considering its size.

Think of data types as if they were different car models in a parking lot -
from the smallest car to the biggest truck. When parking each vehicle in a
specific parking space, we need the car to fit the actual size. We will not park
the smallest car in the truck's space, nor the truck in the smallest space.
Variables work under the same principle: they capture a portion with a
specific memory size in bytes for different data types from the smallest to the
largest.

In C++ there are several data types, each one representing a different memory
size value. Each variable is represented by a unique keyword, which, as you
may remember, are names C++ decided their meaning in your code, and
which you cannot change.

Unleashing the primitive within C++ primitive data types

In C++, variables are considered built-in primitive data types. Why


"primitive"? Well, it's because they are the most fundamental and basic data
types available in the language. Primitive data types are essential to the
language itself and provide a foundation for more complex data types and
structures that programmers can create, and they include fundamental data
types like integers, floating-point numbers, and characters, among others.
Because primitive data types are built into the C++ language and are a core
part of it, they are always available for use and do – in other words: they do
not require additional libraries or packages to use – they are always there for
you to use.

C++ data type – size does matter

As explained, different data types come in different sizes, just like parking
spots for cars of different sizes – it won't be possible to park a truck in a small
cars' space – it won't fit, and it won't make sense to park a mini in a trucks
parking spot – it will be a waste of space.

Under the same logic, if we store a variable using a data type that cannot
accommodate its size, it will result in data loss or unexpected behavior. For
example, storing a large number in a data type meant for smaller numbers
will truncate the value and result in data loss, while storing a smaller number
in a larger data type will waste memory. Therefore, it’s important to choose
the right data type based on the size and type of the data you want to store,
and the rule of thumb is that it's always good to use a data type that has
enough capacity to hold the data you are working with, without exceeding it
unnecessarily.

Good to know

Choosing the right data type is especially important in performance-critical


applications, where using the wrong data type can lead to significant
performance degradation. Note that on different configurations, the same data
types may have different sizes (Win32 or x86 Vs. x64).

Let's explore a few of the C++ data types and their size in bytes as presented
in Table 2.1

Table 2.1 C++ data types

Data Type Keyword Example Size in byte

Integers int int age{46}; 4 bytes

Characters char char letter{'a'}; 1 byte

Boolean bool bool flag {false}; 1 byte


Floating point float float price{34.67F); 4 bytes

Double floating
double double pi{3.14159); 8 bytes
point

wchar_t Hello{L“

Wide characters wchar_t ‫שלום‬ 2 or 4 bytes

”};

No value void No value

Note

When we initialize a floating-point value using a decimal number, the


compiler assumes that it's a double by default. If we want to specify that the
value is actually a float and not a double, we need to add a suffix of f or F at
the end of the number, as we did in our code sample in table 2.1: float
price{34.67F);. If we don’t specify that the value is float, the compiler will
interpret the value as a double, and try to convert it to a float - which can lead
to loss of precision.

Note

In C++, the wchar_t data type is used to represent wide characters. Wide
characters are characters that occupy more than one byte of memory, and
they are commonly used in internationalization and localization to support
non-ASCII characters in different languages. In table x, the expression
wchar_t Hebr {L“‫ ;}”שלום‬demonstrates a wchar_t initialization with the
Hebrew word “‫( "שלום‬Shalom) which stands for hello, or peace. Later in this
book, we go back to the wchar_t data type.

important!
Unlike other variables, when we use char, we must use the single quote signs
' ' before and after the char value. For example, char initials {‘b’};

Into the void – when there is no value, use void

The void data type is a special type that indicates the absence of a value, so
an object with the void type cannot hold any data. The void type is often
used for functions that perform operations that do not return a value, such as
printing output to the console or updating data structures – and in chapter 7
you will learn about functions, including with a void return type, which does
not return any value, void can also be used as a placeholder for data that is
not yet known, but will be defined later. Overall, while the void data type
may not be as commonly used as other data types, it plays an important role
in certain programming tasks and is an essential concept for any C++
programmer to understand.

Let's practice some code and see these variables at work. Note that the value
of each variable will be placed within curly brackets {}. This is called
initialization – we initialize the variables and give them a value. The value is
placed within the {}. We talk more about variable initialization in the next
section.

Listing 2.2 Declaring variables of different data types.

int main()
{
int numOfApples{ 10 }; #A
char favoriteLetter{'J'}; #B
float pi{ 3.14159 }; #C
double distanceToMoon{ 238855.947 }; #D
bool isSunShining{ true }; #E
std::cout << "I have " << numOfApples << " apples." << std::endl;
std::cout << "My favorite letter is " << favoriteLetter << "." << std::e
std::cout << "The value of pi is approximately " << pi << "." << std::en
std::cout << "The distance to the moon is " << distanceToMoon << " miles
std::cout << "Is the sun shining? " << std::boolalpha << isSunShining <<
return 0;
}

Once we run this code, we should expect the following output:


Need more precision? Use type modifiers

As you can see, deciding what is the appropriate data type for your variables
is not that hard, yet there are some additional types we can use to be even
more precise when declaring our variables. These types only work alongside
int, double, and char and they are called type modifiers. For example, the
type modifier long is used to declare an integer variable that can store larger
values than a regular int and increases the range of values that can be stored
in the variable by increasing the number of bits used to represent it. If we
need an even larger value, we can use long long. The type modifier short,
on the other hand, can be used when our integer value is small and doesn’t
require a lot of space.

Before we move on and look at all the type modifiers, there are two variable
type modifiers used in C++ you need to know: signed and unsigned. We use
signed and unsigned modifiers when we want to indicate negative or
positive values. Signed variables will use one bit to flag whether they contain
positive or negative values. Unsigned variables don't use that bit, which
means they can store larger numbers in the same space, but only if these
numbers are positive. (e.g. 0 and up).

Remember

Signed variables can be 0, positive, or negative (since they use a bit in order
to flag the value). Unsigned variables can be 0 or positive (since they can't
flag their value).

Let's look at the C++ type modifier as presented in Table 2.2

Table 2.2 C++ type modifiers

Size in
Data Type Example
byte

unsigned char 1 byte unsigned char num{25};

unsigned int 4 bytes unsigned int count{4294967295};

short 2 bytes short temp{10};

unsigned short 2 bytes unsigned short count{100};

long 8 bytes long num{1234567890L};

unsigned long 8 bytes long num{1234567890L}

long long 8 bytes long long num{1234567890LL};


unsigned long long 8 bytes unsigned long long count{18446744073709551615ULL

Long double 16 bytes long double num{3.1415926535897932L};

Note

Just like when we use the F suffix for float, the L and LL suffix in our long
and long long expressions (long num{1234567890L}; and long long
num{1234567890LL};) indicates that the number should be treated as a long
instead of an int. Without the L or LL, the number would be treated as an int
by default. Since long can represent larger values than int, it is important to
use the appropriate type modifier to avoid unexpected behavior or data loss.

Tip

Don’t let the name long double mislead you – long double is actually a
misnomer: it doesn't necessarily imply a longer length than a regular double -
it's a floating-point data type that usually has more precision than a regular
double, however, its exact size and precision can vary depending on the
platform and implementation.

It's important to understand, that by default, int is a signed data type, which,
as you learned, means it can store both positive and negative values. Because
int is the default type, we don't need to explicitly specify it is signed when
declaring an integer variable. For example, if we declare an int variable like
this: int num {3}; that’s perfectly fine and it means we are dealing with a
signed int. The same applies to long and long long, which, just like in the
case of int, are signed by default.

However

the default ‘signedness’ state of char and short is implementation-defined,


which means it depends on the compiler and platform being used. Therefore,
it is always a good idea to explicitly specify whether a short or char variable
is signed or unsigned.
As you can see from both Table 2.1 and 2.2, we have several data types
which are used alongside variables, and according to this type, the data is
stored. As the data must be stored somewhere in the computer's memory so
that it can be accessed later, when we create a variable in our code, the
computer reserves a certain amount of memory for it based on its data type.
This reserved space in memory is called an "allocation", and the size of the
allocation is determined by the data type of the variable.

As mentioned earlier, each variable has a limited range of values it can hold,
determined by its data type (minimum and maximum values). For example, a
signed int has a capacity of values between -2,147,483,648 and
2,147,483,647 (in a 32-bit system while int captures 4 bytes), while 64bit
machines will have a maximum value of -18446744073709551615 to
18446744073709551615. If we try to assign a value outside of this range, an
unexpected error might occur in our code. This is why it's important to
choose the right data type for the values we want to store.

Note

a table showing the capacity for each variable type can be found in Appendix
D.

Let’s look at some code, and this time we declare and assign values to some
of the type modifiers you just learned for a small shop management program.

Listing 2.3 Declaring type modifiers

#include <iostream>
int main()
{
unsigned int num_customers{ 5000 }; #A
short num_items_sold{ 100 }; #B
unsigned short num_days_open{ 7 }; #C
long total_profit{ 10000000 }; #D
long long total_revenue{ 100000000000 }; #E
long double average_rating{ 4.5 }; #F
std::cout << "There are " << num_customers << " customers who bought " <
<< " items over " << num_days_open << " days." << std::endl;
std::cout << "The store has made a total profit of $" << total_profit <<
<< total_revenue << " in total revenue." << std::endl;
std::cout << "The average customer rating is " << average_rating << " st
return 0;
}

Once we run this code, the output should be:

Signed and unsigned – good to know

When you define a data type as "signed" it means that if this type has the
capacity to hold N values (for example, 65,535 in case of a 'short int'), by
adding the option to hold negative numbers the range of any positive value i
is cut by half and you can now store numbers in the range of -32,768 and
32,767.

If you assign a negative value to an unsigned int, you will probably get a
large number, for that exact reason. To demonstrate that, we write the
following code:
#include<iostream>
int main()
{
unsigned int i{};
i = -16;
std::cout << i;
}

once we run this code, we get a value of 4294967280.

Good to know

If one day you need to work with even larger integers than the C++ built-in
types can handle, libraries such as bigint can help. bigint is short for "big
integer" and is a 3rd party class that allows you to work with integers of
arbitrary size, and perform arithmetic operations on very large integers,
including addition, subtraction, multiplication, and division. Bigint is not part
of the C++ Standard Library, but there are third-party libraries that provide
bigint implementations. Another widely used external library is Boost, which
offers, among many other functionalities, Boost.Multiprecision library, which
was designed to handle integers of arbitrary size as well.

Signed and unsigned char demystified

Before we move to our next example, let’s go back to the char data type for a
second. It’s important to know that though char type stores characters
(letters), the value is actually stored as an integer, or, to be more precise, in
ASCII values (and you may have noticed that in table 2.2 we used the
expression unsigned char num{25}; for unsigned chars).

ASCII is an acronym for American Standard Code for Information


Interchange. It defines a specific way of representing English characters as
numbers. Each letter has its own unique ASCII value. For example, the
ASCII value of the letter 'A' is 65, while the ASCII value of 'a' is 97. Let's see
a code sample. You should already know some of the components in the code
you are about to read, such as std::cout, and more.
#include <iostream>
int main()
{
char letter1{ 'z' }; #A
char letter2{ 'Z' };
std::cout << "The ASCII value of “ << letter1 << “ is " << int(letter1)
std::cout << "The ASCII value of letter2 is " << int(letter2) << std::en
}

The output we should expect is:


Good to know

The ability to change or convert one type to another is called casting. Casting
tells the compiler that even though the value was declared as a char like the
above example, in this instance we want it treated like an int. We will further
discuss casting in Chapter 8 when you learn about functions.

Now that you understand that char values are stored as integers, you can
understand why we also need to use signed and unsigned type modifiers
sometimes. The reason for using signed and unsigned with char variables is
that the value of chars ranged between -128 - 127. Yet, sometimes, with
special characters, the value exceeds 127, which is why we sometimes need
to use unsigned char which can store values from 0 to 255. For example, the
ASCII value 157 equals the letter Ø which is used in some European
languages such as Norwegian and Danish, while the ASCII value of 243
represents the fracture ¾. In both these cases we must use unsigned chars.

To sum up, this section, remember: when selecting a data type, you should
consider the range of values the variable will hold, as well as the level of
precision required

The auto keyword – let the compiler do the heavy lifting for you

So far you saw that in C++ we need to use data types to tell the compiler
what kind of data a variable will hold. However, sometimes it can be tedious
to write out the data type every time we declare a variable, or sometimes we
don’t know which data type to use – and that's where the auto keyword
comes into play.

The auto keyword replaces the data type, so instead of declaring int i; we
can declare auto i;, doing so, the compiler knows to automatically deduce
the data type of a variable based on its initialization, so once the variable is
initialized, the correct data type will be assigned from a compiler point of
view. In this case, if auto i{50}; the compiler will treat the variable like int
i{50}; and if the statement would be auto i{‘c’}; the compiler will treat it
like char i{‘c};.

Let's look at a code sample. In this code, we declare 3 variables using the
auto keyword and initialize each, we then print to the console their type and
value. Note that in this code, we will be using an operator named typeid,
which is used to print the type of each variable. Don’t worry if the syntax
looks unfamiliar – the point of this code is just to show how the compiler
automatically infers the types based on the initialization values, and not how
to use the typeid operator.
#include <iostream>
int main()
{
auto myNum = 42;#A
auto myFloat = 3.14159;#A
auto myChar = 'a';#A
std::cout << "myNum is of type " << typeid(myNum).name() << " and has va
std::cout << "myFloat is of type " << typeid(myFloat).name() << " and ha
std::cout << "myChar is of type " << typeid(myChar).name() << " and has
return 0;
}

Once we run this code, we should expect the following output:


Tip

while using the auto keyword can be helpful in plenty of situations, use it
with care and don’t just treat it as a savior for types. One reason is that not
having explicit knowledge of the type of variables your program will use
might make it harder to claim that the code works as intended. When you
learn to write a more complex code, using functions for example, which
return a value - we don’t want the data type of the returned value to surprise
us – and we get to that later on in this book.

Good to know

C++ allows us to create our own data types using an enum - short for
enumeration. An enum is a user-defined data type, in which we can define a
set of constant values under a single “type” roof. For example, we can create
an enum named months, which will hold enumerators of all 12 months. If we
want to initialize the month of January, we can simply write: months
January{1};. We talk plenty more about enums in chapters 6 and 7, where
you will understand this concept and learn how to use it in code.

2.5.2 Rolling up our sleeves: how to actually work with


variables
Working with variables is pretty easy and consists of three steps, two of
which you are already familiar with, as illustrated in Figure 2.12:

1. We need to define and declare the variable’s type.


2. We then need to identify the variable and give it a name.
3. We can initialize the variable or leave it uninitialized at first.
Figure 2.12 Three steps for working with variables: choose variable type, declare the variable by
naming it, and initialize it with a value (or with no value).

Tip

though you can leave your variable uninitialized, it’s best practice to always
initialize it to a default value such as 0.

Let's go over each step to understand how they work.

2.5.3 Naming names: best practices in naming your variables

As you already saw, when we declare a variable, we choose the appropriate


data type and identify each variable from the others by giving each a name of
our choosing. This stage is called a variable declaration. For example, here
we declare a variable of type int named age:
int age;

Note that some programmers declare multiple variables in a single row:


int a, b, c, d;

In this case, we declared four variables of int type, and named them a, b, c,
and d - each is a separate variable that occupies its own memory space.
However, for clarity, we recommend declaring your variables using separate
lines, which is considered best practice.

At this point, we are only declaring the variables and they have no value to be
stored yet – they are uninitialized. We only decide what would be their
proper data type and name them.

from foo to fabulous – best practices in variable naming

Naming your variables can be crucial, and choosing the right names for your
variables can make a difference between a more readable and easier-to-
understand code, (not just for you, but for anyone else who might read your
code), and a code in need of deciphering. Obviously, variable names should
be clear and descriptive, so that their purpose is immediately apparent, but
that's not all - in this section, we'll go over some best practices for naming
variables and golden rules. Let's go over the basics:

1. Variables names can contain letters, numbers, and underscores.


2. A variable name can never start with a number.
3. Variables names can never be one of the C++-reserved keywords.
4. Variables names cannot be reused within the same scope.

Let's see some examples in the table below.

Table 2.3 Examples of variable declarations

Variable declaration Expected result

int myAge; Correct

int _myAge; Correct

int my_Age Correct


int 34myAge; Wrong – begins with a number

int myAge!; Wrong – contains special character

int break; Wrong – break is a reserved keyword in C++

Good to know

Per the most common convention, variable names generally start with a
lowercase letter and are camel-cased.

Best practices for declaring variables

When declaring variables there is some best practice we recommend


following, also illustrated in Figure 2.13 below.

1. Declare your variables with consistent name conventions. For example,


if you use lowercase at the beginning of each word keep doing so
throughout your code (i.e. myName), and the same if you use
underscores (i.e. my_Name).
2. Try using meaningful words that will make sense to you and to other
programmers. For example, if you declare a variable that will be used to
display an age, the best would be to name it "age” rather than
“some_number”.
3. Keep your declaration close by or initialize it immediately if you can
upon definition. It means that when coding it is always helpful to see the
variable declaration as close as possible to where it will be used in the
code. We wrote some examples throughout this book, keeping this good
practice in mind.
4. Try to avoid very long names or names that are similar to each other as
that might be confusing and even hazardous.
5. Apart from naming your variable with good and meaningful names,
when declaring any variable keep the good practice of commenting on
what this variable will be used for or any other useful comment for
future you or other team members.

Figure 2.13 Variable’s five golden rules

Good to know

If you look for code samples you will probably find a lot of examples using
the name foo. foo is a placeholder or dummy name that is often used as a
variable name, function name, or any other component that needs a
temporary name – think of it as slang among geeks…

2.5.4 With added value - Initializing variables


In the previous code samples, we already assigned a value to our variable,
placing the value within curly brackets (also known as List initialization). We
explained this process is called variable initialization. When we use {},
sometimes we can also leave the variable empty, or uninitialized - Think of it
as if it was a storage unit you just rented – you can store an item in the unit
(initialize), or you can leave it empty (keep it uninitialized) until you store the
item later (initialize later). You can also store an item, and then take the item
out and replace it with another item.

Variables work the same way. The value we assign to a variable (initialize)
will be stored in the computer’s memory. Earlier, you learned that we must
match variables with the correct data type – remember the parking lot
example? You cannot park a bus in a Mini’s space and vice versa. Since each
variable type has a specific size it captures in memory, we cannot mix one
variable type with another variable’s initialization value. For example, long
long type initialized with {3.14}, won’t compile, and we will see an error
message (figure 2.14), although long long has 8 bytes. The problem is the
type mismatch: The value {3.14} is a floating-point literal, and more
specifically - double type. Assigning a double type to a long variable can
result in a loss of precision, and C++ strictly enforces type checking, so
implicit conversions between incompatible types are not allowed - and here's
your "bus and Mini have their own parking space" enforced by C++.

Figure 2.14 We cannot mismatch types as C++ enforces type checking

Remember

The compiler will not compile mismatched pairs of variable type and their
value if they do not match (though there is a way to overcome that which we
won’t get into at this point), and your IDE will indicate an error.
Let’s take a look at the short code below:
#include <iostream>
int main()
{
char name{ 23.45};
int nums{ 366.22267 };
}

Once we run the code the compiler shows us again some errors as shown in
Figure 2.15

Figure 2.15 We cannot mismatch variable types – in this case, a char type cannot hold a value of
float type, and an int type cannot hold the value of double.

How do we initialize variables? Well, just like dancing the Salsa, there is
more than one way to do it.

List initialization using curly brackets {}

Variable initialization using curly brackets {}, which you already know, is
considered modern C++ initialization (or list initialization). List initialization
is the most common method and best practice in C++ to initialize variables
(as well as other objects, such as functions, structures, or classes – all of
which you will learn about later on). The fact this initialization method is
used across various objects, and not only with variables, makes this a unified,
and therefore, a preferred method for many programmers.

We can use empty curly brackets (also known as ‘curlies’) or place the value
inside the curlies. Let’s look at some examples:
Int num {};
int age {15};
long temp {12.3};
char label {'L'};

remember

As mentioned earlier, initializing char is always between two '', for


example, char label {'g'};.

Moving forward, what will happen if we don't initialize a variable at all at


any point in our program? Let's look at a code sample. In the code below, we
don't initialize the variable num1, while using curlies {}.
#include <iostream>
int main()
{
int score1{};
std::cout << score1 << std::endl;
}

When we run this code there are no errors, it runs smoothly, and the console
shows that the value of score1 is 0. The reason is that modern compilers
know to automatically initialize an uninitialized variable to 0 when we use
{}. This is an important point to remember, as other initialization methods,
which you will learn about in the next section will not support an
uninitialized variable.

Tip

It is generally a good practice to initialize variables to a default value,


especially if there is no other initial value assigned to them. This can help
prevent potential issues with uninitialized variables that can lead to undefined
behavior or bugs in the code.

Good to know

The curly brackets {} offer a uniform initialization. It means that they can be
used to initialize any type of variable, including arrays, structures, and
classes, which you will learn all about later in this book.

Variable initialization using the assignment operator =

Another method to initialize our variables is with us since the days of C:


initialization using assignment. Assignment can be done as initialization, but
also assigning a value to a variable after it has already been declared. For
example, we can declare an integer variable named age and assign it with a
value of 25, and then re-assign the value to 27:
int age = 25;
age = 27;

In this example, we declare the variable age and initialized it using an


assignment to the value 25. Later on, we assign the value of 27 to it.

Note

we further explore the assignment operator in the next chapter, when we talk
about the C++ operators, but meanwhile, let's understand how this
assignment works.

How assignment works

When we assign a value to a variable using = the value assigned to the


variable is called the "right-hand value" or "R-value". In the expression age =
27; The value 27 is the right-hand value, or R-value (as you might recall, we
briefly introduced the concept of L and R values earlier in this chapter). The
L-value is the memory location that holds the value of the variable. The R-
value is the actual value being assigned to the L-value. We talk more about
the anatomy of L and R values in the next chapter, so don't worry if this all
might sound a bit confusing at this point.

As explained, one of the advantages of using the assignment operator is that


it allows us to assign values to variables after they have been defined, which
can be useful in certain situations. Let’s read the following code:
#include <iostream>
int main()
{
int age{ 32 };
age = 33;
std::cout << "The value of age is: " << age << std::endl;
return 0;
}

What will be the value of age in our output? It will be 33. We initialized age
to 32 and then assigned the value 33 to it using the assignment operator.

Tip

Bear in mind, that using the assignment = operator to initialize variables


might be useful in some situations, but not recommended in others. One
reason for this is that using = involves the creation of temporary values (a
copy of the original value), which can create an overhead and might slow
down a program when working with large objects. Curly brackets can be a
better choice for initializing large objects because they avoid this overhead.

Initialization using regular bracket ()

Another initialization style you might encounter in some code is using regular
brackets () instead of curly brackets. This method is old-style initialization,
which was replaced once curly brackets were introduced in the C++11
standard. This type of initialization is also called "parenthesized
initialization", or "functional-style initialization". For example:
int age (32);

Though this style is not commonly used today and not considered best
practice, it’s important to get familiar with it.

As you can see, this old style initialization using () is very similar visually to
initialization using {}, but in fact, compiler-wise, there's a big difference: to
begin with, when we use (), the compiler will not allow narrowing
conversions, so if we try to initialize an int with a double using (), the
compiler will show an error. However, if you use {} initialization, the double
will be implicitly converted to an int.
Also, with regular brackets (), the compiler does not know to initialize the
variable to 0, which can lead to undefined behavior (UB). Undefined
behavior in C++ happens when C++ doesn’t have any rules determining what
happens in certain scenarios – in this case, uninitialized variables.

It's s a good opportunity to mention that whenever you see a compiler error or
warning (such as in Figure 2.14), the error number is valuable, as you can
look for it online and understand why the error was caused and what can
resolve it – we explain a lot more about handling errors and exceptions in
later chapters, but generally speaking, any programmer must always think
ahead about potential catastrophic scenarios and allow the software to
navigate through a potential error storm.

NOTE

Throughout this book, we use the modern C++ initialization method with {},
as it's safer to use.

Dynamic initialization during run time

In some cases, we might not know the value of a variable beforehand. For
example, if we're building a program that takes user input, we can't know
what the user will input ahead of time. In such cases, we can declare a
variable without initializing it, like in the first program where we declared the
rate variable as int rate{};. This variable was initialized during runtime,
which means it was given a value while the program was already running.
Initializing variables during runtime is common since we can't always know
the value of a variable in advance, or it might change. That being said, it's the
best practice to always initialize all variables, and use default values in these
cases.

There are several ways to initialize during run time and you will learn much
more about it in the more advanced chapters. One simple and straightforward
way to do it is using the user's input, as you did when you wrote your first
code. Let's look at an example again. First, we declared an uninitialized int
type variable named rate.
int rate{0};

Now, the user inputs the rate and initializes it according to the input result in
real-time.
std::cin >> rate;

At this point, we can use std::cout to display the value of rate:


std::cout << rate << std::endl;

To sum up, what you learned so far, let's explore Figure 2.16, which
illustrates all the initialization methods mentioned.

Figure 2.16 All four methods for initializing variables: C style, old C++ using regular parenthesis,
using curly brackets (curlies), and initialization during run time.

remember

It’s best practice to always use curlies when initializing variables.


2.5.5 Code practice – let’s test what you learned so far

To sum up, everything you learned so far, let's practice with some code. In
the following code, we declare and initialize two int variables: myNum, which
is initialized to 250, and a variable named f, which is initialized as myNum. We
then multiply f by 2 and print the result to the console.

Listing 2.4 A basic calculation program

#include <iostream>
int main()
{
int myNum{ 250 }; #A
int f{myNum}; #B
std::cout << “the result is: “ << std::endl;
f = f * 2.0; #C
std::cout << f << std::endl;
}

As important as it is to write great code, it is just as important to read code


and run it in your head. Try to read this code and do that. What do you think
the output would be? If you guessed 500 you are correct.

2.5.6 Constant variables – make an unbroken promise

Variables are often used to hold values that can change dynamically.
However, there are times when you want to use a static, constant value that
will not change during the course of our program. There are many instances
where a single unchanging value is required, for example, what if we want to
store the value of pi in a variable or declare a variable that holds the number
of months in a year or days in a week? These values should not change, and
we must make sure they will not be changed by mistake during the execution
of our program.

This is where constants or const values come into the picture: Constant
values are the same as any other variable - they occupy storage, they
represent data types, and they can be given unique names and need to be
initialized. The only difference is that they are immutable – meaning that
their value cannot be changed once it has been declared and initialized.
Note

There are a few more types of constants in C++, but for now, we will focus
on literal constants and variables.

In order to declare constant variables, we simply need to use the const


keyword before our variable type:
const int weeks_days{7};
const double pi{3.1415926};

If we try to change the value of one of these variables, we will get a compiler
error. As you can see from the image below, when we try to change the value
of pi, which is a const variable, even before we try to run the following code,
we get an error mark in the IDE (figure 2.17):

Figure 2.17 When we try to change the value of const variables we get an error.

Constants should be treated as a promise never to be broken and hold a fixed


value that will never change during the life cycle of your program. In order
words, your constant variables are used in a "read-only" form.

note

There are some ways to change const variables in C++, some of which we
teach in chapter 7, others are not a part of this book's scope.

2.5.7 Local and global variables – variables in or out of main()

Until now, in all our code samples, we declared variables within the scope of
the main function main(). Earlier, you learned that main() is a function that
is executed first. Anything you want to happen first thing when your program
runs, you should place in this main() function.
Main()
{
// our main function starts here
//The scope of our variables is within the curly braces
}

Variables that are declared and initialized within the main function are called
Local Variables, as they are a local part of the main function. It means that
these local variables are only available to the elements within the main()
function and not to any external elements your program will probably have.
Therefore, local variables will be the first to be executed.

Important

It's important to point out that local variables are scoped within whatever set
of curly braces wraps them, not necessarily the main() function.

Sometimes we want to use variables out of the scope of the main function (or
any function for that matter), and in this case, these variables are called
Global Variables. The fact that these variables are global means that they can
be accessed by every part of your program, which is why it is not
recommended to use global variables unless you must.

Now imagine that you have a son named Johnny. When you're at home and
call out "Johnny", your son will respond. However, if you go to Johnny's
school and call out the name, several kids named Johnny might turn their
heads. But if your son's name was something unique like "Ginger_Johnny",
he would likely be the only one to respond to your call.

Now, let's apply this concept to a program. If you're inside the main()
function and you call for Johnny, the program will assume you're referring to
the local Johnny within main(), regardless of whether there are any other
Johnny outside of main(). However, if there's no local Johnny within main(),
and there's a global Johnny outside of main(), the program will assume
you're referring to the global one.

The “scope” of the main() function, or any function for that matter, is the
code block. The code block is all the lines of code between the opening curly
brackets { and the closing bracket } of the function. However, you can create
scopes inside scopes by adding a block of code, which is separated by
another pair of curly brackets {} and so on. Any local variable resides
between the curly brackets where it was defined and stops its existence right
after the end of the scope. For example, let's look at Figure 2.18. You can see
a scope within the scope of three code blocks, each block represented by a
different color of its curly brackets.

Figure 2.18 You can create scopes inside scopes by adding a block of code, which is separated by
another pair of curly brackets { }.
Important

In the previous section we talked a lot about variable initialization. It is


important to keep in mind, that, unlike local variables, global variables are
automatically initialized to 0.

Important

Generally speaking, when many elements have access to a variable and can
use it, they might also change it or affect it, and the other way around, so
using global variables should be done cautiously, especially when code has
thousands of lines, as you will have difficulties with tracking down errors,
sometimes known as bug hunting in case of issues with global variables.

Let's see an example for using global variables and local variables and
understand the flow of execution, as there are priorities set by the compiler:

Listing 2.5 Using a local variable


#include <iostream>
int main()
{
int age{24}; #A
std::cout << age << std::endl;
return 0;
}

In listing 2.2, age is a local variable. When we execute it, the compiler checks
to find out the value of age and displays it to the console. The default is to
search for a token within its current curly braces with the name age. If found,
it will use that variable to satisfy the request to print to standard out, resulting
in 24 being displayed on the console. Then the console will display 24.

Let's change age from a local variable to a global variable.

Listing 2.6 Using a global variable

#include <iostream>
int age{24}; #A
int main()
{
std::cout << age << std::endl;
return 0;
}

In this code, age is now a global variable. Once we run this code, the
compiler will first search for the value of age within the scope of main(), and
only if it does not find it there, the value will be searched beyond the scope of
main(). The result will be the same in the output.

Now what will happen if we have both global and local variables in our
code? Let's see what it will look like and what it means in terms of execution
of our code:

Listing 2.7 Local and global variables in the same code – how will it work out?

#include <iostream>
int age{24}; #A
int main()
{
int age{10}; #B
std::cout << age << std::endl;
std::cout << "Global age: " << ::age << std::endl; #C
return 0;
}

In this code, age{24} is a global variable, and age{10} is a local variable.


Now we have two variables with the same name (age). When we execute this
code, which value will be displayed? Will it be the local variable or the
global variable?

The answer is that the local variable will be our output as it is the default
variable the elements in main() will use.

But what if we need to access a global variable by the same name as a local
variable, what do we do?

We can use ::
std::cout << "Global age: " << ::age << std::endl;

As you can see, the concept of global and local variables is simple and
straightforward, however, in the future, you will need to make decisions in
which cases to declare global variables and in which local variables, as these
decisions will have a great impact on your program.

Important

It is strongly advised to try to minimize the use of global variables in a C++


program. Global variables hold their values through the lifetime of the
program and are accessible by any part of the code, including any other
components (such as functions and classes you will soon learn about), or
other objects.

Tip

If global variables are used, use the “g_” prefix to indicate that, and to avoid
confusion among variables with identical names.

2.6 Final code practice


To sum up, everything you've learned so far, let's practice some more code.
In the following code, we are going to calculate the area of a circle and a
triangle. To do so, we defined a constant PI with a value of 3.1415 to use in
our circle area calculation. We then calculated the circle area by multiplying
PI with the radius squared. For the triangle area calculation, we simply
multiplied the base and height by 0.5.

note

In this code, we are going to use the arithmetic operator * for multiplication,
which is the same as the one you know from elementary school. We will
explain more about operators, including arithmetic operators in the next
chapter.

Listing 2.8 Calculate the area of a circle and triangle

#include <iostream>
int main()
{
const double PI = 3.1415; #A
double radius, height, base; #B
double circleArea, triangleArea; #C
std::cout << "Enter the radius of the circle: "; #D
std::cin >> radius; #E
std::cout << "Enter the height of the triangle: "; #F
std::cin >> height; #G
std::cout << "Enter the base of the triangle: "; #H
std::cin >> base; #I
circleArea = PI * radius * radius; #J
triangleArea = 0.5 * base * height; #K
std::cout << "Area of the circle: " << circleArea << std::endl; #L
std::cout << "Area of the triangle: " << triangleArea << std::endl; #L
return 0;
}

Once we run this code a possible output might be:


Using the C++ cmath library

There is another way to write this code, taking advantage of the powerful
<cmath> header file, which is a part of the C++ Standard Library. <cmath>
provides mathematical functions and constants, including various functions
such as trigonometric, logarithmic, exponential, and hyperbolic functions, as
well as functions for rounding, absolute value, power, square root, and more.
In our case, of course, we could write the program as is, without using
<cmath>, as the calculations we conducted are pretty basic. However, it’s
useful to get familiar with <cmath>, so let’s re-write our code again using it.

In this case, we are going to use a function named pow(), which is used to
raise a number to a certain power. For example, if you wanted to calculate 2
to the power of 3 (which is 2x2x2 = 8), you would use pow(2, 3). The first
argument is the base number, and the second argument is the exponent. The
result of pow is a floating-point number.
Listing 2.9 Using <cmath> to calculate the area of a circle

#include <iostream>
#include <cmath>
int main()
{
double radius, base, height;
const double pi = 3.14159;
std::cout << "Enter the radius of the circle: ";
std::cin >> radius;
double circle_area = pi * pow(radius, 2); #A
std::cout << "The area of the circle is: " << circle_area << std::endl;
std::cout << "Enter the base and height of the triangle, separated by a
std::cin >> base >> height;
double triangle_area = 0.5 * base * height;
std::cout << "The area of the triangle is: " << triangle_area << std::en
return 0;
}

Note

In this code, the statement std::cin >> base >> height; accepts two values at
once, as we are extracting two values from the user input: base and height.
The extraction is performed sequentially, so the first value entered is stored in
the base variable, and the second value entered is stored in the height
variable.

Once we run this code, we should expect the same output as before.

2.7 Summary
C++ has various basic linguistic building blocks, among them reserved
keywords, punctuation, and logic in structure. It is important to place
each element in your code in the right place and follow the logical and
syntactic structure, which is the proper and logical way to structure your
code, as they work hand in hand.
A compiler is nothing but a program whose role is to analyze your
human-readable source code, then convert it into machine code. The
process is done in three steps:
The IDE is your software environment and allows you to write, edit,
debug, test, and run your code contains your text editor, the compiler,
and the linker.
The linker is what produces the final compilation output from what the
compiler produced, and it links your program with other components
such as libraries and other objects.
Libraries are collections of prewritten code that users can use to
optimize tasks. C++ comes with a large base of pre-written code, which
saves you the time of programming all these basic operations yourself.
The main library used in C++ is the Standard Library, also known as std.
There is a great deal of importance to visibility, which means the way
you can read and understand the code as it gets more and more complex
- by using a lot of comments explaining the code, especially the less
obvious or trickier lines.
Memory allocation is related to variables, as variables are a
simplification of memory assigned to parts of your program. Variables
bridge the gap between our ability to speak in words versus the
computer's ability to read numbers, as memory space in machine
language is a complex set of letters and numbers, while variables are
simply words we invent and use.
Variables represent different data types and serve as containers for
specific memory addresses and sizes in bytes.
Declaring and initializing variables have different styles. Each style has
its own methodology.
Initialization variables using curly braces are the most common
method in modern C++.
In C style initialization we use the assignment operator = assign a
value from the right side (Rvalue) to the left side (Lvalue) as well
as regular parenthesis ().
Dynamic initialization during runtime is used when we don't and
cannot know the value of a variable beforehand. One of the
common methods is to use the user's input for that purpose.
Constant variables are used when we do not want the value of the
variable to change throughout the execution of our code. It's like a
promise never to be broken.
Local variables reside within your main(0 function and when our
program runs, they will be called first.
Global variables reside outside the main() function, and they will be
called only if there is no local variable to be found. Global variables
bring potential risks to your code, as they are subjected to change from
the outside.
[1] https://en.wikipedia.org/wiki/Library
[2]Download a free version of Visual Studio using the link
https://visualstudio.microsoft.com/
[3]Download a free version of Visual Studio code using the link
https://code.visualstudio.com/download
[4]The true entry point of your program is not the main() function, but, in
most IDEs, it's the _start function, which initializes the program runtime,
invokes the program's main function, and handles things like configuring
processors, initializing external memory and more.
3 Smooth operator –exploring C++
operators and conditions
This chapter covers
The role of arithmetical, unary, and assignment operators
Understanding conditional statements using if-else
Unraveling the logic behind logical operators and how to use them
Conducting various comparisons using the comparison operators
Taking a firsthand look into the new C++ 20 three-way-comparison
operator
Understanding bitwise operators and their role.

In this chapter, we will delve deeper into the C++ language by learning about
operators – special symbols that perform various operations within your code.
You may have encountered them briefly in the previous chapter, but now we
will explore them in greater detail with plenty of examples and exercises. We
start with arithmetic operators and assignment operators. We then move to
the basics of conditional statements, using if-else, which work well with
logical and comparison operators. We will also introduce the brand-new
C++20 three-way comparison operator, commonly known as the spaceship
operator, which offers exciting capabilities for comparing objects within your
code. By the end of this chapter, you will have a solid understanding of the
role and use of operators in C++.

This chapter contains a lot of information, in most cases on an introduction


level, but worry not – you will practice it across your journey with us. By the
end of this chapter, you will hold important and fundamental knowledge
which will build up slowly and surely in order to help you read, write, and
handle C++ code with increasing confidence.

3.1 The C++ operators – your programs’ screws


and hinges
In Chapter 2, when you wrote a program that calculates the area of a circle
and triangle, you used the * operator, which is an arithmetic operator you
know from school, so you can understand that operators are everywhere, and
it's hard to get by without them. We like to think of operators as the little
"screws and hinges" in the C++ "engine".

At basic elementary school arithmetic, you are taught how the + - * /


operators are used to add, deduct, multiply, or divide numbers, but there are
many more operators used in C++ than the ones you know from school.

Let's take a closer look at all of the C++ operators, which we divide into five
main groups illustrated in Figure 3.1.

Figure 3.1 The five main groups of C++ operators: arithmetic operators, logical operators,
assignment operators, comparison operators, and bitwise operators.
Each one of these groups contains its own unique operators with their unique
roles and syntax. Let's get to know these operators and how to use each in
real code.

3.2 Arithmetical operators for numerical and other


operands
Starting to understand the C++ operators, many find it easy to start with
Arithmetic operators, as the name clearly implies what they are used for
arithmetical operations. However, these operations can be performed on more
than just the obvious choice of numerical operands (i.e. numbers), but on
other objects such as strings (sequence of characters such as "abc"), and
many more C++ components you will soon be familiar with.

The first four operators we will have a close look at are + (plus), - (minus), *
(multiplication), and / (division). These are simple arithmetic operators you
know from elementary school, and they are pretty useful in code for various
operations.

In the next code sample, you will be using arithmetical operators to convert
temperature from Fahrenheit to Celsius. The conversion will be done using
the following formula:
Fahrenheit = Celsius (°C) times 9/5 plus 32

We first ask the user to input a temperature in Celsius and the output will be
the value in Fahrenheit. You should already be familiar with the structure and
syntax in the code below, such as declaring and initializing a variable, the use
of std::cout/std::cin and std::endl, as well as the use of quotation
marks and semicolons.

Listing 3.1 convert temperature from Fahrenheit to Celsius

#include <iostream>
int main()
{
double Fahrenheit{}, Celsius{}; #A
std::cout << "Please enter a temperature in Celsius: ";
std::cin >> Celsius;
Fahrenheit = Celsius * (9.0 / 5.0) + 32; #B
std::cout << "The temperature in Fahrenheit is: " << Fahrenheit <<
std::endl; #C
return 0;
}

When we run this code and input a temperature of 23 in Celsius, we get a


converted result in Fahrenheit of 73.4 – exactly what we would expect. Try
and practice the use of arithmetical operators while writing some more code
using various conversion formulas. You can convert currencies (such as USD
to Euro), Kilometers to miles, Kilograms to pounds, and more.

Note
A full list of the C++ arithmetical operators can be found in Appendix G.

3.2.1 Precedence day – how C++ follows precedence rules

C++ can be trusted to do the perfect arithmetical calculations for you, as


simple or complex as they may be, yet it is very important to understand the
precedence rules which apply to C++. To understand more, let's use an
arithmetical expression as an example. First, we declared and initialized three
int-type variables (a, b, and c).
#include <iostream>
int main()
{
int a{ 4 }, b{ 6 }, c{ 3 };
std::cout << a + b * c << std::endl;
}

Can you guess whether the answer to this exercise will be “30” (if we move
from left to right), or will it be “22” if we give precedence to the
multiplication first? What is the actual correct solution here?

C++ arithmetic operators’ precedence follows the same algebraic precedence


rules (some might know it as PEMDAS): Parentheses, Exponents,
Multiplication, Division, Addition, and Subtraction). The same rules apply in
C++, as the compiler will give precedence to multiplication, division, and
modulus (which we explore in the next section)- all hold higher precedence,
and will be performed before addition and subtraction, which hold lower
precedence. It means that the expression we just reviewed will be calculated
as:
+ (6 *3) // the result is 22 and not 30

Bear in mind that you can use parenthesis to set your own precedence, and it
might even be useful to prevent confusion.

3.2.2 Divide and conquer - division operator good to know-


hows
When using the division operator, you must remember that the division result
is type-based. In chapter 2 you learned that each data type requires a different
storage size. It means that if we divide two integers, and if the result contains
fractions (for example, dividing 22 / 8), the result will be displayed as whole
numbers, as the integer type does not contain any decimal point. If we divide
float by float, the result can be a float number. But what will happen if we
divide an int type with a double type, or mix any other types when using
division?

Let's see a real code sample and the different outputs based on the operand's
data type in order to answer this question:
#include <iostream>
int main()
{
std::cout << "Result for integer division 13/4:" << std::endl;
std::cout << 13 / 4 << std::endl;
std::cout << "Result for floating-point division 13.0/4.0:" << std::endl
std::cout << 13.0 / 4.0 << std::endl;
std::cout << "Results for mixed division 13.0/4:" << std::endl;
std::cout << 13.0 / 4 << std::endl;
return 0;
}

This is the output we should expect, which answers the question for us.
Result for integer division 13/4: 3
Result for floating-point division 13.0/4.0: 3.25
Results for mixed division 13.0/4: 3.25

As you can see, by dividing the integers the fractional part is discarded. In the
case of floating-point, we receive a floating-point result in both cases.

3.2.3 The % modus operator for integer division


The modulus operator '%' (also known as 'mod'), is used whenever we want to
yield the remainder of an integer division, and it can be used only with
integer operands. If two integers divide without a remainder, the modulus
will be 0. If the result of the division has a remainder, the modulus will
reflect the remainder's value. Below is a quite simple code sample.
#include <iostream>
int main(void)
{
int x, y;
int result;
x = 10;
y = 5;
result = x % y;
std::cout << result << std::endl;
x = 7;
y = 2;
result = x % y;
std::cout << result << std::endl;

return 0;
}

When we run this code, the output will be 0 and 1. As the first result (10/5)
has no remainder, the modus value will be 0, while the second result (7/2) has
a remainder, so the result will be 1, as this is the remainder in this case.

3.3 Assignment operators – assign values to objects


and variables
The assignment operators, which assign values to variables, are small and
powerful, and you already got to know them from the previous chapter, when
we talked about initializing variables through assignment. There are six more
assignment operators in C++, which you will get to know in this section

Remember

As explained in Chapter 2, the other two methods to initialize a variable are


using the curly brackets {}, which is the best and recommended way, and
using regular brackets (), which is called direct initialization style, and is the
less recommended way.

Let's look at a simple example of using the assignment operator =:


#include <iostream>
int main()
{
int a{ 3 }; #A
int b{ 7 }; #B
a = b;
std::cout << a << std::endl;
}

Try to “compile” the code in your head for a second – what will be the
output? in this case, both variables will hold the value 7, as the value of b is
assigned to a, so now a, which was initialized to 3 beforehand, is equal to b,
as shown with the aforementioned equality operator ==:
(a == 7); //true

As you can see, the concept of assigning a value from one variable to another
is simple enough, yet you have to remember the importance of the left side
Vs the right side of the operator. We mentioned rvalues and lvalues in
Chapter 2 when we discussed C-style initialization and talked briefly about
syntax and logic. Now you get a real-life example of the importance of R
(Right) and L (Left) values when dealing with assignments.

Remember

the basic rule is that the rvalue is always assigned to the value of the lvalue.

3.3.1 The anatomy of rvalues and lvalues


Now that you know that every object in C++ belongs to a particular value
category - lvalue or rvalue - you might ask: "Why should I care?". Well, it's a
fair question, and it's important to understand this concept, mostly because
compiler cryptic errors involving L and R values can be difficult to resolve,
and can be avoided if you will understand this concept. Another reason you
should understand this concept is that it can affect how your program will
perform various operations. For example, when assigning values, copying, or
moving objects, L and R values play a significant role.

Before we move on and explain this concept, let's start with Figure 3.2, which
illustrates the relationship between R and L values.

Figure 3.2 R values (on the right side) are always assigned to the L values (on the left side).
Good to know

When the compiler generates machine code from your code, it will analyze
each expression and determine if it's an lvalue or rvalue, and statements will
be read from left to right. This information is then used during the code
generation phase to determine how to generate the appropriate machine code,
which is why lvalue and rvalue are so important to the smooth and flawless
operation of your code.

L is for Location

lvalues are objects which occupy a specific location in memory that can be
accessed. Many use the letter 'L' to remind them it’s all about Location.

In order to simplify this even more, let's look at the following statement:
int i = 10;
The variable i occupies memory space, while the value 10 is merely a
numerical value that can be changed and it has no storage and no location, at
least until we assign it to i – so we assign an R-value to an L-value.

Now let's try something else:


20 = i; // ERROR! This will not work as 20 is not a lvalue
(i + 2) = 20; // ERROR! This will not work as (i+2) is not a lvalue

Both the number 20 and (i+20) are a temporary result of an expression, and
as such they do not and cannot be lvalues (remember: lvalues occupy a
space in memory). There is no sense in assigning them to anything as there is
nothing we can actually assign – as they are temporary. We can however say
this:
int b = i;

The value of i will be stored in b.

The brotherhood of the assignment operators

Now that you understand the mechanism of the assignment operation and the
importance of lvalue and rvalue, let's look at the rest of the assignment
operators in C++.

1. The += assignment operator: this operator assigns a value and


increments it at the same time. Let's look at a simple code and explain it.
In this code, we ask the user to enter his/her age, and then we display
how they will be next year. In order to do that, we assign the age to a
new variable and increment it by 1. Let's see how this should work.
#include <iostream>
int main()
{
int age{}; #A
int older_age{}; #B
std::cout << "Please enter your age:" << std::endl;
std::cin >> age;
older_age = age; #C
older_age += 1; #D
std::cout << "You are " << age << " years old" << std::endl;
std::cout << "Next year you will be " << older_age << " years old" << st
}

When we run this code, this is what we should expect:

When we write new_age += 1 in our code, it is as if we would write


new_age = new_age+1.

2. The -= assignment operator: the -= operator holds the same concept as


the += operator but assigns a decremented value. Let's look at the same
code, this time we tell the user how old the user was last year.
#include <iostream>
int main()
{
int age{};
int young_age{};
std::cout << "Please enter your age." << std::endl;
std::cin >> age;
young_age = age;
young_age -= 1;
std::cout << "You are " << age << " years old" << std::endl;
std::cout << "Last year you were " << young_age << " years old" << std::
}

Note

We changed the variable name from older_age to younger_age, even though


it has no real effect on our code. The change emphasizes the importance of
providing meaningful variable names in your code.

Just like before, when we write younger_age -= 1 it is as if we wrote


younger_age= younger_age - 1.

When we run this code, we should expect the following output:

3. The *= assignment operator: this operator also works in the same way as
its siblings, yet it assigns a multiplier value. In the code below we ask
the user to enter an age and display an age twice as old, stating you are
twice as old.
#include <iostream>
int main()
{
int age{};
int double_age{};
std::cout << "Please enter your age." << std::endl;
std::cin >> age;
double_age = age;
double_age *= 2;
std::cout << "You are " << age << " years old" << std::endl;
std::cout << "Wow! I am " << double_age << " years old - twice your age!
}

This is what we should expect when we run this code:

Like before, when we type double_age *= 2, it is as if we wrote


double_age = double_age * 2.

4. The /= operator: this operator assigns a divided value. At this point you
can take the codes we wrote so far and write your own code, displaying
an age that is half the age of the user's input. Bear in mind that the result
might be an odd number, so you should use a double instead of an int.
Let's call this variable half_age (you can choose your own name of
course).

Remember

Writing half_age /= 2 is like writing half_age = half_age / 2.


5. The %= assignment operator: this operator assigns the remainder of a
division of two variables. Using this operator when we write X %= 4 is
as if we would write X = X%4.

As you can see, this is a quite simple concept to understand and is often used
in C++.

3.3.2 The promise to never break a promise: assignment and


const

Before we move on to the next section and learn about other operators, let's
not forget the concept of constants (const), which was introduced and
explained in Chapter 2.

Here's a brief reminder: a constant is like a promise not to be broken to keep


the same value of a variable, with no ability to change it. As we talk about the
assignment operator, we cannot and must not forget that it will be impossible
to assign any value to a const variable once it has been initially defined, as it
might result either in a compiler error.

Figure 3.3 is a compilation error when we are trying to assign a value to a


const variable.

Figure 3.3 When we try to assign a value to a const variable, Visual Studio does not allow the
code to be compiled and shows a compilation error saying, “You cannot assign to a variable that is
const.”
Note

A list of all the C++ assignment operators can be found in Appendix H.

3.3.3 Unary operators - increment and decrement have never


been easier
A unary operand means an operator which only works with a single (unary)
operand. Many, but not all arithmetic operators can, and do work with more
than one operand (float, integer, etc.), while the unary operator will only
work with one.

Unary operands are commonly used in C++ - you are probably going to use a
lot of them in various codes. Let's look at some more code samples. This time
we are going to use the increment (++) and decrement (--) unary arithmetical
operands.

The ++ (increment) unary operator

The ++ operator increments a single operand by the value of 1. Let's see an


example:
int a = 2;
int b = ++ a;

In this case, b will be assigned the value of 3.

But what if we use the statement


int b = a + 1;

Will it be the same as saying int b = ++a;?

The answer is no. Let’s understand why: the statement int b = ++a; not
only assigns a value to b but also changes (promotes) a, while the statement
int b = a + 1; only assigns a value to b.

Moving on, we can also write the following statement:


int a = 2;
a++;

The result of a++ and ++a will be the same, but is there a difference between
the two? Yes, there is: The statement ++a is a prefix value, which means it
was first changed and then used, while the a++ is a post fix value, which
means it was first used and then changed. Let's assume we have an initial
value of 0 and we use the following statement:
int b = a++;

The statement will assign 0 to b, and then promote a, so b will be equal to 0


and a will be equal to 1.

If we use the following statement:


int b = ++a;

The statement will promote a first and then assign its value to b, so both b
and a will be equal to 1

The – (decrement) unary operator

Just like the '++' unary operator, the '--' (decrement) operator decrements a
single operand by the value of 1. Let's see an example:
int a = 2;
int b = -- a;

In this case, b will be assigned the value of 1

We can also write


int a = 2;
a--;

Both scripts will have the same results, but just like the ++ increment
operator, the --a has a prefix value, which means it was first changed and
then used, and the a-- is a post fix value, which means it was first used and
then changed.
This is all pretty easy, but why do we even need to use these methods to
increment and decrement? Well, there are many reasons and use cases when
we need to use them in real code. For example, when various elements in
your program need to go up and down a ladder with a counter. One step up
the ladder (++) then one step down (--), or maybe just going up until the last
step of the ladder, in this case, we will have to set some condition or rule for
counting steps. And indeed, incrementing and decrementing are used in many
cases for counting, mostly in loops, which you will learn all about in the next
chapter. A loop iterates and each iteration increments a counter by a given
value.

An example from real life would be running in circles: Each time we


complete a loop we count up. We can also pre-set a limit to the number of
loops that will be conducted. For example, set up 1 glass of water at the end
of each loop, so every time you run a loop you drink one until all 10 glasses
are empty, and you know your loops are concluded. Figure 3.4 illustrates the
flow of this sample.

Figure 3.4 A demonstration of the flow of a simple loop. Each time a loop is concluded, as long as
another one starts, we increment the loop cycle by 1 (++).
Can you think of an example for counting down? How about a program that
counts the number of items on a webshop. Each time an item is purchased,
we decrement by one until there are 0 items, then an "out of stock" message
will appear. Of course, in real code, we structure the use of ++ and -- in
various ways, as there are tons of use cases. We will also use unary operators
a lot in this book, so keep them in mind as they are very handy.

the sizeof() unary operator

There are several unary operands, one of them is not in the shape of a symbol
or a sign, but in a form of a "word". It’s called sizeof(), and it is used to
display the size in bytes of any data type.

Let's use sizeof() to take a look into the actual size of some variables you
already know. All we need to do is to place the type we want to determine the
size of in parenthesis while using 'std::cout' in order to print the result to
the console.
#include <iostream>

int main()
{
std::cout << "The size of int is " << sizeof(int) << std::endl;
std::cout << "The size of double is " << sizeof(double) << std::endl;
std::cout << "The size of char is " << sizeof(char) << std::endl;
std::cout << "The size of float is " << sizeof(float) << std::endl;
std::cout << "The size of long is " << sizeof(long) << std::endl;
std::cout << "The size of long long is " << sizeof(long long) << std::endl;
std::cout << "The size of short is " << sizeof(short) << std::endl;
}

Figure 3.5 shows the output we should expect when executing this code.

Figure 3.5 When using the sizeof operator, which is a unary operand (i.e. works with a single
operand), we can measure the size in bytes of various data types.
Tip

sizeof is dependent on the machine and operating system that the program is
running on. The size of data types can vary depending on the platform and
architecture. For example, on a 32-bit system, an integer might be 4 bytes in
size, while on a 64-bit system, it might be 8 bytes, so keep this in mind if you
ever write portable code that needs to run on different systems.

Note

A full list of all C++ unary operators can be found in Appendix E.

3.4 If-else statements – control your programs’ flow


Before we move on and take a look at some more C++ operators, let’s talk
about flow control for a second.

One of the main roles of a programmer is to create programs that use flow
control. By controlling your program's flow, your program will be able to
perform conditional-based decisions. In real life, you can set a condition
followed by outcomes. For example, your mom might have told you: "If you
don't eat your greens, no desert!" (And we've all been there..).

Some C++ operators work best within a conditional statement, which is why
it’s important to learn how to use the if-else statement, before moving on.

In C++ (and other programming languages as well), and as the name if-else
implies, it asks a question: if (you don't eat your greens) then (no dessert).
But what if we want to set a flow of conditions up to a very complex level, or
a grid of conditions. In complex conditional cases, your program should be
able to perform conditional-based decisions with multiple alternatives – this
is where else comes into the picture – and we get to that in a minute.

In terms of C++ as a language, the use of if and else is noticeably clear, and
we use these terms in real-life languages. This simplifies our ability to write
clear code and understand code written by others.

When using an if statement, our program evaluates if a statement is true or


false, and acts upon it, allowing your program to respond to a condition.
Working with comparison operators, for example, is a wonderful example, as
you can evaluate the size relation between one or multiple operands in a
simple-to-use statement – and you will learn all about that in this chapter.

Think of a program that checks if the CPU level is at a certain level: If the
level exceeds a certain value, the program shuts down a process that runs in
the background. If the CPU level does not exceed a certain level, the program
does nothing. Let’s look at an if-else flow chart that illustrates an if-else
statement for this example (figure 3.6).

Figure 3.6 The flow chart of an if-else statement – if our condition is true, we execute a statement,
if false, we do something else (in this case, do nothing).
Now that you understand the logic, let's look at the syntax, which derives
directly from the same logic. Figure 3.2 illustrates the way the if-else syntax
is constructed. We added to it a layer of the same example illustrated in
Figure 3.7, so you can really understand how the logic is translated into a
working code that makes real sense.

Figure 3.7 The syntax of an if-else statement.


What you need to pay attention to is the use of brackets: We have two types
of brackets we use in an if-else statement. The first set is the regular brackets
if (condition). The second set is curly brackets { }, which contain our
statement (what will happen in case the condition is true, or what will happen
if it’s false), as the syntax below demonstrates:
if (condition)
{
Statement;
}
else
{
Statement;
}

Note

You might have also noticed that as mentioned in Figure 3.2, using the else
statement is not a must, and we can do without it. It’s true. We can write a
code using a standalone if statement, without using an else statement in
case the condition is false, and sometimes we do. Also, the curly brackets are
optional (as long as the statement consists of a single line), but recommended
using it, just in case you add additional lines in the future.

Let’s practice some code. In this code, we check if a student's grade is


passing or failing. The user will input a grade, and if it is less than 60, the
program will print "You have failed the course", but if the grade is 60 or
higher, the program will print "Congratulations! You have passed the
course". Note that we are using the < (less than) comparison operator, which
should be familiar to you, yet in this chapter, we go back and talk a bit further
about comparison operators in C++ and how they are used.

Listing 3.2 Using if-else statement to check passed or failed exams

#include <iostream>
int main()
{
int grade;#A
std::cout << “Enter your grade: “;
std::cin >> grade;#B
if (grade < 60) #C
{
std::cout << “You have failed the course.” << std::endl;
}
else #D
{
std::cout << “Congratulations! You have passed the course.” << std::
}
return 0;
}

Try running this code using various inputs and test the results from passing to
failing the course.

As you can see, the use of if-else statements is pretty simple, and makes
sense on a syntactical level, if we look at C++ as a type of spoken language,
telling the machine how and based on what to make a decision and act upon
it. Like bread and butter, it's pure, and basic, and serves as a strong base for
what's to come next.

Tip

if you find the syntax a bit confusing, remember that since if-else statements
are so commonly used, soon enough the if-else syntax will become second
nature. In this chapter, we will use a lot of if-else statements together with
some operators, as well as throughout this book, so practice as much as you
can, and pretty soon you will feel very confident.

Never run out of style – why indentation style matters

Before we move on, there's one important subject that needs readdressing. In
Chapter 2 we mentioned the importance of code indentation and explained
there are several styles to indent your code. We showed you our preferred
style, which is much neater in our opinion, and more organized (Allmani
style). When we deal with more complex structures to our code, such as if-
else statements, and since we use a lot of brackets, which we always need to
open and make sure they will be closed at the end of the statement, the
indentation can make a difference between serenity and mayhem. Many
programmers, including companies who require you to work per their own
style, use the K&R variant style, which, when working with if-else
statements will look like the following example
if (your condition in parenthesis) {
//your code – what will happen if your statement is true
}
else{
// your code – what will happen if your statement is false
}

We personally don't like the K&R variant style and do not recommend using
it, even though it's commonly used. You can clearly see it's not as clear as our
style, especially for beginners. We think K&R is a bit more "messy" visually
and harder to read and follow than the Allmani style, which we personally
use. When you dive into complex if statements, with a lot of ‘if-else’ which
are all part of the same statement (also known as ‘nested if’, as explained in a
bit), the K&R style might become even harder to handle.

3.4.1 Nested if statements – an ‘if’ statement within an ‘if’


statement
Sometimes our condition contains more than just two outcomes, and we need
to create a more complex decision-making mechanism. This is where nested
if statements come into the picture. Nested-if statements provide a more
complex flow which is often needed. As implied by the name, nested if
statements are "nested" within the original if statement and even nested
within a nested statement, so we get a pyramid-like hierarchy where one
statement is placed within another. Let's look at a basic nested-if flowchart as
illustrated in Figure 3.8

Figure 3.8 A basic nested if statement has a pyramid-like hierarchy where one statement is placed
within another.
As you can see from the flowchart, in case the result of the first if statement
is false, we move on to the second nested if statement. So, in this case, we
have two layers of statements in the flowchart: first, the outer layer (if
statement) and then an inner layer (nested if statement). We can of course
nest multiple layers of statements. Think of a “nested” structure as if they
were a Russian matryoshka doll (also known as “Babushka”), where one tiny
doll is nested inside a bigger doll, which is nested inside an even bigger doll,
and so on.

Let's look at a simple and basic example using a driving license eligibility
code. The user is required to enter his age, and the program tells him if he is
eligible for a driving license. When we design this little program there are
many options we can think of: what will happen if the user types '0' as age, or
'200'? You might want to set some additional rules which will prompt an
output, such as "you are too old to drive", "error, this is not a valid age", etc.
This would be a great practice using nested if statements, which are suited
exactly for these use cases.

Note

in this code, we will use two comparison operators you don't know yet: the
<=, which checks if a value is less than or equal to another value, and the >=,
which is the same as the first, but checks if a value is greater than or equal to
another value. Later in this chapter we go back to these operators and provide
some more examples for using them.

Listing 3.3 Code practice – Check if you are eligible for a driving license

#include <iostream>
int main()
{
int age;
std::cout << "Please enter your age" << std::endl;
std::cin >> age;
if (age >= 16) #A
{
std::cout << "you are over 18 years old" << std::endl;
if (age <= 100) #B
{
std::cout << "you are eligible to drive" << std::endl;
}
else
{
std::cout << "but you are too old to drive" << std::endl;
}
}
else
{
std::cout << "Sorry, you are too young to drive" << std::endl;
}
}

Try running this code and enter ages under 16, over 16, and over 100 and test
the results. We tried running it and entered the age 101, which resulted in the
following output:

You can now try and create your own version of if-else statements. Try
writing a small game in which the user must pick a number, unless the
number is correct, the user gets an error message. Remember you must create
a variable with the number the user needs to guess.

3.5 Setting conditions using logical operators


Logical operators are a little 'spark' that makes your program "think" and
operate based on a logical outcome. Logical operators are so useful and
important, and you will see and use them in most, if not all, of your future
codes.

The basic concept is that logical operators are used when we are combining
conditions or constraints with a Boolean output which means a true or false
result, (As you might recall, you learned about Boolean variables in the
previous chapter). Boolean output means the outcome can be either true or
false. For example, we could use a Boolean variable in a code to indicate if
the user entered an input (or a valid input), or not. To do so, we will need to
assign true or false to this variable after checking the user's input.

In real life, we make decisions based on logical conditions all the time. We
can ask: is it raining outside? If yes, take an umbrella. If not – no need for an
umbrella. The basic concept in computer programming is just as simple, and
the use of these logical operators can become even more powerful and robust,
especially in C++. All you need to do is learn some basic rules which we
explain in depth in this section.

In C++ there are three types of logical operators:

1. The operator && means "and"


2. The operator means "or"
3. The operator ! means “different than”

Before we demonstrate how it will work within your code, let's understand
the very basic lingual logic via a real-life example. Let's say we need to
decide whether to go out for a run on a cold and windy day. There are three
possible conditions we might consider:

1. We won't go for a run while it's cold and windy at the same time.
2. We will go for a run only if it's windy but not cold.
3. We will go for a run only if it's cold but not windy.

Let's examine the first scenario. In this case, we will be asking:

Is it windy outside? AND is it cold outside?


If the answer to both questions is a yes, we will stay at home. However, if the
answer to the first question is a no – we don't need to check the other answer,
as it's enough that one condition is false, so we are good to go out for a run in
this case. Figure 3.9 illustrates the different scenarios.

Figure 3.9 Our three different scenarios: first, it’s windy and cold (we stay at home), second, it’s
not windy but it is cold (we go out for a run), and third, it’s not cold but it is windy (we go out for
a run).

The second and third scenarios are if we want to go for a run if only one of
the conditions is true. So, in the second scenario, if it's not windy yet it's cold
outside, we will still go running, and in scenario three, if it's windy but it's not
cold outside, we will go running as well. If it’s not windy and not cold, we
will of course go out for a run. The question we will ask in this scenario is
slightly different:

Is it windy AND cold outside?

As part of the logic we have defined, we need both conditions to be satisfied


in order to stay at home, so we must check both conditions (cold and windy).

Had we defined our logic as such, that when it is either windy or cold, we
stay at home, we would have only had to check one of the two conditions to
get to a resolution as illustrated in figure 3.10:

Figure 3.10 If it’s windy, we need to check if it’s also cold outside in order to decide if we go out
for a run or not.

If the answer to the first question is false (not windy) we then need to move
and check the answer to the second question (figure 3.11):

Figure 3.11 In case the result of the question "Is it windy" is false, we check if it's cold.
Table 3.2 presents the logic behind the &&(AND) logical operator.

Table 3.2 The logic behind the && (AND) logical operator, demonstrating all the possible
outcomes and their results
Table 3.3 presents the logic behind the || (OR) logical operator.

Table 3.3 The logic behind the || (OR) logical operator, demonstrating all the possible outcomes
and their results.
To sum up, In the first scenario, the use of && (and) all conditions must be
true in order for us to stay home.

In the second scenario, the use of || (or) only one condition must be true in
order for us to stay home. This is simple and straightforward.

Let's write a simple code that uses this logic in order to decide if the user will
go out for a run, or stay home. In the code you are about to write, the user is
asked to enter the temperature and the wind speed. Try to read it and
understand it yourself, as by now you should be able to understand it.

Listing 3.4 use logical operators && and ||

#include <iostream>
int main()
{
bool isWindy = false;
bool isCold = true;

if (isWindy && isCold)


{
std::cout << "I am not running" << std::endl;
}
else
{
std::cout << "I am running" << std::endl;
}
}

Let’s analyze this code for a minute: you can see that we set a conditional and
&& operator, meaning that if it’s both cold and windy, we stay home. We also
already set one variable (isWindy) to true, while isCold was set to false. This
is pretty simple.

You may rightfully ask what is the result of the condition we are aiming for?
The answer is that when we are not stating the "what", or specifying the
conditional result, it means a case when the result of the condition is true by
default. You will see a lot of code with no specified conditional outcome, so
keep this rule in mind. Using a statement such as the one we used,
if(isWindy && isCold),
to be the same as if we wrote:
if (isWindy == true && isCold == true).

Same way, had we asked


if (!isWindy && !isCold)

it would be the same as


if (isWindy == false && isCold == false)

Let's make some more changes to the code, this time we ask the user to input
the temperature, and unless it's higher than 5 °C, we will not go out for a run.

Listing 3.5 Code practice – will we go out for a run – using an if-else statement

#include <iostream>
int main()
{
int temperature{ 0 }; #A
int wind_speed {0}; #B
bool isWindy = true; #C
bool isCold = true; #D
std::cout << "What is the temperature (in Celsius)? ";
std::cin >> temperature;#E
std::cout << "How strong is the wind outside from 1 to 10? ";
std::cin >> wind_speed;#F
if(wind_speed >5) #G
{
isWindy = true;
}
else
{
isWindy = false;
}
if (temperature <= 5) #H
{
isCold = true;
}
else
{
isCold = false;
}
if (isWindy && isCold) #I
{
std::cout << "I am not running" << std::endl;
}
else
{
std::cout << "I am going for a run! Yeah!!!" << std::endl;
}

Let's run the code and enter a temperature of 41 degrees with a wind speed of
3. The output should be:

Important! Priority in logical operations

C++ sets a hierarchy, which means some operations are a higher priority than
others. The hierarchy in logical conditions will always be set from left to
right. It means that when we use the && (AND) operator if the condition on
the left is false, then the program will not go further to check whether the
right-hand side condition is true, as BOTH have to be true. In the case of ||
(OR) condition, if the left-hand side is false, then the program will check the
right-hand side condition. There is a lot of sense to this, as you can also see
and understand from the real-life questions we examined earlier.

In some cases, we might even use multiple conditions, using the && and the
|| operators under the roof of a single statement, creating more complex
conditional expressions. BUT, in this case, the rules of precedence take place.
The && (and) operator has higher precedence than the || (or) operator. It
means that when executing your code, the compiler will first check the
conditions giving && precedence and only then move to check the ||
condition. We will demonstrate some examples of this hierarchy later in this
chapter.

However, note that in some cases, parentheses can be used to group


expressions and enforce the desired order of evaluation, regardless of
operator precedence – but we will not go into that in this book.

Elvis has left the building – the ?: ternary Conditional Operator

There is another wonderful way to simplify our code and use more simplified
expressions. The ?: conditional operator is a great example of the use of an
extra layer of simplification of our syntax and can be of great value to you as
a coder. As you may recall from Chapters 1 and 2, simplification in
programming languages helps us to "talk" to a machine and control it by
using the simplest forms of words, and expressions. For you, it simply means
less coding. The rule of thumb is, that the more evolved the language is, the
simpler it becomes to the speaker, or, in this case, the coder.

Good to know

The ?: ternary operator is also called "The old Elvis operator", due to its
resemblance to Elvis Presley’s hairstyle, which resembles the two dots and a
question mark.

Let’s see how the ?: operator makes our life even simpler when we want to
set conditional values. First, let’s look at the syntax used with this expression:
Variable = Condition? Result1 : Result2

If the condition is true, then Result 1 will be chosen (executed) and assigned
to the variable. Note that we highlighted the word “if” in this case, as the ?:
conditional operator is very similar to the if-else condition you will learn
about in the next chapter. The concept is simple enough: in spoken language,
we could say: "If you finish your spinach then you will get an ice cream for
dessert, or else you will get an apple for dessert”. The concept in C++ works
the same way as using the conditional ?: operator.
If the condition is true, Result 1 will be executed (get ice cream), while if the
condition is not true (false), then Result 2 will be executed (eat an apple).
Pretty simple, right? Since the ?: is based on both a question mark and colon,
we can also say that:
True = ?
False = :

Figure 3.12 illustrates the flow of events when using the ?: conditional
operator.

Figure 3.12 The flow of the ?: conditional operator, which can handle three expressions, is
simple: the result depends on the condition’s value: true or false. Each result will lead to a
different outcome.
Because we use three expressions (condition, result 1 and result 2) the ?:
conditional operator is also called a ternary operator.

Good to know

In Latin, the word "ternary" means "three at once", hence ternary relates to
three expressions.

Tip

Using the ternary operator ?: is like saying "Today I am going to " … then
waiting and checking if it's raining or not, and based on the result, say what I
am going to do today. The if-else statement's logic is a bit different: we say
"If it's raining I am not going for a run, else, I am going for a run.

Listing 3.6 demonstrated a code using && and || operators to make a decision
based on the conditions (is it cold and is it windy), to decide if should go for
a run or not.

Now, let's write a small code that takes the question from the previous section
"Should I go out for a run" as in listing 3.5, and this time we use all the
conditional operators you learned so far. This code is a great exercise which
can help you better understand the use of these operators. Let's use a small
code in order to decide whether to go out for a run or not.

Listing 3.6 Using conditional operators

#include <iostream>
int main()
{
bool Wind{ false };
bool Cold{ false };
bool Running{ false }; #A
Wind = true; #B
Cold = true;
Running = !(Wind && Cold); #C
std::cout << "Today I am " << ((Running) ? "" : " not ") << "going to ru
}

Try to change the test values in the code from true to false and check the
result. Will you go out for a run today or not?

3.6 Setting conditions using comparison operators


Earlier in this chapter, you had the chance to use the <=, >= and <
comparison operators in code, and you saw how important and valuable they
can be. We use comparison operators in C++ (as well as in other
programming languages) in various scenarios, such as when we need to
perform some form of sorting and need to determine which element is greater
or smaller than the other.
The comparison seems straightforward - something is either equal, different,
smaller, or bigger than the other, and it is. In fact, the comparisons in C++
have remained largely unchanged over the years, however, the latest C++20
standard has introduced a new operator known as the three-way- comparison
operator, or "the spaceship operator", due to its unique shape <=>. Unlike the
regular comparison operators used before C++20, which only conducted two-
way comparison, the new spaceship operator can conduct three comparisons
at once – and we get to it shortly, but not before we go over the rest of the
comparison operators.

Note

You can find a table of all the C++ comparison operators in Appendix F.

In C++ we have six good old comparison operators as illustrated in Figure


3.13.

Figure 3.13 C++ six comparison operators.

Let's go over each operator briefly and understand its role.

3.6.1 Less than or greater than


The first two operators are easy and familiar: less than (<) or greater than
(>). Both operators take us to two other comparison options: less than or
equal to (<=), and more than or equal to (>=).
By using these operators, we can conduct two comparison valuations at once,
so we can evaluate if an operand is smaller or equal to another operand or if
is it larger and equal then another operand.

On the simplest mathematical level, the results for each comparison can be a
Boolean value – meaning it can be either true or false. This is all quite
simple. Let's write another basic code sample to demonstrate the use of these
basic comparison operators. In this code, we ask the user to enter his/her age,
and using the comparison operators with conditional operators and if-else
statements, we print to the console what the user is or isn't allowed to do in
his/her age (vote, buy alcohol, rent a car).

By this point, you should be able to read and follow this code with good
understanding.

Listing 3.7 Compare to integers and print the result

#include <iostream>
int main()
{
int age;
std::cout << "Enter your age: ";
std::cin >> age;
if (age < 18) #A
{
std::cout << "Sorry, you're not old enough to vote." << std::endl;
}
else if (age >= 18 && age < 21) #B
{
std::cout << "You can vote, but you can't buy alcohol." << std::endl
}
else if (age >= 21 && age < 25) #C
{
std::cout << "You can vote and buy alcohol, but you can't rent a car
}
else if (age >= 25 && age < 79) #D
{
std::cout << "You can vote, buy alcohol, and rent a car. Enjoy!" <<
}
else #E
{
std::cout << "You can still vote and buy alcohol, but you're too old
}
return 0;
}

When we run this code, this is what we should expect if we enter the age of
23:

As you can see, the basics for using these comparison operators are easy, so
now we can move ahead and talk about the equal to == operator.

3.6.2 Not all values were born equal: the == and != operators
C++ has two more comparison operators: the equal to operator ==, which
checks if two values are equal. However, there is another important
comparison operator, the not equal to operator !=, which checks if two values
are not equal. Let’s look at our first example, which checks if the variable i
and equal to the variable j:
#include <iostream>
int main()
{
int i = 100;
int j = 200;
if (i == j)
{
std::cout << "i == j" << std::endl;
}
else
{
std::cout << "i != j" << std::endl;
}
}
The output should be “i != j”

Now let's take a look at an example of using the not equal to operator != in
the same code:
#include <iostream>
int main()
{
int i = 100;
int j = 200;
if (i != 200)
{
std::cout << "i != 200. i is equal to " << i << std::endl;
}
else
{
std::cout << "i == 200" << std::endl;
}
}

The output should be “i != 200. i is equal to 100” .

As you can see, using these two operators is simple and straightforward.

“Equal, or equal not. There is no try” (Yoda)

Yoda, the Star Wars character, is known to speak in a reversed word order
and unique syntax. One of his iconic expressions includes "Do or do not.
There is no try.", (and yes, the heading to this section is a homage to this
statement). What does Yoda have to do with C++ and the == comparison
operator, you might ask. Well, as Yoda would say: “Patience you must have,
my young Padawan.”.

There's only one type separating the == and the = operators. If you type =
instead of == by mistake, you will assign a value instead of comparing a
value. Let’s look at the following code for example:
#include <iostream>
int main()
{
int i = 100;
int j = 200;
if (i = 200)
{
std::cout << "i == 200" << std::endl;
}
else
{
std::cout << "i != 200. i is equal to " << i << std::endl;
}
}

The output of this code will be i==200, and that’s not what we intended for,
as it’s clear the i should be equal to 100.

Do you see what happened in this code? Instead of comparing (i==200) in


our if statement, there’s a typo, and we wrote (i=200). This is why now 200
was assigned to variable i. Since == and = are so similar, the Yoda practice
(also known as the Yoda notation, or Yoda condition) intends to help avoid a
mistake by inverting the operands intentionally. In other words: make an
intentional mistake to remind you to avoid a mistake. As a result, the program
is forced into a syntax error and will not compile, forcing you to pay attention
to your code and avoid the = typo.

Let’s go back to our code and use the Yoda practice. Instead of typing i=200
(by mistake), we prepare ourselves for a typo and “Yoda”. In this case, we
type 200=i.

Now, using the Yoda practice, the code will be:


#include <iostream>
int main()
{
int i = 100;
int j = 200;
if (200 = i)
{
std::cout << "i == 200" << std::endl;
}
else
{
std::cout << "i != 200. i is equal to " << i << std::endl;
}
}
This code won't compile (remember: a number has no storage in memory, so
it cannot be a left value), “helping” you to remember: Hey, you forgot to use
==.

Yoda practice is common, and in many cases, when you browse source
codes, you may find that in many code snippets, the condition (n == 0) is
revered to (0 == n).

We believe the Yoda practice is important to be familiar with since it's out
there, but though it’s commonly used, we do not recommend using it. First,
this practice is not universally accepted. Second, we believe that when this
practice is used, it makes the code harder to read. In fact, the only
justification for the Yoda conditions practice is to prevent a common mistake
where = is used instead of ==. That's a typical bug, but when you learn and
practice, you will easily avoid it without the need to use these types of
practices. Compilers can usually be instructed to warn you if suspicious code
is detected, but there are also static code analysis tools to help you find such
mistakes, such as CPP Check and more.

3.6.3 Compare à trois - introduction to the three-way


comparison
The brand new, three-way comparison <=>, also known as the spaceship
operator due to its spaceship-like shape, was introduced in C++20. This
operator can return the result of three comparisons: the less than operator <,
the equal to operator ==, and the greater than operator > which combined
constructs this: <=>.

Until C++20, the comparison operators were the same for years. All six
operators you just learned about were untouched and their roles were clear
with a straightforward set of rules for how to use them: When comparing two
elements, we normally would require a Boolean value (yes/no): is one
bigger/smaller/equal/incomparable to another. So, the flow would be: is A <
B, and based on the result, check is A > B. If none of these options is true, we
can say that A == B, but this would be out of the scope of our query, since our
quarry only returns "true" or "false". The conclusion is that we need two
queries to check three possible outcomes. Figure 3.14 illustrates the flow of
two operands comparison as it was used before C++20.

Figure 3.14 Comparison operators perform the comparison between two operands and make
decisions based on a true or false outcome. In this case, if A<B is false, and if A>B is false, then
A=B.

The spaceship operator – it's not rocket science

For many C++ developers, and especially those new to C++, the three-way
comparison operator is somewhat confusing at first glance. Many developers
struggle to understand where this spaceship "landed", and why was it needed
in the first place, especially as until now, everything seemed to work just
fine.

Note
In this section, we explain exactly that. However, note that the explanations
in this section are partial and only refer to what the three-way comparison is,
along with its basic logic. You will learn more about this operator and how to
practically use it in Chapter 5, where we demonstrate how to use it to
compare strings.

So what is the comparison operator? Let's look at Figure 3.9 again: you can
see that using the regular operators, we are asking one question. For example,
is A< than B. Though we have three possible outcomes, we only get two
possible outcomes out of the three. It means we need to make another query.
Let's see what we need to ask in this case:

Question 1: is A greater than B?

Then based on the answer you ask the 2nd question:

Question 2: is A equal to B?

The new spaceship operator allows us to conduct the same compression with
one single query. We can use it whenever values are compared
using <, >, <=, >=. It's like going out on a blind date and asking: "Tell me
everything I need to know about you." The three-way comparison allows you
to get your answers in regard to the question: "What can you tell me about the
relationship between A and B."

With the three-way compression you only ask one question and get one of
three results:

1. A > B
2. B > A
3. A == B

Figure 3.15 illustrates the very basic logic behind the three-way comparison.
As you can see, in case A<B the three-way comparison function will return a
number smaller than 0. In case A=B the three-way comparison function will
return a number equal to 0, and in case A>B the function will return a number
> 0.
Figure 3.15 Three-way comparison returns 3 possible results, while the old-time comparison
would only return 2 possible results out of three.

The immense value of this new operator is the fact that by using it, we
consolidate the full set of three conditions into one single sassy query.

Good to know

The three-way comparison methodology is also called trichotomy – division


into three categories. We don't always need to use trichotomy in our code, but
when we do, the spaceship operator is very handy and can simplify our code
a lot.

As code is stronger than words, let's look at the two code snippets below,
which by now you should be able to read and understand easily.

SAMPLE 1:
A <= B && B <= A

In this sample, we ask: is A smaller than or equal to B, and is B smaller than or


equal to A?

SAMPLE 2:
!(A < B) && !(B < A)

In this sample, we ask: A is not smaller than B and B is not smaller than A.

Now, we can consolidate all this messiness into a small clean statement:
A <=> B

How simple and effective is that? Obviously, we need to use simple examples
to simplify the explanation of a complex concept, but you can probably guess
that in real-life code, we will not use <=> to compare simple objects or
integers – there's no point in that. The logic behind adding the three-way
comparison operator was to simplify comparison between complex types,
such as strings, or lists/sequences of elements (vectors and arrays for
example, which you will learn about in the next chapter). In Chapter 5, when
we introduce strings, we also talk about the three-way comparison and
demonstrate a useful way to use it.

The old way of doing things: the memcmp() and strcmp()

If you know a bit about C++ already, you might remember that before the
introduction of the spaceship operator in C++20, developers often used the
older functions memcmp() and strcmp() which came from C, or basic
string::compare(), which came from modern C++. These functions
returned an integer indicating positive or negative values, but it was a
complex and convoluted process. This is one of the main reasons the
spaceship operator was created - to provide a more streamlined and efficient
method for comparing strings, as well as other data types and data structures.

Important

In order to use the three-way comparison in your code, you must include the
header file and library <compare> which contains all the necessary functions
required for the three-way comparison.

3.7 Bitwise operators – get into the bit (literally)


In computer science, a bit is a basic unit of information with a binary value of
either a 1 or a 0. The name "bit" derives from the expression "binary digit",
so in short – "bit." You probably heard the expression "bit and byte", and no,
they are not the same. A byte is a basic unit of information in computer
storage and processing, and it contains 8 adjacent binary digits – which are
called bits. In other words, bits are in the heart (bit) of every byte.

Let's look at a simple diagram of the bit and byte's structure (figure 3.16).

Figure 3.16 The way bit is structured within a byte. Each byte contains 8 adjacent binary digits
(bits). In this example the value of the byte is 1 * 2^0 + 0 * 2^1 + 0 * 2^2 + 1 * 2^3 + 0 * 2^4 + 0 *
2^5 + 0 * 2^6 + 1 * 2^7 = 137.
3.7.1 Why do we need to micro-manage bits?
In the first chapter, when we were talking about the great advantages of
learning C++, we were talking about the capability it provides in micro-
managing each component related to your program, and this statement is true
up to the bit level. For this purpose, we use the bitwise operators, which
come in different types, each designed to handle a different use case.

Once upon a time, there was a computer named Amiga

In 1989, Michael Haephrati (one of this book's authors), invented the first
Multilingual Graphical Word Processor for Amiga computers, named
Rashumon[1]. It was developed using an IDE named Aztec C. Rashumon was
innovative at that time, as its unique feature required developing multilingual
and graphic capabilities from scratch and at the lowest level. For example, to
maintain the current attributes of each character, such as Bold, Italics, or
Underline (figure 3.17), bitwise operators were used.

Though Amiga computers are a thing of the past, bits are not. And yes - in
the good old jolly days of computer programming, a bit meant a lot, which is
why they were micromanaged to death.

Figure 3.17 Rahumon multilingual graphic word processor, created and developed by Michael
Haephrati, was the first of its kind and used bits to manage letter's attributes – bold, italic,
underline, etc.

Now we might need to use bitwise operators in specific cases, mostly when
we must work closely with the core of the machine, developing drivers for
example, or working with embedded systems or reverse engineering. So yes,
bits matter a lot even in today's programming, and we use bitwise operators a
lot in the code we write.

Bitwise operators are a kind of "linguistic molecules", and they allow for
storing several pieces of information in a single variable. A typical example
would be storing different attributes in a single integer, by storing each
attribute in each bit. Let's use the word processor example. When we work
with a variable that is packed with bits, we can decide that each bit will
represent a different attribute such as text style: we can have a text styled as
Bold, Italic, and Underline. When we switch to each style, we need to know
how to turn a bit on and off – on will turn the style on, and off will turn it off.
We use bit so we can allow setting several attributes for each character in our
text. Let's look at an example of such attributes.

Let’s say we define an integer variable and call it currentStyle:


int currentStyle{0};
Then we define the attributes we wish to allow setting or unsetting:
currentStyle_NORMAL
currentStyle_BOLD
currentStyle_ITALIC
currentStyle_UNDERLINE

Next, we would want to define a value in bits for each attribute, whilst
assigning a specific unique bit to it, so each attribute can be set alongside
other attributes. Let's take a look:
#define currentStyle_NORMAL 0x00 // 0000 0000
#define currentStyle_BOLD 0x01 // 0000 0001
#define currentStyle_ITALIC 0x02 // 0000 0010
#define currentStyle_UNDERLINE 0x04 // 0000 0100
#define currentStyle_STRIKETHROUGH 0x08 // 0000 1000

We can illustrate our little program using checkboxes, each checkbox


represents an attribute. For the sake of this explanation, we created a small
mock user interface to illustrate how it should work (figure 3.18).

Figure 3.18 A sample mock interface created to illustrate a program that allows the user to
choose a letter style.

The default state of the program is currentStyle_NORMAL, and we have three


more styles, each one can be defined as part of a set of bits. Before we
defined a set of bits, and give each bit a meaningful purpose, let’s understand
what the bits will do, as we would want to be able to do three things:

1. Turn on a bit (for example, turn on “Bold” or turn on “Italic”)


2. Turn off a bit (for example, turn off “Bold” or turn on “Italic”)
3. Check if a bit is on or off, (for example, check if Bold is on).

To do so, we define the bits:


#define BIT_BOLD 0x001
#define BIT_ITALIC 0x002
#define BIT_UNDERLINE 0x004
#define BIT_STRIKETHROUGH 0x008

Each bit in the following attribute holds a defined job. Each time we want to
change the style of our characters, a bitwise operator will perform
manipulation on the specific bit in charge of the desired operation. Here is
our mock interface again, this time we checked all the boxes (Figure 3.19).

Figure 3.19 All the boxes are now checked in our pretended program, meaning all attributes are
turned on.

In Figure 3.17 the first column to the right is Bold, and you can see the
"checkbox" for this attribute, followed by Italic Underline and Strikethrough
are all turned on. On a bit level, it means the value of our variable int
currentStyle will now be 0x0F (15). Why? Let’s look at the bits again:

As you can see, if we check all the boxes, we “use” all the bits, so the value
will be 0x0F. 1+2+4+8 (15) which is 0x0F.

Remember

When the boxes were not checked, and currentStyle was normal, the value
was 0x000.

As you can see, we use bitwise operators in order to modify variables


according to their bit level. We are not going to dive very deep into bitwise
operators in this book, as this can be a complex subject, yet an interesting
one. In this section, we just want to give you a 'bit' of a taste and help you
understand how little bits can be a big part of your code.

3.7.2 Let's think for a bit


Now that you understand the basic unique role of bitwise operator, and after
this long opening introduction, it's time to introduce you to the very basics of
the bitwise operators and how they are used in C++.

Below is a list of the bitwise operators along with a diagram for each, so you
can see and understand how they affect the actual bits.

1. Bitwise LEFT SHIFT (<<) shifts bits to the left. We can decide how
many places we want to shift the bits to, so if we write in our code <<3
we mean “shift the bits 3 places to the left” as the diagram below
demonstrates (figure 3.20).

Figure 3.20 When we use the '<<' bitwise operator we shift the bits to the left, in this case, 3
places to the left as we use '<<3'.

2. Bitwise RIGHT SHIFT (>>) shifts bits to the right. If we write in our
code >>3 we mean "shift the bits 3 places to the right” as the diagram
below demonstrates (figure 3.21).

Figure 3.21 When we use the '>>' bitwise operator we shift the bits to the right, in this case, 3
places to the right as we use '>>3'

3. Bitwise AND (&) takes two operands. As AND operation is performed on


every bit of two numbers. If both the bits are 1, the result of AND
operation is 1. If either (or both) of the bits are zero, the result is zero, as
the diagram below demonstrates (figure 3.22).

Figure 3.22 When we use the '&' bitwise operator if both the bits are 1, the result of AND
operation is 1. If both the bits are zero, the result is zero.
4. Bitwise OR (|) takes two operands. As OR operation is performed on
every bit of two numbers. If both the bits are 1, the result of OR
operation is 1. If both of the bits are zero, the result is zero. If one of the
bits is 1, the result is 1 as the diagram below demonstrates (figure 3.23).

Figure 3.23 When we use the '|' bitwise operator if either (or both) of the bits are 1, the result of
OR operation is 1. Only if both of the bits are zero, the result is zero.
5. Bitwise XOR (^), which stands for exclusive or, takes two operands and
requires that both bits are different for the resulting bit to be a 1. As XOR
operation is performed on every bit of two numbers. If both the bits are
zero, or both bits are 1 (in other words, if both bits are the same), the
result of the XOR operation is zero. If anyone of the bits is 1 (in other
words: if the 2 bits are different), the result of XOR operation is 1, as
demonstrated in the diagram Figure 3.24.

Figure 3.24 When we use the '^' if both the bits are zero, the result of the XOR operation is zero.
If anyone of the bits is 1, or both bits are one.
6. Bitwise NOT (~) one number is taken as an operand and all the bits of
the number are inverted. 1 becomes zero and zero becomes 1, as
demonstrated in the diagram Figure 3.25.

Figure 3.25 When we use the '~' 1 becomes zero and zero becomes 1.

Going back to our mock interface and checkboxes where we turn the bold,
italic, and underline attributes on or off, we can use bitwise operators as the
sample statements below.

Note

Below is a small taste of how turning the attributes on and off might look in
real code using shifting "formulas"; Even without understanding much, just
by looking at the code snippet below, you can get the idea of how bitwise
operators shift and the value changes. Personally, though we work with
bitwise operators a lot, we don't necessarily remember how to shift them by
heart, and we don't expect you to memorize them as well. Thankfully, there
are plenty of references online in case you need to get into the bit…

If we want to turn on a bit, we can use the statement:


Value |= (1U << Bit);

If we want to turn off a bit, we use the statement:


Value &= ~(1U << Bit);

To check if a bit is on (set), you would need the following statement:


(Value & (1 << Bit));

Now, we can go one step further and say that to know if it’s a box is checked
we would use the statement:
Bool IsBoldOn_;
IsBoldOn_ = Value & (1 << BIT_BOLD);

And to report that its BOLD, we would use the statement:


Value &= ~(1U << BIT_BOLD);

Bitwise operators are useful, and we use them a lot. As your skills grow, you
will be able to dive deeper into this concept and benefit from the use of
bitwise operators in your future programs.

Note
C++ has a more generic construct for handling bits: bitset, which provides a
convenient way to work with individual bits or sets of bits, but is not covered
in this book.

Operator overloading in a nutshell

Now that you are familiar with all of the C++ operators, we can introduce
you to the concept of operator overloading. At the beginning of this chapter,
we asked you to think about operators as if they were your program's cooking
ingredients. We talked about the way C++ allows you to get creative and
"invent" your own new "recipes". Operator overloading is the way to do it –
it's used to manipulate and re-purpose some operators, changing their classic
beyond the obvious familiar role.

Let's take arithmetic operators which are familiar from elementary school for
example. Arithmetic operators are mathematical by nature, yet in C++ the
plus (+), minus (-), multiplication (*), and division (/) can be used more than
simple arithmetical values, but with other objects; for example, characters
(chars), as well as other objects you will learn about.

Changing the natural role using a different implementation of an operator is,


in fact, operator overloading. We "overload" the operator with a new role.
Operator overloading means that C++ can provide the operators with a
special meaning for a data type. In fact, A great example of operator
overloading is the case of the spaceship operator. In this case, the operators <,
= and > changed the naturally known implementation of each one, in order to
form a completely new operator - <=>.

We explain and demonstrate this important capability later in this book, as it


can be implemented with other operator types as well as arithmetical ones.

Operator overloading is one of the small things which, combined with some
other brilliant capabilities, defines C++ as a robust and unique language, as
we can take the arithmetical language and implement it in other ways we
want or need, beyond the obvious arithmetical scope. A simple example
would be using the + operator for adding two words into a single one or using
the % operator (or any other operator we want) to check if one text is equal to
another. This might seem easy but remember – we are "talking" to a machine
that only "speaks" binary, so nothing is as simple as it seems.

3.8 Final exercise – dive into a bit more complex


code
It’s so important to practice a lot of code, and the next code sample is a bit
more complex involving more components and logic in decision-making than
you used so far. The following program offers employees to calculate and
then display their salary raise according to a formula, which uses their
employment type (full-time, part-time, temp), and their success evaluation
score – both will be entered by the user. You can already understand the
program's need to conduct multiple decisions. The program also needs to
conduct various calculations based on a formula that is part of this code. This
is the longest most complex code you have encountered so far in this book,
but everything in it should be familiar and clear if you follow it step by step.

In many cases, the best way to approach complex code is to create a flow
chart that will illustrate the flow of the program. The flow chart below is a
good example of using a flow chart that illustrates our program's flow and
logic, so you can then more easily convert it into working code (figure 3.26).

Figure 3.26 Flowchart illustrating the flow of our program. We have 3 types of workers: full-
time, part-time and temp. Each, except for temp has 3 bonus rates according to their success.
Now we can move on to the code. Read the code carefully, as you should be
able to understand each line. Copy it to your IDE and run it, you can also
make changes and modify it for the sake of practice.

Listing 3.8 Code practice – salary calculator based on success score

#include <iostream>
int main()
{
int myEmploymentStatus; #A
const double FULLTIME_SALARY = 30000;#A
const double PARTTIME_SALARY = 15000;#A
const double TEMPSALARY = 5000;#A
float mySuccessScore, salaryRaise = 0.0;#A
double rate;#A
double bonus_pay = 1500; #A
std::cout << "Please enter your employment type?" << std::endl;
std::cout << "1 - full-time\n2 - part-time\n3 - a temp" << std::endl;
std::cin >> myEmploymentStatus;
std::cout << "Please enter your success score (how are you doing at work
std::cin >> mySuccessScore;
std::cout << std::endl;
if (myEmploymentStatus == 1)#B
{
if (mySuccessScore >= 8)#C
{
rate = 0.04;
salaryRaise = FULLTIME_SALARY * (1 + rate) + bonus_pay;
}
else if (mySuccessScore < 8 && mySuccessScore >= 6)#D
{
rate = 0.025;
salaryRaise = FULLTIME_SALARY * (1 + rate) + bonus_pay;
}
else #E
{
salaryRaise = FULLTIME_SALARY;
}
}
else if (myEmploymentStatus == 2)
{
if (mySuccessScore >= 8)#F
{
rate = 0.03;
salaryRaise = PARTTIME_SALARY * rate;
}
else
if (mySuccessScore < 8 && mySuccessScore >= 6)
{
rate = 0.015;
salaryRaise = PARTTIME_SALARY * rate;
}
else #G
{
salaryRaise = PARTTIME_SALARY;
}
}
else if (myEmploymentStatus == 3)#H
{
salaryRaise = TEMPSALARY;
}
else #I
{
std::cout << " No result" << std::endl;
}
std::cout << "Your salary raise is: $" << salaryRaise << std::endl;
return 0;
}

When we run this code we can get various outputs based on the input we
enter. One possible outcome might be:
3.9 Summary
C++ contains diverse types of operators, which are like the program's
cooking ingredients.
Arithmetical operators are used for various arithmetical operations not
only on integers, but also on other objects, such as strings (sequence of
characters such as abc), and more. When conducting arithmetic
operations, C++ follows precedence rules.
Unary operators only work with a single (unary) operand and are used
for incrementing and decrementing. The sizeof() unary operand is
used to tell the actual size of variables.
If-else statements are the basis of program flow, as your program
evaluates if a statement is true or false, and acts upon it, allowing your
program to respond to the user's input, for example.
In many cases, the required conditions contain more than just two
possibilities, or outcomes, requiring the use of a more complex decision-
making mechanism. In these cases, we use nested if statements, as they
provide a more complex flow of conditions and statements.
Logical operators are used for making logical decisions in our code and
are used when we are combining conditions or constraints with a
Boolean output which means a true or false result.
Comparison operators allow us to compare different objects in our code
and are useful when sorting a range of elements.
C++ 20 provides a brand-new comparison operator - the three-way
comparison, also called "the spaceship operator" due to its spaceship-
like shape. This operator can return the result of three comparisons: the
less than operator <, the equal to operator ==, and the more than operator
> which combined constructs this: <=>.
Assignment operators have a significant role in assigning values from
one variable to another. There is a great deal of importance to the
difference between R (Right) and L (Left) values when dealing with
assignments, as the right-hand side is always assigned the value of the
left-hand side.
Bitwise operators are like "linguistic molecules", and they allow storing
several pieces of information in a single variable and micro-manage the
program up to the single bit level, allowing it to turn different attributes
"on" or "off".
The << and >> operators are also called stream insertion, or stream
extraction operators, and they don't have a specific meaning in the
language itself - instead, their meaning is defined by the context in
which they are used. The << inserts data into our output stream, while
>> extracts data from the input stream.
[1] https://en.wikipedia.org/wiki/Rashumon
4 Let it flow – Conditions, iteration,
and flow control
This chapter covers
The benefits of a switch-case statement
Introduction to loops and iteration
Using while, do-while, and for loops
The basics of using files in your code

C++ offers us various additional methods for controlling the flow of your
code based on conditions and outcomes. In this chapter, we uncover more
methods. In the previous chapter, you learned about the if-else statement,
and how it can control the program’s flow while setting conditions. In this
chapter, we explore another way to control the flow of our program using
conditions, with the switch-case statements. The switch-case statement offers
a more readable way to control the flow of your program through various
conditions and potential outcomes, but it also has some pitfalls.

Next, we’ll delve into loops or iterations, which allow you to repeat sections
of code multiple times. You’ll learn about different types of loops, including
while loops that iterate while a certain condition is true, do-while loops that
run once and then check the loop’s condition, and for loops that are great for
controlling iterations by setting a sentinel or iterating a predefined number of
times. We’ll also use for loops to introduce you to the concept of files and
work with them in a coding exercise. Finally, you’ll learn about infinite
loops, which is a loop state which iterates indefinitely or until someone stops
them.

By the end of the chapter, you will practice your skills with a code exercise
that contains loops, conditions, and logic.

4.1 Switch craft – using switch-case statements


In the previous chapter, you learned about the if-else statements. C++
offers you another way to use conditions in your code – the switch case
statement, which can replace the if-else statement. As you learned, in some
cases we need to use several, or even multiple nested if statements (if-else
within an if-else statement) which might complicate our code. Switch-case
is another way to control the flow of your program, replacing if-else in
some cases with a simplified code, saving you the need to use nested if
statements.

Switch-case is used when you want to check the value of an argument


(variable) towards a list of possible values. For example, let's say we have a
program that displays the days local street cleaning is provided based on the
user's zip code. In this case, we will use a variable that holds the zip code
entered by the user and we will need to compare this variable with different
outcomes. We can use a classic if-else statement to determine which
service will be offered to each zip code.

Let’s look at a basic code we can use in this case.

Listing 4.1 Code practice – Select service according to your zip code using if-else

#include <iostream>
int main()
{
int zip{ 10034 };
std::cout << "Please enter one of two zip codes: 10034, or 10093" << std
std::cin >> zip;
if (zip == 10034)
{
std::cout << "Street cleaning is every Monday" << std::endl;
}
else
if (zip == 10093)
{
std::cout << "street cleaning is every Wednesday" << std::endl;
}
else
{
std::cout << "No valid zip code entered" << std::endl;
}
}
When we run this code, we might get the following output:

switch case can make our code more readable and easier to write. Let’s look
at the syntax first. When you go through the syntax, try to understand the
logic, and you will probably see the way it is structured makes a lot of sense.
Figure 4.1 illustrates the syntax using our zip code example.

Figure 4.1 The syntax of a switch case statement


As you can see, the syntax of switch-case feels less like code but more like
a clear set of instructions, in readable code.

Now let's see what the syntax looks like in the code sample we use earlier
with an if-else statement.

Listing 4.2 Code practice – Select service according to your zip code using switch case

#include <iostream>
int main()
{
int zip{ 10034 };
std::cout << "Please enter one of two zip codes: 10034, or 10093" << std
std::cin >> zip;
switch (zip)
{
case 10034:
std::cout << "street cleaning is every Monday"
[CA]<< std::endl;
break;
case 10093:
std::cout << "street cleaning is every Wednesday" << std::endl;
break;
default:
std::cout << "No valid zip code entered" << std::endl;
break;
}
}

Once we run this code, entering the same zip code as we did in the previous
code, we can see the output is exactly the same:
Remember

When we use a switch case statement, the "default" option tells the
software what to do in case the value checked doesn't match any of the
possible values entered. It's very important to always include a 'default' case
since you can never predict all possible input values, and 'default' addresses
any value you haven't prepared your code to address.

switch case is a solid method, and simple to use. Personally, we use switch-
case statements a lot when we code, and prefer them over if-else statements
whenever possible.

However, there are some limitations to the switch case statement: You can
use single characters or numeric values but you can't use strings literals,
which are sequences of chars you will learn all about in chapter 5.

4.1.1 The brotherhood of if-else, the ternary operator, and the


switch case
If we look at the logic of the switch case, if-else statement, and the ternary
operator ?: (which you learned about in the previous chapter), we can see a
common line between the three, or even the exact same logic. Could it be that
if statements, switch statements, and the ternary operator are merely three
different methods for doing the exact same thing? Well, yes and no. Let’s
explore the similarity and differences, starting with the ternary operator ?:

The benefits and limitations of the ternary operator ?:

If you’re initializing a constant as part of your code, then if-else


statements can’t be used but ?: can. For example, the following
statement shows the initialization of a const variable I, which is
initialized using the ternary operator:
const int i = n() ? 8 : 3;

We cannot initialize a const variable using an if-else, or switch case


statements. If you use a lot of const objects in your code, there's a
benefit to initializing a const properly over the assignment with if-else
statements.

The ternary operator allows us to write concise code, but it’s a matter of
personal choice. What would you prefer:
if (condition)
{ // optional
print (x);
} // optional
else
{ // optional
print (y);
} // optional

or:
print ( (condition) ? x : y );

The choice is yours to make.

Obviously, we should always be asking ourselves how much code we need to


write per each statement and the conditions and outcomes we want to set
simple or complex, as readability is important.

In many cases, if-else statements might become a monstrosity that will eat
you alive, especially with a heavy load if nested. On the other hand, a
complicated ?: structure might get you lost in the noise as well – but why not
see for yourself: Let’s take the is-windy/is-cold sample which we used in the
previous chapter, and add to it is it rainy (is-rainy) and put it all together in a
nested ternary operator:
bool b_IsRaining{ false }, b_isWindy{ true }, b_isCold{ true };
bool b_GoRun =
(b_IsRaining) ?
(b_isWindy ?
(b_isCold ?
false :
true) :
true) :
true;

Let’s see what we’ve got here and try to explain this monstrosity (Figure 4.2).

Figure 4.2 Using the ternary ?: operator is not always a desirable choice when handling complex
conditions and outcomes. The code will work, but readability will be a nightmare.

If you feel a bit dizzy looking at this code – we get it. It’s hard to read and
understand, and, obviously, using ?: in this case will technically work, but
readability-wise, it will be very difficult to read–- and we don't want that, we
want our code to soothe the eye and be readable and friendly as possible. If
else or switch case will do a better job readability-wise. Here is how our code
snippet would look like had we used switch case for example, and as you can
see, it's more readable:
bool b_IsRaining{ false }, b_isWindy{ true }, b_isCold{ true };
bool b_GoRun;
switch (b_IsRaining)
{
case true:
switch (b_isWindy)
{
case true:
switch (b_isCold)
{
case true:
b_GoRun = false;
break;
case false:
b_GoRun = true;
break;
}
break;
case false:
b_GoRun = true;
break;
}
break;
case false:
b_GoRun = true;
break;
}

With the ternary operator, we cannot use code blocks within curly
brackets. For example, you won’t be able to pull this code through:
int i = 10;
int j = 10;
(i == 10) ? ({j = 2; i++;}) : (i++);

But we can use an if-else statement:


if (i == 10)
{
j=2; i++;
}
else
{
i++;
}

Unlike if-else statements, where you can just use an if without else, with
the ternary operator we must include as part of the statement both the if
and the else, so you can’t just write:
j =(i == 10) ? 1;

You will need to write:


j =(i == 10) ? 1 : j;

Remember: As you learned, a ternary operation is called ternary because


it takes three arguments, but if you omit the else, thus removing an
argument out of the three, we will result in an undefined situation that
will not allow our statement to work, as you will get a syntax error
raised by the Compiler.

To if? to switch? are they one or the same?

If else statements and switch case statements share the same logical anatomy
and are basically two different ways of doing the same thing, but there are
slight differences between the two worth looking into.

Readability and clarity


As said, we find switch case statements to be useful, and easier to write,
read, and maintain. switch case is more like a multiple-choice selection
statement, which, in many cases is easier to use over an if-else
statement, and can help us write lovely code.
Speed
Upon compiling a switch statement, the compiler inspects each case and
creates what is called a “jump table” which is used for selecting the path
of execution. If our code contains many values, a switch statement,
therefore, will run much faster than an if-else statement.
Multiple statements Vs a single statement
When using an if-else statement, we can have multiple statements for
multiple choice statements, yet when using a switch case, we only use a
single expression for multiple choices.
Supported data types
The main difference between if-else and switch case is that if-else can
support all date types, while switch case is restricted to integers and
chars, and cannot handle strings as you can see in figure 4.3 (Don't mind
the code, we do not expect you to understand it, as what's important here
is the error message the compiler displays).

Figure 4.3 switch case statements cannot support strings, which are variables that store a
sequence of characters like “hello”.
If we use an if-else statement for the same code, we would not encounter any
problem, as you can see from Figure 4.4:

Figure 4.4 Our code can execute perfectly with the same code, this time using an in-else
statement.
Though you will learn all about strings in Chapter 7, you can understand the
basic concept. Strings are commonly used in code, as a string is a variable
that stores a sequence of letters or other characters, such as "Hello". It means
the inability to use switch case statements is a big downsize.

Note

In chapter 7, we teach how to overcome this obstacle using an enum, which


we briefly mentioned in chapter 3 as a way for you to create your own data
types. Figure 4.9 shows the problematic code we could not execute in Figure
4.5, and now is executed perfectly using enum.

Figure 4.5 We can overcome the restriction of switch case statements and strings using enums, so
our code will run smoothly.
As you can see, there are several benefits and disadvantages to using the
ternary operator ?:, the switch case and if-else statements. As a beginner,
it’s natural to get confused and not to be sure which method to use per each
use case, but as time goes by, and you develop more confidence and skills,
you will find the way which suits you best, as, after all, there’s always more
than one way to write a certain code.

4.2 Looping–- the art of repetition


While the term "going in circles” holds a somewhat negative context, "going
in circles"" within a computer program is far from it: in computer
programming, "going in circles" is looping. We use loops, or looping, when
we need to iterate or use some sort of repetitions in our code. In fact, it's quite
hard to find a decent piece of code that doesn't use some sort of iteration.

There are different iteration methods that we can execute in our code. Though
each method is slightly different than the other, each holds the same
principle: it runs our code, or some sections of our code, several times
repeatedly.

Loops are powerful, and by using them, we can develop complex operations,
solve problems, and create powerful programs. Loops also help us better
control your program's flow.

4.2.1 The basic principles for loops


Whenever we want to use loops, we need to answer two basic questions:
when will the loop start and when will it end. In other words, we must create
a condition in which the loop will start, and once this condition will no longer
be met, the loop will terminate. Once the first condition is met, a statement,
or statements, will be executed until the loop will reach its course, then
terminated.

Loops run as long as we have a true sentinel, and the loop might also run
'forever'. Loops that run with no condition in which they are terminated are
called forever loops or infinite loops. In fact, operating systems, such as
Windows, loop “forever” (as long as the computer is turned on, that is),
allowing the continued operation of the OS without interruptions. In this case,
the iteration will have two conditions: start iterating (looping) when the user
opens the computer (OS uploads) and stop when the PC is turned off. We
demonstrate the infinite loop in a minute, but before we do, let's introduce the
three most commonly used loop types in C++, along with an infinite loop as
presented in Figure 4.6.

Figure 4.6 The Three loop types, and an infinite loop, which is not a loop type, but a loop
behavior – any loop that iterates “forever” unless stopped.

4.2.2 While loop – looping while a condition is true


As you just learned, loops are always based on a condition (or set of
conditions), and the while loop, which is a commonly used looping method,
is based on a condition that is checked before any iteration starts. You can
say that while the condition is true, the loop will iterate – hence the name
“while loop".

Good to know
Loops that are based on a condition before they start iterating are called entry
control loops.

Think of while loop as if you prepare hot oatmeal: you want to stir the pot
until it’s boiling. A while loop would first check if the temperature is above a
certain value, and if it does, we exit the while loop (stop stirring and turn the
heat off). If it doesn’t, and as long it doesn’t, we continue "stirring the
oatmeal”, (i.e. looping). Once the loop begins, and while the condition is true,
it will continue iterating until the condition becomes false, and the loop will
be terminated.

Let's look at the flow of a while loop using a flow chart (figure 4.7).

Figure 4.7 The flow of the while loop starts with a statement and a condition. As long as the
condition is true “while"), the loop will continue to iterate. Once the condition is false we reach
the ‘'break’' statement and the loop will terminate.
Hands-on: Working with while loops

Working with while loops required the use of a specific statement structure,
which is not hard to memorize and understand. Let’s look at the loop’s
structure:
Here is how it will look like as code:
while (condition)
{
statement;
}

Earlier in this section, we compared a while loop to check the temperature of


our oatmeal and keep stirring it until it was ready. Let's write a little program
that "checks" if our oatmeal is ready. In this program, we increment the
temperature of the oatmeal each loop, until it reaches a temperature of 80°C.
Listing 4.3 using the while loop to check if the oatmeal is ready

#include <iostream>
int main()
{
const int boiling_temp = 80;#A
int oatmeal_temp = 20;#B

std::cout << "Preparing oatmeal..." << std::endl;


std::cout << "Current temperature: " << oatmeal_temp << "C" << std::endl
while (oatmeal_temp < boiling_temp) #C
{
std::cout << "Stirring oatmeal..." << std::endl;
oatmeal_temp += 10;#D
std::cout << "Current temperature: " << oatmeal_temp << "C" << std::
}
std::cout << "Oatmeal is ready!" << std::endl;
return 0;
}

Once we execute the code, the following output is expected:


As you can understand from our code, the while loop first checks the sentinel,
and only then, based on the outcome, iterates. To prove this, let's change the
value of oatmeal_temp to 80°C, which is the maximum temperature for our
oatmeal. Once we make the change in our code, we should expect the
following output:

As you can see, our while loop checked the condition while (oatmeal_temp
< boiling_temp) and since the temperature was equal to boiling_temp the
loop did not iterate, so there was no “stirring oatmeal” output.

give me a break - using the break statement as your loop’s exit point

Sometimes we need to exit the loop, and whenever we want to do that, we


can use the break keyword. As the name implies, the break keyword
“breaks” the iteration and stops it outside the sentinel check. For example, in
case an error arises, which makes continuing to iterate in the loop ineffective.
Let’s review an example using a while loop. In this example, we ask the user
if they want to continue looping by typing Y (yes), or N (no), and we use
break; in case the user types in "no". By now this simple code should be
easy to read and follow.
#include <iostream>
int main()
{
while (true)
{
char answer{};
std::cout << “Do you want to continue looping? Y / N” << std::endl;
std::cin >> answer;
if (answer == ‘N’ || answer == ‘n’)
break;
}
}

Going through this code it’s important to point out the reason we use
if (answer == ‘N’ || answer == ‘n’)

Think about it: we can’t be sure if the user will type ‘N’ or ‘n', and since we
want our program to take into account both options, we use the || (or)
operator. This is a simple and basic example of how we, as a programmer,
should think of different scenarios from the user's point of view which our
program will need to handle, especially when input is involved. Also note
that we do not set an if statement when the user enters Y, as the only thing
we care about is if the user enters N, or else, the loop will continue. The user
can also type anything else other than N, and the loop will continue as well.
Obviously, in this code sample, once we press N for no, the program will
terminate.

Let's look at another example. This time, the user is asked to enter a
password. The correct password is 123456. The user will have only four
attempts to type the correct password. If the 4th attempt fails, the user will be
prompted "Sorry. You had too many failed attempts. Goodbye". If the user
succeeds, the user will be prompted "Correct password!".

Let's look at our code. Try to go through each and every line to understand it.

Listing 4.4 Code practice – What is your password

#include <iostream>
int main()
{
std::cout << "Enter your password" << std::endl;
bool correct_password = false;#A
int attempts = 0;#B
const int real_password{ 123456 };#C
int entered_password{};#D
while (attempts <= 3)#E
{
std::cin >> entered_password;
if (entered_password == real_password)#F
{
correct_password = true;
break;
}
else
{
std::cout << "Wrong password. You have " << 3 - attempts <<
attempts++;
}
}
if (correct_password)
{
std::cout << "Correct password!" << std::endl;
}
else
{
std::cout << "Sorry, you had too many failed attempts. Goodb
}
return 0;
}
Try running this code, and type in different attempts.

Break me once, break me twice –breaking from two or more loops

So far you learned how to use break to exit a loop of any kind or a switch
case statement. However, sometimes we need to use break twice or more,
and it might happen when there are two or more nested loops or a
combination of a switch case and a loop. Let's look at the following code for
example:
#include <iostream>
int main()
{
bool bContinue{ true };
int nCounter{ 0 };
while (bContinue)
{
while (bContinue)
{
std::cout << "Inside the 2 loops" << std::endl;
nCounter++;
if(nCounter > 20)
{
bContinue = false;
break;
}
std::cout << "Still going on…" << std::endl;
}
}
std::cout << "broke from 2 loops" << std::endl;
return 0;
}

This program executes a nested loop that prints a message to the console,
increments a counter variable, and then breaks out of the inner loop when the
counter exceeds 20. Once the inner loop has finished, the program exits the
outer loop and prints a message to the console.

When our counter (nCounter) reaches 20, we need to break from the 2nd
while loop and also break from the first while loop. We can't call
break+break - we can only call break once. If you call break while inside
the 2nd while loop, we will break from it but, still be in the 1st while loop.
The same applies to any scenario where we have two nested loops of any
kind. Switch and while, while and for, etc.

So what do we do? How can we solve this paradox? Well, for such a case, we
use the bool variable bShouldRun, which is initialized as true, so the while
loop continues as long as it is true. Before calling break from the while
loop, we set bShouldRun to false, then we break outside the 2stn while, and
the 1st while also breaks because bShouldRun is no longer true

Let’s ‘continue’ – using continue for better control of the loops’ flow

You already learned about the break; keyword, and the role it plays in
keeping the flow and behavior of our loop, as it breaks the loop and moves to
the next statement. C++ allows you to control the flow and behavior of your
loops furthermore.

Under the same principle we use break, we use the continue keyword, but
instead of forcing the loop to terminate, continue forces the next iteration of
the loop to take place, skipping any code in between.

Figure 4.8 demonstrates the flow of a continue statement.

Figure 4.8 We use 'continue' when we want to force the next iteration of the loop to take place,
skipping any code in between
As you can see from the flow chart, the loop will stop executing any
statements in the body of the loop, and the control goes to the next looping
cycle. In other words – stop what goes on now and go back to the beginning
of the loop.

In the next section, we will demonstrate how to use continue in our code.

4.2.3 Do while: the post-step loop


Another available loop type in C++ is the do-while loop, which executes a
statement at least once, regardless of if the condition is true or false. It means
that a do-while loop will always iterate at least once. This type of loop is
called post-test iteration, and in this case, an iteration will execute regardless
of if the condition is true or not. Only after the first execution of the loop, the
condition is checked - If it's true, the loop will continue iterating until the
condition turns to false – just like the normal while loop you already know.

Going back to our oatmeal example, with a do-while loop we will stir the
oatmeal once before checking the temperature, (as you recall, with a while
loop we first checked the temperature, and based on that, “stirred”).

To better understand the logic, let’s look at the flow of a do-while statement
is illustrated below (figure 4.9)

Figure 4.9 The flow of the do-while loop. The first statement will be executed without checking
whether the condition of the loop is true or false. Starting a loop without testing the condition is
called post-test iteration.
The syntax of a do-while loop is pretty simple and looks almost the same as
the one we use for while loops, as illustrated in Figure 4.10.

Figure 4.10 The do-while loop executes at least once before it checks the condition.

Note

using {} when surrounding a single line of code, is optional, and the


statement will work without it, but it is recommended to always use {} with
loops.
Let’s look at a code sample, this time we write a program in which the user is
required to enter a radius of a circle. Once we calculate the circle’s area, the
user will decide whether he wishes to continue and make another calculation,
or to exit the program.

In this case, the 'do' statement will run first and ask the user to enter the
circle's radius. In other words, the user will have to run the program at least
once, and only then he will be asked if he wishes to continue or not.

Listing 4.5 Code practice – calculate circle area

#include <iostream>
int main()
{
char choice{}; #A
do
{
double circle_radius{};
std::cout << "Please enter the circle radius" << std::endl;
std::cin >> circle_radius;
double circle_area{ 3.14 * circle_radius * circle_radius };
std::cout << "The circle area is " << circle_area << std::endl;
std::cout << "Do you want to make an additional calculation? (Y / N)
std::cin >> choice;
}
while (choice == 'y' || choice == 'Y'); #B
std::cout << "Goodbye" << std::endl; #C
}

Once we run this code, this is what we should expect:


Moving forward, let's write another program that uses a switch case inside
the do-while loop. Let’s first understand the program's flow: We offer the
user the chance to buy a T-shirt from a list of 4 T-shirt options. Each T-shirt
is sold for a different price. The user will then be asked if he wants to change
his/her choice, if YES – the menu will be displayed again. If NO – the loop will
terminate, and the user will be asked to "pay", and the total sum of all the
items chosen is displayed.
Can you try and write the code for this program yourself? It's always a good
start to draw a simple flow chart first. Let's look at the flow chart for this
program (figure 4.11).

Figure 4.11 The flow of our program. We start with a 'do', displaying the price list and asking the
user to make a choice. We then move to several if-else statements, depending on the user's choice,
and move either to checkout or back to the beginning.
Once we know the exact flow of our program illustrated by the flow chart
and can write our code.

Listing 4.6 Code practice – Select and “buy” T-shirts

#include <iostream>
int main()
{
char selection{};#A
char answer{};#B
bool correct_selection = false;#C
int total_cost = 0; #D
do #E
{
correct_selection = true;
std::cout << "Please select an item from the list below:" << std::en
std::cout << "----------------------------------------------------"
<< std::endl;
std::cout << "A. White T-shirt (Man)" << std::endl;
std::cout << "B. White T-shirt (Woman)" << std::endl;
std::cout << "C. Blue T-shirt (Unisex)" << std::endl;
std::cout << "D. Red T-shirt (Unisex)" << std::endl;
std::cout << "Enter your selection:" << std::endl;
std::cin >> selection;
switch (selection)
{
case 'A':
case 'a':#F
std::cout << "You selected White T-shirt (Man), cost: $20" << st
total_cost += 20;
break;
case 'B':
case 'b':
std::cout << "You selected White T-shirt (Woman), cost: $15" <<
total_cost += 15;
break;
case 'C':
case 'c':
std::cout << "You selected Blue T-shirt (Unisex), cost: $25" <<
total_cost += 25;
break;
case 'D':
case 'd':
std::cout << "You selected Red T-shirt (Unisex), cost: $12" << s
total_cost += 12;
break;
default:#G
correct_selection = false;
std::cout << "Wrong selection, please try again" << std::endl;
break;
}

if (correct_selection)#H
{
std::cout << "Do you want to continue shopping Y/N" << std::endl
std::cin >> answer;
}
} while (answer == 'Y' || answer == 'y');#I

if (total_cost > 0) #J
{
std::cout << "Total cost: $" << total_cost << std::endl;
}
std::cout << "Item(s) is/are ready for pick up. Pay at the counter. Than
return 0;
}

Let’s run this code to see a possible output:


Tip

Another important part of our code is what we call a 'flag'. It refers to


"correct_choice", which is a boolean variable. "correct_choice" tells us if
a valid answer was entered. Our ‘flag' is initially set to false and will only be
changed to true if the user selects a valid option. In the switch statement, if
the user selects a valid item, the corresponding case statement will execute -
and the flag will be changed to true. On the other hand, if the user selects an
invalid option, the flag will remain false, and our error message will be
printed.

Going over the code and flow chart, you can see that everything you learned
so far makes sense when it comes to implementing it all to code.

Let's look at one last code. This time, we go back to our oatmeal-stirring
code, and write it again, this time using a do-while loop. In this code, we do
exactly what we did in our previous code (code listing 4.3), except, this time,
the oatmeal’s temperature is set to 80°C. We “stir” the oatmeal and raise the
temperature once before the sentinel is checked.

Code listing 4.7 – using a do-while loop to check if the oatmeal is ready

#include <iostream>
int main()
{
const int boiling_temp = 80;
int oatmeal_temp = 80;
std::cout << "Preparing oatmeal..." << std::endl;
std::cout << "Stir the oatmeal once before checking temp" << std::endl;
std::cout << "Current temperature: " << oatmeal_temp << "C" << std::endl
do
{
std::cout << "Stirring oatmeal..." << std::endl;
oatmeal_temp += 10;
std::cout << "Current temperature: " << oatmeal_temp << "C" << std::
}
while (oatmeal_temp < boiling_temp);
{
std::cout << "Oatmeal is ready!" << std::endl;
}
return 0;
}

Once we run this code, we get the following output:

As you can see, our program compiles and runs with no errors, but look at the
output: does anything seems wrong here? There seems to be a logical error in
the code: If the boiling temperature is 80°C, and the oatmeal temperature
increments by 10°C in each iteration, it will exceed the boiling temperature
which will reach 90°C. The logical error in the code occurs because the do-
while loop is executed at least once, even if the condition is already false.

Important!

The conclusion from our logical error is that in situations where it's important
to check the condition before the loop is executed, a while loop is often the
better choice, and it will ensure that the loop is only executed if the condition
is true. In the case of the oatmeal temperature, a while loop would make
much more sense, because the temperature should be checked before it is
stirred, to ensure that it doesn't exceed the boiling temperature.

4.2.4 For the love of loop – the for loop


The best way to explain what a for loop is and how it's used within our code
is to imagine a mouse running in a maze. The mouse needs to collect six
pieces of cheese at the end of the maze, and he can only carry one piece at a
time. The mouse runs once and brings 1 piece, 5 pieces of cheese left. He
runs for the second time, brings another piece, 4 pieces of cheese left – and so
on, counting until our counter reaches 6, This is the exact logic behind a for
loop.

In our example, each time the mouse runs, the round he makes is incremented
(1st round, 2nd round, etc.), and in total, the mouse will make six runs for the
cheese. We can also say that each time the mouse runs the amount of cheese
decrements.

Under the same principle, loops, by default, are suitable for controlling the
iterations, by setting a sentinel and iterating until it’s set, or iterating a
predefined number of times. We can therefore use for loops whenever we
know how many times we want our loop to iterate, or if we need to iterate
over a fixed number of values, otherwise, if the iteration is only based on a
sentinel, and not the number of iterations, we can just use a while loop.

The structure of a for loop is similar to what you learned so far - like any
other loop, we need a condition to keep the loop alive and running. The
condition will always return a true/false boolean value. However, for loops
contain some additional components which defer from any other loops you
know so far. Let's take a look at the for-loop statement structure and break it
down for better understanding.

As you can see from Figure 4.12, we have two new components in a for loop:
initialization and incremented (or decremented) values.

Figure 4.12 The logic of a for loop includes an initialization and an increment or decrement value,
with a statement to be executed while a condition is true.
Note

using {} is optional, and the statement, being a one-liner, will work without
it, but it is recommended to always use {} with for loops.

1. Initialization
Since, as we just explained, for loops can be set to perform a predefined
number of iterations, therefore, we must use a variable that will be used
for controlling the number of times it runs. We can initialize this
variable to the number of times which is required. For example, if we
use an int variable named i (our index), initialize it to i=8, and then
countdown (decrement) to zero, or we can initialize it to 0, and
increment to 8.
2. Our condition
Like any other loop, we need a condition. In this case, the condition
must be directly related to our control variable. So we can say, for
example, that as long as i is smaller or equal to 8, our condition is true.
In our code, it will be i<=8.
3. Incrementing/decrementing
As you just learned, for loops are usually used to increment or
decrement each cycle, just like when we incremented the number of
times the mouse run at the maze, (and they can be used without
incrementing or decrementing). Naturally, incrementing is the last part
of the for-loop structure. We decrement if we go backward, for example,
going backward through a collection of elements.

Let's look at Figure 4.13, which illustrates the flow of a for loop.

Figure 4.13 The flow of a for loop. We first initialize the control variable, then set a condition and
modify it. While the condition is true, the loop will iterate and increment the value of 'i' until the
condition turns false.
We started this section by comparing loops to a mouse running in a maze and
grabbing some cheese. Let's write a little “running mouse” program using for
loop. We will count the number of times the mouse runs to get his cheese (as
you might recall, we only have 5 pieces). First, we need to initialize a control
variable. It is very common in the case of for loops to use the letter i as a
variable. Then we set our condition, and last, we increment.

Below is our code. When you read it try to imagine our little mouse running 5
times to grab the cheese, and see how it makes perfect sense.
Listing 4.8 Code practice – Using for loop for a mouse in a maze

#include <iostream>
int main()
{
std::cout << "The mouse starts collecting his cheese." << std::endl;
for (int run_count = 1; run_count <= 6; ++run_count)
{
std::cout << "The mouse ran " << run_count << " time and got some ch
}
std::cout << "The mouse got all the cheese." << std::endl;
return 0;
}

Once we run this code we should expect the following output:


Tip

You might ask why didn't we didn’t initialize the variable run_count outside
the loop? Well, that's a great question. It's always good practice to have
complete control over our variable within the loop. Can you guess why? Let's
say we initialize run_count outside the loop. What will happen if the value of
run_count changes at some point? It might cause our program to crash once
the for loop with the old value will be executed. But if we initialize
run_count within the loop, we know we can have full control over it to keep
the same value.

Why not give it a try? With everything you learned so far, you should be able
to write a small code that will use the for loop to count from 1-100 but
display only the odd numbers. Here's a hint: You will need to use an if
statement and the % (modus) operator, but you can also use the statement:
for (int i = 1; i <= 100; i += 2)

Let's look at another code sample, this time you need to read this code and
run it in your head only. What will be the output of this code once we “run”
it?
#include <iostream>
int main()
{
for (int i = 7; i > 0; --i)
{
std::cout << i << std::endl;
if (i == 5)
break;
}
return 0;
}

If your answer was 7 6 5 you are correct.

tip

It's good practice to run codes in your head and figure out the output, so we
recommend you continue doing so to improve your coding skills and your
ability to read code and understand it.
Let’s practice some more code. In this code, we iterate using a for loop to
calculate prime numbers between 1 and 1000.

Listing 4.9 Code practice – Using for loop to find prime numbers

#include <iostream>
int main()
{
int n_CurNum{ 2 }, n_TopValue{ 1000 }, i;#A
bool b_IsPrime = true;#B
std::cout << "This program will find all prime numbers between 1 and 100

for (n_CurNum = 2; n_CurNum <= n_TopValue; ++n_CurNum) #C


{
b_IsPrime = true;
for (i = 2; i <= n_CurNum / 2; ++i)#D
{
if (n_CurNum % i == 0)#E
{
b_IsPrime = false;
break;
}
}
if (b_IsPrime)#F
{
if (n_CurNum == 2)#G
{
std::cout << "The prime numbers are: " << n_CurNum;
}
else
{
std::cout << ", " << n_CurNum;
}
}
}
std::cout << "." << std::endl;
return 0;
}

When we run this code, we get the following output:


Let's look at the code again and analyze it step by step:

We started by setting four variables: n_CurNum which is set to 2, and


which is the first number to check if it’s a prime number. The second
variable is n_TopValue which is set to 1000 – our last number to check.
The third variable is i which is a loop variable we will use to iterate
through all possible divisors in the loop.
The fourth variable is bool b_IsPrime;, which is set to true, and is
used to determine whether the current number being tested by the
program is prime or not.
Next, we need to use a for loop that will iterate from 2 to 1000. Inside
this loop, we have another for loop, that checks if the current number
n_CurNum is divisible by any number from 2 to half of n_CurNum
(n_CurNum / 2). If n_CurNum is divisible by any number in this range,
then it is not a prime number, and the program sets b_IsPrime to false.
At this point, we break out of the inner loop.
If the inner loop finishes without finding a divisor, then the program
checks if b_IsPrime is true, and in case it is, the current number
n_CurNum is a prime number.
If n_CurNum is 2, the program prints The prime numbers are: 2.
Otherwise, it prints a comma and the current number n_CurNum.
Finally, the program prints a period to end the output.

Now that you understand this code and the concept of a for loop, ask
yourself: what will happen in case you wish to calculate not 1,000 prime
numbers, but 1,000,000? It will be very uncomfortable using a console to
display the output, and in real-life programs, we normally don't use the
console, which is used mostly during development. Let's write this code
again, but, this time, we will use a file, and you will learn a new and
important skill.

Files are the bread and butter of any program. You use files to store data, read
data back, backup settings, do all kinds of manipulations, and much more. In
fact, almost any program in this book can be expanded to involve a file.
Storing a lot more prime numbers in a file (or a database) will save you time
running the same calculations each time - you can stop at a certain point, save
the results so far, then when you resume work, read the previously saved
results and continue from where you stopped.

Adding file to our prime number calculator program

Before we begin with our code, let’s understand what is required when using
files. We will first need to add one more header file: <fstream>. This library
contains methods that support iostream operations on sequences stored in
external files.

Note

You might recall that in the previous chapter when we talked about the <<
and >> operators, we mentions they are also used with fstream.

We will add to our prime numbers calculator program the ability to write
what we show on the screen and also to a file named results.txt.

Note

After running the program, you should find a file by that name in the path
from which you run the program.

Handling files: open, write, and close

When we are using files in programs, there are normally three operations our
program needs to handle:
1. open the file.
2. write to the file.
3. close the file.

When you think of it, these three operations make sense: it's like using An
MS Word document: you need to open it, write the text, then close it. The
only difference is that now we need to perform these three operations
programmatically. Let's take a closer look at each operation.

Open the file

Files can be opened for reading, writing, appending, etc. In our program, we
will create a new file (and if an old file by that name exists, it will be
overwritten). For this purpose, we will use the following statement:
std::ofstream ofs("results.txt");

Write to the file

We can write to a file in the same way we use the std::cout statement, but
in this case, we use the keyword std::ofs instead of std::cout. So in
addition to
cout << “hello” << endl;

we can write:
ofs << “hello” << endl;

Close the file

Without closing the file, the file won’t be saved or even finalized, and you
won’t see it. You close the file once you no longer need to add anything to it
using the statement
ofs.close();

Now let's look at our code, which is exactly like the one you wrote
previously, except for the two header files and the new three lines we added
(note that the code annotation is the same as the previous code, except for the
parts added here).

Listing 4.10 Code practice – Using for loop to find prime numbers with file

#include <iostream>
#include <fstream>#A
int main()
{
int n_TopValue{ 1000 }, i{ 0 }; #B
std::cout << "This program will find all prime numbers between 1 and " <
std::ofstream ofs("results.txt");
if (!ofs)#C
{
std::cerr << "Error opening output file" << std::endl;
return 1;
}
for (int n_CurNum = 2; n_CurNum <= n_TopValue; ++n_CurNum)#D
{
bool b_IsPrime{ true }; #E
for (i = 2; i <= n_CurNum / 2; ++i)#F
{
if (n_CurNum % i == 0)#G
{
b_IsPrime = false;
break;
}
}
if (b_IsPrime)#H
{
static bool b_FirstPrint{ true }; #I
if (b_FirstPrint) #J
{
ofs << n_CurNum;
std::cout << n_CurNum;
b_FirstPrint = false;
}
else
{
ofs << ", " << n_CurNum;
std::cout << ", " << n_CurNum;
}
}
}
ofs << "." << std::endl;
ofs.close(); #K
std::cout << "." << std::endl;
return 0;
}

After we run this code, go to the same path from which our program runs[1],
we can find the result.txt file (figure 4.14 and Figure 4.15).

Figure 4.14 When you go to the same path from which your program runs, you will find the .txt
file with the output.
Figure 4.15 The output in our .txt file

Using static values

In this code, you probably noticed that we used b_FirstPrint – but so far we
have not discussed what a static variable is, and why we need to use it.

Static variables, when used inside a code block (which can be any type of
code locked inside curly brackets), are used when we don't want the current
value of that variable to be destroyed after we exit that code block. We
always need to initialize such a variable, and from that moment on, whenever
the value is changed, the value will be preserved when we enter that code
block again. In our case, b_FirstPrint is initialized to true only when the
code block is first encountered. Once it is initialized, subsequent executions
of the code block will retain the modified value of b_FirstPrint. This
behavior ensures that the condition in the if (b_FirstPrint) statement is
evaluated correctly each time the code block is executed.

Good to know

Both for and while loops iterate until a condition is met, and they are
different syntactic methods for achieving the same thing. In fact, due to the
similarity, some newer languages such as Golang (Go) only have a for loop.
In the case of Go, it has a for loop that can be used in various ways to achieve
similar functionality as a while loop.

In the next chapter, we go back to for loops and explain how they work with
a sequence of elements.

4.2.5 The infinite loop

An infinite loop is any loop type that will loop "forever" unless stopped. One
important thing to remember is that an infinite loop runs with a sole
expression that always evaluates as true. That can be just the value (true),
(1==1), etc. We control (stop) an infinite loop with a 'break' statement. As
you learned earlier, 'Break' is a commonly used keyword in C++ which is
used to "break" the iteration of a loop. However, the loop will keep on
iterating while the condition upon it is set will remain true.

We should distinguish between infinite loops and loops that would run
throughout the entire lifecycle of the program, but have an exit condition, that
when set, the code execution will exit that loop – we can refer to these loops
as 'semi-infinite loops[2]. A “real” infinite loop will be executed repeatedly
and will never stop. In most, if not all cases, that would indicate a bug.

Figure 4.16 illustrates the flow of an infinite loop.

Figure 4.16 The flow of an infinite loop - As long as the condition is true, the loop will keep on
iterating indefinitely.
Good to know

A loop that would run throughout the lifecycle of a program does exist.
Examples of these 'semi-infinite loops would be programs that are designed
to run without ever being terminated, such as server software, or large-scale
distributed systems. As mentioned earlier, operating systems are also
programs that have a main event loop that is infinite, as long as your
computer is turned on. Another example would be a separate thread, which
sometimes has an infinite loop inside it, and yet, threads can be terminated
(and started) from the program that uses them, so these “infinite loops” inside
threads aren’t really infinite. You will learn all about threads and
multithreading, which is a program that contains two or more parts that can
run concurrently, in chapter 14.

It is advised to never create an infinite loop, and a loop that is supposed to


run “forever” should have an “exit switch”, so there is always a way to exit
them. For example, a loop such as
while(true)
{
}

is infinite, and yet, if you place a call to break; the code execution will exit
it.

Let’s take a look at a basic for loop that will loop forever:
#include <iostream>
int main()
{
for (;;) #A
{
std::cout << "hip hip hip hip hip" << std::endl;#B
}
}

You probably asked yourself why do we use the statement for (;;), and
why are we using two semicolons in the brackets? Well, when you learned
about for loops you learned that three stages are executed with a for loop:
initialization, condition, and incrementing. In the case of an infinite loop, we
don't care about all three, which leaves us with two semicolons in an empty
bracket.

Now try running this code and see what happens – the text "hip hip hip"
will run indefinitely, until we stop the program.

To summarize everything you learned about loop types, let's take a look at
Figure 4.17 which illustrates all loop types, their definition, and syntax.

Figure 4.17 The four loop types: while loop, do while loop, for loop, and an infinite loop.
Note

Throughout this book, we will use loops and iterations, so your skills and
confidence will grow stronger as you continue.

Note

using the curly brackets{} is optional with loops, and the statements will
work without them. However, it is recommended to always use {} with loops.

4.3 Final exercise: solve the jug puzzle


Practice makes perfect, so let's sum up everything you learned so far with
more code. The following code simulates a known puzzle – the jug puzzle.
The objective of this puzzle is to measure exactly 4 liters of water using 3-
liter and 5-liter jugs. Our program will monitor the number of steps the user
tries out, limiting them to 10 steps (using a variable named maxSteps). The
program also offers the user to see the entire solution.

Tip

Many times in job interviews, candidates are asked to solve a puzzle, so


practicing problem-solving is always a good idea.

This code contains a lot of what you’ve learned so far – read it carefully, and
you will probably find you can understand each and every line, and in case
some parts are still confusing – don’t worry, some more practice and you’ll
feel confident soon enough.

Listing 4.11 The jug puzzle

#include <iostream>
int main()
{
int jug3 {0};#A
int jug5 {0};#B
int maxSteps {10};#C
bool solved {false};#D
std::cout << "Two Jugs Problem: Measure exactly 4 liters of water using
for (int step = 1; step <= maxSteps; ++step)#E
{
int action;#F
std::cout << std::endl << "Step " << step << ":" << std::endl;
std::cout << "Choose an action:" << std::endl;
std::cout << "1. Fill the 3-liter jug" << std::endl;
std::cout << "2. Fill the 5-liter jug" << std::endl;
std::cout << "3. Pour the water from the 3-liter jug into the 5-lite
std::cout << "4. Pour the water from the 5-liter jug into the 3-lite
std::cout << "5. Empty the 3-liter jug" << std::endl;
std::cout << "6. Empty the 5-liter jug" << std::endl;
std::cout << "7. Show the solution" << std::endl;
std::cin >> action;
if (action == 7) #G
{
std::cout << std::endl << "Solution:" << std::endl;
std::cout << "1. Fill the 3-liter jug" << std::endl;
std::cout << "2. Pour the water from the 3-liter jug into the 5-
std::cout << "3. Fill the 3-liter jug" << std::endl;
std::cout << "4. Pour the water from the 3-liter jug into the 5-
std::cout << "5. Empty the 5-liter jug" << std::endl;
std::cout << "6. Pour the water from the 3-liter jug into the 5-
std::cout << "Now, the 5-liter jug has exactly 4 liters of water
--step;
continue;
}
if (action < 1 || action > 7) #H
{
std::cout << "Invalid input! Try again." << std::endl;
--step;
continue;
}
switch (action) #I
{
case 1:
jug3 = 3;
break;
case 2:
jug5 = 5;
break;
case 3:
{
int pour = std::min(jug3, 5 - jug5);
jug3 -= pour;
jug5 += pour;
break;
}
case 4:
{
int pour = std::min(jug5, 3 - jug3);
jug5 -= pour;
jug3 += pour;
break;
}
case 5:
jug3 = 0;
break;
case 6:
jug5 = 0;
break;
default:
std::cout << "Invalid action! Try again." << std::endl;
--step;
continue;
}
std::cout << "Jug (3L): " << jug3 << ", Jug (5L): " << jug5 << std::
if (jug3 == 4 || jug5 == 4) #J
{
std::cout << std::endl << "Congratulations! You've solved the ri
solved = true;
break;
}
}
if (!solved) #K
{
std::cout << std::endl << "You've reached the maximum number of step
}
return 0;
}

Try running this code and finding the solution. As you can see, working with
conditions within our code, even before you learned about some other useful
components, is enough to allow us to create a fun game. If that’s what we can
do with so little, what comes next is bound to be even more exciting.

4.4 Summary
We can simplify our code using switch-case statements, replacing if-else
statements. A switch-case statement checks the value of an argument
(variable) towards a list of possible values.
The ternary operator, if-else, and switch case statements share a lot of
similarities, but they each come with a set of benefits and limitations to
consider before using them in your code.
Looping, or iterating within our code is an important part of almost any
program. When we iterate we execute a single or multiple statement
over and over until the iteration reaches its course. Loops are always
based on a condition in which the loop will start, and once this condition
will no longer be true, the loop will terminate.
There are several types of loops used in C++.
While loop, which is an entry control loop, as the condition is
checked before any iteration starts.
Do-while loop, a post-iteration loop, executes a statement at least
once, regardless of whether the condition is true or false.
For loop, which iterates and increments the number of iterations,
and which is used when we want to control the repetition structure
and control a specific number of times our loop will iterate.
Infinite loop, which is a name given to any loop which runs
"forever", unless we manually stop it. In most cases, an infinite
loop will be a result of an error.
The 'break' and 'continue' keywords can be used in loops to 'break' (exit
the loop), or 'continue', which forces the next iteration of the loop to take
place.
Files are an important part of computer programming. C++ offers us a
lot of build methods to handle, read, write, open, close files, and more.
[1]To access the folder from which your program runs, right-click on the top
tab in your IDE with your program's name, then select from the menu "open
containing folder".
[2]
The term 'semi-infinite loops is of our own making for the sake of our
explanation in this book, and not a real term used in the wild.
5 Hip hip array – C++ arrays
This chapter covers
Exploring the concept of object collection
Introduction to Arrays and their role in C++
Working with single and multi-dimensional arrays
Unraveling the std::array container

Up to this point in your C++ journey, you learned how to work only with
single objects (variables) in your code. In this chapter, you will learn how to
work with multiple objects, generally known as collections.

We start by explaining how different data structures are used to manage and
manipulate collections of objects, and the general concept of collections. In
this chapter, we also introduce the basic concept of C++ containers, which
are designed to simplify the task of managing collections of objects by
providing a set of methods and operators that allow you to insert, remove,
search, and traverse the elements stored in them.

You will then learn about arrays, which are a fixed-size list of elements. We
start with C-style arrays, which are still widely used in C++ - you will learn
all about their role, and how and when to implement them in your code. You
will also learn about multidimensional arrays, which are a more complex grid
(matrix) of two arrays or more. You will practice an interesting program that
emulates a spreadsheet, using a multidimensional array.

Once you have mastered the use of C-style arrays, you will learn about the
std::array container, which is a modern alternative that provides a safer and
more convenient way of working with arrays. You will learn about various
methods used with std::array and practice a lot of code. By the end of this
chapter, you will be able to enhance and boost your code, by utilizing a range
of arrays and the brand-new methods you learned.

5.1 Because you’re not alone – working with


collections of elements
Up to this point, whenever you declared and initialized a variable, it always
handled a single value. For example, the statements
int score{90};

handles an element named score with a value of 90.

The statement
char f_name{‘d’};

handles a single value for the variable f_name. Each single variable occupies
its own single memory space.

Working with a single element might be restricting. For example, what if we


wanted to develop a game with 4 players - red, blue, green, and purple. From
what you have learned so far, you will probably write the following statement
to store their score:
int red {3};
int blue {14};
int green {32};
int purple {12};

If we use this statement, it means we need to maintain all four variables.


Imagine what would happen if we had 10,000 players – it would be
exhausting to maintain each one separately. Things can become even more
complex if we wish to set the score of the player – it would require a separate
line of code for each player. The conclusion is that using a single element is
resource-intensive and requires high maintenance of many moving parts.

Out in the wild, almost every real-life program deal with more than just
single elements or values at a time. Rather, programs use collections of
objects, lists of values, or sequences of elements - and for the purposes of this
introduction, let's refer to all these terms as collections. A single collection of
objects can contain many elements, and if we return to the game example, we
might have a single collection of all the players.
What makes collections so special

Collections abstract or hide the complexity of dealing with individual


elements by giving us this convenient way to work with them as a group. It's
like a shortcut that makes our code more organized and manageable.
Collections also provide the ability to iterate over their elements in a
structured and convenient way - this enables us to perform operations on
multiple elements of the collection using loops, such as accessing, modifying,
or performing calculations on each element without explicitly referencing
them individually.

In many collections, elements are accessible through indexing. Each element


is associated with an index value that represents its position within the
collection. This allows for direct access to individual elements based on their
index, providing flexibility in retrieving and manipulating specific elements
within the collection.

Unlike fixed-size single variables, some collection types can dynamically


grow or shrink as elements are added or removed – and we discuss this in the
next chapter when we talk about vectors.

When it comes to memory, you learned that when we deal with single
elements, each element occupies a memory address – these addresses are not
necessarily contiguous (sequential) with other objects. When we deal with a
collection of elements, on the other hand, the difference is that all the
elements will occupy contiguous (sequential) memory spaces. It means that
we can access any element in the collection by knowing the starting memory
address of the collection and the size of our collection (how many elements
are part of it). This is important to keep in mind when working with
collections, as it allows us to efficiently access and manipulate the elements
in the collection, as illustrated in Figure 5.1.

Figure 5.1 Elements in a collection are stored within a sequential memory address.
Tip

A good reference to illustrate the difference between a collection and a single


object memory wise, is a street with detached houses Vs apartment buildings:
Each detached house has its own yard, and it’s not attached to any other
house. A detached house will also hold its own unique house number. On the
other hand, apartment buildings contain several apartments. Each apartment
shares the same street address and house number, but each apartment is
differentiated by a unique and sequential apartment number. Similarly, just
like apartments in a building, elements in a collection share the same memory
address but are differentiated by their index or position in the collection – and
we explain what an index is, shortly.

Another important fact is that, generally, all elements in collections must be


of the same data type. We can have a collection of int, a collection of float,
and a collection of short, but we can’t have a collection of int combined
with float, or a collection of double combined with short.

Note

There are some exceptions, and there are some ways to create collections that
can hold elements of different data types in C++, and we get to that later in
this book, but for now, keep the same type rule in mind, as this is the normal
practice.

Collections and lists

We can think of collections as if it was a shopping list[1], a list of chores, a


phone list, and more. A list can be empty, contain a single value, or it might
contain an enormous number of values. In computer programming, lists
(collections) do not only come in various sizes, but they can also appear in
various forms and structures: they can come in a linear form, which means all
elements are related to their position along a line, non-linear form, table-
based form and more. Let's look at some common structures of data
collections illustrated in Figure 5.2, where you can visually see the difference
in structures. You can get a basic notion of what different structures mean,
even without really understanding the underlying concepts illustrated – just
treat this illustration as a visual cue for what’s coming next.

Figure 5.2 Examples of a few basic data structures: linear structure, binary tree structure, map
structure, and set structure.
As you can see, collections can come in different shapes and forms – and we
teach all about these forms throughout the book, specifically in chapter 12,
when you learn more about containers.

Moving forward, let’s pretend we have a program that handles a collection of


100 names, which, each will be stored in a sequential memory address - how
do we traverse through the collection to access a specific name? Can we add
names to the collection? Can we delete names from the collection? Can we
change a single letter in a name? Can we switch the order in which the names
appear?
These are important questions, and with C++ you can perform all these
operations, and so much more using various methods you are about to learn
in this chapter, as well as throughout this book.

Contain your enthusiasm – a brief introduction to C++ containers

C++ handles collections with grace thanks to the power of containers, which
are object holders that store ("contain") a collection of objects, and their
elements constructing the collection. Each container has the capability to
arrange the data in a certain way according to the data structure and is packed
with built-in capabilities to perform various operations, such as traversing,
inserting, removing elements, sorting, comparing, and more – it’s a simple
and elegant way to work with collections.

Good to know

C++ offers 16 container types as part of its Standard Template Library,


(which you will also learn all about in chapter 11). Each container is suited to
handle different data structures and use cases.

5.2 Array of sunshine: The concept of arrays


The first type of data collection we explore is arrays. An Array is a collection
of elements with a fixed range. When we say "fixed" we mean that arrays can
hold a fixed-sized sequential collection (in a linear form) - once their size is
declared, it cannot be altered. This means that when you declare an array in
your program, you must specify its size, and you cannot add or remove
elements from it during runtime.

Let’s say your array was declared to contain 30 elements - it will be possible
to keep the array empty, or contain 10, 25, or 30 elements, but it will not be
able to contain 31 elements. Can you think why? Simple: once we declare an
array size, this size is set aside by the compiler and memory storage is
assigned accordingly. Once the memory is assigned, we are unable to change
it, and any attempt to add values to an array outside its capacity might result
in a crash or error.
Tip

Think of an array as setting a capacity of 40 children who can ride in a school


bus: each child occupies a seat, up to exactly 40 seats. The bus might ride
empty, or with a single child, it can also ride with a capacity of 40 children,
but the bus driver will not allow the 41st child to board the bus, as it will be
out of capacity - no more seats available.

Remember

All elements in an array are to be of the same type. We cannot create an array
that holds both float and int or double and short.

C++ offers us two types of arrays: C-style arrays, which were inherited from
the C programming language. C-style arrays are a collection of elements, but
they do not come in the form of a container. Instead, the elements of an array
are handled in the same manner they were in C.

The second type of array is std::array, which is a container that belongs to


the Standard Template Library (STL) and was introduced in C++11.
std::array is also a fixed-size container, so just like the C-style array, its
size cannot be changed. std::array comes with the benefits of a range of
methods that allow for efficient element access, manipulation, and iteration
which C-style arrays do not have – and we will explain a bit more about these
differences shortly.

5.2.1 C-style arrays - Declaring and initializing them


Declaring and initializing a C-style array is similar to the way we declare and
initialize variables - the only difference is that we use square brackets to
declare the size of the array (how many elements it will hold). Figure 5.3
illustrates array declaration and initialization syntax.

Figure 5.3 Arrays’ syntax – we always define the elements data type, and define the number of
elements the array contains, followed by the values of the elements (the value can be left empty
during this point).
Let's take a closer look at the similarities and differences between arrays and
regular variables. Let's say we have a list of four scores. How would you
declare and initialize them with everything you learned so far about single-
value variables? You would probably write something like this:
int score_red {10};
int score_blue {20};
int score_green {30};
int score_purple {40};

If we use an array it will simplify our code. We can simply write:


int score [4] {10, 20, 30, 40};

As you can see, we have the data type (int), the variable name (score) the
number of elements in the array [4], and the initialized values of the score of
each player {10, 20, 30, 40}.

Now let's say one of the score is undetermined and is marked with the letters
”xxx”. Look what happens in the IDE (figure 5.4):

Figure 5.4 When we cannot mix data types in an array

What happened? “XXX” is string type (which is a data type you will learn
about in the next chapter), while the other element of the array is declared as
int type, and all the other elements are type int as well. This is the important
rule you must keep in mind: all the elements in an array MUST be of the
same type[2]. You can never mix data types in a single array.

Let's try something else. look what happens to the following code when we
type it into the IDE (figure 5.5):

Figure 5.5 We get an indication of an error if we add the 6th element.


Do you see what’s wrong? We have a little red line under the number 6. If we
hover our mouse over it, we can see why (figure 5.7):

Figure 5.6 If we hover our mouse over the error, we can see what’s wrong.
As you can see, our IDE indicates that we initialized too many values – we
have an array of 5 elements, and we try to initialize 6 – that will not work. Of
course, different IDEs might display different error messages, but the idea is
the same – your code will not compile unless you fix the problem.

Let's look at another simple code sample using an array, this time we use a
list of chars forming the word “hello”. Try and run the following code
#include <iostream>

int main()
{
char word[] = { 'h','e','l','l','o' };
std::cout << word << std::endl;
}

In this sample, we have an array, but we did not declare the number of
elements it will hold. In this case, the compiler will automatically assign the
actual number of elements in our array during compile time.

Important!

Remember that the number of elements in any given array must be at least
one, or larger than one. An array can never have a capacity of zero elements.

When we run this code, we get the word "hello", as we expect, but right
afterward we get some 'garbage' data (figure 5.7):

Figure 5.7 Our output includes some “garbage” data.


What went wrong? Well, when dealing with C-style arrays, and when we use
char type, we must bind them, or, in other words, they must have a defined
beginning and end. The compiler knows where the beginning of the array of
chars is, but if it doesn't know where the array ends, it will result in 'garbage'
data.

To handle this problem, we use a special character for that purpose – a null
terminator \0 which is placed at the end of the array. In our example, if we
place \0 after the last character o, ("'h','e','l','l','o','\0'), it will
display "hello" without 'garbage'.

Now let's run this code again, this time we add the null terminator '\0' at the
end of the array.
#include <iostream>

int main()
{
char word[] = { 'h','e','l','l','o','\0' };
std::cout << word << std::endl;
}

Let’s run this code. Now we get the correct output:

Important

Array of other data types such as int will behave differently and will not
require a null terminator, but the char type will – as we explain later on.
good to know:

We can also use boolean type with arrays when the array needs to store
true/false values or flags.

5.2.2 From zero to index: why the index matters


As you learned, we call the variables of an array “elements”, but each
element does not have its own unique name. As you just learned, each
element of an array can be accessed jointly with all the other elements (as we
demonstrated when we printed the word "hello"). But elements can also be
accessed individually.

Each element in the array holds an index position. Think of it as a row of


seats in the movies – each seat is numbered, or “indexed”. An array also has a
unique index or position, and this position is used to access the element or
perform operations on it. For example, just as you might ask someone to pass
you the popcorn in the third seat from the left, you can access the element in
the third position of an array using its index.

However, in C++ the first element will always be indexed as 0 (zero)[3], while
in real life, the first seat will be numbered 1st. So in the event, an array holds
100 elements, the position of the last element will be 99. The chart below
(Figure 5.8) demonstrates the index of an array that contains 14 char-type
elements.
char letter [14] {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'I', 'j', 'k', 'l'

Figure 5.8 An index of any array will always start the count of elements with a 0.

To access individual elements of an array, we use the array name along with
the operator [] (also known as the subscript operator), along with the index
(also known as a subscript) that tells the compiler which element we want.
This process is called subscribing or indexing.

If we want to access an element, all we need to do is define the index


position. For example, let’s go back to our array of char char word[6] = {
'h','e','l','l','o','\0' }; want to access the first and last letter. Here
is what our code will look like:
#include <iostream>

int main()
{
char word[6] = { 'h', 'e', 'l', 'l', 'o', '\0' };

char first_letter = word[0];


char last_letter = word[4];

std::cout << "The first letter of the word is: " << first_letter << std:
std::cout << "The last letter of the word is: " << last_letter << std::e

return 0;
}

As you can see, we used these two statements:


char first_letter = word[0];
char last_letter = word[4];

to access the 1st and 4th elements. So our output should be:
Let's look at another simple example of accessing a single element in an
array. In the following code, we initialize an array of int and print to the
console values from our array by accessing each element. By this point, the
code should be easy to read and understand.

Listing 5.1 Accessing the elements of your arrays

#include <iostream>

int main()
{
int nums[] = { 5, 10, 15, 20,15 };

std::cout << "I have " << nums[0] << " fingers on my left hand." << std:
std::cout << "I have " << nums[1] << " fingers on my right hand." << std
std::cout << "I need " << nums[2] << " cups of coffee to function in the
std::cout << "I can run " << nums[3] << " miles without stopping." << st
std::cout << "My dog is " << nums[4] << " years old." << std::endl;

return 0;
}

Once we run this code, we should expect the following output:


Now let’s use this code to change the values of our arrays’ third elements. To
do so, we only need to use the assignment operator = in the following
statement:
nums[2] = 3;
If we add this statement after the second std::cout we will get the following
output (figure 5.9):

Figure 5.9 After the assignment to a specific index location our output has changed.
As you can see from both codes, accessing the elements of an array using the
index position, using them, or altering them, is simple and straightforward.

Note

In the next chapter we explore how to access elements using the at(),
front(), and back() functions, which can be used in several types of
containers in C++, including std::array.

5.2.3 While loops and arrays – the power couple

While loops and arrays are a great match: You can use while loops to iterate
through the elements in the array by incrementing or decrementing the index
position. Let’s start with a very simple code. In the example below, we have
an array of int holding seven grades. We can print the elements to the
console using a while loop.

Here is our code:


#include <iostream>

int main()
{
int grades[]{ 60, 98, 46, 84, 55, 100, 67 };
int i{ 0 };#A
while (i < 7)#B
{
std::cout << grades[i] << std::endl;
++i;#C
}

return 0;
}

When running this code, we should expect to see 7 elements (counted from 0-
6 index positions)
Let’s move on to a more interesting code, this time we are going to use a
while loop and some conditions using if-else. The following program is a
joke generator where the user keeps track of the number of times each joke
has been told. We first initialize an array named joke and increment the
count of jokes in the array. We use a while loop as we ask the user if he/she
wants to hear another joke. If the user chooses to hear another joke, the
program selects a joke that hasn't been told, incrementing its count in the joke
array. If the user chooses not to hear another joke, we exit the loop.

Listing 5.2 Using while loop with arrays: A joke program

#include <iostream>

int main()
{
int jokes[] = { 0, 0, 0, 0 }; #A
int jokeIndex = 0; #B
bool keepTellingJokes = true; #C

std::cout << "Why did the chicken cross the road?" << std::endl;
std::cout << "To get to the other side!" << std::endl;
jokes[jokeIndex]++; #D
std::cout << "---------------" << std::endl;

while (keepTellingJokes) #E
{
std::cout << "Do you want to hear another joke?" << std::endl;
std::cout << "Enter Y for yes or N for no: ";
char userChoice; #F
std::cin >> userChoice;

if (userChoice == 'Y' || userChoice == 'y') #G


{
jokeIndex++;
if (jokeIndex > 3)
{
jokeIndex = 0;
}
switch (jokeIndex)#H
{
case 0:
std::cout << "Why don't scientists trust atoms?" << std::end
std::cout << "Because they make up everything!" << std::endl
break;
case 1:
std::cout << "What did the grape say when it got stepped on?
std::cout << "Nothing, it just let out a little wine." << st
break;
case 2:
std::cout << "Why did the tomato turn red?" << std::endl;
std::cout << "Because it saw the salad dressing!" << std::en
break;
default:
std::cout << "Why did the scarecrow win an award?" << std::e
std::cout << "Because he was outstanding in his field!" << s
break;
}

jokes[jokeIndex]++;#I
std::cout << "---------------" << std::endl;
}
else
{
keepTellingJokes = false;
}
}

std::cout << "Thanks for listening! Goodbye!" << std::endl;

return 0;
}

Let’s look at our output:


Let’s try one more code, this time we use continue and break to control the
flow of our loop. In this code, we iterate through an array of integers and
check if each number is divisible by 5. If an element is divisible by 5, the
program will output a message saying so and continue to the next element. If
an element is not divisible by 5, the program will output a message saying so
and count how many elements were not divisible by 5.

Listing 5.3 Using continue and break in a while loop with arrays

#include <iostream>

int main(void)
{
int numbers[]{ 1,3,4,6,7,25,54,44,33,-1 }; #A
int i{ -1 };
int does_not_divide{ 0 };

while (true) #B
{
i++; #C
if (numbers[i] == -1) break; #D
if ((numbers[i] % 5) == 0)
{
std::cout << "number " << numbers[i] << " divides by 5" << std::
continue; #E
}
std::cout << "number " << numbers[i] << " does not divide by 5" << s
does_not_divide++; #F
}
std::cout << "There are " << does_not_divide << " numbers found not divi
}

Once we run this code, we should expect the following output:


5.2.4 For the love of array: For loops and arrays

Just like while loops, for loops, can be a great choice to use whenever we
need to iterate through an array's elements. Let's say we have an array of char
type with letters that form the name P E N E L O P E. How can we display
each letter separately to the console? Let’s see how a for loop can easily help
us.

Tip

Notice that in this case we declare and initialize our control variable i within
the for loop and not beforehand, making the code more concise and easier to
read. It also limits the scope of the variable to the loop, so it’s less likely to be
accidentally used outside of the loop or modified in unintended ways.
#include <iostream>

int main()
{
char name[]{ 'P', 'E', 'N', 'E', 'L', 'O', 'P', 'E' };
for (int i{ 1 }; i <= 7; ++i)
{
std::cout << name[i] << std::endl;
}
}

Once we run this code, it seems that something went wrong - the first letter P
is missing. Can you try and guess why this happened?
An index of arrays always starts with a 0, yet we started incrementing at 1,
which means that the console displayed the second element, cutting our P.
Since the loop iterates one more time, we might even get some garbage at the
end of the list.

In Chapter 2 you learned that if we do not initialize a variable, and leave the
curly brackets empty (for example i{};), the compiler will initialize it to 0
by default. In our case, we can leave i uninitialized, and the problem will be
solved, but it’s best practice and highly recommended to always initialize a
variable when it is defined, rather than relying on its default value, which
may not be 0 and could potentially contain random or garbage values.

Let’s look at our code:


#include <iostream>

int main()
{
char name[]{ 'P', 'E', 'N', 'E', 'L', 'O', 'P', 'E' };
for (int i{ 0 }; i <= 7; ++i)#A
{
std::cout << name[i] << std::endl;
}
}

Range-based 'for' loop – simplifying your code even more

When we used a for loop in the previous chapter, we always had to worry
about the position or length of each element. In other words, we had to know
the number of iterations we want to control beforehand. Sometimes we don't
know how many times we need to iterate, or we just don't want to bother. A
range-based loop (also known as a “for-each” loop), is executed based on
range, "range" being the range of values in an array, is a good way to solve
this problem, and offers a simpler and more concise way to iterate over the
elements of an array, (as well as other collection types such as vectors, which
we teach in the next chapter, and more).

Good to know
Range-based loops were the first modern loop that was introduced in the C++
11 standard, and as such, it uses a more simplified form of iteration,
simplifying code that would otherwise require more verbose loop constructs.

The syntax of range-based loops is similar to what we have used so far, with
simple changes. Let's look at the structure and see what these changes are
(figure 5.10).

Figure 5.10 The syntax of a range-based loop is slightly different than the regular for loop.

Let's look at a very simple example first: in the next code we create the array
arr and then iterate through its elements using the range for loop.
#include <iostream>

int main()
{
int arr[] = { 1, 2, 3, 4, 5 };
for (int x : arr) {
std::cout << x << " ";

Once we run this code, the output will be “1 2 3 4 5”.

Now let’s create an array of chars, and use range-based for loops to display
the elements.
#include <iostream>

int main()
{
char Letters[]{ 'P', 'E', 'N', 'E', 'L', 'O', 'P', 'E' };
for (char Letter : Letters)#A
{
std::cout << Letter << std::endl;
}
}

When we run the code, we get the following output – a list of all the letters in
the name Penelope:
Why range-based for loops rock

We already explained that range-based for loops work great with arrays and
other collections. Another reason why this type of loop rocks is that it can
prevent “off-by-one” errors, which is a common programming mistake that
occurs when a loop, (or index), is incorrectly incremented or decremented,
causing the program to either skip or repeat an element in a collection.
Range-based for loop automatically handles the iteration over the elements in
the collection or array, without needing to track the index variable or the
number of iterations manually.

The thing is, that in a traditional loop, it's easy to make mistakes when
specifying the loop condition, or the iteration step, which can lead to off-by-
one errors or other bugs that are hard to track down.

With a range-based for loop, the loop condition and iteration step are handled
automatically based on the size of the collection or array being iterated over,
so there is less opportunity for mistakes – and that’s why they are great. Let’s
not forget to point out, that the syntax of range-based for loops is often more
concise and readable than traditional loops, making the code easier to
understand and maintain.

As you can see, working with range-based for loops makes life easier - you
don't need to bother with the location or length of the elements in the index,
and it also simplifies our code.

Let’s look at one more code sample. In this code, we initialize an array of int
named nums which holds 5 elements. The user needs to enter a number to
search for, and our range-based for loop is used to search for the number in
the array. If the number is found, it prints out the index at which it was found,
and if not, it prints out a message indicating that it was not found in the array.

Listing 5.4 Using range-based for loops to search for a value in an array

#include <iostream>

int main()
{
const int NUM_ELEMENTS = 5;#A
int nums[NUM_ELEMENTS] = { 1, 2, 3, 4, 5 };#B

std::cout << "Enter a number to search for: ";


int searchNum;#C
std::cin >> searchNum;

bool found = false;#D


int index = 0;#E
for (int num : nums) #F
{
if (num == searchNum) #G
{
found = true;
break;
}
++index;
}

if (found) #H
{
std::cout << searchNum << " was found at index " << index << std::en
}
else
{
std::cout << searchNum << " was not found in the array" << std::endl
}

return 0;
}

Once we run this code, a possible output might be:


5.2.5 Multi-dimensional arrays – simplify visually related
arrays

Up to this point, you learned all about single-dimension arrays, meaning we


have a list of elements indexed from 0 and up. However, C++ supports
multidimensional arrays, which are two or more arrays visibly related – and
though it might sound confusing, it’s really not, as you’re about to see.

First, multidimensional arrays are stored in tabular form, which means using
a chart that organizes information in rows and columns. An example of using
Multidimensional arrays would be a movie rating form, where the user can
input the name of a movie and its rating. We will have two arrays in this case:
one array will hold a list of codes the user can select from, each code
representing a name of a movie, and the other array will hold the rating,
which will be a range of numbers (let’s say 1 to 5 stars). Both arrays will be
set as multidimensional, as there is a direct correlation between the elements
in each array – and we demonstrate it in a code shortly.

Multidimensional arrays – what are they good for?

Many benefits are using multi-dimensional arrays in certain use cases, such
as storing data taken from tables, matrixes, spreadsheets, or databases, where
you need to place values and fetch values based on their relative location
within a large complex data source. For example, we once wrote a program
that handled a set of tables containing information about all the billboards in
the US, divided into each of the 50 US states. In this case, we used a three-
dimensional array: the first represented state (dimension 1), the second
represented the location (dimension 2) and the third represented the data
(dimension 3).

You can also take an Excel spreadsheet and relate it to the data within your
code as a two-dimensional array, where dimension 1 is the row, and
dimension 2 is the column.

Note

We can of course use more than two dimensions and create complex array
grids, but in this book, we only teach you about two-dimensional arrays,
though we wish we had the space to teach you much more, as this concept in
computer programming is challenging and extremely rewarding to master.

Declaring and initializing a 2D array

The syntax for declaring multi-dimensional arrays is pretty simple and


logical, as you can see from the image below (figure 5.11).

Figure 5.11 The syntax of a 2D array

As you can see from Figure 5.11, when we declare and initialize a 2D array,
we need to declare both dimensions. Remember the movie rating example?
Let's design a two-dimensional array that will allow the user to rate a movie.
Here’s the use case:

We have 5 movies and ratings (from 1-5) made by 4 reviewers. The first step
would be to design the attributes we are going to use. Our two-dimensional
data set will have three important attributes:

1. Dimension one (row) - reviews


2. Dimension two (column) – movie
3. Crossing point where dimensions 1 and 2 meet: the rating (1-5) given by
a reviewer (1) to a movie (2) is (3).

This logic is demonstrated in Figure 5.12.


Figure 5.12 Four movies option from 0-3, and 5 rating options (0-4) in a multidimensional array
structure.

As you can see, we have 4 rows (representing 4 reviews), and 5 columns


(representing 5 movies), each with its own index. You can follow each row
and column and see how the rating matches each movie (movies are
represented by index only and not by name here). It's not very complicated -
it's even a bit like working with spreadsheets.

Now let's see how it will look if we declare and initialize this two-
dimensional grid in code, but in this case, we are adding another row for the
user, which will be the 5th reviewer (index 4):
int rate[5][5] =
{
{3, 3, 2, 1, 4}, #A
{2, 1, 2, 5, 3},
{1, 1, 2, 3, 4},
{4, 5, 5, 1, 4},
{-1,-1,-1,-1,-1} #B
};

Notice the difference in the syntactical structure: we initialize each row and
column within curly brackets, which hold other sets of curly brackets per
raw, separated by a comma, while the last curly bracket ends with a
semicolon. This is a more complex statement than what you used so far - take
some time to read it.

Tip

It is best practice to try and build your own multidimensional arrays. Try to
think of use cases and design your own multidimensional arrays accordingly.

The last thing you need to learn is how to access elements within a
multidimensional index. We accessed single-dimensional arrays earlier, and
multidimensional is very similar – the only difference is that we need to
indicate two index positions instead of one. Let's look at an example:
int myRate = rate [2][1];

In this case, we access the 3rd element in index one, and the 2nd element in
index two. Looking at our multidimensional array of movie ratings, can you
guess the result? If you guessed 1, you are correct, taking the value at the
third row (index 2) and second column (index 1) of the array.

Now let’s write our movie rating program. In this code, we ask the user to
choose a movie from the movie list, and then display the rates given to the
movie by other “users”. The user is then asked to input his rating, and the
program will display the list of ratings again.

Note

Because our user might enter lowercase input, we will use the toupper()
function, which simply converts lowercase characters into their
corresponding uppercase.

Listing 5.5 Use a multidimensional array for a movie rating program

#include <iostream>

int main()
{
int rate[5][5] =
{
{3, 3, 2, 1, 4}, #A
{2, 1, 2, 5, 3},
{1, 1, 2, 3, 4},
{4, 5, 5, 1, 4},
{-1,-1,-1,-1,-1} #B
};

std::cout << "Available movies:" << std::endl;


std::cout << "A. Spiderman" << std::endl;
std::cout << "B. Jurassic Park" << std::endl;
std::cout << "C. The Godfather" << std::endl;
std::cout << "D. The Lord of the Rings" << std::endl;
std::cout << "E. Star Wars" << std::endl;

int new_rating_index = 4; #C
char movie_code;
int rating;
std::cout << "Enter the movie code (A-E): ";
std::cin >> movie_code;
movie_code = std::toupper(movie_code); #D
std::cout << "Movie (" << movie_code << ") Got the following ratings: "
unsigned int movie_index = movie_code - 'A'; #E
for (int i = 0; i < 4; i++)
{
std::cout << "Reviewer " << i + 1 << " gave the movie " << rate[i][m
}

std::cout << "Now it's your turn. Enter your rating (1-5 stars) to a mov
std::cin >> rating;

rate[new_rating_index][movie_index] = rating; #G

std::cout << "You rated Movie " << movie_code << " " << rating << " star
std::cout << "Now let's see the overall ratings given" << std::endl;
for (int i = 0; i < 5; i++)
{
std::cout << "Reviewer ";
if (i == new_rating_index)
{
std::cout << "(that's you!)";
}
std::cout << i + 1 << " gave the movie " << rate[i][movie_index] <<
}

return 0;
}

When we run this code we should expect the following possible output:
2D arrays are just fancy 1D arrays…

A 2D array is essentially a collection of 1D arrays. When we declare a 2D


array, we are telling the compiler to create a contiguous block of memory to
hold a certain number of elements in each dimension. In other words, the
elements in each row of the 2D array are stored sequentially in memory. Let’s
look at an example – below we have a 2D array:
int arr[3][4] =
{
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};

In memory, this array is actually represented as a block of 12 consecutive


integers as you can see here:
[1][2][3][4][5][6][7][8][9][10][11][12]

When we access a specific element in the 2D array, we are basically using an


offset from the start of this block of memory. For example, let’s say we want
to access the element arr[2][1], which has the value 10 in this case - we
would use the following offset:
offset = (2 * 4) + 1 = 9

This offset corresponds to the 10th integer in the block of memory - which is
the value we want, so you can see that a 2D array is merely a way to organize
a contiguous block of memory into rows and columns. Accessing an element
in the 2D array is just a matter of using (or computing) the appropriate offset
from the start of the block.

5.2.6 Code practice: Write a two-dimensional array


Spreadsheet-like code
We already mentioned that a two-dimensional array is similar to a
spreadsheet. Let’s say we store data from an Excel spreadsheet in a variable -
this variable will most likely be a two-dimensional array (or a more complex
container, which will be discussed later in this book). Our array will hold a
data type that is suitable to all possible values (since, as explained, a variable
such as an array, must hold data of a single type). Once stored in a two-
dimensional array, we can easily perform manipulations and calculations
such as:

Calculate the sum of the values in each row or column.


Replace the value in a cell with a new value.
Set a given value to an entire sequential cell.

For the purpose of our example, let’s assume we have an Excel spreadsheet
with 10x10 cells. Figure 5.13 shows how our spreadsheet will look like an
MS Excel:

Figure 5.13 An MS Excel spreadsheet with 10X10 cells can be seen as a two-dimensional array.

Let’s write a program that inserts data into specific cells in our spreadsheet.
Let’s start by defining our spreadsheet, as a two-dimensional array of int
(figure 5.14).

Figure 5.14 Our two-dimensional arrays’ structure of rows and columns.

As you can see in Figure 5.7, we initialize all cells to hold a 0 value, and we
initialize each row as a set of values (of each of the columns within that row),
separated by curly brackets.

Tip

Most IDEs will allow you to point the mouse to a variable and view its
contents. In this case, if we point the mouse to a two-dimensional array, we
can see its contents (figure 5.15).

Figure 5.15 When we point our mouse at our two-dimensional array, we can see its content.
Now, we need to insert data into specific cells. In our real spreadsheet, we
can see that each row is given a number from 1 to 10, and each column is
given a character from a to j. To set a value of a cell, we use the two
dimensions of the array to specify the row and the columns starting from 0.
For example, if we want to place 99 in cell equivalent to D4 in a real
spreadsheet, in our code we will use the following statement to do the same
(figure 5.16):

Figure 5.16 Declaring a two-dimensional array requires us to declare both dimensions.


However, when we need to write code to mimic a spreadsheet, we need to
find a way to “search and replace” our two dimensions with rows and cells.
How do we do that? Well, C++ offers us two methods: using a macro (which
is the old and less popular way) or using an inline function (which is the
modern and better way). In the following section, we will explain how to do
it the better way: using an inline function.

Note

At the end of this section, we will briefly explore why macro it’s the less
popular choice in modern C++.

But before we move on and explore how the inline function will work in this
case, let us understand the replacement logic: our replacement will be
conducted during runtime, based on the following formula: the first value
(col) will be replaced with col-‘A’, and the second value (row) will be
replaced with row-1.

remember

Our index starts with 0, yet in a spreadsheet it starts with 1, so we need to


decrement by 1 when we deal with a row (therefore we use row-1). If we deal
with row number 7, and use our formula, the index position of the row will be
7-1, and therefore 6).
Our columns on the other hand are numbered with letters:

We know that the letter A should represent index 0. The ASCII value of A is
65. If we use our formula col – A it’s as if we say 65-65, and the result is 0.
Moving on to the next column B, the ASCII value of B is 66 and the index
position should be 1. Going back to our formula, 66-65 will result in 1, and
so forth.

Now our code can be more intuitive and readable as we write the following
statements:
cell('D',4)= 99;
cell('I', 10) = 30;
cell ('F', 2) = 19;
cell ('G', 5) = 93;
cell ('A', 4) = 12;

Row 4 and column D (4D) will be represented in our index as 3 and 3. The
statement cell ('D',4)=99; will be replaced by our inline function with the
statement spreadsheet[3][3]= 99.

To make it even more clear, we can take the first line in our statement cell
('D',4)= 99; and see how it is represented in a real spreadsheet (figure
5.17).

Figure 5.17 In our program, we want the output to align with our spreadsheet.
Using inline function

Functions in any programming language are a way to simplify our code by


breaking it into smaller, reusable pieces of code. Instead of repeating the
same code multiple times throughout our program. We know we haven’t
explained the concept of functions yet in this book (and we will in chapter 8),
but you don’t need to know functions in order to understand the underlying
logic of what we are trying to achieve here: we need to reputedly replace our
2D dimension into rows and column.

As mentioned, C++ offers a powerful and type-safe method: an inline


function. Though we discuss inline functions in chapter 8, let’s explain what
an inline function is and how it works in a nutshell – and don’t worry if
something will not feel clear – see this section as planting a seed, which will
grow in time.

An inline function is a function that is defined using the inline keyword.


When we call an inline function, the program copies the code from the
function directly into the place where the function was called with minimal
overhead, so it's a great way to replace our dimensions with rows and
columns.

Let’s look at Figure 5.18 which shows the syntax of our inline function in this
case and the replace logic.

Note

Though break the inline function’s statement down for you, it might still
seem a bit confusing. If you feel you don’t understand it, don’t worry – we
will explore the subject and explain it further in chapter 8.

Figure 5.18 Our inline will return the corresponding cell in the spreadsheet based on the input:
the first value (col) lead to returning the 1st index equal to col-‘A’, along with the 2nd index
which is row-1.

Now let’s break down this statement – so what do we know?

We know that col specifies the column letter of the cell we want to retrieve,
and row specifies the row number of the cell we want to retrieve. With this
information, we can locate the cell (and the item in the 2-dimensional array
that represents the Excel sheet).

How do we convert between the way Excel maps cells (using ABC and 123),
into the way arrays (and specifically 2-dimensional ones) are mapped?
As explained, we do that by subtraction by 1. We use row - 1 because an
array in C++ starts at 0, while row numbers start at 1. The subtraction of A
from col is used to convert the column letter to a zero-based index. For
example, if col is the character ‘C’, the expression col - 'A' evaluates to 2,
which corresponds to the third column in the spreadsheet array.

As for the expression int& – well, don’t worry about it too much, as we
explain why it is written the way it is written in chapter 8, but generally
speaking, int& is a reference to an integer, which is a way to create an alias
or “nickname” for an existing integer variable. This is useful when we want
to modify the original integer variable (in this case inside our inline function).
Again - don't be bothered if you don't understand how this inline function
works, or if its syntax confuses you – we go back and explore it in chapter 8
and beyond. Meanwhile, it's enough to understand why we need an easy
replacement mechanism in our code and the replacement's logic.

Note

Our inline function will be declared outside of main(), since it’s a reusable
code block, and our code in main can call the function and use it whenever
needed. We further discuss the reusability and structure of functions within
our code in Chapter 8.

Iterating through our two-dimensional array and printing it to the


console

At this point, we want to print the table to the console. Let's look at the next
code block we want to write and analyze (figure 5.19).

Figure 5.19 Our next code block


\t is for tab
In C++, (as well as in many other programming languages), \t is an escape
sequence that represents a tab. Whenever we include \t in our code, it
instructs our output to move the cursor to the next tab, and we use it
whenever we need to align text in a specific way within a console. In our
code, we need to use a tab to arrange the output in a specific spreadsheet-like
order and we do so using the statement: const char *SEPARATOR = "\t";.

Earlier in this section, we introduced a user-friendly way to access our two-


dimensional array, allowing us to access items in it as if they were Excel cells
(A4, B3, etc.). However, this time we access our array directly and not via the
inline function. In case you're wondering why, the reason is that we are
passing to it indexes which are iterated by the two for loops, as opposed to
passing to it Excel-like cell labels.

Now let’s take a look at our full code and run it.

Listing 5.6 Writing a spreadsheet-like table

#include <iostream>

const int ROWS = 10; #A


const int COLS = 10;#A

int spreadsheet[ROWS][COLS]{}; #B

inline int& cell(char col, int row) #C


{
return spreadsheet[row - 1][col - 'A'];
}

const char SEPARATOR[] = "\t";#D

int main()
{
#E
cell('D', 4) = 99;
cell('I', 10) = 30;
cell('F', 2) = 19;
cell('G', 5) = 93;
cell('A', 4) = 12;

std::cout << SEPARATOR; #F


for (int col = 0; col < COLS; col++)
{
std::cout << (char)('A' + col) << SEPARATOR;
}
std::cout << std::endl;

for (int row = 0; row < ROWS; row++)#G


{
std::cout << row + 1 << SEPARATOR;
for (int col = 0; col < COLS; col++)
{
std::cout << spreadsheet[row][col] << SEPARATOR;
}
std::cout << std::endl;
}

return 0;
}

Once we run our code, we should expect the following output (figure 5.20):

Figure 5.20 The output our program generates resembles a spreadsheet.


Let's take this code a bit further. In an Excel spreadsheet, we can calculate the
sum of values as illustrated in Figure 5.21. Let’s do the same and use column
J for this purpose.

Figure 5.21 the output our program generates with a calculation of the sum of each row in row J.
In order to do so programmatically, we go over each row (from 0 to 9). First,
we set the Sum column of that row to 0.
spreadsheet[row][9] = 0;

then we go over each row from 0 to 8 (as we reserve columned 9 for the sum)
and add the value of that cell to the sum column.
for (int col = 0; col < 9; col++)
{
spreadsheet[row][9] += spreadsheet[row][col];
}

Let’s add this to our code.

Listing 5.7 Inserting values into a spreadsheet-like table with sum calculation

#include <iostream>

const int ROWS = 10;


const int COLS = 10;

int spreadsheet[ROWS][COLS]{};

inline int& cell(char col, int row)


{
return spreadsheet[row - 1][col - 'A'];
}

const char SEPARATOR[] = "\t";

int main()
{
cell('D', 4) = 99;
cell('I', 10) = 30;
cell('F', 2) = 19;
cell('G', 5) = 93;
cell('A', 4) = 12;

std::cout << SEPARATOR;


for (int col = 0; col < COLS; col++)
{
std::cout << (char)('A' + col) << SEPARATOR;
}
std::cout << std::endl;

for (int row = 0; row < ROWS; row++)


{
spreadsheet[row][9] = 0; #A

for (int col = 0; col < 9; col++)#B


{
spreadsheet[row][9] += spreadsheet[row][col];
}
std::cout << row + 1 << SEPARATOR;
for (int col = 0; col < COLS; col++)
{
std::cout << spreadsheet[row][col] << SEPARATOR;
}
std::cout << std::endl;
}

return 0;
}

Once we run this code, we can expect the following output which matches
our spreadsheet (figure 5.22):

Figure 5.22 our spreadsheet-like output with the addition of the total sum
As you can see, multidimensional arrays are not that complicated to work
with, and we continue using them in additional code samples later in the
book, so you can keep on practicing them.

5.2.7 std::array – more powerful than C-style array


As we mentioned earlier, arrays have been around from the early days of C,
and everything you learned so far relates to C-style arrays. Modern C++, and
specifically the C++ 11 standard, took arrays a little bit further and enhanced
the way C-style arrays can be used in your code, by adding std::array
container to the C++ standard library.

C-style arrays and std::array share the exact same concept of fixed size
which cannot be modified after compilation. However, std::array is part of
C++ Sequence containers (and part of the C++ Standard Template Library
you will learn about in chapter 10). We introduced the basic concept of
containers at the beginning of this chapter and described them as object
holders, which can hold objects according to their structure.

In C++, sequence containers store sequencing elements in a linear form,


which means all elements are related to their position along a line[4] - and this
is the exact structure of an array.

Think of std::array as a wrapper that wraps C-style arrays and shares the
concept of restricted size. As explained, the difference between the two is that
the std::array container offers us some useful methods which are not
available with arrays. For example, we can use the function size() to know
the size of an array at any given time. It means we can conduct out-of-bound
checks before and after compilation – doing so can indicate any event where
we are trying to access the array beyond its defined range.

Good to know

In C-style arrays, we can use the sizeof operator to know the size of an
array, but it will only provide us with the size in bytes. If we wish to know
the actual number of elements of a C-style array, we need to do a little
'detour': we must divide the total size in bytes by the size of a single element,
then we can get the number of elements. For example, if we have an int-type
array, and we know its size is 16 bytes, and since we know the size in bytes
of an int is 4, the calculation will be 16:4=4 – now we know we have 4
elements.

Important!

When we use std::array we need to include the <array> header file.

Declaring and initializing std::array


Another difference between C-style arrays and std::array is the
initialization style. Let’s recap what you learned so far and compare it with
the std::array syntax illustrated in Figure 5.23.

Figure 5.23 C style array syntax always contains the data type, an array's name, the number of
elements, and values (values can be left uninitialized).

with std::array we have a slightly different syntax illustrated in Figure


5.24:

Figure 5.24 In std::array style syntax we place the data type followed by the arrays’ size in angle
brackets, followed by the arrays’ name and elements values.
As you can see, the way we declare std::array and C style array is different,
and maybe slightly more eye soaring. With std::array we use the angle
brackets, which contain both the data type and the array's size. We also need
to use std::array as part of our declaration.

Tip

There’s no denying that the std::array syntax will take some getting used to.
But with muscle memory and a lot of practice, it will become natural.

Assigning values to std::array using an initializer list

In C++ we can also use an initializer (assignment operator) to initialize


arrays. For example, let’s say we have the following array:
std::array<int, 5> arr;
arr = { 10, 20, 30, 40, 50 };

This statement will work well as we assign all 5 elements in the array.
arr = { 11, 15, 21 };
This statement will work well but note that elements 3 and 4 will be set to 0.
arr = { 10, 20, 30, 40, 50, 60 };

This statement will not work – there are more elements than the initialized
array size.

std::array and the null terminator

Though std::array is considered type-safe, we still need to use a null


terminator when we handle an array of chars for string purposes. To
demonstrate it, let’s run the following code, which checks if we have a null
terminator or not:
#include <iostream>
#include <array>

int main()
{
std::array<char, 10> a;
a[0] = 'h';
a[1] = 'e';
a[2] = 'l';
a[3] = 'l';
a[4] = 'o';

std::cout << "Array contents: ";


for (char c : a)
{
std::cout << c;
}
std::cout << std::endl;

std::cout << "Printing characters until null terminator: ";


bool hasNullTerminator = false;
for (char c : a)
{
if (c == '\0')
{
hasNullTerminator = true;
break;
}
std::cout << c;
}
std::cout << std::endl;

if (!hasNullTerminator)
{
std::cout << "Warning: no null terminator found in the string." << s
}

return 0;
}

Once we run this code, the output will be:

std::array and range-based for loop

One of the advantages of knowing the size of your array is that it allows
smooth work using range-based for-loops together with std::array. As you
recall, range-based for-loops are a more convenient, safer (and awesome)
way to loop through the elements of a container. We can iterate over each
element of an array without having to worry about its size, as the loop
automatically iterates through each element from the beginning to the end of
the array, preventing off-by-one errors and other common mistakes that can
occur with traditional loops. For example, let’s look at the following code:
#include <iostream>
#include <array>

int main()
{
std::array<int, 5> arr = {1, 2, 3, 4, 5};

for (int element : arr)


{
std::cout << element << " ";
}

std::cout << std::endl;


return 0;
}

Once we run this code, the output will be “1 2 3 4 5”

Remember

Range-based for-loops are generally preferred over traditional for-loops for


iterating through std::array and other containers, as they are simpler to read
and less prone to errors.

What’s your size()

The std::array container (and, in fact, all other C++ containers), comes with a
useful function: size(). The size() function is used to return the number of
elements in the array, which is the size of the array. For example, let's say we
declared a std::array of int type with a size of 5 and initialized it with
values. We then called the size() function, which will return the number of
elements in the array. Let’s see how it works in code:
#include <iostream>
#include <array>
int main()
{
std::array<int, 5> arr = {1, 2, 3, 4, 5};
std::cout << "Size of the array: " << arr.size() << std::endl;
return 0;
}

The output of this code will be: Size of the array: 5

Note

C-style arrays do not support size(), as you can see in Figure 5.25, where we
have a C-style array named arr1 and std::array named arr2. You can see an
error indicating that arr1 does not support size().

Figure 5.25 code sample showing the different ways we declare std::array and C style array. C
style array does not support the size() function, while std::array does.
Important!

The term “size” in the std::array class refers to the array’s length and has
nothing to do with the results of the sizeof() operator when used on a fixed
array. As you learned in Chapter 3, if we use sizeof() it will return the
actual size of the array in memory (we multiply the size of the elements with
the array’s length). It’s a bit inconsistent, and therefore important to note.

Size() is an unsigned integer type, which is an important thing to remember.


Let’s look at the following code to understand a bit better:
#include <iostream>
#include <array>

int main()
{
std::array <int, 5> arr{ 23, 32, 12, 6, 34 };

for (int i{ 0 }; i < arr.size(); ++i)


std::cout << arr[i] << ' ';

std::cout << '\n';

return 0;
}

Can you guess what might be wrong with this code? (Clue: look for signed
and unsigned types).

Well, the answer is that there is a type mismatch, since, as we mentioned,


size() is an unsigned integer type, but the loop counter i is a signed int.
The lines i < arr.size() and the array index arr[i] contain a type
mismatch.

How do we solve this problem? We use a std::size_t – an indexing safe type.

5.2.8 Size meets Mr. T – indexing our array using size_t


size_t is a C++ built-in unsigned integer type, which is defined in the
<cstddef> header file. The size of size_t is platform-dependent but is
guaranteed to be able to represent the maximum number of any object that
can be stored in memory – not just with std::array, but actually with all the
C++ containers, as we further explore as we move forward. size_t is very
useful for indexing because it can index into the largest array you can
allocate.

Going back to our type mismatch – size_t will solve our problem. Let’s take
a look:
#include <iostream>
#include <array>
#include <cstddef> #A

int main()
{

std::array <int, 5> arr{ 23, 32, 12, 6, 34 };

for (std::size_t i{ 0 }; i < arr.size(); ++i)


std::cout << arr[i] << ' ';

std::cout << '\n';

return 0;

When you learned about C-style arrays, you saw that it’s easy to perform
indexing using the operator []. When working with std::array, and as we
can leverage from using the size() function, a safe index type is a type
returned by size(). Obviously, if we declare an array of 6000 int values,
indexed from 0 through 5999, The compiler will treat the 6000 literal as an
int, so there are no real surprises.

But what if the literal was 0x6000 – it won't be so straightforward, as a 16-bit


int cannot represent 24576, which is the decimal equivalent of 0x6000. What
can we do? Can we use long? Well, in potential, yes, but if the literal is
expressed in hexadecimal, we actually need to do better than that and use
unsigned int instead.

In other words, when working with std::array, it is important to choose an


appropriate type to represent the size of the array. If the array size exceeds
the maximum value that can be represented by a 16-bit int, such as when
using the hexadecimal literal 0x6000, it is necessary to use a larger unsigned
integer type to ensure safe and well-defined behavior, which is why size_t is
used.

Note

size_t is not just a part of the std::array container, but a part of all other
containers in C++, such as vector, list, map, and more – basically, any
container that supports size().

Tip

using size_t instead of just any regular integer or unsigned integer, ensures
that the code will work correctly on all platforms and with all sizes of
containers. Also, it makes the code more self-documenting, since it’s clear
that the variable is being used to represent a size or index, rather than an
arbitrary integer value.

Size up: indexing using size_type

When we want to know the size of a container, we can also use


std::array::size_type, which is a “nested” type alias, defined by the
std::array container alone, so it's used specifically for representing the size
of std::array objects. You can say that size_type exists only within the
“namespace” of std::array in this case. std::size_type and std::size_t
share the same underlying type, but in C++, size_type is not a global type
such as int or std::size_t. Instead, it is defined inside the std::array
definition as a nested type. Therefore, when using size_type, it must be
prefixed with the full array type, with std::array acting as a namespace in
this regard, as you can see from the following code:
#include <iostream>
#include <array>
int main()
{
std::array <int, 5> arr{ 23, 32, 12, 6, 34 };
for (std::array<int, 5>::size_type i{ 0 }; i < arr.size(); ++i)
{
std::cout << arr[i] << ' ';
}
std::cout << '\n';
return 0;
}

It’s a bit of an eye sore and looks quirky, which is why many prefer to use
size_t instead.

Remember

size_t is defined as the type used for the size of an object and is platform
dependent, while array::size_type is the type that is used for the number of
elements in the std::array container and is container dependent.

Using to_array

One of the newest additions to C++ is the ability to convert C-style arrays, or
any “array-like” object into a std::array easily, using a new function named
to_array().

In other words, std::to_array() is a useful tool that can help you create a
std::array from an existing array, and you can use it to convert an array
with a fixed size into a std::array, which, as you now know, provides many
additional features, such as bounds-checking and other methods.

Let's say, for example, that we have an array with a fixed size, say int
cArray[5] = {1, 2, 3, 4, 5}; - we can use std::to_array(cArray) to
create a std::array<int, 5> object (let’s call the new array object
stdArray). This new object will have all the features of a std::array, which
can be very useful in many contexts.

Let’s look at how it works in code:


#include <iostream>
#include <array>

int main() {
int cArray[5] = { 1, 2, 3, 4, 5 };
std::array<int, 5> stdArray = std::to_array(cArray);#A

std::cout << "Elements in stdArray: ";


for (auto i : stdArray) {
std::cout << i << " ";
}
std::cout << std::endl;

return 0;
}

As you can see, in our code we used the following statement:


Elements in stdArray: 1 2 3 4 5

Now let’s take a closer look at the syntax: We use the auto keyword here to
let the compiler determine its type based on the value we assign to it, and it
makes the code more concise and allows better readability since we don't
have to write out the entire type ourselves.

You can also see that we use the assignment operator =, to assign the result of
the std::to_array() function to the variable stdArray, indicating in
brackets ()the name of the C-style array that we're passing as an argument to
the std::to_array() function – in this case it’s cArray.

Note

std::to_array copies all elements from a C-style array to a std::array,


which makes the operation a bit expensive resource-wise. It means that in
case we need to create an array more than a few times (via loop for example),
std::to_array should probably be avoided.

5.2.9 Additional std::array methods


The std::array container holds a few handy and useful methods we can use
with our array. In the next section, we will briefly explore two of them:
fill() and swap().

I fill() good – using the fill() function


The fill() function, which is part of the std::array container, is a useful
method with we want to assign the same value to all the elements in an array
(or a container), or, if we initialized an array initializing an array, and we
want to reset all its values to a specific value.

Let’s say we have an array of int that we want to initialize with a value of 0 -
we can use the fill() function to set all the elements of the array to 0 as
follows (listing x):

Listing 5.8 Using the fill() function with std::array

#include <iostream>
#include <array>

int main()
{
std::array<int, 5> arr{};#A

arr.fill(0);#B

for (auto i : arr)


{
std::cout << i << " ";
}
std::cout << std::endl;

return 0;
}

Once we run this code the output will be “0 0 0 0 0”.

King of the swap() – using the swap() function

Whenever we need to exchange the contents of two std::array objects


efficiently without having to iterate through the elements of each array and
copy them one by one, the swap() function comes to the rescue. swap()is a
very efficient function, since, instead of copying the contents of one array to
another, which can be time-consuming for large arrays, it simply exchanges
the pointers to the array data – we know you haven’t learned what a pointer is
yet (you will in chapter 9), but, in a nutshell, a pointer is a variable that stores
to the memory address of another variable. Since the actual memory is used,
there is no need to copy the content of the array. This means that swap() can
be used to quickly and easily exchange the contents of two large arrays
without using much memory or CPU time.

Another benefit of using swap() is that it can be used to create a copy of an


array – all we need to do is to create an empty array of the same size as the
original array, and then call swap() to exchange the contents of the two
arrays, which will copy the contents of the original array into the new array.

The syntax of swap() is pretty simple, as you can see from Figure 5.26:

Figure 5.26 The swap() function can be called on the array object that you want to perform the
swap on, followed by the other array object that you want to swap with

In the next code, we initialize two arrays arr1 and arr2, and swap the
elements between them.

Listing 5.9 Swapping the elements of two arrays using swap()

#include <iostream>
#include <array>

int main()
{
std::array<int, 5> arr1{ 1, 2, 3, 4, 5 };
std::array<int, 5> arr2{ 6, 7, 8, 9, 10 };

std::cout << "Before swap: "<<std::endl;


std::cout << "arr1: ";
for (auto i : arr1)
{
std::cout << i << " ";
}
std::cout << std::endl;

std::cout << "arr2: ";


for (auto i : arr2)
{
std::cout << i << " ";
}
std::cout << std::endl;

arr1.swap(arr2);#A

std::cout << "After swap: "<<std::endl;


std::cout << "arr1: ";
for (auto i : arr1)
{
std::cout << i << " ";
}
std::cout << std::endl;

std::cout << "arr2: ";


for (auto i : arr2)
{
std::cout << i << " ";
}
std::cout << std::endl;

return 0;
}

Once we run this code, we should expect the following output:


Remember

It is good to use swap() when you need to exchange the contents of two
arrays or create a copy of an array in an efficient manner. It is especially
useful when dealing with large arrays or when performance is critical.

Note

As we continue with this book, you'll learn that there are several more
methods that we can use with std::array, many of which are also available
for other containers. We'll be introducing these methods gradually, and we'll
always make a point of highlighting the ones that work specifically with
std::array.

Note

std::array is generally the better choice over C-style arrays because it is safer,
more readable, and easier to use. However, there may be certain situations
where C-style arrays are preferred, such as when interoperating with legacy
code or when working with very large arrays that need to be allocated on the
heap. We will not dive too deep into the use of std::array in this chapter, as,
in most cases, we can use C-style arrays, or, if we need a more robust
solution to work with a collection of elements, we can use other containers,
such as vector, which is introduced in the next section.

5.2.10 Working with multidimensional std::array


Earlier in this chapter we talked at length about multidimensional arrays, and
we demonstrated how to implement them using C-style arrays. Working with
multidimensional std::arrays is the same, but the syntax when declaring and
initializing a multidimensional std::array is naturally slightly different.
Earlier, we initialized a multidimensional array for a movie rating program.
We used the following statement:
int rate[5][5] =
{
{3, 3, 2, 1, 4},
{2, 1, 2, 5, 3},
{1, 1, 2, 3, 4},
{4, 5, 5, 1, 4},
{-1, -1, -1, -1, -1}
};

With std::array the statement will be:


std::array<std::array<int, 5>, 5> rate =
{{
{3, 3, 2, 1, 4},
{2, 1, 2, 5, 3},
{1, 1, 2, 3, 4},
{4, 5, 5, 1, 4},
{-1,-1,-1,-1,-1}
}};

As you can see, the outer array has a size of 4, since there are 4 rows in the
original 2D array. The inner array has a size of 5 since there are 5 columns in
the original 2D array. Also, you probably noticed we use double curly braces
– the outer braces are used to initialize the array (list initializer), and the inner
braces are used to initialize the individual arrays that make up the rows.

It’s important to point out, that when it comes to multidimensional arrays,


std::array also provides several benefits over C-style arrays, such as
bounds checking, size information, and support for other modern C++
features, which are part of the std::array container, such as iterators (which
you will learn about soon), range-based for loops, and algorithms from the
<algorithm> header file, which you will also learn more about soon.

5.3 Final exercise – write a rock-paper-scissors


game
In the final exercise of this chapter, we are going to write a game of rock-
paper-scissors. In our code, we will be using an array of predefined choices
and a predetermined sequence of computer choices to simulate randomness.
The game is played in a continuous loop until the user decides to stop, and in
each round, the user is prompted to enter their choice of rock, paper, or
scissors. The computer's choice is determined by the next item in a
predetermined sequence of choices, and after each round, the program
displays the current score and asks the user if they want to play again.

let’s look at the steps to write this code and build our program.

1. First, we declare and initialize std::array<int, 3> choices = { 0,


1, 2 }; This line creates an array called choices that contains three
integers - 0, 1, and 2. These numbers represent the choices for the game
- rock, paper, and scissors, respectively.
2. Second, we need to handle the computer’s choice, so we declare and
initialize another array called computer_choices that contains 11
integers representing the computer's choices for each round of the game.
std::array<int, 11> computer_choices = { 1, 2, 1, 0, 0, 2, 1, 0, 2, 1, 1 };

3. Next, we need to create two variables to keep track of the number of


wins for the user and the computer:
int user_wins = 0; and int computer_wins = 0;

4. The next step is to create a boolean variable called play_again and sets
it to true. This variable is used to control an outer while loop that
allows the user to play the game multiple times:
bool play_again = true;

5. Our loop while (play_again) is the outer while loop that controls
whether the user wants to play again or not. It continues to loop as long
as the play_again variable is true.
6. The next step is to create an integer variable called
computer_choice_index and sets it to 0. This variable is used to keep
track of which choice the computer should make from the
computer_choices array:

int computer_choice_index = 0;

7. Now we print to the console:


std::cout << "Let's play a game of rock-paper-scissors!" << std::endl;
8. From this point, we move to an inner while loop, that continues to loop
until the game ends. It is used to prompt the user for their choice and
compare it with the computer's choice:
while (true)

9. We ask the user to enter his/her choice:


std::cout << "Enter your choice (0 for rock, 1 for paper, or 2 for scissors)

10. And we handle the user’s input:


int user_choice;
std::cin >> user_choice;

11. These two lines create an integer variable called user_choice and read
the user's input from the console.
12. Now, we need to handle the computer's choice for the current round
from the computer_choices array based on the value of
computer_choice_index.

int computer_choice = computer_choices[computer_choice_index];

13. The program will now outputs messages to the console displaying both
the user's and computer's choices using conditional statements that
check the value of user_choice and computer_choice. For example, if
(user_choice == 0) std::cout << "rock"; outputs "rock" if the
user's choice is 0.
14. Next, the program determines the winner of the game using conditional
statements that check both the user's and the computer's choices. If there
is a winner, the loop is broken using break; and the number of wins for
the user or computer is incremented using user_wins++ or
computer_wins++. If there is a tie, the loop continues.
15. Finally, the program asks the user if they want to play again using
std::cout << "Do you want to play again? (y/n): "; and reads in
the user's choice using std::cin >> play_again_choice;. The value of
play_again is updated based on the user's choice using play_again =
(play_again_choice == 'y' || play_again_choice == 'Y');.
16. The program repeats the loop if play_again is true, and ends when
play_again is false.

Now let’s look at our code:

Listing 5.10 Rock-paper-scissors game

#include <iostream>
#include <array>

int main()
{
std::array<int, 3> choices = { 0, 1, 2 }; #A
std::array<int, 11> computer_choices = { 1, 2, 1, 0, 0, 2, 1, 0, 2, 1, 1
int user_wins = 0; #C
int computer_wins = 0; #D
bool play_again = true; #E
while (play_again) #F
{
int computer_choice_index = 0; #G
std::cout << "Let's play a game of rock-paper-scissors!" << std::end
while (true) #H
{
std::cout << "Enter your choice (0 for rock, 1 for paper, or 2 f
int user_choice;
std::cin >> user_choice; #I
int computer_choice = computer_choices[computer_choice_index];
std::cout << "You chose ";
if (user_choice == 0)
{
std::cout << "rock";
}
else if (user_choice == 1)
{
std::cout << "paper";
}
else if (user_choice == 2)
{
std::cout << "scissors";
}
else
{
std::cout << "Invalid choice! Choose again." << std::endl;
continue;
}
std::cout << ", and the computer chose ";
if (computer_choice == 0) #K
{
std::cout << "rock";
}
else if (computer_choice == 1)
{
std::cout << "paper";
}
else if (computer_choice == 2)
{
std::cout << "scissors";
}
std::cout << std::endl;
if (user_choice == 0)
{
if (computer_choice == 1)
{
std::cout << "Paper covers rock. You lose!" << std::endl
computer_wins++;
break;
}
else if (computer_choice == 2)
{
std::cout << "Rock smashes scissors. You win!" << std::e
user_wins++;
break;
}
else
{
std::cout << "It's a tie! Choose again." << std::endl;

}
}
else if (user_choice == 1) #L
{
if (computer_choice == 2)
{
std::cout << "Scissors cut paper. You lose!" << std::end
computer_wins++;
break;
}
else if (computer_choice == 0)
{
std::cout << "Paper covers rock. You win!" << std::endl;
user_wins++;
break;
}
else
{
std::cout << "It's a tie! Choose again." << std::endl;
}
}
else if (user_choice == 2)
{
if (computer_choice == 0)
{
std::cout << "Rock smashes scissors. You lose!" << std::
computer_wins++;
break;
}
else if (computer_choice == 1)
{
std::cout << "Scissors cut paper. You win!" << std::endl
user_wins++;
break;
}
else
{
std::cout << "It's a tie! Choose again." << std::endl;
}
}
computer_choice_index = (computer_choice_index + 1) % computer_c
}
std::cout << "You have won " << user_wins << " times, and the comput
std::cout << "Do you want to play again? (y/n): ";
char play_again_choice;
std::cin >> play_again_choice;
play_again = (play_again_choice == 'y' || play_again_choice == 'Y');
}
return 0;
}

Once we run our code, here is a possible output (figure 5.27):

Figure 5.27 The output of our “rock-paper-scissors” game. Our program mimics random choices
made by the computer by using an array of choices.
Note

in real life, we would generate random numbers using the <random> header
file, which is part of the C++ Standard Library. We teach you how to
generate random numbers later in this book, and meanwhile, we just created
an array that mimics randomness but is not really random.
5.4 Summary
Arrays are a list or collection of elements such as variables. Arrays have a
fixed range of elements, and all elements in an array must be of the same data
type.

Declaring and initializing arrays involves the use of square braces. Each
element in the array has a unique position within an index, and the index
count always starts with 0. Accessing the array's elements is done via the
index position of the element.
Multidimensional arrays involve two or more arrays that are visibly
related and stored in a tabular form and can group related arrays in a
single block of code instead of multiple chunks.
std::array is a container that wraps C-style arrays and adds a layer of
additional functionality using various functions and methods to handle
arrays with less restriction and effort. std::array and C style array share
the same restriction of size limit once size was declared.
Declaring and initializing std::array is a bit different than C-style arrays:
we use the angle brackets, which contain both the data type and the
array's size. We also need to use std::array as part of our declaration.
Knowing the size of an array enables the use of range-based for-loops
with std::array, which provides a convenient and safer way to iterate
through the elements of the container without worrying about its size.
The std::array container has a function called size() that returns the
number of elements in the array. This function can be used to retrieve
the size of a std::array object, even if its size is not known at compile
time.
In C++, size_t is an unsigned integer type defined in the <cstddef>
header file, and its size is platform-dependent. It can represent the
maximum size of any object that can be stored in memory and is used
for indexing, including with C++ containers such as std::array.
size_t is useful for indexing because it can index into the largest array
that can be allocated.
std::to_array() is a useful and convenient tool that can help you
create a std::array from an existing array, and you can use it to
convert an array with a fixed size into a std::array.
The fill() function is used to assign the same value to all elements in
an array or container or to reset all its values to a specific value. For
example, if we want to initialize an array of int type with a value of 0,
we can use the fill() function to set all its elements to 0.
The swap() function allows for the efficient exchange of the contents of
two arrays without iterating through their elements one by one. Instead
of copying the contents of one array to another, which can be time-
consuming for large arrays, swap() simply exchanges the pointers to the
array data, making it a very efficient function.
In addition to providing bounds checking and size information,
std::array also supports other modern C++ features, such as iterators,
range-based for loops, and algorithms from the <algorithm> header file,
making it a superior choice over C-style multidimensional arrays.
[1]
Note that when we use the term “list”, we do not mean the list container in
C++, but the literal meaning of a list of items.
[2]
There is an exception when using char types with int based arrays, as chars
devolve into integers, as explained later in the book.
[3]Most programming languages use zero-based indexes. Lua programming
language, for example, uses 1 based index, meaning an index will always
start with 1 and not 0.
[4]In Chapter 10 you will learn about all the other container types and data
structures, as some of these concepts are too advanced at this stage.
6 Vectors – your arrays "on
steroids"
This chapter covers
Understanding the role and use of std::vector
Learning how to declare and initialize vectors
Understanding how to access vector elements
Understanding the difference between size and capacity
Using various methods to modify and manipulate vectors

Now that you have learned about arrays and how they are used to manage
collections of objects, it's time to introduce you to another fundamental data
structure in C++ - vectors. While arrays have a fixed size, vectors, which, like
std::array, are a type of container in C++, can dynamically change their
size during runtime, making them more flexible and convenient for many
programming tasks.

In this chapter, you will learn how to define and initialize a vector, how to
access its elements, and how to manipulate them using various methods
which are part of the std::vector container. We will go over useful methods
which are part of the std::vector container, such as methods to insert elements
to a vector, modify the vector, manage its size and capacity, and more.

We will also explore the difference between vectors and arrays, and when it's
appropriate to use one over the other. By the end of this chapter, you will
have a solid understanding of how to use vectors in your C++ programs and
the benefits they offer over arrays.

Understanding how to use vectors is a crucial step in becoming a proficient


programmer, and this chapter will provide you with ample opportunities for
hands-on practice and real-life comparisons to help solidify your
understanding.
6.1 To the vector go the spoils – introduction to
vectors
In the previous chapter, you learned about arrays, and specifically, that they
hold a fixed range of elements. When working with arrays, unless you are
always working with a fixed size of elements, the array’s size restriction
might turn into an obstacle in your code, preventing you from dynamically
growing your data whenever you need to. In addition, you learned that an
array's size restriction also opens the door for unwanted crashes, especially if
you don't know the final or actual size of your stored data. Sure, you can
always pick an array size much larger than your actual needs - but it’s never a
good idea to write an "open cheque" to your arrays.

Vectors to the rescue

C++ comes to our rescue with std::vector, which, just like std::arrays, is
a container type that handles lists of elements. Unlike arrays, vectors can
grow dynamically. However, std::array and std::vector have one more key
difference: with arrays, the size is determined at compile-time, while with
vectors, the size is determined at runtime. This means that vectors can be
resized, and elements can be added or removed as needed, making them more
flexible than arrays.

Tip

If in the previous chapter, we compared arrays to a school bus with a capacity


restriction of 40 children, we can compare vectors to a train: we have a
capacity of 40 children in a single cart, but we can add carts to the train to
make more room for more children. Each time we add a cart to the train, it
will have a capacity of 40, even if we only have 41 children, and it’s because
there’s a difference between the actual size of the vector, which is the number
of elements it holds, and the capacity of the vector, meaning the actual size
the vector can dynamically grow into – and we get to that shortly.

Vectors are, in fact, dynamic arrays, or as the heading of this section implies
"arrays on steroids" - they are more versatile, better, and can hold a whole lot
more of the load. Arrays, on the other hand, are a lower level of data structure
and thus, they are not dynamic as vectors.

Vectors can grow dynamically in size, so we can expand our vector when
needed. This ability certainly makes vectors extremely powerful in the
resource management of any program. Figure 6.1 illustrates the structure of a
vector containing 5 elements and the capacity of the vector, which is the size
of allocated memory.

Figure 6.1 the difference between the actual size and the capacity, which is the size the vector can
expand to once it grows.

The size of a vector refers to the number of elements currently stored in the
vector (and we can check the size using the size() function you learned
about in the previous chapter). The capacity of a vector, on the other hand,
refers to the amount of memory currently allocated to the vector to store
elements. The capacity of a vector is usually greater than or equal to its size.
When this capacity is exhausted and more is needed, it is automatically
expanded by the container (reallocating its storage space) – and we go back
and further explore this concept shortly.

Pre-checking the capacity of a vector is always good

C++ allows us to check the capacity of a vector using a function named


capacity(). This function returns the number of elements that a vector can
store without allocating more memory.

When a vector is created, it is assigned a default capacity. If the number of


elements to be added to the vector is greater than the capacity, the vector
reallocates memory to increase its capacity, which can be an expensive
operation. Therefore, it is useful to know the capacity of a vector to avoid
unnecessary reallocations and improve the performance of our code. For
example:
#include <iostream>
#include <vector>
int main()
{
std::vector<int> myVector(10);
std::cout << "The initial capacity of the vector is: " << myVector.capac
// Add more elements to the vector
for (int i = 0; i < 20; i++)
{
myVector.push_back(i);
}
std::cout << "The new capacity of the vector is: " << myVector.capacity(
return 0;
}

Note that we can also use a function named reserve()to set the capacity of a
vector in advance, which can be helpful when we know how many elements
we will need to store in the vector. By setting the capacity in advance, we can
reduce the number of reallocations and improve the efficiency of our code -
we explore this function later in this chapter.

Important!

Just like with arrays, all the elements in a vector must be of the same type,
and they can also be accessed individually using the same index rules we use
with arrays.

Note

C++ offers a variety of methods and useful functions that work well with
vectors. In this chapter, we cover only a few, and in the next chapters a few
more. However, we cannot teach all of the methods and you are encouraged
to continue learning about them as you advance your C++ skills.

6.1.1 Using vectors in your code


Now that you understand what vectors are and how they can benefit your
code, it's time to learn how to use them in code. The first important thing to
remember is that just like std::arrays, vectors use a special header file
<vector> which contains all the methods we need to work with vectors.
Using the <vector> header file, allows you to use all the pre-written
functions and possibilities vectors offer - less headache, more simple clean
code.

Declaring and initializing vectors

Just like you did with variables and arrays, vector elements must also be
declared and initialized before we can use them in our code. Let's look at the
structure and syntax for declaring vectors as illustrated in Figure 6.2:

Figure 6.2 Declaring and initializing vectors have the same underlying principle you know, such
as arrays and std::arrays.

As you can see, first we use the C++ keyword vector. Second, we declare
the data type which will be used in our vector. The declaration will take place
within the angle brackets. Then we provide our vector with a name of our
choosing.

Now that we declared our vector, we have the option to move on and
initialize it. We initialize the vector the same way we initialized an array – we
use curlies and place a comma to separate each object from one another, as
illustrated in Figure 6.3.

Figure 6.3 We initialize a vector by using curlies and place a comma to separate each object from
one another – just like we do with arrays.

Below you can see what declared and initialized vectors can look like in a
code.
vector<int> score {30, 22, 3, 15, 64, 2};
vector<char> letter {'e', 'f', 't', 'd', 'b', 'l'};

Once we initialize a vector, the compiler automatically allocates the


necessary memory space for the elements we initialized. When a new element
is added, and when needed, a new space in memory is allocated (more
capacity) without any bother.

Let’s look at a basic code sample you should feel comfortable with vectors.
In this code, we declare and initialize a vector of integers to represent some
dates in March. The user needs to enter a date, and if the date entered
matches one of the scheduled dates, the program prints a message telling the
user what’s on the schedule.
Listing 6.1 Using vectors: what’s on your schedule?

#include <iostream>
#include <vector> #A

int main()
{
std::vector<int> march_dates{ 5, 15, 17, 23, 29 }; #B
int date;

std::cout << "Enter a day in March (i.e. 23): ";


std::cin >> date;

switch (date) #C
{
case 5:
std::cout << "It's Friday! Time to party!" << std::endl;
break;
case 15:
std::cout << "It's the Ides of March. Beware!" << std::endl;
break;
case 17:
std::cout << "It's St. Patrick's Day. Time to wear green!" << std::e
break;
case 23:
std::cout << "It's National Puppy Day. Go play with a puppy!" << std
break;
case 29:
std::cout << "It's National Mom and Pop Business Owners Day. Support
break;
default:
std::cout << "Sorry, there is no event scheduled for that date." <<
break;
}

return 0;
}

Running this code and choosing 23 as input will result in the following
output:
6.1.2 Accessing and assigning vector elements
When you learned about arrays, you saw that whenever we want to access the
array's elements, we use a specific index position. Accessing vector elements
works the same way: we also use an index position. C++ offers several
methods for accessing elements based on their index, and we start with
exploring two: using the [] operator (the subscript operator), or using the
at() function - each method has its pros and cons, as we explain in the next
two sections.

Using [] to access elements


You already know how to use the subscript operator []as we used it with
arrays in the previous chapter. We can use [] with vectors as well whenever
we want to access its elements. Let's look at the syntax of both vectors and
arrays and see the similarities (figure 6.4):

Figure 6.4 Both arrays and vectors use the index position to access elements in the collection.

Let’s look at a very basic code sample in which we iterate through the
elements of a vector using the subscript operator [].
#include <iostream>
#include <vector>

int main()
{
std::vector<int> my_vector{ 6, 70, 13, 3 };
std::cout << my_vector[0] << std::endl;
std::cout << my_vector[1] << std::endl;
std::cout << my_vector[2] << std::endl;
std::cout << my_vector[3] << std::endl;
return 0;
}

As we run this code, we expect the following output:


Note

Obviously, using a loop would be better in this type of code, but we use this
method in this case, just for the sake of explaining.

Using at() to access elements

Another method to access elements of a vector is using the at() function.


at() is considered a bit safer than [] because it performs bounds checking to
ensure that the requested element exists in the vector. If we use [] it can lead
to undefined behavior if an index out of range is used to access an element.

Let's take a look at the syntax illustrated in Figure 6.5:

Figure 6.5 When we use the at() function we need to indicate the index position of the required
element.

As you can see from Figure 6.5, the at() function points to an element 'at' a
specific index location.
Good to know

The at() function is available in various container classes in C++, such as


std::vector, and std::array. In the previous chapter we mentioned that we can
use this method with std::array, but it cannot be used with C-style arrays,
which is not a container. In C-style arrays, you would need to use the square
bracket [] operator to access the elements.

Let's look at the same code we used previously, this time instead of [] we use
at().

#include <iostream>
#include <vector>

int main()
{
std::vector<int>count{ 1,2,3,4,5,6 }; #A

std::cout << count.at(0) << std::endl; #B


std::cout << count.at(1) << std::endl;
std::cout << count.at(2) << std::endl;
std::cout << count.at(3) << std::endl;
std::cout << count.at(4) << std::endl;
std::cout << count.at(5) << std::endl;
}

Below is the output we should expect:


Using at() and [] in your code – pros and cons

It is important to note that at() has a slower performance compared to other


methods of accessing elements in a container, such as using the array
subscript operator []. So, if performance is a concern and you are confident
that your code will not access out-of-bounds elements, you might want to
consider using [] instead of at().

Overall, whether or not to use at() in your code will depend on your specific
needs and constraints. It can be a good choice if you want to ensure bounds
checking and catch out-of-bounds errors, but you should consider the trade-
off in performance.

accessing elements using back() and front()

Just like the at() function, the front() and back() functions are part of
several C++ containers, including std::vector (and std::array as well). In
the case of vectors, back() returns a reference to the last element of the
vector, while front() returns a reference to the first element of the vector.
We are using the term “reference" again – we mentioned this term in the
previous chapter when we talked about our inline function. In our context,
"reference" means that back() and front() return the memory address of the
first and last elements of the vector, respectively, allowing the user to access
or modify these elements directly.

Note

returning a reference is different than returning a copy of the element, which


would create a new instance of the element and not modify the original vector
– and we further explain these concepts in chapter 8.

The syntax for using front()and back() is similar to the one we used with
the at() function, as illustrated in Figure 6.6.

Figure 6.6 The syntax of the front() and back() functions is simple: we just need to use the
vector’s name, the dot operator, and the desired function (back() or front()).
Let’s look at a simple code sample, using front() and back(). In this code,
we simply initialize a vector and access its first and last elements.
#include <iostream>
#include <vector>

int main()
{
std::vector<int> vec{ 1, 2, 3, 4, 5 };

std::cout << "First element: " << vec.front() << std::endl;


std::cout << "Last element: " << vec.back() << std::endl;

return 0;
}

The output of this code will be:


6.1.3 Using vector modifiers

The std::vector container includes a variety of functions, or modifiers, that


allow us to change or modify the size of the vector dynamically. These
functions are useful when we need to add or remove elements from the
vector, or when we need to resize the vector based on changing requirements.

At the beginning of this chapter, we mentioned that vectors can change in


size dynamically, and one of the most used two functions for doing so is the
push_back() and pop_back() methods (vector modifiers).

adding elements to your vector – using push_back()

When we want to add elements to our vector, we use push_back(), which


modifies the vector by adding elements to it. As the name implies,
push_back() "pushes" the element to the back of the vector so the last
element to be added will be last in the vector. It seems reasonable to think
that if the vector grows in size, while at the same time, memory capacity
grows in parallel – but that’s not exactly how this works. Let’s explain why:

As we mentioned earlier, size is not the same as capacity. When we talk


about a vector’s size, we mean the number of elements in the vector. When
we talk about capacity, we mean the number of elements for which there is
allocated memory. If size==capacity, a new capacity needs to be added.

Moving forward, when we push_back() an element, the new capacity needs


to be allocated, but it will be resource-consuming to add it each time we add
an element. Instead, capacity is calculated and determined according to an
incremental scheme (which is usually compiler dependent). In most cases, a
doubling method is used.

Let's say we have a vector with size and capacity of 5. After the first
push_back(), capacity will likely double and will be 10. After the second
push_back(), capacity will still be 10, but the size will have changed to 7.
Only once we reach a size of 10, the capacity will double again once we add
a new element (so it will be 20). The entire process of increasing the capacity
while doubling it (or using any other incrementing scheme) is much more
efficient, and it allows you to create faster and more resource-efficient
programs. Figure 6.7 illustrates this logic:

Remember

Though we illustrate push_back(), the logic applies to any method for adding
elements to a vector, which you will learn about in the next few sections.

Figure 6.7 There is a difference between size and capacity. When we add an element to a vector,
we change its size, but only when size is equal to the capacity, the capacity increments, according
to an increment scheme that is Compiler dependent (usually doubling in size). This method is
very resource efficient.
Let's say we have a vector with a single object named num. We use the
push_back() function to add another object – let's say we add the number 1.
The syntax we should use will be:
num.push_back(1).

Again, in this case, we use the dot operator which 'glues' the vector's name
with the push_back() function. If we want to add more objects, we do
exactly the same, as illustrated in Figure 6.8.

Figure 6.8 When using push_back() the new elements join the back of the vector and memory is
allocated when size == capacity. The new capacity is calculated and determined by an algorithm,
so a new section of memory is allocated, and the entire vector is moved.

Let’s look at a very simple code that adds an element to a vector of int named
'eval', containing a list of evaluation grades. We will use the push_back()
function to add two more grades to our vector (grades 78, and 66), and then
display the updated list of objects in the vector.

Listing 6.2 Using push_back to add elements to a vector

#include <iostream>
#include <vector>

int main()
{
std::vector<int> vec = { 1, 2, 3 };

std::cout << "Initial vector: ";


for (auto i : vec) #A
{
std::cout << i << " ";
}
std::cout << std::endl;

vec.push_back(4); #B
vec.push_back(5); #B
vec.push_back(6); #B

std::cout << "After push_back(4), push_back(5), push_back(6): ";


for (auto i : vec) #C
{
std::cout << i << " ";
}
std::cout << std::endl;

return 0;
}

Once you run this code this is the result you should expect:
Now we have three new elements to our vector. As you can see, the entire
process and implementation in our code is simple. It simply makes sense.

How to check the vector’s capacity? simple! just use capacity()

Another method that is part of the std::vector container is the capacity()


function, which allows us to check the size of the internal storage buffer of
our vector. What this function does is return the number of elements that can
be held in the currently allocated storage of the vector. capacity() can be
very useful as it allows us to check how much-unused memory is currently
allocated to the vector – and therefore, it can help us to optimize memory
usage in our program by avoiding unnecessary reallocations, ensuring that the
vector has enough capacity to hold all of the elements it needs to.

Remember

We already mentioned that the capacity of a vector is not necessarily equal to


its size (the number of elements currently held in the vector). Instead, the
capacity represents the amount of memory allocated for the vector, which can
be larger than its current size. When the number of elements in the vector
reaches the capacity, the vector will allocate a larger buffer and move its
existing elements into the new buffer.
The syntax for the capacity() function is not so different than what you've
learned so far, as illustrated in Figure 6.9.

Figure 6.9 The syntax of the capacity() function

In the next code sample, we create an empty vector v and print its initial
capacity using capacity(). We then add elements to the vector using
push_back(), which will cause the capacity to increase as needed. After each
element is added, we print the current size and capacity of the vector.

Listing 6.3 Using capacity() to check the capacity of the vector

#include <iostream>
#include <vector>

int main()
{
std::vector<int> v;

std::cout << "Initial capacity: " << v.capacity() << std::endl;

for (int i = 0; i < 10; ++i) #A


{
v.push_back(i);
std::cout << "Size: " << v.size() << " Capacity: " << v.capacity() <
}

if (v.capacity() > 20) #B


{
std::cout << "Capacity is greater than 20!" << std::endl;
}
else
{
std::cout << "Capacity is less than or equal to 20." << std::endl;
}

return 0;
}

Once we run the code, this is the output we should expect:


Note

Our output may differ on different machines or compilers, especially if they


have different default values for the initial capacity of a vector. However, the
overall behavior and logic of the code should remain the same.

be a pop-star: use pop_back() to delete elements from your vector

Just like the push_back() function adds elements, the pop_back() function is
another modifier that removes, or you might even say "pops" elements, from
the end of the vector. The last elements to be added ('pushed-back'), will be
removed first. Just like all the other functions we used so far, the pop_back()
function also co-resides with the dot operator which is 'glued' to the vector's
name. If we look at the vector we used when you learned about push_back(),
when removing the last element in this vector (the number 12), the syntax we
use will be:
num.pop_back().

Let's look at another code sample where we remove the elements from a
vector.
#include <iostream>
#include <vector>

int main()
{
std::vector<int> eval{ 90, 82, 70, 50, 64 };

eval.pop_back();#A

std::cout << eval[0] << std::endl;


std::cout << eval[1] << std::endl;
std::cout << eval[2] << std::endl;
std::cout << eval[3] << std::endl;
std::cout << eval[4] << std::endl;

return 0;
}
When we run this code we get an error:
What is the reason for this error? Since we removed the last element, we now
have 4 elements and not 5. The problem starts with the following code line:
cout << eval[4] << endl;

This line points to the element at index 4, which is the 5th element. However,
the element at index 4 no longer exists, it was removed ('popped-out'), hence
the error. If we remove this line the code should be executed without errors.

Anything good has a begin() and end()

You just learned that vectors come with the built-in ability to resize in
capacity when we insert an element. The question is: how can we control the
position of the element we are inserting? After all, by using push_back(),
we can only insert an element at the back of the vector. This doesn't sound
very flexible, does it?

To solve the issue, we can use ‘small little devils’ called iterators. Though
we dive deeper into the role of iterators in Chapter 11, at this point it’s
enough to know that iterators allow us to iterate through the vector, and
access elements within a specific index location.

Good to know

At the beginning of the previous chapter, we introduced you to the concept of


containers, and we explained that containers come with what we can refer to
as a "toolbox", or a set of methods, algorithms, and more, to help you handle
your data with ease. Iterators are part of this "toolbox", and they are an
integral part of the C++ containers. If you think about it, it makes a lot of
sense: after all, in most cases, if not all, we need to iterate through elements
and access them, so you can say that iterators are the bridge between the
container and the stored data.

There are several types of iterators in C++, but the ones we are interested in
at this point are the random access iterators, which point to the specific
address in memory of any specific element, and in this chapter, we focus on
two iterating functions: begin() and end().
The names of these functions are particularly clear, as they do exactly what
their name implies: they return the iterator at the beginning of the vector
when we use begin() and an iterator after the last element in the vector when
we use end().

Once we use begin() and end(), we can position or remove elements in


specific locations, but before we can demonstrate how to work with these
iterators in code, we need some additional components, which you will learn
about in the next few sections.

Add elements to your vector using insert()

Earlier, you learned all about the push_back() functions, which allows you to
add elements to the back of your vector. There is another method, and vector
modifier, for inserting elements into a vector: the insert()function. The
insert() function, as its literal name clearly implies, allows us to insert an
element before another element in a specified location. Since we must know
where the element should be inserted, we use begin() or end()to count the
vector's element from the beginning of the vector (using begin()), or from
the end (using end()). Let's see how the statement of both is structured as
illustrated in Figure 6.10.

Figure 6.10 When using the insert() function we need to define the beginning or end of the vector.
We do that by using the begin() or end() functions.
As you can see, the structure of the statement is based on everything you’ve
learned so far: We use the dot operator to "glue" the function we use to the
vector's name. In the case of Figure 6.10, the element '3' will be positioned at
the beginning of our vector.

Of course, we can also position elements at a specific position in the middle


of our vector. In this case, we need to count the number of elements before
the new element we want to insert (whenever we use end()), or after the
element (whenever we use begin()). Let's see what it will look like if we
wish to insert a new element (the number 3) into our vector and position it
after the 4th element from the beginning (figure 6.11).

Figure 6.11 We can also position elements at a specific position, and depending on if we use end()
or begin(), we count how many elements should be before or after the new element...
Let's practice some code. In this code, we have an output of a ‘knock knock’
joke. We use begin() and end() to access and modify elements of the jokes
vector. Specifically, we use begin() to get an iterator to the first element of
the vector, and end() to get an iterator to one past the last element of the
vector. We then use these iterators to access and modify the contents of the
vector using insert().

Listing 6.4 Using insert() and iterators to insert elements to a vector

#include <iostream>
#include <vector>

int main()
{
std::cout << "Knock, knock." << std::endl;
std::cout << "Who's there?" << std::endl;

std::vector<char> word = { 'B', 'e', 'e', 't', 's' };


std::vector<char> response = { 'B', 'e', 'e', 't', 's', ' ', 'w', 'h', '

for (auto it = word.begin(); it != word.end(); it++)#A


{
std::cout << *it;
}
std::cout << std::endl;

std::cout << "Beets who?" << std::endl;


for (auto it = response.begin(); it != response.end(); it++)
{
std::cout << *it;
}
std::cout << std::endl;

return 0;
}

Once we run this code, we can see the vector’s elements before and after
inserting a new element:
Important

Using insert() is an expensive operation, as it requires the copying and


moving of data to make room in the middle. It is therefore recommended to
use insert() when we wish to insert elements in the middle of a vector. If
we wish to insert elements to the back of the vector use push_back().

Using erase() to erase elements at a specific location

Just like the insert() function, which can insert elements at a specific
position with the help of our iterators 'friends' begin() and end(), we can use
the erase() function, which is also a modifier, to remove (erase) elements.
Using our friendly random-access iterators begin() and end(), allows us to
remove elements from specific positions, as we traverse through the vector
from the beginning or end. In fact, the erase() function allows us to remove
several elements from our vector - not just a single one. The image below
(figure 6.12) illustrates the syntax for erasing a single element, in this case,
we erase the 6th element in the vector.

Figure 6.12 Erasing an element work under the same concept as adding one – we need to define
the location from the beginning or end of the vector.

Let’s look at a code sample. In this code, we have a vector with 7 elements,
and we erase the 6th element.
#include<iostream>
#include<vector>

int main()
{
std::vector<int> grades{ 100,85,40,68,50, 90, 87 };

std::cout << "The original vector of grades is: " << std::endl;
for (auto iter = grades.begin(); iter < grades.end(); iter++)
{
std::cout << " " << *iter << std::endl;
}
grades.erase(grades.begin() + 5);#A

std::cout << "After erasing the 6th elements the vector of grades is: "
for (auto iter = grades.begin(); iter < grades.end(); iter++)
{
std::cout << " " << *iter << std::endl;
}
return 0;
}

Once we run this code, we get the following output (we highlighted the
element which was removed):
In case we wish to remove more than a single element from the vector, we
can use the following statement (figure 6.13):

Figure 6.13 We can erase more than a single element from a vector. All we need to do is to define
the number and position of elements we wish to remove.

Let's look at a code sample using erase(). In this code, we have a vector
named my_vector with 4 elements. We display the vector and then use the
erase() function to erase the first element. We then display the vector again,
and this time, we can see that the first element was erased.
#include <iostream>
#include <vector>

int main()
{
std::vector<int>my_vector{ 6,70,13,3 };#A
std::cout << "Before using erase() the first element is: " <<
my_vector[0] << std::endl;
if (my_vector.size() > 0)#B
{
my_vector.erase(my_vector.begin()); #C
}
std::cout << "After using erase() the first element is: " << my_vector[0
return 0;
}

When we run this code, this is what you should expect:

Make it clear - Using clear() for wiping out all elements

C++ allows us to remove all the elements in a vector in a single, simple, and
clean line of code: the clear() function, which is also a vector modifier. The
clear() function, as its name implies, removes all the elements in our vector,
leaving the size of our vector 0.
The only line of code we need to use is:

Let's see how it all works within our code. In this case, we use the same code
as we did for the erase() function, but instead of erasing the first element we
will clear the entire vector. Our program will display the size of the vector
before and after clearing it.
#include <iostream>
#include <vector>

int main()
{
std::vector<int>my_vector{ 6,70,13,3 };
std::cout << "vector size before calling 'clear' " << my_vector.size() <
my_vector.clear(); #A
std::cout << "vector size after calling 'clear' " << my_vector.size() <<

Once we run this code, we should expect the following output:


Now that our vector is cleared, if we try to access one or more elements we
will get an error, therefore, we cannot print to the console any of the vector’s
elements, as they do not exist anymore.

Note

There are a few more vector modifiers and random-access iterators used in
C++. We are not going to teach them all in this book. You can find a list of
all of the C++ modifiers and random-access iterators in Appendix I.

Got the crash? beware of empty vectors

One of the most common mistakes that new programmers make when
working with vectors is forgetting to consider the possibility of the vector
being empty. In this case, using modifiers such as erase(), clear(), or
pop_back() can result in a crash. For example, let's consider the code from
the previous section, but with an empty vector and using pop_back(). If the
vector is empty, this code will cause a crash. It is important to always check
if a vector is empty before using these types of modifiers, or else, we might
get the following Runtime error (depending on the compiler):
Good to know

if you click on “Retry” it will lead you to the exact error as represented in the
<vector> file of the std library, as illustrated in Figure 6.14:

Figure 6.14 Clicking on "Retry" will lead you to the exact error as represented in the <vector>
file of the std library.
Note

Error and exceptions are OS-specific, in the case of Figure 6.14, it's
Windows-specific.

Therefore, it's always good practice to set a condition that checks whether the
vector is empty or not before we erase pop or clear the vector's elements.
There are two methods for doing so.

To ensure that your vector is not empty before attempting to modify it with
functions such as erase() or pop_back,() you can check its size using the
size() function. If the size is 0, the vector is empty, and attempting to
modify it will result in a crash. It is important to consider the possibility of an
empty vector when working with these functions, especially for new
programmers.
Tip

As you might recall, we introduced this function in the previous chapter,


when we introduced the std::array container and mentioned that C-style
arrays do not support the size() function. This is one of the reasons
std::array is a better choice in many cases over C-style arrays.

Below is a code sample demonstrating how to use the size() function, which
we then use as a condition, safeguarding us from removing elements from an
already empty vector. On a side note, by now you should feel comfortable
reading and understanding these types of codes involving vectors, and the
various functions we can use to manipulate and control them.

Listing 6.5 Using the size() function to check if the vector is empty or not

#include <iostream>
#include <vector>

int main()
{
std::vector<int>my_vector{ 6,70,13,3 };
std::cout << my_vector [0] << std::endl;
std::cout << my_vector [1] << std::endl;
std::cout << my_vector [2] << std::endl;
std::cout << my_vector [3] << std::endl;

my_vector.clear();
std::cout << "What is my_vector size ? " << std::endl;
std::cout << "After ‘clear’ my_vector size is " << (my_vector.size()) <<

return 0;
}

The output of this code will be that the size is 0.

The second method is checking if our vector is empty simply by using the
empty() function. All we need to know is to run this function which returns a
true (the vector is empty) or false (the vector is not empty) output. Let’s
check the same code, which should be very clear and easy to read and
understand by now.

Note
Just like with std::array, we can use size_t with std::vector to ensure
that the size of the container is represented with an appropriate unsigned
integer type that can handle the size of large containers, whenever needed.

Listing 6.6 Using empty() to check if the vector is empty or not

#include <iostream>
#include <vector>

int main()
{
std::vector<int>my_vector{ 6,70,13,3 };
std::cout << my_vector[0] << std::endl;
std::cout << my_vector[1] << std::endl;
std::cout << my_vector[2] << std::endl;
std::cout << my_vector[3] << std::endl;

my_vector.clear();

std::cout << "After using clear() - Is my vector empty? " <<


"false") << std::endl;

return 0;
}

In this case, the output will be “true” – the vector is empty:


Change your vector’s size dynamically using resize()

Sometimes we need to resize our vector to a different size than its current
size. We may want to increase its size to accommodate more elements or
decrease its size to free up memory or remove excess elements. This is where
the resize() function comes in handy.

The resize() function allows us to change the size of the vector dynamically
and can take a single argument that specifies the new size of the vector, or
two arguments that specify the new size and a default value for the new
elements added to the vector.

What’s important to remember is that if we increase the size of the vector, the
new elements are default-initialized, and if we decrease the size, the excess
elements are removed.

Important!

If we want to preserve the existing elements and only change the size of the
vector, we need to make sure that the new size is smaller than or equal to the
current size of the vector.

Let’s first look at the syntax of resize() using a single argument (figure
6.15)

Figure 6.15 This resizes the vector to have n elements. If n is greater than the current size of the
vector, new elements are default-constructed. If n is less than the current size of the vector, the
excess elements are destroyed.

Now let’s look at the syntax when we use two arguments (figure 6.16)
Figure 6.16 This resizes the vector to have n elements. If n is greater than the current size of the
vector, new elements are initialized to a value. If n is less than the current size of the vector, the
excess elements are destroyed.

Note

Using the second argument ‘value’ is optional, but if we use it, it must be of
the same type as the other elements of the vector.

In the following code, we create a vector of size 5 with initial values of 0.


Then it resizes the vector to a new size of 8 without specifying a default
value, which leaves the new elements initialized to 0. Next, it resizes the
vector to a new size of 3, which discards the last 5 elements. Finally, it
resizes the vector to a new size of 5 and sets the new elements to have a
default value of 5.

Listing 6.7 Using resize() to resize our vector

#include <iostream>
#include <vector>

int main()
{
std::vector<int> v = { 1, 2, 3, 4, 5 };

std::cout << "Before we resize, the vector contains:"; #A


for (const auto& element : v)
{
std::cout << " " << element;
}
std::cout << std::endl << "Current size: " << v.size() << std::endl;
std::cout << "Resizing the vector to a smaller size using a single argum
v.resize(3);#B

std::cout << "After resizing, the vector contains:";


for (const auto& element : v)
{
std::cout << " " << element;
}
std::cout << std::endl << "New size: " << v.size() << std::endl;

std::cout << "Resizing the vector to a larger size using two arguments..
v.resize(5, 10);#C

std::cout << "After resizing, the vector contains:";


for (const auto& element : v)
{
std::cout << " " << element;
}
std::cout << std::endl << "New size: " << v.size() << std::endl;

std::cout << "Resizing the vector to a larger size using a single argume
v.resize(8); #D

std::cout << "After resizing, the vector contains:";


for (const auto& element : v)
{
std::cout << " " << element;
}
std::cout << std::endl << "New size: " << v.size() << std::endl;

return 0;
}

Once we run this code, we should expect the following output:


As you can see, using resize() is pretty simple and can be handy whenever
we need to change the size of a vector. However, if we also need to assign
values to the elements of the vector at the same time as resizing it, then the
assign() function may be a better choice – and we explore the assign()
function later in this chapter.

Because you reserve it! using the reserve() function to pre-allocate


memory

As you very well know, a vector is a dynamic array that can grow or shrink
as needed, and whenever we add elements to a vector, the vector needs to
allocate memory to store those elements. In C++ we can tell the vector to pre-
allocate memory for a certain number of elements. How do we do that? Using
the reserve() function.

Pre-allocating memory using reserve() can be useful if you know the


approximate size of the vector in advance. By reserving memory, we can
avoid the overhead of multiple memory allocations and reallocations that can
occur when we add elements to the vector. This can make your program
faster and more efficient.

Let’s look at the syntax of the reserve function (figure 6.17):

Figure 6.17 The syntax of reserve() - n is the number of elements that the vector should be able to
hold without reallocating its storage.

Note
Keep in mind that reserve() only allocates memory for elements, it does not
actually create any elements in the vector. You still need to use push_back()
or other methods to add elements to the vector.

Let’s look at two statements:


std::vector<int> vec(10);
std::vector<int> vec;
vec.reserve(10);

Are both statements the same?

well, in the first statement, we have a vector with 10 elements, all initialized
to 0.

In the second statement, we have an empty vector, and using reserve() we


make room for 10 int-type elements. The vector will allocate enough memory
to hold at least 10 integers but the elements will not be initialized.

Tip

We can use capacity() to find out how many elements can fit in the currently
allocated storage of our vector, (or use size() to check how many elements are
currently contained by the vector).

In the following code sample, reserve() is used to pre-allocate space for 10


elements in the vector. This means that when elements are added to the vector
using push_back(), the vector does not need to re-allocate memory to
accommodate them. Instead, the pre-allocated space is used, which can
improve performance in cases where a vector is going to be used to store a
large number of elements.

Code listing 6.8 Using reserve() to pre-allocate memory

#include <iostream>
#include <vector>

int main()
{
std::vector<int> v;
std::cout << "Reserving space for 10 elements..." << std::endl;
v.reserve(10);#A

std::cout << "Adding 5 elements to the vector..." << std::endl;


for (int i = 0; i < 5; i++)
{
v.push_back(i);#B
}

std::cout << "Size: " << v.size() << std::endl;

std::cout << "Adding 5 more elements to the vector..." << std::endl;


for (int i = 5; i < 10; i++)
{
v.push_back(i);#C
}

std::cout << "Size: " << v.size() << std::endl;

return 0;
}

Once we run this code, the output should be:


As you can see, reserve() can improve performance when dealing with
large vectors. However, it does not actually change the size of the vector, it
only reserves memory for future elements. The actual size of the vector can
still be changed using resize() or other functions, such as push_back(),
insert(), and more.

Call a shrink – using the shrink_to_fit() function

When we create a vector, it can sometimes end up taking up more memory


than it actually needs to hold the elements we've added to it. This extra
memory can be wasted and isn't very efficient – and that's where
shrink_to_fit() comes in handy. shrink_to_fit()can release any extra
memory the vector is holding onto, and make sure it's only using as much
memory as it needs. All we need to do is call vec.shrink_to_fit() on our
vector, where vec is the name of the vector we want to shrink.

In the next code sample, we create a vector named vec that holds five
elements. We then resize it to have only three elements using the resize()
function. After that, we call shrink_to_fit() to shrink the capacity of the
vector to fit its contents.

Listing 6.9 Using the shrink_to_fit() function

#include <iostream>
#include <vector>

int main()
{
std::vector<int> vec{ 1, 2, 3, 4, 5 };

vec.resize(3);#A

std::cout << "Before shrink: Size = " << vec.size() << ", Capacity = " <

vec.shrink_to_fit();#B

std::cout << "After shrink: Size = " << vec.size() << ", Capacity = " <<

return 0;
}
The output of our little program should be:
Important!

Keep in mind that calling shrink_to_fit() can be somewhat expensive, so


we generally only want to call it if we're sure we won't be adding any more
elements to the vector.

A RAII of sunshine – how C++ handles deletion of objects internally

When you learned about vectors, you learned that we can resize them by
adding and removing elements. What you were probably unaware of, is that
new objects added to std::vector are, in most cases, allocated on the Heap.
The operations of adding and removing objects from a vector are done behind
the scenes, and as a programmer, you do not see it, or should even care how
it's done. But if you do think about it, you probably noticed that when we
remove elements from a vector, there are no memory leaks, meaning, the
memory space is deallocated automatically. This ability is part of C++, as
std::vector in C++ knows how to perfectly handle memory deletion
internally. This is a powerful concept that only exists in C++ and is called
RAII (Resource Acquisition is Initialization).

RAII means that resources are freed once they have finished their course. The
rule of thumb is that an object's lifetime is bound to the scope of the object.
Once the scope is concluded, there is no point in keeping the object "alive".
You can compare it to having several plates on a table in a restaurant - while
you eat, the plates should stay on the table, but once you are finished, you
expect the waiter to clear the empty plates. You do not expect the waiter to
clear the plates while you eat. Under the same concept, we should not keep
out-of-scope objects as part of our program by keeping the memory space
they used to occupy "forever".

6.1.4 Assign from heaven: Assigning values to vectors


When working with vectors in C++, there are various ways to assign values
to the elements of a vector. One common way is to use the index position of
an element and the assignment operator =. Another way is to use the
assign() function, which allows us to assign a sequence of values to a
vector, either by specifying the number of elements or by providing a range
of values. In this section, we will explore both methods and discuss their use
cases.

Assigning values using the index position

Just like you did when you learned about arrays, you can now assign new
values to the elements in the vector by using their index position. The basic
and simple code sample below demonstrates how we conduct the
assignments of new values.
#include <iostream>
#include <vector>

int main()
{
std::vector<int> player_score{ 30, 22, 3, 15, 64, 2 };
std::vector<char> first_letter{ 'e', 'f', 't', 'd', 'b', 'l' };

player_score[0] = 3;#A

first_letter[2] = 'p';#B

std::cout << player_score[0] << std::endl;


std::cout << first_letter[2] << std::endl;

return 0;
}

Below is the output we should expect when we run the code.


As you can see from this code, it's easy to access elements in a vector and
change them using their index position. Of course, we can use the at()
function as well in this case, and if we want to modify the first or last
elements only, we can use the front() and back() function respectively.
assigning values using the assign() function

In the previous chapter, we talked about the fill() function, which can be
used with std::array to assign the same value to all elements of an array -
setting all the elements in the vector to the same value. But unlike arrays, in
most cases, we need to assign different values to different elements in a
vector. To do that, we need to access each element individually and assign
the desired value using indexing or iterators. That's why we use assign(),
which can be used to assign values to a vector in several ways. We can use
assign() to initialize a vector with a certain number of elements, or to
replace the existing elements of a vector with a new set of elements.

For example, we can use it to assign a single value to all elements of the
vector (like fill()with std::array), or we can use it to assign a range of
values to the vector. We can also use the assign() function to copy the
contents of another vector to the current vector.

Note

When it comes to std::array, the size of the array is determined at compile


time, and is fixed at runtime, which means that we cannot change its size
dynamically. Therefore, std::array does not support assign() to change the
values of its elements, and, as you recall, if we want to change the elements
of an array, we can only use indexing.

Listing 6.10 Using assign() to assign value to a vector

#include <iostream>
#include <vector>

int main()
{
std::vector<int> vec(5);

std::cout << "Initial values of the vector: ";


for (size_t i = 0; i < vec.size(); ++i)
{
std::cout << vec[i] << " ";
}
std::cout << std::endl;
vec.assign({ 1, 2, 3, 4, 5 }); #A

std::cout << "New values of the vector: ";


for (size_t i = 0; i < vec.size(); ++i)
{
std::cout << vec[i] << " ";
}
std::cout << std::endl;

vec.resize(3); #B

std::cout << "Resized vector (size 3): ";


for (size_t i = 0; i < vec.size(); ++i)
{
std::cout << vec[i] << " ";
}
std::cout << std::endl;

vec.assign({ 6, 7, 8 }); #C

std::cout << "New values of the vector: ";


for (size_t i = 0; i < vec.size(); ++i)
{
std::cout << vec[i] << " ";
}
std::cout << std::endl;

return 0;
}

And here is our output:


Note

it's important to note that both resize() and assign() modify the size of the
vector, and we should be careful when using them to avoid unexpected
changes to the vector's contents. Also, keep in mind that these functions can
be used in combination with other vector functions like push_back(),
pop_back(), insert(), and erase() to create more complex vector
operations.

6.2 Final coding exercise


In the final code in this chapter, we are going to write a To-Do-List program,
where you can add a thing to do, view all things to do, and mark a task as
‘completed’. What makes this program special is that it allows you to save
the data for the next time you run the program. If you close the console and
run the code again, you will see the data from the previous execution was
saved. Magic? No. just a really nice code.

Our program provides a menu with five options: add an item, show the list,
mark an item as completed, save the list to a file, and exit the program. The
program can load any saved to-do list from a file and allows the user to add
new items to the list, mark items as completed, and save the updated list to a
file.

Our code will have two vectors: one to hold the names of the tasks, and
another to hold the completion status of each task. The program will open the
task file and read any existing tasks, adding them to the vectors. After that,
our program enters a loop that repeatedly asks the user for input and
continues based on the user's choice:

In the add task option, the program reads a task name from the user and
adds it to the task vector if the maximum number of tasks has not been
reached.
In the view task option, the program displays the list of tasks and their
completion status.
In the marking task as a completed option, the program prompts the user
for the number of tasks to mark as completed and updates the
completion status of that task.
In the same tasks-to-file option, the program saves the current list of
tasks to the file.
The program ends when the user chooses the exit option.

6.2.1 Intro and recap for some old and new concepts
Before we begin, there are some old and new concepts and components we
use in this code, which are important to remember and learn.

Using <fstream> header file

In Chapter 4, you already used the <fstream> header file, which is used
whenever we need to handle files. In this case, since we want the program to
remember the list the user creates, we need to write to a file and then, if the
program runs once more after we closed it, read from a file and upload the
data to our console.

using <limits> header file

We use the <limits> header file in our code, to handle certain situations
where we need to make sure the input we receive is in the expected format.
For example, if we read a number from the user’s input, and then want to
read some text, sometimes the text input can cause unexpected behavior
because there might be leftover input from when we read the number.

The <limits> header file helps us solve this problem by allowing us to clear
any leftover input before reading the next piece of information. That way, our
program can continue running smoothly without any surprises. In simple
terms, <limits> is a tool we use to help make sure our program's input works
as expected, even when dealing with different types of data.

Using getline()

Until now we used std::cin for input, but we did not mention it has a
restriction: it cannot handle multiple space-separated values. It means that if
the input is two or more values with space (for example, “Buy sugar”),
std::cin will only read the first value (“Buy”). This means we cannot use
std::cin if we use multiple words, it will only return the first word.
getline() solve this problem, and can read the input stream up to the end of
the line by default. We use getline() twice in our code. first, in the line:
while (file.getline(line, sizeof(line)) && taskCount < MAX_TASKS)

This line contains a loop that reads lines from a file, one at a time, until either
the end of the file is reached, or the maximum number of tasks have been
read. We need to use getline() because we want to read the entire input and
not just the first word.

second, we use getline() in the line:


std::cin.getline(taskNames[taskCount].data(), MAX_TASKS);

This line reads a line of text input from the user and stores it in an array.

Note

We explore the getline() function further in the next chapter, when we talk
about strings.

file.close()

In Chapter 4 you used file.close() to close the file, and we will use this
function in this case as well in two places in our code, as explained shortly.

file.is_open()

file.is_open() is a function used to indicate whether the file is open and


ready for reading.

using ifstream
std::ifstream is a library that is part of the <fstream> header file, is used to
open and read input from a file, and provides several methods to read data
from a file, (including getline()). We use it in our code for easily reading
files, in our case, the todo_list.txt file where we store our to-do list tasks.

using ofstream

std::ofstream, which is also part of the <fstream> header file, handles an


output stream of data to a file. We use it in our code for easily writing files, in
our case, the todo_list.txt file where we store our to-do list tasks.

Using std::cin.ignore

The std::cin.ignore function is part of the std::cin input handling tool. It


is designed to disregard (discard) characters within the input buffer. ignore()
accepts two details: the number of characters to disregard (in our case, the
maximum possible quantity) and a delimiter character (in our case ‘\n’
which stands for – new line).

Using std::cin.clear

cin.clear() is part of the std::cin input handling tool, which is responsible


for managing input streams. The purpose of the std::cin.clear() function
is to reset any error flags that may have been triggered due to issues in the
input operation. By calling this function, you ensure that the input stream is
ready for subsequent input operations without being affected by prior errors.

In essence, using std::cin.clear() helps to maintain a clean and error-free


input stream, allowing for smooth input operations throughout your program.

6.2.2 How to write this code

1. We start our code by defining some constants:


a. The constant FILENAME represents the name given to our file.
b. The constant MAX_TASKS represents the maximum number of tasks
that your to-do list can contain.
c. The constant COMPLETED_MARK represents the symbol that will be
used to mark a task as completed (each completed task will be
marked with an X).
const char FILENAME[] = "todo_list.txt";
const int MAX_TASKS = 100;
const char COMPLETED_MARK[] = "X";

2. Next, create two vectors to store the task names and completion statuses
of your to-do list:
std::vector<std::string> taskNames;
std::vector<bool> taskCompleted;

3. Next, we create a variable named taskCount to keep track of the number


of tasks on your to-do list. Initialize it to 0, since your list is currently
empty.
int taskCount = 0;

4. We print a welcome message to the user:


std::cout << "Welcome to your to-do list!" << std::endl << std::endl;

5. Next, we open the file with an input file stream using our FILENAME
constant:
std::ifstream file(FILENAME);

And we check if the file is open using:


if (file.is_open())

if it is open, we Read the file line by line using the getline method of the
file stream:
char line[MAX_TASKS + 2];
while (file.getline(line, sizeof(line)) && taskCount < MAX_TASKS)

6. Next, we need to parse each line, and our code parses it character by
character to separate the task name and completed status:
int len = 0;
bool bCompleted{ false };
std::array<char, MAX_TASKS> task;
while (len < MAX_TASKS - 1 && line[len] != '\n' && line[len] != '\0')
{
if (line[len] == COMPLETED_MARK[0])
{
bCompleted = true;
break;
}
else
{
task[len] = line[len];
}

len++;
}
task[len] = '\0';

This code block reads each line and builds our container from the data it
reads from our file. We need to perform 2 actions:

Add the tasks to our container. For that, we just read the data until
reaching a 'new line', which indicates that our line has ended, and
that marks the end of the current task. We then add it and move on
to the next one.
Mark completed tasks as such. We look for the X sign, which is
always placed at the very end of the line. When we find it, we add a
true to our status vector (and if we don’t find it, we add false).
We also remove the X from the task name.

7. Next, we need to store the task name and completed status. Our code
stores the task name and completed status in the taskNames and
taskCompleted vectors:

taskNames.push_back(task);
taskCompleted.push_back(bCompleted);
++taskCount;

8. Next, we close the file using the close method of the file stream:
file.close();
9. We define bool bShouldRun{ true }; as our flag to control the program’s
flow

Note

You might recall that in Chapter 4, we explained the role of such a flag
whenever we need to break from two loops.

10. The next step is to enter a while loop that will execute as long as
bShouldRun is true:

while (bShouldRun)

11. Now we write the while loop’s logic. First, we display a menu of options
for the user to select from:
std::cout << "Please select an option:" << std::endl;
std::cout << "1. Add an item" << std::endl;
std::cout << "2. Show me what I need to do" << std::endl;
std::cout << "3. Mark an item as completed" << std::endl;
std::cout << "4. Save data to a file" << std::endl;
std::cout << "5. Exit" << std::endl << std::endl;

next, we prompt the user to enter their choice of option and read in the
value:
int option;
std::cout << "Your choice: ";
std::cin >> option;
std::cout << "" << std::endl;

12. While we are still in the while loop, we use a switch statement to
execute the code corresponding to the selected option:
switch (option)
{
case 1:
// Code to add a new task
break;
case 2:
// Code to show the list of tasks
break;
case 3:
// Code to mark a task as completed
break;
case 4:
// Code to save the data to a file
break;
case 5:
// Code to exit the program
break;
default:
std::cout << "Invalid option" << std::endl;
}

Let’s look at our code per switch-case option:

Code for case 1 - Add an item to the to-do list:


case 1:
if (taskCount < MAX_TASKS)
{
std::cin.clear();
std::cin.ignore(std::numeric_limits<std::streamsize>::max(),
std::array<char, MAX_TASKS> task;
std::cout << "Enter a task name: ";
std::cin.getline(task.data(), MAX_TASKS);
taskNames.push_back(task);
taskCompleted.push_back(false);
taskCount++;
std::cout << "Task added." << std::endl << std::endl;

}
else
{
std::cout << "Your to-do list is full." << std::endl << std:
}
break;

Code for case 2 - Show me what I need to do:


case 2:
if (taskCount == 0)
{
std::cout << "Your to-do list is empty." << std::endl << std
}
else
{
std::cout << "Your to-do list:" << std::endl;
for (int i = 0; i < taskCount; i++)
{
std::cout << (i + 1) << ". ";
for (int j = 0; j < MAX_TASKS && taskNames[i][j] != '\0'
{
std::cout << taskNames[i][j];
}
std::cout << " [" << (taskCompleted[i] ? COMPLETED_MARK
}
std::cout << "" << std::endl;

}
break;

Code for case 3 - Mark an item as completed:


case 3:
if (taskCount == 0)
{
std::cout << "Your to-do list is empty." << std::endl << std
}
else
{
std::cout << "Enter the number of the task to mark as comple
int index;
std::cin >> index;
std::cin.ignore(std::numeric_limits<std::streamsize>::max(),
std::cout << "" << std::endl;
if (index > 0 && index <= taskCount)
{
taskCompleted[index - 1] = true;
std::cout << "Task marked as completed." << std::endl <<
}
else
{
std::cout << "Invalid task number." << std::endl << std:
}
}
break;

This portion of the code might appear complex and challenging to


comprehend at first glance. The purpose of this lengthy line is to ensure that
the buffer is cleared, effectively handling long input values.

Let’s go over this line:


std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');

This line of code comprises several elements:

std::cin.ignore(): As explained, this function is part of the std::cin


input handling tool. It is designed to disregard (discard) characters
within the input buffer.
ignore() accepts two details: the number of characters to disregard (in
our case, the maximum possible quantity) and a delimiter character (in
our case ‘\n’ which stands for – new line).
std::numeric_limits<std::streamsize>::max():std::numeric_limits
offers information about the features of number-related types.
In our case, it is utilized to acquire the largest possible value (max()) for
the std::streamsize type which is a type that signifies the size of a
sequence of characters in an input or output operation.
To keep things simple, by using this line of code, you will be setting the
largest possible value, making sure our program will disregard all
characters until the delimiter (new line) is found.
'\n': This character acts as a delimiter (as already explained).
The std::cin.ignore() function will cease to disregard characters once
it comes across this delimiter. Here, it's the newline character, which
signifies the conclusion of the input.

In other words, this is one of those cases when you can use this line of code
even if you don't fully understand it, as it serves as a 'helper function'. Our
little helper directs the program to disregard every character in the input
buffer until a newline character is detected, effectively emptying the buffer.
You can comment on that line and run the program. It will work well until
you enter several tasks and the buffer is filled with data, or until the task you
enter is too long, and so on. The purpose is to handle unexpected input, in
terms of size.

Note

We often use such ‘helpers’ in our code when we are dealing with user input,
(which is unknown and can be many things). So we need that code to prevent
any "leftover" characters in the buffer from causing problems in later input
operations.
Code for case 4 – save data to a file:
case 4:
{
std::ofstream file(FILENAME);
if (file.is_open())
{
for (int i = 0; i < taskCount; i++)
{
file << taskNames[i].data();
file << ' '; // Add a space before the taskCompleted sta
file << (taskCompleted[i] ? COMPLETED_MARK[0] : ' ');
file << std::endl;
}
file.close();
std::cout << "Data saved to a file." << std::endl;
}
}
break;

Code for case 5 – exit:


case 5:
std::cout << "Goodbye!" << std::endl;
bShouldRun = false;
break;
default:
std::cout << "Invalid option" << std::endl;

We know the code might seem ultra complex at first glance, but it really isn’t
– the logic is straight forwards - and that’s the beauty of it. Try to go over the
flow and code a few times it will make more sense. For the sake of clarity, we
added an additional explanation of the code in the next section. Let’s look at
the code and run it first.

Listing 6.11 Write a To-Do list program

#include <iostream>
#include <vector>
#include <array>
#include <fstream> #A

int main()
{
const char FILENAME[]{ "todo_list.txt" };#B
const int MAX_TASKS{ 100 };#C
const char COMPLETED_MARK{ 'X' };#D

std::vector<std::array<char, MAX_TASKS>> taskNames;#E


std::vector<bool> taskCompleted;#F

int taskCount{ 0 };#G

std::cout << "Welcome to your to-do list!" << std::endl << std::endl; #H
std::ifstream file(FILENAME);#I
if (file.is_open())#J
{
char line[MAX_TASKS + 2];#K
while (file.getline(line, sizeof(line)) && taskCount < MAX_TASKS)#L
{
int pos{ 0 };#M
bool bCompleted{ false };#N
std::array<char, MAX_TASKS> task; #O
while (pos < MAX_TASKS - 1 && line[pos] != '\n' && line[pos] !=
{
if (line[pos] == COMPLETED_MARK)#Q
{
bCompleted = true;
break;
}
else
{
task[pos] = line[pos];
}

pos++;
}
task[pos] = '\0';
taskNames.push_back(task);#R
taskCompleted.push_back(bCompleted);#S

++taskCount;
}
file.close();#T
std::cout << "Data loaded from file." << std::endl << std::endl;
}
else
{
std::cout << "No saved data found." << std::endl << std::endl;
}
bool bShouldRun{ true };;#U
while (bShouldRun)#V
{
std::cout << "Please select an option:" << std::endl;
std::cout << "1. Add an item" << std::endl;
std::cout << "2. Show me what I need to do" << std::endl;
std::cout << "3. Mark an item as completed" << std::endl;
std::cout << "4. Save data to a file" << std::endl;
std::cout << "5. Exit" << std::endl << std::endl;

int option;
std::cout << "Your choice: ";
std::cin >> option;
std::cout << "" << std::endl;

switch (option)#W
{
case 1:
if (taskCount < MAX_TASKS)
{
std::cin.clear();
std::cin.ignore(std::numeric_limits<std::streamsize>::max(),
std::array<char, MAX_TASKS> task;
std::cout << "Enter a task name: ";
std::cin.getline(task.data(), MAX_TASKS);
taskNames.push_back(task);
taskCompleted.push_back(false);
taskCount++;
std::cout << "Task added." << std::endl << std::endl;
}
else
{
std::cout << "Your to-do list is full." << std::endl << std:
}
break;

case 2:
if (taskCount == 0)
{
std::cout << "Your to-do list is empty." << std::endl << std
}
else
{
std::cout << "Your to-do list:" << std::endl;
for (size_t i = 0; i < taskCount; i++)
{
std::cout << (i + 1) << ". ";
for (size_t j = 0; j < MAX_TASKS && taskNames[i][j] != '
{
std::cout << taskNames[i][j];
}
std::cout << " [" << (taskCompleted[i] ? COMPLETED_MARK
}
std::cout << "" << std::endl;

}
break;

case 3:
if (taskCount == 0)
{
std::cout << "Your to-do list is empty." << std::endl << std
}
else
{
std::cout << "Enter the number of the task to mark as comple
int index;
std::cin >> index;
std::cin.ignore(std::numeric_limits<std::streamsize>::max(),
std::cout << "" << std::endl;
if (index > 0 && index <= taskCount)
{
taskCompleted[index - 1] = true;
std::cout << "Task marked as completed." << std::endl <<
}
else
{
std::cout << "Invalid task number." << std::endl << std:
}
}
break;
case 4:
{
std::ofstream file(FILENAME);
if (file.is_open())
{
for (size_t i = 0; i < taskCount; i++)
{
file << taskNames[i].data();
file << ' ';
file << (taskCompleted[i] ? COMPLETED_MARK : ' ');
file << std::endl;
}
file.close();
std::cout << "Data saved to a file." << std::endl;
}
}
break;

case 5:
std::cout << "Goodbye!" << std::endl;
bShouldRun = false;
break;
default:
std::cout << "Invalid option" << std::endl;
}
}

return 0;

Once you run the program, it will look like so:


6.3 Summary
Just like arrays, Vectors are also lists of elements, but unlike arrays,
vectors can grow dynamically.
Accessing vector elements is done using the same method as an array:
we use square brackets [] with the desired index position.
We can also use the at() function to access elements in a vector. This
function points to an element at a specific index location.
There are several methods for adding or removing elements from a
vector, depending on if we want to add/remove elements from the
beginning, end, or middle of the vector. Some of the methods are:
Using the push_back() function, we can add elements to the back
of the vector.
Using the pop_back() function, we can remove elements from the
end of the vector.
We use the begin() or end() iterators in order to count elements
from the beginning or end of the vector, so we can access, add or
remove elements from a specific index location.
We use the insert() function together with begin() or end() to
insert an element or elements in a specific position within our
vector.
We use the erase() function together with begin() or end() to
erase an element or elements in a specific position within our
vector.
We use the clear() function when we want to clear (remove) the
entire vector.
Whenever we want to check the size of the vector or make sure it's not
empty, we use the size() function, which returns the size of our vector.
If we want to check if the vector is empty, we use the empty() method,
which returns a true/false value.
The capacity() function allows us to check the size of the internal
storage buffer of our vector. What this function does is return the
number of elements that can be held in the currently allocated storage of
the vector.
The resize() function allows us to change the size of the vector
dynamically and can take a single argument that specifies the new size
of the vector, or two arguments that specify the new size and a default
value for the new elements added to the vector.
Using the reserve() function allows us to pre-allocate memory if we
know the approximate size of the vector in advance.
The shrink_to_fit() function can release any extra memory the vector
is holding onto, and make sure it's only using as much memory as it
needs
The assign() function can be used to initialize a vector with a certain
number of elements or to replace the existing elements of a vector with a
new set of elements
7 With strings attached - working
with string literals
This chapter covers
Understanding the concept of strings and how they differ from character
arrays
Declaring and initializing string literals, working with std::string
Using std::string methods to find, insert, replace, remove elements
from a string and more
Trimming and formatting strings
Using string comparison using three-way comparison and other methods
Using wstring multi-lingual input

In this chapter, you will expand your skillset by learning about strings, a
commonly used data type in C++ that represents a sequence of characters.
You will understand what strings are and when they are used (spoiler: in
almost any program). You will also learn about various std::string
methods for manipulating, searching, inserting, replacing, and removing
elements from strings. We will dive deep into string trimming, which is a
way to get rid of leading and trailing whitespace characters from a string. We
will also explore in greater detail how to use the three-way comparison
operator (also known as the spaceship operator) to compare strings, as you
learned about in Chapter 3.

Additionally, you will compare traditional C-style string methods with


modern methods for performing similar tasks and understand the pros and
cons of using each approach. The chapter will also introduce the new
std::format function, which allows you to format a string at runtime using
placeholders for variables or expressions. By the end of the chapter, you will
be proficient in using various string methods in your code for different
purposes.
7.1 Pulling strings – The importance of string
literals
In their simplest definition, strings are a group, or sequence of characters
such as “hello”, or “I enjoy learning C++”, and they play an important role
in almost any program. As a programmer, it’s fundamental to understand
what strings are along with the various methods C++ offers to handle them,
with some fantastic new methods added in C++20 (some of which we teach
in this chapter).

In everyday programming, it’s hard not to come by a program that utilizes


strings. We might even say that strings are one of the most used components
in C++. Once you master the use and implementation of strings in your code,
it will have a tremendous effect on your ability to write better programs with
improved coding skills.

In Chapter 2, you learned about the char data type, and in Chapter 5, you
learned about arrays. By now, you should be able to write a program that uses
an array of chars, which are, in fact, C-style strings, or, in other words, the
way strings were handled in the early days of computer programming. But as
you probably remember, an array of char comes with a major limitation: it’s
restricted in size. Once you declare an array’s size, you cannot change it.
Arrays are also more complicated when we want to support various
operations, such as switching elements, inserting elements, etc. - they require
some overhead. We like to compare C-style strings to typing text in an old
flip phone – yes, you can text, but typing requires more effort and “figure
muscle”.

This is where strings literals come into the picture: though Strings and arrays
of chars share some similarities, strings are much more powerful. Just like
vectors, or std::array, strings are a type of container, which means they
come with built-in functionalities, making it easier for you to write minimum
hustle code.

7.1.1 String Vs. an array of chars


As you learned, in C++, the char data type is used to hold alphanumeric data,
such as any letter, number, and special characters (e.g., 'a', '$', or '9'). A string,
on the other hand, is a data type that holds one or more characters (chars). Of
course, a string can also be empty and hold 0 characters. It can be thought of
as a one-dimensional array or a contiguous sequence of characters that ends
with a NULL character to identify the end of the string. strings also
automatically manage their own memory, so we don't need to worry about
managing memory allocation and deallocation ourselves.

It's important to note that each char is defined in the C programming


language as a signed 8-bit integer, which means that it has a size limitation.
This limitation is also present when using chars to create strings, as each
string is essentially a collection of chars – and we explain all about that in a
bit. Meanwhile, let’s look at Figure 7.1, which illustrates the structure of
strings and how they are handled from a memory point of view, and as you
can see, just like with arrays and vectors, each element is stored in a
sequential memory address.

Figure 7.1 Strings are handled in memory exactly like an array of chars (The null terminator is
discussed in the following section).
We wrote earlier that strings are the bare bones of an array of chars, as you
can see from Figure 7.1. exactly why this statement is true: you are looking at
an array of chars, but the difference is that strings come in a smooth, fluffy
form, with no size limitation like arrays, and with built-in use-with-ease
functionalities.

It's important to note that while arrays and strings are similar in that they both
hold a collection of characters, they have some key differences in C++. An
array is a collection of variables of the same data type, while a string is a
special data type that represents a sequence of characters. Also, as discussed
earlier, strings are essentially an array of chars with a built-in null terminator
and additional functionalities. This can be seen in Figure 7.1, where you can
see an array of chars that have been wrapped in a smooth, fluffy form with no
size limitation like arrays - this “fluffiness” derives from several built-in
functions that make them more convenient to work with than character
arrays. For example, we have functions for finding substrings, concatenating
strings, and converting strings to other data types – and you will learn all
about them in this chapter. In contrast, with arrays or chars, we would need to
write our own functions to perform these operations.
Riding a Unicode

The main difference between an array of chars and a string is not just a matter
of syntax or representation, but also in the way they are treated by the C++
language, especially when working with UNICODE. UNICODE is a
computing standard that aims to represent all characters, scripts, and symbols
from all writing systems in the world – even emojis and musical notes. Each
letter, sign, or symbol is assigned a unique number, which can be encoded
using different character encodings, such as UTF-8 or UTF-16. These
encodings use multiple bytes to represent a single character, allowing for a
wider range of characters to be represented in a string.

Naturally, due to the size limitation, we have with 8-bit chars, UNICODE
cannot be represented using an array of chars. With strings, this restriction
can be lifted, allowing for a broader range of characters to be represented in
strings.

Note

It's worth noting that old-style strings, using arrays of chars, are still fully
functional, but they may not be as convenient or easy to use as std::string.

7.1.2 The Brick and the Wall: compound Vs fundamental


Types

String objects are called compound types (also known as composite data
types), as opposed to fundamental types. In Chapter 2 we explained what
fundamental (AKA primitive) type is, and that it is a basic building block
construct C++, such as int, char, etc. String combines one or more elements
of the fundamental char type, such as length and other characteristics which
are required to facilitate some of the methods made available. All are
combined into a new type named string, which is, therefore, considered a
compound type. You can say that compound types are constructed from
fundamental types, and other composite data types, but not the other way
around.

Note
as you can guess, arrays are also considered a compound type in C++. C++
contains several more compound types, such as functions, pointers, and more,
which we teach in the next few chapters.

If this sounds a bit confusing, you can think about a fundamental type as a
brick: you can pile up several bricks on top of one another and they will
become a pile of bricks, but you can also pile bricks on top of each other, add
some cement and turn them into a wall – a wall has a different definition.
When you see a wall and you don’t think about individual bricks, you think
of a wall. Strings are the wall which is a compound of bricks (fundamental
type chars). You can use strings like you use other data types, you can create
an array of strings, or a vector of strings, without having to handle the char
type anymore.

the difference between strings and vectors

Under the hood, both strings and vectors in C++ are dynamic arrays. They
both manage their memory by allocating more space than is needed for the
current elements. When the pre-allocated space is filled up, they allocate a
larger block of memory, copy the existing elements to the new block, and
then deallocate the old block. This ensures that the elements in both strings
and vectors are stored contiguously in memory.

However, while vectors are used to store a collection of elements of any data
type, strings are used specifically to store a sequence of characters, and as
such, it uses methods that are specifically designed to handle chars, such as
concatenating, searching for substrings and more – as you will learn shortly.
Vectors, on the other hand, have methods that are specific to working with
collections of elements, such as sorting, searching, and iterating over
elements.

You can also say that the key difference is in the interface std::vector and
std::string provide: The string class in C++ provides a higher-level
interface that abstracts away many of the details of memory management
(fluffy – remember?). You can append to a string, or insert characters into it
without having to worry about its size or capacity – and we don’t have to use
methods such as push_back() which we need to use with vectors. Behind the
scenes, the string class automatically manages the memory and ensures that
there is always enough space for the characters.

In contrast, with vectors, you have more control over memory management.
You can use methods like reserve()to directly control the size and capacity
of the vector, which, as you learned, can be useful in situations where you
need finer control over the memory usage, but it also means that you need to
manage these aspects manually. However, this method does not exist with
strings.

The dog ate my null terminator

We already mentioned that in the early days of computer programming,


strings were typically handled using arrays of char type, often referred to as
C-style strings. This method is still widely used today, particularly in lower-
level programming contexts.

While C-style strings can be useful, they come with a set of challenges. One
such challenge is the necessity of a null terminator, signified by '\0'. If you
get the null terminator wrong, if you forget about it, or ignore it, the dog ate it
- you’re in for some trouble, which can lead to bugs and unexpected
behavior. This is commonly referred to as an "off-by-one" error – and we
mentioned it in the previous chapter, explaining that such errors occur when
programmers forget that C-style strings are terminated by a null character
'\0'. As a result, if the programmer tries to access the string up to its length,
they end up going one character beyond the end of the string, potentially
reading garbage data.

In chapter 5 we explained why it is crucial to indicate to the compiler where


an array ends. Failing to do so could result in the output of 'garbage' data. To
avoid this, a null character '\0' is placed at the end of the array, serving as the
null terminator. When the compiler (and later on, the runtime environment)
encounters the null terminator, it knows that it has reached the end of the
array.

Just like arrays of char, C-style strings must always terminate with a null
character. For example, if we have the C-style string "HOUSE", it will be
stored with a null character at the end, marking the termination of the string:
'H' 'O' 'U' 'S' 'E' '\0'

To have the null terminator added automatically, you can use double quotes
to define the string. For example: "HOUSE". Or in code:
char myStr[] = "HOUSE";

In this case, the null terminator is automatically appended at the end of the
string.

7.2 Declaring and initializing strings


As you may already know, working with certain functionalities often requires
us to include specific header files. When dealing with strings in C++, the
<string> header file includes several useful functions and classes. We will
be exploring some of these in this section, but bear in mind that we can't
cover all of them due to their sheer number. As you further your
programming journey, you'll inevitably encounter and familiarize yourself
with many more of these methods.

To use the functionalities provided in the <string> header, we need to include


it at the top of our program using #include <string>. Once that's done, we
can declare and assign values to our string objects, much like we did with
variables, arrays, and vectors in the previous chapters. By this point, this
process should be fairly familiar to you.

note

If you intend to use C-style strings, you'll need to include the <cstring>
header file. And regardless of the type of string you're working with, always
ensure to include the necessary header file — <string> for string objects and
<cstring> for C-style strings.

7.2.1 Declaring and initializing a string variable


Declaring and assigning value to strings is simple and very similar to what
you have learned so far. Let’s take a look at Figure 7.2.
Figure 7.2 The structure of string declaration and initialization. The string’s value is placed
between two quotation marks.

In Chapter 2, in the first code you wrote, we briefly mentioned the need to
use quotation marks which will wrap the strings we used when we asked the
user to rate the book. For example, one of the statements we used was:
"How would you rate this book so far? Please rate from 1-10"

We mentioned that since our C++ code is also written using letters and words
(keywords for example), the compiler needs to tell the difference, or denote,
between C++ core language statements, with strings of our own making. For
example, let’s say we have the statement
int num {5};

We then write a code with an output statement:


cout << “We need to use int num{5}”;

Had we not used quotation marks, from a compiler’s point of view, we would
have had another int num{5}; statement, and our program would not know
what to do with it. Let’s see what it would look like in our IDE (Figure 7.3
and Figure 7.4).

Figure 7.3 When we do not use quotation marks, the compiler cannot tell the difference between
a string statement and an actual code – and why should it?
As you can see from Figure 7.3, it is colored in blue, which means the
compiler thinks it’s an integer – which by all parameters it is.

Note

The color the IDE uses (if at all) varies from one IDE to another.

Figure 7.4 When adding quotation marks the compiler knows it is a string and does not need to
consider ‘int’ and an integer type, but just as a literal.
As you can see from the Figure 7.4when we add the quotation marks, there
are no errors, and the word int is no longer colored in blue, which means the
compiler no longer considers it to be an integer type code statement.

You can say that quotation marks are a safe space, or a confined zone, where
you can type anything, and from the compiler’s point of view, it’s like saying
“Hey, I see that you wrote something here, and I don’t care what the content
is”. The compiler will not consider it as a core C++ statement. We can write
an entire program with quotation marks, and the compiler will not consider it
as a program.

Let’s look at a very simple code sample, demonstrating an easy declaration


and assignment of a string. In this code we declare a string and call it
strMyName, leaving its value empty, then using the assignment operator to
assign a value to it, printing it to the console.
#include <iostream>
#include <string>

int main()
{
std::string strMyName{}; #A
strMyName = "my name is David";#B
std::cout << strMyName << std::endl;#C
}

Quote the quoter: using “ ” inside “ ”

Using quotation marks within quotation marks won’t end well in your code.
The reason is that when you want a given string to contain quotation marks,
the compiler needs to realize that the quotation marks don’t mark the end of
the string, but that they are, in fact, part of the string. To overcome the
problem, we use the escape sequence for quotation marks, which is a
backslash followed by a quotation mark \”. This indicates that quotation
marks are an integral part of the string. An example would be
“… and then he said: \”I don’t like this\””

The backslash before the quotation marks tells the compiler to treat the
quotation marks as literal characters rather than as the end of the string.

7.2.2 Sting input: Why using getline() is far better than ‘cin’

In the final exercise in the previous chapter, we briefly introduced the


getline() function in your final coding exercise (our To-Do list program).
We explained that sometimes values are assigned by the user of our program
during runtime as input, but using std::cin is limited, especially when it
comes to using longer string values. We explained that std::cin cannot
handle multiple space-separated values, and if we use it regardless of input of
multiple sentences, it will only return the first word of each sentence (as it
will interpret the first space as the end of the input). Let’s put it to the test,
and see how std::cin handles a sequence of words we input. Below is a very
simple program that receives an input and displays it as output using
std::cin.

#include<iostream>
#include <string>

int main() {
std::string test{ "" };
std::cout << "Please type any sentence" << std::endl;
std::cin >> test;
std::cout << "your input was: " << test << std::endl;
}

We run the program and write an input such as “This is a good day to learn
C++,” Our output will be (figure 7.5):

Figure 7.5 Our long input stream output will only display the first string.
As you can see, the input stream only displayed the first word “this”, and not
the rest of the sentence. Why does this happen? Well, when using std::cin,
the C++ compiler assumes that white space terminates the token. In other
words, cin reads only up to the first whitespace. If our user inputs five words
in one line, std::cin will read the first word, while the remaining four words
will wait in the input stream to be read. These four words will only be read
upon the next call of std::cin.

One more thing to consider is that the >> operator is oriented toward single
tokens or characters, rather than a sequence of inputs. As you can clearly see,
std::cin might place a huge restriction on our program, especially when we
use strings that, in many cases, span multiple lines or that contain white space
characters. Due to this lack of flexibility, you as a developer cannot rely on
std::cin for processing the user’s input.

This is where getline() comes into the picture: The getline() function
provides a better option for long text statements. Unlike the << operator,
getline() reads the input stream up to the end of the line by default.

Generally speaking, getline() is faster than cin>>, but also, spaces or tabs
are not considered line separators by the getline() function. getline() will
return the entire line, using ‘\n ‘ as the indication for the end of the line
(referred to as “delimiter”).

Let’s look at the syntax for using getline() as illustrated in figure 7.6.

Figure 7.6 The syntax of getline() is simple and requires the source (i.e. input stream, file, etc.)
and the string.
As you can see, everything in the structure of the syntax makes sense, so take
a minute to go over it, and make sure you understand the logic, as explained
in Figure 7.6.

Let’s run our small code again, this time using getline(), and see how it
handles the same input stream.

Listing 7.1 Code practice – Using getline for string input

#include<iostream>
#include <string>

int main()
{
std::string test{ "" };#A
std::cout << "Please type any sentence" << std::endl;
getline(std::cin, test);#B
std::cout << "your input was: " << test << std::endl;
}

When we run this code, the results are very different from our earlier cin
statements (figure 7.7):
Figure 7.7 When we use getline() the output will be the full length of the string input

Remember

getline() is generally better than cin when you need to read input that
spans multiple lines or that contains whitespace characters, because
getline() will read the entire line of input and std::cin will stop reading as
soon as it encounters a whitespace character.

7.3 Playing the strings – string boosting methods


Just like std::vector and std::array, the C++ std::string container
offers us various handy methods to work, handle and manipulate strings.
These methods include the ability to search inside strings, compare them,
replace them, format them, and do other various manipulations. For example,
we can scramble or shuffle characters as part of the protection measures we
use on our code (code string obfuscation[1] for example, which uses various
manipulations over the code to keep it safe from hackers who might try to
crack it).

Of course, there are many other types of manipulations and methods used
with strings and tons of use cases – some of which we explore in this chapter.

Note

In this chapter, we highlight some methods which are interesting and handy,
while, in some cases, we only explain just the basic principles of others.

Let’s go over some useful methods and functions we use with strings in C++.
In the next few sections, you will learn how to copy strings, concatenate
strings, search within strings, trim and tokenize strings, replace strings or
substrings, as well as using insert and remove methods and formatting. You
will also learn how to compare strings using the old way and the three-way
comparison way.

Remember

String methods are pre-built functions that are stored in the <string> header
file and std::string library. Using these pre-written functions can be pretty
easy, as most parts were already written for you. However, some methods are
a bit more complex, and in the next few sections, we try to explain some of
those complex methods in detail.

7.3.1 Copying strings value


Sometimes we need to copy the value of one string to another. For example,
if a user wants to change his username, the new string with the new username
will be copied instead of the old username. Copying string value is very easy
using std::string – all we need to do is to use the assignment operator =.
Let’s look at a simple code sample and see how copying is being
implemented.

Listing 7.2 Code practice – Copy strings

#include <iostream>
#include <string>

int main()
{
std::string User_Name{ "default" }, New_User_Name{}; #A

std::cout << "Please enter a new user name" << std::endl;


std::cin >> New_User_Name;
User_Name = New_User_Name; #B
std::cout << "your new user name is " << User_Name << std::endl;

return 0;

Good to know

When dealing with C-style strings, which are basically arrays of chars, we
can use the strcpy() function. Using strcpy() was similar to using the
assignment operator we use with std::string. It’s easy to remember the name
of the strcpy() function, as it is a combination of the words string (str) and
copy (cpy).

7.3.2 Concatenating strings


In many cases, we need to combine two or more strings together, and we call
this operation concatenating strings. For example, let’s say we have the
following three strings we want to concatenate (figure 7.8):
concatenating the three will result in a single string:

Figure 7.8 Concatenating three strings “Ray” + “Of” + “Sunshine” will result in the new string
“RayOfSunshine".

Looking at Figure 7.8, you can see that we used the addition operator + to
illustrate the concatenation, but in real code, it’s just as simple as that: all you
need to do in order to concatenate strings is to use the concatenation operator
+.

It’s important though to understand, that when we concatenate strings, we


work on at least two arguments (two strings):

dest (stands for the destination, or destination string).


src (stands for the source, or the source string). The src can be more
than two strings.

Concatenation of both strings (dest and src) together is as if you glued one
to the other, so the 2nd string (2nd argument) will append to the 1st string (1st
argument) and will be placed right after it ends. As a result of this operation,
we get a brand-new string, or, as we may call it, the love child of dest and
src.
Let’s see what this statement will look like in the real code.

Listing 7.3 Code practice – Concatenating strings

#include <iostream>
#include <string>

int main()
{
std::string one_word; #A
std::string one{ "Ray" }, two{ "Of" }, three{ "Sunshine" }; #B
one_word = one + two + three; #C

std::cout << one_word << std::endl;


}

When we run the code, the output is RayOfSunshine.

In Chapter 3, when you learned about the C++ operators, you learned about
the addition operator +, and that its obvious role is numerical addition. Can
we use + to add numerical values to a string? Let’s put this to the test in our
IDE.
Well, this will not work, as the second object I am not a string literal – it’s an
integer, and the IDE shows us the + operator does not match std::string
and int type. C++ does offer us ways to overcome this restriction, using a
method called casting, where data can be changed from one type to another,
and the compiler makes the conversion automatically without the
programmer’s intervention. We touch casting later on in this book.

Tip

Whenever you are not sure an operation is possible, put it to the test using
your IDE, which, in most cases, will not only indicate if an operation or
syntax is possible or not but will also tell you what the problem is and what is
wrong.

Good to know

In the old days we used the strcat() function in order to concatenate two C-
style strings, which were basically an array of chars. This method is still
used, and you will find it in a lot of code out there. To make it easier to
remember, you can see that strcat() is a combination of two words: strings
and concatenating.

Sorry if I appended you: Using append() to concatenate strings

C++ offers us another method to concatenate strings: using the append()


function which is part of the <string> library. For example:
#include <iostream>
#include <string>

int main()
{
std::string s1 =„"I am learning„";
std::string s2 =„"C+“";
s1.append(s2);
std::cout << s1 << std::endl;
return 0;
}
Running this code will print“"I am learning C+”".

7.3.3 Keep it short: using erase() to shorten strings

In Chapter 6, you learned how to use the erase() function to erase elements
from a specific position in a given vector. When dealing with strings, the
erase() function is used similarly and eliminates elements from a given
index range [the first element, and the index length) without disturbing the
order of the remaining elements. The syntax for using the erase() function is
easy and makes sense, just like the other functions you just learned about, and
as illustrated in Figure 7.9.

Figure 7.9 The structure of an erase() statement: we need to define the first element position and
end the length of the index (where will it end) for the erasing operation.

Let’s look at a code sample. In this sample, we erase a part of a string and
display the string before and after the erase operation.

Listing 7.4 Code practice – Erase elements from a string

#include<iostream>

int main()
{
std::string str = "This book is funny";
std::cout << "Before we erase the output is: " << str << std::endl;
str.erase(16, 2); #A
std::cout << "After we erase the output is: " << str << std::endl;
return 0;
}

Once we run this code, the output will be (figure 7.10):

Figure 7.10 The output after erasing elements from our string, now “funny” became “fun”.

remember

If you forget to specify both arguments (the first index position and length of
the characters we wish to erase), and you only specify the first index position,
the erase() function will erase all characters from the starting position to the
end of the string.

Using clear() to erase an entire string


If we want to erase the entire string object, we can simply use the clear()
(which you already used with vectors in Chapter 6). This function erases all
characters in a string. The syntax is easy to understand from everything you
have learned so far.

As you can see, all we need to do is to attach the function clear() to the
string we wish to erase completely. Let’s take the previous code and instead
of erasing parts of the string, we clear it.

Listing 7.5 Code practice – Clearing our entire string

#include<iostream>

int main()
{
std::string str = "This book is funny";
std::cout << "Before we clear the output is: " << str << std::endl;
str.clear();
std::cout << "After we clear the output is: " << str << std::endl;
return 0;
}

When we run the code, we can see the following output:


7.3.4 Using the substr() function for obtaining sub-strings
Before we dive into our next subject (searching strings), let’s explore what a
substring is first. Looking at Figure 7.11, you can see that a substring is part
of a string, and in this case, “raptor” is the substring of “Velociraptor”.

Figure 7.11 Substrings are any part or portion of a string.

C++ offers us a simple function to specify, or obtain substrings: the


substr()function. When we want to find a substring, we need to know its
position within a string and its length. Figure 7.12 illustrates the logic using
our Velociraptor string. As you can see, there are two parameters we must
use as part of the substr() function: pos (position) and len (length). The pos
parameter defines the substring's start location, while the len indicates the
number of characters in the substring.

Figure 7.12 The substring “raptor” is part of the string velociraptor. “pos” defines the position of
our substring, and “len” defines the length of the substring.

Let’s look at a code sample. In this code we have the string “I am learning
all about C++ strings”, and we will print the substrings “I” and
“learning” to the console.

Listing 7.6 Code practice – Searching and displaying substrings from a string

#include <iostream>
#include <string>

int main()
{
std::string str = "I am learning all about C++ strings";

std::string my_substr;
my_substr = str.substr(0, 1); #A
std::cout << "The first substring is: " << my_substr << std::endl;
my_substr = str.substr(5, 8); #B
std::cout << "The second substring is: " << my_substr << std::endl;

return 0;
}

When we run this code, our output should be:


7.4 Search and you’ll find() –string searching
methods
In many cases, we need to search elements within our string objects. Let’s
say, for example, that we write a program that can divide a block of sentences
into individual sentences. We will probably need to search for periods ‘.’,
question marks ‘?’, or exclamation marks ‘!’ - all of which is an indication
for the end of a sentence. There are so many other use cases for searching
strings, and the rule of thumb is, that whenever there’s a string, we will
probably need to use one search method or another.

In chapters 5 and 6, you learned how to use the element’s index position to
find the element we are looking for within an array or vector. C++ strings
come with some fantastic and intuitive built-in search methods, saving us the
need to traverse through index positions directly. These methods are part of
the std::string library. In the next few sections, we will go over some of
the most useful and powerful methods C++ can offer us.

Find() me – easy peasy search using the find() function

A comfy way to find what we’re looking for with C++ is the find()function.
We use find()to find specific elements within a string. Yes, find() is a great
name for a function that, well, finds things for you.

There are several things we can do using find(): we can find a word
(substring), or the index of a specific character. find()also indicates a failure
of search, in case what we looked for was not found, and as we explain in a
minute.

Note

If we compare it to other C-style function names you just learned about (i.e.
strcpy(), strcat()) you clearly see how modern C++ has evolved into an
easier and simpler language, with a more human form style and vocabulary.
The syntax of the find() function is as easy to understand as we can expect
from a straightforward function such as find(), and is illustrated in Figure
7.13:

Figure 7.13 The structure of a find() statement: we need to define the string where the search will
take place (string1) followed by the string to be searched (string2).

The find() function returns the position of the first character of our match,
taking our Velociraptor sample we used when you learned about the
substr() function, and as illustrated in figure 7.14 below.

Figure 7.14 The find() statement in action returns the index position of the first character of the
substring we search for.
As you can see in figure 7.14, we search for the substring “raptor” within
the string “Velociraptor”, as find() will return the index position of ‘r’,
which is the first letter of the substring “raptor”.

Let’s look at a code sample using find() where we look for a substring
“raptor” within the string “velociraptor” and display the index position
where our substring starts from, just as illustrated in figure 7.14.

Listing 7.7 Code practice – Using the find() function

#include <iostream>
#include <string>

int main()
{
std::string my_str("Velociraptor"); #A
std::string string_to_find("raptor");#B

std::cout << "We are looking for the string *" << string_to_find << "* w
std::cout << "Is *" << string_to_find << "* part of *" << my_str << "*?"

size_t substring_index_found = my_str.find(string_to_find);#C


if (substring_index_found == std::string::npos)#D
std::cout << "No, the substring was not found" << std::endl;
else
std::cout << "The substring *" << string_to_find << "* was found

return 0;
}

When we run this code, the output will be (figure 7.15):

Figure 7.15 The output of our program will show if the substring “raptor” was found, using the
find() function, within our string “Velociraptor”.
The find() function also allows us to search for any element from a specific
position onward. When searching for a substring for example, we can specify
a position we want the search to start from, and the search will start in this
specific position to the end. Later in this chapter, we will demonstrate how it
works in code.

When a search leads to a dead end – the std::npos value

In the previous code, we used the code line if (substring_index_found ==


string::npos). In this case, the find() function returns a
std::string::npos value. Wait - npo-what? ‘npos’ actually stands for No
Position, or, in other words, the target search item was not found, so it should
be pretty easy to remember and understand what ‘npos’ means. Whenever a
search is conducted, the index position of the result is returned, but if it fails
to find anything, no position within the index is found.

The find() method uses an Integer to represent a true or false value (true
being found and false not found) A valid index value is an integer >= 0
which means the index at which the target string was found.

Below is another example of how to use std::string::npos with the find


function:
#include <iostream>
#include <string>

int main()
{
std::string s = "triceratops";
std::string substring = "raptor";
size_t index = s.find(substring);
if (index == std::string::npos)
{
std::cout << "Substring not found" << std::endl;
}
else
{
std::cout << "Substring found at index " << index << std::endl;
}
return 0;
}
The output of this code would be “Substring not found”.

Good to know

std::npos holds a constant static value with the highest possible value for an
element of type size_t, which is an unsigned integer type (int). In general,
type size is chosen so that it can store the maximum size of a theoretically
possible array of any type. On a 32-bit system size_t will take 32 bits, on a
64-bit one 64 bits. It is unlikely that you ever work with strings long enough
to reach the maximum capacity of std::npos, as the value std::npos can
hold is 18446744073709551615 for X64-bit computers.

7.4.1 “hcraes sgnirts” - Searching strings in reverse


So far you have learned that searching for elements in a string starts at a
given point and then continues to the end of the string or set of strings. The
rfind() function (stands for reverse find) allows you to conduct these
searches from end to start, rather than from start to end. You might be asking
yourself “Why would I need to conduct a backward search?”. Well, there are
plenty of reasons. Let’s say we want to find the name of a folder in which
there’s a file we are searching for. The name of the folder will always appear
last in the path:
C:\Users\Ruth\Documents\Visual Studio 2022\Backup Files\CAN

Since we know the folder’s name is last (the folder ‘CAN’), there is no point to
search from the very beginning of the string - it simply is a waste of time and
resources. We can just search backward, starting our search at the end and
stopping on our right as we reach the backward slash ‘\’, which indicates the
name of the folder has passed the first letter. Searching for files or folders
within a given path is common in computer programming, so knowing this
method is pretty useful.

Working with rfind() is exactly the same as working with find() - let’s
look at a code sample using the rfind(). In this code, listing 7.8, we search
for the name of a file within a given path. However, since we don’t know if
this code will run on Windows-based PC, Linux, or MacOS, and as we need
to use a file path, which differs from one OS to another, we need to use a pre-
processor directive.

Why and when do we need to work with a pre-processor directive

Preprocessor directives are used in C++ (and other programming languages),


to tell the compiler to perform certain operations before the code is actually
compiled. One common use of preprocessor directives is to include platform-
specific code in a program.

For example, different operating systems often have different system calls or
libraries that need to be used to perform certain tasks. By using preprocessor
directives to distinguish between different operating systems, a programmer
can write code that is portable across different platforms, but that includes
platform-specific code when necessary.

The following code uses the preprocessor directive #ifdef to include


platform-specific code for Windows and Linux:
#ifdef _WIN32
// code specific to Windows goes here
#elif _linux_
// code specific to Linux goes here
#endif

In this example, the preprocessor directive #ifdef _WIN32 checks whether


the symbol _WIN32 is defined. If it is, the code within the block following
#ifdef will be included in the compilation. Similarly, the directive
#elif__linux__ checks whether the symbol __linux__ is defined, and if it
is, the code within that block will be included.

Note

As we explained, we need to search for a backward slash ‘\’ for the program
to know it passed the last letter of the file of the folder. However, in C++ ‘\’
is an escape character used alongside several values, such as \n (new line), \t
(tab). We, therefore, need to use a double backward slash ‘\\’, which, from a
compiler point of view, means a single slash ‘\’.

Listing 7.8 Code practice – Searching a string using rfind()


#include <iostream>
#include <string>

int main()
{
#ifdef _WIN32
char path_separator = '\\';
std::string full_path{ "c:\\my drive\\my folder\\myfile.txt" };
#else
char path_separator = '/';
std::string full_path{ "/my drive/my folder/myfile.txt" };
#endif

std::string base_filename = full_path.substr(full_path.rfind(path_separa


std::cout << "The file name extracted from full path (" << full_path <<
return 0;
}

When we run this code we should expect the following results:

Note

The above code sample used a file path from our Windows-based PC. If you
are using MacOS or Linux, paths will vary.

Tip

If you want to use a backslash as a separator between folders in a file path,


you must use two backslashes to escape the first one. For example char
filePath[] = "C:\\Users\\John\\Documents\\example.txt"; If we only
used a single backslash, the compiler would interpret it as an escape character
and try to use the character following it as an escape sequence. Therefore,
using a single backslash as a folder separator would result in a bad string.

7.4.2 Every ‘starts’ has an ‘ends’


One of the nice new features in C++20 is to be able to easily find whether a
substring is positioned at the beginning or the end of a string. Before C++20,
we would have used the compare() function (which you will learn about at
the end of this chapter), which compares strings, or the substr() function
you also learned about earlier. Though both functions do the job well, the
readability of the code we would use with these functions is not that great.
C++20 provided us with two new functions: starts_with() and
ends_with(). These two functions, as their names imply, can efficiently
check if a string begins or ends with the given prefix, or not.

important!

In order to use starts_with() and ends_with() we must add another header


file: the <string_view>. The string_view library offers us methods to view
the string, but not to modify them (read-only view).

Let’s look at a code sample and see how easy it is to locate a substring within
the beginning or end of a string. From the code itself, and with everything
you’ve learned so far, you can understand and remember the syntax and the
way it’s structured.

Listing 7.9 Code practice – Locate substrings from the end and beginning of a string

#include<iostream>
#include <string>
#include <string_view> #A

int main()
{
std::string str1{ "Give papa a cup of proper coffee in a copper coffee c

if (str1.starts_with("Give"))#B
{
std::cout << "The string starts with the substring 'Give'" << std::e
}
if (str1.ends_with("cup"))#C
{
std::cout << "The string ends with the substring 'cup'" << std::
}
}
Look at this code - it’s a “proper cup of code”, so simple to read and
understand, no messing around. Once we run it, our output should be (figure
7.16):

Figure 7.16 Using starts_with() and ends_with() is so very easy, and in this case, we can see the
first string “Give”.

Tip

Did you ask yourself why the program didn’t indicate the strings end with
“cup”? the answer is that it actually ends with “cup.” (with a dot) and not
“cup”. Had we added the dot at the end of the second if statement, we would
have gotten an output with the string “cup.”.

Good to know

One of the advantages of the starts_with() and ends_with() new


functions, is that they also work well with empty strings, which means that,
unlike other functions, when there is no value in the string it will not throw
an error, instead, it returns the value ‘false’.

7.5 “Triminology” – introduction to string


trimming
Trimming is a useful technique for cleaning up strings by removing excess
whitespaces, such as leading and trailing spaces, tabs, and newlines. This can
be particularly helpful when reading input from a user or file, as it allows you
to eliminate any unnecessary whitespace that may have been included.

Trimming strings can also be useful in a variety of other situations, such as


when certain characters are expected within a string but should be removed
from its edges. Some common characters that are frequently trimmed include:

Spaces: These are often trimmed to ensure that strings are properly
formatted and free from unnecessary whitespace.
Tabs: Like spaces, tabs can be trimmed to ensure that strings are
properly formatted and easy to read.
New line symbols: These are used to indicate the end of a line in a text
file and can be trimmed to ensure that strings are properly formatted.
Commas: Commas are often trimmed to remove excess punctuation
from the edges of a string.

One common use case for trimming is when we tokenize strings, but end up
with unnecessary characters at the edges, like a space at the end of each
string. Trimming can help to address this issue and ensure that your strings
are properly formatted for tokenization.

note

Just to be clear: tokenization and trimming are not the same things -
tokenization involves breaking down a whole into smaller components while
trimming involves removing excess characters from the edges of a string.
However, the concept of tokens is similar in both cases, as both involve
dividing a larger unit into smaller pieces. Trimming is often referred to as
splitting, which is a more accurate term, but the term trimming is more
commonly used in practice. In the next sections, we will explain trimming
and use tokenizing just as a tool to generate strings that might need trimming.

Why do we need to tokenize strings?

There are more than a few reasons and use cases in which we need to
tokenize strings:

Let’s say we have a program that reads a file such as a .csv file, (Comma
Separated Values)[2], and our .csv file contains the following string separated
by a comma (“,”):
“one,two,three,four,five”

We then need to separate (tokenize) each number into separate numbers;


therefore, we need to tokenize the string. Once the string is tokenized the
result will be:
one
two
three
four
five

Whenever we have a program that needs to split a sentence into its individual
words, we can use the space between each word as the token. Consider the
following string: "This is a test". If we split this string using the space as
the token, we would end up with the following tokens: "This", "is", "a", and
"test". Alternatively, if we wanted to split a block of sentences into separate
sentences, we could use the period as the token. This would allow us to easily
break the text down into individual sentences for further processing.

Tokenization in the service of command-line arguments

Tokenization is also useful for parsing command-line arguments. When a


program is run from the command line, it is often passed a list of arguments
that can be used to configure its behavior. By default, these arguments are
typically stored in an array and can be accessed directly by the program.
However, in some cases, the syntax for command line arguments may be
more complex, requiring additional parsing steps. This might involve
trimming excess whitespace or using other techniques to extract the relevant
information from the arguments. Tokenization can be an important part of
this process, as it allows you to break down the arguments into smaller, more
manageable pieces.

Let's look at the following code sample which shows a built-in parsing
capability already provided, just to get a general idea of this concept. In this
code, we print the arguments which are passed to the program.
#include <iostream>

#include <iostream>
int main(int argc, char* argv[]) #A
{
std::cout << "The program was called with " << argc-1 << " arguments." <
for (int i = 1; i < argc; i++)
{
std::cout << "arg[" << i << "]: " << argv[i] << std::endl;
}
std::cout << std::endl;
return 0;
}

Now let’s say we write a program that calculates arithmetic expression and
call it ‘calc’. We can type in the Command Line or Terminal:
calc add 1 2

The output of the code above will be:

The program was called with 4 arguments.


arg[0]: our program
arg[1]: add
arg[2]: 1
arg[3]: 2

Next, the program should parse the parameters, which can be achieved in
various ways. For example, we can check arg[1] and if it’s equal to “add”,
we sum up the numeric representation of the parameters that follow (1, 2).
If this concept looks confusing, or if you’re struggling to understand it, note
that in the next chapter, you will learn more about defining and reading
parameters passed to our program, so this concept will become much clearer.

7.5.1 Give me space – handling multiple spaces between


characters
Let's take another common example of trimming spaces. Consider having text
such as the following: " I am writing to you today and letting you
know that I plan to visit you". As you can see, the spaces in this
sentence are not even – some are single-spaced, while some are double-
spaced. In order to create our tokens, we need to write a program that will
analyze the text by first storing a vector of all words. However, if we use the
space " " as a separator between words, we will get :
“ I”
“am”
“writing “
“to“
“you“
“today”
“and”
“letting”
“you”
“know“
“that” Trimming
“I”
“plan”
“to “
“visit”
“you”

We might want to analyze each word, but because of some extra spaces, we
can’t classify “to” and “to “ as the same word, or “I” and “ I”. That’s a
problem we can solve using trimming. spaces help us generalize strings,
(which is the first step in data cleaning, for example). We can trim anything,
and part of cleaning (and cleansing) data is removing unnecessary parts of it.
Imagine a database containing clients’ home addresses. We may want to trim
(remove) unnecessary characters which were inserted by mistake, such as
!@#$%^&*(), as well as spaces “ “ from a field used to store a house number,
etc.
7.5.2 Trimming, C++ and I

Some frameworks (such as Microsoft MFC), or languages, (such as Python),


come with separate trimming functions for each side of the string (beginning
and end), and an additional function for both sides, so you would have the
ability to trim from the beginning (left), trim from the end (right), or just trim
both sides. Unfortunately, the C++ standard library doesn’t come with any
built-in function to trim spaces (or white spaces) like other languages do.
However, C++20 came up with two new and useful functions:
find_last_not_of() and find_first_not_of(). In fact, these new
functions achieve a much better-trimming result than any other related built-
in function in C++ we could have used. Let’s get to know these interesting
functions better.

find_last_not_of()

The find_last_not_of() function searches a string for the last character,


from the end of the string, that does not match any of the characters specified
in its arguments. For example, let's take an MS Word document for example -
We can treat the content of the document as our first argument, and a set of
characters, such as "?!", as our second argument. The find_last_not_of()
function will then search for the first occurrence of a character in the string
that is not one of the "?!" characters, starting the search at the end of the
string. In other words, it will find the last character in the string that is not a
"?!" character, working backward from the end of the string.

Let’s look at the syntax of the find_last_not_of()function (figure 7.17).

Figure 7.17 The structure of a find_last_not_of() statement: We search for defined characters in a
string, but the search tells us what the position is where the characters are not to be found,
starting the search from the end of the string.
find_first_not_of()

The function find_first_not_of() works the same way as


find_last_not_of()with the difference it searches a string for the first
character that does not match any of the characters specified in its arguments,
but from the beginning of the string, not the end. Let’s take the MS Word
document for example again: We can say that the content of the document is
our first argument. Our second argument will be a set of characters, for
example “?!”. The function find_last_not_of()will search for the first
place where our “?!” characters are not found starting the search at the
beginning of the string.

Let’s look at the syntax of the find_first_not_of()function (figure 7.18).

Figure 7.18 The structure of a find_last_not_of() statement: We search for defined characters in a
string, but the search tells us what the position is where the characters are not to be found,
starting the search from the beginning of the string.
Let’s take the string “This is a sentence.” as an example, and search for
the first occurrence of a non-space character and the last space character in
this string. Figure 7.19 illustrates the way both functions will act in this case.

Figure 7.19 When we use find_first_not_of() we start at the first position, while find_last_not_of()
starts at the end of the string.
To better understand, let’s take the string “This is a sentence.” and place it in
real code. In the following code, we use both functions to search for the first
and last occurrence of a non-space character and print the results to the
console.

Listing 7.10 Search for the first and last occurrence of a non-space character

#include <iostream>
#include <string>

int main()
{
std::string str = "This is a sentence.";
std::string space = " "; #A

size_t pos1 = str.find_first_not_of(space); #B


if (pos1 != std::string::npos) #C
{
std::cout << "First non-space character is at position: " << pos1
}
else
{
std::cout << "No non-space characters found." << std::endl;
}

size_t pos2 = str.find_last_not_of(space); #D


if (pos2 != std::string::npos) #E
{
std::cout << "Last non-space character is at position: " << pos2
}
else
{
std::cout << "No non-space characters found." << std::endl;
}

return 0;
}

Once we run this code, we should expect the following output:


Now that you understand how these functions work, you probably understand
that once we find the correct position (wither at the beginning of our string,
end, or both), we know where to trim. Trimming is done using a function
such as erase(), which you just learned about.

Let's write a program that removes leading and trailing whitespaces from
sentences using the erase()function. We will use what is known as escape
sequences to identify the presence of these whitespaces in the input string "I
am writing several sentences. This is the 2nd one. This is the
3rd one. This is number 4". Escape sequences are special characters that
are used to represent nonprintable characters, such as tabs, new lines, and
carriage returns.

Here is a list of the special characters that we will be looking for in our
program:

\t - represents a tab character


\n - represents a new line
\r - represents a carriage return
\f - represents a form feed (used to start a new page)
\v - represents a vertical tab
' ' - represents a single white space character

By using these escape sequences, we can easily search for and remove any
unnecessary whitespaces from the input string.

In this code, we are going to use a new component you did not yet learn:
std::stringstream, which is a part of the C++ Standard Library.
std::stringstream allows you to read from and write to strings as if they
were input/output streams. It actually provides a convenient way to
manipulate strings as streams of characters, using the same methods that you
would use with input/output files or standard input/output streams (e.g.,
std::cin and std::cout).

Tip

One common use of std::stringstream is to extract tokens from a string.

Important

When we use std::stringstream we must include the header file <sstream>

Here’s a little code demonstrating the use of std::stringstream.


#include <sstream>
#include <iostream>

int main()
{
std::string input = "10 20 30 40 50";
std::stringstream stream(input);
int n;
while (stream >> n)
{
std::cout << n << std::endl;
}

return 0;
}

Running this code, we should expect the following output:


10
20
30
40
50

Now, let’s go back to our code exercise and see how we can trim strings
using our newly learned functions find_first_not_of() and
find_last_not_of(), combined with the erase() function. Below is our full
source code.

Listing 7.11 Code practice – String trimming

#include <iostream>
#include <vector>
#include <sstream>

int main()
{
std::string line{ "I am writing several sentences. This is the 2nd one.
std::vector<std::string> sentences; #A
std::istringstream iss(line); #B
std::string sentence; #C
while (std::getline(iss, sentence, '.')) #D
{
if (!sentence.empty()) #E
{
size_t first = sentence.find_first_not_of(" \t\n\r\f\v"); #F
size_t last = sentence.find_last_not_of(" \t\n\r\f\v"); #G
sentence = sentence.substr(first, (last - first + 1)); #H
sentence += "."; #I
sentences.push_back(sentence); #J
}
}

for (size_t i = 0; i < sentences.size(); i++) #K


{
std::cout << "Sentence #" << i << " is: " << sentences[i] << std::en
}
return 0;
}

Let’s run this code and see what happens (figure 7.20):

Figure 7.20 Trimming our sentence and tokenizing each string into sentences based on the period.
As you can see, we trimmed the sentence into four different sentences. Using
find_first_of() and find_last_of() functions, trimming was easy to
implement.

7.6 The whole string of things - more strings


manipulations
Now that you have mastered searching within strings and trimming (or
tokenizing) them, you're ready to learn a few more string methods. Searching
is often just the beginning of a larger task, and once we find what we're
looking for, there's so much more we can do: we can insert new strings or
substrings in specific locations, replace existing strings or substrings, and
even remove them altogether. In this section, we'll explore the powerful
methods available in std::string that allow us to accomplish all these tasks
and more.

7.6.1 Replacing strings


Replacing a string or substring is easy in modern C++ - all we need to do is to
use a method named replace(). Yes, again an easy self-explanatory function
name. When we use replace() we replace a part of the string which will
begin with a certain index position as a

starting point, up to a certain number of characters. This string will be


replaced with another string we specify. The structure of the syntax we use
with the replace() function (figure 7.21) is easy to understand as it makes
perfect sense, especially as we break it down into components.

Figure 7.21 The structure of a replace() statement: we need to define the string we want to
replace, the position of the first character to be replaced, and the number of characters to be
replaced, followed by the string that will replace the other string.
There are many cases in which we would need to replace a string or a
substring, for example, replacing the first letter of a string with a capital
letter, or vice versa.

Let’s look at a code sample. In this code, we ask the user to write something
about him/herself, and then reflect what was written by replacing "I am" with
"You are", so if the user types "I am feeling well", the output will be "You
are saying you are feeling well".

Listing 7.12 Code practice – Search and replace strings

#include <iostream>
#include <string>

int main()
{
std::cout << "Please tell me something about yourself starting with 'I a
std::string line;
while (std::getline(std::cin, line))#A
{
std::string what_to_search{ "I am " }; #B
std::string what_to_replace("You are ");
size_t f = line.find(what_to_search);
if (f != std::string::npos)
{
line.replace(f, what_to_search.length(), what_to_replace);
}

std::cout << "You said " << line << std::endl;


}

return 0;
}

When we run this code, we might get the following output (figure 7.22):

Figure 7.22 The output of our program when we searched and replaced strings. We can now take
the users' input and transplant it into our output as if we were "talking" to the user.
How replace() works

Search or replace always starts with the search, very much like search-replace
in a Word Processor. Once you find what you are looking for, you replace it
with the new text. Replace uses 3 parameters:

1. The place within the string (line) where the text we searched for was
found.

2. The end of the segment in our string that we would like to be replaced
(that would usually be the place of the text found + the size of the text found

The text to be replaced.

7.7 Formatting strings


Even in the early days of computers, programs could perform tasks that made
life easier. For example, one of the first life-changing aspects of programs
used mostly by offices was the new ability to create mailing lists – and when
we say "mailing lists”, we don’t mean email lists, as emails were not really
used back then (the internet was merely an academic concept mostly used
internally in some universities), what we mean are actual letters printed from
a computer and sent using stamps and envelopes. Some early days programs
(such as Rashumon we mentioned in chapter 3), could create a template of a
document with blank spaces, which the program would then fill using an
additional data source. For example, we were able to write something like:

“Date <>

Dear < >,

We would like to remind you that your appointment with us is on the < > at <
>.

Cancellations are permitted no more than 24 hours prior to the appointment.

Sincerely,
< >”

Using these templates, a program was used to fill in the blanks within the
angled brackets < > automatically. So “Date <>” might turn into “May 15th,
1989”, while “Dear < >” might turn into “Dear Miss Cyndi Lauper”. The
funny thing is that back then, recipients of these template letters believed they
received a “personally written” letter just for them - but that was just the 80s:
we were horribly dressed, used a lot of hair spray, and were pretty naïve back
then.

Back to the here and now, you might wonder why we are telling you all this.
Well, it’s more than just a trip down memory lane – it’s the best way to
understand the basic concept of string formatting. In fact, when we talk about
string formatting, we mean composing a string that will be structured in a
combination of text and data that will be added later - similar to our template
example from the 80s. Of course, we are talking about C++ code and
specifically strings, and not about document templates, but you probably get
the idea.

Learning and understanding the concept of string formatting is important and


will serve you a lot in the future.

To understand the concept well, let’s look at another example. Let’s say we
have the following string:
"The sum of x and y is z".

The values x, y, and z are placeholder for actual values which we do not
know yet, or we might know, but we like to define them in one place, and not
necessarily as part of the string (and we explain the reasons for that in a
second). The values of x and y will be placed later and replace the
placeholders. Once the values are added, we might get the following string
statement:
"The sum of 10 and 20 is 30".

String formatting methods: the old way and the C++20 way
As you can understand, string formatting is important, and often used. C++
offers us two methods to format strings: the old, pre-C++ 20 way, and the
new and improved way, which was added in the C++ 20 standard. Both
methods share similarities, yet the new method is easier, cleaner, and faster.
Before we demonstrate and explain both methods, let’s understand the
underlying concept of string formatting first, which is shared between both
old and new methods.

As you understand, we have a string, and part of the string is to be added later
on, or in other words - formatted. Obviously, we need some indication within
our string telling the compiler: “Hey, I’m placing this placeholder here, and
when the time is right, please replace it with an actual value". In our template
document, we placed <>, but in real code, we use special marks called format
specifiers - and this is where the old and new methods differ: the old methods
use somewhat less friendly format specifiers (though widely used), while the
new method is far more friendly and intuitive to use. Now that you
understand the concept, let’s review both methods and demonstrate how to
use each.

String formatting using printf()

Up to C++20 strings were mostly formatted using the printf() method. This
method was inherited from C, and (no offense) is somewhat messy, (though
Java, Golang, and other languages use it as well). Using this method, you
must define the expected value using the following format specifiers:

%d (for signed integers and decimals)


%u (for unsigned integers and decimals)
%x (for signed integers in hex)
%s (when the data is a pointer to a string)

As you can see, each specifier represents a different data type.

Let's see what it would look like in a simple code block (figure 7.23).

Figure 7.23 When we use printf() we add %s as a placeholder, or format specifiers, as the value
will be added later.
As you can see from Image 7.23, the way strings were formatted up until
C++20 made our code look a bit weird with problematic readability,
especially if you’re a new programmer not yet fluent in C++. Thankfully,
C++20 simplified the formatting method tremendously, and personally, we
were happy to adopt and implement the new method in our everyday coding
life.

How does it work – std::format in action

For years, C++ tried to simplify string formatting, and in C++20 it really did,
as it introduced the std::format library, which, unlike the previous method,
allows adding a single format specifier with a familiar face for each value:
the curly brackets {}. The compiler then replaces the curly brackets with the
desired value. So instead of using %d, %s, etc. which are far from friendly, we
just need to use {} – and that’s brilliant.

IMPORTANT

In order to format strings using std::format() (or std::formatter()), you


need to include the <format> header file.

To understand how to format a string you need to keep in mind that


std::format uses three simple principles:

Placeholder-based formatting syntax that supports indexed arguments


and format specifications.
Type-safe formatting, with multiple argument support.
Support for user-defined types through custom formatters.

note

Though there is so much to learn about std::format(), in this book we only


introduce the fundamentals.

Let’s look at the structure of our syntax (figure 7.24) and also compare it
visually to the older way of string formatting.

Figure 7.24 With std::format() all we need to do is place empty curly brackets which will later be
replaced with the value of the variable we specify in the statement as well.
Important

std::format is very, very lightly supported across compilers and is not part
of the standard C++ library yet. It is provided by some third-party libraries,
but it is not available in all C++ environments. You might get a "not found"
error when trying to include <format>, but this issue is expected to solve
itself as compilers keep updating.

Let’s look at a code sample. In this code, we create a new file named
“Learning CPP example.txt” and write something to it. In Chapter 4, you
wrote a code that uses the file system and learned how to create a file, write
to a file, and open a file. We use the same methods here, but this time we use
std::format to insert the file name and the file size into our string, then
which we then print to the console.

NOTE

In the last code we used cerr for output. cerr is a standard output stream for
printing error messages. It is similar to cout, which is a standard output
stream for printing normal output, but cerr is typically used for printing error
messages because it is unbuffered and always prints to the screen
immediately. This can be useful when debugging because it ensures that the
error message will be printed as soon as the error occurs, rather than waiting
for the buffer to be full. In our code, cerr was used to print an error message
to the screen if the file specified by MY_FILE_NAME cannot be opened. If the
file is successfully opened, cerr is not used.

Note

In the following code we are going to use the statement std::cout <<
output.c_str() and we explain why in the next section.

Listing 7.13 Code practice – Formatting strings (using a file)

#include <fstream> #A
#include <string>
#include <iostream>
#include <format> #B

const std::string MY_FILE_NAME = "Learning CPP example.txt"; #C

int main()
{
std::ofstream myfile; #D
myfile.open(MY_FILE_NAME); #E
if (!myfile)
{
std::cerr << "Error: Failed to open file " << MY_FILE_NAME << std::e
return 1;
}
myfile << "Writing this to a file.\n";#G
myfile.close();#H

std::ifstream in(MY_FILE_NAME, std::ifstream::ate | std::ifstream::binar


if (!in)#J
{
std::cerr << "Error: Failed to open file " << MY_FILE_NAME << std::e
return 1;
}

auto myFileSize = in.tellg();#L


std::string output = std::format("File {} is {} bytes", MY_FILE_NAME, (i
std::cout << output.c_str() << std::endl;

return 0;
}

Here is the output once we run this code (figure 7.25)

Figure 7.25 The program displays the size of our file in bytes after we used std::format and
placed placeholders in the string where the size is to be displayed.
Using output.c_str Vs a regular output.

You might have noticed that in the last code listing, we used output.c_str.
Why didn’t we just use a normal output as we did so far? Well, In C++,
sometimes, you may need to use a C-style string, which, as you learned
earlier in this chapter, is just a sequence of characters terminated by a null
character.

When we pass the result of c_str() to a function that expects a C-style string,
we are passing the address location of the first character in the string. This
allows the function to read the characters in the string from memory, starting
at that address location – we explain more about that in the next chapter when
you learn about functions, but generally speaking, there are many advantages
to using the direct memory address rather than the actual object. For example,
if we use a very large object, passing it can be expensive in terms of
resources but if we just pass its address, it might be cleaner and easier in
some cases.

7.8 It’s a small world: handling multilingual


programs
When we want our program to support various languages, especially ones
with different character sets, such as Chinese, Hebrew, Swedish, Arabic,
Indonesian, and many more, we need more space for storing the data inside a
string. Normally, when supporting English, the standard ASCII character set
of 128 characters is enough. However, in real life, we may need our program
to support more languages, especially if we want to appeal to a broader
market of users (think of any famous program you use, like Microsoft Word,
and imagine it would only support English).

To support languages with different character sets, we cannot use regular


strings, as they do not have enough space. Instead, we use a compound type
and function called a wide string, or wstring (the w stands for wide). But
before we move forward, let's look at a small code sample where we use
regular strings with Hebrew letters to display the word "‫“( "שלום‬Shalom”
which means "hello" or "peace" in Hebrew) and “‫“( ”ﺳﻼم‬Salam”, which
means “hello” or “peace” in Arabic).
#include <iostream>
#include <string>

int main()

{
std::string test1{ "‫;} "שלום‬
std::string test2{ " ‫;} "ﺳﻼم‬

std::cout << std::string(test1) << std::endl;


std::cout << std::string(test2) << std::endl;
}
When we run this code, we get garbage output (figure 7.26)

Figure 7.26 When we try to run a code with Hebrew and Arabic letters (or any other special
characters), we get garbage output.
The reason for this output is the regular strings are based on an array of
‘chars’, each is one byte in size, and therefore can hold values from 0 to 127
(when unsigned chars are used), or -128 to 127 (if signed chars are used).
Therefore, it cannot handle characters other than the 128 that are defined by
the ASCII character set, which is why we need to use a wstring.

You’re such a character: let’s discuss character encoding

Wide character encoding is used to represent characters that require more


than a single byte of storage. In C++, the wchar_t data type is typically used
to represent wide characters. Wide characters are designed to support a
broader range of characters beyond the basic ASCII set (hence the name
“wide”), including characters from various international character sets.

Wide characters are commonly used when dealing with Unicode character
encodings, but they can also be used with other character encodings like UCS-
2 or UTF-16, which are subsets of Unicode. The primary advantage of wide
characters is their ability to represent a wider range of characters, providing
better internationalization and multilingual support in the applications we
write.

As explained earlier in this chapter, Unicode encoding is used as a universal


character set that encompasses characters from all major writing systems
around the world. It assigns unique numeric values (code points) to each
character, enabling consistent representation and exchange of text data across
different systems and platforms. In programming, Unicode is often used in
conjunction with wide characters to handle and manipulate text that contains
a broad range of characters. Wide characters provide the storage mechanism
for representing Unicode characters within a program's data structures and
variables.

Using wstring is similar in many ways to strings, we can use the same
std::string functions and perform the same operations, but there are slight
differences in the way we write wstring literals. When we initialize a string,
we use double quotes, for example:
string {“This is a string”}
When initializing wstring we use double quotes as well, but we add an L or a
_T prefix at the beginning. Our string literals will look like so:

wstring sample1 { L”this is my string”};


wstring sample2 {_T(”this is my string”)};

Until now we did not need any prefixes at the beginning of our strings, so
why now? The answer is that we use prefix characters so we can distinguish
wstring from regular string literals containing char-type characters. Think
of the L and _T as your highlighting markers.

But wait, do we use _T or do we use L as a prefix? How do we know which


one to use and does it really matter? well, it does matter. The decision of
which prefixes to use is based on your project’s definition.

The _T() macro, also known as the "generic-text mapping" macro, is used to
create portable code that supports both ASCII and Unicode character sets.
That way, you can compile your project with the UNICODE preprocessor
directive defined, and any string marked with _T() will be treated as
Unicode, or if you don’t define it, any string marked with _T() will be treated
as plain ASCII text.

The L"" prefix is typically used when working with explicit wide character
strings, and it does not depend on the UNICODE macro.

Unicode-based programs, which is a program that leaves the visual rendering


(size, shape, font, or style) to other software, such as a web browser or word
processor, typically uses wide strings. Other programs which are based on
ASCII, do not require the use of wstring, and we can just use regular string
literals. From this point on, throughout this book, we will sometime use
wstrings in some code samples, so you will get the chance to experience
using them.

Now let’s print our Hebrew and Arabic words again to the console, and this
time, using wstring, we added simplified Chinese as well (the word 和平
which means peace).

Note
When we use a wstring we also need to use wcout, which is like cout but
must be used together with wstring. In other words: if we want to have a
wstring output, we need to use wcout and not cout.

#include <iostream>
#include <string>

int main()

{
std::wstring test1{ L"‫;} "שלום‬
std::wstring test2{ L"‫;} "ﺳﻼم‬
std::wstring test3{ L"和平" };

std::wcout << test1 << std::endl;


std::wcout << test2 << std::endl;
std::wcout << test3 << std::endl;

When we try and run this code, we may get an empty string typed. Why is
that? Well, when printing wide characters (wchar_t) to the console on a non-
GUI (graphic user interface) system (our console), we need to use a code
page to ensure that the characters are displayed correctly. This is because the
console uses a specific code page to interpret and display the characters in a
string. A code page is a table that maps the characters in a character set to a
numerical value (called a code point). Different code pages are used for
different character sets, and each code page defines a unique mapping for the
characters in that character set. When you print a string to the console, the
console uses the code page specified for the console to determine which code
points correspond to the characters in the string and how to display them.

For example, in Windows, the number 1255 is the code page for the
Windows-1255 character set, which is a character set that includes Hebrew
characters. When you set the console's output code page to 1255, it tells the
console to use this character set when displaying text, which allows it to
correctly display Hebrew characters.

Note
In Appendix J you can find how to run the same code on Linux or MacOS-
based machines.

Note

Due to the limitation of the Console window, Hebrew text will be typed
reversed, and for that reason, we used "‫( ”אבא‬Dad, in Hebrew) which is a
palindrome, which is a word or phrase that will look the same if reversed.

Listing 7.14 Code practice – Displaying UNICODE characters (Windows)

#include <iostream>
#ifdef _WIN32 #A
#include <io.h> #B
#include <fcntl.h> #C
#else #D
#include <locale> #E
#endif

int main()
{
#ifdef _WIN32 #A
_setmode(_fileno(stdout), _O_U16TEXT); #F
#else #D
std::locale::global(std::locale("")); #G
std::wcout.imbue(std::locale()); #H
#endif
std::wcout << L"‫ << "אבא‬std::endl; #I
}

Our output will be:


Note

In real-life programming, we recommend sticking to wstring as it supports


both common and exotic languages, while string supports only common
languages such as English and does not support other international or foreign
characters.

Remember

Do not forget to place the ‘L’ prefix before your wstring literal, or your
program might not compile.

7.9 Strings comparison – the old way and the


spaceship way
There are a lot of use cases in which we need to compare strings. An obvious
use case is when a user enters a password, and we need to compare the input
to the stored credentials. Let’s look first at a simple comparison of two
strings using the ‘<’ operator.

Listing 7.15 Code practice – String comparison using the ‘<’ operator

#include <iostream>
#include <string>

int main()
{
std::string sample1 { "Time flies like an arrow, but fruit flies like a
std::string sample2 { "Time flies like an arrow, but fruit flies like ba

if (sample1 > sample2)

std::cout << "fruit flies like a banana" << std::endl;


else
std::cout << "fruit flies like bananas" << std::endl;

The output should be:


This statement is simple, and it determines that in the event our condition is
true, (sample1 is greater than sample2), we print something to the console.
First, the program checks if sample1 is bigger than sample2, yet sample1
may be smaller than sample2, and in this case, the program will start the
evaluation all over again from the very beginning of each statement, which is
a waste of resources.

Note

In this code, the strings are compared using their default lexicographic
comparison, which means that uppercase characters are considered to be less
than lowercase characters. Therefore, "A" is considered to be less than "a",
and "B" is considered to be less than "b".

Remember the three-way operator, AKA the spaceship operator, we


introduced in chapter 3? This new comparison method, which was introduced
in C++20 for the first time, works well with strings. In chapter 3 we covered
its basics and explained that the spaceship operator can return the result of
three comparisons: the less than operator '<' , the equal to operator '==' and
the more than operator '>' which combined forms this: <=>. When you learned
about the three-way comparison, it was too early in your journey to dive
deeper. As you advance your knowledge and understanding so far, you can
now get a better understanding of how this unique operator works, and
particularly, how it works with strings. In the next section, you will get to
know the three-way comparison better and implement your knowledge with
strings comparison.

A deeper look into the spaceship operator

When dealing with the new three-way comparison in C++20, there are two
fundamental concepts you need to know. The first concept is ordering, which
is based on the assumption that when sorting a collection of elements, you
need to provide a sorting predicate. Ordering sounds simple to understand yet
it's a complex mathematical concept, (the order theory), which was
implemented along with all its complexity into the new spaceship operator.
We will not dive into complex explanations in this book, but in a nutshell,
during the ordering process, it's imperative to know the entire relationship
between the elements we compare (i.e. greater than, less than, equal to,
equivalent to, incomparable).

When dealing with ordering, the 3-way comparison new module offers us
several ordering types:

strong (std::strong_ordering) - All elements can be compared using


all six relational operators, and all operands are comparable and imply
substitutability.
partial (std::partial_ordering) - All elements can be compared
using all six relational operators, yet they might contain values that are
not comparable (for example, a floating point NaN (also known as
/næn/, Not a Number), which is a numeric data type with undefined or
unrepresentable value), and cannot be compared with anything else.
compared elements do not imply substitutability.
weak (std::weak_ordering) - All elements can be compared using all
six relational operators, yet incomparable values are not supported, such
as case-sensitive string type. Compared elements do not imply
substitutability.

Table 7.1 illustrates the properties of each ordering type.

Table 7.1 (The properties of ordering type)

Comparable
Ordering type Operators Substitutability
values

std::strong_ordering ==, !=, <, >, <=, >= Yes Yes

std::partial_ordering ==, !=, <, >, <=, >= No Yes

std::weak_ordering ==, !=, <, >, <=, >= No No

The second concept in C++20 three-way comparison is comparison


categories, which basically means the data type we compare (i.e. int, float,
etc.). Data types are meaningful to the comparison process, as some data
types have certain characteristics which do not allow perfect ordering. Each
ordering is categorized depending on the type of operand we wish to
compare, so comparing an int type will be different than comparing a float
type, which, in some cases might be incomparable (NaN value we introduced
you to for example).

Let’s take a closer look into the relationship between ordering type and
comparison categories illustrated in Table 7.2.

Table 7.2 (Relation between ordering and comparison categories)


Less Greater Equal Equivalent unordered Used with

Integers and
strong_ordering
pointers*

partial_ordering Floating point

User-defined
weak_ordering
operators

Note

You will learn all about pointers in chapter 9

In this table, we have two comparison categories which might look odd:
equal and equivalent. Wait? Don't the two mean the same? Well, no. In fact,
equal and equivalence are two terms that are often used in math, where the
term equal refers to things that are similar in all aspects, whereas the term
equivalent refers to things that are similar but only in a particular aspect.
Sounds confusing? Let’s simplify it and think about it from a computer
program’s point of view. If a user’s password is Password123, and he enters
“password123”, we can say both passwords are equivalent, but they are not
equal. Equal means that you can substitute one for another, while equivalent
does not necessarily mean the same. Remember that computers “speak”
binary, so equal means equal on a binary level, not just on a value level.

Remember

only std::strong_ordering can only be defined as equal, while all the other
ordering types don't.

comparing strings using C++20 3-way comparison


It might be right to say it's not very practical to use 3-way comparison when
we want to compare floating points or integers, as these data types don't
really benefit from it, we can use simple comparison operators for that.
However, when it comes to comparisons of compound objects such as
strings, it can be very helpful. At the beginning of the section, we looked at a
sample when we compared two sentences using the '<' operator, and we
learned that when the result of the first outcome is false, the program will
start another evaluation. Therefore, using the 3-way comparison method is a
great solution for strings.

Let's take the previous sample and implement it using a three-way


comparison. With this sample, we will use three new functions which are part
of the <compare> module in C++20:

std::is_lt (is less than)


std::is_gt (is greater than)
std:: is_eq (is equal)

Let’s see how we implement these functions in our code.

Listing 7.16 Code practice – Three-way comparison with strings

#include <compare> #A
#include <string>
#include <iostream>

int main()
{
std::string sample1 = { "Time flies like an arrow, but fruit flies like
std::string sample2{ "Time flies like an arrow, but fruit flies like ban

const auto order = sample1 <=> sample2; #B


if (std::is_lt(order)) #C
{
std::cout << "sample1 is less than sample2" << std::endl;
}
else if (std::is_gt(order)) #D
{
std::cout << "sample1 is more than sample2" << std::endl;
}
else
{
std::cout << "sample1 is equal to sample2" << std::endl;
}
}

And the output will be:

Let’s run another example, this time we are comparing two vectors of strings
to check which one is lexicographically greater than the other.

Listing 7.17 Code practice – Three-way comparison with vectors of strings

#include <compare>
#include <string>
#include <vector>
#include <iostream>

int main()
{
std::vector<std::string> vec1{ "apple", "banana", "cherry", "date", "eld
std::vector<std::string> vec2{ "apple", "banana", "cherry", "blueberry",

const auto order = vec1 <=> vec2;


if (std::is_lt(order))
{
std::cout << "vec1 is lexicographically less than vec2" << std::endl
}
else if (std::is_gt(order))
{
std::cout << "vec1 is lexicographically greater than vec2" << std::e
}
else
{
std::cout << "vec1 is lexicographically equal to vec2" << std::endl;
}

return 0;
}

Once we run this code, we should expect the following output:


These were fun and simple code samples, well demonstrating how to use the
three-way comparison operator with ease and confidence. Obviously, we can
think of more complex use cases and code, but for now, let's stay with this
simple-sweet sample which serves its purpose well.

The strcmp() function

The 3-way comparison saves us the time and resources for two evaluations of
the same statement. Up until C++20 one of the C-style string methods which
was part of std::string::compare was used.

One of those methods is the strcmp() function (strcmp stands for string
compare). Using strcmp() we were able to check if a string is empty, and to
compare two strings, and the function would return an integer value smaller
than zero (in case the first argument is less than the second), zero if both
arguments were equal, and greater than zero if the first argument was greater
than the second. Don't forget that the compared strings were in the form of a
char data type. This method had a lot of downsides and is considered unsafe
because you never know if it points to a valid piece of memory, which might
lead to crashes. Another use of strcmp() is to determine if a string contains
anything, and that is done by comparing the string with an empty string:
if(strcmp(my_string,””))
{
// my_string is not empty
}
else
{
// my_string is empty
}

Or to check if my_string1 is identical to my_string2


if (strcmp(my_string1, my_string2))
{
// my_string1 is different than my_string2
}
else
{
// my_string1 is identical to my_string2
}

Another comparison method used, at the lowest level, is the memcmp()


function (memcmp stands for memory compare) but we are not getting into
details about this method as it is beyond the scope of this book.

Using the compare() function to compare strings and substrings

C++ offers us another function we can use - the compare() function, which
has some benefits over the 3-way comparison method, as it offers more
flexibility when it comes to substrings comparison, and in many cases, we are
interested in the substring rather than in the entire string. The compare()
function returns the following values:

Returns 0 in case both strings are equal.


Returns <0 in case the first string is less than the second string.
Returns >0 in case the first string is greater than the second string.

The syntax of the compare() function is similar to what you learned so far
(figure 7.27):

Figure 7.27 The syntax of the compare() method is pretty simple, using the names of both strings
we wish to compare.

string1 is a string object, and we can copy its value into another string
object.
The dot operator is used to “glue” the function into the appropriate
object.
‘string2’ is the string that will be compared with string1.

Let’s look at a simple code sample:

Listing 7.18 Code practice – Comparing strings using compare()

#include<iostream>
#include <string>

int main()
{
std::string string1("Mama Mia"); #A
std::string string2("Mama Mia");

std::cout << "String 1:" << string1 << std::endl;


std::cout << "String 2:" << string2 << std::endl;
int result = string1.compare(string2); #B

if (result == 0) #C
{
std::cout << "Both strings are equal." << std::endl;
}
else if (result < 0)
{
std::cout << "String1 is less than String2." << std::endl;
}
else
{
std::cout << "String1 is greater than string2." << std::endl;
}
}

We can also use the compare() function to compare between two substrings,
using the index location of the elements we wish to compare. Let's look at
another code sample and break it down with an illustration of the logic. In
this sample, we have string1 ("shenanigans”) and string 2 (“nani”). Our
code will compare word 2 (“nani") with the substring of string2 and see if
they are equal or not. Before we write our code, let's understand the logic
illustrated in figure 7.28

Figure 7.28 The logic behind our comparison: we compare a substring starting at index 3 (the
word “nani” with the substring of string2 (shenanigans), and see if they are equal or not string,
Here is how we implement this logic into the code. In this code, we take a
substring and compare it to another string.

Listing 7.19 Code practice – Comparing a substring to a string using compare()

#include<iostream>
#include <string>

int main()
{
std::string string1("shenanigans"); #A
std::string string2("nani");
const int result{ string1.compare(3, string2.length(), string2) }; #B

if (result == 0) #C
{
std::cout << "string2 is equal to the substring" << std::endl;
}
else
{
std::cout << "String2 and substring are not equal" << std::endl;
}
}

When we run this code, we should expect the following output (figure 7.29):

Figure 7.29 The output of our string comparison program indicates that string 2 is equal to the
substring.
As you can see from the illustration, the logic for comparing a string to a
substring is not very complicated and makes a lot of sense code-wise. We
indicate the starting index position and use the length() function to
determine the length according to the length of the string we compare the
substring to.

7.9.1 To string or not to string – that is the question


Before we conclude this section in the book, it is important to consider both
the advantages and drawbacks of using std::string. While std::string is
a useful and widely used feature of C++, some programmers choose to avoid
it in favor of the traditional C-style string methods. This is because
std::string can be seen as a simplification of the language that hides some
of the lower-level details of string manipulation. std::string wrap you in
the warmth and fuzziness of the C++ containers, but whenever you need
more fine-grained control over the contents of a string is needed, it may be
more suitable to use an array of char or wchar_t rather than std::string.
These data structures allow direct access to the individual bytes of memory
where the string is stored and can be more efficient in terms of resource usage
and performance.

However, it is worth noting that std::string has many benefits and can be a
more convenient and easier-to-use option in many cases. Programmers need
to be aware of both options and choose the most appropriate one for the task
at hand. Additionally, it is essential to be mindful of memory management,
std::string have the potential to consume significant resources if used
improperly. Like the rest of the language, if you don't know what you're
doing, it can get really complicated very quickly. If you're careful in
managing your program's memory during runtime (by freeing an object's
memory when you're done using it, which you will learn how to do in chapter
9), there is a performance benefit to using C-style strings considering how
small and lightweight they are. Ultimately, the flexibility of C++ allows
programmers to choose between the convenience of std::string and the
low-level control of C-style strings, depending on the needs of the program."

Don’t get us wrong – we are not advocating against std:string, we only


want you to remember that having the ability to use std:string does not
mean you should ditch the option to access strings using more 'low level'
methods which are part of C style strings, as these are sometimes necessary
and one of the advantages of C++ is this flexibility.

Pulling strings – shared methods across std::string, std::vector and


std::array

In chapters 5 and 6 we explored several methods which are part of the


std::array, or std::vector containers. Some of these methods are also a
part of the std::string container and visa versa. Table 7.1 displays some of
the methods you learned so far, indicating which method can or cannot be
used in each container.

Function std::string std::vector std::array

at() √ √ √

assign() √ √ √

front() √ √ √

back() √ √ √

begin() √ √ √

end() √ √ √

empty() √ √ √

capacity() √ √ √

reserve() √ √ X

shrink_to_fit() √ √ X
insert() √ √ X

resize() √ √ X

append() √ X X

erase() √ √ X

clear() √ √ X

replace() √ X X

compare() √ X X

swap() √ √ √

Note

There are many more string methods we could not cover in this chapter. In
the next few chapters, we will teach a few more methods, yet we are sure you
will find easy-to-learn methods we do not cover, based on everything we
covered in this chapter and in the chapters to come.

7.10 Summary
String literals are a group or sequence of characters such as “hello” or
“This book is brilliant”. A string is a contiguous sequence of characters
ending with a NULL terminator to identify the end of the string.
The ‘cin’ input stream is not recommended for use with strings as it
cannot handle multiple space-separated values, instead, we use getline()
as an input method.
There are many methods we can use to search strings, copy them, erase
them partially or fully, compare them, concatenate them, find elements
within a string, or insert elements.
Copying strings is easy as all we need to do is to use the ‘=’
assignment operator to assign one value to another.
Using the ‘+’ operator we can easily concatenate strings. For
example, the string Apples + the string Oranges will create a new
string “ApplesOranges”.
Using the find() function, we can search for various elements in a
given string.
Using the rfind() function we can reverse a search, so our search
will start from the end of the string and not from the beginning.
We can conduct more advanced searches using find_first_of() and
find_last_of() functions whenever we want to find the location of
the first occurrence of a specified character.
We use the replace() function to replace a string or substring.
The erase() function allows us to erase specific elements from a
given index position, to begin with, and up to the last index
position.
Formatting strings has changed in C++20, and the new std::format
library simplifies the way we use format specifiers. Unlike the old
printf() method, which remained from the days of C, std::format is
type-agnostic, and can even support user-defined types.
C++20 new three-way comparison method is great for comparing
strings. Using three ordering types strong_ordering(),
partial_ordering(), and weak_ordering() we can conduct three way
comparison with strings.
The compare() function can also compare strings, especially
substrings.
The functions starts_with() and ends_with() efficiently check if a
string begins or ends with the given prefix or not.
Using wstring is a better option whenever we want our program to
support multilingual characters. wstring stand for wide strings.
There are many advantages to using std::string methods with string
literals, yet in some cases, std::string is not a good option, as by using it
you might lose control over the contents of a string on a more low-level
programming. If you need to micromanage memory, working directly
with an array of chars might be a better option.
[1]If you want to read more about code obfuscation, see our article
https://www.infoq.com/articles/anatomy-code-obfuscation/?
itm_source=infoq&itm_campaign=user_page&itm_medium=link
[2] https://en.wikipedia.org/wiki/Comma-separated_values
8 Function in action
This chapter covers
Understanding what functions are, how to define and declare functions
Get a first insight to function overloading
Learn how to manage and use recursive functions
Understand function templates and their role
Learn the difference between Macros and functions

In this chapter, you will learn all about functions. Though you already used
more than a few functions in your code practice, until now you used pre-
written functions (or "methods"), which are part of the C++ Standard Library.
For example, when you wanted to insert elements into a string or a vector you
used the insert() function, or when you wanted to erase elements from a
vector you used the erase() function. Using these built-in functions, as well
as many other functions, or methods you learned so far, was easy, and
friendly.

Obviously, learning how to use built-in functions is important, yet in this


chapter, you will move one step up the ladder, and learn how to write your
own functions. You will learn how to define functions, declare them, create a
function prototype, and how to combine and implement functions along with
other elements in your code.

You will also learn about the difference between passing values to a function
by reference, which means passing the memory address of the variable, and
by value, which means passing a copy of the variable.

You will also learn how function calls are made and how to call a function
within a function. You will also learn how to create function templates,
which are like a blueprint of a function written once and used multiple times,
without the need to write any more code. Writing functions is exciting and
allows you to take your code into your own hands, creating something new,
while bringing out the creativity within you.
8.1 Functions are the new black (box)
In many cases, parts of our code will repeat the same operation or similar
operations time and again. Theoretically, it means writing the same code
chunks time and again and placing them in various parts of our program. But
what if we didn't need to replicate the same code and instead, write a specific
code once, then call it whenever we need it? Well, this is basically what
functions do.

To demonstrate the concept of functions and the simplicity they offer,


imagine a man going to his local coffee shop each day. Whenever the waiter
takes his order, the man replies: "Give me The usual”, and each time, the man
gets exactly what he wants. Obviously, since the man provided a detailed
description of his order at least once to the waiter, each time he returns to the
same coffee shop, he is spared the hassle of providing the full detail of the
same order time and again.

In computer programming, functions are kinda the same thing: When you use
functions you don’t need to write the same code over and over again, you just
define once whatever you want (define a function) and call the function
whenever you need to use it (or: “give me the usual”). Sometimes you can
change the function slightly. Going back to our example, the man can tell the
waiter “the usual with extra cheese”, or “the usual, but without the salad”. He
will still get the same order, only slightly modified.

Don’t leave me DRY

DRY, an acronym for "Don't Repeat Yourself," emphasizes the importance of


avoiding redundant code. Repetitively copying and pasting the same
segments of code can quickly transform a straightforward program into a
convoluted one, and a complex program into an ultra-complex one. Functions
facilitate code reuse, allowing us to call the same code from different parts of
the program without having to rewrite it. In essence, functions simplify code
writing and comprehension, while also promoting more efficient code
maintenance by reducing the overall amount of code. If an issue arises within
a function or if there's a need to enhance its code, you only need to address
the changes in a single, specific location (the function itself) rather than
making adjustments in multiple places throughout the program.

Note

It's worth noting that using too many functions or dividing a program into too
many small parts can also make code more complex and harder to follow. So
it's important to strike a balance and use functions only where they make
sense and improve code clarity.

Every C++ program contains at least one function, and, in fact, one of the
first things you learned in this book was about the main() function, which is
part of every C++ program. As you may remember, the main() function is
called at the program’s startup and serves as a designated entry point to a
program that is executed. As you remember, you must use the main()
function in order to write and execute your code.

C++, as well as other programming languages, offers us two types of


functions: C++ Standard Library and the functions it provides, which you
already know, and already used some of these functions, and user-defined
functions, which are of your own making, and which you learn how to write
and use in this chapter.

When you use the insert() function to insert elements into a string or
vector, for example, you call the function without knowing how it works. The
function is part of the Standard Library, which provides a set of pre-written
functions that can be used in your code without having to reinvent the wheel.
By using these functions, you can save time and focus on writing the specific
logic of your program, rather than writing low-level code for common
operations. Whenever you need to insert elements you call the same function,
which means functions are written and called in a manner they can be used
repeatedly and easily in your code. After all, all you had to do is to type the
function’s name “insert()”. How does insert() work? Who knows? How
does it function? Who cares? As long as it works - we're good.

Obviously, under the hood of insert(), the function is much more complex
on a source code level. Whenever you used a function so far in this book, you
didn't care how this function really works on logic, or source code level,
which is why we refer to functions as black boxes – we don't know how they
work, and in most cases, we really shouldn't care.

The function, our "black box", is blocks, or chunks of code, running only
when called, and once it does, the function will perform certain work, and
optionally returns a value. For example, the three-way comparison function
you learned about in chapters 3, and chapter 5, returns the relationship
between elements, while the erase() function Marely performs a procedure
of erasing elements in a string or vector and does not return any value.

Good to know

In the old days, languages such as Fortran and Pascal forced you to use a
different way to handle blocks of code that don't return any result, and blocks
of code that do. Blocks of code that just do something without returning any
result, were called Procedures, while Functions, were the ones that returned a
value. Functions in C++ serve both purposes: they can return a value and
perform procedures.

C++ and 3rd party functions

Typically, when writing code, you will use pre-defined functions, which are
part of the C++ standard library, However, most programmers also use a 3rd
party functions which are parts of various C++ based libraries and
programming engines. For example, game developers usually use C++ based
gaming engines such as Unreal or Unity. These engines provide dedicated
functions which are not part of the conventional C++ library, as these
functions are related specifically to the gaming industry. There are platform-
related libraries such as Windows API (Win32 API). Some other external
libraries contain specific built-in functions, one of them is Matlab. Matlab is
an external library that contains specific built-in functions for technical
computation such as matrix manipulations, numerical analysis, and algorithm
development. It is widely used in fields such as engineering, physics, and
finance to perform complex calculations and simulations.

Form follows function: Understanding functions

As explained, functions modularize your program, separating your code into


logical units, so each unit is self-contained and can be reused for performing
specific tasks within your code. We described it as a ‘chunk’ of code that we
can use and reuse again and again whenever we call it, instead of rewriting it
multiple times. Let's look at the following example of a code that you should
be able to read and understand pretty easily.
void main(void)
{
int i1{ 10 }, i2{ 20 };
int j1{ 30 }, j2{ 45 };

int k = i1 + i2; #A
k *= 40;
if (k > 200)
{
k -= 200;
}

int l = j1 + j2; #B
l *= 40;

if (l > 200)
{
l -= 200;
}
}

As you can see, both the first and second code blocks do the same thing: they
take two variables and calculate their sum, then multiply it by 40. If the result
is greater than 200, it subtracts 200 from it. But what we are doing here is
repeating the same thing. What if we can simplify this code? A function can
save the need to use two repeating code blocks.

Let’s take a closer look at the two code blocks first and see where they differ:
The first block uses ‘i1’ and ‘i2’, while the result is assigned to ‘k’ :
int k = i1 + i2;

The second code block uses ‘j1’ and ‘j2’, while the result is assigned to ‘l’:
int l = j1 + j2;

How can we simplify this code? Since you didn’t learn how to write a
function just yet, let’s just look at the logic of things: it would be wise to
create a function that receives two parameters, (two integers), and returns an
integer as a result. We shouldn’t care if the parameter’s name will be ‘i1’,
‘j1’, and we can even call it tomato1. We also shouldn’t care if the returned
integer will be named ‘i’, ‘k’, or banana2. In fact, we should be able to apply
this logic to any integer object we might create. If implemented in a function,
it would look like the following:
int my_Calc(int param1, int param2)#A
{
int result = param1 + param2; #B
result *= 40;
if (result > 200)
{
result -= 200;
}
return result;
}

Let’s look at this function: Even though you might have never seen a function
before, you can understand the logic: You can call my_Calc() – this is our
function, and pass two numbers over to it, and, in return, get the result as a
single integer. Calling my_Calc() will be the same as calling insert() or
push_back(), or any other function you used previously – it’s what we
referred to as “the usual” in our coffee-shop example.

Now let's take a look at how our code will become shorter and simpler when
we use and call our new function:
int main()
{
int i1{ 10 }, i2{ 20 };
int j1{ 30 }, j2{ 45 };

int k = my_Calc(i1, i2); #A


int l = my_Calc(j1,j2); #B
}

In fact, we can take any hardcoded number in our code, such as 40 (int
result *= 40) or 200 (int if(result >200) result -=200) and make it
modular and customizable by adding it as other arguments to our function.
By doing so, we can call our function even if we use integers other than 40
and 200. In this case, our function will look like the code below:
int my_Calc(int param1, int param2, int factor1, int factor2)#A
{
int result = param1 + param2;
result *= factor1; #B
if (result > factor2) result -= factor2; #C
return result;
}

To call our function without changing the previously used values (40 and
200), this is how our code will look like:
int main()
{
int i1{ 10 }, i2{ 20 };
int j1{ 30 }, j2{ 45 };
int k = my_Calc(i1, i2,40,200); #A
int l = my_Calc(j1,j2,40,200);#B
}

When you look at the code, it should all make sense - now, we can have more
control over our code, and we can call our function in different ways. For
example:
int main()
{
int i1{ 10 }, i2{ 20 };
int j1{ 30 }, j2{ 45 };
int k = my_Calc(i1, i2,50,200);
int l = my_Calc(j1,j2,70,100);
}

You can see that in this case, we pass different arguments to the same
function – we are not limited to using specific arguments. Think about the
insert() function for example the general idea is that we can insert elements
anywhere we want, as the function itself does not impose specific arguments
for us to use. This is exactly what we did here: we allow you to use the same
function in a flexible way, not imposing any restrictions for using specific
arguments.

Remember
in some cases, we need to “hard-code” some arguments, meaning that these
arguments are set by us and cannot be changed during run time. For example,
a function that is used to calculate tax deductions - the deduction formula
would probably need to be hardcoded, as we don’t want it to be changed,
while the actual values passed to the functions, will change.

8.1.1 Function definition – how to define your function


From everything you’ve learned so far, it should come as no surprise that the
first step when writing your own function is to define it first. This step is
called function definition. A function definition is a process in which we
define the core structure of our function and its interface to the compiler. We
do so by providing specific details: the type and number of arguments our
function will use, the name of the function, the return type, and the actual
functionality the function will serve. Figure 8.1 illustrates the basic structure
of a function definition.

Figure 8.1 When defining a function, we need to specify several elements: the name of the
function, the parameters, and their types (if any) that will be passed to the function, the return
type (i.e., the type of the value that the function will return), and the function's body (i.e., the code
that will be executed when the function is called). The return type indicates the data type of the
value that the function will produce as output after it has executed its code.
Let's take a closer look at these four steps.

Defining the name of our function

As you already learned in previous chapters, the names we provide to our


program's components, such as variables, are important. Naming our function
should also make sense and adhere to the purpose of the function, and it is
important to assign meaningful names and follow best practices, making it
easy to follow what the function is responsible for in your code. Therefore,
when naming your functions, it's always good to stick to some best practices
and guidelines, illustrated in figure 8.2.

Figure 8.2 Best practice for naming your function: use a verb that describes what the function
does, use a meaningful name that hints at the purpose of the function, make long names easier to
read using an underscore or capital letter, and stick to your style or the style of others.
Many developers have their own naming styles, and you might find some
differences in style between developers. Some, for example, will always start
a name with a small letter, others with a capital letter, but no matter what, the
following best practice are good to keep in mind:

Use verbs that best describe what the function is meant to do


It's best practice to use a verb that provides some sort of description of
the role of the function, such as multiply_Value(), set_Value(),
return_Date(), search_If_File_Exists(), etc. – you probably get the
idea. There are many more examples you can think about if you go back
to the C++ functions you used so far: functions such as erase(),
insert(), compare(), push_back(), and more – are all examples that
provide an excellent description of what these functions do.

Tip

In case we use a bool return type (a yes or no), it’s common to use the word
‘is’ at the beginning of the name. For example, is_Valid(), is_Empty(), etc.
Another method is to use ‘can’, when we use a function to ask a question, for
example, can_Run(), or can_Open(), etc.

Use meaningful and case-sensitive names: We always want the name


of our function to be case sensitive, and generally describe or hint the
purpose of the function. Doing so will make your code so much easier to
read, understand and maintain by you or by others. Naming your
functions in unrelated names such as func, or myFunc, might only be
good for short tutorial code samples, but will be difficult to follow and
remember whenever you see it in your real code. We do not recommend
using these types of names.

Remember

From the machine's point of view, the names you give really don't matter,
however, naming various components in your code, such as functions,
variables, etc. is a means of communication with your future self or with
other programmers, which is why readability plays a major role in each name
you choose.

Make long names easy to read: There are a lot of naming conventions
out there (the legend tells there are more conventions than
programmers…). However, let’s remember that some conventions are
often more recommended than others. Some programmers use capital
letters at the beginning of the first word, with an underscore in between
in case the function's name is longer than a single word. For example, a
function that returns the name of an object can be called
return_Object_Name. Other programmers skip the underscore and will
use returnObjectName.

Tip

Personally, we tend to use an underscore, as it’s easier to read. For example,


a function named getFirstResultFromLog will be, in our opinion, easier to
read if we call it get_First_Result_From_Log. You can choose your favorite
method, keeping in mind the recommended naming conventions, which you
can also find online in case you are not sure about.

Stick to a style: Once you work on a code, it’s best to stick to the same
naming style, which will keep your code neat. If you work on a code
with other programmers, or which was already written by others, best to
keep the same style used in the code, so the code can remain neat and
consistent.

Breaking the Camels’ back

Using CamleCase style (i.e. using capital letters for each first letter), or
lowerCamelCase style (i.e. naming the first name with a lowercase and the
rest with capital letters), are two solid options, and you will see both used.
However, most programmers (as do we) prefer to use lowerCamelCase.
Nevertheless, putting style aside, at the end of the day, the most important
thing is that the name you picked is readable and easy to follow.

Defining the function’s return type

Functions may return a value of any type. The value has a type – for example,
an integer (int), or a boolean (bool). In cases the function does not return
any value, the return type will be void. The return value is calculated by the
function and is returned by a return statement. A single function can contain
several return statements, and return a single return value which might be
altered during the execution of the function. In the sample we used
previously, we used the statement return result; to return the result of our
calculation. The return value of the function may vary depending on the
argument(s) passed to it, so each time the function is called with a different
argument, the result may also differ.

Remember

When we define a function in C++, we must specify its return type, which is
indicated by the data type that precedes the function's name. For example, in
the function 'int Return_ID()', the 'int' indicates that the function returns an
integer value. The choice of return type depends on the functionality of the
function and the data that needs to be returned.

Using the ‘auto’ keyword to automatically determine the return type

In Chapter 5 you learned about iterations and the use of the 'auto' keyword.
You learned that the 'auto' keyword is used when we do not want to
explicitly provide the type of the variable we will use for iteration. The
‘auto’ keyword is also useful with functions: it allows the compiler to infer
the return type of a function based on the expression used in the 'return'
statement. This can be useful in situations where the return type is complex
or where it may change over time, as it simplifies the code and reduces the
chance of errors. However, it's important to note that 'auto' should be used
with caution, as it can make the code less readable if used excessively or
inappropriately.

Defining the function’s parameters list

In the code sample we used at the very beginning of this chapter, we defined
the parameters which our function will use:
int my_Calc(int param1, int param2)

The parameter list is the list of the variables, or data items used by our
function, with a specification of their type. How many parameters can we
define? Well, anything between 0 to 256. This is really just a theoretical limit,
as in practice, you're very unlikely to need to define a function with that
many parameters, as the more parameters a function has, the more difficult it
can become to understand and maintain. As such, best practices often
recommend limiting the number of parameters a function takes, with some
style guides suggesting no more than 3-5 parameters for a function.

In our code, we first had two parameters: parm1 and parm2, but remember
what happened when we enhanced our function? we added two more
parameters:
int my_Calc(int param1, int param2, int factor1, int factor2);

Let’s go back to our coffee-shop example. When we talked about this


example, we mentioned that the man ordering “the usual” might sometimes
slightly change his usual order. Under the same concept, when using a
function, there will always be a combination between the common code (“the
usual”), and the changed code. The parameters passed to the function are the
parts we can change.

To better understand, let’s say we use a function named


give_Me_My_Usual_Cup_Of_Coffee(). This function will determine which
type of coffee is "the usual", so the parameter, in this case, could be "for
whom": we can say that if the value is 1, it will be coffee for Michael (a
cappuccino), while if the value is 2, it will be coffee for Ruth (a decaf late).

Code-wise, our function prototype can look like the following:

We will go back to this code in a minute, but first, it’s important to discuss
the functions’ body, which actually defines what our function will do once
it’s called.
Using different parameter types in the same function

In the last sample, we used the same parameter type (int), but the nice thing
about functions is that we can use different parameter types under the roof of
the same function. For example, the definition of the following arguments
(char, int, float, and wstring) would be absolutely fine:
int Return_Value(char param1, int param2, float factor1, wstring factor2);

We can absolutely use various data types within a function, and in many
cases we do. It’s also important to remember that there are cases when there
are a lot more arguments we can use in a function. Think about the main()
function for example we write our code in main, and it can be a huge code
with tons of arguments we use under the single roof of main().

Good to know

If a function in C++ has two or more parameters, the order in which the
parameters are listed in the function call must match the order in which they
are declared in the function header. This means that the function will execute
in the same order as the arguments are passed to i.

Defining the function’s body

By now you know a few steps in defining your function: naming our
function, and defining the return type and parameters list. Once we define all
three, we can move on and define the body of our function, which means
defining what will happen once our function is called.

Now, let's go back to our coffee selection program. In the previous section,
we showed how we define the function's parameter (for_Whom). Now, let's
look at the body of the function (listing 8.1). We can use a switch case to
decide which coffee to make.

Listing 8.1 Code practice – Decides which type of coffee to prepare

#include <iostream>
const int forRuth = 1, forMichael = 2; #A

void give_Me_My_Usual_Cup_Of_Coffee(int forWhom) #B


{
switch (forWhom) #C
{
case forRuth:
std::cout << "Preparing a decaf Latte" << std::endl;
break;
case forMichael:
std::cout << "Preparing a strong Cappucino" << std::endl;
break;
default:
std::cout << "What???" << std::endl;
break;
}
return;
}
int main()
{
int forWhom{}; #D
std::cout << "Whom do you want to coffee to be prepared for?" << std::en
std::cout << "Press 1 for Ruth, or 2 for Michael:" << std::endl;
std::cin >> forWhom;
give_Me_My_Usual_Cup_Of_Coffee(forWhom); #E

Once we run this code, here’s one result we might expect:


Note

if you look at the code we just wrote, you can see that we now have a very
simple code in main(), which serves as a great example of how functions can
simplify our code.

Earlier, in figure 8.1 we illustrated the general structure of a function. Now,


let’s take figure 8.1, and "dress" is with the function we just used (figure 8.3).

Figure 8.3 Our functions’ structure: the return type (void), the name
(give_Me_My_Usual_Cup_Of_Coffee), the parameter (int for_Whom), and the body.

Important
The body of our function must always be in between curly braces {}, just like
the main() function always starts and ends with them.

To sum up the steps for creating your own function, let’s look at figure 8.4,
which illustrates all four components we need for our function’s definition:
name, return type, parameters, and body.

Figure 8.4 How to define a function in four steps: We define the return type, we name it, we
define the parameters and their type, and write the function’s body.

As you can see from figure 8.4, the process of defining your program is
logical, and based on everything you’ve learned so far, it should really make
sense. However, it might take some time to write functions with ease - which
is only natural, and as we continue with this book, you will probably feel,
slowly and surely, how writing good functions becomes easier.

8.1.2 Function declaration


You just learned all about creating your own functions and writing your own
function definition. A function declaration is actually not that different than a
function definition - the only difference between the two is that a function
declaration only specifies the name of the function, the return type, and its
parameters (if any) but not the function’s body. To illustrate it better, let's
take our coffee selection function and show what it looks like with a
definition and with a declaration. Figure 8.5 illustrates the difference between
the two. At the top we have a defined function with a body, and at the
bottom, the same function but without a body definition – this function is just
declared, not defined yet.

Figure 8.5 The difference between function declaration and function definition: A declared
function (bottom) does not contain a body, so it is not defined yet what the function will do. A
function definition (top) includes the function’s body, so we already know what the function will
be performing once called.
Let's go back to a previous example we used earlier in this chapter when you
just learned about functions. We wrote a small function that calculates some
values. Our original function was:
int my_Calc (int param1, int param2, int factor1, int factor2)
{
int result = param1 + param2;
result *= factor1;
if (result > factor2) result -= factor2;
return result;
}

But if we only declare the function and not define it, our function will be:
int my_Calc (int param1, int param2, int factor1, int factor2);

Don’t forget

Always end your function declaration with a semicolon ;.

We can also declare a very simple and basic function which can look
something like this:
int my_Calc();

This short declaration is a valid function declaration, which can be used as a


sort of placeholder until we define it.

Tip

When we declare a function, it’s like telling the compiler: “Hey, I am placing
this function here, it has a name and a type. At this point, the compiler can
handle most (but not all) uses of the function, without the full definition. In
other words: the compiler can call your function before you've defined it, as
long as you've declared it. On the other hand, defining a function means
providing all of the necessary information to create a function in its entirety,
which is how it differs from a function declaration.

A function declaration can also be referred to as a function prototype, as it


provides us with a prototype of a function, like a blueprint of its internal
components without the actual functioning commands. This function has no
body. It is only declared, not defined. The declaration can also have default
values, as long as the default values are set to any number of the last
parameters of the function.

To explain the meaning of 'last parameters', let's examine the following


prototype:
int my_Calc(int param1, int param2, int factor1, int factor2);

You can add default values to any number of arguments, as long as they are
the last ones in the parameter list. That can be:

factor2
int my_Calc(int param1, int param2, int factor1, int factor2=200);

factor1, factor2
int my_Calc(int param1, int param2, int factor1=40, int factor200);

param2, factor1, factor2


int my_Calc(int param1, int param2=50, int factor1=40, int factor2=200);

param1, param2, factor1, factor2


int my_Calc(int param1=10, int param2=50, int factor1=40, int factor2=200);

Let’s look at another example.


int my_Calc(int param1, int param2, int factor1 = 40, int factor2 = 200);

Default values will allow you to call the function without specifying these
values, and in such cases, these defaults will be used.

You can avoid specifying factor1 and factor2, and just call the function
like in our initial example, and in such case, 40 and 200 will be used.
int main()
{
int i1{ 10 }, i2{ 20 };
int j1{ 30 }, j2{ 45 };
int k = my_Calc(i1, i2,25);
int l = my_Calc(j1,j2,25,200);
}

Or you can use your own values or some of them. Any of these options are
valid.
int main()
{
int i1{ 10 }, i2{ 20 };
int j1{ 30 }, j2{ 45 };
int k = my_Calc(i1, i2, 25);
int l = my_Calc(j1, j2, 25, 200);
int m = my_Calc(215, 45);
}

Below is our complete code:

Listing 8.2 Calculations with default parameters

#include <iostream>

int my_Calc(int param1, int param2, int factor1 = 40, int factor2 = 200);
int my_Calc(int param1, int param2, int factor1, int factor2)
{
std::cout << "\tmy_Calc started..." << std::endl;
int result = param1 + param2;
std::cout << "\t[1]result( " << result << ") = " << param1 << " + " << p
int old_result{ result };
result *= factor1;
std::cout << "\t[2]result( " << result << ") = " << old_result << " x "
old_result = result;
if (result > factor2)
{
std::cout << "\t[3]result( " << result << ") is larger than " << fac
result -= factor2;
std::cout << "result(" << result << ") = result(" << old_result << "

}
std::cout << "\tmy_Calc ended. result = " << result << std::endl;
return result;
}

int main()
{
int i1{ 10 }, i2{ 20 };
int j1{ 30 }, j2{ 45 };
int k = my_Calc(i1, i2, 25);
std::cout << "Result of my_Calc(" << i1 << "," << i2 << "," << 25 << ")

int l = my_Calc(j1, j2, 25, 200);


std::cout << "Result of my_Calc(" << j1 << "," << j2 << "," << 25 << ","
int m = my_Calc(200, 40);
std::cout << "Result of my_Calc(" << 200 << "," << 40 << ") = " << m <<

The code contains detailed output which is important for debugging and for
following up the flow of the program. There is output inside my_Calc()
which is printed after a TAB ‘\t’ which gives some nice indentation, and
separates the calculations that take place inside our function and the
function's result.

Note

When we declare a function we tell the compiler a function with that


signature exists. If the syntax of the declaration is incorrect, the program is
ill-formed. we explain all about function signature in a bit, when we talk
about function calls.

Note
unlike function declaration which does not involve the linker, with function
definition the linker will cross-reference the call.

There is one more difference between declaring and defining a function: In


some cases, depending on the compiler and the way the program is built,
when you declare a function, no storage space is allocated, while when you
define a function, a relocatable and reloadable address space is set.

Tip

Function declaration can be very useful when we deal with multiple source
files, and as our function might need to be used in multiple nodules and files:
we don't want to put the body of the function in multiple files, but you do
need to provide a declaration to it.

Function declarations and definitions – where to declare/define it?

It is advised to place all function declarations or definitions in a header (.h)


file. The first reason is that sometimes our code is spread across different files
and sources, so placing it in a header file each source can use, it will allow
other modules to include this header and call the function. It’s like placing a
book in the library, so anyone who needs access to it can just come and
borrow it, and once he finished, return it for others to borrow, rather than
each person keeping a copy at home.

Second, sometimes we need our function to be called several times. Since the
compiler reads the code in main() from top to bottom, in case our code is
written on top and was read already, it might cause a problem to call the same
piece of code again. Let's say we have 2 functions, A and B, and A calls B.
We must place either the B function before A or at least the B declaration
before A, otherwise, the compiler won't find it. If we place a declaration but
the function itself is missing, the code will compile but linking will fail

Placing functions declaration in a header file is actually a great place. You


can add much needed comments explaining what each function does. In
many cases, code is distributed with closed libraries that their source code
isn't public, so the header file is among the few files that are distributed and is
used to explain how to use the library.
Note

A guide on how to open and use a header (.h) file is found in Appendix K.

Remember

It is advised to place function declarations in a header file when the function


is intended to be used by other parts of the program, but its implementation is
defined in a separate source file. This is because the header file provides a
way to make the function's interface available to other parts of the program,
allowing them to call the function without knowing the details of its
implementation.

Why declare and not simply define?

As explained, a declaration (or prototype) is sometimes needed to simply


allow calling the function and is extremely useful when we deal with multiple
source files. All we need to do is to declare the function in one header file
(.h), and then place the function definition in one source file (.cpp file).
Whenever we use a code that uses the function, we will just need to include
the .h file and link the resulting object files with the object file from
compiling the source file.

Functions and Application Programming Interface (API)

As you can see, functions give you a lot of flexibility and can save you a lot
of coding time. At the beginning of the chapter, we compared functions to a
black box. The reason functions are like a black box is that once the function
is ready, you can forget about it. In many cases, you would deploy your code
without revealing the actual code of your function, and just give access to
your function via its declaration. This is called Application Programming
Interface, mostly known as API.

In the example we used previously, our API will allow the coder who uses it
to call my_Calc and to pass to it 2 to 4 integers and receive the result as
another integer. Our API is the declaration along with the meaning of each
variable, and it will look like the sample below:
myFunc API Documentation
Usage:
int myCalc(int param1, int param2, int factor1=40, int factor2=200);
// param1 – the first number we wish to pass to our calculation
// param2 – the second number we wish to pass to our calculation
// (optional) factor1 – a number used to be multiplied by the sum of
// ‘param1’ and ‘param2’ (default value: 40)
// (optional) factor2 – a number used to reduce the value of the result in
// cases its higher than it. (Default value: 200).

With such documentation, the programmer who wishes to use our function,
knows that he/she needs to pass either just param1 and param2 and let the
function use its default values, or specify the additional values (factor1 and
factor2).

There is more to know about APIs, but in the most general term, an API
provides a way for programmers to interact with specific software, by
defining a set of functions, methods, and more, that can be used to request or
exchange data.

Passing by: passing arguments to a function

As explained, when you define or declare a function, you declare its


arguments along with its type. When the function is called, the arguments are
passed to the function. Passing arguments can be done in two ways: they can
be passed by value, meaning the actual defined value of the object (which is
what you've done so far), or by reference, which means the value stored in
the actual memory. It might sound a bit confusing at first, as, after all, isn't
the value stored in memory in any case? So why should there be a difference
if we pass the value as initialized, or pass the memory address? Well, both
values might not be the same. To better understand, let's take a closer look
into both methods, understand the difference between them, and how, or
when they are used.

Passing arguments by value

Passing an argument by value means that a copy of the actual value of the
argument is passed, so if we have an argument int i{50} a copy of the value
50 will be passed to the function. The parameter's real values are called
actual parameters, while the copied values are called formal parameters.
When we pass by value, we get two independent variables with the same
value – one is the actual parameter, and the other is a copy. If the function
modifies the parameter value, the effect is not visible anywhere outside the
function. It makes sense – if the function only uses a copy, and changes this
copy, it will not affect the actual (original) value which was copied from. It’s
easier to understand this concept with the following code.

In this code, we wrote a function that checks if our favorite coffee shop is
open or not, along with its current address, which should be 250 Broadway
Avenue.

Listing 8.3 Code practice – Check if your coffee shop is open or not

#include <iostream>
#include <string>
bool IsCoffeeshopOpen(std::string address, int hour) #A
{
if (address == "120 Main Road") #B
{
address = "250 Broadway Avenue"; #C
}
if (hour >= 8 && hour <= 18)
{
return true;
}
else
{
return false;
}
}

int main()
{
std::string My_Coffee_Shop_Address{ "120 Main Road" };#D
bool Is_Open = IsCoffeeshopOpen(My_Coffee_Shop_Address, 12);
if (Is_Open)
{
std::cout << "The coffee shop is open!" << std::endl;
}
else
{
std::cout << "The coffee shop is closed" << std::endl;
}
std::cout << "The up-to-date address is: " << My_Coffee_Shop_Address.c_s
}

What will happen when we run this code? The output in this case will be:

According to the function, the address was changed from 120 Main Road to
250 Broadway Avenue, yet when we run this code, the address displayed is
the old address 120 Main Road.

Why does this happen? Since the parameter string address is passed by
value, we pass a copy to the function, whatever happens to the copy is
outside of the scope of the function call. In our case, the changed parameter
did not affect our code in main(). Since a function is a black box, changes
made within the function (i.e. when the parameters are passed by value) will
not be “seen” by any part of the program residing outside the function.

But what if we don't want a copy of the value, which, as you just learned,
might reflect a value that was already changed. Instead, what if we want to
show the actual value the parameter holds at any given moment, even if it
was changed? In the next section, you will learn exactly how to do so by
passing by reference.

Passing value by reference

As you just saw, passing by value is passing a copy of the value that does not
necessarily reflect the actual value the argument holds in real-time in case it
was changed. As you learned very early in this book, (when you were
introduced to variables), values are stored in the computer's memory, which
means that if we access the stored values in the actual memory, we would be
able to know their real-time value at any given moment. Passing an argument
by reference is doing exactly that: when we pass an argument by reference,
the address of the argument is passed to the function as opposed to the value.

Note

Though the address is passed, what the function gets is the value of the object
within the address and not the address itself. We discuss this further in
chapter 9, when you learn about pointers.

In terms of syntax, passing arguments by reference looks exactly the same as


passing them by value, except for the use of a small and powerful operator:
the ampersand ‘&’. The ampersand ‘&’ is also called the address operator, as
whenever we use it, it leads us to the address of the parameter it points to. In
the next chapter (chapter 9) you will learn more about the ‘&’ operator when
we teach all about pointers, but meanwhile, it’s easy to understand how to use
this operator when passing arguments to a function by reference.

Unlike the pass by value method, when parameters are passed by reference,
the calling code can see any changes made to the arguments by the function.
Had it been a parameter that was passed by value, any changes to it made
within the function won't affect the outside keeping these changes "in the
family", as you just saw in the previous code sample. Let's look at the same
code sample, this time we use pass by reference using the '&’ operator.

Listing 8.4 Code practice – Check if your coffee shop is open or not using ‘&’

#include <iostream>
#include <string>

bool IsCoffeeshopOpen(std::string& address, int hour) #A


{
if (address == "120 Main Road") #B
{
address = "250 Broadway Ave."; #C
}
if (hour >= 8 && hour <= 18)
{
return true;
}
else
{
return false;
}
}

int main()
{
std::string My_Coffee_Shop_Address{ "120 Main Road" };
bool Is_Open = IsCoffeeshopOpen(My_Coffee_Shop_Address, 12);
if (Is_Open)
{
std::cout << "The coffee shop is open!" << std::endl;
}
else
{
std::cout << "The coffee shop is closed" << std::endl;
}
std::cout << "The up-to-date address is: " << My_Coffee_Shop_Address.c_s

Now when we run this code, we get the current up-to-date address of the
coffee shop:

Remember

This is the exact same code we used in the previous example, except we only
added the ‘&’ before the parameter we wanted to pass by reference (in this
case, “address”).

note

In listing 7.3 we used the statement string &address, yet the placement of
the '&' can be near the name or the type, but it only actually changes the type.

To sum this up, let’s look at figure 7.6, which illustrates the difference when
running the code using both methods.

Figure 8.6 When we pass by value, a copy of the argument 'address' is passed so any change to
'address' will be disregarded. When passing by reference, the actual value stored in the memory
address is passed and not a copy.
As you can see from figure 8.6, passing arguments by value or reference
access different locations in memory, which means we can access the copied
values or real-time values.

Casting: Changing values of passed by reference parameters

A function can enjoy both worlds and return a value, but it can also change
the values of parameters passed by reference. But before we explain further,
since this code sample uses casting, let's go back a bit and recap on what
casting is. In chapter 2, we briefly explained that casting is a conversion
process, which provides the ability to change or convert one type to another,
so, for example, we can convert an int into a float, a char into an int, etc.
Casting tells the compiler that even though a type was declared, we want to
convert the type into another type. In chapter 2, we used a simple example for
casting, which was actually an older way to cast, which was simple to teach 6
chapters ago.
Now that you are more proficient in C++, it's time to talk about advanced
ways to cast using C++. Casting is used a lot with functions, since in many
cases, while a function expects to receive parameters in a certain type (for
example int, int, float), you may need to pass to the function slightly different
types of data, and for that purpose, there is casting.

Important

If there is a significant difference between types, casting won't work or will


cause problems. You can't just cast an integer (5) to become a string ("5") by
casting it. But you can cast a float (5.0) to become an integer (5) and vice
versa. Casting an int into float won't cause any loss of information, but if the
float would be 5.1 and we cast it to integer, you will lose the .1 and get 5.

note

additional information about other casting methods can be found in Appendix


L.

static_cast is a compile-time cast and one of the most commonly used


casting methods. static_cast tells the compiler to attempt to convert
between two different data types. It will convert between built-in types, even
when there is a loss of precision.

Using static_cast is easy, and this time we dive right into it with code
while you learn how to use it, so let's go back to changing values passed by
reference parameters first. As explained, a function can change the values of
parameters passed by reference. For example, the following function takes 2
numbers (int), cast them into a float, and divides the 1st one with the 2nd one.
However, before doing so, it checks if the 2nd argument is 0, if so, it returns
false, if not, returns true. The 3rd parameter is passed by reference and
therefore can hold the result. When we use static_cast, we just replace the
value with the new one:
float result = static_cast<float>(n1);

The old way to implement casting – the C way


The old way to implement casting had we used it in our current code, and
which would still work in today's C++, would have looked like the sample
below:
int n1{10};
float result = (float)n1;

remember

when using static_cast we need to use the static_cast keyword, followed


by the type we wish to convert to in angled brackets (in our case <float>,
then the parameter’s name in regular brackets (in our case (n1)).

Now let’s look at the full function we would use in our code:
bool Divide(int n1, int n2, float &result)
{
if(n2 != 0)
{
result = static_cast<float>(n1) / static_cast<float>(n2);
return true;
}
else
{
return false;
}
}

The next function takes 2 parameters (int) and a 3rd parameter by reference
and updates the 3rd parameter to hold the sum of the 1st and 2nd parameters
void Sum(int n1, int n2, int &result)
{
result = n1 + n2;
}

Below is what our full code will look like (listing 7.4).

Listing 8.5 code practice: Changing the value of a passed parameter

#include <iostream>

bool Divide(int n1, int n2, float& result)


{
if (n2 != 0)
{
result = static_cast<float>(n1) / static_cast<float>(n2);
return true;
}
else
return false;
}

void Sum(int n1, int n2, int& result)


{
result = n1 + n2;
}

int main()
{
int n1{ 10 }, n2{ 20 }, n3{ 0 }; #A
int mysum; #B
float myfraction; #C

Sum(n1, n2, mysum); #D


std::cout << n1 << " + " << n2 << " = " << mysum << std::endl;

std::cout << std::fixed; #E


if (Divide(n1, n2, myfraction))
{
std::cout << n1 << " / " << n2 << " is " << myfraction << std::endl;
}
else
{
std::cout << "Can't divide " << n1 << " and " << n2 << std::endl;
}
if (Divide(n1, n3, myfraction))
{
std::cout << n1 << " / " << n3 << " is " << myfraction << std::endl;
}
else
{
std::cout << "Can't divide " << n1 << " and " << n3 << std::endl;
}
return 0;
}

Once we run this code, we should expect the following output:


Tip

By default, you may get std::cout print of float numbers rounded. To avoid
that, use std::fixed, which is a type of manipulator placed in the output
stream. std::fixed writes floating-point values in fixed-point notation, so
you get exactly as many digits in the decimal part as specified by the
precision field, which specifies the total number of digits to be displayed.

Pass by value or pass by reference – which is best?

Now that you understand the difference between passing by value and
passing by reference, you might be wondering which method is best to use in
your code. Well, both are and the decision of which one to use really depends
on the purpose and structure of your program. If you are worried about
objects being modified by other parts of your program, passing by value can
be the right choice, as it saves the compiler the overhead of checking for the
address of your arguments. But if you think, or know, that some objects are
subjected to changes by other parts of your program, using pass by reference
is the better choice.

Note

Using non-const references is not advisable. Out-parameters are not a very


good pattern and modern C++ tries very hard to make it easy and efficient to
rely on passing around values.
8.2 Under the hood of function calls
Earlier in this chapter we explained that a function is called whenever the
program needs it. The way functions are actually called is a subject of its
own, and in this section, we provide the basic concept of function calls.

In chapter 2, you learned how memory allocation works, and were introduced
to the stack and the heap. Let’s focus on the stack again, which is used for
temporary memory allocation. In the stack, memory allocation happens on
contiguous blocks of memory which are "stacked" one on top of the other.
Both insertion and deletion take place from just one end. In computer science
it’s called LIFO – Last In, First Out. Adding an element on top is called
"Push," and removing it is called "Pop”.

Let's say you have a stack of t-shirts piled on top of one another. You can
grab the t-shirt on top which is the last t-shirt placed on the pile, and you will
need to move it to grab the one underneath or move a few more t-shirts to get
the one on the very bottom. Figure 8.7 illustrates the logic of the stack's
structure.

Figure 8.7 Adding and removing an element from the stack is always from the top.
Each program you write has a certain structure based on the way data is
collected and used. Whenever a function is called, an activation record of the
function is created on the stack. The collection of the function's data is called
a "stack frame". A stack frame is an area of memory that temporarily holds
the arguments to the function, as well as any variables that are defined local
to the function. Stack frame variables are also called "automatic variables",
as the compiler automatically allocates the space for them. Once the
statements in the function block get executed, we should reach the return
statement. The return statement forces the control flow to be returned to the
calling function. At this point, the automatically allocated stack frame is
discarded, and the control goes back to the caller code.

Stack frames contain the packed information related to a function call. This
information generally includes arguments passed to the function, local
variables, and where to return upon terminating. The layout of the stack
frame and layout scheme can be compiler dependent. In general, stack frame
size is not limited, but never say "never" - in theory, the size of the stack can
be infinite, but in reality, we always need to consider the machine's resources,
and if you overdo it, like running too many functions at the same time we
might run into trouble: if the stack is completely used, it will lead to a stack-
full condition, also referred to as stack overflow, which will crash your
program. The stack also contains a red/protected zone, which is kept allowing
system calls, which are calls related to the operating system and must be
executed without interfering with the stack frame.

The stack frame facilitates the function’s body. When a function is called, the
call is preceded by the caller code preparing the stack to push parameters.
The process is followed by the callee code preparing the stack for its local
variables. To make sure the stack frame is restored, once the callee code
returns, it has to make sure the stack points it back at the same value when its
code was entered in the first place.

Figure 8.8 illustrates the structure of a function stack frame once a function is
called.

Figure. 8.8 The structure of the function stack frame when a function is called. The function is
the callee, while the portion of the program that calls the program is the caller. In order to make
sure the stack frame is restored, once the callee code returns, it has to make sure the stack points
it back at the same value when its code was entered in the first place.
Each function has its own stack frame, and the size and structure of each
frame vary and, as said, are determined upon compilation time. Each sub-
program call is organized in a stack data structure. It's not as confusing as it
might sound at first: the first collection of data pushed onto the call stack will
be the last stack frame to terminate. When dealing with functions, the
parameters passed to the functions (either by value or reference, as you
learned previously), are pushed onto the stack frame ("push"). The
parameters are pushed in reverse order from the order they were declared in
the called functions parameter list.

When the function has finished its course, the activation record is "popped"
(“pop”) from the stack. This procedure happens very efficiently and
unnoticeably, yet sometimes it can create some overhead, which is unwanted.

saving the overhead of function calls – back to the inline function

In order to overcome potential issues related to function calls, such as stack


overflow, C++ allows us to compile functions inline – and as you probably
recall, we used a small inline function back in chapter 5, when we wrote a
spreadsheet-like 2D array.

Compiling function inline means the compiler copies the code from the
function definition directly into the code of the calling function, rather than
creating a separate set of instructions in memory. This method also improves
the execution speed.

Note

The decision to inline is left entirely to the compiler. The inline keyword can
be ignored, and the compiler can decide to simply call the function anyway.

Remember

Inline functions can potentially save us the overhead of function calls, and
may generate faster assembly code inline, depending on the specific
implementation and context. However, overusing inline might ‘bloat’ your
code, since, as you just learned, the compiler copies the code, and in some
cases, it might copy it to too many locations – which might be
counterproductive. The good news is that most modern compilers are pretty
sophisticated, and they know when to use inline even without your help.
Let’s take a look at an inline function to understand it firsthand.

note
Inline functions generate faster assembly code than regular function calls
based on the specific use case and the performance characteristics of the
compiler and hardware. In some cases, inline functions may actually lead to
slower code due to increased code size or register pressure.

The following program demonstrates a call to an inline function named


set_lower(). The function changes string arguments passed to it to
lowercase. We hardcoded the string "AbcDEFghi”, and once we run the
program it will be changed to “abcdefghi”.

Listing 8.6 Code practice – Using an inline function to convert upper to lowercase

#include <iostream>
#include <string>
#include <algorithm> #A

inline void set_lower(std::string& s) { std::transform(s.begin(), s.end(), s

int main()
{
std::string test{ "AbcDEFghi" }; #C
std::cout << "Before calling set_lower() " << test.c_str() << std::endl;
set_lower(test); #D
std::cout << "After calling set_lower() " << test.c_str() << std::endl;
}

Here’s what happens once we run this code:


tip

We use inline to hint the compiler to embed the function body directly
within each place the function is called, potentially reducing the overhead of
function calls. In our case, we call set_lower() once per string, not once per
character. Our function uses std::transform() to iterate over each character
just once, applying the case transformation. So using inline here can provide
a minor speed-up by eliminating the function call overhead for set_lower(),
however, it's important to note that using the inline keyword does not
guarantee that the function will be inlined (the final decision is made by the
compiler). For small, frequently called functions like set_lower(), using
inline can be beneficial.

calling a function within a function

In C++ we can have a call to a function within another function, so if we have


function_a and function_b, we can place a call to function_b somewhere
inside the body of function_a and wise verse. Let’s look at a very simple
code sample in which we have four functions (including main()), each
function prints something to the console, then calls the next function.

Listing 8.7 Code practice – Calling a function from another function

#include <iostream>

void Func_A(); #A
void Func_B(); #B
void Func_C(); #C

int main()
{
std::cout << ("The main function says hi!") << std::endl;
Func_A(); #D
}

void Func_A()
{
std::cout << ("This is Function A calling to say hi!") << std::endl;
Func_B(); #E
}

void Func_B()
{
std::cout << ("This is Function B calling to say hi!") << std::endl;
Func_C(); #F
}

void Func_C()
{
std::cout << ("This is Function C calling to say hi!") << std::endl;;
}

When running this code this is what we should expect (figure 8.9):

Figure 8.9 The output of our program shows the order in which one function calls another.
Figure 8.10 illustrates what the function calls in the code sample we just used
look like from the stack frame point of view.

Figure 8.10 Function A is calling Function B. The stack frame of function A is on “hold” until
Function B finishes its course. Function B is calling function C, now the stack frame of Function
B is on hold. Once function C has terminated, we are back to function B, which terminates and
goes back to Function A.

We can also set the return value of a function to be a call to another function.
Instead of 'return something' we can place ‘return call_other_function()’.
Calling a function within a function is commonly used in computer
programing, for example, when function A calls function B to calculate
something, and returns the result back to function A.
When calling a function within a function, the return statement is utilized
(unless we call a void function, as explained in a bit). If we wish to return a
value that is in fact a call to the other function, then the called function should
have a return value to fit this notation. The calling function should have the
same return type if the only expression after the return statement is the callee
function, as demonstrated in the following example code:
#include <iostream>

int add_int(int i, int j) { return i + j; }

int multiply_int(int i, int j)


{
int calc_result = add_int(i, i * j); #A
return calc_result; #B
}
int main()
{
std::cout << "multiplyAccumulate(1,2) = " << multiply_int(3, 4) << std::

return 0;

Once we run the program, our output should be:

The importance of checking the result of a called function first

Now that you see how a function can call another function, and as you looked
into the above sample code closely, remember this important warning: a
caller function should never trust a result of a callee function blindly, we
must have the option to test the results first. We don’t want to be giving
add_int() we used in the previous example, a blank check. In the above
code sample, it’s exactly what we are doing by using return calc result;

Think about it this way: if you were the boss, and asked your employee to
write a report which you then need to use as part of your report and send to
your boss – wouldn't you read the employee's report first to make sure there
are no embarrassing mistakes?

Some programmers do not bother to check the result, as it's a faster way to
code. However, this method is considered dirty and unsafe. The safe way to
handle these types of function calls is to check the results first, so always
keep it in mind.

When a function is calling a void function

When calling a void function within another function, there is no need to use
a return statement, because a void function does not return any value.

Instead, the void function is called just like any other function, using its name
followed by any necessary arguments in parentheses. When the void function
is called, it performs its intended actions or operations but does not return any
value back to the calling function.

The more the better: when functions return more than a single piece of
data

C++ in its basic form only supports functions to return a single value.
However, in many cases, we will need a function to return multiple pieces of
data (or values) to the caller.

Let's say we have a restaurant guide: We can use a function that checks the
information about a given restaurant and returns the appropriate value. In this
case, we might get several pieces of information returned by the function: the
location of the restaurant (which may be a complex set of latitude and
longitude), along with its opening hours, plus its rating – each of these pieces
of information might be of a different data type. How can we handle such a
case using a single function? Well, there are several methods, and in this
section, we will introduce you to one of the three: std::tuple.
Note

Two more methods for returning more than a single piece of data using struct
or a class are explained in chapter 9.

std::tuple is merely a collection of heterogenous objects, and we can use it


whenever different types of objects need to be grouped together to form a
semantic meaning. For example, if we want to represent a point in 3D space,
we could use a tuple with three elements, each representing a coordinate in
one dimension. std::tuple is mostly used when a function needs to return
multiple objects of different types and is a useful tool for working with
heterogeneous collections of objects in C++.

Tip

std::tuple can hold a number of elements of different data types, which are
ordered. The elements of tuples are initialized as arguments in the order in
which they will be accessed. However, once a tuple is created, its size and
contents cannot be changed. We cannot add or remove elements from a tuple
at runtime.

Important

when we work with a tuple, we need to add the tuple header file
#include<tuple>

Declaring a tuple is the first step. In this declaration, we specify the types of
the elements in the order they will appear in the tuple. To declare a tuple, we
use the std::tuple class, which is part of the C++ Standard Library. Here is an
example of a tuple declaration:
std::tuple<int, std::string, bool> client;

In this declaration, client is a tuple that will contain an int, a std::string, and a
bool, in that order. Note that the types are specified inside the angle brackets
(< >).

When we wish to initialize the tuple, we will add the values separated by a
comma:
std::tuple<int, std::string, bool> client {4, "John", false};

Now let’s look at our code (listing 7.7).

Listing 8.8 Code practice – Using std::tuple

#include <tuple>
#include <iostream>
#include <string>

std::tuple<std::string, int, int>get_data() #A


{
return std::make_tuple("Susan", 1234, 25); #B
}

void put_data(const std::tuple<std::string, int, int>& data) #C


{
std::cout << get<0>(data) << ' ' << get<1>(data) << ' ' << get<2>(data)
}

int main()
{
auto [name, id, age] = get_data(); #D
auto data = get_data(); #E
auto combined = std::tuple_cat(data, std::make_tuple(1.0f)); #F
std::cout << std::get <std::string>(combined) << std::endl; #G
{
auto data = std::tie(name, id, age); #H
data = std::make_tuple("foobar", 123, 50);
}
put_data({ "Joe", 4567, 30 });
}

Tip

One of the con of tuple, is that the caller cannot know what exactly is being
returned from the function. In such cases, sometimes you will need to look at
the documentation to understand the API.

8.3 The load is on - Function overloading


Once you get your hands dirty and code a lot, you will probably encounter
two or more functions under the same scope that share the same name, yet
each uses different parameters. Figure 8.11 illustrates the same function, with
the same functionality, using different types of parameters.

Figure 8.11 We have two functions that have the same definition and body, and the only
difference is the parameter type: one is int type and the other is float type.

Both functions in figure 8.11 are the same, yet they form an overloaded set. If
you remember, we already mentioned operator overloading in chapter 3,
when we explained how the less than <, greater than >, and equal = operators
were overloaded to form the new spaceship operator <=>. Function
overloading works in the same concept: we might have several functions with
the same name, but each one handles different parameters. Overloading
allows us to repurpose the function beyond its original scope, so if the first
function handled only int type, we repurposed it (overloaded it) to handle
float type as well, or if one handles two parameters, the other might handle
three, etc. We can keep on repurposing a function, or better say overload it so
it can handle more data types or parameters. In fact, in C++ and programming
in general, function overloading is commonly used, and you will probably
use and see it a lot.

Let's look at a code sample demonstrating basic function overloading. In this


example we have a simple calculation function named calc_sum() in two
versions: The original version takes two integers (two parameters), while the
other version takes three integers (three parameters). Both functions must
return the same type as the original one.

Listing 8.9 Code practice – Simple calculation using function overloading

#include <iostream>

int calc_sum(int x, int y) #A


{
int result = x + y;
return result;
}

int calc_sum(int x, int y, int z) #B


{
int result = x + y + z;
return result;
}

float calc_sum(float x, float y) #C


{
float result = x + y;
return result;
}

int main()
{
int test1 = calc_sum(10, 20);
std::cout << "The result of the original function is: " << test1 << std:

int test2 = calc_sum(10, 20, 30);


std::cout << "The result of the first overloaded function is: " << test2

float test3 = calc_sum(10.6f, 20.2f);


std::cout << "The result of the second overloaded function is: " << test

return 0;
}

Once we run this code, one possible outcome might be:

Can you see what is going on here? The second function, which takes three
int type parameters, is now part of the overloaded set together with the third
function, which takes two float type parameters is overloaded as well.

How overloading works

Overloading works using the function’s signature, which is the number and
types of the function's parameters. Each function has its own signature which
tells the compiler which function to call. Whenever an overloaded function is
called, the compiler determines the appropriate definition to be used, and it
does so by comparing the argument types when calling the function, with the
parameter types specified in the definitions. This process is called overload
resolution.
Remember

function overloading allows us to use multiple functions with the same name
in our program, subject that all parameter lists differ from one another.
Overloading functions is a valuable option, and it simplifies our code and
saves a lot of time writing multiple codes that do the same thing, instead of
using lots of functions under different names.

Good to know

Function overloading is a form of static polymorphism, which means calling


the same name for something which works with several types of data.
Polymorphism is a subject on its own, and we teach it in depth further along
the way in chapter 9.

Watch out! Overloading with reference parameters can backfire

Earlier in this chapter you learned about parameters passed by reference,


which means pointing to the address of the parameter rather than the copy of
the value when passing by value. When we overload a function with
reference parameters, we need to be extra careful. Why? Let's say we
overload the parameter type data_type with a function that holds the
parameter type data_type&. In this case, the compiler will not be able to
determine which function to use from the argument. Let's look at the
following example. In this code, we have a function that takes parameters by
value and an overloaded function that takes the same parameters by
reference.
#include <iostream>

int calc(int num1, int num2)


{
int result = num1 + num2;
return result;
}
int calc(int& num1, int& num2)
{
int result = num1 + num2;
return result;
}
int main()
{
int num1{ 5 };
int num2{ 10 };
int result{};
result = calc(num1, num2);
}

If we look at the console (and depending on the type of console you use), you
will probably get a red error mark under calc, with a warning (figure 8.12):

Figure 8.12 Red error mark under ‘calc’ is caused as the compiler will not be able to decide
which version of the argument to call.
Why do we have this problem? Well, when we run this code, the second
statement can call either int calc(int num1, int num2), or it can call int
calc(int& num1, &int num2), as the compiler will not be able to decide
which version to call - keep that in mind when using function overloading.

8.4 Function recursion Function recursion Function


recursion
In C++, a function can call itself, which is known as a recursive function, or
recursion. In computer science, recursion is a technique used in programming
to solve problems that can be broken down into smaller instances of the same
problem. In a recursive algorithm, the function calls itself with smaller
instances of the same problem until a base case is reached. The base case
represents the smallest possible instance of the problem, for which a direct
solution can be easily computed.

While recursion is considered a complicated subject in computer science, in


this section we teach the very basics, so you can understand them with
confidence, as recursion is well-suited to solve many problems.

To simplify recursion a bit more, imagine you have some boxes placed in
different spots across a room. Some of the boxes contain objects, while some
contain other boxes. Let’s say we have a function that can tell you what's in
the box.

You call this function and get a list of items in each box, such as a watch, a
rubber duck, a book, etc. The input is the box, the output is the item inside
the box. If one of the boxes contains another box, you would need to call the
function again from itself. The function will call itself, pass itself the new
box, which was found inside another box, list the items within it, add the
content to the list of other items found, and then return it to the caller.
Whenever the function needs to call itself (recursive function), it's as if the
function pauses and takes a break while waiting for its "other self" (list what's
inside the box within a box), and then collects the data and continues. Now
imagine "matryoshka" boxes: a box inside a box, inside a box, and so on –
each time the function will have to call itself and list what's in the next box.
At some point, we will reach the last box, and the function recursion will
reach its course.

As you can understand, there are three basic recursion concepts illustrated in
figure 8.13.

Figure 8.13 When using function recursion, we must have a base case. The base case is the
condition to stop the recursion. A recursive function must also change its state and move toward
the base case and must call itself, recursively.

Figure 8.14 illustrates how basic recursion will look in our code.

Figure 8.14 A basic recursive function with a base case. The function will keep calling itself until
the base case is resolved and the recursion terminates.

When dealing with recursive functions, the compiler only needs the function
declaration, not its entire definition, for you to be able to call it. This is
because a function declaration tells the compiler the function's name, return
type, and parameter types, which are enough to allow the function to be
called from other parts of the program.

The first line of the definition also serves as a declaration. Of course, if a


function calls itself, it can lead to an endless loop of calls. To avoid that, a
function should either call itself or end, based on a given criteria. This way,
the recursion occurs until the task assigned to the function is completed and
we can rest assured it will not loop and call itself forever.

GOOD TO KNOW

The Ackermann function, named after Wilhelm Ackermann, recursively


expands to unimaginable proportions, calling itself over 100 billion times.
Realistically, the expected completion falls after the end of the universe. The
Ackermann function will come to a resolution eventually, but we won't be
around to enjoy the solution... In other words, recursive functions must be
implemented efficiently, preferably coming to a resolution within our
lifetime.

Let’s move to an interesting example: In this recursive code, we map and


enumerate all the files in a given path. If the function finds a file, it adds it to
the list. If it finds a directory, the function calls itself, sending this directory
as a parameter. Our recursive function is using directory_iterator which
iterates over the directory_entry elements of a directory (but does not visit
the subfolders). When we hit a subfolder, our function recursively calls itself,
and by doing so, we get the entire list of files in all folders and subfolders.

NOTE

This is an advanced code, which uses several interesting and powerful


features in C++. Read the code carefully. We’ve written a lot of comments
and explanations to ease it for you. Even if it might look hard in the
beginning, once you dive into it, it should all really make sense.

Listing 8.10 Code practice – Map and enumerate all files in a given path

#include <fstream>
#include <iostream>
#include <filesystem>
#include <vector>

void recGetAllFiles(std::wstring path,


std::vector<std::wstring>& filenames,
const std::wstring& space) #A
{

const std::filesystem::path filesVault{ path }; #B


try
{
for (auto const& dir_entry : std::filesystem::directory_iterator{ fi
{
std::wcout << space << dir_entry.path().filename().wstring() <<

filenames.push_back(dir_entry.path().wstring()); #D
if (dir_entry.is_directory()) #E
{
recGetAllFiles(dir_entry.path().wstring(), filenames, L"
}
}
}
catch (std::filesystem::filesystem_error const& ex) #F
{
std::wcout << L"Error: " << std::endl
<< L"what(): " << ex.what() << std::endl
<< L"path1(): " << ex.path1().wstring() << std::endl
<< L"path2(): " << ex.path2().wstring() << std::endl;
}
}

void scanFilesAndPrintResult(std::wstring path, std::vector<std::wstring>& f


{
std::wcout << path << std::endl;
recGetAllFiles(path, filenames, L" |_>");
}

int main(int argc, wchar_t** argv)#H


{
std::wstring path;
if (argc == 1)
{
std::wcout << L"Please enter a starting path to scan all files withi
std::getline(std::wcin, path); #I

}
else if (argc == 2)
{
path = argv[1];
}
std::vector <std::wstring> filenames = std::vector<std::wstring>(); #J
scanFilesAndPrintResult(path, filenames); #K

#L
for (auto it = filenames.begin(); it != filenames.end(); ++it)
{
std::wstring s((*it).begin(), (*it).end());
std::wcout << s << std::endl;
}

return 0;
}

Try and run this code - you should get a list of all files in a given path.

The filesystem library – a powerful tool for working with files


The C++ filesystem library (also known as the std::filesystem library)
provides a powerful and flexible set of tools for working with file systems.
You can use classes and functions for working with the file system. Using
this library, you can create, rename, copy, and delete files and directories.

Some of the key classes in the library include std::filesystem::path,


which represents a path to a file or directory,
std::filesystem::directory_entry, which represents a directory entry
(such as a file or directory) within a directory, and
std::filesystem::directory_iterator, which provides an iterator
interface for iterating over the entries in a directory.

There are other useful tools the filesystem library provides, such as support
for file system queries (checking if a file exists), determining if a given path
is a regular file or directory, and getting the size and modification time of a
file.

One of the main advantages of the C++ filesystem library is its cross-
platform support, so it provides a consistent interface for working with file
systems across different platforms, such as Windows, macOS, and Linux. It
also supports both wide-character and narrow-character path strings, making
it easy to work with paths in different encodings.

Tip

In most cases, passing parameters as const & (called “const reference”) is the
preferred best practice to pass objects to a function, as it avoids making
copies of it, which saves memory and speeds up execution. Note that it also
means we cannot make any changes to the original object.

Recursion: the good, the bad, and the overflow

Recursion can work like a charm once you learn how to work with it:

1. If used correctly, it can make your code look smooth and shiny, compact,
and easy to read and maintain.

2. It can reduce code redundancy in some cases, (depending on the specific


problem being solved, and based on the assumption that the problem is well-
suited to a recursive approach).

3. Some problems are best solved using recursion, for example tree traversal
or graphs. Bear in mind, that in some cases, a non-recursive solution may be
more concise and easier to understand than a recursive solution, even if it
involves more code.

However, recursion has some downsides, which you must consider. Some
main issues relate to resource management: recursion is expensive. It has
greater space requirements than in iterative code, as the function call will
remain in the stack until the function’s base case has reached its course.

Bear in mind, that recursion tends to take more time than an iterative solution
when the recursion depth is high, the base case is complicated, or when the
problem being solved is not well-suited to a recursive approach. In these
cases, an iterative solution may be faster and more efficient.

The main problem is that recursion is prone to stack overflows, so recursion


requires careful management of the function call stack. For example, if the
recursion depth is too high, the program may run out of stack space and
crash. Alternatively, if the base case is not defined correctly or the
termination condition is not met, the recursion may continue indefinitely,
causing the program to hang or run out of memory. However, with careful
design and testing, many of these issues can be mitigated

While some problems are best solved using recursion, others might not be,
and are better solved using iteration, so keep that in mind. If you do use
recursion, just like anything else in life, you need to know when to stop.

8.5 Function Templates – write once, then reuse,


reuse reuse.
Sometimes, we need to create a function that will be used for several data
types. Instead of writing separate functions for each type, we can create a
function template. A template is a simple yet powerful tool in C++. The idea
is to pass the data type as a parameter, which saves us the need to write the
same code whenever we need to use different data types.

Function templates are a concept that derives from generic programming.


Generic programming is a large-scope concept, but in general, it refers to the
easiness of writing code, and allowing us, as code artists, to use as little code
as possible to perform more. It emphasizes creating reusable and flexible
code that can work with multiple data types and structures.

Writing generic parts of code, and reusing them with various and changing
elements, or data types is the essence of generic programming. According to
Stroustrup, templates are the basis for generic programming in C++, and in
his own words “I wanted three properties for templates: full
generality/expressiveness, zero overhead compared to hand coding [and]
well-specified interfaces.[1]”

What are function templates? Well, let's take the sort() function, for
example. This is a recursive function, which is used when we need to sort
elements within an array, vector, or string. Sometimes, we might need to sort
different data types, so instead of writing and maintaining multiple codes, we
can write a single sort() template and pass it to its data type as a parameter.
In other words: function templates can define how a group of functions will
be generated.

Function templates enable the creation of generic functions that can work
with multiple data types. This means that a single function template can be
defined with the same functionality and used for two or more different data
types, making it easier to create and reuse code. A good example for
understanding the concept of a function template is to think about an MS
Word template: we can create a template of a Word document design and
layout, so each time we need to write a document we don't need to bother
with redesigning it from scratch, yet each document we create using the
template will probably be different in content. A function template is your
blueprint or recipe for a function. You can create as many function instances
as you wish from a single function template, and you don't need to code the
function over and over again.

A template parameter is a type of parameter passed to a function, just like a


normal parameter. However, it is used specifically to specify the data type
that will be used by the function. This allows different functions that
originated from the same template to be distinguished based on their data
type.

When dealing with function templates there are two new keywords you need
to learn: ‘template’ and ‘typename’. Let’s see how to declare a template using
the new keywords you just learned, illustrated in figure 8.15.

Figure 8.15 The syntax of function template: we need to use the keyword "template", to declare
the type and function declaration.

Below is a real-life example of a template:


template <typename fType>
fType CalcMax(fType a, fType b)
{
return (a > b ? a : b);
}

Now, we can implement specific functions based on the actual type:


FunctionName <type>(parameters);
int i, j;
char a,b;
CalcMax <int>(i, j);
CalcMax <char>(a, b);

Implements CalcMax using an ‘int’ as its data type. We can use other types
such as ‘long’
long k,l;
CalcMax <long> (k,l);

As you can see, we defined two template-based functions, by specifying the


type within < > brackets, but we can skip these and just use:
int i,j;
CalcMax(i,j);

long k,l;
CalcMax(k,l);

In this case, the compiler will know to which function to associate the call,
based on the types of the parameters (either ‘int’ or ‘long’). In other words,
when the compiler sees a function template, it remembers the definition but
emits no assembly instructions. Once there is a use of the template with a
particular type, it generates a fresh code fragment by adding the particular
type everywhere in the body of the function definition. Our template will now
be a newly generated code. The function templates are expanded at compiler
time, and as explained, the compiler does type checking beforehand. Figure
8.16 illustrates how this process works with our function template CalcMax.

Figure 8.16 During run time the template is expended by the compiler, and the data type is
internally added to the function.
As you can see, function templates are easy to use, and they are super handy,
saving you time and effort whenever you need to use the same function with
various data types, allowing you a more generic way of coding. One of the
many advantages of using templates is that it allows you to write abstracted
code, so by creating a template that can work with multiple data types, you
can write code that is more generic and flexible, and that can be used in a
wider variety of situations. Another great thing we can do with function
template is called template metaprogramming. In a nutshell, it means using
templates to make decisions, or perform calculations during compilation, and
some programmers love to use this advantage during the development of
complex programs.

Remember that like everything else in computer programming, you always


need to make good use of function templates, in a way that will be effective
for your code and decision-making. This means that you should consider the
specific needs and requirements of your program, and determine whether
templates are the best approach for addressing those needs.

In addition, it is important to consider factors such as code readability,


maintainability, and efficiency when using templates. Writing overly
complex or convoluted template code can actually make your program more
difficult to understand and maintain, so it's important to strike the right
balance between flexibility and simplicity.

8.6 passing an array to a function


Sometimes, we need to write a function that can work with any number of
inputs, but we don't know in advance how many there will be. One way to
handle this is to use an array to hold all of the inputs. However, in C++, we
can't just pass an array to a function as we would with a regular variable - we
have to do it a little differently.

To pass an array to a function, we can pass a reference to the array instead.


This means that the function gets a copy of the address where the array is
stored and can use that address to access the elements of the array. In Chapter
5, you learned about C-style arrays and std::array, which is just a more
modern way to work with arrays in C++.

It is important to note that the use of a C-style array (AKA a “naked” array)
passed to a function is frowned upon, so, instead, the use of std::array is
recommended. The reason is that C-style arrays have certain caveats which
std::array resolves. For example, C-style arrays cannot be assigned when
trying to create a copy of them, and when the copy is created, it manually
iterates over the entire array, as it cannot be passed by value. In other words:
a new array needs to be created manually by the caller, just so it can be
passed to a function. Another issue with C-style arrays is that they do not
support the assignment operator '=' along with many other utility functions
provided by std::array, and they cannot be returned from functions either.

Moving on, when we pass an array to a function, we can either pass it by


value or by reference. Passing an array by value means that each element in
the array is copied, which can take a lot of time for large arrays. On the other
hand, passing an array by reference means that we only need to pass the
address of the array, which is much faster. References are like aliases to
existing objects, so when we pass an array by reference, we're not actually
making a copy of the array. This makes passing arrays by reference a more
effective way of passing arrays to functions.

Note

in the next chapter we will look into passing an array to a function using
pointers, and we discuss the differences between the two very similar
methods of passing using reference and pointers.

remember

As explained, the data inside std::array is guaranteed to be stored in a


contiguous block of memory which makes the access time constant O(1) in
theory and extremely fast. In real-life code, there is a tendency to use fewer
C-style arrays, as they are not consistent with other containers provided by
the standard library in C++ (which you will learn more about in chapter 12).

Let’s look at a code sample using std::array for passing an array to a


function in two methods: by reference and by value. For each method, we are
going to use a template.

1. Template 1: The first template sets all values of the given array to its
default constructed value. The original array gets modified as the
function takes a reference to the original array.
template<typename T, std::size_t size>
void with_ref(std::array<T, size> &arr)
{
for (auto &val : arr)
{
val = {};
}
}

2. Template 2: Our second template uses operator overloading to print the


contents of a C-style integer array passed by constant reference, not by
value. If you remove the const you can modify the array.
template<typename T, std::size_t size>
void with_ref(std::array<T, size> &arr)
{
for (auto &val : arr)
{
val = {};
}
}

Now let’s move over to our full code (listing 7.10).

Listing 8.11 Code practice – Passing an array by value with template

#include <iostream>
#include <array>

template<typename T, size_t size> #A


void with_ref(T(&arr)[size])
{
for (auto& val : arr)
{
val = 1;
}
}

template<size_t size> #B
std::ostream& operator << (std::ostream& os, const int(&arr)[size])
{
for (const auto& val : arr) #C
{
os << val << ' ';
}

return os;
}

int main()
{
{
int arr[] = { 1, 2, 3, 4, 5 }; #D
std::cout << "Original array: " << arr << std::endl;
with_ref(arr);
std::cout << "Passed by reference (original array gets modified): "
}

std::cout << "\n----------------------------------" << std::endl;

{
int arr[] = { 1, 2, 3, 4, 5 }; #E
std::cout << "\nOriginal array: " << arr << std::endl;

int copy_of_arr[sizeof(arr) / sizeof(arr[0])];


for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); ++i)
{
copy_of_arr[i] = arr[i]; #F
}
std::cout << "Passed copy of original array (original array doesn't
}
}

Once we run this code, we should expect the following output (figure 8.17):

Figure 8.17 The output we should expect when running this code shows the difference when we
pass an array by reference or by value.
Tip

If we don't pass an array as a reference, by default it will decay the array to a


pointer (which you will learn about in the next chapter), which has several
disadvantages, one of which is that the arrays will not be compatible with the
modern C++ features like the range-based-for loop. However, passing an
array to a function that takes an array by reference does not decay the array to
a pointer and preserves the array size information. In any case, remember that
the best practice would be to use std::array.

8.7 Macros, Inlines, and functions – what the fuss is


about?
If you ever coded before, or have some knowledge, you probably heard about
macros, and we actually already briefly mentioned macros in chapter 5 when
you wrote a code that emulates a spreadsheet using a multidimensional array.

It is important to understand the concept of Macros, as they are part of


computer science in general, and are used in many use cases for different
purposes. Though since C++ 20, it is recommended to use inline functions
instead of macros for reusability and faster compilation time – but macros are
still out there, so it’s important to at least know what’s all the fuss is about.

So what are macros? You can think of macros as a search-replace mechanism


(whenever you see X, replace it with Y). Once the compiler encounters this
name, it replaces the name with the entire piece of code assigned to the
label/name. As we explained, we use inlines for the same purpose, however,
inline functions are safer and easier to debug. The kind of 'search/replace'
happens during runtime.

The problem with macros is that the preprocessor simply substitutes the
macro name with the corresponding macro body before compilation, without
any type checking or evaluation of the input. Therefore, if you pass different
types of input to the macro, it might not work as intended and could cause
issues. On the other hand, functions and inline functions are type-sensitive
and perform type checking during compilation, ensuring that the input passed
to a function is of the correct type.

Good to know

Macros are defined using a #define directive – which is an easy term to


remember. A basic macro function will be: #define macro_name
text/function. The name provided to the macro is the label for the entire text
or function that it represents. When the compiler encounters the name of the
macro in the code, it replaces it with the text or function that it represents.
Overusing macros might cause bugs if not properly coded, and some
programmers avoid macros.
As explained in chapter 5, instead of macros, it is recommended to use inline
functions. Inline functions have the same speed capacity as macros, but
without disadvantages. In the case of different types, templates can be used
instead of macros.

The macro and the beast: why macros are an endangered specie

Back in the days of C, the best way to address "search and replace" would
have been using a macro – without going too much into details, macros
provide the functionality of a "search and replace", which works just like a
search and replace command in an MS Word document. Macros allowed you
to search for a specific piece of code and replace it with another piece of
code. The difference between macros and inline functions is that macros are
preprocessor directives that replace code before compilation, while inline
functions are actual functions that the compiler replaces at compile time.

It means that macros are less safe because they are expanded by the
preprocessor before the compilation, and may result in unexpected behavior,
such as unwanted side effects and potential naming conflicts. In contrast,
inline functions are safer because they are compiled by the compiler and are
subject to the usual rules of scope and type safety.

You can say that if C++ can fly, macros can’t really - they are a basic form of
doing things, which is somewhat unsafe. Sometimes, using macros creates a
set of nonstandard language features which are hard to debug, for example, it
might behave differently from what a programmer might expect.

The C++ language has been striving to eliminate macros altogether – an


attempt which is not completely successful, but will be eventually.

In conclusion, while macros are somewhat of an endangered species in C++,


they can still be useful when needed. However, it is recommended to use
inline functions and other language features instead of macros whenever
possible to write safer and more efficient code.

Final chapter exercise

To sum up this chapter let's write a program that contains some concepts you
learned about in this chapter and previous chapters. The program maintains
the data on the average temperature per month of the year in four cities (New
York, London, Barcelona, and Paris). The data is stored in a two-dimensional
array. The user is asked to enter a city and a month, and the program displays
the average temperature according to his selection.

This code is the most complex you have written so far, so take your time to
read and understand it, as everything in it should make sense following
everything you’ve learned.

This exercise demonstrates several concepts (all of which are further


explained as part of the code annotation):

1. Multiple dimensional arrays – In our case, we store rows of data per


city, while each column is a different month. We assign all month names
to row 0, and all city names to column 0. We store the data in a wstring
type of array, so it can support both names (city or month) and the data
(which is numeric but stored as wstring for simplicity). Table 8.1
illustrates our array.

Table 8.1 Our two-dimensional array

1 2 3 4 5 6 7 8 9 10 11 12

Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec

New York 2 4 0 8 12 18 27 28 31 19 7 1

London 8 3 10 12 19 22 25 29 18 15 7 5

Barcelona 10 9 15 20 25 27 30 33 25 12 10 8
Paris 0 3 10 15` 19 21 27 31 `20 13 4 0

2. Inline functions - In the final exercise we use two inline functions that
simplify the way we fetch the city name or month name from a two-
dimensional array.
a. The GET_CITY_NAME() inline function gets the city name from a
two-dimensional array. It accesses the first (0) item in the 1st
dimension to get these names.
b. The GET_MONTH_NAME()inline function gets the name of a month
from a two-dimensional array. it accesses the first (0) item in the
2nd dimension to get these names.
c. set_lower() is an inline function and is used to convert text to its
lowercase version, for case-insensitive search.
3. getline() - We use the getline() function to be able to receive input
even if it contains several words (i.e. New York). As you learned in
chapter 5, getline() replaces ‘cin’ whenever the input is longer than a
single word, as ‘cin’ does not handle long text well.
4. Using an enum - An enum is a type of variable that allows you to create
your own lists of values. You can use it to assign values to things like
colors, names of places, or anything else that you can put in a list. For
example, let's say you create an enum called "Colors" that includes the
colors red, blue, yellow, and green. Each color in the enum is assigned a
number value by default starting from 0, but you can also assign specific
values to each item in the enum.
Using an enum can be helpful because you can easily iterate through the
values in the list, assign them to variables, or read them in your code.
For instance, if you call "spring + 1" using a "Session" enum, you will
get "summer", which is the next season.
5. Your own defined function - The GetData() function receives our new
type (Element) as one of its parameters, allowing the same function to
handle validating both city names and months. The returned value is the
index of the city / month, or -1 if they don’t exist. You can test it by
trying to enter “aaa” as either city or month names as input.
6. With the index of the city and the month, we can then pull the data and
display it.
7. Let’s go over the code, section by section, and explain the process and
logic.

Note

For convenience, we explain all the components in the code, but our
explanation is not always in the order these components appear in the final
code.

First step: create the TWO-DIMENSIONAL array

The first step is to declare and initialize our array with the names of the cities
and the temperatures per month.

Note

in real-life programs, we would probably read the data from external files
such as a CSV file, rather than using it as part of our code. However, in this
code, we hardcoded the data into an array. Also, Even though some of our
variables are int type (the temperature), we use a wstring type for all, as it's
more convenient to use a single type.

Below is our array:

First, we define 2 consts which are used for the size of each dimension:
const int numMonths{ 12 };
const int numCities{ 4 };

Then we define the array itself


std::wstring Temp_Data[numCities+1][numMonths+1] =
{
{L"", L"January", L"February", L"March", L"April", L"May", L"June", L"Ju
{L"New York", L"2", L"4", L"0", L"8", L"12", L"18", L"27", L"28", L"31",
{L"London", L"8", L"3", L"10", L"12", L"19", L"22", L"25", L"29", L"18",
{L"Barcelona", L"10", L"9", L"15", L"20", L"25", L"27", L"30", L"33", L"
{L"Paris", L"0", L"3", L"10", L"15", L"19", L"21", L"27", L"31", L"20",
};

Second step: We create an enum type


In our case, our enum holds a textual name given to two types of data we pull
from our two-dimensional array: month and city. In other words: we use our
enum to define what are we asking to fetch from our array. This teaches you
two methods: defining a type using the typedef directive and creating an
enum. typedef is used to define a new type. The type can be identical to
another existing type:
typedef myspecialint int;

(or it can be a more complex type).

In our code, our enum will be:


typedef enum
{
Month,
City
} Element;

‘Element’ is the name that we gave our newly created type.

We defined an enum as a new type (typedef) named Element. An Element


can be either City or Month.

Third step - We write our inline function

Inline functions are meant to run faster, and therefore should be short. For
example, our ‘set_lower’ function is inline as it contains one line of code
which is: transform(s.begin(),s.end()); We use inline function a lot in
this code, so we need it to run fast.

Forth step - We write our GetData function

We use a single function named GetData to return either a city name or a


month name from our array. To do so, we specify a parameter which is of the
type ‘Element’ which we made up as the type for our defined enum. An
Element can be either Month or City, so the function GetData can know
which data type we need. The purpose of the function is to work in
conjunction with the user’s input. When the user enters either a city or a
month, we validate the input, and check whether we have such a value in our
array. If we do, we return its index.
int GetData(std::wstring input, Element element)
{
bool found{ false };
int result{ -1 };
set_lower(input);
found = false;
switch (element)
{
case Month:
{
for (int i = 1; i < numMonths+1; i++)
{
std::wstring temp = Temp_Data[0][i];
set_lower(temp);

if (temp == input)
{
found = true;
result = i;

break;
}
}
}
break;
case City:
{
for (int i = 1; i < numCities+1; i++)
{
std::wstring temp = Temp_Data[i][0];
set_lower(temp);

if (temp == input)
{
found = true;
result = i;
break;
}
}
}
break;
default:
break;
}
return result;
}

Fifth step - Our code in main()

Our program asks the user to enter a city, validates this input and gets the city
index by calling GetData(), and then proceeds. It then asks the user to enter
a month and does the same: calling GetData()to validate the input and return
the month index in our array. With both returned values month_index and
city_index, we pull the temperatures of the requested city in the requested
month. We only display the result if the two input fields (city and month) are
valid. If not, bShouldRun will become false, and the result won't be shown.
Since we use case-sensitive comparison, we first convert the input to
lowercase by our inline function set_lower(), so all values checked and
compared too are converted to their lower-case version.
int main()
{
std::wstring city;
std::wstring month;
int city_index{ -1 };
int month_index{ -1 };
bool bShouldRun{ true };
while (bShouldRun)
{
std::wcout << L"Please enter a city: " << std::endl;
std::getline(std::wcin, city);
city_index = GetData(city, City);
if (city_index != -1) break;
std::wcout << L"City " << city << L" not found." << std::endl;
bShouldRun = false;
}
while (bShouldRun)
{
std::wcout << L"Please enter the month in which you wish to get the
std::wcin >> month;
month_index = GetData(month, Month);
if (month_index != -1) break;
std::wcout << L"Month " << month << L" not found." << std::endl;
bShouldRun = false;
}
if (bShouldRun)
{
std::wcout << L"The average temperature in " << GET_CITY_NAME(city_i
}
return 0;
}

Below is our full code with additional explanation with code annotation.

Listing 8.12 Fetching the average temperature per city per month

#include <iostream>
#include <string>
#include <algorithm>

const int numMonths{ 12 }; #A


const int numCities{ 4 }; #B

std::wstring Temp_Data[numCities + 1][numMonths + 1] = #C


{
{L"", L"January", L"February", L"March", L"April", L"May", L"June", L"Ju
{L"New York", L"2", L"4", L"0", L"8", L"12", L"18", L"27", L"28", L"31",
{L"London", L"8", L"3", L"10", L"12", L"19", L"22", L"25", L"29", L"18",
{L"Barcelona", L"10", L"9", L"15", L"20", L"25", L"27", L"30", L"33", L"
{L"Paris", L"0", L"3", L"10", L"15", L"19", L"21", L"27", L"31", L"20",
};

inline std::wstring GET_CITY_NAME(int n) { return Temp_Data[n][0]; } #D


inline std::wstring GET_MONTH_NAME(int n) { return Temp_Data[0][n]; } #E

typedef enum #F
{
Month,
City
} Element;

inline void set_lower(std::wstring& s) { std::transform(s.begin(), s.end(),

int GetData(std::wstring input, Element element) #H


{
int result{ -1 };
set_lower(input);
switch (element)
{
case Month: #I
{
for (int i = 1; i < numMonths + 1; i++)
{
std::wstring temp = Temp_Data[0][i];
set_lower(temp);

if (temp == input)
{
result = i;
break;
}
}
}
break;
case City: #J
{
for (int i = 1; i < numCities + 1; i++)
{
std::wstring temp = Temp_Data[i][0];
set_lower(temp);

if (temp == input)
{
result = i;
break;
}
}
}
break;
default:
break;
}
return result;
}

int main()
{
std::wstring city; #K
std::wstring month;
int city_index{ -1 }; #L
int month_index{ -1 };
bool bShouldRun{ true }; #M
while (bShouldRun)
{
std::wcout << L"Please enter a city: " << std::endl; #N
std::getline(std::wcin, city);
city_index = GetData(city, City); #O
if (city_index != -1) break; #P
std::wcout << L"City " << city << L" not found." << std::endl; #Q
bShouldRun = false; #R
}
while (bShouldRun)
{
std::wcout << L"Please enter the month in which you wish to get the
std::wcin >> month;
month_index = GetData(month, Month); #T
if (month_index != -1) break; #U
std::wcout << L"Month " << month << L" not found." << std::endl; #V
bShouldRun = false; #W
}
if (bShouldRun)
{
std::wcout << L"The average temperature in " << GET_CITY_NAME(city_i
}
return 0;
}

When we run this code and select New York, December, we should expect
the following output (figure 8.19):

Figure 8.19 Our output once running the code shows the average temperature in a chosen city
per a chosen month.
Now that you run this code and went through each and every step of
composing it, you should feel your growing C++ “muscle” and skills which
you will continue to nourish in the next few chapters.

8.8 Summary
Functions modularize your program, separating your code into logical
units, so each unit is self-contained and can be reused, always
performing specific tasks within your code. With functions we just write
a specific code once, then call it whenever we need it.
Function definition is the process in which we define the core structure
of our function, and its interface to the compiler. We do so by providing
details such as the type and number of arguments our function will use,
the name of the function, the return type, and the actual functionality the
function will serve.
We can make our functions even more generic and create function
prototypes, which act as a blueprint of the function’s internal
components without the actual functioning commands.
When a function is called, arguments are passed to the function.
Arguments can be passed in two ways:
Pass by value, which means the actual value of the argument is
passed. When passing by value, changes are made to the copy, and
the copy is only available inside the function.
Pass by reference, which means passing the address of the
argument passed to the function and not the value. When we want
to pass arguments by reference, we use the ampersand '&', which is
also called the address operator, and which is used to indicate the
type is a reference. When parameters are passed by reference, the
calling code can get access to any changes made to the arguments
by the function.
When we use inline functions, the compiler copies the code from the
function definition directly into the code of the calling function, rather
than creating a separate set of instructions in memory, saving the
overhead of function calls.
A function can also call another function. When the calling function
calls another function, it pauses until the callee function finishes its
course.
Whenever our function returns multiple values, some even of different
types, we can use std::tuple, and utilizing it allows multiple elements to
be passed back to the caller.
Function overloading allows us to repurpose the function beyond its
original scope. If the definition is exactly the same then you should
probably use a template.
Overloading is useful if you want to reuse the name and may have
(slightly) different definitions. Function recursion is whenever a
function calls itself one or more times. When using function recursion
we must have a base case, the function must change its state towards a
solution while recursing.
Function templates allow developers to define a generic function that
can work with multiple data types, without having to write separate
functions for each data type. The function template is defined with
placeholders for the data type, which are replaced with the actual data
type at compile-time.
[1]
https://www.infoworld.com/article/3155288/bjarne-stroustrup-mines-
generic-programming-for-a-better-c.html

You might also like