Skip to content
C++
Language
since C++11
Basic

C++ for C Developers

A focused guide for C developers adopting C++: what's actually different, what you can keep, what to unlearn, and the 10 C++ idioms that pay off immediately.

If you're a C developer, you're already 70% of the way to writing good C++. The machine model is identical. The syntax is a superset. Most C code compiles as C++ with minor edits. This page focuses on what's actually different β€” the idioms that make C++ more than "C with classes."

What you keep

Everything in C works in C++ (with a few minor exceptions):

  • Pointers, pointer arithmetic, malloc/free (though you'll use them less)
  • struct, union, enum, typedef
  • All operators and control flow
  • #include, #define, preprocessor
  • Inline assembly
  • Casting (though C++ adds safer cast operators)
  • volatile for memory-mapped hardware
  • extern "C" to mix C and C++ translation units

Immediate wins: 5 things to adopt now

1. References instead of pointer parameters

c
// C: pointer parameter β€” caller must pass address
void double_value(int* p) { *p *= 2; }
int x = 5;
double_value(&x);   // caller: &x

// C++: reference parameter β€” simpler syntax, no null allowed
void double_value(int& n) { n *= 2; }
int x = 5;
double_value(x);    // caller: just x β€” no &

// Output parameters: pointer (C style) vs reference (C++ style)
void parse(const char* s, int* out_value, bool* out_ok);  // C
void parse(std::string_view s, int& value, bool& ok);     // C++

References are aliases β€” not nullable, not rebindable, not pointer-arithmetic-able. Use them for "this thing must exist and we won't null-check it."

2. std::string instead of char*

c
// C: manual string management
char* greeting = (char*)malloc(32);
snprintf(greeting, 32, "Hello, %s!", name);
// ... remember to free(greeting) ...

// C++: automatic
std::string greeting = "Hello, " + name + "!";
// automatic allocation, automatic deallocation
// no buffer overflows, no manual sizing

// Interop: still works with C APIs
printf("greeting: %s\n", greeting.c_str());
fopen(path.c_str(), "r");

std::string is a value type β€” it copies when assigned, moves when returned (C++11). No malloc/free, no size tracking.

3. std::vector instead of manually-sized arrays

c
// C: dynamic array β€” manual everything
int* buf = (int*)malloc(n * sizeof(int));
if (!buf) { /* handle */ }
memset(buf, 0, n * sizeof(int));
// ... use buf ...
free(buf);

// C++: automatic
std::vector<int> buf(n, 0);  // n zeros, auto-allocated
buf.push_back(42);           // grows automatically
// no free β€” ~vector() cleans up

std::vector<T> is a contiguous array β€” same memory layout as a C array. Pass buf.data() to C APIs that expect T*.

4. Range-for instead of index loops

cpp
// C style (still valid C++):
for (int i = 0; i < n; i++) {
    process(arr[i]);
}

// C++ range-for β€” no off-by-one:
for (int x : arr) { process(x); }         // read-only copy
for (int& x : arr) { x *= 2; }            // modify in place
for (const auto& x : arr) { print(x); }   // const reference (no copy)

Works on arrays, std::vector, std::string, std::map, and anything with begin()/end().

5. Function overloading

c
// C: type-specific names
int    abs_i(int x)    { return x < 0 ? -x : x; }
double abs_d(double x) { return x < 0 ? -x : x; }
float  abs_f(float x)  { return x < 0 ? -x : x; }

// C++: same name, different types
int    abs(int x)    { return x < 0 ? -x : x; }
double abs(double x) { return x < 0 ? -x : x; }
float  abs(float x)  { return x < 0 ? -x : x; }
// compiler selects by argument type

The big shift: RAII β€” resources tied to objects

This is the most important mental shift from C to C++. In C, resources have a manual lifetime:

c
FILE* f = fopen("log.txt", "w");
if (!f) { /* handle error */ }
// ... write ...
fclose(f);   // if you return early or throw: LEAK

In C++, resources are owned by objects whose destructors clean up automatically:

cpp
#include <fstream>

void write_log() {
    std::ofstream f("log.txt");   // opens in constructor
    if (!f) throw std::runtime_error("cannot open");
    f << "log entry\n";
    // ~ofstream() closes the file automatically β€” even on early return or exception
}

The key insight: the destructor always runs when the object goes out of scope, regardless of how the scope is exited (normal return, early return, or exception). You never need fclose, free, pthread_mutex_unlock etc. in normal code.

RAII classes you get for free:

cpp
std::unique_ptr<T>    // owns heap memory (replaces malloc/free)
std::lock_guard<M>    // holds a mutex (replaces pthread_mutex_lock/unlock)
std::ifstream         // file handle
std::vector<T>        // dynamic array
std::string           // string buffer

Namespaces β€” no more name collision workarounds

c
// C: prefix everything to avoid collisions
int mylib_init();
void mylib_cleanup();
typedef struct MyLib_Config { ... } MyLib_Config;

// C++: group in a namespace
namespace mylib {
    int init();
    void cleanup();
    struct Config { ... };
}

mylib::init();   // explicit namespace
// Or:
using namespace mylib;   // import all (avoid in headers)
using mylib::init;       // import one name

Classes β€” structs with methods (and more)

In C++, struct and class are nearly identical. The only difference: struct members default to public, class members to private.

cpp
// C: struct with functions that take a pointer
typedef struct {
    int x, y;
} Point;
void point_move(Point* p, int dx, int dy) { p->x += dx; p->y += dy; }

// C++: struct with member functions
struct Point {
    int x = 0, y = 0;

    void move(int dx, int dy) { x += dx; y += dy; }   // implicit `this` pointer
    double distance_to(const Point& other) const;      // `const` method: doesn't modify *this
};

Point p{3, 4};
p.move(1, 1);      // calls p.move() β€” p is the implicit `this`

Constructors and destructors are the RAII entry points:

cpp
class FileHandle {
    FILE* f_;

public:
    explicit FileHandle(const char* path, const char* mode)
        : f_(fopen(path, mode))
    {
        if (!f_) throw std::runtime_error(std::string("cannot open: ") + path);
    }

    ~FileHandle() noexcept { if (f_) fclose(f_); }   // guaranteed cleanup

    FILE* get() const noexcept { return f_; }

    // Prevent accidental copies (double-close)
    FileHandle(const FileHandle&) = delete;
    FileHandle& operator=(const FileHandle&) = delete;
};

FileHandle log{"log.txt", "w"};
fprintf(log.get(), "hello\n");
// ~FileHandle() closes when log goes out of scope

Smart pointers β€” replacing malloc/free

c
// C: heap allocation β€” you must free
Widget* w = (Widget*)malloc(sizeof(Widget));
widget_init(w, id);
// ... use w ...
widget_destroy(w);
free(w);

// C++: unique_ptr β€” sole owner, auto-freed
auto w = std::make_unique<Widget>(id);
// ... use w ...
// ~unique_ptr<Widget>() calls Widget destructor and frees memory automatically

// Transfer ownership
auto w2 = std::move(w);   // w is now empty; w2 owns the Widget

// Share ownership (like refcounting)
auto shared = std::make_shared<Widget>(id);
auto copy   = shared;   // both own it; freed when last one is destroyed

What to unlearn

❌ Don't use NULL β€” use nullptr:

cpp
// C: NULL is just (void*)0 or 0
FILE* f = NULL;
if (f == NULL) { }

// C++: nullptr is type-safe
FILE* f = nullptr;
if (f == nullptr) { }
// nullptr doesn't accidentally match int overloads

❌ Don't use malloc/free for objects:

cpp
Widget* w = (Widget*)malloc(sizeof(Widget));  // no constructor called!
free(w);                                       // no destructor called!

Widget* w = new Widget(id);   // calls constructor (still avoid)
delete w;                     // calls destructor (still avoid)

auto w = std::make_unique<Widget>(id);  // best: constructor + auto destructor

❌ Don't cast with C-style casts:

cpp
// C: (int)x or (Widget*)ptr
// C++: named casts that express intent
static_cast<int>(x)           // well-defined numeric conversion
static_cast<Derived*>(base)   // downcast (unsafe if wrong type)
reinterpret_cast<int*>(ptr)   // reinterpret bit pattern (dangerous)
const_cast<int*>(cptr)        // remove const (only for C interop)

❌ Don't use typedef struct:

cpp
// C: typedef needed for struct name
typedef struct { int x; } Point;

// C++: struct is already a type name
struct Point { int x; };   // no typedef needed
Point p;                   // just works

C interop β€” when you need both

cpp
// Calling a C library from C++ β€” just include and use
extern "C" {
    #include <some_c_library.h>
}

// Wrapping a C resource in RAII
struct CFileDeleter {
    void operator()(FILE* f) const { if (f) fclose(f); }
};
using CFile = std::unique_ptr<FILE, CFileDeleter>;

CFile open_file(const char* path, const char* mode) {
    return CFile{fopen(path, mode)};
}

C β†’ C++ transition checklist

C habitC++ replacementWhy
malloc/freemake_unique, make_shared, vectorRAII, exception safety
char* stringsstd::string, std::string_viewBounds safety, no manual sizing
C arrays T arr[N]std::array<T,N>, std::vector<T>Size tracking, iterator support
NULLnullptrType safe, no int ambiguity
typedef structplain structStruct names are already type names
C-style cast (T)xstatic_cast<T>(x)Expresses intent, searchable
printf/scanfstd::print/std::format (C++20/23)Type safe, no %s/%d mismatch
FILE* + fopen/fclosestd::ifstream/std::ofstreamRAII, exception integration
Manual error codes + gotoExceptions or std::expectedPropagation without boilerplate
pthread_mutex_*std::mutex + std::lock_guardRAII unlock, portable