Professional Documents
Culture Documents
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
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++.
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.
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.
In this book
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++.
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
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.
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.
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.
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.
Figure 1.2 A small sample of C++ code and how it looks like had it been written as machine code.
Note
good to know
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.
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.
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.
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.
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.
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...
Remember
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”.
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.
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++.
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
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.
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.
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.
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.
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
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.
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.
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.
Good to know
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.
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.
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.
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.
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.
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.
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.
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.
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
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.
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
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”.
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.
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.
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.
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
#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.
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:
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.
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.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.
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.
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.
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.
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
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.
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.
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.
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
Let's explore a few of the C++ data types and their size in bytes as presented
in Table 2.1
Double floating
double double pi{3.14159); 8 bytes
point
wchar_t Hello{L“
”};
Note
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’};
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.
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;
}
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).
Size in
Data Type Example
byte
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
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.
#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;
}
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;
}
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.
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).
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;
}
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.
Tip
though you can leave your variable uninitialized, it’s best practice to always
initialize it to a default value such as 0.
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.
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:
Good to know
Per the most common convention, variable names generally start with a
lowercase letter and are camel-cased.
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…
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++.
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.
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
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
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.
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.
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
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.
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;
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
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.
#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;
}
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.
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.
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.
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
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:
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.
#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;
}
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
Tip
If global variables are used, use the “g_” prefix to indicate that, and to avoid
confusion among variables with identical names.
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.
#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;
}
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++.
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.
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.
#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;
}
Note
A full list of the C++ arithmetical operators can be found in Appendix G.
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?
Bear in mind that you can use parenthesis to set your own precedence, and it
might even be useful to prevent confusion.
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.
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.
Remember
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.
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.
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;
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++.
Note
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!
}
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
As you can see, this is a quite simple concept to understand and is often used
in C++.
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.
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
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 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.
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 promote a first and then assign its value to b, so both b
and a will be equal to 1
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;
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.
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.
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
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.
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.
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.
#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.
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.
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.
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.
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.
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:
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.
#include <iostream>
int main()
{
bool isWindy = false;
bool isCold = true;
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).
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:
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.
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.
#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?
Note
You can find a table of all the C++ comparison operators in Appendix F.
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.
#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;
}
}
As you can see, using these two operators is simple and straightforward.
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.
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.
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.
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.
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 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
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
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.
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.
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.
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.
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
Figure 3.18 A sample mock interface created to illustrate a program that allows the user to
choose a letter style.
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.
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'
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…
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);
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.
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.
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.
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.
#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.
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.
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.
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 );
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++);
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;
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.
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
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.
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.
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.
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;
}
#include <iostream>
int main()
{
const int boiling_temp = 80;#A
int oatmeal_temp = 20;#B
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
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.
#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.
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 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.
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
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.
#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
}
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.
#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;
}
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;
}
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.
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;
}
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;
}
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
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.
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.
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.
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");
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;
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
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.
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 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.
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.
Tip
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.
#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.
The statement
char f_name{‘d’};
handles a single value for the variable f_name. Each single variable occupies
its own single memory space.
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
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
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.
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.
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
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
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.
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};
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):
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.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):
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;
}
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.
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.
int main()
{
char word[6] = { 'h', 'e', 'l', 'l', 'o', '\0' };
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;
}
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.
#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;
}
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.
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.
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.
#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;
jokes[jokeIndex]++;#I
std::cout << "---------------" << std::endl;
}
else
{
keepTellingJokes = false;
}
}
return 0;
}
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
}
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.
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;
}
}
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 << " ";
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
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;
}
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.
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.
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:
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.
#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
};
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…
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.
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).
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):
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
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
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.
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.
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).
Now let’s take a look at our full code and run it.
#include <iostream>
int spreadsheet[ROWS][COLS]{}; #B
int main()
{
#E
cell('D', 4) = 99;
cell('I', 10) = 30;
cell('F', 2) = 19;
cell('G', 5) = 93;
cell('A', 4) = 12;
return 0;
}
Once we run our code, we should expect the following output (figure 5.20):
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];
}
Listing 5.7 Inserting values into a spreadsheet-like table with sum calculation
#include <iostream>
int spreadsheet[ROWS][COLS]{};
int main()
{
cell('D', 4) = 99;
cell('I', 10) = 30;
cell('F', 2) = 19;
cell('G', 5) = 93;
cell('A', 4) = 12;
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.
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.
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!
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).
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.
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.
int main()
{
std::array<char, 10> a;
a[0] = 'h';
a[1] = 'e';
a[2] = 'l';
a[3] = 'l';
a[4] = 'o';
if (!hasNullTerminator)
{
std::cout << "Warning: no null terminator found in the string." << s
}
return 0;
}
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};
Remember
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;
}
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.
int main()
{
std::array <int, 5> arr{ 23, 32, 12, 6, 34 };
return 0;
}
Can you guess what might be wrong with this code? (Clue: look for signed
and unsigned types).
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()
{
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.
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.
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.
int main() {
int cArray[5] = { 1, 2, 3, 4, 5 };
std::array<int, 5> stdArray = std::to_array(cArray);#A
return 0;
}
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
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):
#include <iostream>
#include <array>
int main()
{
std::array<int, 5> arr{};#A
arr.fill(0);#B
return 0;
}
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.
#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 };
arr1.swap(arr2);#A
return 0;
}
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.
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.
let’s look at the steps to write this code and build our program.
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;
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.
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.
#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;
}
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.
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
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.
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.
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'};
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;
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.
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;
}
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.
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
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
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.
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
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 };
return 0;
}
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.
#include <iostream>
#include <vector>
int main()
{
std::vector<int> vec = { 1, 2, 3 };
vec.push_back(4); #B
vec.push_back(5); #B
vec.push_back(6); #B
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.
Remember
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.
#include <iostream>
#include <vector>
int main()
{
std::vector<int> v;
return 0;
}
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
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.
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
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().
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.
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().
#include <iostream>
#include <vector>
int main()
{
std::cout << "Knock, knock." << std::endl;
std::cout << "Who's there?" << std::endl;
return 0;
}
Once we run this code, we can see the vector’s elements before and after
inserting a new element:
Important
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;
}
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() <<
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.
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
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 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.
#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();
return 0;
}
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.
#include <iostream>
#include <vector>
int main()
{
std::vector<int> v = { 1, 2, 3, 4, 5 };
std::cout << "Resizing the vector to a larger size using two arguments..
v.resize(5, 10);#C
std::cout << "Resizing the vector to a larger size using a single argume
v.resize(8); #D
return 0;
}
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.
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.
well, in the first statement, we have a vector with 10 elements, all initialized
to 0.
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).
#include <iostream>
#include <vector>
int main()
{
std::vector<int> v;
std::cout << "Reserving space for 10 elements..." << std::endl;
v.reserve(10);#A
return 0;
}
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.
#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!
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".
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
return 0;
}
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
#include <iostream>
#include <vector>
int main()
{
std::vector<int> vec(5);
vec.resize(3); #B
vec.assign({ 6, 7, 8 }); #C
return 0;
}
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.
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.
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.
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.
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()
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
Using std::cin.ignore
Using std::cin.clear
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;
5. Next, we open the file with an input file stream using our FILENAME
constant:
std::ifstream file(FILENAME);
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;
}
}
else
{
std::cout << "Your to-do list is full." << std::endl << std:
}
break;
}
break;
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;
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.
#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::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;
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.
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.
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.
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.
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.
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.
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.
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.
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};
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.
int main()
{
std::string strMyName{}; #A
strMyName = "my name is David";#B
std::cout << strMyName << std::endl;#C
}
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’
#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.
#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.
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.
#include <iostream>
#include <string>
int main()
{
std::string User_Name{ "default" }, New_User_Name{}; #A
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).
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
+.
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.
#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
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.
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+”".
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.
#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;
}
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.
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.
#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;
}
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;
}
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.
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.
#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 << "*?"
return 0;
}
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.
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.
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.
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.
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.
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 ‘\’.
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
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
important!
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
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.
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”
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.
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
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.
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
find_last_not_of()
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()
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
return 0;
}
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:
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
Important
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;
}
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.
#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
}
}
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.
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".
#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);
}
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
“Date <>
We would like to remind you that your appointment with us is on the < > at <
>.
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.
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.
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:
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.
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
note
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.
#include <fstream> #A
#include <string>
#include <iostream>
#include <format> #B
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
return 0;
}
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.
int main()
{
std::string test1{ ";} "שלום
std::string test2{ " ;} "ﺳﻼم
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.
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.
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:
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.
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.
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"和平" };
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.
#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
}
Remember
Do not forget to place the ‘L’ prefix before your wstring literal, or your
program might not compile.
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
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".
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:
Comparable
Ordering type Operators Substitutability
values
Let’s take a closer look into the relationship between ordering type and
comparison categories illustrated in Table 7.2.
Integers and
strong_ordering
pointers*
User-defined
weak_ordering
operators
Note
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.
#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
Let’s run another example, this time we are comparing two vectors of strings
to check which one is lexicographically greater than the other.
#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",
return 0;
}
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
}
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:
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.
#include<iostream>
#include <string>
int main()
{
std::string string1("Mama Mia"); #A
std::string string2("Mama Mia");
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.
#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.
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."
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.
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.
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.
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.
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.
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.
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 };
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.
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.
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:
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.
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
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.
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.
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.
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.
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);
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.
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.
#include <iostream>
const int forRuth = 1, forMichael = 2; #A
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.
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.
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
We can also declare a very simple and basic function which can look
something like this:
int my_Calc();
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.
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);
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);
}
#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 << ")
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
Note
unlike function declaration which does not involve the linker, with function
definition the linker will cross-reference the call.
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.
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
A guide on how to open and use a header (.h) file is found in Appendix K.
Remember
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 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.
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.
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>
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.
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
note
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);
remember
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).
#include <iostream>
int main()
{
int n1{ 10 }, n2{ 20 }, n3{ 0 }; #A
int mysum; #B
float myfraction; #C
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.
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
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.
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.
Listing 8.6 Code practice – Using an inline function to convert upper to lowercase
#include <iostream>
#include <string>
#include <algorithm> #A
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;
}
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.
#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>
return 0;
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 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.
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};
#include <tuple>
#include <iostream>
#include <string>
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.
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.
#include <iostream>
int main()
{
int test1 = calc_sum(10, 20);
std::cout << "The result of the original function is: " << test1 << std:
return 0;
}
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.
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
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.
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.
GOOD TO KNOW
NOTE
Listing 8.10 Code practice – Map and enumerate all files in a given path
#include <fstream>
#include <iostream>
#include <filesystem>
#include <vector>
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;
}
}
}
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.
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 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.
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.
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.
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.
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.
Implements CalcMax using an ‘int’ as its data type. We can use other types
such as ‘long’
long k,l;
CalcMax <long> (k,l);
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.
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.
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
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 = {};
}
}
#include <iostream>
#include <array>
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): "
}
{
int arr[] = { 1, 2, 3, 4, 5 }; #E
std::cout << "\nOriginal array: " << arr << std::endl;
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
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
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.
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.
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.
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.
First, we define 2 consts which are used for the size of each dimension:
const int numMonths{ 12 };
const int numCities{ 4 };
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.
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;
}
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>
typedef enum #F
{
Month,
City
} Element;
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