DEV Community

Cover image for What i Learn from basic C++ Console Project
UPinar
UPinar

Posted on

What i Learn from basic C++ Console Project

Tombala

This will be a blog about what I learn during this project. First of all, this project is a console application and its written in C++. In the beginning it asks you how many bingo cards do you want and randomly creates cards which have 15 numbers in it and all the numbers are from 1 to 90. Every column, what I mean by column is, from 1 to 10 it is the 1st, from 11 – 19 the 2nd and from 81 – 90 the 9th, can have max 2 numbers. There are also 3 rows in a card and every row needs to have 5 numbers in it. After all cards have been randomly created, game starts. From 1 to 90 we put all numbers in a bag then we started choosing randomly and delete those numbers from cards which have those. The numbers we chose and deleted from the cards, were thrown away. First card wins 1st Cinko needs to remove all the numbers in 1 row before every other card does and the first card wins 2nd Cinko needs to remove all numbers in 2 row before others finally when a card’s all numbers were picked, that card wins the Tombala.

gameStart

gameEnd

Lets start!


Using “using” keyword to declare a type

using basicCardType = std::array<int, constants::numberCountInCard>;

using advancedCardType = std::array <std::array<int, constants::columnCountCard>, constants::rowCountCard>;

using bingoDeckType = std::array <std::array<int, constants::columnCountBingo>, constants::rowCountBingo>; 

using winnerType = std::array<int, constants::winnerCount>;

Enter fullscreen mode Exit fullscreen mode

These are the types in project that i used a lot times. For saving time and for the sake of readability it is better using “using” keyword to declare a type.

Let’s look at basicCardType code above.
It's type is std::array<int, constants::numberCountInCard>.
The value of constants::numberCountInCard is 15 so it is an integer array stores 15 integers.

basicCardType createBasicCard(){}

  • We can set that type as a return type from the function.

basicCardType sortBasicCardNumbers(basicCardType basicCard) {}

  • We can use that type as a parameter in function and send it to function as an argument.

Adding optional parameters to function

void printBingoDeck(bingoDeckType bingoDeck, int bingoNumber = 0, bool gameStarted = false)


Enter fullscreen mode Exit fullscreen mode

In the printBingoDeck() function above we use 3 parameters.

  1. bingoDeck
  2. bingoNumber
  3. gameStarted

Normally we don’t assign a value to parameter in function like int bingoNumber = 0. This means, if there is no value send as an argument to bingoNumber set its value to 0, and it is same in gameStarted = false too. If there is no value used in gameStarted argument it takes value false.
Lets check the code inside of the function.

     if (!gameStarted)
           std::cout << "Bingo game is ready!" << '\n' << '\n';
     else
           std::cout << "Bingo number is: " << bingoNumber << '\n' << '\n';

Enter fullscreen mode Exit fullscreen mode

If gameStarted did not send to function as an argument, function will set its value to false and get inside of an if statement.

printBingoDeck(mainBingoDeck);

printBingoDeck(mainBingoDeck, randomNumber, gameStarted);

Both function calls above are possible because the last 2 arguments are optional we do not need to use them as arguments.

There is a different way to use optional parameters in function. Which is std::optional. There is a link in references about std::optional for more information.


Understanding std::vector and std::vector::iterator

Vectors are basically dynamic arrays which can be updated in runtime. In arrays it is not possible to change the size, after size has been set but in vectors you can change size by adding a new value or remove some of them in runtime.

In the beginning of this blog, we mentioned that, every column in card can have maximum 2 numbers in it. If we have [21,27] in 3rd column, we can not have [21,24,27] in there. So while we are creating our cards from randomly selected numbers, we need to eliminate numbers between 20 to 29 if we already have picked 2 numbers from that column. That’s where using std::vectors becomes handy. Lets check how we actually use them.

int getIndexValueFromVector(std::vector<int> numberVector, int startingNumber)
{
    std::vector<int>::iterator it;
    it = std::find(numberVector.begin(), numberVector.end(), startingValue);
    return static_cast<int>(it - numberVector.begin());
}
Enter fullscreen mode Exit fullscreen mode

The function above returns the index value of the number that we need to start the removing process. For example if we need to remove 40 to 49, function will return us the index of value 40 from the values inside of that vector.

Before we get into that function lets understand std::vector::iterator
Iterators job is basically starting from the beginning of the vector and going to the end to find the value and return the index of that value. Iterator starts from the vector[0](index = 0) and goes to the vector[last + 1](index = last + 1).

But why it ends in index[last + 1] ?

std::vector<int> myVector{ 11,12,13,14,15,16 };
myVector[0] = 11 -> iterator starts from here.
myVector[5] = 16 -> iterator has not come to an end yet because there is still a value in 5th index.

Lets check the type of the value in 6th index.

typeid(myVector[6].name()) = not initialized int

when iterator sees not initialized value, it understands that it comes to the end of the iteration and gets the value of index[last + 1] as its value. 

So when we get to the end of the iteration process, our iterator = 6 which is the same as iterator = myVector.end()

It is important to remember when we don’t initialize an integer, it takes a value of 0 !

std::cout << myVector[6]; will print 0.
so myVector[6] = 0. myVector's 6th index, the value 0 is iterators end but it is not iterators end in yourVector below

std::vector yourVector {11,12,13,14,15,16,0,17}

6th index in yourVector is also equals to 0 and std::cout << yourVector[6]; will also print 0 but it is not the end of iterator when we iterate in yourVector.

Lets go back to myVector. myVector.begin() will return 0, myVector.end() will return 6(last+1) and the index of 14 is 3 (myVector[3] = 14) when we are iterating in myVector.

Now that we understand iterators beginning and end values, we can talk about
std::find(InputIterator first, InputIterator last, const T& val)

If we return getIndexValueFromVector() function above we saw,

std::vector<int>::iterator it; //iterator created
it = std::find(numberVector.begin(), numberVector.end(), startingNumber);
return static_cast<int>(it - numberVector.begin());

Enter fullscreen mode Exit fullscreen mode

Applying std::find() function to the myVector to check value 14, we will get it = 3. Because of it is not letting us directly return the value of it, we will return it – myVector().begin

for (int k = 0; k < 10; k++)
{
    allNumbersInDeck.erase(allNumbersInDeck.begin() + startingIndex);
}
Enter fullscreen mode Exit fullscreen mode

We can use erase() method to remove same index for 10 times. For example we want to remove values from 20 to 29. At first it will remove the value 20, then the same index's value will become 21 because of 20 is deleted.
allNumbersInDeck are also a vector.

std::vector<int> allNumbersInDeck{};
allNumbersInDeck.reserve(constants::totalBingoDeckNumbers);

for (int i = 0; i < constants::totalBingoDeckNumbers; i++)
{
    allNumbersInDeck.push_back(i + 1);
}
Enter fullscreen mode Exit fullscreen mode

Another function is reserve(), if we know the size of vector it is a good practice to reserve the memory before assigning values for a better performance.

push_back(value) function adds a new value at the end of the vector.

int getIndexValueFromVector(std::vector<int> numberVector, int startingNumber)
{
    for (int i = 0; i < numberVector.size(); i++)
    {
        if (numberVector[i] == startingNumber)
            return i;
    }   
}
Enter fullscreen mode Exit fullscreen mode

The way that we get Index value above, instead of using iterator is, i think more cheaper but it is not bad learning it.


Using reference parameters in functions

int firstFunction(int parameter)
{   
  parameter += 1;
  return parameter;
}
int secondFunction(int& referenceParameter)
{
    referenceParameter += 2;
    return referenceParameter;
}
int thirdFunction(const int& constReferenceParameter)
{
    constReferenceParameter += 3;
    return constReferenceParameter;
}
int main()
{
    int firstVar{ 1 };
    int secondVar{ 1 };
    int thirdVar{ 1 };

    firstFunction(firstVar);
    std::cout << "After first Function : " << firstVar << '\n';

    secondFunction(secondVar);
    std::cout << "After second Function : " << secondVar << '\n';

    thirdFunction(thirdVar);
    std::cout << "After second Function : " << thirdVar << '\n';
}

Enter fullscreen mode Exit fullscreen mode

We will read the code snippet one by one.

Lets check the firstFunction(). It has an integer parameter. When we send firstVar to the firstFunction() our parameter will take the value of firstVar. Parameter will create another integer in memory and initialize it with the value of firstVar. Inside the function we add 1 to the new integer that we created and we return the value of new integer that we created inside firstFunction(). After function ends and when we turn back to main() function the new integer that has been created inside firstFunction() will destroy itself. Now in stack we only have 1 integer which is the first variable (firstVar) that we create inside of main().

So what will firstVar will print and does it changed after firstFunction() executed?

firstFunction() returned a value of (firstVar + 1) but we did not assign it to firstVar.So its value does not changed after firstFunction() executed. Basically we copy firstVar and used its value inside firstFunction() as another integers value and we return the value of (firstVar + 1).

Lets check the secondFunction. It has an integer reference parameter. We sent secondVar as an argument to secondFunction(), if we check the function parameter it is int& referenceParameter.

Lets check what happens under the hood and look at assembly code.

Assembly Code in GodBolt 

ASM Code is compiled by x86-64 clang 15.0.0

main:                                   # @main
    1   push    rbp
    2   mov     rbp, rsp
    3   sub     rsp, 16
    4   mov     dword ptr [rbp - 4], 1
    5   lea     rdi, [rbp - 4]
    6   call    secondFunction(int&)
    7   mov     dword ptr [rbp - 8], eax
    8   xor     eax, eax
    9   add     rsp, 16
    10  pop     rbp
    11  ret
Enter fullscreen mode Exit fullscreen mode

main:

line 4 – move decimal 1 to the and store it inside of 4 byte memory which is DWORD PTR [rbp - 4]

line 5 – move(lea acts like mov) value inside the memory [rbp - 4] which is decimal 1 to rdi(64bit register that will carry value to secondFunction()).

line 6 – calls the function secondFunction(int&)

secondFunction(int&):                   # @secondFunction(int&)
     1   push    rbp
     2   mov     rbp, rsp
     3   mov     qword ptr [rbp - 8], rdi
     4   mov     rax, qword ptr [rbp - 8]
     5   mov     ecx, dword ptr [rax]
     6   add     ecx, 2
     7   mov     dword ptr [rax], ecx
     8   mov     rax, qword ptr [rbp - 8]
     9   mov     eax, dword ptr [rax]
     10  pop     rbp
     11  ret
Enter fullscreen mode Exit fullscreen mode

secondFunction(int&):

line 3 - we get the firstVar’s memory address and value from [rdi] register comes from main() function and store it inside QWORD PTR [rbp - 8]. Normally integers are stored in 4 byte size but in here because of we use reference parameter (in ASM reference behaves and have a 8 byte memory like pointers) So the value inside [rdi] will store in QWORD PTR [rbp – 8].

  • QWORD PTR[rbp – 8] inside secondFunction(int&)'s adress is now equals to firstVar's adress.

line 4 & 5 – these lines used same way in line 8 & 9. Because of we are using reference parameter and store it in 8 bytes of memory like in line 3 QWORD PTR[rbp – 8], when we want to move it we need to store it in 64 bit(8 byte) register which is [rax] as we see in line 4. We set QWORD PTR [rbp -8]'s adress to [rax] registers adress. 

mov [rax], QWORD PTR [rbp -8] 

but for moving the value of integer we need use 32 bit register because integer uses 4 bytes of memory but the size of [rax] is 64 bit. Then we use [ecx] which is a 32 bit register. It is also important to mention that we are moving 4 bytes of [rax] register. 

mov [ecx], DWORD PTR [rax]

If we use QWORD PTR [rax], it will move the value inside of an 8 byte memory but because we move integer type, which is 4 byte, using DWORD and sending half of the [rax] registers memory, still can hold the value of integer.

  • DWORD PTR [rax] and [ecx], they both have the same values which are also equals to the value of firstVar inside main() function.

line 6 – we add 2 to the value of [ecx] which is equals to decimal 1 like firstVar in main() function. Now [ecx]'s value becomes decimal 3. Because of [ecx] and firstVar have the same memory adresses now firstVar's value is equal to decimal 3.

line 7 – we take the new value of [ecx] which is 3 and move it to the value of DWORD PTR [rax]. The value of DWORD PTR [rax] is now also 3.

line 8&9 - in line 8&9 we will think more like firstVar.

mov [rax], QWORD PTR [rbp -8] 

because of QWORD PTR [rbp -8] stores the adress of firstVar, we now move the adress of firstVar to [rax] register.

mov [eax], DWORD PTR [rax]

because of now [rax] register has an adress of firstVar, the value inside firstVar adress is 3 and because we move that value to [eax], [eax] register is now also have a value of decimal 3.

main:

line 7 - DWORD PTR [rbp - 8], because of we get out of secondFunction(int&) now our rbp(base pointer register)'s adress is different too..[rbp - 8] in secondFunction(int&) and [rbp - 8] in main() function are not the same adresses. In the last line of secondFunction(int&) we set [eax] a value of decimal 3. Now in line7 in main() we will assign its value to DWORD PTR [rbp - 8] which is 4 byte integer.

What happened ?

Our firstVar's (int argument) value changed when we use it as reference parameter and change its value inside the function secondFunction(int&).

 
Lets check the third function. Because of we use const we can not compile this program it will throw error. Constant values can not be changed.

 `constReferenceParameter += 3;` // will throw error.
Enter fullscreen mode Exit fullscreen mode

Using srand() and rand()

rand() function creates series of numbers between 0 – 0x7FFF which is 524287. But every time that we run the program again we will get same values.

for (int i = 0; i < 5; i++)
{
     std::cout << rand() << '\n';
}
Enter fullscreen mode Exit fullscreen mode

Console output will always be -> 41 18467 6334 26500 19169

In this case srand() will show himself. When we seed srand() function with a value it changes random values created.

srand(5);
for (int i = 0; i < 5; i++)
{
    std::cout << rand() << ' ';
}
Enter fullscreen mode Exit fullscreen mode

But in this time because of its seed value is never changing.
Console output is always -> 54 28693 12255 24449 27660.
So we need to seed that srand() function with a value that always changes. Time for example, is a good way to seed.

Lastly, to use it in algorithm we just need to take outputs modulus.

srand(time(0));
for (int i = 0; i < 5; i++)
{
    // we need values from 0 to 90
    std::cout << rand() % 91 << ' ';
}
Enter fullscreen mode Exit fullscreen mode

We seed srand() with time(0) then take MOD91 of the values so we get values from 0 to 90 and we have a random function. This is not the safest and secure way to use random number generator but it works well in this project.

That's it.

Here is the Source Code of the project
Tombala Project Source Code

Useful Links

Top comments (0)