Writing a Safer Constructor
Constructor is the most basic and important concept in C++.
Take a look the basic example here, and try finding the problems.
The header file: DebugOnly.h for logging debug message
#ifndef DebugOnly_h_
#define DebugOnly_h_
#include <iostream>
#include <sstream> // std::stringstream
#include <string> // std::string
#define DEBUG true
void Log(std::string msg)
{
DEBUG && std::cout << msg << std::endl;
}
template<class T>
std::string ToAddressString(const T& t)
{
const void* address = static_cast<const void*>(&t);
std::stringstream ss;
ss << address;
return ss.str();
}
#endif // #ifndef DebugOnly_h_
The core part for our demo:
#include <iostream>
#include "DebugOnly.h"
using std::string;
struct Person
{
string name;
unsigned int age;
Person(string name, int age)
: name(name)
, age(age)
{
}
};
class Book
{
public:
Book(string title, int price, Person* author)
: title(title)
, price(price)
, author(author)
{
}
string Title()
{
return title;
}
unsigned int Price()
{
return price;
}
Person& Author()
{
return *author;
}
~Book()
{
Log("~Book: " + title);
// Release it to avoid memory leak
delete author;
}
private:
string title;
unsigned int price;
Person* author;
};
// Helper Functions
// ===================================
void LogBook(Book& book)
{
Log("[" + ToAddressString(book) + "] " + // memory address
book.Title() + ", " + // book's title
std::to_string(book.Price()) + ", " + // book's price
book.Author().name + ", " + // author's name
std::to_string(book.Author().age)); // author's age
}
void LogPerson(Person& person)
{
Log("[" + ToAddressString(person) + "] " + // momery address
person.name + " " + // name
std::to_string(person.age)); // age
}
// Main Program
// ===================================
int main()
{
Log("----- book 1 -----");
{
Person* ChunMin = new Person("ChunMin", 20);
LogPerson(*ChunMin);
Book book1 = Book("C++ Notes", 179, ChunMin);
LogBook(book1);
}
Log("----- book 2 -----");
Person Jimi("Jimi", 18);
LogPerson(Jimi);
{
Book book2 = Book("Python Notes", 79, &Jimi);
LogBook(book2);
}
}
Do you find the problems in the above example? If you already find it, cheers! You might not need to read the remaining parts. If you don't, don't worry, I have a good news to you: You will learn something in this post!
The Gone Jimi
Let's see the executed result of the above example:
----- book 1 -----
[0x7fb402404a90] ChunMin 20
[0x7fff594cb8a8] C++ Notes, 179, ChunMin, 20
~Book: C++ Notes
----- book 2 -----
[0x7fff594cb858] Jimi 18
[0x7fff594cb818] Python Notes, 79, Jimi, 18
~Book: Python Notes
a.out(2100,0x7fff7293d300) malloc: *** error for object 0x7fff594cb858: pointer being freed was not allocated
*** set a breakpoint in malloc_error_break to debug
Abort trap: 6
The error is occured at address 0x7fff594cb858
.
The problem is that:
There is no allocated object there to be freed!
Why? We can easily prove that
there is a Person
object whose name is Jimi
allocated at
address 0x7fff594cb858
from the log.
Why my computer will tell me that the Jimi
is gone?
The truth is that:
The Jimi
object is actually freed twice!
The lifetime of the variable is depended on
its containing scope('{' and '}').
The book1
will be freed once the program runs
beyond the '}' after LogBook(book1);
.
You can see that the book1
's deconstructor is called
between LogBook(book1);
and Log("----- book 2 -----");
.
Similarly, the book2
's deconstructor will be called
upon the program runs beyond the '}' after LogBook(book2);
.
At that time, the memory pointed by the book2.author
,
which is exactly the Jimi
object, will be freed.
That is the first time the Jimi
object will be released.
Next, after the program runs beyond the final '}',
the Jimi
object will be freed again
because the program already exceeds
its containing scope(the '{' and '}' of main
).
That is the second time.
However, one memory space can't be freed twice,
and that's why we get an error there.
Leave Jimi alone
How to avoid that kind of problems? It's pretty easy.
The answer is that we have to clone the Jimi
in book2
.
That is, we have to clone our own Person
object,
author, in the Book
class.
Then, we don't need to worry about we'll kill the wrong guy
in the Book
's deconstructor anymore.
We will always kill our own copied author data in the Book
's deconstructor.
Thus, we should write a safer constructor like:
#include "DebugOnly.h"
using std::string;
struct Person
{
string name;
unsigned int age;
Person(string name, int age)
: name(name)
, age(age)
{
}
};
class Book
{
public:
Book(string title, int price, Person& author) // Using reference instead of pointer
: title(title)
, price(price)
, author(new Person(author)) // Create its own data here!
{
}
string Title()
{
return title;
}
unsigned int Price()
{
return price;
}
Person& Author()
{
return *author;
}
~Book()
{
Log("~Book: " + title);
// Release it to avoid memory leak
delete author;
}
private:
string title;
unsigned int price;
Person* author;
};
// Helper Functions
// ===================================
void LogBook(Book& book)
{
Log("[" + ToAddressString(book) + "] " + // memory address
book.Title() + ", " + // book's title
std::to_string(book.Price()) + ", " + // book's price
book.Author().name + ", " + // author's name
std::to_string(book.Author().age)); // author's age
}
void LogPerson(Person& person)
{
Log("[" + ToAddressString(person) + "] " + // momery address
person.name + " " + // name
std::to_string(person.age)); // age
}
// Main Program
// ===================================
int main()
{
Log("----- book 1 -----");
{
Person* ChunMin = new Person("ChunMin", 20);
LogPerson(*ChunMin);
Book book1 = Book("C++ Notes", 179, *ChunMin); // Using reference instead of pointer
LogBook(book1);
}
Log("----- book 2 -----");
Person Jimi("Jimi", 18);
LogPerson(Jimi);
{
Book book2 = Book("Python Notes", 79, Jimi); // Using reference instead of pointer
LogBook(book2);
}
}
And here is the executed result, without error:
----- book 1 -----
[0x7fdd9a404a90] ChunMin 20
[0x7fff59c6f8a8] C++ Notes, 179, ChunMin, 20
~Book: C++ Notes
----- book 2 -----
[0x7fff59c6f858] Jimi 18
[0x7fff59c6f818] Python Notes, 79, Jimi, 18
~Book: Python Notes
Actually, we might directly use a Person
object as our member variable
more often than a pointer to Person
object.
The new
operator at author(new Person(author))
in the second example will request a memory chunk
as big as the Person
object needed from heap,
and then the allocated memory will be initialized by Person
's constructor
with the input author
's data(the auther
in Person(author)
).
Finally, the address will be set to the Book.author
(the auther
outside of the (new Person(...))
).
If there're lots of Book
objects,
we will request lots of memory chunks from heap.
It will lead the memory fragment problem
(please refer our memory fragment to get more detail),
especially when we don't know when the Book
objects will be released.
Therefore, the less new
operators are used,
the less fragment are generated in memory.
Don't let Jimi mess about in memory
struct Person
{
string name;
unsigned int age;
Person(string name, int age)
: name(name)
, age(age)
{
}
};
class Book
{
public:
Book(string title, int price, Person& author) // Using reference instead of pointer
: title(title)
, price(price)
, author(author) // Create its own data by copy!
{
}
string Title()
{
return title;
}
unsigned int Price()
{
return price;
}
Person& Author()
{
return author;
}
~Book()
{
Log("~Book: " + title);
}
private:
string title;
unsigned int price;
Person author;
};
It's not only more intuitive,
but also can avoid the memory fragment problem
when there're lots of Book
objects.