Advanced C++

Memory usage

Máté Cserép

April 2021, Budapest

Overview

  • Storage classes, lifetime
  • Difference between new expression and the new operator
  • New, placement new, nothrow new,
  • Custom new operators and their delete pair 
  • Overloading class new and global new operators
  • Objects only on heap, objects never on heap
  • Arrays and polymorphism
  • Exceptions and memory handling, RAII 
  • Smart pointer fundamentals
    auto_ptr, unique_ptr, shared_ptr, weak_ptr
  • Source and sink strategy
  • Reference counting solutions vs. copying

Storage types

const char *hello1 = "Hello World";
      char *hello2 = "Other Hello";
      
hello1[1] = 'a';    // syntax error
hello2[1] = 'a';    // could cause runtime error

char *s = const_cast<char *>(hello1);   // dangerous
s[3] = 'x';                             // could be runtime error

Constant data: values are known at compile-time

This kind of data stored outside of the program read-write area. Therefore a write attempt is undefined.

Automatic life: non-static objects local to a block has automatic life. 

void f()
{
  int i = 2;  // life starts here with initialization
  /* .... */
}             // life finished here

Such objects are created in the stack. The stack is safe in a multithreaded environment. Objects are created when the declaration is encountered and destroyed when control leaves the declaration block.

Dynamic life

char *p = new char[1024];   // life starts here
/* .... */
delete [] p;                // life finished here

Objects with dynamic life are created in the free store. The lifetime starts with the evaluation of a new expression (not the new operator). The life ends at the delete expression.

There are two kind of allocation: one for single objects and one for arrays. The delete operation must be corresponding with the type of the new operation.

char *p1 = new char[1024];
char *p2 = new char[1024];

delete [] p1;
delete [] p1;   // could be runtime error: p1 deleted twice
delete    p2;   // could be runtime error: p2 points to array

The free store is often referred as the heap. This is not correct: free memory (new) and heap (malloc) is not necessarily the same.

Static life

date d(2015,3,13);      // life of "d", "i" starts here
static int i;

int main() {            // initialization / constr call happens here
  /* ... */
}                       // destr call happens here

Global variables, namespace variables, and static data members have static life. Their life starts at the beginning of the program, and ends at the of it. The order of creation is well-defined inside a compilation unit, but no defined between source files.

Local statics are declared inside a function as local variables, but with the static keyword. The life starts (and the initialization happens) when the declaration first time encountered and ends when the program is finishing.

int main() {
  while (/* ... */)
    if (/* ... */) {
      static int j = 6;   // initialization happens here
    }
  return 0;
}                         // destr call happens here

Array elements

Array elements are created with the array itself in order of indices, and destroyed when the array itself is deleted.

 

Built-in arrays by the standard must have their size known by the compiler (constant expression).

 

Some compiler, like GNU G++ is submissive: they accept variable size arrays with warnings.

Note: in C99 variable size arrays are accepted.

Object attributes

Non-static data members are created when their holder object is created.

If they have constructor, then their constructor will be woven into the container object constructor.

The sub-objects will be initialized by their constructor.

Built-in types have no constructor, so they must be explicitly initialized.

struct X {
    int i;      // will not be initialized
};

int main() {
  X x;
  x.i;          // undefined
}

X::X() : i(0)  { }   // use initialization list!

Union

  • Can have member functions, but not virtual ones.
  • Cannot have base classes or be used as a base class.
  • Cannot have data members of reference types.
  • Cannot contain a non-static data member with a non-trivial special member function (until C++11).
/* Union */
union A {
    int i;
    double d;
    char c;
}; // the whole union occupies max(sizeof(int), sizeof(double), sizeof(char))

int main()
{
    A a = { 43 }; // initializes the first member, a.i is now the active member
    // at this point, reading from a.d or a.c is UB
    std::cout << "a.i = " << a.i << std::endl;
    a.c = 'a'; // a.c is now the active member
    // at this point, reading from i or d is UB but most compilers define this
    std::cout << "a.i = " << a.i << std::endl; // 97 most likely
    return 0;
}

Temporaries

void f(string &s1, string &s2, string &s3)
{
  const char *cs = (s1+s2).c_str();
  cout << cs;           // Bad!!

  if (strlen(cs = (s2+s3).c_str()) < 8 && cs[0] == 'a' )  // OK
  cout << cs;           // Bad!!
}

Temporary objects:

  • Created under the evaluation of an expression
  • Destroyed when full expression has been evaluated

Temporaries

void f(string &s1, string &s2, string &s3)
{
    cout << s1 + s2;
    string s = s2 + s3;

    if (s.length() < 8 && s[0] == 'a' )
      cout << s;
}

The correct way:

Another correct way (lifetime extension):

void f(string &s1, string &s2, string &s3)
{
    cout << s1 + s2;
    const string &s = s2 + s3;

    if (s.length() < 8 && s[0] == 'a' )
      cout << s;  // Ok
} // s1+s2 destroys here, when the const ref goes out of scope

New and Delete

namespace std
{
    class bad_alloc : public exception { /* ... */ };
}

void* operator new(size_t)              // operator new() may throw bad_alloc
void  operator delete(void *) noexcept; // operator delete() never throws

The new operator throws std::bad_alloc, when the allocation fails.

Exceptions are not always the desired solution, sometimes we want to avoid them. The nothrow version of new operation returns null pointer rather throwing exception.

// indicator for allocation that doesn't throw exceptions
struct nothrow_t {};
extern const nothrow_t nothrow; 

// nothrow version
void* operator new(size_t, const nothrow_t&);
void  operator delete(void*, const nothrow_t&) noexcept;

Note: the compiler needs to free the memory it allocated via operator new with a matching call to operator delete, hence the overload for delete.

New and Delete

int main() {
    try {
        while(true) {
            new int[100000000ul];   // throwing overload
        }
    } catch (const std::bad_alloc& e) {
        std::cout << e.what() << std::endl;
    } 
    while (true) {
        int* p = new(std::nothrow) int[100000000ul]; // non-throwing overload
        if (p == nullptr) {
            std::cout << "Allocation returned nullptr"  << std::endl;
            break;
        }
    }
}
MyClass *mc1 = new MyClass(42);               // throwing overload
MyClass *mc2 = new(std::nothrow) MyClass(42); // non-throwing overload

Usage:

More complex example:

New and Delete

void* operator new[](size_t);
void  operator delete[](void*) noexcept;

void* operator new[](size_t, const nothrow_t&);
void  operator delete[](void*, const nothrow_t&) noexcept;

There are separate operators for arrays.

typedef void (*new_handler)();
new_handler set_new_handler(new_handler new_p) noexcept;

What to do, when error occurs on allocation:

The new-handler function is the function called by allocation functions whenever a memory allocation attempt fails.
Possible options:

  1. make more memory available somehow;
  2. terminate the program (e.g. by calling std::terminate);
  3. throw exception of type std::bad_alloc or subtype (default).

New and Delete: Placement

void* operator new(size_t, void* p)  { return p; }
void  operator delete(void* p, void*) noexcept { }

void* operator new[](size_t, void* p)  { return p; }
void  operator delete[](void* p, void*) noexcept { }

Sometimes we want to create objects without allocating new memory. There is a placement version of new operator.

struct Test
{
  Test(int a, int b, int c) : a(a), b(b), c(c) { }
  int a, b, c;
};

int main()
{
  for(int i = 0; i < 100000000; ++i)
  {
    Test *t = new Test(1, 2, 3);
    /* ... */
    delete t;
  }
  return 0;
}

This is a way to optimize running time, e.g.:

New and Delete: Placement

struct Test
{
  Test(int a, int b, int c) : a(a), b(b), c(c) { }
  int a, b, c;
};

int main()
{
  std::aligned_storage<sizeof(Test), alignof(Test)>::type als[1];
  for(int i = 0; i < 100000000; ++i)
  {
    Test *t = new(als) Test(1, 2, 3);
    /* ... */
    t->~Test();
  }
  return 0;
}

The optimized code using placement new:

Result for creating 100.000.000 objects:

  • Normal new: 5309ms
  • Placement new: 593ms

Overloading the new and delete operators

New and delete operators could be defined by the user in two levels:

  1. As a static member function of a class
  2. As a global operator

 

The member new and delete is responsible for allocating objects for that certain class.

The global operator however is responsible for all memory allocation.

Overloading the new and delete operators

struct Test
{
  Test(int d) : data(d) { cerr << "Test::Test"  << endl; }
  ~Test()               { cerr << "Test::~Test" << endl; }

  /* static */ void* operator new(size_t sz);
  /* static */ void  operator delete(void* p) noexcept;
  int data;
};

// member new and delete
void* Test::operator new(size_t sz)
{
  cerr << "Test::new" << endl;
  return ::operator new(sz);
}
void Test::operator delete(void* p) noexcept
{
  cerr << "Test::delete" << endl;
  ::operator delete(p);
}

// global new and delete
void* operator new(size_t sz)
{
  cerr << "::new" << endl;
  return malloc(sz);
}
void operator delete(void* p) noexcept
{
  cerr << "::delete" << endl;
  free(p);
}

int main()
{
    Test *t = new Test(42);
    /* ... */
    delete t;

    return 0;
}

Extra parameters for new and delete

struct Test
{
  Test(int d) : data(d) { cerr << "Test::Test"  << endl; }
  ~Test()               { cerr << "Test::~Test" << endl; }

  /* static */ void* operator new(size_t sz, int d);
  /* static */ void  operator delete(void* p) noexcept;
  int data;
};

// global new and delete
void* operator new(size_t sz)
{
  cerr << "::new" << endl;
  return malloc(sz);
}
void operator delete(void* p) noexcept
{
  cerr << "::delete" << endl;
  free(p);
}

void* operator new(size_t sz, int d)
{
  cerr << "::new, data:" << d << endl;
  return malloc(sz);
}
void operator delete(void* p, int d) noexcept
{
  cerr << "::delete, data:" << d << endl;
  free(p);
}

// member new and delete
void* Test::operator new(size_t sz, int d)
{
  cerr << "Test::new, data:" << d << endl;
  return ::operator new(sz, d);
}
void Test::operator delete(void* p) noexcept
{
  Test* t = reinterpret_cast<Test*>(p);
  cerr << "Test::delete, data:" << t->data << endl;
  ::operator delete(p, t->data);
}

int main()
{
  int x;
  while (cin >> x)
  {
    Test *t = new(x) Test(x);
    /* ... */
    delete t;
  }
  return 0;
}

As we can overload new and delete operators at class or global level:
we can provide extra parameters, for debug or other purposes.

Smart pointers

struct Res
{
    char* cp;
    Res(int n) { cp = new char[n]; }
    ~Res() { delete [] cp; }
    char *getcp() const { return cp; }
};

void f()
{
    Res res(1024);

    g(res.getcp());
    h(res.getcp());
}

Stroustrup: Resource Allocation Is Initialization

But be careful:

struct BadRes
{
    char* cp;
    BadRes(int n) { cp = new char[n]; ... init(); ... }
    ~BadRes()     { delete [] cp; }
    void init()   {  ... if (error) throw XXX; ... }
};

Motivation

We solve these problems with smart pointers for memory resources.


The main issue with smart pointers is that who have to free the resources? Two main strategies exist for smart pointers:

  1. ownership
  2. reference count

auto_ptr

auto_ptr<int> p(new int(42));
auto_ptr<int> q;

cout << "after initialization:" << endl;
cout << " p: " << (p.get() ? *p : 0) << endl;
cout << " q: " << (q.get() ? *q : 0) << endl;

q = p;
cout << "after assigning auto pointers:" << endl;
cout << " p: " << (p.get() ? *p : 0) << endl;
cout << " q: " << (q.get() ? *q : 0) << endl;

*q += 13;  // change value of the object q owns
p = q;
cout << "after change and reassignment:" << endl;
cout << " p: " << (p.get() ? *p : 0) << endl;
cout << " q: " << (q.get() ? *q : 0) << endl;

The auto_ptr is a ownership-based smart-pointer. Copy constructor and assignment transfers the ownership.

Definition of auto_ptr

template<class T>
class auto_ptr
{
private:
    T* ap;    // refers to the actual owned object (if any)
public:
    typedef T element_type;

    // constructor
    explicit auto_ptr (T* ptr = 0) throw() : ap(ptr) { }

    // copy constructors (with implicit conversion)
    // - note: nonconstant parameter
    auto_ptr (auto_ptr& rhs) throw() : ap(rhs.release()) { }

    template<class Y>
    auto_ptr (auto_ptr<Y>& rhs) throw() : ap(rhs.release()) { }

    // assignments (with implicit conversion)
    // - note: nonconstant parameter
    auto_ptr& operator= (auto_ptr& rhs) throw()
    {
        reset(rhs.release());
        return *this;
    }
    template<class Y>
    auto_ptr& operator= (auto_ptr<Y>& rhs) throw()
    {
        reset(rhs.release());
        return *this;
    }

    // destructor
    ~auto_ptr() throw()
    {
        delete ap;
    }

    // value access
    T* get() const throw()
    {
        return ap;
    }
    T& operator*() const throw()
    {
        return *ap;
    }
    T* operator->() const throw()
    {
        return ap;
    }

    // release ownership
    T* release() throw()
    {
        T* tmp(ap);
        ap = 0;
        return tmp;
    }

    // reset value
    void reset (T* ptr=0) throw()
    {
        if (ap != ptr)
        {
            delete ap;
            ap = ptr;
        }
    }
};

Issues with auto_ptr

auto_ptr<int>  p1(new int(1));  // Ok
auto_ptr<int>  p2 = new int(2); // Error, explicit constructor

auto_ptr has explicit constructor:

Ownership strategy:

auto_ptr<int>  p1(new int(1));
auto_ptr<int>  p2(new int(2));
p2 = p1;  // delete 2, transfers ownership from p1

Copies of auto_ptr are not equivalent:

template <class T>
void print( auto_ptr<T> p)
{
    if (p.get())
        cout << *p;
    else
        cout << "null";
}
auto_ptr<int>  p(new int);
*p = 4;
print(p);
*p = 5;     // runtime error

Issues with auto_ptr

const auto_ptr<int>  p(new int), q;
*p = 4;
print(p);   // compile-time error: cannot change ownership
*p = 5;     // ok: const of ownership
 q = p;     // compile-time error: q const

To avoid this mistakes we may ensure keeping the ownership:

vector< auto_ptr<int> >  v;

v.push_back( auto_ptr<int>( new int(1) ) );
v.push_back( auto_ptr<int>( new int(4) ) );
v.push_back( auto_ptr<int>( new int(3) ) );
v.push_back( auto_ptr<int>( new int(2) ) );

sort( v.begin(), v.end() ); // What about the pivot element, etc...
// unique(), remove(), etc...

auto_ptr is not suitable for STL containers:

Memory Handling Strategy for auto_ptr

There is a so-called: source-sink strategy using auto_ptr class.

Source is a function, which creates new memory resource and transfers it back to the caller. Sink is a function, which gets the ownership from its caller, and removes the memory resource.

auto_ptr<int> source(int n)
{
    auto_ptr<int> p(new int(n));
    //  ...
    return p;  // transfers ownership to the caller
}

void sink( auto_ptr<int>) { };  // removes memory resource
auto_ptr<int> p1 = source(1);  // create, get ownership
// ...
sink(p1);     // gets ownership and delete p1 on return

Usage of auto_ptr as class member

class B;

class A
{
private:
    const auto_ptr<B> p1;
    const auto_ptr<B> p2;
public:
    A( B b1, B b2) : p1(new B(b1)), p2(new B(b2))  {  }
    A( const A& rhs) : p1( new B(*rhs.p1)), p2( new B(*rhs.p2))  {  }
    A& operator=(const A& rhs)
    {
        *p1 = *rhs.p1;
        *p2 = *rhs.p2;
        return *this;
    }
};

We do not need a destructor anymore.

Arrays and auto_ptr

void f(int n)
{
    auto_ptr<int> p1(new int);
    auto_ptr<int> p2(new int[n]);
    //...
} // delete and not delete[] for p2

The auto_ptr is not able to manage arrays, because the destructor wired-in to call non-array delete.

An array delete adapter is a solution to manage arrays by auto_ptr:

template <typename T>
class ArrDelAdapter
{
public:
    ArrDelAdapter(T *p) : p_(p) { }
    ~ArrDelAdapter() { delete [] p_; }
    // implement operators like ->, *, etc...
private:
    T* p_;
};
struct X
{
    X()  { std::cout << "X()"  << std::endl; }
    ~X() { std::cout << "~X()" << std::endl; }
};

int main()
{
    std::auto_ptr< ArrDelAdapter<X> > pp(
        new ArrDelAdapter<X>(new X[10]));
    return 0;
}

auto_ptr

Summary:

  • The only smart pointer in C++98/03
  • Cheap, ownership-based
  • Not works well with STL containers and algorithms
  • Not works with arrays
  • Deprecated in C++11
  • Removed from C++ since C++17

Smart pointers in C++11

In order to fix the caveats of auto_ptr, C++11 introduced new smart pointers types:

  • unique_ptr
  • shared_ptr
  • weak_ptr

At the same time auto_ptr became deprecated in C++11 and removed in C++17.

unique_ptr

struct MyClass
{
    MyClass() { cout << "MyClass ctor runned." << endl; }
    ~MyClass() { cout << "MyClass dtor runned." << endl; }
};

int main() 
{
    unique_ptr<MyClass> up1(new MyClass);
    up1 = make_unique<MyClass>();  // since C++14
	
    {
        cout << "Entered inner block." << endl;
        unique_ptr<MyClass> up2(new MyClass);
 
        //up1 = up2;        // Syntax error
        up1 = move(up2);    // Ownership moved from up2 to up1
        up2.reset();	    // No memory deallocation
        cout << "Leaving inner block." << endl;
    }
    cout << "Left inner block." << endl;
 
    up1.reset();		    // Memory deallocation
    cout << "Leaving main() function." << endl;
    
    return 0;
} 

shared_ptr

struct MyClass
{
  MyClass() { cout << "MyClass ctor runned." << endl; }
  ~MyClass() { cout << "MyClass dtor runned." << endl; }
};

int main() 
{
  shared_ptr<MyClass> sp1(new MyClass);
  cout << "Use count: " << sp1.use_count() << endl;

  shared_ptr<MyClass> sp2 = sp1;
  cout << "Use count: " << sp1.use_count() << endl;

  sp1.reset();
  cout << "Use count: " << sp2.use_count() << endl;

  {
    cout << "Entered inner block." << endl;
    shared_ptr<MyClass> sp3 = sp2;
    cout << "Use count: " << sp2.use_count() << endl;
    cout << "Leaving inner block." << endl;
  }
  cout << "Left inner block." << endl;
  cout << "Use count: " << sp2.use_count() << endl;

  cout << "About to delete the last shared pointer." << endl;
  sp2.reset();
  cout << "Deleted the last shared pointer." << endl;

  return 0;
}

weak_ptr

struct MyClass
{
  MyClass() { cout << "MyClass ctor runned." << endl; }
  ~MyClass() { cout << "MyClass dtor runned." << endl; }
};

int main() 
{
  shared_ptr<MyClass> sp1 = make_shared<MyClass>(); // since C++11
  cout << "Use count: " << sp1.use_count() << endl;

  weak_ptr<MyClass> wp = sp1;
  cout << "Use count: " << sp1.use_count() << endl; // Only shared_ptr counts,
                                                    // weak_ptr does not
  cout << "Use count: " << wp.use_count() << endl;

  {
    cout << "Entered inner block." << endl;
    shared_ptr<MyClass> sp2 = wp.lock();
    cout << "Use count: " << sp1.use_count() << endl;

    if(sp2)
      cout << "Lock acquired." << endl;
    cout << "Leaving inner block." << endl;
  }
  cout << "Left inner block." << endl;
  cout << "Use count: " << sp1.use_count() << endl;

  cout << "About to delete the last shared pointer." << endl;
  sp1.reset();
  cout << "Deleted the last shared pointer." << endl;

  shared_ptr<MyClass> sp3 = wp.lock();
  if(!sp3)
    cout << "Shared object does not exists anymore." << endl;

  if(wp.expired())	// Not thread safe solution.
    cout << "Shared object does not exists anymore." << endl;

  return 0;
}

shared_ptr and cyclic references

struct MyClass
{
  shared_ptr<MyClass> next;

  MyClass(shared_ptr<MyClass> next)
  {
    this->next = next;
    cout << "Constructor completed for a MyClass object." << endl;
  }

  virtual ~MyClass()
  {
    cout << "Destructor completed for a MyClass object." << endl;
  }
};

int main() 
{
  shared_ptr<MyClass> nullp;
  shared_ptr<MyClass> sp1(new MyClass(nullp));
  shared_ptr<MyClass> sp2(new MyClass(sp1));

  {
    cout << "Entered inner block." << endl;
    shared_ptr<MyClass> sp3(new MyClass(sp2));
    sp1->next = sp3;	// Create cyclic referencing
    cout << "sp3 use count: " << sp3.use_count() << endl;
    cout << "Leaving inner block." << endl;
  }
  cout << "Left inner block." << endl;

  cout << "sp1 use count: " << sp1.use_count() << endl;
  cout << "sp2 use count: " << sp2.use_count() << endl;
  cout << "sp3 use count: " << sp1->next.use_count() << endl;

  sp1.reset();
  sp2.reset();

  cout << "Released sp1 and sp2, dtors still not executed." << endl;

  return 0;
}

Custom deleters for smart pointers

shared_ptr<MyClass> sp(new MyClass, [](auto p) {
  std::cout << "Freeing resource..." << std::endl;
  delete p;
});
struct MyClassDeleter { 
    void operator()(MyClass* p) const {
        std::cout << "Freeing resource..." << std::endl;
        delete p;
    }
};

shared_ptr<MyClass> sp(new MyClass, Deleter);

An optional deleter expression can be defined to implement a custom method to free the allocated resource.

Using a functor:

Using a lambda expression:

The default deleter simply frees the pointer, properly handling arrays.

Enable shared from this

MyClass* p = new MyClass;
shared_ptr<MyClass> sp1(p);
shared_ptr<MyClass> sp2(p);                       // bad: sp2 != sp1
shared_ptr<MyClass> sp3 = make_shared<MyClass>(); // bad: sp3 != sp1

shared_ptr<MyClass> sp4 = sp1;                    // good: sp4 == sp1

Creating multiple distinct shared_ptr objects with separate reference counts is a trap, which shall be avoided.

class MyClass
{
public:
    std::shared_ptr<MyClass> getPtr() {
        return shared_ptr<MyClass>(this);
    }
};

shared_ptr<MyClass> sp1(new MyClass);
shared_ptr<MyClass> sp2 = sp1->getPtr(); // bad: sp2 != sp1
shared_ptr<MyClass> sp3 = sp1->getPtr(); // bad: sp3 != sp1 && sp3 != sp2

The naive technique of just returning a shared_ptr to this leads to separate reference counts:

Enable shared from this

Instead use the enable_shared_from_this base class:

class MyClass : public std::enable_shared_from_this<MyClass>
{
public:
    std::shared_ptr<MyClass> getPtr() {
        return shared_from_this();
    }
};

shared_ptr<MyClass> sp1(new MyClass);
shared_ptr<MyClass> sp2 = sp1->getPtr(); // good: sp2 == sp1
shared_ptr<MyClass> sp3 = sp1->getPtr(); // good: sp3 == sp2 == sp1

How does it work:

  • enable_shared_from_this<T> has a weak_ptr<T> data member;
  • shared_ptr<T> constructor can detect if T is derived from enable_shared_from_this<T>;
  • If so, shared_ptr<T> constructor will assign *this to the weak_ptr data member in enable_shared_from_this<T>.

Objects with
Special Storage Restrictions

Sometimes restriction of object storage can be useful:

  • Objects only in the Heap
  • Objects not in the Heap

Objects only in the Heap

class X {
public:
  X() {}
  void destroy() const { delete this; }
protected:
  ~X() {}
};

class Y : public X { };    // inheritance is ok
class Z { X x; };          // syntax error, use pointer
X x1;                      // syntax error
int main() {
    X x2;                  // syntax error
    Y y1;                  // ok, would be syntax error if ~X() was private
    X* xp = new X;         // ok
    Y* yp = new Y;         // ok

    delete xp;             // syntax error
    xp->destroy();         // ok
    delete yp;             // ok, would be syntax error if ~X() was private
};

The following class X could be created only by new operator in the HEAP. This is important when new takes extra parameters, or defines special semantics: like persistence or garbage collection.

Objects never in the Heap

class X
{
protected:
  static void* operator new(size_t sz);
  static void  operator delete(void* p) noexcept;
  // static void operator delete(void *) noexcept = delete; in C++11
};

class Y : public X { };  // inheritance is ok
class Z { X x; };        // ok
X x1;                    // ok
int main()
{
  X x2;                  // ok
  Y y1;                  // ok
  X* xp = new X;         // syntax error
  Y* yp = new Y;         // syntax error

  delete xp;             // syntax error
  delete yp;             // syntax error
};

Advanced C++: Memory usage

By Cserép Máté

Advanced C++: Memory usage

  • 156