Consider the following hello world program written in C:
#include <stdio.h>
int main() {
printf("hello c%dcpp!\n", 2);
}
Let’s write a Makefile that builds it for us.
hello: hello.o
gcc hello.o -o hello
hello.o: hello.c
gcc -g -Wall -c -o hello.o hello.c
A Makefile is a collection of rules, where each rule lists some targets,
some prerequisites, and a recipe of commands to run when executing the
rule. Here, we’ve broken down the build into compilation and linking, with a
rule for compiling the source code (hello.c
) into an object file (hello.o
)
and then another rule for linking the object file into an executable (hello
).
When we run make
in a directory with just the Makefile
and hello.c
, make
does the following:
make
will execute the default rule, which in this case is the hello
rule whose target is the hello
executablehello.o
is missing, so make
recursively executes the
hello.o
rule first, compiling hello.c
into hello.o
hello.o
is produced, make
continues with the hello
rule, linking
the object file into the hello
executableSubsequent invocations of make
will examine the timestamps on the targets and
prerequisites to determine which rules need to be executed again and which rules
can be omitted.
We’ll also add some phony targets, as shown below. These don’t correspond to
actual files in our directory; we define them for our convenience. When we run
make clean
we just want make
to run the rm
command to delete the binary
files produced by compilation and linking. make all
will first execute the
clean
rule, and then the hello
rule. Since clean
deletes all binary files,
this effectively rebuilds them all.
.PHONY: clean
clean:
rm -f *.o hello
.PHONY: all
all: clean hello
make
can deduce compilation and linking commands in most cases but we need to
define some Makefile variables to specify which compiler and flags to use.
Below, we rewrote the Makefile to delete the explicit commands for compilation
and linking. The CC
variable specifies gcc
as the command for compilation
and linking. When gcc
is invoked for linking, it will call the real linker
ld
and specify the necessary C standard library on our behalf. The CFLAGS
variable lists the compilation flags to use when gcc
is invoked for compiling.
We list -g
so that the compiler includes debugging information and -Wall
so
the compiler emits all warnings.
CC = gcc
CFLAGS = -g -Wall
hello: hello.o
hello.o: hello.c
.PHONY: clean
clean:
rm -f *.o hello
.PHONY: all
all: clean hello
A valid C program is usually a valid C++ program, but C++ is not a strict
superset of C. We’ll see a bunch of examples of this later, but it’s something
to keep in mind when porting C code to C++. In this case, our hello.c
is
totally fine as hello.cpp
! Let’s rewrite our Makefile to build it in C++:
CC = g++
CXX = g++
CXXFLAGS = -g -Wall -std=c++20
hello: hello.o
hello.o: hello.cpp
.PHONY: clean
clean:
rm -f *.o hello
.PHONY: all
all: clean hello
The CXX
variable defines the C++ compiler. Note that we also redefined CC
.
This is because make
uses the CC
variable to compose the linking command,
and we need to invoke g++
to link with the C++ standard library rather than
the C standard library.
We’ve also added -std=c++20
to the compilation flags – it tells the compiler
what version of the C++ standard to use. C++ is still an evolving language, so
it’s good practice to specify the C++ standard version to be explicit about what
features you depend on. This’ll be the default standard version we use unless
otherwise specified.
iostream
With compilation and linking out of the way, let’s rewrite our hello
C++
program to be more idiomatic.
#include <iostream>
int main() {
std::cout << "hello c" << 2 << "cpp!" << std::endl;
}
We’ve included the C++ iostream
header and are now writing to std::cout
using the put-to (<<
) operator. We’ll study the iostream
library in
greater detail later, but for now, focus on the std::cout
and std::endl
.
std::cout
is an ostream
object that corresponds to C stdout
.
std::endl
writes a '\n'
to the ostream
and flushes it.
Coming from C, you might think that <<
means bitwise left-shift, but that’s
clearly not the case here. In C++, you can overload what operators do in
many cases. The C++ standard library has defined operator<<()
such that, when
the left-hand operand is an ostream
object, the right-hand operand gets
written to it. Here, we see that operator<<()
can seamlessly work with
different types, like string literals and integers.
We can also see the chaining you can do with the put-to operator. This is
because of the left-associativity of the operator and the fact that the operator
keeps returning the same std::cout
object in every subexpression so we can
keep writing to it.
C++20 introduced std::format()
which lets you perform Python-like string
formatting, allowing us to alternatively write the print line as follows:
std::cout << std::format("hello c{:d}cpp!", 2) << std::endl;
What’s up with the std::
prefix on cout
and endl
? It refers to the C++
standard library namespace, where those objects are defined. In C++, symbols can
be defined within namespaces, so it’s not always enough to include the header
that declares the symbol; you’ll have to qualify the reference with the
namespace it is in.
C++ namespaces are a huge improvement from C. Consider a huge C codebase that
links with several, potentially external, libraries. It’s inevitable that some
symbol names clash. You’d have to prefix symbol names with some unique-ish name
to avoid collisions, like c2cpp_my_function()
. That’s the gist of C++
namespaces.
The using namespace std
statement below in the main()
function allows us to
drop the std::
prefix from cout
and endl
.
#include <iostream>
int main() {
using namespace std;
cout << "hello c" << 2 << "cpp!" << endl;
}
It’s good practice to restrict the scope of the using namespace
statement to
avoid polluting the current namespace. We chose to write the statement within
the main()
function where we need it, instead of at the top of the file in the
global namespace. Generally, you should avoid placing using namespace
statements in a header file since that can pollute whatever source files that
include it.
We can define our own namespace like this:
#include <iostream>
namespace c2cpp {
int main() {
using namespace std;
cout << "hello c" << 2 << "cpp!" << endl;
return 0;
}
} // namespace c2cpp
int main() {
return c2cpp::main();
}
A C++ program requires a main()
function in the global namespace, so we
defined one that simply calls our c2cpp::main()
.
The C++ standard library defines a string
object. At last, no more dealing
with char *
! Here, we declare an empty string object s
, assign it to
"hello"
, and then reassign it to the concatenation of s
and "world"
.
string s;
s = "hello";
s = s + "world";
That’s awfully verbose though; can’t we just write it like this?
s = "hello " + "world ";
Nope, we get a compilation error:
error: invalid operands of types ‘const char [7]’ and ‘const char [7]’ to binary ‘operator+’
s = "hello " + "world ";
~~~~~~~~ ^ ~~~~~~~~
| |
| const char [7]
const char [7]
Coming from Python or Java, this would be total nonsense, but this makes sense
coming from C. You can’t add two arrays in C. In general, C++ avoids changing
existing C behavior, so it’s not possible to overload operator+()
for this
kind of expression.
What about this kind of expression?
s = s + 100
In Java, it appends the string “100”. In Python, this expression throws a
TypeError
since Python won’t let you append an integer to a string. Both make
sense. C++ acts like Python here; there is no operator+()
overload for string
and int
.
error: no match for ‘operator+’ (operand types are ‘std::string’ {aka ‘std::__cxx11::basic_string<char>’} and ‘int’)
s = s + 100;
~ ^ ~~~
| |
| int
std::string {aka std::__cxx11::basic_string<char>}
If s = s + 100
gave us an error, we would expect the following expression fail
in the same way:
s += 100
Not quite. In this case, C++ overloads operator+=()
for string
and char
and appends 'd'
(ASCII code 100) to the string.
Remember that string
is part of the C++ standard library, not a built-in
language feature. For some reason, the designers decided that the operator+=()
overload makes sense but not the operator+()
overload.
C++ is a powerful language with a ton of features, but it’s a double-edged sword. It’s full of gotchas and requires a solid understanding of the language fundamentals to use safely and effectively. In this course, you’ll learn to understand the subtleties of the language and learn how to parse C++ documentation that is often dense. Let’s get to work!
Last updated: 2025-08-20