Leetcode - C++ surprise interview

C++ surprise interview

1. Compile memory related

1.1. C++ program compilation process

The compilation process is divided into four processes: compilation (compilation preprocessing, compilation, optimization), assembly, and linking.

  • Compile preprocessing : process instructions beginning with #;

  • Compile and optimize : translate the source code .cpp file into .s assembly code;

  • Assembly : translate assembly code .s into machine instruction .o file;

  • Link : The object file generated by the assembler, that is, the .o file, will not be executed immediately, because it may appear that a function in the .cpp file refers to a symbol defined in another .cpp file or calls a library file. The function. The purpose of the link is to connect the target files corresponding to these files into a whole, so as to generate an executable program .exe file.
    insert image description here
    There are two types of links:

  • Static link : The code is copied from the static link library where it is located to the final executable program. When the program is executed, the code will be loaded into the virtual address space of the process.

  • Dynamic link : The code is placed in a dynamic link library or an object file of a shared object, and the linker only records some information such as the name of the shared object in the final executable program. When the program is executed, the entire content of the dynamic link library will be mapped to the corresponding virtual address space at runtime.

    Advantages and disadvantages of both:

  • Static link : a waste of space, each executable program will have a copy of the target file, so if the target file is updated, it needs to be recompiled and linked to generate an executable program (updating is difficult); the advantage is the running speed when executing Fast, because the executable has everything the program needs to run.

  • Dynamic link : saves memory and is convenient to update. However, dynamic link is required for each execution when the program is running. Compared with static link, there will be a certain performance loss.

1.2. C++ memory management

C++ memory partitions: stack, heap, global/static storage area, constant storage area, code area .

  • Stack : store local variables, function parameters, return addresses, etc. of the function, which are automatically allocated and released by the compiler.
  • Heap : The dynamically applied memory space is the memory block allocated by malloc, and the programmer controls its allocation and release. If the program execution is not released, the operating system will automatically reclaim it.
  • Global area/static storage area (.bss section and .data section) : store global variables and static variables, and the operating system will automatically release the program after running. In C language, the uninitialized ones are placed in the .bss section, and the initialized ones are placed in In the .data section, no distinction is made in C++.
  • Constant storage area (.data section ): Stores constants, which cannot be modified, and will be released automatically after the program runs.
  • Code area (.text segment) : store code, modification is not allowed, but it can be executed. The compiled binaries are stored here.

1.3. Difference between stack and heap

  • Application method : the stack is automatically allocated by the system, and the heap is actively applied by the programmer.
  • System response after application : allocate stack space, if the remaining space is larger than the requested space, the allocation is successful, otherwise the allocation fails and the stack overflows; when applying for heap space, the way the heap is presented in memory is similar to a linked list (a linked list that records free address space), in the linked list Search for the first node larger than the application space and assign it to the program, and delete the node from the linked list. In most systems, the first address of the block space stores the size of the allocated space for easy release. The remaining space is connected to the free list again.
  • The stack is a continuous piece of space in memory (extends to low addresses), the maximum capacity is predetermined by the system, and the space in memory (extends to high addresses) of the heap is discontinuous.
  • Application efficiency : the stack is automatically allocated by the system, and the application efficiency is high, but the programmer cannot control it; the heap is actively applied by the programmer, which is inefficient and convenient to use but prone to fragmentation.
  • Stored content : local variables and function parameters are stored in the stack; the content stored in the heap is controlled by the programmer.

1.4. The difference between variables

The difference between global variables, local variables, static global variables, and static local variables:

C++ variables have different scopes according to the different life cycles of the defined positions. The scopes can be divided into 6 types: global scope, local scope, statement scope, class scope, namespace scope and file scope .

From the perspective of scope:

  • Global variables : have global scope. Global variables only need to be defined in one source file, and they can be applied to all source files. Of course, other source files that do not contain global variable definitions need to declare this global variable again with the extern keyword.
  • Static global variables : have file scope. The difference between it and global variables is that if the program contains multiple files, it acts on the file where it is defined, and cannot affect other files, that is, variables modified by the static keyword have file scope. In this way, even if two different source files define static global variables with the same name, they are different variables.
  • Local variables : have local scope. It is an automatic object (auto), which does not exist all the time during the running of the program, but only exists during the execution of the function. After a call of the function is executed, the variable is revoked and the memory it occupies is also recovered.
  • Static local variables : have local scope. It is only initialized once, and it exists from the first time it is initialized until the end of the program. The difference between it and the global variable is that the global variable is visible to all functions, while the static local variable is always only for the function body that defines itself visible.

From the allocation of memory space:

  • Static storage area : global variables, static local variables, static global variables.
  • Stack : local variables.

1.5. What's wrong with defining global variables in header files?

If a global variable is defined in a header file, when the header file is included by multiple files, the global variable in the header file will be defined multiple times, resulting in repeated definitions, so global variables cannot be defined in the header file.

1.6. Memory Alignment

What is memory alignment? The principle of memory alignment? Why memory alignment and what are the advantages?

Memory alignment : The compiler arranges each "data unit" in the program in the memory pointed to by an address that is an integer multiple of a word.
Principles of memory alignment:

  1. The first address of a structure variable can be divisible by the smaller of its widest basic type member size and alignment base;
  2. The offset (offset) of each member of the structure relative to the first address of the structure
    is an integer multiple of the smaller of the size of the member and the alignment base, and the compiler will add padding bytes between the members if necessary (internal padding);
  3. The total size of the structure is an integer multiple of the smaller of the size of the widest basic type member of the structure and the alignment base. If necessary, the compiler will add trailing padding after the last member.

Reasons for memory alignment : (mainly hardware issues)

  1. Some hardware devices can only access aligned data, and accessing unaligned data may cause exceptions;
  2. Some hardware devices cannot guarantee that the operation when accessing unaligned data is an atomic operation;
  3. Accessing unaligned data takes more time than accessing aligned data;
  4. Although some processors support access to unaligned data, it will trigger an alignment trap (alignment trap);
  5. Some hardware devices only support unaligned access of simple data instructions, but do not support unaligned access of complex data instructions.

Advantages of memory alignment :

  1. It is easy to port between different platforms, because some hardware platforms cannot support data access at arbitrary addresses, and can only fetch certain specific data at certain addresses, otherwise an exception will be thrown;
  2. Improve memory access efficiency, because when the CPU reads memory, it reads one by one.

1.7. What is a memory leak

memory leak: The failure of a program to free memory that is no longer in use, due to inadvertence or error.
Further explanation:

  • It does not mean that the memory disappears physically, but that the program loses control of the memory due to negligence or errors during the running process, resulting in a waste of memory.
  • It often refers to heap memory leaks, because the heap is dynamically allocated and controlled by the user. If used improperly, memory leaks will occur.
  • When using malloc, calloc, realloc, new, etc. to allocate memory, call the corresponding free or delete to release the memory after use, otherwise this memory will cause a memory leak.
  • pointer reassignment
char *p = (char *)malloc(10);
char *p1 = (char *)malloc(10);
p = np;

At the beginning, the pointers p and p1 respectively point to a memory space, but the pointer p is reassigned, so that the memory space initially pointed to by p cannot be found, thus a memory leak occurs.

1.8. How to prevent memory leaks? How does the memory leak detection tool work?

Ways to prevent memory leaks:

  1. Internal encapsulation : Encapsulate the allocation and release of memory into the class, apply for memory at the time of construction, and release the memory at the time of destruction. (Note: But this is not the best practice, when the object of the class is copied, the program will release the same memory space twice)
  2. Smart pointer : Smart pointer is a tool that has encapsulated memory leaks in C++ and can be used directly. Smart pointers will be explained in detail in the next question.

Implementation principle of memory leak detection tools:
There are many memory detection tools, here we will focus on valgrind.

1.9. What types of smart pointers are there? How does the smart pointer work?

Smart pointers are proposed to solve memory leaks caused by dynamic memory allocation and to release the same memory space multiple times. In C++11, it is encapsulated in the <memory> header file.

Smart pointers in C++11 include the following three types:

  • Shared pointer (shared_ptr) : The resource can be shared by multiple pointers, and the use counting mechanism indicates that the resource is shared by several pointers. Check the number of resource owners through use_count(), which can be constructed through unique_ptr and weak_ptr, call release() to release the ownership of the resource, and the count will be reduced by one. When the count is reduced to 0, the memory space will be released automatically, thus avoiding memory leak.

  • Exclusive pointer (unique_ptr) : A smart pointer with exclusive ownership, the resource can only be occupied by one pointer, and the pointer cannot be copied and assigned. But move construction and move assignment construction (calling the move() function), that is, one unique_ptr object is assigned to another unique_ptr object, can be assigned through this method.

  • Weak pointer (weak_ptr) : points to the object pointed to by shared_ptr, which can solve the circular reference problem caused by shared_ptr.

The realization principle of smart pointer: counting principle.

#include <iostream>
#include <memory>

template <typename T>
class SmartPtr
{
    
    
private : 
	T *_ptr;
	size_t *_count;

public:
	SmartPtr(T *ptr = nullptr) : _ptr(ptr)
	{
    
    
		if (_ptr)
		{
    
    
			_count = new size_t(1);
		}
		else
		{
    
    
			_count = new size_t(0);
		}
	}

	~SmartPtr()
	{
    
    
		(*this->_count)--;
		if (*this->_count == 0)
		{
    
    
			delete this->_ptr;
			delete this->_count;
		}
	}

	SmartPtr(const SmartPtr &ptr) // 拷贝构造:计数 +1
	{
    
    
		if (this != &ptr)
		{
    
    
			this->_ptr = ptr._ptr;
			this->_count = ptr._count;
			(*this->_count)++;
		}
	}

	SmartPtr &operator=(const SmartPtr &ptr) // 赋值运算符重载 
	{
    
    
		if (this->_ptr == ptr._ptr)
		{
    
    
			return *this;
		}
		if (this->_ptr) // 将当前的 ptr 指向的原来的空间的计数 -1
		{
    
    
			(*this->_count)--;
			if (this->_count == 0)
			{
    
    
				delete this->_ptr;
				delete this->_count;
			}
		}
		this->_ptr = ptr._ptr;
		this->_count = ptr._count;
		(*this->_count)++; // 此时 ptr 指向了新赋值的空间,该空间的计数 +1
		return *this;
	}

	T &operator*()
	{
    
    
		assert(this->_ptr == nullptr);
		return *(this->_ptr);
	}

	T *operator->()
	{
    
    
		assert(this->_ptr == nullptr);
		return this->_ptr;
	}

	size_t use_count()
	{
    
    
		return *this->count;
	}
};

1.10. How to assign a unique_ptr to another unique_ptr object?

With the help of std::move(), one unique_ptr object can be assigned to another unique_ptr object, and its purpose is to realize the transfer of ownership.

// A 作为一个类 
std::unique_ptr<A> ptr1(new A());
std::unique_ptr<A> ptr2 = std::move(ptr1);

1.11. What's wrong with using smart pointers? How to deal with it?

Possible problems with smart pointers: circular references

In the following example, two classes Parent and Child are defined, and the shared pointer of the object of the other class is defined in the two classes. After the program ends, the two pointers point to each other's memory space, resulting in the memory cannot be released.

#include <iostream>
#include <memory>

using namespace std;

class Child;
class Parent;

class Parent {
    
    
private:
    shared_ptr<Child> ChildPtr;
public:
    void setChild(shared_ptr<Child> child) {
    
    
        this->ChildPtr = child;
    }

    void doSomething() {
    
    
        if (this->ChildPtr.use_count()) {
    
    

        }
    }

    ~Parent() {
    
    
    }
};

class Child {
    
    
private:
    shared_ptr<Parent> ParentPtr;
public:
    void setPartent(shared_ptr<Parent> parent) {
    
    
        this->ParentPtr = parent;
    }
    void doSomething() {
    
    
        if (this->ParentPtr.use_count()) {
    
    

        }
    }
    ~Child() {
    
    
    }
};

int main() {
    
    
    weak_ptr<Parent> wpp;
    weak_ptr<Child> wpc;
    {
    
    
        shared_ptr<Parent> p(new Parent);
        shared_ptr<Child> c(new Child);
        p->setChild(c);
        c->setPartent(p);
        wpp = p;
        wpc = c;
        cout << p.use_count() << endl; // 2
        cout << c.use_count() << endl; // 2
    }
    cout << wpp.use_count() << endl;  // 1
    cout << wpc.use_count() << endl;  // 1
    return 0;
}

The solution to circular references: weak_ptr

Circular reference : The called destructor is not called, resulting in a memory leak.

  • weak_ptr has a non-owning (weak) reference to the object managed by shared_ptr , which must be converted to shared_ptr before accessing the referenced object;
  • weak_ptr is used to break the circular reference problem of the object managed by shared_ptr. If this ring is isolated (there is no external shared pointer pointing to the ring), the reference count of shared_ptr cannot reach 0, and the memory is leaked; let one of the pointers in the ring be Weak pointers can avoid this situation;
  • weak_ptr is used to express the concept of temporary ownership. When an object only needs to be accessed when it exists, and may be deleted by others at any time, you can use weak_ptr to track the object; when you need to obtain ownership, convert it to shared_ptr. At this time, if the original shared_ptr is destroyed, the lifetime of the object is extended until this temporary shared_ptr is also destroyed.
#include <iostream>
#include <memory>

using namespace std;

class Child;
class Parent;

class Parent {
    
    
private:
    //shared_ptr<Child> ChildPtr;
    weak_ptr<Child> ChildPtr;
public:
    void setChild(shared_ptr<Child> child) {
    
    
        this->ChildPtr = child;
    }

    void doSomething() {
    
    
        //new shared_ptr
        if (this->ChildPtr.lock()) {
    
    

        }
    }

    ~Parent() {
    
    
    }
};

class Child {
    
    
private:
    shared_ptr<Parent> ParentPtr;
public:
    void setPartent(shared_ptr<Parent> parent) {
    
    
        this->ParentPtr = parent;
    }
    void doSomething() {
    
    
        if (this->ParentPtr.use_count()) {
    
    

        }
    }
    ~Child() {
    
    
    }
};

int main() {
    
    
    weak_ptr<Parent> wpp;
    weak_ptr<Child> wpc;
    {
    
    
        shared_ptr<Parent> p(new Parent);
        shared_ptr<Child> c(new Child);
        p->setChild(c);
        c->setPartent(p);
        wpp = p;
        wpc = c;
        cout << p.use_count() << endl; // 2
        cout << c.use_count() << endl; // 1
    }
    cout << wpp.use_count() << endl;  // 0
    cout << wpc.use_count() << endl;  // 0
    return 0;
}

2. Language Comparison

2.1 New features of C++11

1. auto type deduction
auto keyword: automatic type deduction, the compiler will deduce the type of the variable through the initial value during compilation, and the variable defined by auto must have an initial value.
The basic usage syntax of the auto keyword is as follows:

2. decltype type derivation
decltype keyword: decltype is the abbreviation of "declare type", translated as "declare type". Same function as auto, both are used for automatic type deduction at compile time. If you want to infer the type of the variable to be defined from the expression, but you don't want to initialize the variable with the value of the expression, you can no longer use auto. The role of decltype is to select and return the data type of the operand.

the difference:

auto var = val1 + val2; 
decltype(val1 + val2) var1 = 0; 
  • auto deduces the variable type according to the initial value val1 + val2 on the right side of =, and assigns the initial value to the variable var; decltype deduces the variable type according to the val1 + val2 expression, and the initial value of the variable has nothing to do with the value of the expression .
  • auto requires that the variable must be initialized, because it deduces the type of the variable based on the initialized value, but decltype does not require it, and it can be initialized or not initialized when defining the variable.

3. Lambda expression
Lambda expression, also known as lambda function or lambda anonymous function.

Definition of lambda anonymous function:

[capture list] (parameter list) -> return type
{
    
    
   function body;
};

in:

  • capture list: The capture list refers to the list of local variables defined in the function where the lambda resides, and is usually empty.
  • return type, parameter list, and function body: respectively indicate the return value type, parameter list, and function body, which are the same as ordinary functions.
#include <iostream>
#include <algorithm>
using namespace std;

int main()
{
    
    
    int arr[4] = {
    
    4, 2, 3, 1};
    //对 a 数组中的元素进行升序排序
    sort(arr, arr+4, [=](int x, int y) -> bool{
    
     return x < y; } );
    for(int n : arr){
    
    
        cout << n << " ";
    }
    return 0;
}

4. Range for statement

for (declaration : expression){
    
    
    statement
}

The meaning of the parameters:

  • expression: must be a sequence, such as an initial value list enclosed in curly braces, an array, vector, string, etc. The common feature of these types is that they have beign and end members that can return iterators.
  • declaration: A variable is defined here, and each element in the sequence can be converted into the type of the variable. The auto type specifier is commonly used.

5. Rvalue references
Rvalue references: references bound to rvalues, use && to obtain rvalue references, rvalue references can only be bound to objects to be destroyed. To distinguish it from rvalue references, regular references are called lvalue references.

#include <iostream>
#include <vector>
using namespace std;
int main()
{
    
    
    int var = 42;
    int &l_var = var;
    int &&r_var = var; // error: cannot bind rvalue reference of type 'int&&' to lvalue of type 'int' 错误:不能将右值引用绑定到左值上

    int &&r_var2 = var + 40; // 正确:将 r_var2 绑定到求和结果上
    return 0;
}

6. Standard library move() function
move() function: through this function, an rvalue reference bound to an lvalue can be obtained, and this function is included in the utility header file. This knowledge point will be explained in detail in subsequent chapters.

7.
The relevant knowledge of smart pointers has been explained in detail in the first chapter, and will not be repeated here.

8. delete function and default function

  • delete function: = delete means that the function cannot be called.
  • default function: = default means that the compiler generates a default function, for example: generate a default constructor.
#include <iostream>
using namespace std;

class A
{
    
    
public:
	A() = default; // 表示使用默认的构造函数
	~A() = default;	// 表示使用默认的析构函数
	A(const A &) = delete; // 表示类的对象禁止拷贝构造
	A &operator=(const A &) = delete; // 表示类的对象禁止拷贝赋值
};
int main()
{
    
    
	A ex1;
	A ex2 = ex1; // error: use of deleted function 'A::A(const A&)'
	A ex3;
	ex3 = ex1; // error: use of deleted function 'A& A::operator=(const A&)'
	return 0;
}

2.2 Difference between C and C++

Let me talk about object-oriented and process-oriented first:

  • Process-oriented thinking: analyze the steps required to solve the problem, and use functions to implement these steps in sequence.
  • Object-oriented thinking: decompose the transaction that constitutes the problem into various objects, and the purpose of establishing an object is not to complete a step, but to describe the behavior of a certain transaction in the steps of solving the entire problem.

Differences and connections:

  • The language itself: C language is process-oriented programming, its most important feature is the function, and
    each sub-function is called through the main function. The order in which the program runs is determined in advance by the programmer. C++ is object-oriented programming, and class is its main feature. During the execution of the program, the main function first enters, defines some classes, and executes the member functions of the class as needed. The concept of process is downplayed (in fact, the process is still Yes, those statements of the main function.), run as a class driver, and the class is the object, so we call it object-oriented programming. When object-oriented analyzes and solves problems, it encapsulates the data and data operations involved in classes, through which objects can be created, and events or messages can be used to drive objects to perform processing.
  • Application fields: C language is mainly used in embedded fields, driver development and other fields that directly deal with hardware, and C++ can be used in application layer development, user interface development and other fields that deal with operating systems.
  • C++ not only inherits the powerful low-level operation characteristics of C, but also is endowed with an object-oriented mechanism. It has many features, such as multiple inheritance in object-oriented languages, the distinction between passing by value and passing by reference, the const keyword, and so on.
  • The "enhancement" of C++ to C is manifested in the following aspects: Type checking is stricter. Added object-oriented mechanism, generic programming mechanism (Template), exception handling, operator overloading, standard template library (STL), namespace (to avoid global naming conflicts).

2.3 Differences between Python and C++

the difference:

  • Language itself: Python is a scripting language, interpreted and executed without compilation; C++ is a language that needs to be compiled before it can run, and it runs after compilation on a specific machine.
  • Operating efficiency: C++ has high operating efficiency, safety and stability. Reason: Python code and C++ will eventually become CPU instructions to run, but in general, such as reversing and merging two strings, Python will eventually convert a lot more CPU instructions than C++. First of all, Python involves more content than C++, after more layers, even numbers in Python are objects; secondly, Python is interpreted and executed, and there is an interpreter layer between the CPU of the physical machine, while C++ is What is compiled and executed is directly the machine code, and the compiler can perform some optimizations during compilation.
  • Development efficiency: Python development efficiency is high. Reason: The functions that Python can achieve with one or two lines of code, C++ often requires more code to achieve.
  • Different writing format and grammar: Python's grammatical format is different from its C++ definition and declaration, and it is extremely flexible and completely oriented to higher-level developers.

3. Object-oriented

3.1 What is object-oriented? The three main characteristics of object-oriented

Object-oriented : An object refers to a specific thing, and the abstraction of these things is a class, which contains data (member variables) and actions (member methods).

The three main characteristics of object-oriented:

  • Encapsulation : Encapsulate the specific implementation process and data into a function, which can only be accessed through the interface to reduce coupling.
  • Inheritance : The subclass inherits the characteristics and behaviors of the parent class. The subclass has non-private methods or member variables of the parent class. The subclass can rewrite the method of the parent class, which enhances the coupling between classes, but when the parent class When the member variable, member function or class itself is modified by the final keyword, the modified class cannot be inherited, and the modified members cannot be rewritten or modified.
  • Polymorphism : Polymorphism refers to objects of different inheritance classes that respond differently to the same message. The pointer of the base class points to or binds to the object of the derived class, so that the pointer of the base class presents different expressions.

3.2 The difference between overloading, rewriting and hiding

  1. Overloading : refers to several functions with the same name declared in the same accessible area with different parameter columns (parameter type, number, order), which function to call is determined according to the parameter list, and overloading does not care about the return type of the function.
class A
{
    
    
public:
    void fun(int tmp);
    void fun(float tmp);        // 重载 参数类型不同(相对于上一个函数)
    void fun(int tmp, float tmp1); // 重载 参数个数不同(相对于上一个函数)
    void fun(float tmp, int tmp1); // 重载 参数顺序不同(相对于上一个函数)
    int fun(int tmp);            // error: 'int A::fun(int)' cannot be overloaded 错误:注意重载不关心函数返回类型
};
  1. Hidden : It means that the function of the derived class shields the base class function with the same name, mainly as long as the function with the same name, regardless of whether the parameter list is the same, the base class function will be hidden.
#include <iostream>
using namespace std;

class Base
{
    
    
public:
    void fun(int tmp, float tmp1) {
    
     cout << "Base::fun(int tmp, float tmp1)" << endl; }
};

class Derive : public Base
{
    
    
public:
    void fun(int tmp) {
    
     cout << "Derive::fun(int tmp)" << endl; } // 隐藏基类中的同名函数
};

int main()
{
    
    
    Derive ex;
    ex.fun(1);       // Derive::fun(int tmp)
    ex.fun(1, 0.01); // error: candidate expects 1 argument, 2 provided
    return 0;
}

Explanation : In the above code ex.fun(1, 0.01); an error occurs, indicating that the function with the same name of the base class is hidden in the derived class. If you want to call the function with the same name in the base class, you can add the type name to specify ex.Base::fun(1, 0.01);, so that you can call the function with the same name in the base class.

  1. Rewriting (covering) : refers to the presence of redefined functions in derived classes. The function name, parameter list, and return value type must be the same as the overridden function in the base class, only the function body is different. When the derived class calls, the overridden function of the derived class will be called, and the overridden function will not be called. The overridden function in the overridden base class must have virtual decoration.
#include <iostream>
using namespace std;

class Base
{
    
    
public:
    virtual void fun(int tmp) {
    
     cout << "Base::fun(int tmp) : " << tmp << endl; }
};

class Derived : public Base
{
    
    
public:
    virtual void fun(int tmp) {
    
     cout << "Derived::fun(int tmp) : " << tmp << endl; } // 重写基类中的 fun 函数
};
int main()
{
    
    
    Base *p = new Derived();
    p->fun(3); // Derived::fun(int) : 3
    return 0;
}

The difference between rewriting and overloading:

  • Scope difference : For the overloading or rewriting of functions in a class, the overloading occurs inside the same class, and the rewriting occurs between different classes (between the subclass and the parent class).
  • Parameter difference : the overloaded function needs to have the same function name and different parameter list as the original function, and does not pay attention to the return value type of the function; the function name, parameter list and return value type of the rewritten function need to be the same as the original function , the overridden function in the parent class needs to be decorated with virtual.
  • virtual keyword : The rewritten function base class must be decorated with the virtual keyword, and the overloaded function can be decorated with the virtual keyword or not.

The difference between hiding and rewriting, overloading:

  • Scope difference : hiding is different from overloading scope, hiding happens in different classes.
  • Parameter difference : the hidden function and the hidden function parameter list can be the same or different, but the function name must be the same; when the parameters are different, no matter whether the function in the base class is modified by virtual, the base class function is hidden instead
    of rewrite.

3.3 How to understand that C++ is object-oriented programming

Explanation: It is best to explain this question in combination with your own project experience, or give some appropriate examples, and compare it with process-oriented programming.

  • Procedure-Oriented Programming : An approach to writing software centered on procedures, or functions, that perform program operations. Program data is usually stored in variables, separate from these procedures. So variables must be passed to functions that need to use them. Cons: As programs become more complex, the separation of program data from running code can cause problems. For example, the specification of a program often changes, requiring changes to the format of data or the design of data structures. When the data structure changes, the code that operates on the data must also be changed to accept the new format. Finding all the code that needs to be changed creates extra work for the programmer and increases the chances of making the code buggy.
  • Object-Oriented Programming (OOP) : Centered on creating and using objects. An object (Object) is a software entity that combines data and programs in one unit. An object's data items, also known as its properties, are stored in member variables. The procedures an object performs are called its member functions. Binding an object's data and procedures together is called encapsulation.

Object Oriented Programming explains further:

Object-oriented programming encapsulates data members and member functions into a class, and declares the access levels (public, private, protected) of data members and member functions, so as to control the access of class objects to data members and functions, and play a role in data members. Certain protective effect. Moreover, when an object of a class calls a member function, it only needs to know the name, parameter list, and return value type of the member function, without knowing the implementation principle of the function. When the data members or member functions inside the class are changed, the code outside the class is not affected.

3.4 What is polymorphism? How is polymorphism implemented?

Polymorphism : Polymorphism refers to objects of different inheritance classes that respond differently to the same message. The pointer of the base class points to or binds to the object of the derived class, so that the pointer of the base class presents different expressions. Add the virtual keyword before the function of the base class, rewrite the function in the derived class, and the corresponding function will be called according to the actual type of the object at runtime. If the object type is a derived class, the function of the derived class is called; if the object type is a base class, the function of the base class is called.

Implementation method : polymorphism is realized through virtual functions, the address of the virtual function is stored in the virtual function table, and the address of the virtual function table is stored in the memory space of the instance object of the class containing the virtual function.

Implementation process:

  1. A function declared with the virtual keyword in a class is called a virtual function;
  2. Classes with virtual functions have a virtual function table. When an object of this class is created, the object has a virtual table pointer pointing to the virtual function table (the virtual function table corresponds to the class, and the virtual table pointer corresponds to the object) ;
  3. When the base class pointer points to the derived class object and the base class pointer calls the virtual function, the base class pointer points to the virtual table pointer of the derived class. Since the virtual table pointer points to the derived class virtual function table, the corresponding virtual function is found by traversing the virtual table .
#include <iostream>
using namespace std;

class Base
{
    
    
public:
	virtual void fun() {
    
     cout << "Base::fun()" << endl; }

	virtual void fun1() {
    
     cout << "Base::fun1()" << endl; }

	virtual void fun2() {
    
     cout << "Base::fun2()" << endl; }
};
class Derive : public Base
{
    
    
public:
	void fun() {
    
     cout << "Derive::fun()" << endl; }

	virtual void D_fun1() {
    
     cout << "Derive::D_fun1()" << endl; }

	virtual void D_fun2() {
    
     cout << "Derive::D_fun2()" << endl; }
};
int main()
{
    
    
	Base *p = new Derive();
	p->fun(); // Derive::fun() 调用派生类中的虚函数
	return 0;
}

The virtual function table of the base class is as follows: The
insert image description here
virtual function table of the object of
insert image description here
the derived class is as follows: Simple explanation: When the pointer of the base class points to the object of the derived class, the virtual function table is found through the virtual table pointer of the object of the derived class (the object of the derived class virtual function table), and then find the corresponding virtual function Derive::f() to call.

4. Keyword library functions

4.1 The difference between sizeof and strlen

  1. strlen is a function in the header file and sizeof is an operator in C++.
  2. strlen measures the actual length of the string (its source code is below), terminated by \0 . And sizeof measures the allocated size of the character array.
strlen 源代码:
size_t strlen(const char *str) {
    
    
    size_t length = 0;
    while (*str++)
        ++length;
    return length;
}
#include <iostream>
#include <cstring>

using namespace std;

int main()
{
    
    
    char arr[10] = "hello";
    cout << strlen(arr) << endl; // 5
    cout << sizeof(arr) << endl; // 10
    return 0;
}
  1. If the character array arr is used as the formal parameter of the function, arr in sizeof(arr) is treated as a character pointer, and arr in strlen(arr) is still a character array, as can be seen from the running results of the following program.
#include <iostream>
#include <cstring>

using namespace std;

void size_of(char arr[])
{
    
    
    cout << sizeof(arr) << endl; // warning: 'sizeof' on array function parameter 'arr' will return size of 'char*' .
    cout << strlen(arr) << endl; 
}

int main()
{
    
    
    char arr[20] = "hello";
    size_of(arr); 
    return 0;
}
/*
输出结果:
8
5
*/
  1. strlen itself is a library function, so the length is calculated during the running of the program; while sizeof is calculated at the time of compilation;

  2. The parameter of sizeof can be a type or a variable; the parameter of strlen must be a variable of type char*.

4.2 Specific applications and usage scenarios of lambda expressions (anonymous functions)

The definition form of lambda expression is as follows:

[capture list] (parameter list) -> reurn type
{
    
    
   function body
}

in:

  • capture list: The capture list refers to the list of local variables defined in the function where the lambda expression is located. It is usually empty, but if the local variable of the function where the lambda expression is located is used in the function body, the variable must be captured, that is, the variable must be written in capture list. Capture methods are divided into: reference capture method [&], value capture method [=].
  • return type, parameter list, and function body: respectively indicate the return value type, parameter list, and function body, which are the same as ordinary functions.

Example:
lambda expressions are often used with sorting algorithms.

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

int main()
{
    
    
    vector<int> arr = {
    
    3, 4, 76, 12, 54, 90, 34};
    sort(arr.begin(), arr.end(), [](int a, int b) {
    
     return a > b; }); // 降序排序
    for (auto a : arr)
    {
    
    
        cout << a << " ";
    }
    return 0;
}
/*
运行结果:90 76 54 34 12 4 3
*/

4.3 The role of explicit (how to avoid implicit type conversion by the compiler)

Function: It is used to declare that the class constructor is called explicitly, not implicitly, and can prevent implicit conversion when calling the constructor. It can only be used to modify single-argument constructors, because no-argument constructors and multi-argument constructors are explicitly called, and the addition of the explicit keyword is meaningless.

Implicit conversion:

#include <iostream>
#include <cstring>
using namespace std;

class A
{
    
    
public:
    int var;
    A(int tmp)
    {
    
    
        var = tmp;
    }
};
int main()
{
    
    
    A ex = 10; // 发生了隐式转换
    return 0;
}

In the above code, A ex = 10; Implicit conversion is performed at compile time, 10 is converted into an object of type A, and then the object is assigned to ex, which is equivalent to the following operation:

In order to avoid implicit conversion, it can be declared with the explicit keyword:

#include <iostream>
#include <cstring>
using namespace std;

class A
{
    
    
public:
    int var;
    explicit A(int tmp)
    {
    
    
        var = tmp;
        cout << var << endl;
    }
};
int main()
{
    
    
    A ex(100);
    A ex1 = 10; // error: conversion from 'int' to non-scalar type 'A' requested
    return 0;
}

4.4 Difference between C and C++ static

  • In C language, use static to define local static variables, external static variables, static functions
  • In C++, use static to define local static variables, external static variables, static functions, static member variables, and static member functions. Because there is the concept of class in C++, static member variables and static member functions are all concepts related to classes.

4.5 The role of static

Function:
static defines static variables and static functions.

  • Keep variable content persistent: static acts on local variables, changing the life cycle of local variables so that the variable exists from the time it is defined until the end of the program.
#include <iostream>
using namespace std;

int fun(){
    
    
    static int var = 1; // var 只在第一次进入这个函数的时初始化
    var += 1;
    return var;
}
  
int main()
{
    
    
    for(int i = 0; i < 10; ++i)
    	cout << fun() << " "; // 2 3 4 5 6 7 8 9 10 11
    return 0;
}
  • Hidden: static acts on global variables and functions, changing the scope of global variables and functions, so that global variables and functions can only be used in the file in which they are defined, and do not have global visibility in the source file. (Note: Ordinary global variables and functions have global visibility, that is, other source files can also be used.)
  • static acts on the member variables and member functions of the class, so that the class variables or member functions are related to the class, that is to say, these static members can be accessed through the class without defining the object of the class. Note: Static member functions of a class can only access static member variables or static member functions, and static member functions cannot be defined as virtual functions.
#include<iostream>
using namespace std;

class A
{
    
    
private:
    int var;
    static int s_var; // 静态成员变量
public:
    void show()
    {
    
    
        cout << s_var++ << endl;
    }
    static void s_show()
    {
    
    
        cout << s_var << endl;
		// cout << var << endl; // error: invalid use of member 'A::a' in static member function. 静态成员函数不能调用非静态成员变量。无法使用 this.var
        // show();  // error: cannot call member function 'void A::show()' without object. 静态成员函数不能调用非静态成员函数。无法使用 this.show()
    }
};
int A::s_var = 1;  // 静态成员变量在类外进行初始化赋值,默认初始化为 0

int main()
{
    
    
    
    // cout << A::sa << endl;    // error: 'int A::sa' is private within this context
    A ex;
    ex.show();
    A::s_show();
}

4.6 Precautions for using static in classes (definition, initialization and use)

static static member variables:


  1. Static member variables are declared within the class, defined and initialized outside the class, and the static keyword and private, public, and protected access rules should not appear when defining and initializing outside the class .
  2. Static member variables are equivalent to global variables in the class domain and are shared by all objects of the class, including objects of derived classes.
  3. Static member variables can be used as parameters of member functions, but ordinary member variables cannot.
#include <iostream>
using namespace std;

class A
{
    
    
public:
    static int s_var;
    int var;
    void fun1(int i = s_var); // 正确,静态成员变量可以作为成员函数的参数
    void fun2(int i = var);   //  error: invalid use of non-static data member 'A::var'
};
int main()
{
    
    
    return 0;
}
  1. The type of a static data member can be the type of the class to which it belongs, while the type of an ordinary data member can only be a pointer or reference of the class type.
#include <iostream>
using namespace std;

class A
{
    
    
public:
    static A s_var; // 正确,静态数据成员
    A var;          // error: field 'var' has incomplete type 'A'
    A *p;           // 正确,指针
    A &var1;        // 正确,引用
};

int main()
{
    
    
    return 0;
}

static static member function:

  1. Static member functions cannot call non-static member variables or non-static member functions, because static member functions do not have this pointer. Static member functions act as global functions at class scope.
  2. Static member functions cannot be declared as virtual functions (virtual), const functions and volatile functions.

4.7 Similarities and differences between static global variables and ordinary global variables

Same point:

  • Storage method: ordinary global variables and static global variables are both static storage methods.

difference:

  • Scope: The scope of ordinary global variables is the entire source program. When a source program is composed of multiple source files, ordinary global variables are valid in each source file; static global variables limit their scope, namely It is only valid in the source file that defines the variable, and cannot be used in other source files of the same source program. Because the scope of the static global variable is limited to one source file, it can only be shared by the functions in this source file, so it can avoid causing errors in other source files.
  • Initialization: Static global variables are only initialized once to prevent them from being used in other files.

4.8 The function and usage of const

effect:

  • const modifies member variables and defines them as const constants. Compared with macro constants, type checking can be performed, which saves memory space and improves efficiency.
  • const modifies the function parameter so that the value of the passed function parameter cannot be changed.
  • The member function is modified by const, so that the member function cannot modify any type of member variable (except the variable modified by mutable), nor can it call the non-const member function, because the non-const member function may modify the member variable.

Usage in class:

const member variables:

  • const member variables can only be declared and defined within the class, and initialized in the constructor initialization list.
  • A const member variable is only constant during the lifetime of an object, but it is variable for the entire class, because a class can create multiple objects, and the values ​​of const member variables of different classes are different. Therefore, the const member variable cannot be initialized in the declaration of the class, the object of the class has not been created, and the compiler does not know its value.

const member functions:

  • The value of a member variable cannot be modified unless it is modified with mutable; only member variables can be accessed.
  • Non-constant member functions cannot be called to prevent modification of the value of member variables.

4.9 Difference between define and const

the difference:

  • Compilation stage: define is to be replaced in the compilation preprocessing stage, and const is to determine its value in the compilation stage.
  • Security: The macro constants defined by define have no data type and are simply replaced without type safety checks; the constants defined by const are typed and need to be judged to avoid some low-level mistakes.
  • Memory occupation: The macro constant defined by define will be replaced as many times as it is used in the program. There are multiple backups in the memory, which occupy the space of the code segment; the constant defined by const occupies the space of the static storage area, and the program runs There is only one copy in the process.
  • Debugging: The macro constants defined by define cannot be debugged because they have been replaced in the precompilation stage; the constants defined by cons can be debugged.

Advantages of const:

  • There are data types, and security checks can be performed in the definition.
  • adjustable.
  • Take up less space.

4.10 Difference between define and typedef

  • Principle: #define is used as a preprocessing instruction, and the replacement operation is performed during compilation and preprocessing, and no correctness check is performed. Only when the source program that has been expanded will it find possible errors and report an error. typedef is a keyword, processed at compile time, has type checking function, and is used to give an alias to an existing type, but typedef cannot be used in a function definition.
  • Function: typedef is used to define the alias of the type, which is convenient to use. #define can not only alias the type, but also define constants, variables, compilation switches, etc.
  • Scope: #define has no scope limitation, as long as it is a previously predefined macro, it can be used in future programs, while typedef has its own scope.
  • Operation of pointers: typedef and #define are not exactly the same when dealing with pointers
#include <iostream>
#define INTPTR1 int *
typedef int * INTPTR2;

using namespace std;

int main()
{
    
    
    INTPTR1 p1, p2; // p1: int *; p2: int
    INTPTR2 p3, p4; // p3: int *; p4: int *

    int var = 1;
    const INTPTR1 p5 = &var; // 相当于 const int * p5; 常量指针,即不可以通过 p5 去修改 p5 指向的内容,但是 p5 可以指向其他内容。
    const INTPTR2 p6 = &var; // 相当于 int * const p6; 指针常量,不可使 p6 再指向其他内容。
    
    return 0;
}

4.11 Use macros to realize comparison size and the minimum value of two numbers

#include <iostream>
#define MAX(X, Y) ((X)>(Y)?(X):(Y))
#define MIN(X, Y) ((X)<(Y)?(X):(Y))
using namespace std;

int main ()
{
    
    
    int var1 = 10, var2 = 100;
    cout << MAX(var1, var2) << endl;
    cout << MIN(var1, var2) << endl;
    return 0;
}
/*
程序运行结果:
100
10
*/

4.12 inline function and usage

Function:
inline is a keyword that can be used to define inline functions. Inline functions are called like ordinary functions, but they are not called through the function call mechanism but are directly expanded at the call point, which can greatly reduce the overhead caused by function calls, thereby improving the operating efficiency of the program.

Instructions:

  1. The default member function defined in the class is an inline function
    . The member function defined in the class does not need to add the inline keyword in the function head, because the compiler will automatically define the function (constructor, destructor, ordinary member function) in the class etc.) declared as an inline function, the code is as follows:
#include <iostream>
using namespace std;

class A{
    
    
public:
    int var;
    A(int tmp){
    
     
      var = tmp;
    }    
    void fun(){
    
     
        cout << var << endl;
    }
};

int main()
{
    
        
    return 0;
}
  1. Define a member function outside the class. If you want to define it as an inline function, you need to declare it with a keyword.
    When declaring a function inside a class and defining a function outside the class, if you want to define the function as an inline function, you can declare it inside the class The inline keyword is not added when the function is defined, but the inline keyword is added when the function is defined outside the class.
#include <iostream>
using namespace std;

class A{
    
    
public:
    int var;
    A(int tmp){
    
     
      var = tmp;
    }    
    void fun();
};

inline void A::fun(){
    
    
    cout << var << endl;
}

int main()
{
    
        
    return 0;
}

In addition, you can add inline when declaring the function and defining the function; you can also add inline only when the function is declared, but not when defining the function. Just make sure to tell the compiler about the inline before calling the function.

4.13 How inline functions work

  • The inline function does not have a control transfer relationship when it is called, but embeds the function body into each statement block that calls the function at the compilation stage, and the compiler uses the inline function in the calling expression that appears in the program. to replace the body of the function.
  • Ordinary functions transfer program execution to the memory address stored by the called function, and return to the place before the function was executed after the function is executed. The transfer operation needs to protect the site, and then restore the site after the called function is executed, which requires a large resource overhead.

4.14 Difference between macro definition (define) and inline function (inline)

  1. Inline functions are expanded at compile time, while macros are expanded during compilation and preprocessing; at compile time, inline functions are directly embedded into the object code, and macros are just a simple text replacement.
  2. An inline function is a real function, which is expanded directly at the call point in the same way as a normal function call, avoiding the push operation of function parameters and reducing the call overhead. The macro definition is more complicated to write, and it is often necessary to add some parentheses to avoid ambiguity.
  3. The macro definition only performs text replacement, and does not check the type of parameters, whether the statement can be compiled normally, etc. The inline function is a real function, which will check the type of parameters and whether the statements in the function body are written correctly.
#include <iostream>

#define MAX(a, b) ((a) > (b) ? (a) : (b))

using namespace std;

inline int fun_max(int a, int b)
{
    
    
    return a > b ? a : b;
}

int main()
{
    
    
    int var = 1;
    cout << MAX(var, 5) << endl;     
    cout << fun_max(var, 0) << endl; 
    return 0;
}
/*
程序运行结果:
5
1

*/

4.15 What is the function of new?

new is a keyword in C++, used to dynamically allocate memory space, the implementation is as follows:

int *p = new int[5]; 

4.16 How do new and malloc determine whether memory is allocated?

  • malloc: If the memory is successfully applied for, a pointer to the memory is returned; if the allocation fails, a NULL pointer is returned.
  • new: If the memory is allocated successfully, a pointer of the object type is returned; if the allocation fails, a bac_alloc exception is thrown.

4.17 What is the implementation principle of delete? The difference between delete and delete[]?

The implementation principle of delete:

  • First execute the destructor of the class to which the object belongs;
  • And then release the occupied memory space by calling the standard library function of operator delete.

The difference between delete and delete []:

  • delete is used to release the space occupied by a single object, and only calls the destructor once;
  • delete [] is used to release the array space, and the destructor will be called once for each member in the array.

4.18 The difference between new and malloc, the difference between delete and free

When using, new and delete are used together, and malloc and free are used together.

  • malloc and free are library functions, while new and delete are keywords.
  • When new applies for space, there is no need to specify the size of the allocated space, the compiler will calculate it by itself according to the type; when malloc applies for space, it needs to determine the size of the requested space.
  • When new applies for space, the returned type is the pointer type of the object, which is a type-safe operator without mandatory type conversion; when malloc applies for space, the returned type is void*, which needs to be converted to object type by mandatory type conversion. pointer.
  • When new allocation fails, a bad_alloc exception will be thrown, and when malloc allocation fails, a null pointer will be returned.
  • For a custom type, new first calls the operator new() function to apply for space (the bottom layer is implemented by malloc), then calls the constructor to initialize, and finally returns a pointer to the custom type; delete first calls the destructor, and then calls operator delete( ) to release space (the bottom layer is realized by free). malloc and free cannot construct and destruct objects of custom types.
  • The new operator dynamically allocates memory for an object from free storage, while the malloc function dynamically allocates memory from the heap. (free store is not equal to heap)

4.19 What is the principle of malloc? The underlying implementation of malloc?

The principle of malloc:

  • When the allocated space is less than 128K, call the brk() function by moving _enddata;
  • When the allocated space is larger than 128K, call the mmap() function, which is realized by opening up a memory space in the virtual address space.

The underlying implementation of malloc:

  • The realization principle of the brk() function: move the pointer _enddata pointing to the high address of the data segment to the direction of the high address.

  • The principle of mmap memory mapping: 1. The process starts the mapping process, and creates a virtual mapping area for the mapping in the virtual address space; 2. Calls the system call function mmap()
    in the kernel space to realize the one-to-one mapping between the physical address of the file and the virtual address of the process
    3.
    The process initiates access to this mapped space, causing a page fault exception, and realizing the copy of the file content to the physical memory (main memory).

4.20 What is the difference between C and C++ struct?

  • In C language, struct is a user-defined data type; in C++, struct is an abstract data type that supports the definition of member functions.
  • In C language, struct has no setting of access rights, it is a collection of some variables, and cannot define member functions; in C++, struct can have access rights like classes, and can define member functions.
  • For the self-defined data type defined by struct in C language, the struct keyword needs to be added when defining the variable of this type, for example: struct A var;, to define the variable of type A; while in C++, this keyword is not required, for example :A var;

4.21 Why do we keep struct when we have class?

  • C++ is developed on the basis of C language. In order to be compatible with C language, struct is reserved in C++.

4.22 The difference between struct and union

Explanation: union is a union, and struct is a structure.

the difference:

  • Unions and structures are composed of several data members of different data types. When used, unions have only one valid member; all members of structs are valid.
  • Assigning values ​​to different members of the union will overwrite the values ​​of other members, while assigning values ​​to different members of the structure will not affect each other.
  • The size of the union is the maximum value of all internal variables, and the size is allocated in multiples of the largest type; the size of memory allocated by the structure follows the principle of memory alignment.
#include <iostream>
using namespace std;

typedef union
{
    
    
    char c[10];
    char cc1; // char 1 字节,按该类型的倍数分配大小
} u11;

typedef union
{
    
    
    char c[10];
    int i; // int 4 字节,按该类型的倍数分配大小
} u22;

typedef union
{
    
    
    char c[10];
    double d; // double 8 字节,按该类型的倍数分配大小
} u33;

typedef struct s1
{
    
    
    char c;   // 1 字节
    double d; // 1(char)+ 7(内存对齐)+ 8(double)= 16 字节
} s11;

typedef struct s2
{
    
    
    char c;   // 1 字节
    char cc;  // 1(char)+ 1(char)= 2 字节
    double d; // 2 + 6(内存对齐)+ 8(double)= 16 字节
} s22;

typedef struct s3
{
    
    
    char c;   // 1 字节
    double d; // 1(char)+ 7(内存对齐)+ 8(double)= 16 字节
    char cc;  // 16 + 1(char)+ 7(内存对齐)= 24 字节
} s33;

int main()
{
    
    
    cout << sizeof(u11) << endl; // 10
    cout << sizeof(u22) << endl; // 12
    cout << sizeof(u33) << endl; // 16
    cout << sizeof(s11) << endl; // 16
    cout << sizeof(s22) << endl; // 16
    cout << sizeof(s33) << endl; // 24

    cout << sizeof(int) << endl;    // 4
    cout << sizeof(double) << endl; // 8
    return 0;
}

4.23 Similarities and differences between class and struct

  • Both struct and class can customize data types and also support inheritance operations.
  • The default access level in struct is public, and the default inheritance level is also public; the default access level in class is private, and the default inheritance level is also private.
  • When class inherits struct or struct inherits class, the default inheritance level depends on the class or struct itself, class (private inheritance), struct (public inheritance), that is, depends on the default inheritance level of derived classes.
struct A{
    
    }class B : A{
    
    }; // private 继承 
struct C : B{
    
    }// public 继承

Example:

#include<iostream>

using namespace std;

class A{
    
    
public:
    void funA(){
    
    
        cout << "class A" << endl;
    }
};

struct B: A{
    
     // 由于 B 是 struct,A 的默认继承级别为 public
public:
    void funB(){
    
    
        cout << "class B" << endl;
    }
};

class C: B{
    
     // 由于 C 是 class,B 的默认继承级别为 private,所以无法访问基类 B 中的 printB 函数

};

int main(){
    
    
    A ex1;
    ex1.funA(); // class A

    B ex2;
    ex2.funA(); // class A
    ex2.funB(); // class B

    C ex3;
    ex3.funB(); // error: 'B' is not an accessible base of 'C'.
    return 0;
}
  • class can be used to define template parameters, and struct cannot be used to define template parameters.

4.24 What does volatile do? Is it atomic and what impact does it have on the compiler?

The role of volatile : When the value of an object may be changed outside the control or detection of the program, the object should be declared as volatile, telling the compiler that such an object should not be optimized.

volatile is not atomic.

The impact of volatile on the compiler : After using this keyword, the compiler will not optimize the corresponding object, that is, it will not cache variables from memory to registers, preventing multiple threads from using variables in memory. Use a variable in a register, causing a program error.

4.25 Under what circumstances must use volatile, can it be used together with const?

Scenarios using the volatile keyword:

  • When multiple threads will use a certain variable, and the value of the variable may change, the variable needs to be modified with the volatile keyword;
  • The variable accessed in the interrupt service routine or the variable of the hardware register of the parallel device is preferably modified with the volatile keyword.

The volatile keyword and the const keyword can be used at the same time, a certain type can be both volatile and const, and have the properties of both.

4.26 What happens when I return the address of a static variable in a function?

#include <iostream>
using namespace std;

int * fun(int tmp){
    
    
    static int var = 10;
    var *= tmp;
    return &var;
}

int main() {
    
    
    cout << *fun(5) << endl;
    return 0;
}

/*
运行结果:
50
*/

Explanation: In the above code, the static local variable var is defined in the function fun, so that the variable will not be destroyed after leaving the scope of the function, and the variable still exists when returning to the main function, so that the program can get the correct running result . However, the static local variable is not destroyed until the program finishes running, wasting memory space.

4.27 What is the role of extern C?

When a C++ program needs to call a function written in C, C++ uses a linkage directive, extern "C" to indicate the language used by any non-C++ function.
Example:

// 可能出现在 C++ 头文件<cstring>中的链接指示
extern "C"{
    
    
    int strcmp(const char*, const char*);
}

4.28 What is the result of sizeof(1==1) in C and C++?

C language code:

#include<stdio.h>

void main(){
    
    
    printf("%d\n", sizeof(1==1));
}

/*
运行结果:
4
*/

C++ code:

#include <iostream>
using namespace std;

int main() {
    
    
    cout << sizeof(1==1) << endl;
    return 0;
}

/*
1
*/

4.29 What is the underlying principle of the memcpy function?

void *memcpy(void *dst, const void *src, size_t size)
{
    
    
    char *psrc;
    char *pdst;

    if (NULL == dst || NULL == src)
    {
    
    
        return NULL;
    }

    if ((src < dst) && (char *)src + size > (char *)dst) // 出现地址重叠的情况,自后向前拷贝
    {
    
    
        psrc = (char *)src + size - 1;
        pdst = (char *)dst + size - 1;
        while (size--)
        {
    
    
            *pdst-- = *psrc--;
        }
    }
    else
    {
    
    
        psrc = (char *)src;
        pdst = (char *)dst;
        while (size--)
        {
    
    
            *pdst++ = *psrc++;
        }
    }

    return dst;
}

4.30 What's wrong with the strcpy function?

The defect of the strcpy function: the strcpy function does not check the size boundary of the destination buffer, but assigns all the source strings one by one to a continuous memory space starting from the address of the destination string, and adds the string terminator at the same time, which will cause Other variables are overwritten.

#include <iostream>
#include <cstring>
using namespace std;

int main()
{
    
    
    int var = 0x11112222;
    char arr[10];
    cout << "Address : var " << &var << endl;
    cout << "Address : arr " << &arr << endl;
    strcpy(arr, "hello world!");
    cout << "var:" << hex << var << endl; // 将变量 var 以 16 进制输出
    cout << "arr:" << arr << endl;
    return 0;
}

/*
Address : var 0x23fe4c
Address : arr 0x23fe42
var:11002164
arr:hello world!
*/

Explanation : It can be seen from the above code that the last six digits of the variable var are changed by the three characters "d!\0" of the string "hello world!", and the corresponding ascii code of the three characters is in hexadecimal It is: \0(0x00), !(0x21), d(0x64).
Reason : The variable arr only allocates 10 memory spaces. From the address in the above program, it can be seen that arr and var are continuously stored in the memory, but when calling the strcpy function to copy, the source string "hello world!" The memory space occupied is 13, so the memory space of var will be occupied during the copying process, causing the last six digits of var to be overwritten.

4.31 Principle of auto type deduction

The principle of auto type deduction:

The compiler deduces the type of the variable according to the initial value, and it is required to have an initial value when defining a variable with auto. Sometimes the auto type inferred by the compiler is not exactly the same as the initial value type, and the compiler will appropriately change the result type to make it more consistent with the initialization rules.

5. Class related

5.1 What is a virtual function? What is a pure virtual function?

Virtual function : A member function modified by the virtual keyword is a virtual function.

#include <iostream>
using namespace std;

class A
{
    
    
public:
    virtual void v_fun() // 虚函数
    {
    
    
        cout << "A::v_fun()" << endl;
    }
};
class B : public A
{
    
    
public:
    void v_fun()
    {
    
    
        cout << "B::v_fun()" << endl;
    }
};
int main()
{
    
    
    A *p = new B();
    p->v_fun(); // B::v_fun()
    return 0;
}

Pure virtual function:

  • When the pure virtual function is declared in the class, add =0;
  • A class containing a pure virtual function is called an abstract class (as long as it contains a pure virtual function, the class is an abstract class), and there are only interfaces in the class, and there is no specific implementation method;
  • If a derived class that inherits a pure virtual function does not fully implement the pure virtual function of the base class, it is still an abstract class and cannot instantiate an object.

illustrate:

  • Abstract class objects cannot be used as parameters of functions, cannot create objects, and cannot be used as function return types;
  • You can declare abstract class pointers, and you can declare abstract class references;
  • The subclass must inherit the pure virtual functions of the parent class and realize all of them before creating an object of the subclass.

5.2 What is the difference between a virtual function and a pure virtual function?

  • Virtual functions and pure virtual functions can appear in the same class, which is called an abstract base class. (Classes with pure virtual functions are called abstract base classes)
  • The usage is different: the virtual function can be used directly, and the pure virtual function must be implemented in the derived class before it can be used;
  • The definition forms are different: when defining a virtual function, add the virtual keyword on the basis of the ordinary function, and when defining a pure virtual function, in addition to adding the virtual keyword, you need to add =0;
  • The virtual function must be implemented, otherwise the compiler will report an error;
  • For a derived class that implements a pure virtual function, the pure virtual function is called a virtual function in the derived class, and both the virtual function and the pure virtual function can be rewritten in the derived class;
  • The destructor is best defined as a virtual function, especially for classes with inheritance relationships; the destructor can be defined as a pure virtual function, at this time, the class it is in is an abstract base class, and instantiated objects cannot be created.

5.3 Implementation mechanism of virtual functions

Implementation mechanism : Virtual functions are implemented through virtual function tables. The address of the virtual function is stored in the virtual function table. In the memory space where the object of the class is located, a pointer to the virtual function table (called "virtual table pointer") is stored. The virtual function corresponding to the class can be found through the virtual table pointer. surface. The virtual function table solves the inheritance problem of the base class and the derived class and the coverage problem of the member functions in the class. When a pointer of the base class is used to operate a derived class, this virtual function table indicates the actual function that should be called.

Virtual function table related knowledge points:

  • The content stored in the virtual function table: the address of the virtual function of the class.
  • The time when the virtual function table is established: the compilation stage, that is, the address of the virtual function will be placed in the virtual function table during the compilation process of the program.
  • The location where the virtual table pointer is saved: the virtual table pointer is stored at the frontmost location in the memory space of the object, this is to ensure that the offset of the virtual function is correctly fetched.

Note: virtual function table and class binding, virtual table pointer and object binding. That is, the virtual function tables of different objects of the class are the same, but each object has its own virtual table pointer to point to the virtual function table of the class.

Example:

Cases without virtual function coverage:

#include <iostream>
using namespace std;

class Base
{
    
    
public:
    virtual void B_fun1() {
    
     cout << "Base::B_fun1()" << endl; }
    virtual void B_fun2() {
    
     cout << "Base::B_fun2()" << endl; }
    virtual void B_fun3() {
    
     cout << "Base::B_fun3()" << endl; }
};

class Derive : public Base
{
    
    
public:
    virtual void D_fun1() {
    
     cout << "Derive::D_fun1()" << endl; }
    virtual void D_fun2() {
    
     cout << "Derive::D_fun2()" << endl; }
    virtual void D_fun3() {
    
     cout << "Derive::D_fun3()" << endl; }
};
int main()
{
    
    
    Base *p = new Derive();
    p->B_fun1(); // Base::B_fun1()
    return 0;
}

The inheritance relationship between the base class and the derived class:
insert image description here
the virtual function table of the base class:
insert image description here
the virtual function table of the derived class:
insert image description here
the pointer p of the base class in the main function points to the object of the derived class, when calling the function B_fun1(), through the derived class The virtual function table finds the address of the function, thus completing the call.

5.4 Virtual function table structure of single inheritance and multiple inheritance

The compiler processes virtual function tables:

  • The compiler puts the pointer of the virtual function table in the memory space of the instance object of the class. When the object calls the virtual function of the class, it finds the virtual function table through the pointer, and finds the corresponding virtual function table according to the address of the virtual function stored in the virtual function table. virtual function.
  • If the derived class does not redefine the virtual function A of the base class, the virtual function table of the derived class stores the address of the virtual function A of the base class, that is to say, the addresses of the virtual function A of the base class and the derived class are the same .
  • If a derived class rewrites a virtual function B of the base class, the derived virtual function table stores the address of the rewritten virtual function B, that is to say, there are two versions of the virtual function B, which are stored in the base class respectively. In the virtual function table of the class and derived classes.
  • If the derived class redefines a new virtual function C, the virtual function table of the derived class stores the address of the new virtual function C.
  1. The case of single inheritance without virtual function coverage:
#include <iostream>
using namespace std;

class Base
{
    
    
public:
    virtual void B_fun1() {
    
     cout << "Base::B_fun1()" << endl; }
    virtual void B_fun2() {
    
     cout << "Base::B_fun2()" << endl; }
    virtual void B_fun3() {
    
     cout << "Base::B_fun3()" << endl; }
};

class Derive : public Base
{
    
    
public:
    virtual void D_fun1() {
    
     cout << "Derive::D_fun1()" << endl; }
    virtual void D_fun2() {
    
     cout << "Derive::D_fun2()" << endl; }
    virtual void D_fun3() {
    
     cout << "Derive::D_fun3()" << endl; }
};
int main()
{
    
    
    Base *p = new Derive();
    p->B_fun1(); // Base::B_fun1()
    return 0;
}

Inheritance relationship between base class and derived class:
insert image description here
virtual function table of base class:
insert image description here
virtual function table of derived class:
insert image description here
2. In the case of single inheritance with virtual function coverage:

#include <iostream>
using namespace std;

class Base
{
    
    
public:
    virtual void fun1() {
    
     cout << "Base::fun1()" << endl; }
    virtual void B_fun2() {
    
     cout << "Base::B_fun2()" << endl; }
    virtual void B_fun3() {
    
     cout << "Base::B_fun3()" << endl; }
};

class Derive : public Base
{
    
    
public:
    virtual void fun1() {
    
     cout << "Derive::fun1()" << endl; }
    virtual void D_fun2() {
    
     cout << "Derive::D_fun2()" << endl; }
    virtual void D_fun3() {
    
     cout << "Derive::D_fun3()" << endl; }
};
int main()
{
    
    
    Base *p = new Derive();
    p->fun1(); // Derive::fun1()
    return 0;
}

The virtual function table of the derived class:
insert image description here
3. The case of multiple inheritance without virtual function coverage:

#include <iostream>
using namespace std;

class Base1
{
    
    
public:
    virtual void B1_fun1() {
    
     cout << "Base1::B1_fun1()" << endl; }
    virtual void B1_fun2() {
    
     cout << "Base1::B1_fun2()" << endl; }
    virtual void B1_fun3() {
    
     cout << "Base1::B1_fun3()" << endl; }
};
class Base2
{
    
    
public:
    virtual void B2_fun1() {
    
     cout << "Base2::B2_fun1()" << endl; }
    virtual void B2_fun2() {
    
     cout << "Base2::B2_fun2()" << endl; }
    virtual void B2_fun3() {
    
     cout << "Base2::B2_fun3()" << endl; }
};
class Base3
{
    
    
public:
    virtual void B3_fun1() {
    
     cout << "Base3::B3_fun1()" << endl; }
    virtual void B3_fun2() {
    
     cout << "Base3::B3_fun2()" << endl; }
    virtual void B3_fun3() {
    
     cout << "Base3::B3_fun3()" << endl; }
};

class Derive : public Base1, public Base2, public Base3
{
    
    
public:
    virtual void D_fun1() {
    
     cout << "Derive::D_fun1()" << endl; }
    virtual void D_fun2() {
    
     cout << "Derive::D_fun2()" << endl; }
    virtual void D_fun3() {
    
     cout << "Derive::D_fun3()" << endl; }
};

int main(){
    
    
    Base1 *p = new Derive();
    p->B1_fun1(); // Base1::B1_fun1()
    return 0;
}

The relationship between the base class and the derived class:
insert image description here
the virtual function table of the derived class: (the order of the base class is the same as the order of the declaration)
insert image description here
4. In the case of multiple inheritance with virtual function coverage:

#include <iostream>
using namespace std;

class Base1
{
    
    
public:
    virtual void fun1() {
    
     cout << "Base1::fun1()" << endl; }
    virtual void B1_fun2() {
    
     cout << "Base1::B1_fun2()" << endl; }
    virtual void B1_fun3() {
    
     cout << "Base1::B1_fun3()" << endl; }
};
class Base2
{
    
    
public:
    virtual void fun1() {
    
     cout << "Base2::fun1()" << endl; }
    virtual void B2_fun2() {
    
     cout << "Base2::B2_fun2()" << endl; }
    virtual void B2_fun3() {
    
     cout << "Base2::B2_fun3()" << endl; }
};
class Base3
{
    
    
public:
    virtual void fun1() {
    
     cout << "Base3::fun1()" << endl; }
    virtual void B3_fun2() {
    
     cout << "Base3::B3_fun2()" << endl; }
    virtual void B3_fun3() {
    
     cout << "Base3::B3_fun3()" << endl; }
};

class Derive : public Base1, public Base2, public Base3
{
    
    
public:
    virtual void fun1() {
    
     cout << "Derive::fun1()" << endl; }
    virtual void D_fun2() {
    
     cout << "Derive::D_fun2()" << endl; }
    virtual void D_fun3() {
    
     cout << "Derive::D_fun3()" << endl; }
};

int main(){
    
    
    Base1 *p1 = new Derive();
    Base2 *p2 = new Derive();
    Base3 *p3 = new Derive();
    p1->fun1(); // Derive::fun1()
    p2->fun1(); // Derive::fun1()
    p3->fun1(); // Derive::fun1()
    return 0;
}

The relationship between the base class and the derived class:
insert image description here
the virtual function table of the derived class:
insert image description here

6. Language feature related

6.1 What is the difference between lvalue and rvalue? The difference between lvalue reference and rvalue reference, how to convert lvalue to rvalue?

Lvalue: Refers to the persistent object that still exists after the expression ends.

Rvalue: A temporary object that ceases to exist when the expression ends.

The difference between lvalue and rvalue: lvalue is persistent, rvalue is short-lived

Difference between rvalue reference and lvalue reference:

  • An lvalue reference cannot be bound to an expression being converted, a literal constant, or an expression returning an rvalue. Rvalue references are just the opposite, and can bind to such expressions, but not to an lvalue.
  • An rvalue reference must be bound to an rvalue reference, obtained with && . An rvalue reference can only be bound to an object that is about to be destroyed, so its resources can be moved freely.

std::move can coerce an lvalue to an rvalue, which can then be used through an rvalue reference for move semantics.

#include <iostream>
using namespace std;

void fun1(int& tmp) 
{
    
     
  cout << "fun1(int& tmp):" << tmp << endl; 
} 

void fun2(int&& tmp) 
{
    
     
  cout << "fun2(int&& tmp)" << tmp << endl; 
} 

int main() 
{
    
     
  int var = 11; 
  fun1(12); // error: cannot bind non-const lvalue reference of type 'int&' to an rvalue of type 'int'
  fun1(var);
  fun2(1); 
}

6.2 Implementation principle of std::move() function

std::move() function prototype:

template <typename T>
typename remove_reference<T>::type&& move(T&& t)
{
    
    
	return static_cast<typename remove_reference<T>::type &&>(t);
}

Explanation: Quote the principle of folding

  • The formal parameter T&& passed from an rvalue to the above function is still an rvalue, that is, T&& && is equivalent to T&&.
  • The formal parameter T&& passed from the lvalue to the above function is still an lvalue, that is, T&& & is equivalent to T&.

Summary: By referring to the principle of folding, we can know that the formal parameter of the move() function can be either an lvalue or an rvalue.

The specific implementation of remove_reference :

//原始的,最通用的版本
template <typename T> struct remove_reference{
    
    
    typedef T type;  //定义 T 的类型别名为 type
};
 
//部分版本特例化,将用于左值引用和右值引用
template <class T> struct remove_reference<T&> //左值引用
{
    
     typedef T type; }
 
template <class T> struct remove_reference<T&&> //右值引用
{
    
     typedef T type; }   
  
//举例如下,下列定义的a、b、c三个变量都是int类型
int i;
remove_refrence<decltype(42)>::type a;             //使用原版本,
remove_refrence<decltype(i)>::type  b;             //左值引用特例版本
remove_refrence<decltype(std::move(i))>::type  b;  //右值引用特例版本 

Example:

int var = 10; 

转化过程:
1. std::move(var) => std::move(int&& &) => 折叠后 std::move(int&)

2. 此时:T 的类型为 int&typename remove_reference<T>::type 为 int,这里使用 remove_reference 的左值引用的特例化版本

3. 通过 static_castint& 强制转换为 int&&

整个std::move被实例化如下
string&& move(int& t) 
{
    
    
    return static_cast<int&&>(t); 
}

Summary:
std::move() implementation principle:

  • Use the principle of reference folding to pass rvalues ​​through T&& to keep the type unchanged or rvalues, and to turn lvalues ​​into
    ordinary lvalue references through T&&, so as to ensure that the template can pass any actual parameter and keep the type unchanged;
  • Then remove the reference through remove_refrence to get the specific type T;
  • Finally, static_cast<> is used for mandatory type conversion, and a T&& rvalue reference is returned.

6.3 What is a pointer? Pointer size and usage?

Pointer : A composite type that points to another type.
Size of pointers : On 64-bit computers, pointers occupy 8 bytes of space.

#include<iostream>

using namespace std;

int main(){
    
    
    int *p = nullptr;
    cout << sizeof(p) << endl; // 8

    char *p1 = nullptr;
    cout << sizeof(p1) << endl; // 8
    return 0;
}

Usage of pointers:

  1. pointer to plain object
#include <iostream>

using namespace std;

class A
{
    
    
};

int main()
{
    
    
    A *p = new A();
    return 0;
}
  1. pointer to constant object: const pointer
#include <iostream>
using namespace std;

int main(void)
{
    
    
    const int c_var = 10;
    const int * p = &c_var;
    cout << *p << endl;
    return 0;
}
  1. pointer to function: function pointer
#include <iostream>
using namespace std;

int add(int a, int b){
    
    
    return a + b;
}

int main(void)
{
    
    
    int (*fun_p)(int, int);
    fun_p = add;
    cout << fun_p(1, 6) << endl;
    return 0;
}
  1. Pointers to object members, including pointers to object member functions and pointers to object member variables.
    Special attention: When defining a pointer to a member function, it is necessary to indicate the class to which the pointer belongs.
#include <iostream>

using namespace std;

class A
{
    
    
public:
    int var1, var2; 
    int add(){
    
    
        return var1 + var2;
    }
};

int main()
{
    
    
    A ex;
    ex.var1 = 3;
    ex.var2 = 4;
    int *p = &ex.var1; // 指向对象成员变量的指针
    cout << *p << endl;

    int (A::*fun_p)();
    fun_p = A::add; // 指向对象成员函数的指针 fun_p
    cout << (ex.*fun_p)() << endl;
    return 0;
}
  1. this pointer: A pointer constant pointing to the current object of the class.
#include <iostream>
#include <cstring>
using namespace std;

class A
{
    
    
public:
    void set_name(string tmp)
    {
    
    
        this->name = tmp;
    }
    void set_age(int tmp)
    {
    
    
        this->age = age;
    }
    void set_sex(int tmp)
    {
    
    
        this->sex = tmp;
    }
    void show()
    {
    
    
        cout << "Name: " << this->name << endl;
        cout << "Age: " << this->age << endl;
        cout << "Sex: " << this->sex << endl;
    }

private:
    string name;
    int age;
    int sex;
};

int main()
{
    
    
    A *p = new A();
    p->set_name("Alice");
    p->set_age(16);
    p->set_sex(1);
    p->show();

    return 0;
}
  1. What are wild pointers and dangling pointers?
    dangling pointer:

If the pointer points to a memory space, after the memory space is released, the pointer still points to the memory space. At this time, the pointer is called a "dangling pointer".

void *p = malloc(size);
free(p); 
// 此时,p 指向的内存空间已释放, p 就是悬空指针。

wild pointer:

"Wild pointer" refers to a pointer whose point is uncertain, and an uninitialized pointer is a "wild pointer".

void *p; 
// 此时 p 是“野指针”。

6.5 Advantages of C++11 nullptr over NULL

  • NULL: The preprocessing variable is a macro whose value is 0 and defined in the header file, ie #define NULL 0.
  • nullptr: A keyword in C++11, which is a special type of literal that can be converted to any other type.

Advantages of nullptr:

  1. There are types, the type is typdef decltype(nullptr) nullptr_t;, use nullptr to improve the robustness of the code.
  2. Function overloading: Because NULL is essentially 0, in the process of function calling, if the function is overloaded and the actual parameter passed is NULL, it may appear that it does not know which function matches; but passing the actual parameter nullptr will not This happens.
#include <iostream>
#include <cstring>
using namespace std;

void fun(char const *p)
{
    
    
    cout << "fun(char const *p)" << endl;
}

void fun(int tmp)
{
    
    
    cout << "fun(int tmp)" << endl;
}

int main()
{
    
    
    fun(nullptr); // fun(char const *p)
    /*
    fun(NULL); // error: call of overloaded 'fun(NULL)' is ambiguous
    */
    return 0;
}

6.6 What is the difference between a pointer and a reference?

  • The memory space pointed to by the pointer can be changed during the running of the program, but the object bound by the reference cannot be changed once bound. (whether variable)
  • The pointer itself occupies the memory space in the memory, and the reference is equivalent to the alias of the variable, and does not occupy the memory space in the memory. (whether it occupies memory)
  • Pointers can be null, but references must bind objects. (can be empty)
  • Pointers can have multiple levels, but references can only have one level. (Can it be multi-level)

6.7 Difference between constant pointer and pointer constant

Constant pointer:
A constant pointer is essentially a pointer, except that the object pointed to by this pointer is a constant.
Features : The position of const is on the left side of the pointer declaration operator *. As long as const is to the left of *, whether it is to the left or right of the type name, it means a pointer to a constant. (It can be understood that the left side of * indicates the object pointed to by the pointer, and the object is a constant, so the pointer is a constant pointer.)

const int * p;
int const * p;

Note 1: The object pointed by the pointer cannot be modified through this pointer, that is to say, the constant pointer can be assigned as the address of the variable. The reason why it is called a constant pointer is that it limits the value of the variable through this pointer.

#include <iostream>
using namespace std;

int main()
{
    
    
    const int c_var = 8;
    const int *p = &c_var; 
    *p = 6;            // error: assignment of read-only location '* p'
    return 0;
}

Note 2: Although the object pointed to by the constant pointer cannot be changed, because the constant pointer itself is a variable, it can be reassigned.
For example:

#include <iostream>
using namespace std;

int main()
{
    
    
    const int c_var1 = 8;
    const int c_var2 = 8;
    const int *p = &c_var1; 
    p = &c_var2;
    return 0;
}

Pointer constant:
A pointer constant is essentially a constant, except that the value of this constant is a pointer.
Features: const is located on the right side of the pointer declaration operator, indicating that the object itself is a constant, and the left side of * indicates the type pointed to by the pointer, that is, with * as the dividing line, the left side indicates the type pointed to by the pointer, and the right side indicates the pointer itself nature.

const int var;
int * const c_p = &var; 

Note 1: The value of a pointer constant is a pointer. Because this value is a constant, the pointer itself cannot be changed.

#include <iostream>
using namespace std;

int main()
{
    
    
    int var, var1;
    int * const c_p = &var;
    c_p = &var1; // error: assignment of read-only variable 'c_p'
    return 0;
}

Note 2: The content of the pointer can change.

#include <iostream>
using namespace std;

int main()
{
    
    
    int var = 3;
    int * const c_p = &var;
    *c_p = 12; 
    return 0;
}

6.8 Difference between function pointer and pointer function

Pointer function:
A pointer function is essentially a function, except that the return value of the function is a pointer. Compared with ordinary functions, only the return value is a pointer.

#include <iostream>
using namespace std;

struct Type
{
    
    
  int var1;
  int var2;
};

Type * fun(int tmp1, int tmp2){
    
    
    Type * t = new Type();
    t->var1 = tmp1;
    t->var2 = tmp2;
    return t;
}

int main()
{
    
    
    Type *p = fun(5, 6);
    return 0;
}

Function pointer:
A function pointer is essentially a pointer variable, except that the pointer points to a function. A function pointer is a pointer to a function.

Example:

#include <iostream>
using namespace std;
int fun1(int tmp1, int tmp2)
{
    
    
  return tmp1 * tmp2;
}
int fun2(int tmp1, int tmp2)
{
    
    
  return tmp1 / tmp2;
}

int main()
{
    
    
  int (*fun)(int x, int y); 
  fun = fun1;
  cout << fun(15, 5) << endl; 
  fun = fun2;
  cout << fun(15, 5) << endl; 
  return 0;
}
/*
运行结果:
75
3
*/

The difference between function pointer and pointer function:

  • The essence is different
    1. The pointer function is essentially a function whose return value is a pointer.
    2. The function pointer is essentially a pointer variable, which points to a function.
  • The definition forms are different
    1. Pointer function: int* fun(int tmp1, int tmp2); where * means that the return value type of the function is a pointer type.
    2. Function pointer: int ( fun)(int tmp1, int tmp2);, here indicates that the variable itself is a pointer type.
  • usage is different

6.9 What types of mandatory type conversions are there?

  • static_cast : Used for mandatory type conversion of data, forcibly converting one data type to another data type.
    1. Used for conversion of basic data types.
    2. It is used for the conversion of pointers or references between base classes and derived classes between class hierarchies (it is not required to contain virtual functions, but must be classes that are related to each other), and perform uplink conversion (pointer or reference conversion of derived classes It is safe to perform downcasting (the pointer or reference of the base class is converted into the representation of the derived class) because there is no dynamic type check, so it is unsafe, and it is best to use dynamic_cast for downcasting.
    3. A null pointer can be converted into a null pointer of the target type.
    4. Any type of expression can be converted into void type.
  • const_cast : Forcibly remove the constant attribute, can not be used to remove the constant of the variable, can only be used to remove the constant of the pointer or reference, convert the constant pointer to a non-constant pointer or convert a constant reference to a non-constant reference (note: the expression is the same type as the type to be converted).
  • reinterpret_cast : Change the type of pointer or reference, convert the pointer or reference to an integer of sufficient length, and convert the integer to a pointer or reference type.
  • dynamic_cast :
    1. The other three types are completed at compile time. Dynamic type conversion is processed when the program is running, and type checking will be performed at runtime.
    2. It can only be used for the conversion of pointers or reference objects of base classes or derived classes with virtual functions. If the conversion succeeds, it returns a pointer or reference to the type, and if the conversion fails, it returns NULL; it cannot be used for the conversion of basic data types.
    3. When converting upward, that is, the pointer of the derived class is converted to the pointer of the base class, and the effect of static_cast is the same. (Note: here only the type of the pointer is changed, and the type of the object pointed to by the pointer has not changed) .
#include <iostream>
#include <cstring>

using namespace std;

class Base
{
    
    
};

class Derive : public Base
{
    
    
};

int main()
{
    
    
    Base *p1 = new Derive();
    Derive *p2 = new Derive();

    //向上类型转换
    p1 = dynamic_cast<Base *>(p2);
    if (p1 == NULL)
    {
    
    
        cout << "NULL" << endl;
    }
    else
    {
    
    
        cout << "NOT NULL" << endl; //输出
    }

    return 0;
}

4. During the downstream conversion, the pointer type of the base class is converted to the pointer type of the derived class. Only when the object type pointed to by the pointer to be converted is the same as the converted object type, the conversion will succeed.

#include <iostream>
#include <cstring>

using namespace std;

class Base
{
    
    
public:
    virtual void fun()
    {
    
    
        cout << "Base::fun()" << endl;
    }
};

class Derive : public Base
{
    
    
public:
    virtual void fun()
    {
    
    
        cout << "Derive::fun()" << endl;
    }
};

int main()
{
    
    
    Base *p1 = new Derive();
    Base *p2 = new Base();
    Derive *p3 = new Derive();

    //转换成功
    p3 = dynamic_cast<Derive *>(p1);
    if (p3 == NULL)
    {
    
    
        cout << "NULL" << endl;
    }
    else
    {
    
    
        cout << "NOT NULL" << endl; // 输出
    }

    //转换失败
    p3 = dynamic_cast<Derive *>(p2);
    if (p3 == NULL)
    {
    
    
        cout << "NULL" << endl; // 输出
    }
    else
    {
    
    
        cout << "NOT NULL" << endl;
    }

    return 0;
}

6.10 How to judge whether the structures are equal? Can the memcmp function be used to judge structure equality?

Operator == needs to be overloaded to judge whether two structures are equal, and the function memcmp cannot be used to judge whether two structures are equal, because the memcmp function is compared byte by byte, and there are characters when the structure is saved in the memory space Section alignment, when byte alignment, the complemented byte content is random, which will generate garbage values, so it cannot be compared.

Use operator overloading to compare struct objects:

#include <iostream>

using namespace std;

struct A
{
    
    
    char c;
    int val;
    A(char c_tmp, int tmp) : c(c_tmp), val(tmp) {
    
    }

    friend bool operator==(const A &tmp1, const A &tmp2); //  友元运算符重载函数
};

bool operator==(const A &tmp1, const A &tmp2)
{
    
    
    return (tmp1.c == tmp2.c && tmp1.val == tmp2.val);
}

int main()
{
    
    
    A ex1('a', 90), ex2('b', 80);
    if (ex1 == ex2)
        cout << "ex1 == ex2" << endl;
    else
        cout << "ex1 != ex2" << endl; // 输出
    return 0;
}

6.11 When passing parameters, what is the difference between passing by value, passing by reference, and passing by pointer?

Three ways to pass parameters:

  • Passing by value: The formal parameter is a copy of the actual parameter, and all operations of the function on the formal parameter will not affect the actual parameter.
  • Pointer passing: It is essentially value passing, except that the value of the pointer is copied. After copying, the actual parameter and the formal parameter are different pointers. Through the pointer, the object pointed to by the pointer can be accessed indirectly, so that the object it points to can be modified. value.
  • Pass by reference: When a formal parameter is a reference type, we say that its corresponding actual parameter is passed by reference.
#include <iostream>
using namespace std;

void fun1(int tmp){
    
     // 值传递
    cout << &tmp << endl;
}

void fun2(int * tmp){
    
     // 指针传递
    cout << tmp << endl;
}

void fun3(int &tmp){
    
     // 引用传递
    cout << &tmp << endl;
}

int main()
{
    
    
    int var = 5;
    cout << "var 在主函数中的地址:" << &var << endl;

    cout << "var 值传递时的地址:";
    fun1(var);

    cout << "var 指针传递时的地址:";
    fun2(&var);

    cout << "var 引用传递时的地址:";
    fun3(var);
    return 0;
}

/*
运行结果:
var 在主函数中的地址:0x23fe4c
var 值传递时的地址:0x23fe20
var 指针传递时的地址:0x23fe4c
var 引用传递时的地址:0x23fe4c
*/

Explanation: From the running results of the above code, it can be seen that only when the value is passed, the address of the formal parameter and the actual parameter are different, and the variable itself is not manipulated in the function body. Passing by reference and passing by pointer, the variable itself is manipulated in the function body.

6.12 What is a template? How to achieve?

Template : Create a blueprint or formula for a class or function, divided into function templates and class templates.
Implementation : A template definition begins with the keyword template , followed by a list of template parameters.

  • The template parameter list cannot be empty;
  • The keyword class or typename must be used before the template type parameter. In the template parameter list, these two keywords have the same meaning and can be used interchangeably.
template <typename T, typename U, ...>

Function templates: By defining a function template, you can avoid defining a new function for each type.

  • For function templates, the template type parameter can be used to specify the return type or parameter type of the function, as well as variable declaration or type conversion in the function body.
  • Function template instantiation: When calling a template, the compiler uses the function arguments to infer the template arguments, thereby using the types of the arguments to determine the types bound to the template parameters.
#include<iostream>

using namespace std;

template <typename T>
T add_fun(const T & tmp1, const T & tmp2){
    
    
    return tmp1 + tmp2;
}

int main(){
    
    
    int var1, var2;
    cin >> var1 >> var2;
    cout << add_fun(var1, var2);

    double var3, var4;
    cin >> var3 >> var4;
    cout << add_fun(var3, var4);
    return 0;
}

Class templates: Like function templates, class templates begin with the keyword template, followed by a list of template parameters. However, the compiler cannot infer the template parameter type for a class template, and the type needs to be specified in angle brackets after the template name when using the class template.

#include <iostream>

using namespace std;

template <typename T>
class Complex
{
    
    
public:
    //构造函数
    Complex(T a, T b)
    {
    
    
        this->a = a;
        this->b = b;
    }

    //运算符重载
    Complex<T> operator+(Complex &c)
    {
    
    
        Complex<T> tmp(this->a + c.a, this->b + c.b);
        cout << tmp.a << " " << tmp.b << endl;
        return tmp;
    }

private:
    T a;
    T b;
};

int main()
{
    
    
    Complex<int> a(10, 20);
    Complex<int> b(20, 30);
    Complex<int> c = a + b;

    return 0;
}

6.13 What is the difference between a function template and a class template?

  • The instantiation methods are different: the instantiation of function templates is automatically completed by the compiler when processing function calls, and the instantiation of class templates needs to be explicitly specified in the program.
  • The result of instantiation is different: the function template is a function after instantiation, and the class template is a class after instantiation.
  • Default parameters: Class templates can have default parameters in the template parameter list.
  • Specialization: Function templates can only be fully specialized; class templates can be fully or partially specialized.
  • The calling methods are different: function templates can be called implicitly or explicitly; class templates can only be called explicitly.
    Example of calling a function template:
#include<iostream>

using namespace std;

template <typename T>
T add_fun(const T & tmp1, const T & tmp2){
    
    
    return tmp1 + tmp2;
}

int main(){
    
    
    int var1, var2;
    cin >> var1 >> var2;
    cout << add_fun<int>(var1, var2); // 显式调用

    double var3, var4;
    cin >> var3 >> var4;
    cout << add_fun(var3, var4); // 隐式调用
    return 0;
}

6.14 What are variadic templates?

Variadic template: A template function or class that accepts a variable number of arguments. The variable number of parameters is called a parameter pack, including template parameter packs and function parameter packs.

  • Template parameter pack: represents zero or more template parameters;
  • Function parameter pack: Indicates zero or more function parameters.

Use an ellipsis to indicate that a template parameter or function parameter indicates a package, and in a template parameter list, class... or typename... indicates that the following parameters indicate a list of zero or more types; a typename followed by an ellipsis indicates zero or a list of multiple non-type parameters of the given type. When you need to know how many elements are in the bag, you can use the sizeof... operator.

template <typename T, typename... Args> // Args 是模板参数包
void foo(const T &t, const Args&... rest); // 可变参数模板,rest 是函数参数包
#include <iostream>

using namespace std;

template <typename T>
void print_fun(const T &t)
{
    
    
    cout << t << endl; // 最后一个元素
}

template <typename T, typename... Args>
void print_fun(const T &t, const Args &...args)
{
    
    
    cout << t << " ";
    print_fun(args...);
}

int main()
{
    
    
    print_fun("Hello", "wolrd", "!");
    return 0;
}
/*运行结果:
Hello wolrd !

*/

Explanation: Variadic functions are usually recursive, the first version of print_fun is responsible for terminating the recursion and printing the last argument in the initial call. The second version of print_fun is the variadic version that prints the arguments bound to t and is used to call itself to print the remaining values ​​in the function's argument pack.

6.15 What is template specialization? Why specialize?

Reasons for template specialization: Templates are not suitable for any template argument and can be instantiated. In some cases, the definition of a general template is not suitable for a specific type, and compilation may fail, or the correct result may not be obtained. Thus, a specialization of a class or function template can be defined when the template version is not desired.

Template specialization: The concrete implementation of a template parameter under a specific type. Divided into function template specialization and class template specialization

  • Function template specialization: specialization of all types in the function template is called function template specialization.
  • Class template specialization: specialization of some or all types in the class template is called class template specialization.

Specialization is divided into full specialization and partial specialization:

  • Full specialization: All template parameters in the template are specialized.
  • Partial specialization: Only part of the template parameters in the template are determined, and the rest needs to be determined when the compiler compiles.

Explanation: To distinguish between function overloading and function template specialization
, defining a specialized version of a function template essentially takes over the work of the compiler and defines a special instance for the original function template instead of function overloading. Function template specialization Normalization does not affect function matching.

#include <iostream>
#include <cstring>

using namespace std;
//函数模板
template <class T>
bool compare(T t1, T t2)
{
    
    
    cout << "通用版本:";
    return t1 == t2;
}

template <> //函数模板特化
bool compare(char *t1, char *t2)
{
    
    
    cout << "特化版本:";
    return strcmp(t1, t2) == 0;
}

int main(int argc, char *argv[])
{
    
    
    char arr1[] = "hello";
    char arr2[] = "abc";
    cout << compare(123, 123) << endl;
    cout << compare(arr1, arr2) << endl;

    return 0;
}
/*
运行结果:
通用版本:1
特化版本:0
*/

6.16 The difference between include " " and <>

The difference between include<filename> and #include "filename" :

  • Find the location of the file: include<file name> search in the directory where the standard library header file is located, if not, then search in the directory where the current source file is located; #include "file name" search in the directory where the current source file is located, If not; then look in the system directory.
  • Usage habits: For header files in the standard library, include<filename> is often used, and for header files defined by yourself, #include "filename" is often used

6.17 How to implement generic programming?

The foundation of generic programming: templates. A template is a blueprint or formula for creating a class or function. When using a generic type like vector or a generic function like find, it will be converted into a specific class or function during compilation.

Generic programming involves a wide range of knowledge points, for example: containers, iterators, algorithms, etc. are all implementation examples of generic programming. The interviewer can choose to expand on the aspect that he or she has a solid grasp of.

  • Container: It involves containers in STL, such as: vector, list, map, etc., and you can choose to explain the containers that are familiar with the underlying principles.
  • Iterator: Traverse the elements in the container without knowing the underlying principle of the container.
  • Templates: You can refer to template-related issues in this chapter.

7. Design Patterns

7.1 What design patterns do you know?

There are 24 design patterns mentioned in the book "Big Talk Design Patterns". These 24 design patterns do not need to be exhaustive, but you must have a deep understanding of several of them. It is best to combine your own examples in the actual development process for in-depth understanding.

Design patterns have 6 design principles:

  • Single Responsibility Principle: As far as a class is concerned, there should be only one reason for it to change.
  • Open and closed principle: software entities can be extended, but not modified. That is, in the face of demand, changes to the program can be done by adding code, but the existing code cannot be changed.
  • Liskov Substitution Principle: If a software entity uses a base class, it must be applicable to its derived classes. That is, in software, the behavior of the program does not change if the base class is replaced with a derived class.
  • Dependency Inversion Principle: Abstractions should not depend on details, details should depend on abstractions. That is, program to the interface, not the implementation.
  • Demeter Principle: If two classes do not communicate directly, then the two classes should not interact directly. If a class needs to call a method of another class, the call can be forwarded through the third class.
  • Interface isolation principle: There are no methods in each interface that cannot be used by derived classes but must be implemented. If not, the interface must be split and multiple isolated interfaces should be used.

Design patterns fall into three categories:

  • Creative patterns: singleton pattern, factory pattern, builder pattern, prototype pattern
  • Structural patterns: Adapter pattern, Bridge pattern, Appearance pattern, Composite pattern, Decoration pattern, Flyweight pattern, Proxy pattern
  • Behavioral patterns: chain of responsibility pattern, command pattern, interpreter pattern, iterator pattern, intermediary pattern, memo pattern, observer pattern, state pattern, strategy pattern, template method pattern, visitor pattern

Here are some common design patterns:

  • Singleton mode: Ensure that there is only one instance of a class and provide a global access point to access it.
  • Factory pattern: including simple factory pattern, abstract factory pattern, factory method pattern
    1. Simple factory pattern: mainly used to create objects. Use a factory to generate different classes according to the input conditions, and then get different results according to the virtual functions of different classes.
    2. Factory method pattern: Fixed the open and closed principle not being followed in the simple factory pattern. The selection judgment is moved to the client to implement. If you want to add new functions, you don’t need to modify the original class, you can directly modify the client.
    3. Abstract Factory Pattern: Defines a series of related or interdependent interfaces without specifying their concrete classes.
  • Observer mode: defines a one-to-many relationship, allowing multiple observers to monitor a subject object at the same time. When the subject object changes, all observers will be notified so that they can update themselves.
  • Decoration mode: Dynamically add some additional responsibilities to an object. In terms of adding functionality, the decoration mode is more flexible than generating derived classes.

7.2 What is the singleton pattern? How to achieve? Application scenario?

Singleton mode : Ensure that there is only one instantiated object of the class, and provide a global access point to access it.
Application scenario :

  • The class representing the file system, an operating system must have only one file system, so there is only one instance of the file system class.
  • An example of a printer printing program, a computer can be connected to several printers, but there is only one printing program on the computer, so the singleton mode can be used to prevent two print jobs from being output to the printer at the same time.

Implementation method :
The singleton mode can be implemented in the form of global or static variables, which is relatively simple, but it will affect the encapsulation, and it is difficult to ensure that other codes will not affect the global variables.

  • The default constructor, copy constructor, and assignment constructor are declared as private , which prohibits the creation of the object outside the class;
  • The global access point should also be defined as a member function of static type , without parameters, and returns the pointer type of this class. Because when using an instantiated object, the function is called directly through the class, instead of creating an object of the class first and calling it through the object.

Unsafe implementation:

Reason: Consider that when two threads call the getInstance method at the same time and detect that the instance is NULL at the same time, the two threads will instantiate the object at the same time, which does not meet the requirements of the singleton mode.

class Singleton{
    
    
private:
    static Singleton * instance;
    Singleton(){
    
    }
    Singleton(const Singleton& tmp){
    
    }
    Singleton& operator=(const Singleton& tmp){
    
    }
public:
    static Singleton* getInstance(){
    
    
        if(instance == NULL){
    
    
            instance = new Singleton();
        }
        return instance;
    }
};
Singleton* Singleton::instance = NULL;

Classification:

  • Lazy mode: It is not instantiated until the first instance of the class is used. The above is the lazy implementation.
  • Hungry man mode: when the class is defined, it is instantiated.

Implementation of thread-safe lazy man mode:
Method: Locking
Problems: Every time it is judged whether the instance object is empty, it must be locked. If it is multi-threaded, it will cause a large number of threads to block.

class Singleton{
    
    
private:
    static pthread_mutex_t mutex;
    static Singleton * instance;
    Singleton(){
    
    
        pthread_mutex_init(&mutex, NULL); 
    }
    Singleton(const Singleton& tmp){
    
    }
    Singleton& operator=(const Singleton& tmp){
    
    }
public:
    static Singleton* getInstance(){
    
    
        pthread_mutex_lock(&mutex);
        if(instance == NULL){
    
                
            instance = new Singleton();            
        }
        pthread_mutex_unlock(&mutex);
        return instance;
    }
};
Singleton* Singleton::instance = NULL;
pthread_mutex_t Singleton::mutex;

Method: Internal static variables , define static instances in the global access point getInstance.

class Singleton{
    
    
private:
    static Singleton* instance;
    Singleton(const Singleton& temp){
    
    }
    Singleton& operator=(const Singleton& temp){
    
    }
protected:
	 Singleton(){
    
    } 
public:
    static Singleton* getInstance(){
    
     
        return instance;    
    }
};
Singleton* Singleton::instance = new Singleton();

7.3 What is the factory pattern? How to achieve? Application scenario?

Factory pattern: including simple factory pattern, abstract factory pattern, factory method pattern

  • Simple factory pattern: mainly used to create objects. Use a factory to generate different classes according to the input conditions, and then get different results according to the virtual functions of different classes.
  • Factory method pattern: Fixed open closed principle not being respected in simple factory pattern. The selection judgment is moved to the client to implement. If you want to add new functions, you don’t need to modify the original class, you can directly modify the client.
  • Abstract Factory Pattern: Defines an interface that creates a series of related or interdependent interfaces without specifying their concrete classes.
  1. The simple factory pattern
    is mainly used to create objects. Use a factory to generate different classes according to the input conditions, and then get different results according to the virtual functions of different classes.

Application scenario:

  • When creating different classes for different situations, you only need to pass in the parameters of the factory class without knowing the specific implementation method. For example: For the same input in a calculator, different operations are performed: addition, subtraction, multiplication, and division.
    Method to realize:
#include <iostream>
#include <vector>
using namespace std;

// Here is the product class
class Operation
{
    
    
public:
    int var1, var2;
    virtual double GetResult()
    {
    
    
        double res = 0;
        return res;
    }
};

class Add_Operation : public Operation
{
    
    
public:
    virtual double GetResult()
    {
    
    
        return var1 + var2;
    }
};

class Sub_Operation : public Operation
{
    
    
public:
    virtual double GetResult()
    {
    
    
        return var1 - var2;
    }
};

class Mul_Operation : public Operation
{
    
    
public:
    virtual double GetResult()
    {
    
    
        return var1 * var2;
    }
};

class Div_Operation : public Operation
{
    
    
public:
    virtual double GetResult()
    {
    
    
        return var1 / var2;
    }
};

// Here is the Factory class
class Factory
{
    
    
public:
    static Operation *CreateProduct(char op)
    {
    
    
        switch (op)
        {
    
    
        case '+':
            return new Add_Operation();

        case '-':
            return new Sub_Operation();

        case '*':
            return new Mul_Operation();

        case '/':
            return new Div_Operation();

        default:
            return new Add_Operation();
        }
    }
};

int main()
{
    
    
    int a, b;
    cin >> a >> b;
    Operation *p = Factory::CreateProduct('+');
    p->var1 = a;
    p->var2 = b;
    cout << p->GetResult() << endl;

    p = Factory::CreateProduct('*');
    p->var1 = a;
    p->var2 = b;
    cout << p->GetResult() << endl;

    return 0;
}
  1. factory method pattern

Fixed open closed principle not being respected in simple factory pattern. The selection judgment is moved to the client to implement. If you want to add new functions, you don’t need to modify the original class, you can directly modify the client.

Application scenario:

A class does not know the class of the object it needs: In the factory method pattern, the client does not need to know the class name of the specific product class, but only needs to know the corresponding factory, and the specific product object is created by the specific factory class; The client needs to know the factory class for creating concrete products.
A class specifies which object to create through its derived class: In the factory method pattern, the abstract factory class only needs to provide an interface for creating products, and its derived class determines the specific object to be created, using object-oriented polymorphism Based on the nature and Liskov substitution principle, when the program is running, the derived class object will cover the parent class object, which makes the system easier to expand.
Delegate the task of creating objects to one of multiple factory-derived classes. When using it, the client does not need to care which factory-derived class creates the product-derived class. It can be dynamically specified when necessary. The class name of the specific factory class can be Stored in configuration file or database.

Method to realize:

#include <iostream>
#include <vector>
using namespace std;

// Here is the product class
class Operation
{
    
    
public:
    int var1, var2;
    virtual double GetResult()
    {
    
    
        double res = 0;
        return res;
    }
};

class Add_Operation : public Operation
{
    
    
public:
    virtual double GetResult()
    {
    
    
        return var1 + var2;
    }
};

class Sub_Operation : public Operation
{
    
    
public:
    virtual double GetResult()
    {
    
    
        return var1 - var2;
    }
};

class Mul_Operation : public Operation
{
    
    
public:
    virtual double GetResult()
    {
    
    
        return var1 * var2;
    }
};

class Div_Operation : public Operation
{
    
    
public:
    virtual double GetResult()
    {
    
    
        return var1 / var2;
    }
};

class Factory
{
    
    
public:
    virtual Operation *CreateProduct() = 0;
};

class Add_Factory : public Factory
{
    
    
public:
    Operation *CreateProduct()
    {
    
    
        return new Add_Operation();
    }
};

class Sub_Factory : public Factory
{
    
    
public:
    Operation *CreateProduct()
    {
    
    
        return new Sub_Operation();
    }
};

class Mul_Factory : public Factory
{
    
    
public:
    Operation *CreateProduct()
    {
    
    
        return new Mul_Operation();
    }
};

class Div_Factory : public Factory
{
    
    
public:
    Operation *CreateProduct()
    {
    
    
        return new Div_Operation();
    }
};

int main()
{
    
    
    int a, b;
    cin >> a >> b;
    Add_Factory *p_fac = new Add_Factory();
    Operation *p_pro = p_fac->CreateProduct();
    p_pro->var1 = a;

 - List item

    p_pro->var2 = b;
    cout << p_pro->GetResult() << endl;

    Mul_Factory *p_fac1 = new Mul_Factory();
    Operation *p_pro1 = p_fac1->CreateProduct();
    p_pro1->var1 = a;
    p_pro1->var2 = b;
    cout << p_pro1->GetResult() << endl;

    return 0;
}

  1. abstract factory pattern

Defines an interface that creates a series of related or interdependent classes without specifying their concrete classes.

Application scenario:

  • It is important for all types of factory patterns that a system should not depend on the details of how product class instances are created, composed, and represented.
  • There are more than one product families in the system, and only one of them is used at a time.
  • Products belonging to the same product family will be used together and this constraint must be reflected in the design of the system.
  • The product level structure is stable. After the design is completed, no new product level structure will be added to the system or the existing product level structure will not be deleted.

Method to realize:

#include <iostream>
#include <vector>
using namespace std;

// Here is the product class
class Operation_Pos
{
    
    
public:
    int var1, var2;
    virtual double GetResult()
    {
    
    
        double res = 0;
        return res;
    }
};

class Add_Operation_Pos : public Operation_Pos
{
    
    
public:
    virtual double GetResult()
    {
    
    
        return var1 + var2;
    }
};

class Sub_Operation_Pos : public Operation_Pos
{
    
    
public:
    virtual double GetResult()
    {
    
    
        return var1 - var2;
    }
};

class Mul_Operation_Pos : public Operation_Pos
{
    
    
public:
    virtual double GetResult()
    {
    
    
        return var1 * var2;
    }
};

class Div_Operation_Pos : public Operation_Pos
{
    
    
public:
    virtual double GetResult()
    {
    
    
        return var1 / var2;
    }
};
/*********************************************************************************/
class Operation_Neg
{
    
    
public:
    int var1, var2;
    virtual double GetResult()
    {
    
    
        double res = 0;
        return res;
    }
};

class Add_Operation_Neg : public Operation_Neg
{
    
    
public:
    virtual double GetResult()
    {
    
    
        return -(var1 + var2);
    }
};

class Sub_Operation_Neg : public Operation_Neg
{
    
    
public:
    virtual double GetResult()
    {
    
    
        return -(var1 - var2);
    }
};

class Mul_Operation_Neg : public Operation_Neg
{
    
    
public:
    virtual double GetResult()
    {
    
    
        return -(var1 * var2);
    }
};

class Div_Operation_Neg : public Operation_Neg
{
    
    
public:
    virtual double GetResult()
    {
    
    
        return -(var1 / var2);
    }
};
/*****************************************************************************************************/

// Here is the Factory class
class Factory
{
    
    
public:
    virtual Operation_Pos *CreateProduct_Pos() = 0;
    virtual Operation_Neg *CreateProduct_Neg() = 0;
};

class Add_Factory : public Factory
{
    
    
public:
    Operation_Pos *CreateProduct_Pos()
    {
    
    
        return new Add_Operation_Pos();
    }
    Operation_Neg *CreateProduct_Neg()
    {
    
    
        return new Add_Operation_Neg();
    }
};

class Sub_Factory : public Factory
{
    
    
public:
    Operation_Pos *CreateProduct_Pos()
    {
    
    
        return new Sub_Operation_Pos();
    }
    Operation_Neg *CreateProduct_Neg()
    {
    
    
        return new Sub_Operation_Neg();
    }
};

class Mul_Factory : public Factory
{
    
    
public:
    Operation_Pos *CreateProduct_Pos()
    {
    
    
        return new Mul_Operation_Pos();
    }
    Operation_Neg *CreateProduct_Neg()
    {
    
    
        return new Mul_Operation_Neg();
    }
};

class Div_Factory : public Factory
{
    
    
public:
    Operation_Pos *CreateProduct_Pos()
    {
    
    
        return new Div_Operation_Pos();
    }
    Operation_Neg *CreateProduct_Neg()
    {
    
    
        return new Div_Operation_Neg();
    }
};

int main()
{
    
    
    int a, b;
    cin >> a >> b;
    Add_Factory *p_fac = new Add_Factory();
    Operation_Pos *p_pro = p_fac->CreateProduct_Pos();
    p_pro->var1 = a;
    p_pro->var2 = b;
    cout << p_pro->GetResult() << endl;

    Add_Factory *p_fac1 = new Add_Factory();
    Operation_Neg *p_pro1 = p_fac1->CreateProduct_Neg();
    p_pro1->var1 = a;
    p_pro1->var2 = b;
    cout << p_pro1->GetResult() << endl;

    return 0;
}

7.4 What is the observer pattern? How to achieve? Application scenario?

Observer mode : define a one (observed class) to many (observed class) relationship, allowing multiple observed objects to monitor an observed object at the same time, when the state of the observed object changes, all observed objects will be notified, so that They are able to update their own state.

There are two roles in the Observer pattern:

  • Observer: It contains the observed object internally. When the state of the observed object changes, it updates its own state. (receive notification update status)
  • Observed: It contains all observer objects internally, and notifies all observers to update their state when the state changes. (send notification)

Application scenario:

  • When the change of an object needs to change other objects at the same time, and you don't know how many objects need to be changed, you should consider using the observer mode;
  • An abstract model has two aspects, one of which depends on the other. At this time, the observer pattern can be used to encapsulate the two in independent objects so that they can be changed and reused independently.

Method to realize:

#include <iostream>
#include <string>
#include <list>
using namespace std;

class Subject;
//观察者 基类 (内部实例化了被观察者的对象sub)
class Observer
{
    
    
protected:
    string name;
    Subject *sub;

public:
    Observer(string name, Subject *sub)
    {
    
    
        this->name = name;
        this->sub = sub;
    }
    virtual void update() = 0;
};

class StockObserver : public Observer
{
    
    
public:
    StockObserver(string name, Subject *sub) : Observer(name, sub)
    {
    
    
    }
    void update();
};

class NBAObserver : public Observer
{
    
    
public:
    NBAObserver(string name, Subject *sub) : Observer(name, sub)
    {
    
    
    }
    void update();
};
//被观察者 基类 (内部存放了所有的观察者对象,以便状态发生变化时,给观察者发通知)
class Subject
{
    
    
protected:
    list<Observer *> observers;

public:
    string action; //被观察者对象的状态
    virtual void attach(Observer *) = 0;
    virtual void detach(Observer *) = 0;
    virtual void notify() = 0;
};

class Secretary : public Subject
{
    
    
    void attach(Observer *observer)
    {
    
    
        observers.push_back(observer);
    }
    void detach(Observer *observer)
    {
    
    
        list<Observer *>::iterator iter = observers.begin();
        while (iter != observers.end())
        {
    
    
            if ((*iter) == observer)
            {
    
    
                observers.erase(iter);
                return;
            }
            ++iter;
        }
    }
    void notify()
    {
    
    
        list<Observer *>::iterator iter = observers.begin();
        while (iter != observers.end())
        {
    
    
            (*iter)->update();
            ++iter;
        }
    }
};

void StockObserver::update()
{
    
    
    cout << name << " 收到消息:" << sub->action << endl;
    if (sub->action == "梁所长来了!")
    {
    
    
        cout << "我马上关闭股票,装做很认真工作的样子!" << endl;
    }
}

void NBAObserver::update()
{
    
    
    cout << name << " 收到消息:" << sub->action << endl;
    if (sub->action == "梁所长来了!")
    {
    
    
        cout << "我马上关闭NBA,装做很认真工作的样子!" << endl;
    }
}

int main()
{
    
    
    Subject *dwq = new Secretary();
    Observer *xs = new NBAObserver("xiaoshuai", dwq);
    Observer *zy = new NBAObserver("zouyue", dwq);
    Observer *lm = new StockObserver("limin", dwq);

    dwq->attach(xs);
    dwq->attach(zy);
    dwq->attach(lm);

    dwq->action = "去吃饭了!";
    dwq->notify();
    cout << endl;
    dwq->action = "梁所长来了!";
    dwq->notify();
    return 0;
}

Guess you like

Origin blog.csdn.net/luanfenlian0992/article/details/118771472