Exploring software

Table of Contents

WIP: ChatGPT converted this from my LaTeX document to orgmode so I still need to review this.

1. A brief history of software

A computer needs to be told how to do everything. If you were writing a program totally from scratch, you would have to tell it how to load a bitmap file, how to draw a rectangle, how to detect mouse clicks, how to animate a transition, and everything else.

However, software has been evolving for decades now, and a lot of these features are already implemented in libraries. Libraries are sets of pre-written code meant for other programs to import and use.

With some of the first computers, the only commands you could program in directly mapped to the hardware on the machine (machine code / assembly). Later, developers such as Grace Hopper worked on ways to write code that lets you fit multiple machine-code-pieces into one command that was more human-readable, leading to early languages like COBOL.

Many of these "higher-level" languages (higher-level meaning further from the hardware; more abstracted) eventually will get turned into machine code through a process called compiling.

// Example C++ program
#include <iostream>
using namespace std;

int main()
    cout << "Hello, world!" << endl;
    return 0;

And here is the corresponding assembly code:

// Corresponding assembly code
;5  :	{
0x5555555551a9	endbr64
0x5555555551ad	push   rbp
0x5555555551ae	mov    rbp,rsp
;6  :	    cout << "Hello, world!" << endl;
0x5555555551b1	lea    rsi,[rip+0xe4c]        # 0x555555556004
0x5555555551b8	lea    rdi,[rip+0x2e81]        # 0x555555558040 <std::cout@@GLIBCXX_3.4>
0x5555555551bf	call   0x555555555090 <std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)@plt>
0x5555555551c4	mov    rdx,rax
0x5555555551c7	mov    rax,QWORD PTR [rip+0x2e02]        # 0x555555557fd0
0x5555555551ce	mov    rsi,rax
0x5555555551d1	mov    rdi,rdx
0x5555555551d4	call   0x5555555550a0 <std::ostream::operator<<(std::ostream& (*)(std::ostream&))@plt>
;8  :	    return 0;
0x5555555551d9	mov    eax,0x0
;9  :	}
0x5555555551de	pop    rbp
0x5555555551df	ret

Compiled languages aren't the only variety - Java runs in a Java Virtual Machine, and Python is a scripting language that runs via a Python executable. But we're focusing on C++ here.

The point is, modern software is built on top of many layers: high-level languages that compile down to machine code, pre-written libraries of code that handle common features for the programmers so they don't have to reinvent the wheel.

2. What is a program?

Since a computer needs to be told how to do everything, a computer program is a list of very specific instructions on how to execute some task (or tasks, for larger programs).


Figure 1: Inputs and Outputs

* Inputs: Some programs don't take any *input and just run a set of pre-defined instructions. Most programs do take some form of input, however, whether that's when the program first starts up or during its runtime.

Inputs could include things like passing in a filename to a program (e.g., please open this file) or other pieces of data, or getting keyboard input, gamepad input, mouse input, touch screen input, or receiving signals via the network or internet.

* Outputs: Programs also often will return some form of *output, but this is also optional. If a program doesn't return output, maybe the user just wants to tell the program to do a job, but doesn't need confirmation that the job was done (these are usually called background processes).

Outputs could be something like displaying a message box, changing text on the screen, playing a sound, or writing out a text file.


Figure 2: Variables

* Variables: Our program may also use *variables to store data during its execution. Variables are locations in memory (RAM) where we can store numbers, text, or more complex objects like an image. We give variables a name to reference it by, such as `userName` or `cartTotal`, and we can do math operations on it, as well as write it to the screen or read new values into it from the user.

** Branching and looping:


Figure 3: Branching

We can also change the instructions our program runs based on some condition we set. For example, if `bankBalance < 0` then maybe we display an error message. Otherwise, we can withdraw some `amount` of money from the `bankBalance`.


Figure 4: Looping

We may also want to take advantage of looping to do a set of instructions repeatedly, possibly with some variables changing each time through the loop. An example for this could be to loop over all of a student's `assignmentGrades`, adding up all the points so we can find an average.

2.1. Example programs

** Area calculator:

cout << "Width: ";      // Display message
cin >> width;           // Get user input

cout << "Height: ";     // Display message
cin >> height;          // Get user input

area = width * height;  // Do math

cout << "Area: " << area << endl;

** Recipe program

cout << "How many batches? "    // Display message
cin >> batches;                 // Get user input

flour       = 2.75 * batches;   // Calculate amounts
bakingSoda  = 1 * batches;
butter      = 1 * batches;
whiteSugar  = 1.5 * batches;
eggs        = 1 * batches;
vanilla     = 1 * batches;

cout << flour << " cups flour" << endl;
cout << bakingSoda << " tsp baking soda" << endl;
cout << butter << " cups butter" << endl;
cout << whiteSugar << " cups white sugar" << endl;
cout << eggs << " eggs" << endl;
cout << vanilla << " tsp vanilla" << endl;

** ATM - Withdrawing money:

cout << "Withdraw: ";       // Display message
cin >> withdrawAmount;      // Get user input

if ( withdrawAmount > bankBalance )
    cout << "Error! Not enough money." << endl;
    bankBalance = bankBalance - withdrawAmount;

3. C++ Programs

Each programming language is a little different in how it looks, but whether you're using C++, Java, C#, Python, or many other languages, you'll still encounter branching with if statements, looping, functions, and classes (though older languages may not have classes at all, like C!).

** main(): The starting point

With C++, Java, and C#, programs must begin in a function called `main()`. The compiler (that turns the program into machine code) is always expecting `main()` to be there and to begin running the program at the top of it.

** Basic C++ syntax

In C++, Java, C#, and other C-like languages, there are some basic rules of syntax you should know about:

* Lines of code:

A code statement ends with a semi-colon. Statements are single commands like `cout` (console-out) to display text to the output, `cin` (console-in) to read input from the keyboard, declaring variables, assigning variables, or doing math operations.

* Variable names:

There are some rules for naming variables in C++. Generally, you can use any upper or lower case letters, numbers, and underscores in variable names - no spaces allowed. Beyond that, variables cannot begin with a number, and you cannot use a keyword (such as `if`) as a variable name.

* Code blocks:

There are certain types of instructions that contain additional code. If statements, for example, contain a set of instructions to execute only if its condition evaluates to `true`. Any time an instruction contains other instructions, we use opening and closing curly braces `{}` to contain this internal code. Additionally, with these instructions, they do not end with semicolons.

* Comments:

It is often useful to add comments to programs to specify what is going on in the code. There are two ways to add comments in C++. Generally, you can use any upper or lower case letters, numbers, and underscores in variable names - no spaces allowed. Beyond that, variables cannot begin with a number, and you cannot use a keyword (such as `if`) as a variable name.

  • Whitespace and code cleanliness:

Generally, C++ doesn't care if multiple commands are on one line or several lines. The compiler is going to compile it all either way. You should, however, care about the code's readability to humans. Add enough new lines to separate sections of a program, use consistent indentation, and give variables descriptive names.

* When writing code inside of a code block, you should always tab forward internal code by one tab:

if ( order == "beer" )
    // One tab forward
    cout << "Order: beer" << endl;

    if ( age >= 21 )
        // One more tab forward
        cout << "You get beer!";

4. Diagnosing, testing, and debugging


Figure 5: Debugging

Software is an amorphous, intangible thing of arbitrary complexity. It's bad enough when you're working alone, but once you get other people involved at varying skill levels (and varying levels of writing clean code), the amount of potential issues can quickly skyrocket.

There are techniques to writing software that can help you validate your own logic and check for errors before they occur, as well as tools to help you diagnose and debug issues, as well as error messages to also help give hints to what's going wrong. All of these things are important to becoming a good software developer.

** Synax, Runtime, and Logic errors

* Syntax errors:

The best type of error you can get is a syntax error, even though it may feel bad to get them. Syntax errors are when you have a typo or have otherwise miswritten a statement in the code, and the compiler doesn't know what to do with it. It will then display a build error telling you what it needs. Diagnosing syntax error messages can be confusing at first: often the compiler doesn't know exactly what's wrong, just what it's expecting. But, the good thing about syntax errors is that your program won't run if it can't build - that means you must fix these errors before continuing. It also means that these errors can't just be hidden in the code, like other error types.

* Logic errors:

Another type of error that's much harder to track down are logic errors. Logic errors can be errors in formulas, bad if statement conditions, or other things that don't do what the programmer was intending to do (again, either because of a typo, or not quite understanding the program flow, or for other reasons). Logic errors don't show up in the error list, but can cause your program to crash down the line - or worse, never crash but create bad output. Because it's not crashing, you may assume everything is working fine, but incorrect output can cause problems.

* Runtime errors:

When a logic error causes the program to crash while it's running, it is called a runtime error.

** Debugging

At some point, you will need to debug a program that builds and runs, but something is just wrong. Perhaps it crashes, or perhaps it just doesn't do what you expect it to. We will learn about how to use debugging tools in IDEs like Visual Studio and Code::Blocks later on. Some of the tools included in a debugger are:

  • Breakpoints: You can set these in your code and when the program reaches that part it will pause execution. Then, you can investigate the values of different variables, and also keep stepping forward line-by-line to observe changes and program flow.
  • Watch windows: You type in variable names in these windows and it will show you the variable values at different points in the program (using breakpoints).
  • Call stack: This will show which functions have been called up until where you're paused in the program. This can be handy to see how the program flows between different functions.

* Output here, there, everywhere:

A common, though somewhat unsophisticated debugging technique, is to add output statements at various points in your program to essentially "trace" the program flow - what is getting executed? What is displayed before the program crashes? This technique takes more cleaning up after you're done, and is only so helpful, but it can be a good way to see how your program is running.

** Testing

Programs need to be tested. Starting off, you will probably be manually running the program, entering inputs, and checking outputs. However, this gets tedious after a while and you'll probably start entering gibberish as inputs and assuming everything works if it outputs something. Manual testing is good to do, but it is easy to become sloppy. You can also write code to automatically test certain things for you. This will become more relevant to us once we're working with functions in C++.

The main idea behind testing is that programs or functions will be taking some sort of inputs, and given those inputs you have some expected outputs or results. If the actual outputs don't match the expected outputs, then there is something wrong.

We will cover testing techniques more later on.

** Coding techniques for newbies

When you're first starting with C++, it can feel like your code just never works. It can be good to adopt a few of these practices while you're still new, to minimize errors and frustration:

  • Compile often: Get into the habit of using keyboard shortcuts to build your code after every few lines - especially when you're first starting out. If you write two lines of code and get a syntax error, it's much easier to diagnose than if you write a whole program and then build.
  • Search for syntax error messages: Most of these messages have been posted about before on boards like Stack Overflow. Reading through these can help give you insight into what the error means, and common ways to fix them.
  • Comment things out: If you have a lot of code but can't get it compiling and aren't sure what to do, you can comment out large chunks of the program until it builds again. Then, bit by bit, uncomment out different regions until you find what's causing the build error.

These practices can help you become a more efficient and effective programmer, especially when you're first starting out. Debugging and testing are essential skills for any programmer, and learning them early can save you a lot of time and frustration in the long run.

Author: Rachel Wil Sha Singh

Created: 2023-10-01 Sun 13:22