The goal of this lab is to explore how C++ adds minimal extensions to C in order to provide several features that have become common in object-oriented languages. The subsequent lab will have you use these features to work with some basic C++ code.
This lab introduces key features C++
, and describes how it deffers from C
. Once you’ve completed the reading, we ask you implement a postfix calculator using C++
. If you already know C++, skip to end of lab and start coding.
Many parts of this writeup will ask you to inspect various aspects of compiled C++ code. You can run a C++ compiler by using clang++
(or g++
); to access the resulting assembly use the -S
flag (e.g., clang++ -S ex.cpp
generates assembly in ex.s
); to inspect the data set by the code use lldb
(or gdb
).
You’ll likely find that -O1
will generally create much shorter code without removing useless code that you might have wanted to test something.
You should always give your C++ files the extension .cpp
. Some
In C, if you name a function baz
then it is compiled with label baz
. That means you cannot re-use the name baz
for more than one function.
In C++, function names are only part of the compiled label: argument types and where the name appeared are also part of it. This allows code like
int abs_val(int x) { return x < 0 ? -x : x; }
double abs_val(double x) { return x < 0 ? -x : x; }
int main() {
printf("%d %f", abs_val(-23), abs_val(-20.3f));
}
to be compiled; the compiler picks which version of abs_val
to invoked in each instance using information derived during type checking.
This renaming of functions to arrive at labels is called name mangling.
In C, every function and global variable’s name must be unique among all the files you link with. When you write all your own code, that’s not too hard to do; but when you are using third-party libraries there is no reason to expect them not to conflict in names. Because of this, major C libraries generally pick some kind of prefix to all function names, but it can be annoying to read and write code when virtually every function starts with the same prefix.
Later languages have added in a full module or package system, with full scoping and so on, but C++ uses a simpler mechanism called namespaces. These are included in the name mangling to further decrease the chance of name collisions.
Name mangling for namespaces uses a length-before-name technique, so that namespace welcome
would become 7welcome
in the mangled name. This is important to be able to have numbers in the namespace hierarchy: 7welcome2it
represents the two nested namespaces welcome::it
while 10welcome2it
is the single namespace welcome2it
C++ allows you to redefine what almost every operator does. You can define functions whose name is operator+
, operator<<=
, etc, and they will be used in place of built-in behavior for +
, <<=
, etc, when those operators are applied to the appropriate types.
C++ compilation also makes use of a few other syntactic sugar pieces to help make code easier to write. One example is the pass by reference
syntax, a short-hand way of using pointers. For example, what in C might be
char *strsep_c(char **txt, char delim) {
if (!txt || !*txt) return 0;
char *ans = *txt;
while(**txt && **txt != delim) *txt += 1;
if (**txt == delim) {
*txt = 0;
*txt += 1;
}
return ans;
}
can be written in C++ as
char *strsep_c(char *& txt, char delim) {
if (!txt) return 0;
char *ans = txt;
while(*txt && *txt != delim) txt += 1;
if (*txt == delim) {
txt = 0;
txt += 1;
}
return ans;
}
These two compile to virtually the assembly except that the compiler takes care of ensuring that the second, which uses pass-by-reference syntax, is always given a valid reference so the two-part check in the first is just a 1-part check in the second
Recent versions of C++ have added many other similar tricks for efficient coding with more compile-time checking, such as more careful casts, self-deallocating pointers, etc.
C++ is often characterized as C supporting objected oriented coding
. This section explores several of those concepts:
C++ adds a class
keyword that is identical to struct
in almost every way, except a class
defaults to private:
members and a struct
to public:
. C++ also removes the need to typedef
to get a simple name for a struct
or class
.
C++ also extends struct
and class
to allow them to declare member functions as well as member attributes:
.hpp header |
.cpp implementation |
---|---|
new
and delete
Constructors are an important part of how OO objects are initialized. C++ has added an operator new
that acts like
and an operator malloc
1 and then invoke a constructordelete
that acts like invoke a destructor and then
,free
Thus, we could test the code above with
To add a constructor, make a member function with the same name as the class; a destructor’s name is preceded by a tilde ~
. Neither should have a return type specified. Destructors are usually used to delete
anything the constructor new
ed, and are typically omitted if there is no need to do that.
class gleam2 {
int a;
int b;
public:
int inOrder();
gleam2(int x, int y) { this->a = x; this->b = y; }
~gleam2() { puts("Gone"); }
};
We’ll talk about how C++ implements inheritance and overloading in lecture.
C++ also has a template language
, which looks superficially like Java’s generic syntax but is actually a full programming language in its own right. Most casual C++ programmers I know only use the basic type-generic aspect of templates, not any of their advanced features.
In C, if you have an argument of type int *
you don’t know if it is an array or just a value you are supposed to set. C++ adds syntax to allow these to look different the reference
argument type.
If you write a function where an argument name is followed by an ampersand (e.g., int &x
) then C++ will compile it to be a pointer, but have your syntax look like it is a variable.
The following two pieces of code are equivalent in how they run
C code | C++ code |
---|---|
Both pieces of code turn into equivalent assembly, but the C++ version uses type-checking to ensure that x
is never treated as if it were an array instead of a single reference.
A template in C++ is an outline of how to create code if it is needed. Templates uses angle-brackets, like generics in Java. Java, however, discards the generics during compilation as part of the type checking step. C++ instead uses them to generate special code for each template argument created. Thus, C++ code like
does not create any code, by itself. Instead, each time you use the template with different template parameters the compiler creates code for that parameterization. Thus, if you later have
the compiler will create two different foo
functions: one that has an int
parameter and return type and another that has double
.
Note that the compiler will try to guess what template parameters were missing; if you want to be explicit, you can also write
int x0 = foo(3); // compiler figures out it is a template, and deduces type
int x1 = foo<>(3); // told it is a template, compiler deduces type
int x2 = foo<int>(3); // told it is a template and type
{.aside} Another difference from Java is that the parameters of a template do not need to be type names; they could also be values, like integers. Value templates are unusual enough in C++ we’ll say no more about them here.
{.aside} C++ template metaprogramming is a complicated topic on which entire books have been written. Some libraries, such as boost and FFTW are well-known for complicated template usage. If you find yourself intrigued by templates, I recommend the D language as having a richer, more programmer-friendly template mechanism than C++.
C++ recognizes that there is no magic to operators, and it is awkward to write your matrix class with x.add(y)
when you write your numbers as x + y
instead, so it lets you overload operators. In essence, this lets you add, to any class you want to work with, functions that are the implementation of operators. This lets you write math with your custom matrix class just like you do with the built-in number types.
Once you have this, though, it becomes tempting to use in other ways. One of the most famous is the ostream
type, C’s wrapper over file writing, which overloads the left-shift operator to mean write
.
The following C++ code writes Hello, world!
and then an end-of-line. There are no integers being shifted; <<
means left-shift
for integers, but it means here’s some output
with ostreams like cout
This code also uses a namespace; namespaces are a way of avoiding name collisions, and most of the common C++ libraries we’ll use are in the std
namespace. We could have equivalently written it as
Because <<
is also overloaded to work with multiple types, we can write
#include <iostream>
int main() {
std::cout << "Hello, all " << (3 + 1) << " dimensions!" << std::endl;
}
cout
, by the way, is C++’s ostream
wrapper around file descriptor 1
, just as stdout
is C’s FILE *
wrapper around the same.
Not all ostream
s will flush output as promptly as others. For debugging, you should definitely use cerr
instead of cout
as it is better about showing everything when you ask it to be shown instead of delaying until later.
C++ also allows what it calls
functions, which are used to add functions and operators to existing classes in new files. A common example is to add friend
ostream << mytype
operators for new types.
The C++ Standard Library has a variety of types and algorithms in it. Some of the best known of these are part of the Standard Template Library (STL)
, and sometimes people use STL
informally to mean the full C++ Standard Lbirary. There is more in the library than we can fully cover, but the following will get you going:
include | provides | STL? | description |
---|---|---|---|
<list> |
std::list<T> |
STL | doubly-linked list; key functions push_ /pop_front , push_ /pop_back |
<map> |
std::map<K,V> |
STL | tree-map; key function [key] |
<stack> |
std::stack<T> |
STL | stack; key functions push , pop , top |
<queue> |
std::queue<T> |
STL | queue; key functions push pop , front |
<vector> |
std::vector<T> |
STL | resizable array; key functions [index] , insert , push_ /pop_back |
<iterator> |
std::iterator<T> |
STL | use as for(it = c.begin(); it != c.end(); ++it) for STL iterator it and collection c |
<string> |
std::string |
Wraps a const char * with various useful methods (find , replace , size , += , == , etc) |
|
<iostream> |
std::istream and std:ostream |
C++ file handling; operator << writes and >> reads; defines cin , cout, and cerr |
|
<sstream> |
std::stringstream |
Lets you treat a string like a file |
There are many others not listed above; see wikipedia for a reasonable overview.
Note that many C++ types overload ==
to compare values the way that .equals()
does in Java. Hence "Hi" == "Hi"
compares pointers, but string("Hi") == string("Hi")
compares words.
Implement a postfix calculator, like you did in PA09 but this time use C++.
You must use cin
to read input, >>
to parse numbers, string
and ==
to find operators, and stack<double>
as your stack.
You must print your stack by repeated display-and-pop, as stack
does not have printing capability.
ostream & operator<<(ostream & o, stack<double> s)
to display a stack, and end your program with cout << my_stack;
instead.We strongly recommend, but do not require, using a two-step read: read a word from cin
into a string
, then feed the string
into a stringstream
and use the stringstream to parse a number. However, there are other solutions and you do not have to do this if you do not want to.
We strongly recommend, but do not require, adding using namespace std;
after your #include
s so that you can refer to included types by name without prefixing std::
before each.
The input processing can look like a loop which, as long as cin
is .good()
(i.e., not closed and with no read errors)
>>
a string
from cin
make a stringstream
and <<
a string
into it
>>
a double
from the stringstream
if that >>
ing into the stringstream
causes the stringstream
to .fail()
,
check if the string
is ==
to an operator, and
if so apply the operator (ending if there are fewer than 2 operands)
otherwise, end the loop
otherwise, push the double
Use a stack<double>
as your stack. Note that pop
returns void
; you’ll need to use top
to retrieve a value before pop
ing it off the stack.
Displaying a stack
is not something the STL natively supports, so you’ll need to do that manually.
If you have time, implement your own ostream & operator<<(ostream & o, stack<double> s)
which repeatedly displays (with <<
) the top()
and then pop()
s until the stack is .empty()
. Note that we pass in the ouput stream by reference (with &
), but pass the stack by value. Passing by value means passing a copy, so pop
ing s
does not pop
the stack that was passed in, only the local copy of it that the operator<<
function contains.
C++ generally manages the heap slightly differently when using malloc
vs new
, so never free
something allocated with new
or vice verse.↩︎