More Effective C++学习笔记(2)

条款5:对定制的"类型转换函数"保持警觉

  • 有两种函数可让编译器执行隐式转换,分别是单参数构造函数(single-argument constructors)和隐式类型转换运算符。这种两种情况下根本问题是当你在不需要使用转换函数时,这些的函数缺却会被调用运行。结果,这些不正确的程序会做出一些令人恼火的事情,而你又很难判断出原因。
  • 单参数构造函数是指只用一个参数即可以调用的构造函数(很多类都会有单参数的构造函数)。该函数可以是只定义了一个参数,也可以是虽定义了多个参数但第一个参数以后的所有参数都有缺省值。
class Name {                                 // for names of things
public:
  Name(const string& s);                     // 转换 string 到
                                             // Name
  ...  
}; 
class Rational {                             // 有理数类
public:
  Rational(int numerator = 0,                // 转换int到
           int denominator = 1);             // 有理数类
  ... 
};
  • 隐式类型转换运算符只是一个样子奇怪的成员函数:operator 关键字,其后跟一个类型符号。你不用定义函数的返回类型,因为返回类型就是这个函数的名字。
class Rational {
public:
  ...
  operator double() const;                   // 转换Rational类成
};                                           // double类型
//在下面这种情况下,这个函数会被自动调用:
Rational r(1, 2);                            // r 的值是1/2
 
double d = 0.5 * r;                          // 转换 r 到double,
                                         // 然后做乘法
  • 避免使用隐式类型转换运算符的方法:提供功能等同的函数,显示调用
class Rational {
public:
  ...
  double asDouble() const;                   //转变 Rational
};                                       // 成double
//这个成员函数能被显式调用:
Rational r(1, 2);
 
cout << r;                             // 错误! Rationa对象没有
                                     // operator<< 
cout << r.asDouble();                   // 正确, 用double类型 
                                    //打印r
  • 避免单参数构造函数被编译器用于隐式转换的方法1构造函数用explicit声明,如果这样做,编译器会拒绝为了隐式类型转换而调用构造函数。显式类型转换依然合法。下例中static_cast< Array<int> >(b[i])两个相邻的< <符合之间一定要加空格,C++把"<<"当作一个符合解释。
 template<class T>
class Array {
public:
  ...
  explicit Array(int size);           // 注意使用"explicit"
  ...
}; 
Array<int> a(10);                 // 正确, explicit 构造函数
                               // 在建立对象时能正常使用
Array<int> b(10);                // 也正确 
if (a == b[i]) ...                   // 错误! 没有办法
                               // 隐式转换
                               // int 到 Array<int> 
if (a == Array<int>(b[i])) ...        // 正确,显式从int到
                               // Array<int>转换
                               // (但是代码的逻辑
                               // 不合理) 
if (a == static_cast< Array<int> >(b[i])) ...
                               // 同样正确,同样
                               // 不合理 
  • 避免单参数构造函数被编译器用于隐式转换的方法2:根据规则“没有任何一个转换程序可以内含一个以上的用户定制转换行为”,因此可以在有可能被隐式转换的类型上套上一层,构建一个新class辅助使用。如下例,先要建立一个新类ArraySize。这个对象只有一个目的就是表示将要建立数组的大小,代替int直接表示,将int"套一层"。你必须修改Array的单参数构造函数,用一个ArraySize对象来代替int。
class Array {
public: 
  class ArraySize {                    // 这个类是新的
  public:
    ArraySize(int numElements): theSize(numElements) {}
    int size() const { return theSize; } 
  private:
    int theSize;
  }; 
Array(int lowBound, int highBound);
  Array(ArraySize size);                  //**注意新的声明*** 
... 
};
//此时如果int要被隐式转换为Array,则需要经历两次转换,int到ArraySize,ArraySize到Array,与规则相悖

//编译器意识到它能从int参数转换成一个临时ArraySize对象,
//ArraySize对象只是Array<int>构造函数所需要的,这样
//编译器进行了转换。函数调用(及其后的对象建立)也就成功了。
Array<int> a(10); //成功
  • 除非你确实需要,否则不要定义类型转换函数。

条款6:自增(increment)、自减(decrement)操作符前缀形式与后缀形式的区别

  • 为了使得++ --前置和后置重载进行区分,规定后缀形式有一个int类型参数,当函数被调用时,编译器传递一个0做为int参数的值给该函数。
class UPInt {                            // "unlimited precision int"
public:
  UPInt& operator++();                   // ++ 前缀
  const UPInt operator++(int);              // ++ 后缀 
  UPInt& operator--();                    // -- 前缀
  const UPInt operator--(int);              // -- 后缀 
  UPInt& operator+=(int);                // += 操作符,UPInts
                                      // 与ints 相运算
  ...
};
UPInt i;
++i;                       // 调用 i.operator++();
i++;                       // 调用 i.operator++(0);
--i;                       // 调用 i.operator--();
i--;                       // 调用 i.operator--(0);
  • 熟悉前置后置的重载函数定义,前置形式有时叫做“先增加然后取回”,后置形式叫做“先取回然后增加”,因此前置式返回值为引用,后置式返回值为const临时对象(返回临时对象是因为返回值是先前被取得值,而不是加1过后的值;const属性是为了避免"i++++"被合法化)。
// 前缀形式:增加然后取回值
UPInt& UPInt::operator++()
{
  *this += 1;                             // 增加
  return *this;                           // 取回值
}
// postfix form: fetch and increment
const UPInt UPInt::operator++(int)
{
  UPInt oldValue = *this;                 // 取回值
  ++(*this);        // 增加,重点!!以前置操作为基础
return oldValue;           // 返回先前被取回的值
} 
  • 由于前置式返回值为引用,后置式返回值为const临时对象(需要额外构造+析构),所以前置式的效率要高于后置式
  • 为了保证前置和后置行为一致,后置操作以前置操作实现为基础而实现的,见上述两者函数定义中的代码。

条款7:千万不要重载&&,||和,操作符

  • C++对于“真假值表达式”采用“骤死式”评估方法:一旦该表达式的真假值(0或1)确定,及时表达式中还有部分尚未检验,整个评估工作仍宣告结束,效率会提高,评估顺序是从左到右。如下例,这里不用担心当p为空时strlen无法正确运行,因为如果p为空,(p != 0) 为假,整个表达式已经可以确定为假,则strlen不会被调用。
 char *p;
...
if ((p != 0) && (strlen(p) > 10)) ...
  • 如果重载operator&& 和operator||,那么“函数调用”语义将会替代原本的“骤死式”语义。“函数调用”语义将会导致两个参数值均被计算,并且无法控制操作符两边的值谁会被先评估
if (expression1 && expression2) ...
	对于编译器来说,等同于下面代码之一:
if (expression1.operator&&(expression2)) ...
                              // when operator&& is a
                              // member function
if (operator&&(expression1, expression2)) ...
                              // when operator&& is a
                              // global function
  • 一个包含逗号的表达式首先计算逗号左边的表达式,然后计算逗号右边的表达式;整个表达式的结果是逗号右边表达式的值。下例中,编译器首先计算++i,然后是—j,逗号表达式的结果是–j。
 for (int i = 0, j = strlen(s)-1; i < j; ++i, --j) 
  • 不能重载和可以重载的操作符如下
 .              .*              ::             ?:          &&        ||new          delete        sizeof      typeid
static_cast  dynamic_cast  const_cast  reinterpret_cast
你能重载:
operator new        operator delete
operator   new[]    operator delete[]
 +    -   *   /   %   ^     &   |     ~
!    =   <   >  +=   -=   *=   /=   %=
^=  &=  |=  <<  >>   >>=  <<=  ==   !=
<=  >=  &&  ||  ++   --    ,   ->*  ->
()  []
(有关new和delete还有operator new, operator delete, operator new[], and operator delete[]的信息参见条款M8)

条款8:了解各种不同意义的new和delete

  • new operator表示new操作符,它有两个步骤:(1)调用operator new(new操作)分配内存;(2)调用构造函数,初始化内存。不能重载
 string *ps = new string("Memory Management");  //new operator
上述操作等价于如下操作:
void *memory =                              // 得到未经处理的内存
  operator new(sizeof(string));             // 为String对象
call string::string("Memory Management")    //调用构造函数初始化,
on *memory;                                 // 内存中的对象,只有编译器能调用
string *ps =                                // 是ps指针指向
  static_cast<string*>(memory);             // 新的对象
  • delete operator表示delete操作符,它有两个步骤:(1)调用析构函数;(2)调用operator delete(delete操作)释放内存。不能重载。
delete ps;
   导致编译器生成类似于这样的代码:
ps->~string();                      // call the object's dtor
operator delete(ps);                // deallocate the memory
                                   // the object occupied
  • operator new和operator delete只负责分配内存和释放内存,不会调用构造和析构函数。可以重载。
void *rawMemory = operator new(sizeof(string));
	操作符operator new将返回一个指针,指向一块足够容纳一个string类型对象的内存。
void *buffer =                      // 分配足够的
  operator new(50*sizeof(char));      // 内存以容纳50个char
                                 //没有调用构造函数
...
operator delete(buffer);              // 释放内存
                                 // 没有调用析构函数

  • placement new是operator new的一种,可以重载用来在一些已经分配好内存上构建对象(让operator new不分配内存,直接返回指向内存的地址,对应上述new operator转换代码好理解)。
class Widget {
public:
  Widget(int widgetSize);
  ...
};
Widget * constructWidgetInBuffer(void *buffer,
                                 int widgetSize)
{
  return new (buffer) Widget(widgetSize);//调用new operator
 //但是new operator中的隐式调用operator new被重载了,不分配内存了
}

void * operator new(size_t, void *location)
{
  return location;
}
  • 如果你用placement new在内存中建立对象,你应该避免在该内存中用delete operator。因为delete operator调用operator delete来释放内存,但是包含对象的内存最初不是被operator new分配的,placement new只是返回转递给它的指针。如果要删除placement new创建的对象并释放内存,应该先用指针调用析构函数,之后再调用仅释放内存的函数
// 在共享内存中分配和释放内存的函数
void * mallocShared(size_t size);
void freeShared(void *memory);
void *sharedMemory = mallocShared(sizeof(Widget));
Widget *pw =                                   // 如上所示,
  constructWidgetInBuffer(sharedMemory, 10);   // 使用
                                               // placement new 
...
delete pw;            // 结果不确定! 共享内存来自
                      // mallocShared, 而不是operator new
pw->~Widget();        // 正确。 析构 pw指向的Widget,
                      // 但是没有释放
                      //包含Widget的内存
freeShared(pw);       // 正确。 释放pw指向的共享内存
                      // 但是没有调用析构函数
  • 当用new创建一个数组时,内存不再用operator new分配,代替以等同的数组分配函数,叫做operator new [],相对应的是operator delete []。operator new []和operator new均可以被重载,同理operator delete []也如此。
  • 数组版的new operator 必须针对数组中每个对象调用一个构造函数,数组版的delete operator为每个数组元素调用析构函数,然后调用operator delete来释放内存
string *ps =               // 调用operator new[]为10个
  new string[10];          // string对象分配内存,然后对每个数组元素调用
                           // string对象的缺省构造函数。
delete [] ps;               //为每个数组元素调用析构函数,
							//然后调用operator delete来释放内存         
  • new和delete operator是内置的,其行为不受你的控制,但是它们调用的内存分配/释放函数则可以重载控制
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值