Skip to main content

Object Creation and Destruction


Object Creation and Destruction

Object Construction

- Constructors can be called directly during the object creation.

- It has no return type
e.g.
class Point {
private:
    int x;
    int y;
public:
    Point() {
        x = 0;
        y = 0;
    }
    Point(int init_x, int init_y) {
        x = init_x;
        y = init_y;
    }
}

int main(void) {
    Point p1;
    Point p2(4,5);
    return EXIT_SUCCESS;
}

- As the constructor is inside of an objet, it has an explicit this parameter, which points at the newly created object

Overloading

- If we don't write any constructor, C++ compiler will provide a default constructor that basically behaves as if we declared like this:
class MyClass {
    public:
        MyClass() {}
};

- If we write any other constructor, C++ compiler does not provide this constructor for you.

- If we have written any constructors, the class is non-POD by virtue of having a constructor. 

- A class type that has a public default constructor - whether explicitly declared or automatically provided - is called a default constructible class.


Dynamic Allocation

- To allocate memory in C++, we can use the new operator:
- It allocates memory for one object of type Bankaccount and calls constructor to initialize the newly created object. 
i.e.
BankAccount * accountPtr = new BankAccount();

- This expression: new, evaluates to a pointer to the newly allocated object and has type BankAccount *.

- We can use new[] to allocate space for an array. 
i.e.
BankAccount * accountArray = new BankAccount[42]();

- But if we create an array of objects, we can only have them constructed with their default constructor. 

- We can also create an array of 3 pointers to point:
i.e.
Point ** point_ptrs = new Point * [3];

Types of Initialization

- There are two statements:
BankAccount * accountPtr = new BankAccount();
BankAccount * accountPtr = new BankAccount;

- The first one uses value initialization. The second uses default initialization. Each of these has different behavior and the specifics depend on whether the type being initialized is POD or non-POD.

- When value initialization is used, 
    1) a class with a default constructor is initialized by its default constructor
    2) a class without any constructor has every field value initialized
    3) Non-class types are zero initialized
    4) Array have their elements value initialized
- When default initialization is used, 
    1) non-POD types are initialized by their default constructor
    2) POD types are left initialized
    3) Arras have their elements default initialized

- The best approach is to just always include a default constructor in the class.

Initializer Lists

- In above section, while constructor for the BankAccount works fine, it makes use of traditional assignment statements, rather than the preferred C++ approach. 

- C++ uses an initializer list.

- An initializer list is a list of initializations written by placing a colon after the close parenthesis of the constructor's parameter list and writing a sequence of initializations before the open curly brace of the function's body.
e.g.
BankAccount::BankAccount() : balance(0) {}
BankAccount::BankAccount(double initBal) : balance(initBal){}

-Each initialization takes the form name(values)
e.g. for constructor:
BankAccount::BankAccount() : accountNum(nextActNum,
                                                    balance(0.0)) {
    nextActNum++;   
}
BankAccount::BankAccount(double b) : accountNum(nextActNum,
                                                                   balance(b)) {
    nextActNum++;   
}

-The reason why we use initialization instead of assignment is that C++ makes a distinction between initialization and assignment.

- One important reason is that C++ ensures all fields have some form of initialization before the open curly brace of the constructor. If we specify it, we get exactly what we want, or it will be default initialized. 

- default initialization for POD types leaves them with unspecified value. So, we'd better use this way to initialize.

- If we use assignment statement, it actually does not specify the initialization behavior. Instead, if we use assignment, it will assign to the already created object, changing its fields. It's slower and may have some noticeable effects. 

-If the class has fields of a reference type, we must initialize them in the initializer list. Or we will receive error message.

- If the class contains a variable which has const modifier, we must initialize it using initialized list. 

- The order in which fields are declared is the order in which they are declared in the class. Not their initializers order. In some cases it is important. 

- Arguments that are not explicitly initialized will be initialized by the default constructor. 

What We Do

We should
- Make classes default constructible by writing default constructor in every class that we write.

- Using initializer list to initialize class's fields instead of initializing them in the constructor's body. 

- In the initializer list, explicitly initialize every field

- Initialize the fields in the order they are declared.

- Use new and new [] instead of malloc.


Object Destruction

- A destructor is named the same as the class it appears in, except with a tilde (~)

- Destructor doesn't have return type, not even void. 

- Destructor cannot be overloaded. A class may only have one destructor and it must take no parameters.

- Destructor is inside the class, therefore it receives an implicit this parameter. 

- It can be public or private.

- Use delete and delete() to free the memory allocated by new and new[].

When are destructors invoked

- A destructor is invoked whenever the "box" for an object is about to be destroyed.

- This destruction can happen either due to dynamic deallocation using delete or delete[], by a local variable going out of scope, or by one object that contains another eing destroyed. 

- "Box" is only actually destroyed after the destructor completes.

- The order of destruction is the opposite of the order of construction.

- For local variables, their "box" is destroyed whenever they go out of scope. Before they are destroyed, their destructors are invoked in the opposite order they are constructed.

- Fields inside of a class are also destroyed as part of the destruction of the enclosing object. After the class's destructor completes, each of the fields is destroyed in the reverse of the order in which they initialized.

- plain old data: no object


Object Copying

- just passing the object file in the function will result in segmentation fault.

- We should not use naive copy to copy object

- C++ distinguishes between two types of copying: copying during initialization (the copy constructor) and copying during assignment (the copy assignment operator)

Copy Constructor

- Copying during initialization occurs when a new object is created as a copy of an old one. 
e.g. 
1. when objects are passed to functions by value
2. when an object is returned from a function by value
3. explicitly when the programmer writes another object of the same type as the initializer for a newly declared object

- Whatever the reason for initializing an object from a copy of another object, the copy constructor is invoked to perform the copying in the fashion the class defines.

- The copy constructor has no return type. 

- It is very specific overloading of the constructor. 
e.g.
Polygon(const Polygon & rhs) : points(new Points[rhs.numPoints]),
                                                    numPoints(rhs.numPoints){
    for (size_t i = 0; i < numPoints; i++) {
        points[i] = rhs.points[i];
    }
}

- If we don't explicitly specify a copy constructor, C++ will automatically provide one. 

- The provided one performs as if it contains an initializer list which initializes every field from the corresponding field in the argument. i.e.
class SomeClass {
    Type1 field1;
    Type2 field2;
    ...
    TypeN fieldN;
}
public :
    SomeClass (const someClass & rhs):field1(rhs.field1),
                                                                field2(rhs.field2),
                                                                ...
                                                                fieldN(rhs.fieldN) {}

-for non-class types, the initialization simply copies the value. For fields of class types, this initialization invokes the corresponding copy constructor.

- With default constructors, a copy constructor may be classified as trivial. (automatically provided by the compiler)

Assignment Operator

- previous: copy during initialization

-copy during assignment changes the value of an object that already exists

-copy by assignment makes use of assignment operator, opeartor=.

- difference between assignment operator and the copy constructor

-difference between assignment operator and copy operator
1. assignment operator returns a value
2. operator begins with this != &rhs
3. assign constructor must clean up the existing object before we assign the value 

- default assignment operator: not contain self-assignment before coping.


Rule of Three

- If you write a destructor, copy constructor. or assignment operator, then you must write all three of them.


Unnamed Temporaries

- We must be careful to treat unnamed temporaries when they are objects.

- a + b * c: b * c is an unnamed temporary with an object type. The creation and destruction of this object is nontrivial.

- The simplest way of creating unnamed temporaries is to declare an object with no name by writing the class name followed by parentheses and the initializers of the object.

- Such a statement will allocate space for an object of type MyClass in the current stack frame but without name. Then, it will pass 42 to its constructor. After constructor, this object will be destroyed immediately. The destructor will be invoked to clean up the object. 

- An unnamed temporary object is deallocated at the end of the full expression.

-x = MyClass(42) + MyClass(17) + MyClass(3);
It create five unnamed temporaries, these three and the result of the multiplication, and the result of addition. Order are not guaranteed. 

Parameter Passing

- Unnamed temporaries can be useful in passing parameters to functions. e.g.
someFunction(MyClass(42));

- Most implementations will have the called function destroy everything except the parameters, and the caller destroy the parameters. 

- It is generally preferable to have functions take a const reference rather than a copy of an object.e.g.
int someFunction(const MyClass & something) {...};
In above statement, no copying is involved in any case.  The compiler will create a "box" for the unnamed temporary and pass the address of that box as the pointer that is the reference.

-If the reference is non-const, then we would not be able to pass an unnamed temporary, as it is not an lvalue.

Return Values 

- The creation of unnamed temporary can happen in two ways.

- Function creates an object, and that object is explicitly copied to initialize the unnamed temporary. It will use copy constructor.

- C++ allows the compiler to elide the copy, even if the copy constructor has noticeable effects. And in this way the result is directly initialized from within the function. This optimization is called the return value optimization. 

- When executing code by hand, we can choose whether to assume the compiler performs return value optimization or not. For most code, the optimization does not change the correctness or behavior of the code.

Implicit Conversions

- We can pass an unnamed temporary to someFunction, .e.g.
someFunction(MyClass(42));

-But we even can pass like this:
someFunction(42);

- In C++, any constructor that takes one argument is considered as a way to implicitly convert from its argument type to the type of the class it resides in, unless the constructor is declared explicit.

- The compiler is only allowed to apply this rule once per initialization. If class A has a constructor that takes a B, and B has a constructor that takes a C, then we may not pass a C where an A is required and expect the compiler to make a temporary B from which it then can then make a temporary A.

- We should declare all of our single-argument constructors except our copy constructors as explicit, such as:
explicit MyClass(int x) : someField(x) {...}

- This can help the compiler to detect our mistakes instead of translating it into a legal type

- IMPORTANT: All single-argumennt constructors should be declared as explicit, except for the copy constructor. And copy constructor should never be declared as explicit. It cannot be used for an implicit conversion. 

summary& understanding  after reading <<All of Programming>> Chapter 15

Comments

Popular posts from this blog

Templates

  Template - Polymorphism is the ability of the same code to operate on different types. This ability to operate on multiple types reduces code duplication by allowing the same piece of code to be reused across the different types it can operate on. - Polymorphism comes in a variety of forms. What we are interested in at the moment is parametric polymorphism, meaning that we can write our code so that it is parameterized over what type it operates on.  -That is, we want to declare a type parameter T and replace int with T in the above code. -Then, when we want to call the function, we can specify the type for T and get the function we desire. C++ provides parametric polymorphism through templates. Templated Functions - We can write a templated function by using the keyword template followed by the template parameters in angle brackets (<>). - Unlike function parameters, template parameters may be types, which are specified with typename where the type of the parameter wo...

useMemo的使用场景

 useMemo是用来缓存 计算属性 的。 计算属性是函数的返回值,或者说那些以返回一个值为目标的函数。 有些函数会需要我们手动去点击,有些函数是直接在渲染的时候就执行,在DOM区域被当作属性值一样去使用。后者被称为计算属性。 e.g. const Component = () => { const [ params1 , setParams1 ] = useState ( 0 ); const [ params2 , setParams2 ] = useState ( 0 ); //这种是需要我们手动去调用的函数 const handleFun1 = () => { console . log ( "我需要手动调用,你不点击我不执行" ); setParams1 (( val ) => val + 1 ); }; //这种被称为计算属性,不需要手动调用,在渲染阶段就会执行的。 const computedFun2 = () => { console . log ( "我又执行计算了" ); return params2 ; }; return ( < div onClick = { handleFun1 } > //每次重新渲染的时候我就会执行 computed: { computedFun2 () } </ div > ); }; 上面的代码中,在每次点击div的时候,因为setParams1的缘故,导致params1改变,整个组件都需要重新渲染,也导致comptedFunc2()也需要重新计算。如果computedFunc2的计算量很大,这时候重新计算会比较浪费。 可以使用useMemo: const Com = () => { const [ params1 , setParams1 ] = useState ( 0 ); const [ params2 , setParams2 ] = useState ( 0 ); //这种是需要我们手动去调用的函数 const handleFun1 ...

Valgrind

  Using Valgrind to check memory How to use Valgrind -To valgrind the program, run the valgrind command and give it our program name as an argument. -For example, if we want to run ./myProgram hello 42, we can simply run Valgrind ./myProgram hello 42.  Uninitialized Values -When we run the program, we may use uninitialized values. It needs to be fixed. Valgrind can tell us about the use of uninitialized values. But it only tell when the control flow of the program depends on the unitialized value. For example, uninitialized value appears in the conditional expression of an if, or a loop, or in the switch statement. -If we want to know where the uninitialized value is from, we can use Valgrind    --track-origins=yes ./myProgram -Using -fsanitize=address can find a lot of problems that Memcheck cannot.  -We can use Valgrind with GDB to debug. We can run: --vgdb=full --vgdb-error=0., then Valgrind will stop on the first error that it encounters and give control to ...