COMS W4995 C++ Deep Dive for C Programmers

C to C++ (c2cpp)

Hello World in C

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:

Subsequent 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

Hello World in C++

Compilation

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;

Namespaces

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().

Strings

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