Table of Contents

Background

  • C++ started out as “C with Classes” but has grown to be much more
    • C translates very naturally into assembly - Terry Davis
  • Both C and C++ are considered by some to be middle-level languages because they support both low-level and high-level features.
    • It can be (sometimes) as easy to program as Java (thanks to the classes (high-level)), but it also provides the ability to interact directly with memory (support pointer arithmetic (low-level))
    • C/C++ own memory management leads to more efficient programs but also more bug prone, but you can use high-level constructs in C++ that abstract memory management for you if you don’t want to deal with them yourself
  • Both C and C++ language program are compiled into machine code, but are machine independent (a feature of high-level). However they have to be compiled differently for each architecture and OS, which sometimes lead to one code not to be compatible with another platform. Java is always machine indendent because it’s run on a JVM.
  • C and C++ focus on performance, efficiency and flexibility
    • Zero Overhead Abstractions. There’s no automatic memory management (which for some qualifies them to be considered low-level languages)

Comparing C to x86 assembly language (AT&T syntax)

cat fib.c # display the contents of fib.c in the terminal
#include <stdio.h>

int main(void) {
  int x, y, z;

  while(1){
    x = 0;
    y = 1;
    do {
      printf("%d\n",x);

      z = x + y;
      x = y;
      y = z;
    } while (x < 255);
  }
}
gcc -o fib  fib.c # compile to binary
objdump -d fib # translate (dissasemble) to human readable assembly language
# Removed the hex values and added comments
0000000000001149 <main>:
  1149:	endbr64

  114d:	push   %rbp             # push the base pointer (to rsp register aka stack pointer)
  # The base pointer is used to mark the start of a function's stack frame (our starting address of the subroutine)

  114e:	mov    %rsp,%rbp        # set stack pointer (top of the stack address) equal to the start of the subroutine (base pointer)
  
  # This is "aligning the stack", we start with the top of the stack starting at the beginning of the function main, and the stack pointer increases as things are being pushed (to larger memory addresses)
  # rsp minus rbp is the amount of memory that the subroutine is using

  # The two above instructions are standard for all "subroutines" (aka functions/methods) and it's called the "prologue.

  # In the event that the subroutine plans to use extra registers (i.e. to hold values closer to the CPU to make the program run faster), we need to make sure to restore them later in the epilogue (as another routine might be relying on them after the current one ends), a way to do it is to push them right after having aligned the stack with pushq rbp, movq rsp rbp; and once the subroutine is done and the stack re-aligned to where it was then pop them back in the same order
  #..n:	pushq  %RegisterA       # temporaily store register A contents
  #n+1:	pushq  %RegisterB       # temporaily store register B contents

  1151:	sub    $0x10,%rsp       # C stuff (also part of preparing the stack frame)

  # actual start of the fibonacci program
  1155:	movl   $0x0,-0xc(%rbp)  # x = 0 (offsetted addres 0xc = 0)
  115c:	movl   $0x1,-0x8(%rbp)  # y = 1 (offsetted addres 0x8 = 1)

  # calling printf (passing the 2nd last fibonacci number (x) to be printed)
  1163:	mov    -0xc(%rbp),%eax  # passing x to printf input register?
  1166:	mov    %eax,%esi        #
  1168:	lea    0xe95(%rip),%rdi # 2004 <_IO_stdin_used+0x4>
  # I think at this point printf already has the input available to fetch
  116f:	mov    $0x0,%eax        # resetting the %eax register?
  1174:	callq  1050 <printf@plt># calling printf 
  
  1179:	mov    -0xc(%rbp),%edx  # moving x to sum operand A
  117c:	mov    -0x8(%rbp),%eax  # moving y to sum operand B
  117f:	add    %edx,%eax        # summing A and B, saved at A
  1181:	mov    %eax,-0x4(%rbp)  # z = x + y (0x4 = x + y)
  1184:	mov    -0x8(%rbp),%eax  # temporarily move y to eax
  1187:	mov    %eax,-0xc(%rbp)  # x = y
  118a:	mov    -0x4(%rbp),%eax  # temporarily save z to eax
  118d:	mov    %eax,-0x8(%rbp)  # y = z
  1190:	cmpl   $0xfe,-0xc(%rbp) # (compare long) sets "less than or equal" flag if (x <= 254)
  # it's funny because the compiler could have just otherwise do what was written in C:
    #   cmpl   $0xff,-0xc(%rbp) # (x < 255) 
    #   jl     1163
  1197:	jle    1163 <main+0x1a> # jumps to beginning of the loop if jle flag is set
  1199:	jmp    1155 <main+0xc>  # jumps to beginning of the subroutine (starts fibonacci from 0)

  119b:	nopl   0x0(%rax,%rax,1) # C stuff

  # eventually somewhere in an implicit C subroutine there's an epilogue that
  
  # a) restores the stack frame (this is not exactly how the C fibonacci program actually did it):
  #...: movq   %rbp,rsp       # moves the base pointer (beginning of the subroutine) to the stack pointer
  # Which means, in this context where we're leaving the program, that anything after (larger memory address) the stack pointer, is garbage (and thus free real estate for next programs)
  #...:	popq    %rbp          # restore base pointer location (back to where we started the program)

  # b) restores any registers it previously used which might be used by other subroutines
  #..j:	popq  %RegisterA      # restore register A
  #j+1:	popq  %RegisterB      # restore register B

Why C++ for computer graphics

  • Need low-level support to communicate with the GPU hardware
  • Many algorithms are computationally expensive (slow in C/C++, let alone in python…)
    • Python is the slowest because it is interpreted
      • The code is executed line by line: each line is converted to CPU instructions and executed
    • Java is faster than Python but it has a higher overhead at the beginning because it’s compiled to bytecode and then run in a virtual machine
    • C++ is the fastest because (like C) it’s compiled into native instructions
      • It must be recompiled for each different CPU architecture and operating system
  • High-level programming support (such as object oriented programming) makes it easier to work with than C
    • C++ is mostly a superset of C which makes it almost as easy to work with as Java

Hello world

  • Java
class HelloWorld {
  public static void main (String[] args) {
    System.out.println("Hello, world!");
  }
}
  • C++
#include<iostream> //equivalent of import java.Lang.system

int main() { //free standing functions are not part of any class (in java everything must belong to a class)
  std::cout << "Hello, world!" << std::endl;
  return 0;
}
  • std::cout:
    • Unlike printf() which comes from C (which btw can be inserted into C++ just like Java can be inserted in Scala) and makes you look up the syntax for the variable type i.e. %d to insert an integer variable, std::cout handles all types for you
    • It’s easier to mantain (you don’t need to change code in multiple places)
    • The cout API abstracts the output to the terminal such that it’s a “stream” and can be easily fed with multiple inputs (all these stream “inputs” are actually char arrays and their pointers are added together)
printf("Error %d: %s.\n", id, errors[id]);
std::cout << "Error " << id << ": " << errors[id] << "." << std::endl;
  • Operator << nexto std::out pushes the values on the right to the output stream
  • endl just pushes a line break to the output stream
  • return 0: 0 means success (thus no error). I think in C/C++ main must return a value (0 is success, -1 is error and 1 appears when it’s aborted by the user)

Syntax, types, namespaces, libraries

  • C++ syntax is very similar to Java (it influenced it)
  • The basic datatypes are:
    • int
    • char
    • float
    • bool
    • void
  • Namespaces are used by the libraries to ensure that there are no naming conflicts
namespace foo {
  int var = 50;
}

namespace bar {
  int var = 42;
}
  • foo::var is 50 and bar::var is 42

  • The standard C++ library is a collection of common functions and classes (e.g std::cout, std::sin…)
    • Stored across manyfiles (i.e <iostream>) all using the std namespace
  • C++ builds on top of C, which has its own standard library
    • Files that end with .h (i.e. <math.h>) are C files
    • It’s better to use the C++ version, which does not have an .h (i.e. <cmath>)
  • When using cmath libary, prefer the functions without an f postfix
    • std::sqrt(42.0f) instead of std::sqrtf(42.0f)

Containers

  • In java a container is a (abstract) class that serves as a general purpose holder of other component objects (primitives or other objects)
    • List
    • Stack
    • Queue
  • C++ has many, of which the first 2 are enough for CSE2215 Computer Graphics
    • std::vector (#include )
    • std::array (#include )
    • std::deque (#include )
    • std::forward_list (#include )
    • std::list (#include )
    • std::stack (#include )
    • std::queue (#include )
    • std::priority_queue (#include )
    • std::set (#include )
    • std::unordered_set (#include )
  • Vector is a child of array (in terms of Liskov substitution principle), so techinically only with vector you can do all CSE2215 assignments.
    • It’s not a linear algebra vector but an automatically enlarged/shrinken array (like a java ArrayList)
  • You can find a list of containers hhere]
  • Full documentation of the Standard Library can be found here]

std::array (#include <array>)

  • Size must be known at compile time (fixed size)
  • No memory allocations required (fast)
  • May cause stack overflow when you create a very large array
std::array<int, 42> myArray; //stores 42 integers
std::array<std::string, 8> myArray; //stores 8 std::strings
std::array<MyClass, 4> myArray; //Stores 4 MyClass instances

std::vector (#include <vector>)

  • Size can be changed dynamically
  • Allocates memory under the hood
std::vector<int> myVector;
myVector.push_back(32) //Add the number 42 to the end of the vector
std::cout << "Item 0 has value " << myVector[0] << std::endl;

For each loops (range-based)

  • Same syntax as java
std::array numbers {42, 58, 4, 7};

for (int n : numbers) {
  std::cout << item << std::endl;
}

Classes

  • Classes are implemented in two steps

Class declaration (contract)

  • my_class.h may just declare the class (i.e. to inform for the compiler and the IDE which attributes and methods are available under that class)
  • Then the actual implementation of the attributes values and method bodies are defined in my_class.cpp
  • See that unlike java, c++ does not use CamelCase for the class files, but does for the ClassNames inside the files themselves
  • We use headerfiles because before we can start using a variable or a function, this one must be first at least declared (such that the compiler, which goes line by line, already knows the memory location). Having header files that are being included (literally copy pasted) at the top of cpp files, allows us to already make use of them in the early lines of code of the .cpp file, while leaving the actual implementation of those functions to later lines, such that the order in which functions are used does not require us to implement them (assign) in advance.
cat my_class.h # header file
class MyClass {
  public:
    void setValue(int desiredValue)
    int getValue();
  
  private:
    int value;
};
  • Like java you can see that the setter and getter methods are public, while the attributes are private
  • Don’t forget the semi-colon after the curly braces of the class in the header file!

Class definition (implementation)

cat my_class.cpp # implementation file
void MyClass::setValue(int desiredValue){
  value = desiredValue;
}

int MyClass::getValue(){
  return value;
}

Class declaration & definition (Java style)

  • Like in Java, you may declare and define the class in the same file (my_class.h)
cat my_class.h
class MyClass {
  public:
    inline void setValue(int desiredValue){
      value = desiredValue;
    }
    inline int getValue(){
      return value;
    }
  
  private:
    int value;
};
  • We add inline keyword before every function for pretty console debugging (I think)

Structs

  • Carried over from C
  • The same as classes except everything is public by default
  • The good practice is to use them when you have an “object” that just contains data (i.e. 2D point)
#include <iostream>

struct Point {
  int,x y;
};

int main() {
  Point p {10, 20};
  std::cout << "x = " << p.x << std::endl;
  std::cout << "y = " << p.y << std::endl;
};

vs

#include <iostream>

class Point {
  public:
    int x,y;
};

int main() {
  Point p {10, 20};
  std::cout << "x = " << p.x << std::endl;
  std::cout << "y = " << p.y << std::endl;
};

Constructors

  • Just like in java, a class may include or not a constructor (if it doesn’t then it gets initalized with all the declared undefined attributes as null/0?)
class MyClass{
  public:
    inline MyClass(int v) {
      value = v;
    }
    inline getValue() {
      return value;
    }
  
  private:
    int value;
};
  • Constructor is run when class is created, for which you don’t use the keyword new (it means something different than in Java
  • C++ have many ways of initializing an object and they all do (roughly) the same
MyClass foo = MyClass(42);
MyClass foo = MyClass{ 42 };
MyClass foo { 42 };
MyClass foo = { 42 };

Assignment operator is pass by value

  • When copying two variables x = y x actually copies the contents at y’s address location, not the address itself like java for non primitives (reference).

  • C++:

struct Point {
  int x, y;
};

int main() {
  Point p1 = Point { 100, 100 }; //a way to construct x,y without needing a constructor (they are assigned based on their implicit order)
  Point p2 = p1;
  p2.x = 200;
  std::cout << "p1.x = " << p1.x << std::endl;
  std::cout << "p2.x = " << p2.x << std::endl;
}
p1.x = 100
p2.x = 200
  • Java:
class Point {
  public int x, y;
}

class MainClass {
  public static void main() {
    Point p1 = new Point { 100, 100 } //implicit constructor like in c++
    Point p2 = p1;
    p2.x = 200;
    System.out.println("p1.x = " + p1.x);
    System.out.println("p2.x = " + p2.x);
    return 0;
  }
}
p1.x = 200
p2.x = 200

Copy pass by reference

  • To copy the memory location use &:
Point& p2 = p1;
  • Now both p2 and p1 are pointing to the same memory location and assigning a new value for one also applies that same value to the other

  • If possible, copy memory locations (pass by reference) for non primitive types to save memory.

    • All const should by default be copied with pass by reference.

Pointers

  • They are like memory references, but more scary.
  • Not needed in CSE2215 Computer Graphics
  • The new keyword allocates a object on the heap and returns a pointer
    • if you forget to delete then you will leak memory
    • Don’t use new in the assignments!

Strings

#include <string>

int main() {
  std::string text = "Hello, ";
  text += "World!";

  std::cout << text << std::endl;
  return 0;
}
  • You can use + to concatenate strings like in java (it’s actually adding two pointers (memory addresses))
  • A C++ string is equivalent to a pointer that points to an array of characters
int main() {
  const char* hello = "Hello, ";
  const char* hello = "World!";
  const std::string helloWorld = hello + world;
  std::cout << helloWorld << std::endl;
  return 0;
}
  • If you concatenate a string with an integer the integer will be interpreted as an ascii character
int main() {
  int numEggs = 123;
  std::string text = "I have ";
  text += numEggs;
  text += " eggs!";
  std::cout << text << std::endl;
  return 0;
}

I have { eggs

  • Use std::to_string to convert numbers into strings
int main() {
  int numEggs = 123;
  std::string text = "I have ";
  text += stdd::to_string(numEggs);
  text += " eggs!";
  std::cout << text << std::endl;
  return 0;
}

I have 123 eggs

  • Or as explained earlier, std::cout << variable automatically takes care of the types and outputs the “toString()” version implicitly

Classless function and compilation example

cat square.cpp
#include <iostream>

int square(int a){
  return a * a;
}

int main() {
	std::cout << "42^2 = " << square(42) << std::endl;
  return 0;
}
g++ square.cpp -o square
./square

42^2 = 1764

Compiling program with refactored functions

Inefficient way

  • We can seperate the function in a separate file and include it in the main file
cat square.cpp
int square(int a){
  return a * a;
}
cat main.cpp
#include <iostream>
#include "square.cpp" //#include literally copy/pastes the contents into our current file

int main() {
	std::cout << "42^2 = " << square(42) << std::endl;
  return 0;
}
g++ main.cpp -o main
./main

42^2 = 1764

Efficient way

  • Separating code with header files (like in the classes section) and only include the header file #include "square.h"
  • Then compile each .cpp individually with g++ file_a.cpp file_b.cpp file_etc.cpp don’t mention the .h files
cat square.h
#pragma once //always put this at the top of a header fil!
int square(int a);
cat square.cpp
int square(int a) {
  return a * a;
}
cat main.cpp
#include <iostream>
#include "square.h"

int main() {
  std::cout << "42^2 = " << square(42) << std::endl;
  return 0;
}
g++ main.cpp square.cpp
./main

42^2 = 1764

Tutorials and documentation