Welcome to our exploration of numeric variables in C++! In this lesson, we dive deeper into the world of numeric data types and their operations.
Building on our previous discussion about integers and floating-point numbers, we will expand your understanding of how numbers function in C++ and how you can manipulate them effectively.
As a reminder, here is how we create variables. This is similar to what we saw in the previous chapter, except we're now also showing the creation of a float
type. A float
is a number with a decimal point:
bool isDead { false };
int Level { 5 };
float Armor { 0.2 };
int LargeNumber { 100000000 };
When dealing with large numbers, it is often helpful to add separators to make them more readable for humans. For example, we often add commas to a number like 1,000,000.
We can do something similar in our code, if we want. In C++, we can not use commas for this purpose - instead, we can use the single quote character: '
int LargeNumber { 100'000'000 };
We can do all the standard maths operations to our numbers. This includes all the basic operators:
+
for addition-
for subtraction*
for multiplication/
for divisionFor example, we can use a mathematical expression when setting the initial value of our variable, or updating it:
// Level is initialized with a value of 5
int Level { 2 + 3 };
// It is then updated with the value of 10
Level = 5 * 2;
Here are some more examples:
// Level is initialized with a value of 5
int Level { 2 + 3 };
Level = 5 + 1; // Level is 6
Level = 5 - 1; // Level is 4
Level = 5 * 2; // Level is 10
Level = 6 / 2; // Level is 3
Level = 1 + 2 + 3; // Level is 6
The order of operations applies in C++ just as it does in maths. Operations are not always performed left-to-right.
For example, multiplication happens before addition, so our following code will result in Level
having a value of 7
.
The 2 * 3
part of our expression will happen first, then 1
will be added to that result:
// Level is initialized with a value of 7
int Level { 1 + 2 * 3 };
As with maths, we can introduce brackets - (
and )
- to manipulate the order of operations. Expressions in brackets happen first:
// Level is initialized with a value of 9
int Level { (1 + 2) * 3 };
We can use the values contained in other variables within our arithmetic expressions:
int StartingHealth { 500 };
int Lost { 100 };
// This will have a starting value of 400
int RemainingHealth { StartingHealth - Lost };
We can also use the current value of the variable when updating it, using an expression like this:
int Level { 5 };
Level = Level + 1; // Level is now 6
=
)A statement like Level = Level + 1
may seem weird if we interpret it as an equation.
But in C++, and most programming languages, the =
symbol is an operator. It does not specify an equation - instead, like any operator, it acts upon its operands.
=
is often referred to as the assignment operator. It updates the value of its left operand (Level
, in this case) with the value of its right operand (Level + 1
, in this case).
After running the following code, what will be the value of Health
?
int BaseHealth { 200 };
int HealthBuff { 2 };
int Health { BaseHealth * HealthBuff };
After running the following code, what will be the value of Level
?
int Level { 10 };
int Level = Level + 1;
The above example of increasing the value contained in a variable by 1
is so common, that programming languages often offer a quicker way of writing it.
Increasing a value by one is commonly called incrementing, and it has a dedicated operator: ++
.
The operator can go before or after the variable. There is a subtle difference, which we'll discuss in a later chapter.
int Level { 5 };
Level++; // Level is now 6
++Level; // Level is now 7
We also have the --
operator for decrementing:
int Level { 5 };
Level--; // Level is now 4
--Level; // Level is now 3
Additionally, we have further operators to provide a quicker way of implementing arithmetic. These are referred to as compound assignment operators
For example, +=
will increase the variable on the left of the operator by the value on the right.
int Level { 5 };
Level += 3; // Level is now 8
We also have the -=
, *=
, and /=
operators to apply subtraction, multiplication, or division in the same manner
int Level {8};
Level -= 3; // Level is now 5
Level *= 3; // Level is now 15
Level /= 5; // Level is now 3
Level *= Level; // Level is now 9
After running the following code, what will be the value of Level
?
int Level { 2 };
Level = Level * Level + 2;
Level -= 2;
We have access to negative numbers
int NegativeValue { -5 };
They behave exactly like they would in maths
int Health { 100 };
int HealthModifier { -10 };
Health += HealthModifier; // Health is now 90
Health *= -1; // Health is now -90
When used like this, -
is another example of an operator. It is referred to as the unary minus. We’ll discuss what "unary" means later in the course.
The implication of -
being an operator means it doesn’t necessarily only appear before numbers - it can be applied more widely.
For example, we can access the negative form of a variable or another expression by prefixing it with -
:
int Input { 4 };
int Result { -Input }; // Result is -4
Result = -(Input + 2); // Result is -6
So far, we've just been using the simple int
data type for storing our integers, but there are other options. C++ has many different integer types, with names such as short int
and unsigned long int
Why would there be multiple integer types? One reason is to give us options for how much memory we want to allocate for our number.
For example, if our maximum Level
is 50, we don't need to allocate enough memory for that variable to store huge numbers. 8 bits of memory would be enough - that would allow for 256 possible values in that space.
A bit (short for "binary digit") is the smallest unit of data in computing. A bit can store two possible values - 1
or 0
. By combining multiple bits, we can represent more and more complex data.
Two bits can have four possible configurations (00
, 10
, 01
, or 11
). Three bits can have 8, four can have 16 and, in general, each additional bit doubles the number of distinct values that can be represented.
A byte is a combination of 8 bits, for example, 01001110
or 10101001
. There are 256 different possible configurations for 8 bits. Therefore, a byte can store one of 256 possible values.
Those values could represent anything we want - for example:
If we use a variable that has more bits, we can store a wider range of values, at the expense of our software using some additional resources. 16-bit integers can store 65,536 different possibilities; 32 bits could store over 4 billion
The specifics of the different types of integers and their memory usage aren't that important at this stage. The basic int
will have enough bits for our needs. It's just worth noting at this stage that there are many different types of integers. You may see alternatives being used in other places.
Another reason we have different types of integers is to give us more options on how we use the memory. We saw how 32 bits of memory give us access to around 4 billion different possible integers. But what range of integers should we map to those options?
We could use a range from around negative 2 billion to positive 2 billion. But, what if we knew our variable could never be negative? We're wasting half of that range.
A number that cannot be negative is said to be unsigned. We can create an unsigned integer like this:
unsigned int Health { 5 };
This makes the intent of what our variables will contain clearer for anyone reading our code. In addition, our health values can get to be twice as large before our variable runs out of available memory.
Unfortunately, there is a major drawback with unsigned integers. Even for values we don't ever want to be negative, such as a Health
value, using an unsigned number can cause a lot of pain.
Something weird happens when we push a number beyond its range. This is called an overflow and it happens when we decrease a number below its lower limit or increase a number above its upper limit.
With an unsigned integer, its lower limit is 0
. An unsigned int
cannot go below 0
- if our code causes that to happen, we will create an overflow.
Let's imagine we have an unsigned int
that currently has a value of 0
, and we cause an overflow by subtracting 1
from it:
#include <iostream>
using namespace std;
int main() {
unsigned int Health { 0 };
Health -= 1;
cout << Health;
}
The effect is not that Health
remains at 0
. Rather, it wraps around to its other extreme. The previous program’s output is:
4294967295
This seems likely to cause some issues!
Having our variables regularly store values that are at the extremes of their range makes our lives a lot more difficult. We could always be mindful of this and constantly code against overflows, but it's a pain and generally not worth it.
Rather, we should design our software so that our variables are not constantly close to overflowing.
As a result, this means using unsigned integers for any variable we're going to be performing arithmetic on is generally not recommended.
What would the result be of dividing 5
by 2
?
#include <iostream>
using namespace std;
int main(){
cout << "5 / 2 = " << 5 / 2;
}
Perhaps surprisingly, C++ says it is 2
:
5 / 2 = 2
We may have expected it to be 2.5
. However, dividing an integer by an integer always yields an integer.
So why is it not 3
? The specification of the built-in int
data type is to discard any floating point component - not to round it. So, this leaves us with the integer 2
This is true whatever we do with the result of the expression. Above, we are logging it out, but we could also be assigning it to a variable, even a floating point variable:
// IntegerLevel is 2
int IntegerLevel { 5/2 };
// FloatingLevel is 2.0
float FloatingLevel { 5/2 };
Even though we're assigning the result of the expression to a floating point variable that could store 2.5
, the 5/2
expression happens first.
5/2
resolves to 2
, and then we convert 2
to a float
, which yields 2.0
.
After running the following code, what will be the value of Level
?
int Level { 3 / 2 };
After running the following code, what will be the value of Level
?
float Level { 1 / 2 };
In the previous example where we cout
we saw that the division of two integers always yields an integer. If either, or both of the values in the expression were floating point numbers, we would get a floating point output.
All of these examples would log out 2.5
:
cout << 5.0/2;
cout << 5/2.0;
cout << 5.0/2.0;
This is also true for the other basic maths operations. Combining a float and an integer in the same operation yields a float, regardless of whether the float is on the left or the right side of the operator:
5 + 1.0; // 6.0
5.0 + 1; // 6.0
5 - 1.0; // 4.0
5.0 - 1; // 4.0
5 * 2.0; // 10.0
5.0 * 2; // 10.0
5.0 / 2; // 2.5
5 / 2.0; // 2.5
We can create floating point variables in the way you might expect:
float Health { 2.5 };
The float
data type also can be created from an integer
float Health { 5 }; // Health is 5.0
Like with any other variable, we can initialize it with an expression. For example, this can be the result of a maths operation, or the value contained in another variable:
// MaxHealth is 2.5
float MaxHealth { 5.0 / 2.0 };
// CurrentHealth is 2.5
float CurrentHealth { MaxHealth };
Remember, the expression is calculated before the variable type is considered.
In the below example, we perform integer division to yield 5 / 2 = 2
. We then assign the integer 2
to the float Health
, which will convert it to the floating point number 2.0
float Health { 5/2 }; // Health is 2.0
All the operators we've seen for integers are also available to floating point numbers.
float Health { 5.0 }; // Health is 5.0
Health = Health + 20.0; // Health is now 25.0
Health++; // Health is now 26.0
Health--; // Health is now 25.0
// We can freely combine floating and
// integer numbers in the expressions
Health += 25; // Health is now 50.0
Health -= 10; // Health is now 40.0
Health *= 2.5; // Health is now 100.0
Health /= 3; // Health is now 33.3333...
Like with integers, we have different options for how much memory we want our floating point number to consume. Unlike integers, however, floating points are often implemented slightly differently in terms of how they use their memory.
What changes when we give floating point numbers more memory is how precise they are. More bits allow them to store more "significant figures", increasing their accuracy.
The most common data types for floating point numbers are float
and double
. A double
is given the name because it uses double the number of bits as a regular float
. This means a double
is more precise than float
at the expense of consuming more memory.
This is also the source of the phrase "double precision", which you may have heard in the context of science or programming.
The basic float
will be sufficient for our needs. What's important is just to recognize that there are different options, and you may see them being used in other code samples.
We've been using literals quite a lot already, but it's worth taking a moment to explain them in a bit more depth.
When we write an expression like 4.0
or "Goblin Warrior"
, we are using a literal. A literal is a way of expressing a fixed value in our code.
Like variables, literals also have a type. Rather than needing to explicitly state it, the type of a literal is instead inferred based on the specific syntax we use. For example,
6
is an int
literal4.0
is a double
literal4.0f
(with an f
suffix) is a float
literal'a'
(straight single quotes) is a char
literal"Goblin Warrior"
(straight double quotes) is a char*
literal - a type which we’ll cover later in the course"Goblin Warrior"s
(straight double quotes and s
suffix) is a string
literalWe’ve seen in previous code examples that conversions from these literals into other types can often be done automatically We’ve been creating float
objects from double
literals, and string
objects from char*
literals:
// Creating a float from a double literal
float MyNumber { 4.0 };
// Creating a string from a char* literal
string MyString { "Goblin Warrior" };
If preferred, we can add the f
and s
suffix to our floats and strings. The previous code could be written like this:
float MyNumber { 4.0f };
string MyString { "Goblin Warrior"s };
To make our code samples as simple as possible for beginners, we will not include the f
and s
, except in the rare scenarios where doing so makes a difference. In those situations, we will point it out and explain why.
Often, we will be using float and double literals that are initially "round", eg, 4.0
and 5.0
.
Where the floating component is 0
, it can be removed entirely from the literal. This is shown below, where both variables will have the value 4.0
:
float VariableA { 4. };
float VariableB { 4.f };
We don't do this in our code samples, but it's worth mentioning, as this often causes some confusion when looking at other people's code.
In this lesson, we journeyed through the fundamentals of numeric variables in C++, covering key concepts:
float
type for numbers with decimal points.++
, --
) and their significance in simplifying code.+=
, =
, =
, /=
) for efficient value manipulation.In our next lesson, we'll shift our focus to another fundamental aspect of programming: Boolean variables. This upcoming session will include:
An introduction to the different types of numbers in C++, and how we can do basic math operations on them.
Become a software engineer with C++. Starting from the basics, we guide you step by step along the way