zoukankan      html  css  js  c++  java
  • C++之动态内存与类型转换

    Dynamic memory

    In the programs seen in previous chapters, all memory needs were determined before program execution by defining the variables needed. But there may be cases where the memory needs of a program can only be determined during runtime. For example, when the memory needed depends on user input. On these cases, programs need to dynamically allocate memory, for which the C++ language integrates the operators new and delete.

    Operators new and new[]

    Dynamic memory is allocated using operator new. new is followed by a data type specifier and, if a sequence of more than one element is required, the number of these within brackets []. It returns a pointer to the beginning of the new block of memory allocated. Its syntax is:

    pointer = new type
    pointer = new type [number_of_elements]
    

    The first expression is used to allocate memory to contain one single element of type type. The second one is used to allocate a block (an array) of elements of type type, where number_of_elements is an integer value representing the amount of these. For example:

    int * foo;
    foo = new int [5];
    

    In this case, the system dynamically allocates space for five elements of type int and returns a pointer to the first element of the sequence, which is assigned to foo (a pointer). Therefore, foo now points to a valid block of memory with space for five elements of type int.

    Here, foo is a pointer, and thus, the first element pointed to by foo can be accessed either with the expression foo[0] or the expression *foo (both are equivalent). The second element can be accessed either with foo[1] or *(foo+1), and so on...

    There is a substantial difference between declaring a normal array and allocating dynamic memory for a block of memory using new. The most important difference is that the size of a regular array needs to be a constant expression, and thus its size has to be determined at the moment of designing the program, before it is run, whereas the dynamic memory allocation performed by new allows to assign memory during runtime using any variable value as size.

    The dynamic memory requested by our program is allocated by the system from the memory heap. However, computer memory is a limited resource, and it can be exhausted. Therefore, there are no guarantees that all requests to allocate memory using operator new are going to be granted by the system.

    C++ provides two standard mechanisms to check if the allocation was successful:

    One is by handling exceptions. Using this method, an exception of type bad_alloc is thrown when the allocation fails. Exceptions are a powerful C++ feature explained later in these tutorials. But for now, you should know that if this exception is thrown and it is not handled by a specific handler, the program execution is terminated.

    This exception method is the method used by default by new, and is the one used in a declaration like:

    foo = new int [5];  // if allocation fails, an exception is thrown  
    

    The other method is known as nothrow, and what happens when it is used is that when a memory allocation fails, instead of throwing a bad_alloc exception or terminating the program, the pointer returned by new is a null pointer, and the program continues its execution normally.

    This method can be specified by using a special object called nothrow, declared in header <new>, as argument for new:

    foo = new (nothrow) int [5]; 
    

    In this case, if the allocation of this block of memory fails, the failure can be detected by checking if foo is a null pointer:

    int * foo;
    foo = new (nothrow) int [5];
    if (foo == nullptr) {
      // error assigning memory. Take measures.
    }
    

    This nothrow method is likely to produce less efficient code than exceptions, since it implies explicitly checking the pointer value returned after each and every allocation. Therefore, the exception mechanism is generally preferred, at least for critical allocations. Still, most of the coming examples will use the nothrow mechanism due to its simplicity.

    Operators delete and delete[]

    In most cases, memory allocated dynamically is only needed during specific periods of time within a program; once it is no longer needed, it can be freed so that the memory becomes available again for other requests of dynamic memory. This is the purpose of operator delete, whose syntax is:

    delete pointer;
    delete[] pointer;
    

    The first statement releases the memory of a single element allocated using new, and the second one releases the memory allocated for arrays of elements using new and a size in brackets ([]).

    The value passed as argument to delete shall be either a pointer to a memory block previously allocated with new, or a null pointer (in the case of a null pointer, delete produces no effect).

    // rememb-o-matic
    #include <iostream>
    #include <new>
    using namespace std;
    
    int main ()
    {
      int i,n;
      int * p;
      cout << "How many numbers would you like to type? ";
      cin >> i;
      p= new (nothrow) int[i];
      if (p == nullptr)
        cout << "Error: memory could not be allocated";
      else
      {
        for (n=0; n<i; n++)
        {
          cout << "Enter number: ";
          cin >> p[n];
        }
        cout << "You have entered: ";
        for (n=0; n<i; n++)
          cout << p[n] << ", ";
        delete[] p;
      }
      return 0;
    }
    //How many numbers would you like to type? 5
    //Enter number : 75
    //Enter number : 436
    //Enter number : 1067
    //Enter number : 8
    //Enter number : 32
    //You have entered: 75, 436, 1067, 8, 32,
    

    Notice how the value within brackets in the new statement is a variable value entered by the user (i), not a constant expression:

    p= new (nothrow) int[i];
    

    There always exists the possibility that the user introduces a value for i so big that the system cannot allocate enough memory for it. For example, when I tried to give a value of 1 billion to the "How many numbers" question, my system could not allocate that much memory for the program, and I got the text message we prepared for this case (Error: memory could not be allocated).

    It is considered good practice for programs to always be able to handle failures to allocate memory, either by checking the pointer value (if nothrow) or by catching the proper exception.

    Dynamic memory in C

    C++ integrates the operators new and delete for allocating dynamic memory. But these were not available in the C language; instead, it used a library solution, with the functions malloc, calloc, realloc and free, defined in the header <cstdlib> (known as <stdlib.h> in C). The functions are also available in C++ and can also be used to allocate and deallocate dynamic memory.

    Note, though, that the memory blocks allocated by these functions are not necessarily compatible with those returned by new, so they should not be mixed; each one should be handled with its own set of functions or operators.


    Type conversions

    Implicit conversion
    Implicit conversions are automatically performed when a value is copied to a compatible type. For example:

    short a=2000;
    int b;
    b=a;
    

    Here, the value of a is promoted from short to int without the need of any explicit operator. This is known as a standard conversion. Standard conversions affect fundamental data types, and allow the conversions between numerical types (short to int, int to float, double to int...), to or from bool, and some pointer conversions.

    Converting to int from some smaller integer type, or to double from float is known as promotion, and is guaranteed to produce the exact same value in the destination type. Other conversions between arithmetic types may not always be able to represent the same value exactly:

    • If a negative integer value is converted to an unsigned type, the resulting value corresponds to its 2's complement bitwise representation (i.e., -1 becomes the largest value representable by the type, -2 the second largest, ...).
    • The conversions from/to bool consider false equivalent to zero (for numeric types) and to null pointer (for pointer types); true is equivalent to all other values and is converted to the equivalent of 1.
    • If the conversion is from a floating-point type to an integer type, the value is truncated (the decimal part is removed). If the result lies outside the range of representable values by the type, the conversion causes undefined behavior.
    • Otherwise, if the conversion is between numeric types of the same kind (integer-to-integer or floating-to-floating), the conversion is valid, but the value is implementation-specific (and may not be portable).

    Some of these conversions may imply a loss of precision, which the compiler can signal with a warning. This warning can be avoided with an explicit conversion.

    For non-fundamental types, arrays and functions implicitly convert to pointers, and pointers in general allow the following conversions:

    • Null pointers can be converted to pointers of any type
    • Pointers to any type can be converted to void pointers.
    • Pointer upcast: pointers to a derived class can be converted to a pointer of an accessible and unambiguous base class, without modifying its const or volatile qualification.

    Implicit conversions with classes

    In the world of classes, implicit conversions can be controlled by means of three member functions:

    • Single-argument constructors: allow implicit conversion from a particular type to initialize an object.
    • Assignment operator: allow implicit conversion from a particular type on assignments.
    • Type-cast operator: allow implicit conversion to a particular type.

    For example:

    // implicit conversion of classes:
    #include <iostream>
    using namespace std;
    
    class A {};
    
    class B {
    public:
      // conversion from A (constructor):
      B (const A& x) {}
      // conversion from A (assignment):
      B& operator= (const A& x) {return *this;}
      // conversion to A (type-cast operator)
      operator A() {return A();}
    };
    
    int main ()
    {
      A foo;
      B bar = foo;    // calls constructor
      bar = foo;      // calls assignment
      foo = bar;      // calls type-cast operator
      return 0;
    }
    

    The type-cast operator uses a particular syntax: it uses the operator keyword followed by the destination type and an empty set of parentheses. Notice that the return type is the destination type and thus is not specified before the operator keyword.

    Keyword explicit

    On a function call, C++ allows one implicit conversion to happen for each argument. This may be somewhat problematic for classes, because it is not always what is intended. For example, if we add the following function to the last example:

    void fn (B arg) {}
    

    This function takes an argument of type B, but it could as well be called with an object of type A as argument:

    fn (foo);
    

    This may or may not be what was intended. But, in any case, it can be prevented by marking the affected constructor with the explicit keyword:

    // explicit:
    #include <iostream>
    using namespace std;
    
    class A {};
    
    class B {
    public:
      explicit B (const A& x) {}
      B& operator= (const A& x) {return *this;}
      operator A() {return A();}
    };
    
    void fn (B x) {}
    
    int main ()
    {
      A foo;
      B bar (foo);
      bar = foo;
      foo = bar;
      
    //  fn (foo);  // not allowed for explicit ctor.
      fn (bar);  
    
      return 0;
    }
    

    Additionally, constructors marked with explicit cannot be called with the assignment-like syntax; In the above example, bar could not have been constructed with:

    B bar = foo;
    

    Type-cast member functions (those described in the previous section) can also be specified as explicit. This prevents implicit conversions in the same way as explicit-specified constructors do for the destination type.

    Type casting

    C++ is a strong-typed language. Many conversions, specially those that imply a different interpretation of the value, require an explicit conversion, known in C++ as type-casting. There exist two main syntaxes for generic type-casting: functional and c-like:

    double x = 10.3;
    int y;
    y = int (x);    // functional notation
    y = (int) x;    // c-like cast notation 
    

    The functionality of these generic forms of type-casting is enough for most needs with fundamental data types. However, these operators can be applied indiscriminately on classes and pointers to classes, which can lead to code that -while being syntactically correct- can cause runtime errors. For example, the following code compiles without errors:

    // class type-casting
    #include <iostream>
    using namespace std;
    
    class Dummy {
        double i,j;
    };
    
    class Addition {
        int x,y;
      public:
        Addition (int a, int b) { x=a; y=b; }
        int result() { return x+y;}
    };
    
    int main () {
      Dummy d;
      Addition * padd;
      padd = (Addition*) &d;
      cout << padd->result();
      return 0;
    }
    

    The program declares a pointer to Addition, but then it assigns to it a reference to an object of another unrelated type using explicit type-casting:

    padd = (Addition*) &d;
    

    Unrestricted explicit type-casting allows to convert any pointer into any other pointer type, independently of the types they point to. The subsequent call to member result will produce either a run-time error or some other unexpected results.

    In order to control these types of conversions between classes, we have four specific casting operators: dynamic_cast, reinterpret_cast, static_cast and const_cast. Their format is to follow the new type enclosed between angle-brackets (<>) and immediately after, the expression to be converted between parentheses.

    dynamic_cast <new_type> (expression)
    reinterpret_cast <new_type> (expression)
    static_cast <new_type> (expression)
    const_cast <new_type> (expression)
    

    The traditional type-casting equivalents to these expressions would be:

    (new_type) expression
    new_type (expression)
    

    but each one with its own special characteristics:

    dynamic_cast

    dynamic_cast can only be used with pointers and references to classes (or with void*). Its purpose is to ensure that the result of the type conversion points to a valid complete object of the destination pointer type.

    This naturally includes pointer upcast (converting from pointer-to-derived to pointer-to-base), in the same way as allowed as an implicit conversion.

    But dynamic_cast can also downcast (convert from pointer-to-base to pointer-to-derived) polymorphic classes (those with virtual members) if -and only if- the pointed object is a valid complete object of the target type. For example:

    // dynamic_cast
    #include <iostream>
    #include <exception>
    using namespace std;
    
    class Base { virtual void dummy() {} };
    class Derived: public Base { int a; };
    
    int main () {
      try {
        Base * pba = new Derived;
        Base * pbb = new Base;
        Derived * pd;
    
        pd = dynamic_cast<Derived*>(pba);
        if (pd==0) cout << "Null pointer on first type-cast.
    ";
    
        pd = dynamic_cast<Derived*>(pbb);
        if (pd==0) cout << "Null pointer on second type-cast.
    ";
    
      } catch (exception& e) {cout << "Exception: " << e.what();}
      return 0;
    }
    //Null pointer on second type-cast.
    
    • Compatibility note: This type of dynamic_cast requires Run-Time Type Information (RTTI) to keep track of dynamic types. Some compilers support this feature as an option which is disabled by default. This needs to be enabled for runtime type checking using dynamic_cast to work properly with these types.

    The code above tries to perform two dynamic casts from pointer objects of type Base* (pba and pbb) to a pointer object of type Derived*, but only the first one is successful. Notice their respective initializations:

    Base * pba = new Derived;
    Base * pbb = new Base;
    

    Even though both are pointers of type Base*, pba actually points to an object of type Derived, while pbb points to an object of type Base. Therefore, when their respective type-casts are performed using dynamic_cast, pba is pointing to a full object of class Derived, whereas pbb is pointing to an object of class Base, which is an incomplete object of class Derived.

    When dynamic_cast cannot cast a pointer because it is not a complete object of the required class -as in the second conversion in the previous example- it returns a null pointer to indicate the failure. If dynamic_cast is used to convert to a reference type and the conversion is not possible, an exception of type bad_cast is thrown instead.

    dynamic_cast can also perform the other implicit casts allowed on pointers: casting null pointers between pointers types (even between unrelated classes), and casting any pointer of any type to a void* pointer.

    static_cast

    static_cast can perform conversions between pointers to related classes, not only upcasts (from pointer-to-derived to pointer-to-base), but also downcasts (from pointer-to-base to pointer-to-derived). No checks are performed during runtime to guarantee that the object being converted is in fact a full object of the destination type. Therefore, it is up to the programmer to ensure that the conversion is safe. On the other side, it does not incur the overhead of the type-safety checks of dynamic_cast.

    class Base {};
    class Derived: public Base {};
    Base * a = new Base;
    Derived * b = static_cast<Derived*>(a);
    

    This would be valid code, although b would point to an incomplete object of the class and could lead to runtime errors if dereferenced.

    Therefore, static_cast is able to perform with pointers to classes not only the conversions allowed implicitly, but also their opposite conversions.

    static_cast is also able to perform all conversions allowed implicitly (not only those with pointers to classes), and is also able to perform the opposite of these. It can:

    • Convert from void* to any pointer type. In this case, it guarantees that if the void* value was obtained by converting from that same pointer type, the resulting pointer value is the same.
    • Convert integers, floating-point values and enum types to enum types.

    Additionally, static_cast can also perform the following:

    • Explicitly call a single-argument constructor or a conversion operator.
    • Convert to rvalue references.
    • Convert enum class values into integers or floating-point values.
    • Convert any type to void, evaluating and discarding the value.

    reinterpret_cast

    reinterpret_cast converts any pointer type to any other pointer type, even of unrelated classes. The operation result is a simple binary copy of the value from one pointer to the other. All pointer conversions are allowed: neither the content pointed nor the pointer type itself is checked.

    It can also cast pointers to or from integer types. The format in which this integer value represents a pointer is platform-specific. The only guarantee is that a pointer cast to an integer type large enough to fully contain it (such as intptr_t), is guaranteed to be able to be cast back to a valid pointer.

    The conversions that can be performed by reinterpret_cast but not by static_cast are low-level operations based on reinterpreting the binary representations of the types, which on most cases results in code which is system-specific, and thus non-portable. For example:

    class A { /* ... */ };
    class B { /* ... */ };
    A * a = new A;
    B * b = reinterpret_cast<B*>(a);
    

    This code compiles, although it does not make much sense, since now b points to an object of a totally unrelated and likely incompatible class. Dereferencing b is unsafe.

    const_cast

    This type of casting manipulates the constness of the object pointed by a pointer, either to be set or to be removed. For example, in order to pass a const pointer to a function that expects a non-const argument:

    // const_cast
    #include <iostream>
    using namespace std;
    
    void print (char * str)
    {
      cout << str << '
    ';
    }
    
    int main () {
      const char * c = "sample text";
      print ( const_cast<char *> (c) );
      return 0;
    }
    //sample text
    

    The example above is guaranteed to work because function print does not write to the pointed object. Note though, that removing the constness of a pointed object to actually write to it causes undefined behavior.

    typeid

    typeid allows to check the type of an expression:

    typeid (expression)
    

    This operator returns a reference to a constant object of type type_info that is defined in the standard header <typeinfo>. A value returned by typeid can be compared with another value returned by typeid using operators == and != or can serve to obtain a null-terminated character sequence representing the data type or class name by using its name() member.

    // typeid
    #include <iostream>
    #include <typeinfo>
    using namespace std;
    
    int main () {
      int * a,b;
      a=0; b=0;
      if (typeid(a) != typeid(b))
      {
        cout << "a and b are of different types:
    ";
        cout << "a is: " << typeid(a).name() << '
    ';
        cout << "b is: " << typeid(b).name() << '
    ';
      }
      return 0;
    }
    //a and b are of different types:
    //a is: int *
    //b is: int  
    

    When typeid is applied to classes, typeid uses the RTTI to keep track of the type of dynamic objects. When typeid is applied to an expression whose type is a polymorphic class, the result is the type of the most derived complete object:

    // typeid, polymorphic class
    #include <iostream>
    #include <typeinfo>
    #include <exception>
    using namespace std;
    
    class Base { virtual void f(){} };
    class Derived : public Base {};
    
    int main () {
      try {
        Base* a = new Base;
        Base* b = new Derived;
        cout << "a is: " << typeid(a).name() << '
    ';
        cout << "b is: " << typeid(b).name() << '
    ';
        cout << "*a is: " << typeid(*a).name() << '
    ';
        cout << "*b is: " << typeid(*b).name() << '
    ';
      } catch (exception& e) { cout << "Exception: " << e.what() << '
    '; }
      return 0;
    }
    //a is: class Base *
    //b is: class Base *
    //*a is: class Base
    //*b is: class Derived
    

    Note: The string returned by member name of type_info depends on the specific implementation of your compiler and library. It is not necessarily a simple string with its typical type name, like in the compiler used to produce this output.

    Notice how the type that typeid considers for pointers is the pointer type itself (both a and b are of type class Base *). However, when typeid is applied to objects (like *a and *b) typeid yields their dynamic type (i.e. the type of their most derived complete object).

    If the type typeid evaluates is a pointer preceded by the dereference operator (*), and this pointer has a null value, typeid throws a bad_typeid exception.

    References

    [1] Dynamic memory
    [2] Type conversions

    • 变更记录
    时间 地点 修改人 备注
    2020-09-23 Foshan PatrickLee First version of this note
  • 相关阅读:
    idea 没有 persistence
    java 枚举(二) 级联关系
    java to edi 动态/静态映射
    edi to java
    C# 扩展方法
    最详细的C++对应C#的数据类型转换
    c# .Net随机生成字符串代码
    遍历结构体内部元素和值(Name and Value)
    寒假学习计划
    python os.path模块
  • 原文地址:https://www.cnblogs.com/leaguecn/p/13721332.html
Copyright © 2011-2022 走看看