Legacy C++: Raw Memory, C Arrays & Casts
Many Indian IT jobs in C++ involve existing code: banking systems, telecom software, embedded devices and game engines that have been maintained for ten or twenty years. Much of that code was written before C++11. It owns memory through raw pointers, passes C arrays with a separate length, uses char* strings, C-style casts and macros. You need to read it confidently, change it without breaking it, and know which modern tool replaces each old pattern.
From C++98 to C++23 gave you the big picture. This lesson goes deeper into the patterns that cause real bugs in old code, and shows how to make them safe step by step.
Raw ownership and the rule of three
Before smart pointers, a class that owned heap memory had to manage it by hand. Here is a typical C++98 class:
#include <cstring>
class Name { // C++98 style
public:
explicit Name(const char* text) : data_(new char[std::strlen(text) + 1]) {
std::strcpy(data_, text);
}
~Name() { delete[] data_; }
const char* c_str() const { return data_; }
private:
char* data_;
};
int main() {
Name a("Ananya");
Name b = a; // copies the POINTER: a and b share one buffer
} // both destructors delete[] the same memory: double freeThe compiler-generated copy constructor copies each member, so b.data_ points at the same memory as a.data_. This is a shallow copy. When main ends, both destructors free the same block, which is undefined behaviour. The old fix is the rule of three: a class that needs a destructor almost always needs its own copy constructor and copy assignment operator too.
#include <cstring>
#include <print>
#include <utility>
class Name {
public:
explicit Name(const char* text) : data_(new char[std::strlen(text) + 1]) {
std::strcpy(data_, text);
}
Name(const Name& other) : Name(other.data_) {} // deep copy
Name& operator=(Name other) { // copy-and-swap
std::swap(data_, other.data_);
return *this;
}
~Name() { delete[] data_; }
const char* c_str() const { return data_; }
private:
char* data_;
};
int main() {
Name a("Ananya");
Name b("Ishaan");
b = a;
std::println("{} {}", a.c_str(), b.c_str());
}Ananya Ananya
The assignment takes its argument by value (that makes the copy), swaps buffers with it, and lets the parameter's destructor free the old buffer. This copy-and-swap idiom is common in older code because it is safe even if copying throws. The modern answer is much shorter: store a std::string, write no special functions at all, and follow the rule of zero from Destructors & RAII.
Also check every delete in old code against its new:
| Allocated with | Must be freed with | Modern replacement |
|---|---|---|
new T | delete p | std::make_unique<T>() |
new T[n] | delete[] p | std::vector<T> or std::make_unique<T[]>(n) |
std::malloc | std::free | Containers; malloc does not run constructors |
Mixing the forms (delete on an array, free on new memory) is undefined behaviour, and AddressSanitizer reports it as an alloc-dealloc mismatch.
C arrays, pointers and lengths
Old functions usually take a pointer to the first element and a separate count. Inside such a function the array's size is lost, and the famous sizeof trick silently gives the wrong answer:
#include <print>
void show(int marks[]) { // really: int* marks
std::println("inside: {}", sizeof(marks) / sizeof(marks[0]));
}
int main() {
int marks[5] = {78, 92, 65, 88, 71};
std::println("outside: {}", sizeof(marks) / sizeof(marks[0]));
show(marks);
}outside: 5 inside: 2
Inside show, marks is a pointer, so sizeof(marks) is the size of a pointer (8 bytes on a typical 64-bit system) divided by the size of an int (4 bytes). GCC warns about this with -Wall. When you cannot change an old function's signature, wrap the pointer and count in a std::span as soon as they enter new code. A span keeps them together, works with range-for, and can be bounds-checked by hardened libraries:
#include <span>
int total(const int* marks, int count) { // old interface stays
std::span<const int> view(marks, static_cast<std::size_t>(count));
int sum = 0;
for (int m : view) {
sum += m;
}
return sum;
}C strings
Legacy code stores text in char arrays ending with a zero byte ('\0'). Functions such as strcpy and sprintf write past the end of the array if the text is too long, a classic buffer overflow. If you must fill a fixed buffer, use std::snprintf, which never writes more than the size you give it and returns the length it wanted to write, so you can detect truncation:
#include <cstdio>
#include <cstring>
#include <print>
#include <string>
int main() {
char city[8];
int needed = std::snprintf(city, sizeof city, "%s", "Thiruvananthapuram");
std::println("needed {} chars, kept: {} ({} chars)", needed, city, std::strlen(city));
std::string full = "Thiruvananthapuram";
std::puts(full.c_str()); // hand a std::string to a C function
}needed 18 chars, kept: Thiruva (7 chars) Thiruvananthapuram
Seven characters plus the terminating zero fill the 8-byte buffer. In new code keep text in std::string and call .c_str() (or .data()) only at the moment you call an old C function. Going the other way is easy too: std::string s = some_c_string; copies the text.
C-style casts and the named casts
A C-style cast (T)x tries several kinds of conversion until one compiles, including ones that remove const or reinterpret raw memory. It hides dangerous conversions inside harmless-looking syntax. C++ has four named casts, each allowed to do one kind of job, and each easy to search for:
| Cast | Use it for |
|---|---|
static_cast<T>(x) | Ordinary conversions: double to int, enum to number, base to derived when you are sure |
dynamic_cast<T*>(p) | Checked downcast in a class hierarchy; returns nullptr if the object is not a T |
const_cast<T*>(p) | Removing const to call an old API that forgot it but does not write |
reinterpret_cast<T*>(p) | Viewing memory as an unrelated type; low-level code only |
const int seats = 72;
int* p1 = (int*)&seats; // compiles: const silently removed
*p1 = 80; // undefined behaviour: seats is really const
// int* p2 = static_cast<int*>(&seats); // does not compile: static_cast keeps constWhen you modernise, replace each C-style cast with the named cast that states its intent. If only reinterpret_cast or const_cast will compile, you have found code that deserves a closer look.
Macros
Old code uses #define for constants and small "functions". A macro is pure text replacement, so arguments can be evaluated twice:
#include <algorithm>
#include <print>
#define MAX(a, b) ((a) > (b) ? (a) : (b))
int main() {
int tickets = 5;
int biggest = MAX(++tickets, 4); // ++tickets appears twice after expansion
std::println("macro: biggest = {}, tickets = {}", biggest, tickets);
int seats = 5;
int largest = std::max(++seats, 4); // a real function: evaluated once
std::println("std::max: biggest = {}, tickets = {}", largest, seats);
}macro: biggest = 7, tickets = 7 std::max: biggest = 6, tickets = 6
The macro expands to ((++tickets) > (4) ? (++tickets) : (4)), so tickets is increased once for the comparison and again for the result. Replace function-like macros with constexpr or template functions and constant macros with constexpr variables. Macros are still the right tool for include guards and for code that must differ per platform (#ifdef _WIN32).
Wrapping C APIs with smart pointers
Old and C libraries return handles that you must release with a special function. A std::unique_ptr with a custom deleter turns such a handle into an RAII object without writing a whole class:
#include <cstdio>
#include <memory>
struct FileCloser {
void operator()(std::FILE* f) const { std::fclose(f); }
};
using FilePtr = std::unique_ptr<std::FILE, FileCloser>;
int main() {
FilePtr log{std::fopen("backup-log.txt", "w")};
if (log) {
std::fputs("Backup finished\n", log.get()); // get(): raw pointer for the C function
}
} // fclose runs automatically, even if an exception is thrownunique_ptr only calls the deleter when it holds a non-null pointer, so a failed fopen is handled correctly.
Common mistakes
- Copying a class that owns a raw pointer without a user-written copy constructor: double free.
- Using
deletefornew[]memory, orfreefornewmemory. - Trusting
sizeof(arr) / sizeof(arr[0])inside a function. The array has already become a pointer. - Using
strcpyorsprintfinto fixed buffers. Usestd::string, orstd::snprintfwith the buffer size. - Silencing a compiler error with a C-style cast. The error was probably right.
Key points
- Pre-C++11 classes that own memory need the rule of three; modern classes use members such as
std::stringand need none. - Match
newwithdelete,new[]withdelete[],mallocwithfree, or better, avoid all of them. - C arrays decay to pointers and lose their size; wrap pointer and count in
std::span. - Use
std::stringand.c_str()at C boundaries; neverstrcpyinto a fixed buffer. - Replace C-style casts with named casts and function-like macros with
constexprfunctions.
Exercise
An old billing module has char* make_invoice_id(int number) that returns new char[16] filled with sprintf(buf, "INV-%d", number), and callers must remember delete[]. Write a modern replacement that cannot leak or overflow, and show it with invoice number 2026.
Show solution
#include <format>
#include <print>
#include <string>
std::string make_invoice_id(int number) {
return std::format("INV-{}", number);
}
int main() {
std::string id = make_invoice_id(2026);
std::println("{} has {} characters", id, id.size());
}INV-2026 has 8 characters
std::string owns its memory, so there is nothing to delete, and std::format sizes the text itself, so there is no fixed buffer to overflow. If an old C function still needs a const char*, pass id.c_str().
Quick Quiz
1. A C++98 class has a destructor that calls delete[] on a member pointer, but no copy constructor. What happens when an object is copied?
The generated copy constructor copies the pointer value, which leads to a double free.
2. Inside void f(int a[]), what is sizeof(a)?
Array parameters are really pointers, so the array's size is lost.
3. Which cast should you use to convert a double price to int paise?
static_cast performs ordinary value conversions.
4. Why is #define MAX(a, b) ((a) > (b) ? (a) : (b)) risky?
Text replacement copies each argument expression wherever it appears.
Learn C++ with a mentor
Free lessons get you started. Our Java, C & C++ Programming training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
