C++ classes and dynamic memory allocation

1. Dynamic memory and classes

Static data members are declared in the class declaration and initialized in the file containing the class methods. The scope operator is used during initialization to indicate the class to which the static member belongs. If the static member is an integral or enumeration type const, it can be initialized in the class declaration.

When using new in the constructor to allocate memory, you must use delete in the corresponding destructor to release the memory. If you use new[] to allocate memory, you should use delete[] to free the memory.

class StringBad
{
private:
	char *str;
	int len;
	static int num_strings;
public:
	StringBad(const char* s);
	StringBad();
	~StringBad();
	friend std::ostream &operator<<(std::ostream &os, const StringBad &st);
};
 
int StringBad::num_strings = 0;
 
StringBad::StringBad(const char*s) {
	len = strlen(s);
	str = new char[len + 1];
	strcpy(str, s);
	num_strings++;
}
 
StringBad::StringBad()
{
	len = 4;
	str = new char[4];
	strcpy(str, "C++");
	num_strings++;
}
 
StringBad::~StringBad()
{
	num_strings--;
	delete[] str;
}
 
ostream& operator<<(ostream &os, const StringBad &st)
{
	os << st.str;
	return os;
}

The above is an incomplete class, which will be used for experiments below.

	{
		StringBad s1("headline11");
		StringBad s2 = s1;
	}

Which constructor does StringBad s2 = s1 use? Not a default constructor, nor a const char* constructor. An initialization of the form above is equivalent to the following statement:

StringBad s2 = StringBad(s1);

When using an object to initialize another object, the compiler will automatically generate the above constructor (copy, copy constructor) to create a copy of the object for it, but the automatically generated constructor does not know that the static variable num_string needs to be updated, Therefore, the count will be messed up, and the same memory will be destroyed twice during destruction.

2. Special member functions

C++ automatically provides the following member functions:

2.1, the default constructor

If no constructor is defined, the compiler will provide a default constructor. A parameterized constructor can also be a default constructor, as long as all parameters have default values, for example if there is a definition like this:

class Klunk {
public:
	Klunk() { ct = 0; };
	Klunk(int n = 0) { ct = n; };
private:
	int ct;
};
 
Klunk a(1);
Klunk b;     // error

2.2, the default destructor

2.3, copy constructor

If no copy constructor is defined, the compiler provides a default copy constructor. The copy constructor is used to copy an object to a newly created object. It is used in the initialization process instead of the normal assignment process. The prototype is usually: Class_name(const Class_name&). Create a new object and initialize it to an existing object of the same type, the copy constructor will be called, and the following declarations will call the copy constructor:
 

	StringBad s1;
	StringBad s2(s1);
	StringBad s3 = s2;
	StringBad s4 = StringBad(s3);
	StringBad *s5 = new StringBad(s4);

Whenever a program makes a copy of an object, the copy constructor is used. Passing by value will create a copy of the original variable, so the copy constructor will also be called, so pass by reference should be used when passing classes.

The default copy constructor will copy non-static members one by one (shallow copy), copying the value of the member, so the equivalent of the copy constructor is as follows: static variables belong to the entire class and are not affected.

		StringBad s1("headline11");
        // StringBad s2 = s1;
		StringBad s2;
		s2.len = s1.len;
		s2.~StringBad = s1.str;

But shallow copying cannot solve the problem of freeing the same block of memory twice in 12.1, which requires defining an explicit assignment constructor to solve the problem (deep copying).

StringBad::StringBad(const StringBad& st)
{
	num_strings++;
	len = st.len;
	str = new char[len + 1];
	strcpy(str, st.str);
}

The reason for defining a copy constructor is that some class members are pointers to data initialized with new, rather than the data itself.

2.4. Assignment operator

If no assignment operator is defined, the compiler will provide a default assignment operator whose prototype is as follows:

Class_name & Class_name::operate=(const Class_name &)

When assigning an existing object to another object, the overloaded assignment operator is used.

	{
		StringBad s1("headline11");
		StringBad s2;
		s2 = s1;
        // StringBad s3 = s1; // copy construction
	}

s2 = s1 uses the overloaded assignment operator, and StringBad s3 = s1 belongs to the initialization and uses the copy constructor.

The above assignment operator will also cause problems, because its implicit implementation is to copy each member one by one. The solution is to provide a deep copy assignment operator. The implementation method is similar to the copy constructor, but there are differences:

Since the target object may refer to previously allocated data, delete should be used to release these data; the function should avoid assigning the object to itself, otherwise, before reassigning the object, the release memory operation may delete the object's content; the function returns a pointer to the call reference to the object.
 

StringBad& StringBad::operator=(const StringBad &st)
{
	if (this == &st)
		return *this;
	if (str != NULL)
	{
		delete[] str;
		str = NULL;
	}
	str = new char[st.len + 1];
	len = st.len;
	strncpy(str, st.str, len+1);
	return *this;
}

2.5, address operator

Returns the value pointed to by this pointer by default

3. Description of the returned object

When member functions or stand-alone functions return objects, there are several return options:

3.1. Return a reference to a const object

A common reason to use a const reference is to improve efficiency, and if a function returns the object passed to him, it can be improved by returning a reference. Returning an object will call the copy constructor, but returning a reference will not, so the second one in the example below is more efficient.

StringBad MaxLen(const StringBad& st1, const StringBad& st2)
{
	if (st1.len > st2.len)
		return st1;
	else
		return st2;
}
 
const StringBad& MaxLen2(const StringBad& st1, const StringBad& st2)
{
	if (st1.len > st2.len)
		return st1;
	else
		return st2;
}

3.2. Return a reference to a non-const object

Two common cases of returning a non-const object are overloading the assignment operator and overloading the << operator for use with cout:

StringBad& StringBad::operator=(const StringBad &st)
{
	if (this == &st)
		return *this;
	if (str != NULL)
	{
		delete[] str;
		str = NULL;
	}
	str = new char[st.len + 1];
	len = st.len;
	strncpy(str, st.str, len+1);
	return *this;
}
 
s3 = s2 = s1;

The above is an example of returning a non-const object reference, which can be used to avoid calling the copy constructor to create a new object.

3.3, return object

If the returned object is a local variable in the called function, it should not be returned by reference, because after the called function is executed, the local object will call its destructor, and the object pointed to by the reference will no longer exist.

StringBad operator+(const StringBad& st1, const StringBad& st2)
{
	StringBad ret;
	ret.len = st1.len + st2.len;
	if (ret.str != NULL)
	{
		delete[] ret.str;
		ret.str = NULL;
	}
	ret.str = new char[ret.len + 1];
	strncpy(ret.str, st1.str, st1.len);
	strncpy(ret.str+st1.len, st2.str, st2.len+1);
	return ret;
}
 
	{
		StringBad s1("HELLO WORLD");
		StringBad s2(" OK");
		StringBad s3;
		s3 = s1 + s2;
		cout << s3.str;
	}

The above code creates a temporary variable ret, and when it returns, it will call the copy constructor to create an object that the calling program can access, and then call the assignment operator to assign a value to s3.

3.4, return const object

The above code that returns an object can also be used as follows:

		StringBad s1("HELLO WORLD");
		StringBad s2(" OK");
		StringBad s3;
        s1 + s2 = s3;

But this is not as expected, the + operator should be overloaded as follows:

const StringBad operator+(const StringBad& st1, const StringBad& st2)

To summarize again, if a method wants to return a local object, it should return the object, not a reference to the object. In this case, a copy constructor will be used to generate the returned object. If a method returns an object of a class that does not have a public copy constructor, it must return a reference to such an object. If a method can return an object or a reference, then a reference is preferred.

Tags: C++ programming language

Posted by rocket on Sun, 01 Jan 2023 23:52:07 +0300