Declarations are a way of telling the compiler what the type of a name is. Names must be declared before they can be used. Declarations consist of a type followed by a name.
int numberOfApples;
// numberOfApples is an integer
std::string myName;
// myName is a string
Declarations can also tell the compiler about functions. Function declarations start with the return type of the function, followed by a name, followed by arguments types and names.
int RandomInteger();
// RandomInteger is a function that takes no arguments and returns an integer
void SetVoltage(float voltage);
// SetVoltage is a function that takes a float argument and returns nothing (void)
int AddIntegers(int a, int b);
// AddIntegers is a function that takes two integer arguments and returns an integer
We can use the =
sign to assign values to variables.
int numberOfApples;
numberOfApples = 7;
std::string myName;
myName = "John Doe";
We can combine the syntax for declaration assignment to do both at the same time.
int numberOfApples = 7;
std::string myName = "John Doe";
Analogously, we can define the behavior of a function without needing to write a separate declaration.
int RandomInteger() {
return 4; // https://xkcd.com/221
}
void SetVoltage(float voltage) {
// do some magic
}
We can use a special type called auto
when using combined declaration and assignment syntax. The auto
type will automatically deduce a type for a variable depending on what it is being assigned to it.
class RobotContainer {
public:
RobotContainer();
static RobotContainer* GetInstance();
}
auto m_container = RobotContainer::GetInstance();
// The compiler will automatically decude that m_container is a RobotContainer*
// because the return type of RobotContainer::GetInstance is a RobotContainer*
Note that auto
only works when declaring and assigning to a variable at the same time. The compiler needs to be able to deduce a type from the right side of the =
sign.
We should only use auto
in cases where the right side of the =
sign has an obvious type so that it does not hinder readability.
Classes are user-defined types that combine variables and functions. Variables associated with a class are called member variables and functions associated with a class are called methods. Consider this example bank account class.
class BankAccount {
public:
BankAccount(float initialBalance) {
m_balance = initialBalance;
}
void Deposit(float amount) {
m_balance = m_balance + amount;
}
void Withdraw(float amount) {
m_balance = m_balance - amount;
}
float GetBalance() {
return m_balance;
}
private:
float m_balance;
};
The class has one member variable: m_balanace
. The class also has four methods: BankAccount
, Deposit
, Withdraw
, and GetBalance
. The first method is a special method called a constructor. We call the constructor in order to create a new object or instance of the class.
BankAccount myAccount = BankAccount(21.35);
// or
auto myAccount = BankAccount(21.35);
// or
BankAccount myAccount(21.35);
All of these lines would create a new BankAccount
object with an initial balance of 21.35 by calling the constructor. The last style is the most succint, so we prefer it over the others.
Once we have an instance of a class, we can call its methods. We use the .
operator to do this.
BankAccount myAccount(21.35);
myAccount.Withdraw(20);
float updatedBalance = myAccount.GetBalance();
In the bank account example, all of the methods are defined within the public:
section, while m_balanace
member variable is defined within the private:
section. This means that while all of the examples so far will compile, the following example will not.
BankAccount myAccount(21.35);
myAccount.m_balance = 1000000;
Private member variables can only be accessed from methods of the same instance. Similarly, private methods can only be called from methods of the same instance. In practice, this means that we can only interact with classes through their public methods and member variables. This helps prevent us from accidentally changing the internal state of an instance in an unexpected way. For example, if we were allowed to overwrite m_balance
we could potentially bypass checks in the Withdraw
method to ensure that account balances do not go below $0.
We usually split classes into a .h
(header) file and a .cpp
file. This is not strictly required, but helps reduce compilation times. For the bank account example, this would look something like the following.
// BankAccount.h
class BankAccount {
public:
BankAccount(float initialBalance);
void Deposit(float amount);
void Withdraw(float amount);
float GetBalance();
private:
float m_balance;
};
// BankAccount.cpp
#include "BankAccount.h"
BankAccount::BankAccount(float initialBalance) {
m_balance = initialBalance;
}
void BankAccount::Deposit(float amount) {
m_balance = m_balance + amount;
}
void BankAccount::Withdraw(float amount) {
m_balance = m_balance - amount;
}
float BankAccount::GetBalance() {
return m_balance;
}
We declare the class in BankAccount.h
and define its methods in BankAccount.cpp
.
Class methods marked as static
can be called without going through an instance of the class. Essentially, the are plain old functions that are associated with a class that do not have access to non-static member variables and methods.
class RandomNumberUtilities {
static int RandomInteger() {
return 4;
}
}
We call static methods using the class name followed by ::
and the method name.
int randomInt = RandomNumberUtilities::RandomInteger();
Pointers are types of variables that "point" to other variables. To declare a pointer, we write the type of the variable we want to point to followed by an asterisk (*
) and a name.
int* myNumberPointer;
If we have a variable, we can use an ampersand (&
) to obtain a pointer to it.
int numberOfApples = 7;
int numberOfOranges = 17;
int* foodCount = &numberOfApples;
// Suppose we want to change what foodCount points at
foodCount = &numberOfOranges;
Conversely, we can use an asterisk to dereference a pointer.
int numberOfApples = 6;
int* applesPointer = &numberOfApples;
*applesPointer = 7;
// numberOfApples is now 7
int numberOfApples2 = *applesPointer;
// numberofApples2 is now 7
We primarily use pointers with functions. Normally, calling a function creates a copy of each of its arguments. Consider the following example.
void Magic(int x) {
x = 2135;
}
int myNumber = 1;
Magic(myNumber);
// myNumber is still 1
Calling the Magic
function will not change the value of myNumber
because its value is copied into the x
argument. We can work around this behavior using pointers.
void Magic(int* x) {
*x = 2135;
}
int myNumber = 1;
Magic(&myNumber);
// myNumber is now 2135
Calling the Magic
function still creates a copy of the &myNumber
argument, but a copy of a pointer will still point to the same place. We can derefernce this pointer to modify the variable that it points at.
We commonly use pointers to point at class instances. Instance pointers have a special shorthand for calling methods that lets us avoid dereferencing them. Rather than using a .
before the method name, we use ->
.
BankAccount* GetMyBankAccount() {
// magic goes here
}
auto myAccount = GetMyBankAccount();
float myBalance = myAccount->GetBalance();
Pointers do not necessarily have to point to a valid variable. Dereferencing a pointer that poins to an invalid location will cause the program to crash.
int* myNumberPointer = nullptr; // nullptr is not a valid location to dereference
int myNumber = *myNumberPointer; // This line causes the program to crash
References are a special type of pointer that can only point at other variables. Because of this requirement, references must always be declared and assigned at the same time. References are delcared similarly to pointers, but they use an ampersand instead of an asterisk.
int& myNumberReference; // This will not compile because it does not point at anything
int myNumber = 2135;
int& myNumberReference = myNumber; // This is OK
Note that we do not need to use an ampersand when setting myNumberReference
to myNumber
like we would have to for a normal pointer. We also do not need to use the ->
syntax when calling methods on a reference of a class instance. References are transparent, meaning that they behave exactly like the referenced type.
BankAccount& GetMyBankAccount() {
// magic goes here
}
auto myAccount = GetMyBankAccount();
float myBalance = myAccount.GetBalance();
Unlike normal pointers, references cannot be reassigned to reference a different underlying variable.
We should prefer to use references over normal pointers where possible, due to their improved safety and transparent ergonomics.
void Magic(int& x) {
x = 2135;
}
int myNumber = 1;
Magic(myNumber);
// myNumber is now 2135