Class with dynamic memory
A string example
String class definition
class MyString {
private:
int buf_len;
char * characters;
public:
(int buf_len = 64, char *data = nullptr){
MyStringthis->buf_len = 0;
this->characters = nullptr;
(buf_len, data); // this is a helper function, it will allocate memory and copy data
create}
~MyString(){
delete[] characters;
}
// other member functions
};
- Before a
MyString
object is used, the length of the string is not konwn.- As a result, the memory should be allocated dynamically.
The create
function
bool create(int buf_len, const char * data)
{
this->buf_len = buf_len;
if( this->buf_len != 0)
{
this->characters = new char[this->buf_len]{};
if(data)
(this->characters, data, this->buf_len);
strncpy}
return true;
}
In the main
function, we can use the MyString
class as follow:
int main()
{
(10, "stevens");
MyString str1<< "str1: " << str1 << endl;
cout
= str1;
MyString str2 << "str2: " << str2 << endl;
cout
;
MyString str3<< "str3: " << str3 << endl;
cout = str1;
str3 << "str3: " << str3 << endl;
cout
return 0;
}
- Why memory leak and memory double free?
Break down
Hard copy
Helper functions
release()
: release the memorycreate()
: first release the current memory, then allocate new memory and copy data
bool MyString::release(){
this->buf_len = 0;
if(this->characters!=nullptr){
delete []this->characters;
this->characters = nullptr;
}
return true;
}
bool MyString::create(int buf_len, const char * data){
();
releasethis->buf_len = buf_len;
if( this->buf_len != 0){
this->characters = new char[this->buf_len]{};
}
if(data)
(this->characters, data, this->buf_len);
strncpyreturn true;
}
Copy constructor & copy assignment operator
One reason causes memory leak and double free is that the copy constructor provided by the compiler is not appropriate.
::MyString(const MyString & other){
MyStringthis->buf_len = 0;
this->characters = nullptr;
(other.buf_len, other.characters);
create}
& MyString::operator=(const MyString & other){
MyString(other.buf_len, other.characters);
createreturn *this;
}
- Now each objects has its own memory.
- It’s a hard copy.
Self-assignment
We should improve the copy assignment operator by checking if the source and the target are the same object.
- If the address of the source and the target are the same(self-assignment), do nothing.
- If not, it will release the current object’s memory, then you can’t find the data anymore!
- Otherwise, release the current memory, then allocate new memory and copy data.
& MyString::operator=(const MyString & other){
MyStringif(this == &other)
return *this;
(other.buf_len, other.characters);
createreturn *this;
}
Soft copy
Problem of Hard copy
- Frequently allocate and deallocate memory.
- Not efficient when the memory is large.
Soft copy helps to solve the problem. However,
- Multiple objects share the same memory.
- How to decide when to release the memory?
Solution 1: Reference counting
In each MyString
object, we maintain a reference count.
- The reference count is initialized to 1.
- The count will increase/decrease when the object is assigned or destroyed.
- When the reference count is 0, the memory is released.
- The reference count is stored in a shared part of the memory.
- We can use a integer on the heap to store the reference count.
Solution 2: a struct
to store both the reference count and the memory
Since both the reference count and the memory are used on the heap
- we can use a
struct
to store them together.
=delete
and =default
=delete
is used to disable a function.=default
is used to enable a function that is defined by the compiler by default.
// in class definition
(const MyString & other) = delete;
MyString& operator=(const MyString & other) = delete;
MyString
// main function
= "stevens";
MyString str1 = str1; // error
MyString str2 ;
MyString str3= str1; // error str3
- Now the copy constructor and copy assignment operator are disabled.
- You can’t use copy constructor and copy assignment operator.
Smart pointers
std::unique_ptr
Different from std::shared_ptr
, std::unique_ptr
is a unique owner of the memory.
- It does not allow copy assignment or copy constructor.
- It can be moved to another
std::unique_ptr
, bystd::move()
. - When the
std::unique_ptr
goes out of scope, the memory is released.
std::unique_ptr<MyTime> mt1 = std::make_unique<MyTime>(1, 70);
std::unique_ptr<MyTime> mt_test = mt1; // error
std::unique_ptr<MyTime> mt2 = std::move(mt1); // ok
<< "mt1: " << *mt1 << endl; // segmentation fault
cout << "mt2: " << *mt2 << endl; // ok cout
std::move()
is used to transfer the ownership of the memory.- After the transfer,
mt1
loses the ownership of the memory, and it becomes a null pointer.
Summary
Rule of three
If a class requires a user-defined destructor, copy constructor, or copy assignment operator, it likely requires all three.
- If all the data members are plain data types, no need to define your own.
- If you handle the dynamic memory by yourself, you must define all three.
Alternatively,
- Smart pointers can help you manage the memory automatically.
- You can use
=delete
to disable the copy constructor and copy assignment operator.
Why no many kinds of constructors in Java and Python?
- Programmers in Java and python deal with open files, sockets, database connections, etc.
- These works are often connected to the memory management.
- As a result, Java and Python simplify the memory management by using shared pointers.
- Every object except for primitive types is an object.
- Every copy is a soft copy and object deallocation is handled by the reference count.
- Therefore, C++, which focuses on system-level programming, algorithmic data structures, and high-performance computing as its main business, has developed these concepts.
- It incorporates the following ideas as fundamental elements of the language:
- Copy/move operations
- Pointers
- Mutability
- Multithreading
- Copy/move operations
- These concepts are extremely important in our operations, making them irreplaceable.
Recursion
Recursion is the technique of making a function call itself.
- Break down a problem into smaller problems of the same type.
- Each recursive call should make the problem smaller.
- There should be a base case that terminates the recursion.
int factorial(int n){
if(n == 0)
return 1;
return n * factorial(n-1);
}
- The base case is
n == 0
, which returns 1. - Each recursive call makes the problem smaller by
n-1
. - When
n
becomes 0, the recursion terminates.
Another example:
void div2(int n){
<< "Entering div2 with n = " << n << endl;
cout if(n == 0)
return;
(n/2);
div2<< "Leaving div2 with n = " << n << endl;
cout }
int main(){
(1024);
div2return 0;
}
- The video
Almost all recursive functions can be converted to iterative functions.
Pros of recursion:
- Good at tree traversal
- Less lines of source code
Cons of recursion:
- Consume more stack memory
- May be less efficient
- Hard to implement and debug