首页 > 技术文章 > Bertrandt 理论题

leon2014dresden 2020-11-26 18:58 原文

Smart pointers were introduced in C++11 to assist with memory management and prevent memory leaks. The following questions address the allocation of memory for data used within the program, irrespective of type.

Briefly describe the difference between:

  1. unique_ptr<>
  2. shared_ptr<>
  3. weak_ptr<>

1. A unique pointer (unique_ptr) is a smart pointer that will automatically deallocate the reserved memory as soon as it is out of scope. Only one unique_ptr can point to a resource, so it is illegal (at compile time) to make a copy of a unique_ptr.

2. A shared pointer (shared_ptr) is also a smart pointer that automatically deallocates the reserved memory once it is out of scope. However, a single resource can be simultaneously referenced by many shared pointers. An internal reference counter is used to keep track of the shared_ptr count for any given resource. If there are no references then the memory is freed. This is susceptible to the problem of circular dependency in cases where two or more shared pointers reference each other.

3. A weak pointer (weak_ptr) indeed references the memory, but is not able to use it before being converted into a shared pointer. This is done by using the lock() member function.  Using weak pointers is a common way to deal with the problem of circular dependencies. There is no reference count, but as such, the resource is first verified as soon as lock() is called. If the memory is still available then it is usable as a shared pointer. However, had it been deallocated previously then the lock() would fail.

In addition to the array functionality built into the language, the STL provides for a number of container classes. Two of these are List (std::list) and Vector (std::vector). What are the differences between these two container types, and in what circumstances would you choose one over the other?

The vector and list classes are both sequentially stored containers, but the internal implementation is significantly different. The vector is stored in contiguous memory locations like an array. A list, on the other hand, is stored internally as a doubly linked list. Both of these structures have their own caveats.

Vector

  1. Insertion and deletion is expensive because:
  2. If either occurs in the middle off the list then all of the elements have to be shifted, either to make room or to clear unused space.
  3. If an element is added to the end then it may require re-allocation of memory and the copying of all of the elements.
  4. C++ Iterators can be invalidated.
  5. Random access is both possible and efficient, using the array operator (eg: tasks[12]).
  6. Other STL algorithms that require Random Access Iterators can safely be used with the vector class. An example of this is std::sort(…), which on a vector can be called: std::sort(myVector.begin(), myVector.end());

List 

  1. Insertion or deletion at any point (start, middle, end) only requires changing a few pointers. During insertion, of course, new memory will have to be allocated for the incoming element.
  2. Random access is not possible in a list. In order to access the tasks in the previous example, tasks [12], it requires starting at the beginning of the list and iterating through the first 11 elements one at a time.
  3. Iterators are not invalidated by list insertion or deleting because no element is moved from its position.
  4. Random access iterators are by default invalid for lists. Consequently, custom functions using non-random access iterators need to be implemented. Due to how commonly lists are sorted, the functionality is part of the STL:
  5. std::sort(..) does not work for lists; However, std::list::sort() is part of the STL

What is a class template? Comment on how using a template promotes code reuse.

A class template allows classes to be abstract in the sense that they do not know what datatype will be passed in and handled by its operations. A template does not depend on the characteristics of a particular datatype; rather, the focus is on the logic.

The std:vector template class is an example provided by the STL. It is generic because a vector of any datatype can be instantiated. This ability allows for efficient code reuse, and consequently the entire system is easier to implement and maintain.

How does the compilation process work?

The compilation of a C++ program involves three steps:

  1. Preprocessing: the preprocessor takes a C++ source code file and deals with the #includes, #defines and other preprocessor directives. The output of this step is a "pure" C++ file without pre-processor directives.

  2. Compilation: the compiler takes the pre-processor's output and produces an object file from it.

  3. Linking: the linker takes the object files produced by the compiler and produces either a library or an executable file.

Preprocessing

The preprocessor handles the preprocessor directives, like #include and #define. It is agnostic of the syntax of C++, which is why it must be used with care.

It works on one C++ source file at a time by replacing #include directives with the content of the respective files (which is usually just declarations), doing replacement of macros (#define), and selecting different portions of text depending of #if#ifdef and #ifndef directives.

The preprocessor works on a stream of preprocessing tokens. Macro substitution is defined as replacing tokens with other tokens (the operator ## enables merging two tokens when it makes sense).

After all this, the preprocessor produces a single output that is a stream of tokens resulting from the transformations described above. It also adds some special markers that tell the compiler where each line came from so that it can use those to produce sensible error messages.

Some errors can be produced at this stage with clever use of the #if and #error directives.

Compilation

The compilation step is performed on each output of the preprocessor. The compiler parses the pure C++ source code (now without any preprocessor directives) and converts it into assembly code. Then invokes underlying back-end(assembler in toolchain) that assembles that code into machine code producing actual binary file in some format(ELF, COFF, a.out, ...). This object file contains the compiled code (in binary form) of the symbols defined in the input. Symbols in object files are referred to by name.

Object files can refer to symbols that are not defined. This is the case when you use a declaration, and don't provide a definition for it. The compiler doesn't mind this, and will happily produce the object file as long as the source code is well-formed.

Compilers usually let you stop compilation at this point. This is very useful because with it you can compile each source code file separately. The advantage this provides is that you don't need to recompile everything if you only change a single file.

The produced object files can be put in special archives called static libraries, for easier reusing later on.

It's at this stage that "regular" compiler errors, like syntax errors or failed overload resolution errors, are reported.

Linking

The linker is what produces the final compilation output from the object files the compiler produced. This output can be either a shared (or dynamic) library (and while the name is similar, they haven't got much in common with static libraries mentioned earlier) or an executable.

It links all the object files by replacing the references to undefined symbols with the correct addresses. Each of these symbols can be defined in other object files or in libraries. If they are defined in libraries other than the standard library, you need to tell the linker about them.

At this stage the most common errors are missing definitions or duplicate definitions. The former means that either the definitions don't exist (i.e. they are not written), or that the object files or libraries where they reside were not given to the linker. The latter is obvious: the same symbol was defined in two different object files or libraries.

Give an example of lambda function

int getSum(int a, int b)
{
int bias = 3;
int sum;
auto lf = [&bias](int x, int y) { return x + y; };
 
// Calculate the sum using the lambda function
sum = lf(a, b);
 
return(sum);
}

Get the maximum product of two elements in an array

1. Solution

class Solution
{
public:
    int maxProduct(vector<int>& nums)
    {
        sort(nums.begin(), nums.end(), greater<int>());
        return (nums[0] * nums[1]);
    }
};

2. Solution

class Solution
{
public:
    int maxProduct(vector<int>& nums)
    {
        int max1 = 0, max2 = 0;
        for(int i = 0; i < nums.size(); i++)
        {
            if(nums[i] >= max1)
            {
                max2 = max1;
                max1 = nums[i];
            }
            else if(nums[i] <= max1 && nums[i] > max2)
                max2 = nums[i];
        }
        return (max1 * max2);
    }
};

 

推荐阅读