title | subtitle | author | date | mainfont | monofont | fontsize | geometry | toc | documentclass | header-includes |
---|---|---|---|---|---|---|---|---|---|---|
CS31: Introduction to Computer Science |
Professor Smallberg |
Thilan Tran |
Fall 2019 |
Libertinus Serif |
Iosevka |
14pt |
margin=2cm |
true |
extarticle |
\definecolor{Light}{HTML}{F4F4F4}
\let\oldtexttt\texttt
\renewcommand{\texttt}[1]{
\colorbox{Light}{\oldtexttt{#1}}
}
\usepackage{fancyhdr}
\pagestyle{fancy}
|
\newpage{}
#include <iostream> // defines a special library of commands to be inserted here by compiler
using namespace std; // different families / 3rd party libraries use
// diff. namespaces to help distinguish themselves
// 'using' now defaults to this namespace
int main()
{
cout << "Hello!" << endl; // cout prints to standard output
// endl moves cursor to next line
}
- not
#include <iostream.h>
as in C - include statement:
- compiler has to learn standard library beyond built-in language
- ie. fundamental parts of English vs. parts that change over time
- fully qualified cout statement would be
std::cout
- whitespace in code doesn't matter (format for readability), but spaces in literal text does matter ("Hello!")
- cin reads input into a variable
int main()
{
cout << "How many hours? ";
// variable declaration form: type identifier
double hoursWorked; // uninitialized, value is unspecified / garbage
cin >> hoursWorked;
cout << "Pay rate? "; // enter for input already skips the next line
double payRate;
cin >> payRate;
cout << "The hours worked are " << hoursWorked << endl; // testing print statements
cout << "The pay rate is " << payRate << endl;
cout << "You made $" << hoursWorked * payRate << endl; // spaces matter in literal text!
cout << "Money withheld $" << .1 * (hoursWorked * payRate) << endl;
}
- C++ naming conventions:
- eg. hours_worked, HoursWorked, hoursWorked (can't be reserved words)
- case sensitive!
cout <<
, output, read outcin >>
, input, read in
#include <string> // string header library
string personsName;
getline(cin, personsName); // only used for strings
cin >> personsName; // ignores whitespaces, only grabs one word
cin >> age; // must be used for integer variables
// skips input if getline() is called after because of the trailing '/n'
cin.ignore(10000, '/n'); // throws away buffer up to and including newline character
// this issue only occurs when reading a number then call getline()
string s = "Hello";
for (int k = 0; k != s.size(); K++) // s.size() returns 5, char c = s[1];
cout << s[k] << endl; // use subscript operator, behaves like an array of characters, from 0 to 4
const double PAYRATE_THRESHHOLD; // constants naming conventions and syntax
if (citizen == "US")
if (age >= 18)
cout << "You can work." << endl;
else cout << "Not U.S. citizen." << endl; // automatically pairs to second if (closesnt unpaired if)
// must add brackets for intended operation
- calling getline() after a cin execution leaves a '/n' in the buffer
- cin does not consume '/n'
- so getline() sets string to empty string
- program continues executing
- must use
cin.ignore()
- modifying cout with flags:
cout.setf(ios::fixed);
, different double modes, scientific, exponentialcout.precision(2);
, number of digits after decimal point- showpoint command always shows point (even with no floating digits)
- size() function (historically, length())
- s[0] accesses individual characters
- use
i != s.size()
when iterating through string- technically, returns type string::size_type, or an unsigned int
- if unsigned, can't iterate size_type backwards, will give an error
- '+=' operator can be used to append to strings
- substr() function
s.substr(5, 3)
create a substring starting at position 5 going for 3 characterst = t.substr(6, t.size() - 6)
, clips off the first six characters
- can't access index of a string out of bounds
- when using toupper() and other conversion functions, make sure to save the char
- eg.
s[0] = toupper(s[0]);
- eg.
if (t[k] == 'E' || t[k] == 'e')
is equivalent toif (toupper(t[k] == 'E'))
- assignment returns a value:
n = 2 * (k = 3 + 5);
- chars share a lot of properties with int (automatic conversion)
char ch = 76;
ch is now 'L', depending on character setint k = 'L';
k is now 76 (integer encoding of the character)- standard dictates:
- 'a'
<
'b, 'y'<
'z', etc. - 'A'
<
'B', 'Y'<
'Z', etc. - but no special relationship between the two
- doesn't guarantee contiguous encoding! (but ASCII does)
- '0' '1' '2' etc. are contiguous
cout << tallySeats(...,...,s) << " " << s;
- where s is a reference to a declared int
- standard doesn't dictate which operand is processed first (s or tallyseats())
- solve by splitting into two statements
- or use assert to test (will short-circuit / evaluate from left to right)
- 'a'
#include <cctype> // do operations / checks on characters
isalpha();
isupper();
islower();
isdigit();
tolower();
toupper();
- useful for self-contained sequences, reusing specific codes / functions
- void functions don't return value, can use return within function block to break out
- variables obey strict scope guidelines within function blocks
- functions with return type must return a value
- every possible path must result in a return statement
- boolean type: holds true or false (keywords)
- naming convetion: use predicate forms, eg. isdigit(), isalpha(), livesin()
- it is not possible to return more than one value
- instead have function save values into variables using pass by reference
- passing by value -> copy
- passing by reference -> another name for original
- otherwise values will not save
- double means a memory location that can hold a double
- double& means another name for an already existing double, "reference to double"
- instead have function save values into variables using pass by reference
- functions need prototypes so the compiler knows functions when they are called before implemented
cout << "Phone #";
string phone;
getline(cin, phone);
while (!isValid(phone))
{
cout << "Must have 10 digits" << endl;
cout << "Phone #";
getline(cin, phone); // repetition of code twice! can we replace with do-while loop?
}
// VS.
for (;;) // n-and-a-half-times loop
{
cout << "Phone #";
getline(cin, phone);
if (isValid(phone)) // condition is tested in the middle, not top (while loop), or bottom (do-while loop)
break;
cout << "Must have 10 digits" << endl;
}
// Another example:
int nScores = 0;
int total = 0;
for (;;) // n-and-a-half-times loop
{
int s;
cin << s;
if (s < 0)
break;
total += s;
nScores++;
}
// cout << "Average " << total / nScores << endl; error, integer division, also should check nScores == 0
cout << "Average " << static_cast<double>(total) / nScores << endl; // cast creates temporary, unnamed object
for (...)
{
if (...) // if and else close, easy to see
...
else
{
... // nested, indented code can be hard to read, can we improve?
...
}
}
// VS.
for (...)
{
if (...)
{
...
continue; // abandons current iteration of the for loop, jumps to end of the brackets
}
...
...
}
int k;
for (k = 0; k < 10; k++)
{
...
if (...)
continue; // k++ still happens at the end of the loop after continue
}
// VS.
while (k < 10)
{
if (...)
continue; // k++ is skipped after continue
k++;
}
// continue will act differently in these formats!
- how to set up a table for irregular patterns such as month/day?
- use arrays:
const int daysInMonth[12] = {
31, 28, 31, 30, 31, 30 // easier to see array if split up/organized
31, 31, 31, 31, 31, 31
};
- arrays start with 0
- undefined out-of-bounds behavior
- can utilize paired/parallel arrays (eg. month name and days in month)
- good practice to hold similar digits in const variable with symbolic name
- there is NO size/length function for arrays!!!
- arrays size must be known at compile time!!!
int n;
cin >> n;
double d[n]; // error! not a const variable
int main()
{
const int MAX_NUM_SCORES = 10000;
int scores[MAX_NUM_SCORES];
int nScores = 0;
... fill up array partially
computeMean(scores, nScores);
int stuff[100];
computeMean(stuff, 100);
}
double computeMean(int a[], int n) // cannot check number of elements, must be passed as a parameter
{
int total = 0;
for (int k = 0; k < n; k++)
total += a[k]; // passes directly by reference, not a copy
return static_cast<double>(total) / n;
}
- const arrays cannot be modified
- cannot be passed to functions modifying it (without const)
- compiler catches the error
#define _CRT_SECURE_NO_WARNINGS
#include <cstring>
char t[10] = { 'G', 'h', 'o', 's', 't'}; // allowed to have initializer list < total length
char t[10] = "Ghost"; // uses null character in order to denote end of a string, '\0' (zero byte)
char s[100] = "";
for (int k =0; t[k] != '\0'; k++)
cout << t[k] << endl;
cout << t; // cout << is overloaded
cin.getline(s, 100);
// s = t; Error! Can't assign arrays!
strcpy(s, t); // strcpy(destination, source), up to and including zero byte
strcat(s, "!!!"); // now s is "Ghost!!!"
// if (t < s) Compiles, but compares addresses, not the actual strings.
if (strcmp(a, b) < 0)
//if (strcmp(a, b)) //Yields OPPOSITE result
if (strcmp(a, b) == 0)
- c-strings are character arrays terminated by zero byte
- null pointer != null character (zero byte)
- technically, string literals are always c-strings
- declaring with double quotes automatically appends a zero byte
- don't use
k != strlen[t]
when iterating through c-string, instead uset[k] != '\0'
- function library with c-strings:
- cstring library has strlen(), unlike c++ strings
- getline() has different parameters for c-strings, string and buffer size
- when using strcpy(), make sure t is a valid c-string and destination has a large enough size.
- strcat() finds zero byte and then appends. Make sure destination string is big enough and has a zero byte.
- strcmp(a, b) returns:
- c++ strings: a OP b
- c-strings: strcmp(a, b) OP 0
- negative if a < b
- 0 if a == b
- positive if a > b
- use
#define _CRT_SECURE_NO_WARNINGS
to stop compiler warnings
C++ Strings | C-Strings |
---|---|
string s; // default constructor, guaranteed the empty string, unlike other built-in types | char s[100]; // unintialized, not empty string |
size() | strlen(t) |
[ ] operator for characters | [ ] subscript operator |
getline(cin, s); // read in input | cin.getline(s, 10); // read in input for 10 characters |
s = t; | s = t; // error, arrays can't be assigned |
s += "!!!"; | s += "!!!"; // error, not supported |
t < s |
use strcmp(a, b) for comparison |
void f(const char cs[])
{
...
}
int main()
{
string s = "Hello";
f(s); // Won't compile
f(s.c_str()); // OK
char t[10] == "Ghost";
s = t; // Assigning c-string to c++
t = s; // Won't compile, can't use assignment OP with c-strings
t = s.c_str(); // Won't compile
strcpy(t, s.c_str()); // Works
}
Structured tables are easier to visualize, eg. calendar:
const int NWEEKS = 5;
const int NDAYS = 7;
int attendance[NWEEKS][NDAYS];
cout << attendance[2][5];
for (int w = 0; w < NWEEKS; w++) // Iterate through 2-D arrays with nested loops
{
int t = 0;
for (d = 0; d < NDAYS; d++)
t += attendance[w][d];
cout << "The total for week " << w << " is " << t << endl;
}
const string dayNames[NDAYS] = {
"Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"
};
int grandTotal = 0; // Put assignment in the right area
for (int d = 4 /*Friday*/ ; d < NDAYS: d++)
{
int t = 0;
for (int w = 0; w < NWEEKS; w++)
t += attendance[w][d];
cout << "The total for " << dayNames[d] << "is " << t << endl;
gradTotal += t;
}
cout << grandTotal;
double meanForADay(const int a[][NDAYS], int nRows, int dayNumber)
// MUST specify bounds for any other dimensions (first dimension usually pased as a parameter)
{
if (nRows <= 0)
return 0;
for (int r = 0; r < nRows; r++)
total += [r][dayNumber];
return static_cast<double>(total) / nRows;
}
int main()
{
int attendance[NWEEKS][NDAYS];
double meanFri = meanForADay(attendance, NWEEKS, 4 /*Friday*/);
}
int multiplexChainAttendance[5][7][10][16];
// Valid, 10 multiplexes with 16 screenings, should use symbolic constants
void f(int b[][7][10][16], ...);
Array of c-strings is an array of array of chars - another 2-D array.
const int MAX_WORD_LENGTH = 6;
int countLength(const char a[][MAX_WORD_LENGTH + 1], int n, int targetLength);
int main()
{
const int MAX_PETS = 5;
char pets[MAX_PETS][MAX_WORD_LENGTH + 1] = {
// Longest word length in this case is 6, +1 to account for zero byte.
"cat", "mouse", "eel", "ferret", "horse"
};
cout << countLength(pets, MAX_PETS, 5); // How many 5-character strings?
}
// Only really 1-D arrays : (2-D arrays are simply arrays of elements,
// where each element is another array).
int countLength(const char a[][MAX_WORD_LENGTH + 1], int n, int targetLength)
{
int total = 0;
for (int k = 0; k < n; k++)
{
if (strlen(a[k]) == targetLength) // a[1], or shorthand for an element of the 2-D array, is the second row,
// is array of chars, can treat as c-string and use strlen()
// However, arrays and columns are not treated similarly
total++;
}
return total;
}
- Restriction of having to predetermine size of arrays / c-strings: use const variables.
- If using countLength() with strings, similar structure:
- pass array as
const string a[]
if(a[k].size() == targetLength)
- pass array as
- Another way to implement passing by reference
void f(int& n)
in C++, reference-to-int or another-name-for-some-int- in C, pointers are only way to pass by reference
- Traverse arrays
- Manipulate dynamic storage
- Represent relationships in data structures
- Pointers as alternative way to pass as reference:
- pointers are variables that point to the memory location of another variable
- eg. pointer 'xx' passes "an indication of where x is", not a copy of 'x'
- stores values in variable where 'x' is, not in 'xx'
- pointers are variables that point to the memory location of another variable
#include <cmath>
int main()
{
polarToCartesian(r, angle, &x, &y);
}
void polarToCartesian(double rho, double theta, double* xx, double* yy)
{ // 'xx' is not a double!
// cannot pass double to 'yy' either, will not compile
// pointers (has actual value) and references (another name) are not same type either
// xx = rho * cos(theta); Will not compile!
*xx = rho * cos(theta);
*yy = rho * cos(theta);
}
- double& means reference-to-int or another-name-for-some-int
- double* means pointer-to-double or the-address-of-some-double
- &x means "generate a pointer to x" or "address of x" (operator)
*p
means "follow the pointer p" or "the object that p points to"
double a = 3.2;
double b = 5.1;
double* p = &a;
// double* q = 7.6; Won't compile, wrong types.
double c = a;
// double d = p; Another type incompatibility.
double d = *p;
double& dd = d; // Usually references are only used when passing to functions.
// p = b; Another type incompatibility.
p = &b; // Assigning one pointer to another
//or
//*p = b; Assigns b to a
*p += 4; // *p = *p + 4, b is 9.1
int k = 7;
// p = &k; Won't compile since &k is pointer to int and p is pointer to double
// bit patterns wouldn't match up
// cannot convert pointers of one type to another
int* z = &k; // New pointer type
cout << (k * b);
// cout << (k * p); Won't compile, can't multiply an int and a pointer.
cout << (k * *p); // Ignores whitespace, so equivalent to (k**p).
cout << (*z * *p);
double* q;
// *q = 4.7; Undefined, trying to follow a pointer that hasn't been initialized.
// Run-time error, similar to index-out-of-bounds.
// could be outside accessible memory, or stored in random program memory location.
q = p; // points to b
double* r = &a;
*r = b; // assigns b to a;
if (p == r) // false. comparing two pointers
cout << "Hello";
if (p == q) // true
if (*p == *r) // true
- Pointers as an alternative to parsing through arrays:
- another way to visit each element
const int MAXSIZE = 5;
double da[MAXSIZE];
int k;
double* dp;
for (k = 0; k < MAXSIZE; k++)
da[k] = 3.6;
for (dp = &da[0]; dp < &da[MAXSIZE]; dp++) // dp++ ==> dp +=1
// dp = &da[0] + 1;
// dp = &da[0 + 1];
// dp = &da[1];
// &da[t];
// &da[0 + 5];
// &da[0] + 5;
// da + 5;
*dp = 3.6;
// *dp = 3.6;
// *(&da[0]) = 3.6;
// da[o] = 3.6;
// syntax of loop above is equivalent to:
for (dp = da; dp < da + MAXSIZE; dp++)
int lookup(const string* a, int n, string target)
{
... a[k] ...
}
int main()
{
string sa[5] = {"cat", "mouse", "eel", "ferret", "horse"};
lookup(sa , 5, "eel");
lookup(&sa[0], 5, "eel");
lookup(sa + 1, 3, "ferret"); // passing &sa[1], checks elements 2 through 4
}
*&x ==> x
&a[i] + j ==> &a[i + j]
&a[i] <&a[j] ==> i < j
, also other logical operators such as<=
, !=, etc as long as referencing the same array- allowed to generate pointer just past end of the array, but cannot follow that pointer
- cannot generate pointer of negative index
- pointer arithmetic always in terms of the pointer type
dp++
in machine language is translated to adding 8 bytes to the pointer-to-double- in most expressions, array name by itself is treated as pointer to element 0 of array
- ie. when passing arrays to functions
lookup(const string a[]), string sa[5], function call lookup(sa)
string a[]
is reallystring*
- generates a pointer to sa[0]
- could have passed &sa[0]
- ie. when passing arrays to functions
p[i] ==> *(p + i)
- thus, can work on any contiguous portion of an array by passing a pointer
string* fp;
sstring fish[5];
fp = &fish[4]; // fp = fish + 4;
*fp = "yellowtail"; // fp[0] = "yellowtail";
*(fish + 3) = "salmon"; // fish[3] = "salmon";
fp -= 3;
fp[1] = "loach"; // (fp + 1)[0] = "loach"; fp isn't changed
fp[0] = "trout";
bool d = (fp == fish);
bool b = (*fp == *(fp + 1));
/* fp[1] == *(fp + 1)
*fish, *fp
fish[0], fp[0] */
int findFirstNegative(double a[], int n) // Returning an index with array notation.
{
for (int k = 0; k < n; k++)
{
if (a[k] < 0)
return k;
}
return -1;
}
int findFirstNegative(double a[], int n) // Returning an index with pointer notation.
{
for (double* p = a; p < a + n; p++)
{
if (*p < 0)
return p - a; // How far *ahead* is one element from the other.
// Remember, in terms of memory, compilor is still
// working in terms of the type pointed to.
}
return -1;
}
double* findFirstNegative(double a[], int n) // Returning a pointer.
{
for (double* p = a; p < a + n; p++)
{
if (*p < 0)
return p;
}
return a + n; // Returns pointer just past end of the array.
// return nullptr; Alternative return value.
}
int main() // Returning an index.
{
double da[5];
int fnpos = findFirstNegative(da, 5);
if (fnpos == -1)
cout << "No negatives." << endl;
else
{
cout << "First negative value is " << da[fnpos] << endl;
cout << "At element " << fnpos << endl;
}
}
int main() // Where findFirstNegative() returns a pointer.
{
double da[5];
double*pfn = findFirstNegative(da, 5);
if (pfn == da + 5)
// if (pfn == nullptr) Alternative.
cout << "No negatives." << endl;
else
{
cout << "First negative value is " << *pfn << endl;
cout << "At element " << pfn - da << endl; // pfn - &da[0];
}
}
- There is another way to indicate a pointer function has failed
- null pointer value
- c++11: nullptr
- earlier: NULL
- double* p = nullptr;
if (p == nullptr)
if (p != nullptr)
*p
is undefined if p has null pointer value
int* p1;
int* p2 = nullptr;
... *p1 ... // undefined behaior : p is not initialized
... *p2 ... // undefined behavior: p has the null pointer value
// Reality is that program usually crashes
// 0x0000000 indicates null pointer
- Remember, arrays must be of the same type.
- How to deal with keeping track of strings and other types all at once? (ie. data of employees)
- Use multiple arrays with corresponding index.
- But this is a little clunky to access corresponding values.
- Want a collection of employees!
- Can introduce new types into the language
struct Employee // Defines what it means to be an employer.
// Usually means we will use a lot of Employees.
{
string name; // Called data members (fields, instance variables, attributes).
double salary;
int age;
}; // This type *NEEDS* a semicolon!
// Without semicolon, compilor will throw an error regarding the next line.
int main()
{
Employee e1; // Has three data members.
// Empty string, uninitialized double and int.
Employee e2;
e1.name = "Fred";
e1.salary = 60000;
e1.age = 50;
e2.name = "Ethel";
e1.age++; // Can do anything you would do to an int.
cout << "Enter name: ";
getline(cin, e2.name); // Can do anything you would do to a c++ string.
Employee company[100];
company[3].name = "Ricky";
// To print name vertically:
for (int k = 0; k != company[3].name.size(); k++) // Same '.' operator
{
cout << company[3].name[k] << endl; // company[3]name is a string
}
}
- member function syntax:
- an object of some member type . the name of a member of that type
- Structures essentially add a new functional type through declaration
void printPaycheck(const Employee& e);
void celebrateBirthday(Employee* ep);
double totalPayroll(const Employee eps[], int n);
int main()
{
Employee company[100];
int nEmployees = 0;
// fill some of arrays and set nEmployees
printPaycheck(company[0]);
celebrateBirthday(&company[2]);
cout << totalPayroll(company, nEmployees);
for (Employee* ep = company; ep < company + nEmployees; ep++) // ep goes forward one employer
cout << ep->name << endl;
}
void printPaycheck(Employee e) // pass by value
{
cout << "Pay to " << e.name << " the amount $" << e.salary/12 << endl;
// Passing by value, copies company[0] to e!
// Could be an issue when copying huge structures
}
void printPaycheck(const Employee& e) // pass by constant reference, e is another name for company[0]
// use const keyword, reference to a constant employee
// makes it clear we are not modifying employee
{
cout << "Pay to " << e.name << " the amount $" << e.salary/12 << endl;
}
void celebrateBirthday(Employee& e) // using reference to change object
{
e.age++; // Won't compile if paramater is const Employee& e
}
void celebrateBirthday(Employee* ep) // using pointers to change object
{
(*ep).age++; // NEED parantheses to bypass c++ order of operations
// dot operator has higher precedence than star or ++ operator
ep->age++; // or use arrow operator
}
double totalPayroll(const Employee eps[], int n) // array is really a pointer to first element
{
double total = 0;
for (int k = 0; k < n; k++)
total += emps[k].salary;
return total;
}
-
caller's object should not change:
- pass by value (cheap to copy)
- pass by constant reference (not cheap to copy, large structure)
-
caller's object should change
- pass by non-constant reference
- pass by non-constant pointers
-
pointers can be declared constant as well
- eg. constant types can only be assigned to the corresponding constant pointer type
- for constant pointers, cannot modify the object being pointed to
- but, that pointer itself can be modified, eg. replaced with another object's address
-
a pointer to an object of some struct type -> the name of a member of that type
p->m
=(*p).m
- can't use
ep.age
ore->age
- "abstraction" - generalizing operations
- "abstracts" away the intricacies of the actual machine language process
- just go through the interface, not the implementation (how processes are done)
- eg. * operator for multiplication
Code example:
class Target
{
public: // any part of the program can access these members
// Member functions AKA operations, methods
Target(); // constructor, automatically called when object is created
// no return type, not even void, never const
void init(); // no longer necessary, use constructors instead
bool move(char dir);
int position() const; // const has to go after close parentheses, function promises not to modify anything
void replayHistory() const;
// Invariants (constraints, must have valid state):
// History consistst only of Rs and Ls
// pos == number of Rs in history minus number of Ls in history
private: // can only be mentioned in the implementations of the member functions
// Data members AKA fields, attributes, instance variables
int pos;
string history; // instead of array of ints or array of characters
};
Target::Target()
{
pos = 0; // in implementation, if modifying data members, can leave off this->
// compiler assumes we are are talking about the object the function was called with
// as long as local variables/parameters of the same name don't exist
this->history = "":
}
void Target::init() // no longer necessary, use constructors instead
{
this->pos = 0;
this->history = "":
}
bool Target::move(char dir) // move() by itself has nothing to do with targets
// needs to relate back to the Target class
{
switch (dir)
{
case 'R':
case 'r': // member function can use the keyboard 'this'
// is the pointer to target object that called member function
this->pos++;
break;
case 'L':
case 'l':
this->pos--;
break;
default:
return false;
}
this->history += toupper(dir);
return true;
}
int Target::position() const
{
return this->pos;
}
void Target::replayHistory() const
{
for (int k = 0; k != this->history.size(); k++) // this->history is a string!
cout << this->history[k] << endl;
}
// can leave off this-> for all the above implementations!
void repeatMove(Target& x, char dir, int nTimes) // Non-member function! Not part of any type.
{
for (int k = 0; k < nTimes; k++)
x.move(dir); // simply calls a public member function of Target
}
void f(const Target& x) // won't compile even though position() doesn't modify x
// compiler can't distinguish or check
// will compile after position() is declared as a const member function
{
cout << x.position() << endl;
}
int main()
{
Target t; // automatically calls constructor
// t.init(); no longer necessary, use constructors instead
// t.pos = 0;
// t.history = "";
t.move('R'); // move() member function with t object
// member function is AUTOMATICALLY passed a pointer to t
// calling move() should retain the target in a valid state
// throwing away return value because 'R' is known to be valid
t.replayHistor`y();
Target t2;
...
t2.move('L');
char ch;
... read a character into ch
if (!t2.move())
... problem! ...
/*
t.pos++; // Nothing stops user from moving position but not recording in history
// Is there a mechanism for minimizing the possibility of this issue?
t.history += 'R';
*/
t.pos = 42; // now it won't compile, private!
repeatMove(t, 'R', 3); // Non-member function; doesn't need to use structure syntax
cout << t.pos; // won't compile, can't even "look" at the data member!
cout << t.position();
}
- the name of some struct type :: the name of a member of that type
- steps of abstraction
- the bulk of the program should not be allowed to modify/access position/history
- eg. can encourage this by having a built-in function that handles valid states for target (move())
- user has to go through the provided interfaces
- writer of program can specify permissions for variables, etc.
- to enforce this, should set up a "wall" with "gates", set up private data members
- interfaces are the gates (accessible to user), implementations can access data members (inaccessible to user)
- ie. code cannot directly access data members, but there are certain functions that can be called that do modify data members
- member functions can also be private (helper functions)
- if user can't access data, how can we first instantiate these objects?
- Let's try using an init() function
- how 'bout constructors instead?
- now Targets can never be put in a bad state!
- except if init() is never called!
Left-Side | Operator | Right-Side |
---|---|---|
an object of some member type | . | the name of a member of that type |
a pointer to some object of some struct type | -> | the name of a member of that type |
the name of some struct type | :: | the name of a member of that type |
void f() // issues before Target had a constructor
{
...
Target tg;
...
tg.move('R'); // bug, window of opportunity between when target is created and then initialized
...
tg.init(); // tg isn't in a valid state until here
...
...
}
- close this window of opportunity for bugs
- c++ has an initialization function that is immediately called when object is created called a constructor
- same name as its type
- no return type, not even void
- automatically called when object is created
- eg. strings have a constructor that creates an empty string
- constructors cannot be called separately from object creation
- constructors can be private, but there must be at least one public constructor
- otherwise results in a compile error when attempting to create an object
- There is no difference between classes and structs in c++ except:
- struct without explicit public/private declarations assumes by default public
- class without explicit public/private declarations assumes by default private
- by convention:
- for collection of data, eg. a point type, with no interesting behavior (functions), tend to use struct keyword
- when adding behavior to types, eg. rotating or translating a point, tend to use class keyword
- Data members should generally be private for two reasons:
- prevent data from being sent into a bad state
- gives more freedom to change the implementation
- otherwise, if the program is modified, program may no longer compile, data members accessed in program may no longer exist/different functionality
- EXCEPT, if a simple struct that is just a collection of data AND there is no way to set the data to a bad state, probably better to have public data
- eg. point struct vs. date class
Code example:
int main()
{
Target ta[3]; // calls the constructor three times, sets each of them to a valid state
// constructor is called for each element of the array
// what if we don't know how many targets we need? (eg. expensive objects)
ta[0].move('L');
ta[1].move('R');
repeatMove(ta[2], 'L', 3);
}
void f()
{
while (...)
playGame();
}
void playGame() // needs a lot of targets, but not at the beginning
{
Target* targets[1000]; // cheap declaration and provides uninitialized targets!
int nTargets = 0;
...
if (...)
addTargets(targets, nTargets, 3);
...
int i;
... something gives i a value, eg. 1
targets[i]->move('R'); // have to use arrow operator
...
delete targets[1]; // give a pointer to a dynamically allocated object
// this following proccess is necessary to shift the dangling pointer away
targets[1] = targets[2]; // have to get rid of dangling pointer
nTargets--;
targets[2] = nullptr; // not neccessary, but comforting to some
// clearing all dynamically allocated memory
for (int k = 0; k < nTargets; k++)
delete targets[k];
// now it's safe to leave the function
} // after playGame() ends, local variables (the array of pointers, ints) go away,
// but not the storage allocated to targets by the new keyword
// cannot even refer to these objects anymore; pointer variables have gone out of scope
// on each iteration, we don't get rid of target objects, leads to crash due to lack of memory
void addTargets(Target* ta[], int& nt, int howManyMore) // want to update number of targets
{
for (int k = 0; k < howManyMore; k++)
{
/*
* Target t; // incorrect implementation: this creates a local target! (eg. local to main routine)
* ta[nt] = &t; // at the end of the iteration of the loop, after the curly braces, target no longer exists
* nt++; // don't want to point to local variables
*/
ta[nt] = new Target; // allocates space for a target, call the constructor, and returns a pointer to that object
// target has no name, only way to access is through that pointer
// storage for the object does not go away, even if that pointer dissapears as a local variable
nt++;
}
}
class Person
{
public:
Person(string nm, int by); // constructors can take arguments
string name() const;
private:
string m_name;
int m_birthYear;
}
string Person::name() const // will not compile!!!
{
return m_name; // does not realize if name is refering to data member or member function
}
Person::Person(string nm, int by)
{
// there aren't really reasonable default initial values, so use a constructor with parameters
m_name = mm; // or this->name = name if parameter was named 'name'
m_birthYear = by;
}
Person p; // compiler writes a constructor that leaves built-in types uninitialized, but calls constructors for other classes (eg. string)
Person p("Fred", 1999); // with a different constructor with arguments
- Another use for pointers: manipulating dynamic storage
- eg. only create targets when we need to use them
- built-in types are not initialized
- array of targets is expensive, but an array of pointers to targets is very cheap (pointers are a built-in type)
- use
new
keyword; this is called dynamic allocation
- targets created by the new keyword DO NOT go away unless explicitly told to
- program may crash because there is no more storage, called a memory leak
- "garbage" are objects that have been allocated, but are no longer accessible
- issue may not be detected unless program runs for a while
- have to delete objects with delete keyword
- delete takes a pointer to a dynamically allocated object
- leaves a dangling pointer not pointing to any valid object, cannot follow that pointer (although it may look like the object is still there)
- naming conventions
- the same name will often repeat in the data members, member functions, parameters, etc.
- generally use the most direct name for the public member functions (will be seen/used the most)
- for data members then, should follow a pattern/convention
- eg.
name_
orm_name
- eg.
- parameters are the least visible!
- sp can have suggestive, but not necessarily 'pretty' name, eg. nm
int x = 5;
int y = 10;
int z = 15;
int* arr[3];
*arr = &x;
arr[0] = &x;
arr[1] = &y;
arr[2] = &z;
// c++ will write a constructor if there isn't one, simply calls the constructors for data members
struct Chair
{
int height; // default to public
int legs[4];
void destroy();
};
class Table
{
int height; // default to private
void destroy();
public:
Table(); // must be declared public
~Table(); // destructor, goes with delete()
int getheight() const; // won't modify data members, won't call any const member functions
private:
bool ischanged;
};
Table::Table()
{
height = 10;
}
Table::getheight() const
{
return height;
}
int DontChangeTable(const Table& t)
{
// can only call const functions
}
Chair c1; // memory is allocated for data members contiguously, similar to an array
// function is also stored in memory
c1.destroy(); // looks for that function in c1's "array" of memory
Table t1;
t1.destroy(); // won't compile, this function can only be called with other Tables
Table* pt = &t1;
pt->destroy();
(*pt).destroy();
#include <iostream>
class Table
{
private:
int h;
public:
int* hptr;
Table()
{
h = 0;
hptr = &h;
}
};
int main()
{
Table t;
std::cout << *(t.hptr) << std::endl;
(*(t.hptr))++;
std::cout << *(t.hptr) << std::endl;
}
- Take in 2 int pointers and swaps those two ints
void swap(int* p1, int* p2)
{
int temp = *p1;
*p1 = *p2;
*p2 = temp;
}
// Swap two pointers
void ptrSwap(int*& p1, int*& p2)
{
int* temp = p1;
p1 = p2;
p2 = temp;
}
- normal static objects have their attributes/sizes known at compile-time (static memory, the stack)
- vs. dynamically allocated objects are just a pointer to an object at compile-time (dynamic memory, the heap)
- these must be deleted!
- can't delete statically allocated memory!
- Where different types of variables can lie in memory:
- local variables (automatic-variables) live on "the stack"
- automatically go away, scope
- variables declared oytside of any storage live in the "global storage area" (static storage area)
- don't automatically go away, live for the life of the program
- end with main routine
- dynamically allocated objects live on "the heap"
- manage using
new
anddelete
keywords
- manage using
- local variables (automatic-variables) live on "the stack"
class Toy
{...
};
class Pet
{
public:
Pet(string nm, int initialHealth);
~Pet(); // destructor, no return type not even void, no arguments
void cleanup();
void addToy();
private:
string m_name;
int m_health;
Toy* m_favoriteToy; // is an optional feature, can be a nullptr
// Toy m_favoriteToy; not what is desired, Pet may or may not have a Toy
};
Pet::Pet(string nm, int initialHealth);
{
m_name = nm;
m_health = initialHealth;
m_favoriteToy = nullptr; // all data members should be in a valid state
}
Pet::~Pet() // automatically called when object is about to go away
// write destructors whenever cleanup is neccessary, eg. dynamic variables
// eg. strings have a destructor, remove targets from the display
{
delete m_favoriteToy; // harmless if delete is passed a nullptr
}
void Pet::cleanup()
{
delete m_favoriteToy;
}
void Pet::addToy()
{
delete m_favoriteToy; // delete old toy, harmless if nullptr
m_favoriteToy = new Toy(); // must make sure to delete this Toy
}
void f()
{
Pet p("Frisky", 20);
p.addToy();
p.addToy();
Pet* pp;
// pp = new Pet; // doesn't work!!!
// only generates default constructor if there is no constructor
pp = new Pet("Fido", 10);
pp->addToy();
// pp going away will not call the destructor automatically, it's a pointer
delete pp;
// p.cleanup(); // would work, but unpleasant to use
// would have to call cleanup() every possible way we leave the function
// delete p.m_favoriteToy; // WRONG, private data member
// destructor is automatically called
// but what if addToy() is called twice?
Pet p("Fido", 10);
Pet* pp = new Pet("Fluffy", 20); // 1+ arguments, use parentheses when constructing
Target t;
Target* tp = new Target; // 1 argument, no parentheses
Target* tp = new Target();
Target t2(); // compiles, but doesn't actually create a target
// technically, this is a function declaration
t2.move('R');
}
- if you declare no constructor at all:
- compiler writes a zero-argument constructor (default-constructor) for you
- any built-in data types are uninitialized
- class data types have their default-constructors called (eg. strings default to empty string)
- compiler writes a zero-argument constructor (default-constructor) for you
- if you do write a constructor with arguments, there is no zero-argument constructor automatically created
Target ta[100]; // default constructor, pos at 0
string sa[100]; // 100 empty strings
Employee ea[100]; // unintialized ints, name is an empty string
// Pet pa[100]; // ??? no default constructor, compiler doesn't write one
// cannot declare an array where there is no default constructor whatsoever
// could define another default constructor, but what would the reasonable default values be?
// for some types, eg. string, there is a natural default value (empty string)
Pet* ppa[100]; // dynamically allocate new pets instead
- eg. for a class representation of a class registrar:
- better for each class to have array of pointers to students
- rather than arrays of actual students (who will take multiple classes, expensive)
- 'has-a' relationship
- but then how do we find all the courses a single student is taking?
- need additional pointers the other way, from student to the class
- 'is-a' relationship
- so, when to have objects within the class, or reference external objects with pointers?
- directly contain: always there, existence tied to the class
- pointer: optional, existence independent from the class
Code example:
class Fan // turn on fan of robot when carrying heavy rocks
{ public: void turnOn();
};
class Rock // various rocks in the arena
{ public: double weight() const;
};
class Robot
{
Fan m_cooler; // every robot has a fan, fans don't need to be in the game after robot is destroyed
Rock* m_rock; // rock is not always necessarily associated with the robot (optional)
};
void Robot::blah()
{
if (m_rock != nullptr && m_rock->weight() >= 50) // arrow operator and check for null
m_cooler.turnOn(); // dot operator
}
class Employee
{
public:
Employee(string nm, double sal);
void receiveBonus const;
// void receiveBonus(double rate) const;
private:
string m_name;
double m_salary;
Company* m_company;
};
Employee::Employee(string nm, double sal, company* cp)
{
m_name = nm;
m_salary = sal;
m_company = cp;
}
Employee::receiveBonus() const // previously had parameter 'double rate'
{
// cout << "pay to " << m_name << " $" << rate * m_salary << endl;
cout << "pay to " << m_name << " $" << m_company->bonusRate() * m_salary << endl;
}
class Company
{
public:
company();
~company();
void hire(string nm, double sal);
void setBonusRate(double rate);
void giveBonuses() const;
double bonusRate() const;
private:
Employee* m_employees[100];
int m_nEmployees;
double m_bonusRate;
};
Company::Company()
{
m_employees = 0;
m_bonusRate = 0;
}
Company::~Company(){
for (int k = 0; k < m_nEmployees; k++)
delete m_employees[k];
}
void Company::hire(string nm, double sal)
{
if (m_nEmployees == 100)
ERROR
m_employees[m_nEmployees] = new Employee(nm, sal, this);
m_nEmployees++;
}
void Company::setBonusRate(double rate)
{
m_bonusRate = rate;
}
void Company::giveBonuses() const
{
for (int k = 0; k < m_nEmployees; k++)
m_employees[k]->receiveBonus(); // previously passed m_bonusRate
}
double Company::bonusRate() const
{
return m_bonusRate;
}
int main()
{
Company myCompany;
myCompany.hire("Ricky", 80000);
myCompany.hire("Lucy", 50000);
myCompany.setBonusRate(.02);
myCompany.giveBonuses();
Company yourCompany;
yourCompany.hire("Fred", 40000);
}
- what happens when a company goes away?
- in real world model, employees remain, find another job
- however, this implementation is not representative of the real world
- thus, make sure to document which elements of reality are accounted for in programs
- in this case, the employees go away when the company goes away
- when adding new data members, make sure to check constructors and destructors for additional behavior
- different possible implementations of a bonus function
- company contains the bonus function, must ask for parts of the employee
- employee contains the bonus function and is passed the bonus rate
- employee contains a zero parameter bonus function
- so now has to ask company for its bonus rate, but how does employee know which company to ask?
- need pointers both ways
class Complex
{
public:
Complex(double re, double im);
Complex();
double real() const;
double imag() const;
private:
double m_rho; // polar, more efficient apparently
double m_theta;
};
Complex::Complex(double re, double im)
{
m_rho = sqrt(re*re + im*im);
m_theta = atan(im, re);
}
Complex::Complex() // can have multple constructors with different number of arguments and/or types
{ // function overloading works for any functions, not just constructors
m_rho = 0;
m_theta = 0;
}
double Complex::real() const
{
return m_rho * cos(m_theta);
}
double Complex::imag() const
{
return m_rho * sin(m_theta);
}
int main()
{
Complex c1(4, -3); // 4-3i
cout << c1.real(); // writes 4
Complex ca[100]; // won't compile, no default constructor
}
- you can overload a function name if the functions differ in the number or types of parameters
- overloading is not possible in C
- would need functions with different names, eg.
drawRect(), drawCirc()
- vs. in C++, would only need one function named
draw()
Code example:
void draw(Rectangle r);
void draw(Circle c);
int main()
{
Rectangle a;
Circle c;
draw(a);
draw(b)
}
void f(int i);
void f(double d);
void g(int i, double d);
void g(double d, int i);
int main()
{
f(3);
f(3.0);
// what if there's not an exact match?
f('A'); // not int or double, but will be treated as an int
g(1, 2); // ambigiuous! no best function! compilation error!
}