C++ Knowledge series Conversion & Constructor & Destructor

时间:2022-11-26 20:37:43

Everything has its lifecycle, from being created to disappearing.

Pass by reference instead of pass by value

  1. 尽量用“传引用”pass reference 而不用“传值” pass value
  2. c语言中,什么都是通过传值来实现的,c++继承了这一传统并将它作为默认方式。除非明确指定,函数的形参parameter总是通过“实参argument的拷贝”来初始化的,函数的调用者得到的也是函数返回值的拷贝. Pointer also is copied to pass as parameter. If the pointer of argument is modified, it only means that the temporary pointer just be modified.
  3. “通过值来传递一个对象”的具体含义是由这个对象的类的拷贝构造函数定义的。这使得传值成为一种非常昂贵的操作. Temporary object created by copy constructor.
  4. 通过引用来传递参数还有另外一个优点advantage/merit/virtue:它避免了所谓的“切割问题(slicing problem)”。当一个派生类的对象作为基类对象被传递时,它(派生类对象)的作为派生类所具有的行为特性会被“切割”掉,从而变成了一个简单的基类对象。这往往不是你所想要的.
  5. 引用几乎都是通过指针来实现的,所以通过引用传递对象实际上是传递指针。因此,如果是一个很小的对象——例如int——传值实际上会比传引用更高效efficient。
  6. 必须返回一个对象时不要试图attempt返回一个引用return reference to local variable.
  7. 据说爱因斯坦曾提出过这样的建议:尽可能地让事情简单,但不要过于简单。在c++语言中相似的说法应该是:尽可能地使程序高效,但不要过于高效。

Conversion

  1. Default promotion conversion (for primitive type) Conversion operator  Constructor conversion  Derived to Base conversion  non-const to const  static_cast conversion.
  2. Widening conversion / narrowing conversion / boxing and unboxing conversion / reference type conversion / exception conversion / method’s argument conversion
  3. 另一种必须使用重载函数的情况是:想完成一项特殊的任务,但算法取决于给定的输入值。这种情况对于构造函数很常见:“缺省”构造函数是凭空(没有输入)构造一个对象,而拷贝构造函数是根据一个已存在的对象构造一个对象. The same function name with different parameter.
  4. 这个方法——在重载函数overloaded function中调用一个“为重载函数完成某些功能”的公共的底层函数——很值得牢记,因为它经常有用.
  5. 避免对指针和数字类型重载.
  6. 成员函数模板(往往简称为成员模板)。顾名思义just as its name implies,成员函数模板是在类的内部为类生成成员函数的模板。拿上面关于null的讨论来说,我们需要一个“对每一个t类型,运作起来都象static_cast<t*>(0)表达式”的对象。即,使null成为一个“包含一个隐式类型转换运算符”的类的对象,这个类型转换运算符可以适用于每种可能的指针类型。
  7. C++也有一种思想:它认为潜在的二义性ambiguity不是一种错误。
  8. 一种方法是调用类A的构造函数,它以b为参数构造一个新的A的对象。另一种方法是调用类B里自定义的转换运算符,它将b转换成一个A的对象。因为这两个途径都一样可行,编译器拒绝decline/refuse从他们中选择一个。
    • ClassB::operator A() const.  // conversion operator
    • Class::ClassA(const B& b);  // copy constructor, if use keyword explicit to prevent from converting Implicitly
  9. 另一种类似的similar/analogous二义的形式源于C++语言的标准转换standard conversion——甚至没有涉及到类
  10. long 是该转换成int还是char呢?两种转换都可行,所以编译器干脆不去做结论。幸运的是fortunately,可以通过显式类型转换type conversion来解决这个问题.static_cast<int> long
  11. 多继承 multiple inheritance充满了潜在二义性的可能。最常发生的一种情况是当一个派生类从多个基类继承了相同的成员名时.
  12. 当类Derived继承两个具有相同名字的函数时,C++没有认为它有错,此时二义只是潜在的potential/underlying。然而,对doIt的调用迫使编译器面对这个现实,除非显式地通过指明函数所需要的基类来消除eliminate二义,函数调用就会出错.
  13. 如果不想使用隐式生成的函数就要显式地禁止它explicitly prohibit.
  14. 方法是声明这个函数(operator=),并使之为private。显式地声明explicitly declare一个成员函数,就防止了编译器去自动生成它synthesize的版本;使函数为private,就防止了别人去调用它. 这个方法还不是很安全,成员函数和友元函数还是可以调用私有函数private function,除非unless/except——如果你够聪明的话——不去定义(实现)这个函数 no implementation of function。这样,当无意间调用了这个函数时,程序在链接时就会报错. 实际应用中,你会发现赋值assignment function和拷贝构造函数copy function具有行为上的相似性comparability,这意味着几乎任何时候当你想禁止forbid/prohibit它们其中的一个时,就也要禁止另外一个。
  15. 另一个比较好的方法是使用c++ namespace。namespace本质上essentially/substantially和使用前缀prefix(postfix)的方法一样,只不过避免了别人总是看到前缀而已。 Namespace Dragon {  ………}
  16. 用户于是可以通过三种方法来访问这一名字空间里的符号:将名字空间中的所有符号全部引入到某一用户空间;将部分符号引入到某一用户空间;或通过修饰符显式地一次性使用某个符号. Using  namespace Dragon;  using Dragon::something; using-declaration/qualified name
  17. 名字空间带来的最大的好处之一在于:潜在的二义ambiguity不会造成错误。所以,从多个不同的名字空间引入同一个符号名不会造成冲突conflict(假如确实真的从不使用这个符号的话)。
  18. 对于类型名,可以用类型定义(typedef)来显式地去掉空间引用。例如,假设结构s(模拟的名字空间)内有个类型名t,可以这样用typedef来使得t成为s::t的同义词:typedef sdm::handle  handle. 对于结构中的每个(静态)对象x,可以提供一个(全局)引用x,并初始化为s::x:const double& book_version = sdm::book_version. When using template class, it is best to do so in order to modify easily the name of type.
  19. 处理函数的方法和处理对象一样,但要注意,即使定义函数的引用是合法的,但代码的维护maintain/maintenance者会更喜欢你使用函数指针。
    • STD::Dragon & ( * const getDragon)( ) = STD::getDragon; //const pointer to function
    • STD::Dragon & ( &getDragon)( ) = STD::getDragon;  //const reference of function

Constructor and Destructor

  1. 几乎所有的类都有一个或多个构造函数constructor,一个析构函数destructor和一个赋值操作符assignment operator。这没什么奇怪的,因为它们提供的都是一些最基本的功能。构造函数控制对象生成时的基本操作basic,并保证对象被初始化initialize;析构函数摧毁一个对象并保证它被彻底清除clear up;赋值操作符则给对象一个新的值。在这些函数上出错就会给整个类带来无尽的负面影响,所以一定要保证其正确性correctness。本章我将指导如何用这些函数来搭建一个结构良好的类的主干.
  2. 解决这类指针混乱问题的方案solution在于,只要类里有指针时,就要写自己版本的拷贝构造函数和赋值操作符函数。在这些函数里,你可以拷贝那些被指向的数据结构,从而使每个对象都有自己的拷贝;或者你可以采用某种引用计数机制 mechanism of reference count(见条款 m29)去跟踪track当前有多少个对象指向某个数据结构。引用计数的方法更复杂,而且它要求构造函数和析构函数内部做更多的工作,但在某些(虽然不是所有)程序里,它会大量节省内存并切实提高速度. Auto_ptr / smart_ptr
  3. 可以只声明这些函数copy constructor/assignment operator(声明为private成员)而不去定义(实现)它们。这就防止prevent了会有人去调用它们,也防止prevent了编译器去生成generate它们。
  4. 一定要经常注意,当且仅当if and only if相应的new用了[ ]的时候,delete才要用[ ].
  5. As fact of matter, the new (sometime implemented with placement) and delete operator is static member of class. If you want delete operator can work correctly, you had better to declare the destructor virtual, for delete operator will computer the size of real object pointed to by pointer inside compiler implementation.
  6. Many tricks: private new, private delete, private destructor, private constructor, friend, inheritance with template
  7. If defining new as private, it keep user from building object in heap.
  8. If defining destructor as private, it keep user from building object in stack by providing other method to release resource.
  9. 尽量使用初始化而不要在构造函数里赋值. The cost of temporary object(constructer and then assignment). Const variable and reference variable must be assigned through initial list when constructor is invoking.
  10. 用成员初始化列表还是比在构造函数里赋值要好.
    1. 分配适当的空间
    2. 基类的初始化
    3. 数据成员初始化( 调用copy构造函数), 安插vptr when necessary,
    4. 执行被调用构造函数体内的动作(数据成员的构造函数,默认赋值,调用assignment operator/variable initializer). 对有基类的对象来说,基类的成员初始化和构造函数体的执行发生在派生类的成员初始化和构造函数体的执行之前.
  11. const member of class must be initialized with initialization argument list. For const member can’t be initialized without other object.
  12. static member of class must be initialized in scope of implementation of class.
  13. 特别是const和reference引用数据成员只能用初始化,不能被赋值. 但还是要在构造函数的初始化列表里对引用进行初始化。还可以对名字同时声明const和引用,这样就生成了一个其名字成员在类外可以被修改而在内部是只读only-read的对象.
  14. no using argument list如果没有为name指定初始化参数,string的缺省构造函数会被调用。当在namedptr的构造函数里对name执行赋值时,会对name调用operator=函数。这样总共有两次对string的成员函数的调用:一次是缺省构造函数,另一次是赋值.
  15. 相反,如果用一个成员初始化列表来指定name必须用initname来初始化,name就会通过拷贝构造函数copy constructor以仅一个函数调用的代价被初始化。
  16. 随着类越来越大,越来越复杂complex,它们的构造函数也越来越大big而复杂complex,那么对象创建的代价也越来越高high。养成尽可能使用成员初始化列表的习惯member initialization list,不但可以满足const和引用成员初始化的要求,还可以大大减少低效地初始化数据成员的机会
  17. static类成员永远也不会在类的构造函数初始化。静态成员在程序运行的过程中只被初始化一次,所以每当类的对象创建时都去“初始化”它们没有任何意义。
  18. 初始化列表initialization list中成员列出的顺序和它们在类中声明的顺序相同. Order is same
  19. 类成员是按照它们在类里被声明的顺序进行初始化的,和它们在成员初始化列表中列出的顺序没一点关系。In the order as declared in class
  20. 对一个对象的所有成员来说,它们的析构函数被调用的顺序总是和它们在构造函数里被创建的顺序相反。那么,如果允许上面的情况(即,成员按它们在初始化列表上出现的顺序被初始化)发生,编译器就要为每一个对象跟踪其成员初始化的顺序,以保证它们的析构函数以正确的顺序被调用。这会带来昂贵的开销。所以,为了避免这一开销,同一种类型的所有对象在创建(构造)和摧毁(析构)过程中对成员的处理顺序都是相同的,而不管成员在初始化列表中的顺序如何
  21. 如果你深究一下的话,会发现只是非静态non-static数据成员的初始化遵守以上规则regulation。静态static数据成员的行为有点象全局global和名字空间对象,所以只会被初始化一次。另外,基类数据成员总是在派生类数据成员之前被初始化,所以使用继承时,要把基类的初始化列在成员初始化列表的最前面。(如果使用多继承,基类被初始化的顺序和它们被派生类继承的顺序一致,它们在成员初始化列表中的顺序会被忽略。使用多继承有很多地方要考虑。
  22. Global variables are initialized before main( ) function, which is completed by compiler.
  23. 确定基类有虚析构函数, for dynamic binding. Make sure that the correct destructor is invoked.
  24. 实现虚函数需要对象附带一些额外信息extra/additional information,以使对象在运行时run-time可以确定该调用哪个虚函数。对大多数编译器来说,这个额外信息的具体形式是一个称为vptr(虚函数表指针)的指针。vptr指向的是一个称为vtbl(虚函数表)的函数指针数组。每个有虚函数的类都附带有一个vtbl。当对一个对象的某个虚函数进行请求调用时,实际被调用的函数是根据指向vtbl的vptr在vtbl里找到相应的函数指针来确定的.this->vptr[index]()
  25. 当且仅当if and only if类里包含至少一个虚函数的时候才去声明虚析构函数.virtual destructor
  26. 最后,值得指出的是,在某些类里声明纯虚析构函数pure virtual destructor很方便。纯虚函数将产生抽象类abstract class——不能实例化instantiate的类(即不能创建此类型的对象)。有些时候,你想使一个类成为抽象类,但刚好又没有任何纯虚函数。怎么办?因为抽象类是准备被用做基类的,基类必须要有一个虚析构函数,纯虚函数会产生抽象类,所以方法很简单:在想要成为抽象类的类里声明一个纯虚析构函数 with definition.
  27. 必须提供纯虚析构函数pure virtual function的定义definition, 这个定义是必需的necessary,因为虚析构函数工作的方式是:最底层的underlying/basic/fundamental(most-derived)派生类的析构函数最先被调用,然后各个基类的析构函数被调用。这就是说,即使是抽象类,编译器也要产生对~awov的调用invocation,所以要保证为它提供函数体。如果不这么做,链接器就会检测出来,最后还是得回去把它添上.
  28. The pure virtual function in base class must have a definition for being called when deleting a pointer to derived class. The invocation of destructor on base class is defined in destructor of derived class by compiler. Only the address of destructor of derived class in vtbl when it is virtual. Also, the compiler calculates the size of object pointed to by pointer based on destructor of class as deleting a base type pointer to object of derived type.
  29. 因为析构函数为虚,它的地址必须进入到类的vtbl(见条款m24)。但内联函数inline不是作为独立的函数存在的(这就是“内联”的意思),所以必须用特殊的方法得到它们的地址。条款33对此做了全面的介绍,其基本点是:如果声明虚析构函数为inline,将会避免调用它们时产生的开销,但编译器还是必然necessarily会在什么地方产生一个此函数的拷贝.
  30. 让operator=返回*this的引用, for successive assignment
  31. 一般情况下几乎总要遵循operator=输入和返回的都是类对象的引用的原则principle,然而有时候需要重载operator=使它能够接受不同类型的参数
  32. 在operator=中对所有数据成员赋值. 实际编程中,这意味着写赋值运算符时,必须对对象的每一个数据成员赋值. Check for self-assignment
  33. The member of base class also must be assigned by yourself.只是显式地调用了base::operator=,这个调用和一般情况下的在成员函数中调用另外的成员函数一样,以*this作为它的隐式左值。base::operator=将针对*this的base部分执行它所有该做的工作.
  34. static_cast<base&>(*this) = rhs; static cast to reference of object怪异的代码将*this强制转换为base的引用,然后对其转换结果赋值。这里只是对derived对象的base部分赋值。还要注意的重要一点是,转换的是base对象的引用,而不是base对象本身。如果将*this强制转换为base对象,就要导致调用base的拷贝构造函数,创建出来的新对象(见条款m19)就成为了赋值的目标,而*this保持不变。这不是所想要的结果.
  35. 另一个经常发生的和继承有关的类似similar/analogous问题是在实现派生类的拷贝构造函数时.derived的拷贝构造函数必须保证调用的是base的拷贝构造函数而不是base的缺省构造函数.
  36. 在operator=中检查给自己赋值的情况  Check for self-assignment

New and delete

  1. 对应的new和delete要采用相同的形式. New--delete.   New[ ] -> delete[ ]
throwing (1)
void* operator new (std::size_t size);
nothrow (2)
void* operator new (std::size_t size, const std::nothrow_t& nothrow_value) noexcept;
placement (3)
void* operator new (std::size_t size, void* ptr) noexcept;
throwing (1)
void* operator new[] (std::size_t size);
nothrow (2)
void* operator new[] (std::size_t size, const std::nothrow_t& nothrow_value) noexcept;
placement (3)
void* operator new[] (std::size_t size, void* ptr) noexcept;

ordinary (1)
void operator delete[] (void* ptr) noexcept;
nothrow (2)
void operator delete[] (void* ptr, const std::nothrow_t& nothrow_constant) noexcept;
placement (3)
void operator delete[] (void* ptr, void* voidptr2) noexcept;
ordinary (1)
void operator delete (void* ptr) noexcept;
nothrow (2)
void operator delete (void* ptr, const std::nothrow_t& nothrow_constant) noexcept;
placement (3)
void operator delete (void* ptr, void* voidptr2) noexcept;

 

  1. 程序员可以使用全局域解析操作符来选择调用全局操作符new(). Dragon *ps = ::new Dragon.
  2. 操作符的size_t 参数自动被初始化为Screen 类的大小, 如果在基类定义了new, 派生类则不需要重新定义new, 派生类的大小会自动传给 new operator.
  3. 增加或去掉一个类的操作符delete()并不要求改变用户的代码, delete 表达式在调用全局操作符delete()和类成员操作符delete()时, 其形式相同. 如果Screen 类没有定义自己的操作符delete(),  则上面的delete 表达式仍然有效, 只不过调用了全局操作符delete().
  4. 为一个类类型而定义的delete()操作符, 如果它是被delete 表达式调用的, 则它可以有两个参数而不是一个, 第一个参数仍然必须是void*型, 而第二个参数必须是预定义类型size_t.
  5. void operator delete( void *, size_t );
  6. 操作符new()和delete()都是类的静态static 成员, 它们遵从静态成员函数的一般限制, 这些操作符被自动做成静态成员函数, 而无需程序员显式地把它们声明为静态的, 尤其要记住的是静态成员函数没有this 指针, 因此它们只能访问静态数据成员.
  7. 只要每个声明都有惟一的参数表我们也可以重载类的成员操作符new(), 但是任何一个类操作符new()的第一个参数的类型都必须是size_t 例, void *operator new( size_t, void* );
  8. 我们也可以重载针对数组的定位操作符new[]()和delete[]().
  9. 用new的时候会发生两件事。首先,内存被分配allocate(通过operator new 函数),然后,为被分配的内存调用一个或多个构造函数。用delete的时候,也有两件事发生:首先,为将被释放的内存调用一个或多个析构函数,然后,释放内存(通过operator delete 函数)。对于 delete来说会有这样一个重要的问题:内存中有多少个对象要被删除?答案决定了将有多少个析构函数会被调用.
  10. If you explicitly call new operator with size argument, then the constructor will not be called automatically.
    MyClass * p3 = (MyClass*) ::operator new (sizeof(MyClass));//allocates memory by calling: operator new (sizeof(MyClass)),but does not call MyClass's constructor
  11. 要被删除的指针指向的是单个对象single object呢,还是对象数组array of object?这只有你来告诉delete。如果你在用delete时没用括号bracket,delete就会认为指向的是单个对象,否则,它就会认为指向的是一个数组
  12. 析构函数里对指针成员调用delete, 在每个构造函数里对指针进行初始化。对于一些构造函数,如果没有内存要分配给指针的话,指针要被初始化为0(即空指针), 删除现有的内存,通过赋值操作符分配给指针新的内存, 在析构函数里删除指针.
  13. 删除空指针null pointer是安全的(因为它什么也没做)。所以,在写构造函数,赋值操作符,或其他成员函数时,类的每个指针成员要么指向有效的内存,要么就指向空,那在你的析构函数里你就可以只用简单地delete掉他们,而不用担心他们是不是被new过.
  14. 因为内部范围声明的名称会隐藏hide掉外部范围的相同的名称,所以对于分别在类的内部和全局声明的两个相同名字的函数f来说,类的成员函数会member function隐藏掉hide/conceal全局函数global function, 派生类的成员函数隐藏了基类的成员函数。
  15. 如果写了operator new就要同时写operator delete. 为了效率performance/efficiency。缺省的operator new和operator delete具有非常好的通用性,它的这种灵活性也使得在某些特定的场合下,可以进一步改善improve它的性能。尤其在那些需要动态分配大量的但很小的对象的应用程序里,情况更是如此.
  16. 因为缺省版本的operator new是一种通用型的内存分配器allocator,它必须可以分配任意大小的内存块。同样,operator delete也要可以释放release任意大小的内存块。operator delete想弄清它要释放的内存有多大,就必须知道当初operator new分配的内存有多大。有一种常用的方法可以让operator new来告诉operator delete当初分配的内存大小是多少,就是在它所返回的内存里预先附带一些额外信息addition information,用来指明被分配的内存块的大小
  17. 自定义的内存管理memory management程序可以很好地改善improve/better/程序的性能,而且它们可以封装在象pool这样的类里
  18. 尽量用const和inline而不用#define. 这个条款term/item最好称为:“尽量用编译器compiler而不用预处理器preprocessor”,因为#define经常被认为好象不是语言本身的一部分。
  19. 定义指针常量时会有点不同。因为常量定义一般是放在头文件中head file(许多源文件会包含它),除了指针所指的类型要定义成const外,重要的是指针也经常要定义成const。定义某个类(class)的常量一般也很方便,只有一点点不同。要把常量限制在类中,首先要使它成为类的成员;为了保证常量最多只有一份拷贝copy/,还要把它定义为静态成员static member
  20. 有了const和inline,你对预处理preprocess的需要减少了,但也不能完全没有它。抛弃#include的日子还很远,#ifdef/#ifndef在控制编译的过程中还扮演重要角色。预处理还不能退休,但你一定要计划给它经常放长假.
  21. malloc和free(及其变体variant)会产生问题的原因在于它们太简单:他们不知道构造函数和析构函数. 既然new和delete可以这么有效地与构造函数和析构函数交互interaction,选用它们是显然的apparent/evident.