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)
volatilefor memory-mapped hardwareextern "C"to mix C and C++ translation units
Immediate wins: 5 things to adopt now
1. References instead of pointer parameters
// 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: 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: 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 upstd::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
// 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: 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 typeThe 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:
FILE* f = fopen("log.txt", "w");
if (!f) { /* handle error */ }
// ... write ...
fclose(f); // if you return early or throw: LEAKIn C++, resources are owned by objects whose destructors clean up automatically:
#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:
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 bufferNamespaces β no more name collision workarounds
// 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 nameClasses β 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.
// 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:
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 scopeSmart pointers β replacing malloc/free
// 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 destroyedWhat to unlearn
β Don't use NULL β use nullptr:
// 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:
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:
// 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:
// 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 worksC interop β when you need both
// 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 habit | C++ replacement | Why |
|---|---|---|
malloc/free | make_unique, make_shared, vector | RAII, exception safety |
char* strings | std::string, std::string_view | Bounds safety, no manual sizing |
C arrays T arr[N] | std::array<T,N>, std::vector<T> | Size tracking, iterator support |
NULL | nullptr | Type safe, no int ambiguity |
typedef struct | plain struct | Struct names are already type names |
C-style cast (T)x | static_cast<T>(x) | Expresses intent, searchable |
printf/scanf | std::print/std::format (C++20/23) | Type safe, no %s/%d mismatch |
FILE* + fopen/fclose | std::ifstream/std::ofstream | RAII, exception integration |
| Manual error codes + goto | Exceptions or std::expected | Propagation without boilerplate |
pthread_mutex_* | std::mutex + std::lock_guard | RAII unlock, portable |