The goal of CS2024 is to teach as much of the C++ language as possible with an eye towards your being able to use it effectively in future classes that may depend on it and/or in a professional setting. C++ is ever changing with new standards released every three years. We look to strike a balance between making sure you thoroughly understand “historic” C++ as well as introducing you to new features enabled in the language in the past decade.
Lec01 Introduction
Explaining our First Program
#include <iostream>
Tells the compiler that we would like to load definitions from a header file named “iostream”.
The # (pound sign) indicates this is a preprocessor directive, it gets dealt with BEFORE your code is compiled
std::cout << “Hello World!” << std::endl;
<<
is an operator that directs content from the right to the left. In this case, we direct the string “Hello World” tostd::cout
, which is the console
Compiling C++
- Windows: use Visual Studio
- Linux:
g++ -std=c++11 -lstdc++ -o demo1 demo1.cpp
:-o
specifies the name of the compiled file
Compiler takes the text of the source code and converts it into a binary object so that it can execute it a bit more efficiently.
Lec02 Input/Output and Operators
Input and Output
>>
stream extraction operatorstd::cin >> k
take a value fromcin
, which is the input stream keyboard, and assign it tok
getline(cin,str)
:cin
uses space as delimiter so it won’t read in a whole line. Use this to read a full line
Using
using
is similar to import
in java, so that you don’t have to use the full name of a function when calling it.
1 | using std::cout; |
Lec03 Introduction to Classes
Struct
C-Style structure definition: (Define a structure called Course
, which has three fields )
1 | typedef struct { |
Classes
Variables defined inside that class are called member variables.
Functions defined inside the class are called member functions
Public vs. Private
public
and private
keywords can appear as many times as you want in the class definition.
1 | class Course { |
Declaration and Definition of Member Functions
You don’t have to define the functions where they are declared. Instead, you can define them outside of the class declaration. When you define them outside of the class declaration, you can still access the member variables inside that class. That’s because you are telling the compiler that this is a member function.
1 | class Course { |
You usually want to define your getter and setter functions inside class definition.
When other functions you are trying to define are too big, we usually define them outside the class definition and usually in a separate file. So we declare the functions in header file **.h
and define them in another file **.cpp
1 | /* <Courses.h> */ |
Constructors
Constructors have to have the same name as the class. Constructors have no return type. You can define Constructors outside of class definition too.
Constructors are called when you declare an instance of that type: MyClass instance
. Note defining a pointer of that class without allocating memory to that pointer MyClass *p
will not call the constructor, but declaring a pointer and allocating memory will call the constructor, because that’s the real time an instance is created MyClass *p = new MyClass()
.
Lec5 Functions I
Enum
If you don’t assign values to the ones following the first, they will all have value of previous increment 1.
1 | // Define error codes |
In C++11, we can use the class keyword to define sort of a “namespace” for the enum.
1 | enum class Months { |
Function Declaration and Definition Revisited
1 | // mymath.h -- header file for math functions |
You should never include a “.c++” file in another c++ file.
Lec6 Function II
Inline Functions
1 | inline int performAddition(int x,int y) |
Wherever this function is called the compiler has the option of replacing the call with the body of the actual function, instead of creating a memory stack for that function call and etc.
The compiler may not do that when it’s a recursive call or that function is really long.
Pass By Reference
Why and when do you want to use pass by reference?
- You need to return multiple values. C++ only allows you to return one value. So you send those values as pass by reference parameters
- You are passing a large structure/class. When passing values, the compiler will make a copy of those structure/class and pass them, which takes up a lot of stack space.
In the second case, maybe you don’t want to change anything in the structure, but passing by reference makes such a mistake likely to happen. To fix this, you can declare this passed by value as const
, so when you accidentally modify it, you will get a compile-time error.
1 | bool isBusy(const BIGDataType &arg1) |
Default Argument
When we declare a function, we can set a default value to its argument. (Don’t set a default value in function definition)
1 | class Counter { |
Unary Scope Operator
When you have 3 variables with the same name defined in global scope, local scope, and a nested scope inside local scope and you want to access the variable in the global scope inside some scope, you can use the ::
before calling this variable. There is no way for you in the nested scope to access the variable with a same name in local scope (parent scope).
1 | int x=1; // in the global scope |
Lec7 Function III
Function Templates
1 | template <typename a,typename b,…> return_type function_name (formal args) |
At compilation time the compiler will look at your code and generate a separate function for each type used throughout your code when calling template functions. For example, for this maximum
below, when the compiler sees the call to maximum(3,5,8)
, it uses the function template to automatically generate an overloaded version of maximum() that takes three variables of type int as its arguments.
1 | template <class T> T maximum(T val1, T val2, T val3) |
Lec8 Arrays and Vectors
Arrays
Arrays don’t have boundary checking.
1 |
|
Vectors
1 |
|
Lec9 Pointers
Dynamic Allocation
1 | int *iPtr; // declares a pointer to int |
Note: in the above example, a memory in the heap is allocated to this pointer
iPtr
contains one of the following:
- A pointer to the newly allocated data type (in this case, an int)
- NULL (if the pointer could not be allocated due to insufficient memory)
We should always check whether it is NULL
before using a dynamically allocated pointer.
We can use delete iPtr
to dispose a dynamically allocated pointer.
1 | int *iPtr; // iPtr points to some random memory |
Pointers to Already Existing Values
Existing values are in stack frame, so when our pointers point to something already existed, they point to something in the stack frame, but remember variables in stack frame can disappear when out of scope.
1 | int main() |
So the danger is you will have to know how long this stack frame will live, or you will lose track of what you are pointing to and end up pointing something totally irrelevant.
Common Confusion with *
int *p
- declaring a pointer: The star is part of the type name, and says that we want a pointer to some other type (in our example,int *
is the type ofp
).r = *p
- dereferencing a pointer (RHS): The star is the dereference operator. This assignment gives the variabler
a new value, namely the value inside the box that the pointerp
points to.*p = r
- dereferencing a pointer (LHS): The star is the dereference operator. This assignment changes the value inside the box thatp
points to be a new value, namely the value of the variabler
.
Pointer Chaos
1 | int *a = 5, *b = 7; |
Pointers to User-Defined Types
When we want to use member member (functions/ variables), we can use one of the following:
1 | Course *aCourse = new Course; |
Passing Pointers as Arguments
1 | int *a = new int; |
Const
with Pointers
Principle of Least Privilege: Any operation you do should only be given the opptunity to happen if it absolutely needs to.
Following this principle, we don’t want to give writing privilege to functions doing reading.
There are four possibilities between constant/non-constant pointers pointing to constant/non-constant data:
1 | // Non-Constant Pointer, Non-Constant Data |
Lec10 Classic Arrays and Pointer Arithmetic
Classic Array
1 | int *j[4]; == (int *) j[4]// array of 4 pointers |
Arrays are somewhat pointers. For example, if we have int b[10]
, b
always points to the first element in this array: b == &b[0]
Pointer Arithmetic
For any array p[n] == *(p+n)
. In particular, *(p+n)
gives the contents of we have after advancing n
steps from p
. In fact, we also have p[n] == n[p]
, because our a[m]
is just a syntactic sugar for *(a+m)
Dynamic Allocation of Arrays
1 | int a1[8] = new int; // WRONG |
There are more scope issues when you use arrays as pointers. For example, the following code returns a pointer to something inside current call stack frame. It will disappear when out of the scope. Therefore, the returned pointer from function MakeArray()
actually points to something undefined.
1 | int *MakeArray() { |
The following code behaves differently. Instead of returning a pointer to something in the call stack, it returns something in the heap, which will not disappear after the function finishes execution.
1 | int *MakeArray(int size) { |
Passing Arrays as Parameters
Since arrays are pointers, you can only pass the real array to a function. There is no concept of passing a copy of that array. These are standard ways of declaring a function taking in arrays as its parameters.
1 | void swap(int *A, int j, int k); |
Memory Allocation with malloc
and sizeof
malloc
is a function for dynamic memory allocation and it only takes inbyte
.sizeof(SomeDataType)
returns the number of bytes this data type needs.
Say we want to declare an array of 6 Courses in heap here.
1 | Course *courseArray = malloc(sizeof(Course) * 6); // Old C way to initialize array in heap |
Lec11 Classes – A Deeper Look
1 | clang -std=c+11 -lstdc++ -c MyString.cpp |
Implicit Inline
When you define a function right in the class definition, you make this function implicitly inline. Therefore, there’s no actual method/function created; the code of the method is substituted through the rest of the code wherever that method is called.
Multiple Constructors
You can use a delegate constructors to save yourself from writing duplicate code. It will just call that constructor, if the delegate constructors take in arguments, you can just pass in those arguments there.
1 | // older c++ style |
Destructor
The destructor is a special method (similar to constructor) that is called just before an object is destroyed. There is only one destructor per class (can’t overload). It takes no arguments. A destructor should be used to clean up any dynamically allocated resources (memory, OS objects). You call the destructor when using delete
keyword.
Passing and Returning Reference
Passing Reference
If you modified a parameter passed by reference in a function, the change would persist in the calling function. Note that the way we call this function has not changed. We still pass in two strings instead of pointers.
You don’t have to do anything differently to specify that the string arguments are being passed “pass-by-reference” when I call the function; I only need to specify that I want to use pass-by-reference when I declare the getTimeAndTemp
function.
1 | void getTimeAndTemp(string &time,string &temp){ |
Returning Reference
When we add a &
before the function name, the function still returns whatever type it returns, but now the function call can appear on left side of assignment operator and we can write a new value to the memory address the returned value is stored in.
For the following example, charAt
still returns a char
type. The only difference is that we can now directly change the returned value stored in the object by using the assignment operator.
1 | char &MyString::charAt(int index) { |
const
in class
As a qualifier to a member variable. It means that the member variable cannot be changed
As a qualifier to a member function. It means that the member function cannot change anything in the class:
1
string getName() const { return mName; }
static
in class
- There is ever only one copy of that variable that is shared among all the instances of the class.
- The storage for this variable must be declared in the global scope using the fully qualified name of the variable (
classname::static_variable_name
) - The shared copy of the variable can be accessed either as a field of any instance or using the fully qualified name of the variable
1 | // "Person.h" |
this
in class
- Its “type” is pointer to class type. So, if we have a
Person
class,Person
has an implicitly defined member variable named this that is of typePerson *
- Any of the member variable and functions in the class can be referenced from this
Lec12 Operator Overloads
Unary Operator Overloads
we just have to use the operator
keyword.
1 | // "MyString.h" |
Binary Operator Overloads
We define most binary operator overloads globally when it doesn’t make “sense” which of the two instances of the operands should “host” the overload. (Expressions on both ends are to some extent equal to the other)
We use inline
to allow us to place this in the header file without causing multiple definition errors, so we are never really “defining” it, but just replace the code whenever it is called.
1 | inline MyString operator+(const MyString &str1, |
Here we have an instance of binary overload not done globally. It is a “binary operator” but only takes one argument.
1 | T &operator[](int i) { |
Copy Constructors
Whenever we use the assignment operator to initialize a variable when it is declared, the compiler actually looks for a constructor that takes in a single argument that matches the type of the value you are assigning to the newly declared instance.
If we have MyString str2 = 1;
, the compiler would look for a constructor for MyString
that took a single integer: MyString::MyString(int arg)
. If you copy constructors take in some object, it must be pass-by-reference!
1 | Point::Point(Point &anotherPoint) { |
Overloading Assignment =
Rather than initialize some variable, we want now to assign a new value to an existing variable. Rather than a global function, we will define it as a member function in our class.
1 | MyString &MyString::operator=(const MyString &sourceStr) |
Overloading Stream Direction <<
and >>
1 | inline ostream& operator<<(ostream &os, MyString &str) { |
Lec13 Inheritance
Basic Syntax
1 | class DerivedClass : public BaseClass |
Override
We can override a function by just reimplementing it in our derived class. To access the original implementation from the base class, we use its fully qualified name in the derived class.
1 | void Person::printInfo() |
Virtual Functions
Say we overwrite the printInfo
function in Person
and define a global function that takes in a Person
class and call the printInfo
function on that class. When we pass a Student
instance to it, it will actually use the printInfo
function of Person
instead of Student
. That’s because the compiler thinks the function just takes in a Person
.
1 | void printPersonInfo(Person &aPerson) |
If you want to use the overridden version of the function, you will have to declare the function in base class as a virtual function. By defining a virtual function, we tell the compiler to call the overridden version no matter what type that instance may be cast to. However, to achieve this effect, we should also pass in an reference or pointer of instance of our derived class. Only in this way can the compiler knows what type our object was declared as. If we just pass by value (a copy of that instance), it will create a copy of our instance with whatever type specified in the function. More specifically, it calls the copy constructor of the specified class. It has no knowledge of what the original type of the argument was.
1 | class Person |
Lec14 Polymorphism
We can dynamically allocate an instance of the derived class and store it in a base class pointer variable. Since Instructor is derived from Person, this is legal.
1 | Person *aPerson = new Student(); // a pointer of base class(Person) pointing to its derived class(Student) |
Abstract Class
We can make a function to be pure virtual (abstract) by adding a = 0
after its declaration. Any new class derived from this class must implement pure virtual methods if the class is going to work. A class with pure virtual functions is an abstract class.
Virtual Destructors
If you have an abstract class, you would need to have an abstract/virtual destructor. That is because when a derived class’s destructor is called, it will (implicitly) call destructors in all base classes it inherits from as well.
1 | // Person.h |
Lec15 Stream
Simple Stream I/O
put/get
: For any stream, the simplest I/O routines let you input or output one character at a time.- End of File
eof
: a special character (usually has value -1) that signals you’ve reached an end of file state. When we reacheof
, we cannot read any further from the file. (Ctrl+Z
on Windows,Ctrl+D
on other OS) getline
: pass in a whole line of characters ( read in until encountering with a\n
)
When you type in “This” while running the following code without hitting Enter, it will not print anything, because all characters you typed in have not been sent into the buffer yet. After you hit Enter, “This” will be echoed back. So everything got sent into the buffer, we get one out of it each time, and put it to the outstream, repeat the process until we encounter an eof
(Ctrl+Z
).
1 | while (!cin.eof()) { |
Error Handling
Once an cin
attempt failed, an error flag is set and future attempts to get input will fail. Failure happens when type entered doesn’t match the type of the variable you are assigning value to.
cin.fail()
: returns true if the lastcin
assignment failed.cin.clear()
: repairs the stream by clearing the error flag incin
.cin.ignore(n, c)
: ignores the followingn
characters or onec
character. Therefore,cin.ignore(100,'\n')
ignore all input until you’ve already ignored 100 of them or ignore 1 ‘\n’ character.
1 | cin >> id; |
Int Stream Manipulator
dec
: decimal, base 10oct
: octal, base 8hex
: hexadecimal, base 16setbase(n)
: set to n base
These stream manipulators are “sticky”. They will remain the format of your output (even though you start another sentence of cout
), until you set another stream manipulator.
1 | cout << oct << 8; //10 |
Float Stream Manipulator
fixed
: print out float number in decimal/fixed point notationscientific
: print out float number in scientific notationsetprecision(n)
: always print out 3 digits after the decimal point
They are all “sticky”. You’ll have to manually set it back to previous state.
1 | int curPrecision = cout.precision(); // current setting |
Fixed Width
left
: align to left, the output is padded to the field width appending fill characters at the endright
: align to right, the output is padded to the field width by inserting fill characters at the beginning
These two stream manipulators are sticky.
We also use setw(n)
to make sure at least n
characters are printed. If the string to print has fewer than n
characters, fill with space. If it has more than n
characters, print everything. setw(n)
is not sticky. That’s because most output methods automatically calls setw(0)
each time you call them. setw(n)
is in the library #include <iomanip>
.
1 |
|
Custom Manipulator
Manipulators are just globally defined functions that take an ostream
reference and return an ostream
reference. Following are some examples:
1 | ostream& beep(ostream &output) |
Lec16 Functional Programming
auto
keyword
The auto keyword is used to declare a variable whose type is determined by the value it is initialized to. It must be initialized at the moment it is declared (or it will cause a static time compiler error).
1 | auto f = 3.14 // f is made a double |
Function Pointers
When we define a function pointer, we need to define its return type and what type of arguments it takes in: return_type (* function_name) (argument_type1, argument_type2, ...)
. Note: All these parameters are required. We can declare a function pointer with no allocation. We can assign it to any function that matches the argument type and return type as we do to most pointers.
We can also use the C++11 style function in STL functional
: std::function< return_type (argument_type1, argument_type2, ...) > function_name
, but this is much heavier.
1 | int SimpleAdd(int arg1,int arg2) |
Function as Parameter
When we want to pass a function as a parameter of another function, we can pass it as a C-style pointer or C++11 std::funciton
. We can also use a template and let the compiler to figure it out.
1 | void OldCallMe(int (*f)(int), int x) {...} |
Lambda Expressions
A lambda expression evaluates to a function pointer. It takes the following format: [vars](args) -> returntype { // body of function };
, where return type and arrow can be omitted.
1 | // Declare a lambda with the auto keyword (we don't know what type of a function that is) |
Capture Local Variables
We can use lambda expressions to capture local variables. This will be an important way to still be able to use variables in a function that no longer exists when the lambda finally gets executed. If you want to capture local variables, always use C++11 std::function
when defining either the lambda expression or the function you want to take this lambda expression.
1 | void CallMe(std::function<int()> fn) {...}; |
You also have the option of capturing local variables by reference. That means if the lambda expression modifies them, the modifications persist back into the “hosting” function where these variables were defined. (Just like any pass by reference function call). Pass by reference or pass a pointer will do.
1 | int myX = 300; |
Lec17 Files I/O
ofstream
We use ofstream
to write to file. A constructor of ofstream
takes in two arguments
- name of the file to open
- specifies which mode to use:
ios::out
open file for writing, overwrite existing fileios::app
open file for writing, append to existing file
We don’t have to close the stream after writing, ofstream
has a destructor that is automatically called at the end of the program. That being said, we can still call out.close()
manually.
We use out.is_open()
to make sure the file is indeed successfully opened and ready to be written in. Directly evaluating the stream variable out
itself as a boolean also does the check.
1 |
|
ifstream
We use ifstream
to read from a file.
1 | ifstream in(“myFile”,ios::in); |
Sequential Files
Suppose we have a csv
file that uses comma as the delimiter and space as a record separator. If we want to change only a specific record, how are we supposed to move around in that file?
tellg()
: returns the offset from the beginning of the file where the next read operation will get data from.tellp()
: returns the offset from the beginning of the file where the next write operation will put data to.seekg(n)
: sets the “get” offset to the nth character in the fileseekp(n)
: sets the “put” offset to the nth character in the file
Reading and Writing at the Same Time
When declare an fstream
variable, we can specify using multiple “modes” at the same time by putting the or operator |
between different modes. We can then use whatever function those modes give
1 | fstream file(“ages.dat”, ios::in | ios::out) //reads and write to "ages.dat" at the same time |
Lec18 Standard Template Library
Iterator
begin()
points to the first element in the object. end()
points to one after the last element. Common operators like + < >
are all overloaded for iterators.
1 | for (vector<string>::iterator p = stringVector.begin(); |
Vector
1 | vector<string>::iterator q = stringVector.begin(); |
Map
Map is based on valuetype
, which has type <key, value>
. All operations come from this pair. We can use typedef
to name some very complicated data type that is frequently used.
1 | typedef map<int,string>::value_type IDRecord; // IDRecord is in fact of "pair<int,string>" type |
Lec19 Exceptions
Basic Syntax
Exceptions can be of any type. We can do throw 3.14
, throw "Unexpected"
, or throw some object.
1 | enum MathErr { noErr, divByZero, genericOverflow }; |
Exception Object and Inheritance
As said in previous section, we can throw an object.
1 | class MyIndexError { |
Lec20 Custom Templates
Basic Syntax
1 | template <class placeholder> // declare placeholders |
“Definition” template class should also be in the same .h
file. Because the compiler needs to generate a separate set of member functions for each type used to create an instance of this class at compile time. That means that these definitions are needed at compile time and not at link time, so .cpp
won’t enable us to actually call those functions.
Non-Type Parameters
We specified a data type calls placeholder in the template class. We can also specify a constant expression when we declare a template class. This will have the same effect as setting a const
value specific for that instance, except previously we couldn’t assign values to const
variable.
1 | template <class storageType,int size> class MyArray {...} |
Lec22 STL Algorithms
#include<algorithm>
for all functions below.
fill(iterator begin, iterator end, T value)
: take two iterators/pointers and one value. Fill every position in between with that value.:1
2char *ptr = new char[10] // An array of 10 chars
fill(ptr,ptr+9,’A’);generate(iterator begin, iterator end, function g)
: assigns every position in between the two iterators/pointers according to the generating functiong
.1
2
3
4
5
6
7int nextVal() { static int number = 0;
return number++;}
int main(int argc,char *argv[]) {
std::vector<int> intVector(10); // A vector of integers
std::generate(intVector.begin(),intVector.end(),nextVal);
}fill_n(begin,count,value)
: fill from begin to begin+count with specified valuegenerate_n(begin,count,function)
: fill from begin to begin+count with generated valueremove(begin,end,value)
: remove all elements == value in range from begin to endreplace(begin,end,value,replaceWith)
: replace all elements == value in range from begin to end WITH replaceWith
Lec23 Smart Pointers
Shared Pointer
You can declare multiple pointers pointing to the same thing using shared pointer and they will all be released when you release one of them, so it’s safer than the classic pointer, where the pointer will hang over there.
You can call the use_count()
method to get how many shared pointers are out there pointing to this same thing.
1 | int main(int argc,char *argv[]) { |
Unique Pointer
There is only this one pointer pointing to that thing. No other shared pointers can be created pointing to the same thing. For the same reason, use_count()
is not available either.
Lec24 Namespaces and C/C++ Differences
Namespace
Declaring Namespace
We define a namespace by putting it inside a namespace declaration and its corresponding scope, just like what we do to a class. What’s different is that a single namespace may span multiple files. Therefore, we can declare/define a single namespace in multiple files.
1 | namespace CornellCS2024 { // Defines a namespace named CornellCS2024 |
Using Namespace
We can use anything declared in the namespace by quoting the fully qualified name
1
CornellCS2024::MyString aString;
We can designate a specific class to use in the rest of the file.
1
2using CornellCS2024::MyString;
MyString aString;We can simply state that we want to use everything declared in this namespace. That’s what we usually do to
std
in small file.1
2using namespace CornellCS2024;
MyString aString;
C/C++ Difference
- only supports
/* block comments */
- variable declarations had to appear the beginning of a scope before any other statements were encountered
- only has
struct
, noclass
- no overloads, Namespaces, Declaring a counter variable in a loop, String type, Exceptions, Templates
- does not use new/delete for dynamic memory allocation/deallocation. Instead, C uses
malloc()
allocates memory. It needs to be given the exact number of bytes you want to dynamically allocatecalloc()
is the same as malloc() but initializes all allocated memory to 0realloc()
“grow” a dynamic allocation: basically allocates new space and copies all original memory to new space.free()
releases allocated memory
Lec99 From Assignments
new
keyword returns a pointer to an object. You don’t have to usenew
when creating a new object. Reference1
2
3test t = test("rrr", 8);
test t("rrr", 8);
test *t = new test("rrr", 8);