C++ basic grammar (in classes and objects)

Foreword: The knowledge of C++ grammar is complicated, and there are many details to consider. It is not a good way to learn C++ well from the beginning. The content of books is generally more rigorous, but for beginners, many concepts cannot be understood , You may be hit in the head when you come up. Therefore, it is recommended to learn C language well before learning C++, and then listen to the introductory courses. C++ has many grammatical concepts that are a supplement to C language. After learning C language, you can better understand why it is designed in this way. I am also a beginner. This kind of article written is only for summary of notes and analysis and discussion of some concepts, which is convenient for future recall. Due to limited knowledge, mistakes are inevitable. Welcome to read and advise

When I wrote this article on classes and objects, I knew that in order to understand classes and objects, I had to go through a lot of practice. Based on my beginner’s few words, I must be full of mistakes. After thinking about it, I might as well write about beginners. For the understanding of classes and objects, describe the classes and objects in your own eyes, and share your own views, which may provide you with a different perspective. Welcome to advise

In the previous article, we briefly understood the idea of ​​​​classes and the definition of classes. If nothing is added after creating a class, does this class have nothing? In fact, this is not the case, because when creating an empty class, the compiler will create several special member functions by default, namely constructor, copy construction, destructor, assignment operator overloading, const member function, address and const The address operator is overloaded, why are they special? One of the reasons is that these functions are either implemented manually by us or automatically implemented by the compiler , so what are these functions used for? Let's start with destructors and constructors

Table of contents

Constructor 

The purpose of the constructor

constructor definition

 The default constructor generated by the compiler itself

destructor 

Purpose of destructor 

Definition of destructor

copy construction

Uses of copy construction

Definition of copy construction

 The reason why copying by value leads to infinite recursion

operator overloading

What is operator overloading?

Use of operator overloading

assignment operator overloading

Definition of assignment operator overloading 

const member function

What is a const member function

Points to note 

take address operator overloading

const address-of operator overloading


Constructor 

The purpose of the constructor

When writing a program to create a data type, we know that the first thing to do is to initialize the created type, otherwise it will cause the program to crash. For example, we use the C language structure to create a stack data type. There is a size variable to record the number of elements in the stack. At the beginning, we must initialize the value of size to 0. We can write an initialization function to solve

There is no exception in the class. If we create an object of the stack class, then we must initialize the object. We can write an initialization function like C language, but the problem comes, if I forget to write the initialization function What should I do with the function? Maybe this mistake is not easy to make. The terrible thing is that we wrote the initialization function, but forgot to call it. After debugging for a long time, we finally found out that the initialization function was not called. As long as you write enough code, then you must have made such mistakes. Of course, the boss is no exception, but it is really annoying, so it would be better if the object of the class is automatically initialized when it is created, and this initialization work is realized by the constructor. After creating an object of a class , the compiler will automatically call the constructor of this class to initialize the object. If you forget to write the initialization constructor, the compiler will automatically generate a default constructor with no parameters. If you write the constructor yourself, then The compiler will no longer implement it, but will automatically call the constructor you wrote

constructor definition

The compiler can automatically call the constructor, so how does the compiler distinguish whether you are an ordinary member function or a constructor? This is about the second special point of the constructor - the definition of the constructor. The compiler can distinguish between ordinary member functions and constructors. Naturally, the definition of the constructor is different. So what is special about the definition of the constructor?

1. The function name is the same as the class name
2. No return value
3. The constructor can be overloaded

A class's constructor is called only once during the lifetime of the object

class stack
{
public:

	stack()
	{
		_pos = (int*)malloc(sizeof(int) * 4);
		_size = 0;
		_capacity = 0;
	}

	stackpush();
	stackpop();
	stacktop();

private:
	int* _pos;
	int _size;
	int _capacity;
};

As in the above code, the author wrote a no-argument constructor of this class. The function name is the same as the class name and has no return value. Inside the constructor, the variables of the class are initialized. This function does not need to be called by us. After creating a When the object of the class, the compiler will automatically call this constructor to complete the initialization

class stack
{
public:

	stack(int PosSize , int size, int capacity)
	{
		_pos = (int*)malloc(sizeof(int) * PosSize);
		_size = size;
		_capacity = capacity;
	}

	stackpush();
	stackpop();
	stacktop();

private:
	int* _pos;
	int _size;
	int _capacity;
};


int main()
{
	stack T(4, 0, 0);
}

You can define a constructor without parameters. Of course, you can also define a constructor with parameters. For example, in the above code, we define a constructor with parameters. When creating an object of a class, just pass the parameters to the constructor. Specifically Operate as above code

class stack
{
public:

	stack(int PosSize  = 4, int size = 0, int capacity = 0)
	{
		_pos = (int*)malloc(sizeof(int) * PosSize);
		_size = size;
		_capacity = capacity;
	}

	stackpush();
	stackpop();
	stacktop();

private:
	int* _pos;
	int _size;
	int _capacity;
};


int main()
{
	stack T(4, 0, 0);//使用自己传过去的值
    stack T;//使用缺省值
    
    //注意,使用全缺省构造时,如果不传参数,不要写成 stack T(); 这种形式
    //因为编译器无法区分这究竟是一个返回值是类的函数的声明,还是一个不传参的实体类的创建
}

Of course, you can also perform default construction. What I wrote above is the full default construction. You can try other methods. It should be noted that the constructor without parameters and the default constructor are both called default constructors. And there can only be one default constructor. No-argument constructors, all default constructors, and constructors generated by the compiler without writing, can all be considered
as default constructors

 The default constructor generated by the compiler itself

Maybe you have such doubts, if you don’t write the constructor yourself, the compiler can generate it by itself, so why should I bother to write it by myself and let the compiler generate it by itself?

The compiler will indeed automatically generate one, but when formulating the rules, there may be some reasons for consideration. The C++ rules divide the types of variables in the class into built-in types and custom types. Built-in types are the most basic types provided by the language system. type, such as char int float, etc., and a custom type is a type defined by a structure or class

The C++ grammar rules do not stipulate that the no-argument default constructor generated by the compiler must process the built-in type, and for the custom type, it calls the default constructor of the custom type

To put it simply, the constructor generated by the compiler does not handle the built-in type. As for the custom type, let me call the default constructor of your custom type for you. Here are a few examples to see

class stack
{
public:
	void print()
	{
		std::cout << _size << ' ' << _capacity << std::endl;
	}
private:
	int* _pos;
	int _size;
	int _capacity;
};


int main()
{
	stack T;
	T.print();
}

Looking at the above piece of code, we did not write a constructor for the stack, that is to say, the constructor of this class is generated by the compiler at this time. After running, you can see that for the built-in type, the default constructor generated by the compiler is not correct. It processes and prints out garbage values. Do you feel that the default constructor generated by the compiler is useless? The C++ committee is also aware of this problem. Later, by patching, it is allowed to assign initial values ​​​​to the built-in types in the class. If you don’t write a constructor yourself, Then use this initial value, we still use the above code to demonstrate

class stack
{
public:
	void print()
	{
		std::cout << _size << ' ' << _capacity << std::endl;
	}
private:
	int* _pos = nullptr;   //可以给这些内置类型赋初始值
	int _size = 0;
	int _capacity = 0;
};


int main()
{
	stack T;
	T.print();
}

Now we don't write a constructor, but assign an initial value to the built-in type. Although the default constructor generated by the compiler will not handle the built-in type, we can solve it by assigning an initial value.

Of course, the default constructor generated by the compiler itself is not useless. When the class you write has no built-in types and contains other classes (this class must have a default constructor, remind again, no parameter constructor, all missing Provincial constructor, we did not write the default constructor generated by the compiler, which can be considered as the default constructor ), at this time, the default constructor generated by the compiler is sufficient, and the default constructor generated by the compiler itself will call the included class default constructor

destructor 

Purpose of destructor 

When writing a program, not only forgetting to initialize is a headache, but also forgetting to release memory, resulting in the risk of memory leaks. Then the function that can automatically complete the release of memory space is called a destructor. A destructor and a constructor are both It is automatically called by the compiler. When the life cycle of an object is about to end, the compiler will call the destructor of the class to release the memory requested by the object. If we implement the destructor ourselves, the compiler will not generate , if we don't implement it ourselves, the compiler will generate one

Definition of destructor

1. The destructor name is the character ~ before the class name
2. The destructor has no return value and no parameters
3. A class can only have one destructor. If not defined by yourself, the compiler will automatically generate a default destructor, and the destructor cannot be overloaded.
4. When the object life cycle ends, the C++ compilation system automatically calls the destructor

Let's take a look at the code of the destructor, still taking the stack class as an example

class stack
{
public:
	stack()
	{
		_pos = (int*)malloc(sizeof(int)*4);
		_size = 0;
		_capacity = 4;
	}

	~stack()
	{
		free(_pos);
	}

private:
	int* _pos;
	int _size;
	int _capacity;
};

Above we use the destructor to release the space pointed to by _pos. The built-in type will be automatically destroyed after the object life cycle, and we do not need to destroy it ourselves. If we don't write it ourselves, the compiler will automatically generate one. This automatically generated destructor will call the destructor of the custom type to ensure that each type is completely released. If there is still unused memory in the current object release, then the writing of the destructor cannot be omitted

copy construction

Uses of copy construction

The constructor mentioned above is to initialize the variables in the class. If I use this class to create an object T1, after a period of operation, I need to create another object T2, and all the values ​​must be the same as the values ​​of T1. The same, it is equivalent to copying a copy of T1. If I assign the value of T1 to T2 one by one, it is okay to only copy one copy. The pile of code is not good-looking

C++ provides us with a solution, that is, copy construction, which is an overload of the constructor

If you do not explicitly define a copy constructor, the compiler will automatically implement one, but it should be noted that the compiler implements a byte-by-byte copy. If the class contains pointers and points to other resources, it will result in a shallow copy. may cause program error

Definition of copy construction

The parameter of the copy constructor is the object to be copied. This parameter must be passed by reference and cannot be passed by value, otherwise it will cause infinite recursion and the program will crash

class stack
{
public:
	stack(const stack & tmp)
	{
		_pos = (int*)malloc(sizeof(int)*tmp._capacity);
		memcpy(_pos, tmp._pos, sizeof(int) * tmp._size);
		_size = tmp._size;
		_capacity = tmp._capacity;
	}

	~stack()
	{
		free(_pos);
	}

private:
	int* _pos;
	int _size;
	int _capacity;
};


int main()
{ 

  stack T1;
  stack T2(T1);  //如此,便可以将T1的值拷贝给T2
}

From the above code, we can know that T1 is the parameter of the copy construction. It should be noted that for the content pointed to by _pos, it is not possible to directly assign the _pos of T1 to the _pos of T2, then the two _pos point to the same If there is a piece of space, the copy is meaningless. The correct way is to re-open a space of the same size as T1’s _pos for T2’s _pos, and put the content pointed by T1’s _pos into one byte and one word. Copy of section to _pos of T2

 The reason why copying by value leads to infinite recursion

Next, let's discuss another question, why is it said that parameters are called by value instead of by reference, which will cause infinite recursion? Let's analyze it next. First, let's look at the code for passing values. const means that the passed T1 cannot be modified. In order to be more concise, we temporarily remove the const                

class stack
{
public:
	stack(stack tmp)
	{
		_pos = (int*)malloc(sizeof(int)*tmp._capacity);
		memcpy(_pos, tmp._pos, sizeof(int) * tmp._size);
		_size = tmp._size;
		_capacity = tmp._capacity;
	}

	~stack()
	{
		free(_pos);
	}

private:
	int* _pos;
	int _size;
	int _capacity;
};


int main()
{ 

  stack T1;
  stack T2(T1);  
}

The above code is call by value. First of all, we must keep in mind that the constructor is for initialization processing. Although it is called copy construction, it still does the work of the constructor. Copying T1 to T2 is actually using T1 to initialize T2. Call the copy structure of T2, take T1 as a parameter, and pass T1 to tmp. Here is the key to understanding. We know that calling a function by value is to make a temporary copy of the parameter to be passed, that is, copy the value of T1 to tmp. Wait, T1 is an object, tmp is also an object, tmp and T1 belong to the same class, copying T1 to tmp will call the copy structure of tmp, then it can be written as stack tmp(T1), tmp is the same as T2 It is the same class, and then if you want to copy T1 to tmp, you have to pass T1 to the copy constructor of tmp, and then return to the above process, and the program continues to call like this until the stack overflows

operator overloading

What is operator overloading?

In the daily coding process, we will encounter situations where two objects of a class compare in size, determine whether two objects are equal, assign one object to another, and so on. Take the previous stack class as an example, if the two stacks are equal, it is necessary to judge that the contents of the space pointed to by _pos in the two objects are equal, and the size and capacity are equal, so that the two objects can be said to be equal. let's see how to write

class stack
{
public:
	stack(stack tmp)
	{
		_pos = (int*)malloc(sizeof(int)*tmp._capacity);
		memcpy(_pos, tmp._pos, sizeof(int) * tmp._size);
		_size = tmp._size;
		_capacity = tmp._capacity;
	}


    bool stack_if_equal(const stack& T2)
	{
		if (_size != T2._size || _capacity != T2._capacity)
		{
			return false;
		}
		
		for (int i = 0; i < _size; i++)
		{
			if (_pos[i] != T2._pos[i])
			{
				return false;
			}
		}

		return true;
	}


	~stack()
	{
		free(_pos);
	}

private:
	int* _pos;
	int _size;
	int _capacity;
};


int main()
{ 

  stack T1;
  stack T2;  

  /* 假设中间两个栈分别进行了一系列不同的操作*/

	bool tmp = T1.stack_if_equal(T2);
    这样我们可以判断T1与T2是否相等
}

However, do you think that this way of writing is not very good-looking, and it’s okay to write it yourself. How would others feel when reading your code? You may feel confused and have to guess for a long time what this stack_if_equal() means. If you can’t even see it, you can only look at the source code, which greatly deepens the unreadability of the code, so you can’t directly use the "==" symbol to represent it. Are the two objects equal?

It’s okay, C++ provides us with operator overloading, which is to solve this problem

Use of operator overloading

Because it is not easy to read with function names, C++ provides operator overloading. We can use operator overloading in the future when we need to perform operator operations on certain types of objects. The symbol of operator overloading is the operator operator. If you want to judge whether two objects are equal, you can write it as bool operator==(const stack &T2), which is how you write it when defining a function. When we want to compare, you can directly write it as bool tmp = T1 == T2; 

class stack
{
public:
	  
    //为了更清晰,这里我就暂时把构造函数和析构函数删掉了

    bool operator==(const stack& T2)
	{
		if (_size != T2._size || _capacity != T2._capacity)
		{
			return false;
		}
		
		for (int i = 0; i < _size; i++)
		{
			if (_pos[i] != T2._pos[i])
			{
				return false;
			}
		}

		return true;
	}


	
private:
	int* _pos;
	int _size;
	int _capacity;
};


int main()
{ 

  stack T1;
  stack T2;  

  /* 假设中间两个栈分别进行了一系列不同的操作*/

	bool tmp = T1 == T2;  //本质上就是bool tmp = T1.operator==(T2);
    
    //这是运用运算符重载的写法,是不是更清晰的表达出意思了呢
}

Except for .* ,  ::  , ?: , sizeof , . ,  except for these five operators, operator overloading is not allowed, and other legal operators are allowed. Next, look at the example of judging the size of two objects and get familiar with the operation of operator overloading

For example, if we want to compare the size of two objects of the stack class, the number of elements in the stack is used to determine the size, and the stack with more elements will be larger, so we overload the operator> 

class stack
{
public:
	 
    bool operator>(const stack& T2)
	{
		if (_size > T2._size)	
			return true;
        else
		    return false;
	}


	
private:
	int* _pos;
	int _size;
	int _capacity;
};


int main()
{ 

  stack T1;
  stack T2;  

  /* 假设中间两个栈分别进行了一系列不同的操作*/

	bool tmp = T1 > T2;  //本质上就是bool tmp = T1.operator>(T2);
    
}

Now we read the code again, and we can see at a glance that this is the size of two objects of a certain type. It can be seen that operator overloading can help us read the code clearly. It is a function that can be used frequently. Everyone must to master

assignment operator overloading

From the name, we can guess that this is the overloading of the assignment operator, that is, assigning the value of one object to another object. Does it sound familiar? Isn’t it the copy construction we mentioned earlier? Since there is Does the copy construction mean that assignment operator overloading is unnecessary?

Although the functions are the same, there is still a big difference. The assignment operator overload can assign the value of one object to another object at any time, but the copy construction can only be assigned when the object is created, because the constructor is in the life cycle of the object It can only be called once, and it is called automatically when the object is created. It can be seen that assignment operator overloading is very useful

Definition of assignment operator overloading 

Parameter type: const T&, passing references can improve the efficiency of parameter passing Return
value type: T&, returning references can improve the efficiency of returning, the purpose of returning values ​​is to support continuous assignment, and to detect whether to assign values ​​​​to oneself

 

It should be noted that if the assignment operator overloading is not implemented manually, then the compiler will automatically generate one, but what is automatically generated by the compiler is a byte-by-byte copy, which can only be shallow copied. What do you mean? The stack classes mentioned earlier, like _size, _capacity, can indeed be copied as they are, but _pos cannot. If you copy the _pos of T1 to the _pos of T2, then the two _pos will not be the same Pointing to the same space, like this, only simple assignment copy can be performed, shallow copy cannot be realized for deep space levels (such as the space pointed to by pointers), if you want to realize deep copy, you still have to write it manually, Next, look at the code for assignment operator overloading

class stack
{
public:

	stack& operator=(const stack &tmp)
	{
		_pos = (int*)realloc(_pos, sizeof(int)*tmp._capacity);
		memcpy(_pos, tmp._pos, sizeof(int) * tmp._size);
		_size = tmp._size;
		_capacity = tmp._capacity;
           
        return *this;
	}

private:
	int* _pos;
	int _size;
	int _capacity;
};


int main()
{ 

  stack T1;
  stack T2;  

/*假设进行一些列操作后,把T1赋值给T2*/
  T2 = T1;

}

You may think that there is nothing wrong with the above code, but what is not a problem is that T1 is larger than T2, and realloc is used to expand the space. If the space of T2 is larger than that of T1, what should we do?

So we can't write like this, we can't determine the size of T1 and T2 spaces, so we simply release T2 directly, re-open up a space as large as T1, and then assign the value of T1 to T2, and then look at the modified program

class stack
{
public:

	stack& operator=(const stack &tmp)
	{
		free(_pos);
		_pos = (int*)malloc(sizeof(int)*tmp._capacity);
        memcpy(_pos, tmp._pos, sizeof(int) * tmp._size);
		_size = tmp._size;
		_capacity = tmp._capacity;
           
        return *this;
	}

private:
	int* _pos;
	int _size;
	int _capacity;
};


int main()
{ 

  stack T1;
  stack T2;  

/*假设进行一些列操作后,把T1赋值给T2*/
  T2 = T1;

}

But is that right? If there is a situation where T2 = T2, you will free T2 when you come up, and then open a new space, copy the value of T2 space, and so on! It turns out that T2 has been released, and if you still copy it, it is an out-of-bounds access. It seems that it is not a simple matter to implement a program correctly! Difficulties come, if you solve them, your ability will increase. If you can’t solve them, the problem will still be there. Don’t panic, just do it and it’s over.

Fortunately, it is relatively easy to solve here. It is enough to judge that the assignment and the assigned party cannot be equal. The following is the correct code

class stack
{
public:

	stack& operator=(const stack &tmp)
	{
      if ( this != &tmp)                                                                          
	  {   
           free(_pos);
		   _pos = (int*)malloc(sizeof(int)*tmp._capacity);
           memcpy(_pos, tmp._pos, sizeof(int) * tmp._size);
		   _size = tmp._size;
		   _capacity = tmp._capacity;
      }
 
        return *this;
	}

private:
	int* _pos;
	int _size;
	int _capacity;
};


int main()
{ 

  stack T1;
  stack T2;  

/*假设进行一些列操作后,把T1赋值给T2*/
  T2 = T1;

}

const member function

What is a const member function

We know that the reason why the member functions in the class can access the private variables in the class is because a this pointer is hidden in the parameters of the member functions, through which we can access the private variables in the class.

When we write code every day, we will encounter such a situation. When writing a member function of a class, we just want to check the value of the member variable in the class, and do not want to modify the value of the member variable, but I am afraid that after a period of time, I or others will The value of a member variable in the class was accidentally changed while modifying the member function.

This is very troublesome, because a small mistake may cause the entire program to crash, so how to prevent such a thing from happening? We know earlier that member functions rely on the this pointer to access member variables, so add a const modification to the this pointer to restrict it. It makes sense, let’s take a look at the writing method of the hidden this pointer

class type * const this   

This const modifies the this pointer itself, indicating that the this pointer cannot change the pointed object. This is easy to understand. The object pointed to by this must not be modified.

Now our requirement is to prevent the value of the object pointed to by this from being modified. According to our idea just now, it can be written as

const class type * const this

These two const modified objects are different, one is to modify the this pointer itself, and the other is to modify the object pointed to by this 

This problem seems to be perfectly solved, but unfortunately we overlooked a problem at the beginning, the this pointer is hidden, which means we can’t see it, and we can’t see how to add const

And this uses the const member function. To put it simply, the const member function is to solve the problem of adding a const to this. The solution is also very simple. Since I can't see the hidden this pointer, I will add the const to The definition or declaration of the function, and this const-added member function is a const member function

The author here takes checking the size of the space in the stack class as an example, and writes a member function to check the number of spaces that have been opened up.

class stack
{
public:

	stack();

    //这是const成员函数的正确写法
	int Check_capacity() const            
	{
		return _capacity;
	}
	

	~stack();

private:
	int* _pos;
	int _size;
	int _capacity;
};

Points to note 

When using const constraints, special attention should be paid to the issue of permissions. Permissions can be narrowed, but cannot be enlarged. For example, const member functions cannot call other non-const member functions, because the this pointer of const member functions points to The object is constrained by const, and the permission is read-only. The object pointed to by the this pointer of the non-const member function is not constrained, and the permission is read and write. If it is called, the permission will be expanded, which is not allowed

It is possible to call const member functions instead of const member functions, because permissions can be narrowed

take address operator overloading

The overloading of the address operator is to obtain the address of the calling object. This overloaded function does not need to be implemented by ourselves, and the compiler will automatically implement it. Now you can rest assured, because the compiler will really implement it. Haha, after reading the previous content, a When it comes to the implementation of the compiler itself, it may arouse our vigilance, hahaha, there is not much to say about this function, let’s take a look at the implementation of this function

//还是以栈类为例
stack* operator&()
	{
		return this;
	}

const address-of operator overloading

This is slightly different from the above one. This function is used to get the address of the object modified by const. The object modified by const cannot directly call the overloaded address operator.
As we mentioned earlier, permissions cannot be enlarged. The permission of the object modified by const is read-only, and the permission of the object pointed to by this pointer is read and write. Passing it will cause the permission to be enlarged, so give the const Write an address operator overload function for the modified object. You don’t need to write this manually. The compiler automatically implements it. You just need to understand it.
const stack* operator&()const
{
		return this;
}

Guess you like

Origin blog.csdn.net/m0_61350245/article/details/127185383