04: Variables, Values, and Types

UC Irvine - Fall ‘22 - ICS 45C

Quick list of things I want to talk about:

  • Types
    • Sizes
      • Overflow/underflow
      • char is just a small int
  • Declaring variables
    • Static types
  • Constants

Expanded notes:

In previous notes we saw how to compile and run C++ code, and how to do simple I/O in the terminal.

While doing input, we used cin to store values into variables. Let’s dive a little deeper into how we define and use variables.

Types

There are many types of variables in C++. Coming from python, you’re probably used to int, float, str. In C++, however, there are 5 different types just for integers!

Integers

The main difference between each type is the size. Each different integer type is able to store a different range; so you must consider the magnitude of the values you plan to store when deciding on your variables.

The integer types are:

  • char: takes 1 byte
  • short: usually 2 bytes
  • int: usually 4 bytes
  • long: usually 8 bytes
  • long long: usually 8 bytes

Notice that chars are 1 byte long, but the others are usually x bytes. This is because different platforms/compilers might provide different sizes for this. The only guarantee we have is that:
char <= short <= int <= long <= long long

You can use the function sizeof to see how many bytes each type takes using your platform/compiler:

#include <iostream>

using namespace std;

int main() {
  cout << "char: " << sizeof(char) << "byte(s)\n";
  cout << "short: " << sizeof(short) << "byte(s)\n";
  cout << "int: " << sizeof(int) << "byte(s)\n";
  cout << "long: " << sizeof(long) << "byte(s)\n";
  cout << "long long: " << sizeof(long long) << "byte(s)\n";

  return 0;
}

In my computer, I have 1, 2, 4, 8, 8 bytes on the respective types, which is the usual expected for 64-bit Linux/Mac systems. If you’re on 64-bit Windows, you’ll probably get 1, 2, 4, 4, 8. Your results may be different, and that’s fine!

PS: We’re not going to cover it in this class, but there are ways to get the exact size you want: https://en.cppreference.com/w/cpp/types/integer

Floating-point

For floating-point numbers, we have 3 types:

  • float
  • double
  • long double

Similarly to the integers, the difference here is the size of each type. Floating-point numbers are a little harder to support, so the standard says a float is 4 bytes, if supported. So just assume the size order:
float <= double <= long double

If you want to see how many bytes in your platform, you can use a similar code as above.

Other types

  • bool: stores true (1) or false (0);

Declaring Variables

We’ve been declaring variables already. Whenever you have a statement like int x;, that creates a variable you can use later.

If you want to declare many variables, you can do it one per line:

int x;
int y;
float z;

If you have many variables of the same type, you can combine their declarations in the same line:

int x, y;
float z;

Assigning values

We’ve seen how to read input and store it into a variable:

int x;
cin >> x;

But you can also assign numerical values:

int x, y;
x = 1;
y = x;
// would be the same as y = x = 1;

And use math operators:

int x;
x = (1 + 1) * 3;

You can also assign initial values directly in the declaration:

int x=1, y=2;

Type mismatch

Once you create a variable of a certain type, you can’t change it. So, for example, if you have an int variable x and later want to change it to a float, you’ll need a new variable.

When assigning values to different types, the compiler might give you warnings.
For example, when compiling this code:

#include <iostream>

using namespace std;

int main() {
  int x = 2.5;
  cout << x;
  return 0;
}

You should get an error like this:

test-type.cpp:6:11: error: implicit conversion from 'double' to 'int' changes value from 2.5 to 2 [-Werror,-Wliteral-conversion]
  int x = 2.5;
      ~   ^~~
1 error generated.

If you want to change a certain value to a different type, you can do type-casting the way we did in the overflow example. You would put (NEW_TYPE) before the value/variable you want to cast.

For example, we can do it like this:

#include <iostream>

using namespace std;

int main() {
  int x = (int) 2.5;
  cout << x; // should output "2"
  return 0;
}

This is particularly important if you’re dividing two integers but want a float result:

#include <iostream>

using namespace std;

int main() {
  cout << "2 / 3 -> " << 2 / 3 << "\n";
  cout << "(float) 2 / 3 -> " << (float) 2 / 3 << "\n";
  return 0;
}

This should give you the following output:

2 / 3 -> 0
(float) 2 / 3 -> 0.666667

Since 2 and 3 are both ints, C++ will also make the result an int. So this ends up being a floor division (i.e., it will truncate/round down the result). If you want the float result, you should cast one of the operands. The same would happen if we had variables instead of 2 and/or 3.

Finally, it’s important to keep in mind the size of the types you’re trying to convert. It’s generally safe going up in size (e.g., from short to int), but you might get weird values if you go in the other direction.
For example, if you run the following code:

#include <iostream>

using namespace std;

int main() {
  int x = 100000;
  short y;

  y = x;
  cout << "x = " << x << "\ny = " << y << '\n';

  return 0;
}

The value of y won’t be the same as x, since 100,000 is above the limit for short.

Constants

Lastly, we can create constants in C++. If there’s a variable that you don’t want to be changed, you can use the const keyword to make it like that.

For example, you could define pi:

const float pi = 3.1415;

If somewhere in the code you try to overwrite that value, the compiler would give you an error and would not allow it. Constants need to be initialized during declaration.

Integer Ranges

Okay, so we have different sizes of variables… but what does that mean?

It means that we’ve limited space to store numbers. Each variable has a number of bytes available, and each byte is commonly 8 bits long.

For floating-points, this usually changes the precision you can have (how many decimal places of accuracy). For integers, it changes the absolute minimum and maximum values that can be stored. Let’s talk about the integer limitations.

When we want to store a number, we convert it to binary form and store each 1/0 in one of the bits. Figure 1 below shows some numbers as decimal and binary:

Decimal and binary numbers comparison

In the example above, we can see that on the binary column, we have 4 bits, going from 0000 to 1111, and the corresponding decimal values go from 0 to 15.

This is the range of 4 bits! Because you only have 4 positions, you’ll have the least significant bit (i.e., first bit on the right) representing $2^0$ and the most significant bit (i.e., first bit on the left) representing $2^3$. So the maximum value we can make is $2^3 + 2^2 + 2^1 + 2^0$, which is 15, exactly like the table. That sum, $2^3 + 2^2 + 2^1 + 2^0$, is the same as $2^4 - 1$!

So, for example, if you have 8 bits for positive numbers, you can store from 0 to 255 ($2^8-1$).

Signed vs unsigned

That example is simple enough, but what about negative numbers?

If we want to store both positive and negative numbers, we use half of the range for negative and half for positive. This is usually done by making the most significant bit (i.e., first one on the left) negative.

So if you have 8 bits, the first one means -$2^7$, while the other ones mean $2^6, 2^5, … 2^0$. Considering that, using 8 bits, we could store from -128 (-$2^7$) to 127 ($2^7 - 1$).

In C++, the default is to have signed numbers. If you want some unsigned value, for example, the number of people in a room, you can use the keyword unsigned in front of the type.

So we have:

  • unsigned char;
  • unsigned short;
  • unsigned int;
  • unsigned long;
  • unsigned long long.

These types have the same size as their signed counterparts.

Usual ranges

Assuming the same bytes we saw on the previous list, 1, 2, 4, 8, 8, these would be the expected ranges:

  • char: from -128 to 127 (unsigned, 0 to 255);
  • short: from -32,768 to 32,767 (unsigned, 0 to 65,535);
  • int: from -2,147,483,648 to 2,147,483,647 (unsigned, 0 to 4,294,967,295);
  • long: from -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807 (unsigned, 0 to 18,446,744,073,709,551,615);
  • long long: same as long above.

Over/underflow

Since we have limits on the values each type of variable can hold, what happens if we go above/below the allowed range?

Let’s look at an example:

#include <iostream>

using namespace std;

int main() {
  char x = 127; // 2^7 - 1; max value for signed char.
  unsigned char y = 255; // 2^8 - 1; max value for unsigned char.

  cout << "original values:\n";
  cout << "x=" << (short) x << ", y=" << (short) y << "\n\n";

  // Overflow: going above the top range
  x++;
  y++;
  cout << "after increment:\n";
  cout << "x=" << (short) x << ", y=" << (short) y << "\n\n";

  // Underflow: going below the minimum range
  x--;
  y--;
  cout << "after decrement:\n";
  cout << "x=" << (short) x << ", y=" << (short) y << "\n\n";

  return 0;
}

This should output:

original values:
x=127, y=255

after increment:
x=-128, y=0

after decrement:
x=127, y=255

Note when we try to increase the maximum value, we go to the lowest value of the range. From 127 to -128, and from 255 to 0. This is called an overflow.

Similarly, when we try to decrease the lowest value, we end up with the highest one. From -128 to 127, and from 0 to 255. This is called an underflow.

Overflows and underflows happen because we are going over/under the limit of that type’s range.

A few things to highlight on that code:

  • we use (short) in the couts because otherwise, C++ would try to find the character for that value since it’s a char (remember ASCII/Unicode?). We actually want to see the number, so we tell the compiler to convert it to a short.
  • x++ increments the value of x by one and returns the original value. So if you did int x=1; cout << x++; you’d see 1 printed out, but the value of x is now 2. There is also a ++x operator, which increments before return, so int x=1; cout << ++x; prints 2 and x also has 2.
  • x--; does the same as x++; but in the opposite direction, it decrements the variable. There is also --x;.

References