Some C++ Basics

Spring 2020

The questions below are due on Tuesday February 09, 2021; 11:59:00 PM.
 
You are not logged in.

If you are a current student, please Log In for full access to the web site.
Note that this link will take you to an external site (https://shimmer.csail.mit.edu) to authenticate, and then you will be redirected back to this page.
Back to Exercise 01

Song: Can't You C?

1) Overview

Since you should have satisfied our pre-requisite of 6.145 or 6.0001, we will assume you have done some Python programming before. We'll be using Python in 6.08 for some things, but we'll also be doing quite a bit of C/C++. For our ESP32 SoC, we'll be working exclusively in C/C++. On this page we'll go over some basics of C/C++.

2) Variables

The big C/C++ variable types that we will be using in 6.08 are:

int x = 5; //used for integers (positive and negative), 
char c = 'S'; //used for single "characters"
float f = 14.6; //also used for decimals (real numbers)
double g = 14.5; //used for decimals (real numbers)
uint32_t i = 123455; //used for large numbers we know will be positive...also called a long on ESP32

The difference between double x; float y; and float is that double uses twice as much memory as float and so allows for much more precision (many more significant digits). The downside is it takes up more memory. We'll talk about that below.

The size of different data types in C++ varies in memory (in terms of how many bytes they take) can vary with the system it is being used on. For the ESP32's Xtensa processor we use in 6.08, those sizes are:

int x = 5; //takes four bytes (meant for holding positive and negative integers)
char c = 'S'; //takes one byte
float f = 14.6; //takes four bytes (32 bits)
double g = 14.5; //takes eight bytes (64 bits) 
long i = 123455; //takes four bytes (same as an int on the ESP32)
unsigned long i = 123455; //takes four bytes, meant for holding unsigned integers (positive only)

The size of these data types becomes very important when we start working with pointers.

One difficulty about variable types in C/C++ is that on different systems, generic names such as int can have different sizes associated with them. On a 16 bit microcontroller, an int may be only 16 bits (two bytes), whereas on a 32 bit microcontroller (like our ESP32), it is 32 bits (four bytes). We'll assume we're working on a 32 bit system in 6.08.

One way to resolve this potential difficulty is to use a different class of variable types shown below (these remove ambiguity since they specify how many bits each variable is):

  • uint8_t: an unsigned 8 bit integer (can represent integers from 0 to 255)
  • int8_t: a signed 8 bit integer (can represent integers from -128 to 127)
  • uint16_t: an unsigned 16 bit integer (can represent integers from 0 to 65535)
  • int16_t: a signed 16 bit integer (can represent integers from -32768 to 32767)
  • uint32_t: an unsigned 32 bit integer (can represent integers from 0 to 4294967295)
  • int32_t: a signed 32 bit integer (can represent integers from -2147483648 to 2147483647)

On the ESP32, our primary embedded system, some of the key types we first listed out are "code" for more specific types in the second list above. For example:

  • An int is the same as a long, which is the same as a int32_t
  • An unsigned long or a unsigned int is the same as a uint32_t

You'll see us probably use a mix of these two variable declaration types throughout the course, so be aware what they mean.

Why use something like a uint8_t instead of just always using a int/int32_t? It seems like you can do anything you need with the int (represent integers from -2147483648 to 2147483647) as you can with a uint8_t (represent integers from 0 to 255). The reason is simply space. If what you need to use a variable for can be fully realized using only one byte (8 bits) representing the nubmers 0 to 255, you're wasting space in memory with a larger variable. There's actually not a ton of that to go around on the ESP32, so being aware of what you'll be using your variables for is really important!

In many cases, choosing int vs. uint8_t won't matter in 6.08, but in many future cases where you're trying to get the most out of a limited resource, wasting 75% of memory allocated could cause lots of problems. So when writing code for the embedded system in 6.08, try to be aware and make efforts to keep things in as small a footprint as possible!

You should now accept that in C/C++ every variable must have a "type" declared upon declaration and what we put into that variable must match its type. If you try to shove a char into a variable that is an int then your code will not compile and you will get an error. The reason for this is that C pre-allocates memory for its variables, and different types of variables take up different amounts of space in memory.

This is in contrast to Python, which is much more flexible in how it handles its variables. For example, in Python, to create four variables storing the above values as in the C case you'd just do:

x = 5
c = 'S'
g = 14.5
f = 14.6

As you can see Python makes a lot more assumptions for us. We don't need to specify that x is storing an "integer". Formally, C/C++ are statically typed languages, whereas Python is dynamically typed. In fact, in Python we could do:

x = 5
x = 'T'

Anarchy! We just changed the variable from an int type to a string type! This would never fly in C/C++. Python is ok with this, and the reason for it comes from how Python handles variables, which is something 6.009 discusses more. (Also, note that single letters/numbers/etc. in Python are strings, not characters! This is a fundamental difference between Python and C. For now, you should not have to worry too much about this point, but keep it in mind.)

3) Arrays and Lists

In addition to variables that contain single values most languages allow groups values to be connected together in an ordered fashion. In fact there are a whole bunch of different ways of doing this, and we'll start with the most basic one that exists in C/C++, the `array`.

In C++ you can create an array of a certain type by doing any of the following:

int height[5];
int temp[] = {32,35,78,56,45};
int temp2[6] = {32,35,78,56,45};
double cash[15] = {13.0,14.0,5.0};

In the examples listed above:

  • We first create a pre-made array of 5 elements in length that is comprised of int data types. Note that we do not need to initialize the values that are in those five slots.

  • We then create an array of type int that is also 5 elements long but specify its length by specifying the elements in it

  • We then create an array of type int that is 6 elements long, but which we only initialize the first five elements of the array.

  • We then finally create an array made up of double data types that we specify will be 15 elements long and then specify the first three.

The usual method used in C++ to declare an array is the following:

data_type Name[size];

One occasionally frustrating thing is that upon declaration of the array, you need to tell either implicitly or explicitly what its size is supposed to be. This is because there really is a block of memory of that size that is reserved for that variable. As you can also see in some of the examples above, you don't need to directly declare the size if it can be inferred from what you are assigning as its contents at the beginning. This is done by leaving the size argument as blank, however, remember that later on if you need to enlarge that array, you can't just tack more onto the end of it 1

You can access and reassign individual elements of an array by doing: int y = height[4]; where we assign the value of of the of the fourth element of the array height to the newly declared int y. You could also do: height[4] = 5;, where assuming height has already been declared, we're assigning its fourth value to be 5.

Arrays in C (and also in Python) are zero-indexed, meaning that the first element of the array is actually accessed by looking up the [0] element. For the sake of consistency in this class we'll call that element the "zeroth" element in this course...so "first" is actually the second element in everyday speak.

Remember, going off of how the variables are declared and defined above, DO NOT write code like int z = height[5];, which will attempt to index into the fifth element of the variable height. This element does not actually exist, but this won't throw a compile error. Remember that when we initialize height as int height[5];, that means it has 5 items inside of it, making the last element actually height[4]. The problem in C is that depending on the context, it won't throw an out-of-bounds error like Python will and will happily report back whatever data is currently sitting in that spot in memory. This will make more sense when we get to pointers.

Arrays can be any data type in in C/C++, including float, double, bool (aka boolean)2 and even other arrays. For example, to create an array of arrays you can do:

int image[5][4];

which creates (though does not initialize individual values inside) an array that is five long, with each element of the array being another array of length 4.

This is often called a multi-dimensional array, since it it could be thought of as a 5 by 4 grid of int values. If you wanted to access one of these values you could do: image[2][3] = 14;, which would assign the third element of the second array (remember zero indexing! We start with the "zeroth" element!) to be a value of 14.

Can we have arrays of the char data type? In traditional C an array of data type char is actually how one represents strings. So you could do the following:

char Lalphabet[] = "abcdefghijklmnopqrstuvwxyz";

which creates an array 27 elements long of all the characters of the lowercase letters of the Roman alphabet. You might be saying, "Wait a second, there are only 26 letters in the Roman alphabet!", and you're right, but in C when you create a character array in the style above, a "null character" ('\0', which is actually one character) is added to the end. So you can access elements of this array as with any other array.

Let's say in the future we decide to get rid of the letter "i" and replace it with the number 7. You could do:Lalphabet[8] = '7'; and life would be good3. Notice that we did not replace the 8th element with the number 7 but rather '7', which is a char. This is subtle but important.

In 6.08 we'll use the so-called C-string representation as much as possible. This is the char array we just spoke about above. It is just about as basic of a "string" representation that you can get in programming. Individual characters located side-by-side in memory in order. Most languages (Python, Java, and even C/C++) have higher-level data structures to make working with the concepts of strings easier, but for the sake of learning we'll spend a lot of time with char arrays.

4) for-loops in C++

Just like Python, C/C++ has for loops and they look like the following:

for (int x = 0; x < 5; x++) {
    Serial.println(x);
}

with a Python equivalent being:

for x in range(0,5):
  print(x)

Both will print out the numbers 0 through 4

As you can see, they look similar. C/C++ is using curly brackets where Python uses indents. C/C++ needs more guidance in setting up its iterator variable x than in Python, and you need to more clearly define what exactly will happen in each run through the loop. A for loop in C/C++ will carry out whatever code is contained within the confines of its body (between its curly braces) until its condition argument is no longer met. In general:

for (initialization, condition, operation) {
  // Lines of code to be executed each time through the loop
}

where the initialization is the creation of some variable which is done before the loop begins, condition is checked at the start of every loop iteration, and the operation is carried out after executing the body.

Answer the following questions about C's' for loops:

for(int x = 0; x < 50; x += 2) {
  Serial.println(x);
}

The += is a shorthand way of increasing x by 2

How many times will the for loop above execute?

Will the for loop above terminate?

5) while Loops in C/C++

There are also while loops in C++ and again their syntax will look very similar to what you know from Python. The code:

int x = 1;
while (x < 4) {
  Serial.println(x);
  x++;
} 

...will run and print 1, then 2, then 3 before finishing.

We sometimes use while loops when waiting on outside events. For example if you wanted to wait for a button push to happen you would do:

while (digitalRead(15)==1) {
  Serial.println("Waiting...");
} 
Serial.println("Button Pushed");

Just be careful with waiting on outside events. This is what we call "blocking" meaning the entire program will stop running while you wait for this thing to complete.

There's also do-while loops in C++ which are similar but different in one key way: They are guaranteed to run at least one time (whereas a standard while loop is not). Their syntax looks like the following:

do{
  Serial.println("Waiting...");
} 
while (digitalRead(15)==1);
Serial.println("Button Pushed");

In the case of the while loop above, if going into this block of code the digitalRead(15) is already 0, the code will never execute. In the case of the do-while loop, if going into this block of code the digitalRead(15) then it will print "Waiting..." once before exiting.

6) if-else in C/C++

Flow control in C/C++ is similar to Python, though with some syntactical differences. If-else is a common flow control structure found in programming languages. It allows selective operations of portions of code based on evaluating conditions. Below are some examples:

int x = 5;
if (x > 4) {
  Serial.println("yay x is big");
}

or an if-else

int x = 5;
if (x > 4) {
  Serial.println("yay x is big");
} else {
  Serial.println("ohhh x is small");
}  

or multiples:

int x = 5;
if (x > 4) {
  Serial.println("yay x is big");
} else if (x <= 2) {
  Serial.println("wow x is very small");
} else {
  Serial.println("x is a mediocre value between 2 and 4");
}

7) Functions

In C/C++, functions are declared like the following (in pseudocode):

type name(arguments) {
  code here
  more code here
 return statement (if type is not void)
}

Just like when you declare variables in C++, for declaring functions you need to specify the type that will be returned (the value that is spat out of a function when called).

An example of a C++ function that takes in two integers and returns their sum would be:

int sum_both(int val1, int val2) {
  return val1 + val2;
}

We could then use this function in code somewhere else by doing:

int q = 5;
int z = 4;
int sum = sum_both(q,z);

You've got to be careful of types though when doing math or merging variables! Unlike in Python 3, which can really help you out, C++ is still subject to the classic coding bug of integer division.

int x = 5;
int y = 4;
int z = x/y;

What will z equal?

Try the following exercise for practice:

Define a C/C++ function int compare(int x, int y) that returns 1 if x is greater than y, 0 if x is equal to y, and -1 if x is less than y.

int compare(int x, int y){ //your code here }

If a function doesn't return anything, its type should be specified as void. We've already seen this void word in our microcontroller code! It is the return type for our setup and loop functions, actually! This should make sense because those two functions don't actually return anything, they just run other pieces of code inside of them.

And remember as you learn C/C++ that you often the best way to really learn a language is to do stuff in it. Pull out your ESP32, plug it in, write your code that you want to mess with, and see what it does! Aren't sure what will happen when you add two numbers together? Try it:

void setup() {
  Serial.begin(115200);
}
 
void loop() {
  int x =5;
  int y = 6;
  int z= x+y;
  Serial.println(z);
}

Want to test some function you're writing? Write some test cases!

void setup() {
  Serial.begin(115200);
}
 
void loop() {
  int x =5;
  int y = 6;
  int z= math_op(x,y);
  Serial.println(z); //hopefully is what I expect!
}

int math_op(int x, int q){
  return x*4 + 6*q + 11;
}

And the Serial Monitor will just keep printing the result forever! Very cool.

67
67
67
...
Back to Exercise 01

 
Footnotes

1There are actually ways to do this using things like malloc or (even better) realloc, but we'll leave them for now since they can be dangerous to use. (click to return to text)

2bool is defined straight-off in C++ and in Arduino-world, but in C proper you need to do a bit more work. (click to return to text)

3in however you may define "good" in whatever hellscape of a future where we no longer have the letter "i" (click to return to text)