《More Effective C++》学习笔记(二)

操作符

条款5: 对定制的“类型转换函数”保存警觉

    C++允许编译器在不同类型之间执行隐式转换(implicit conversions),你可以选择是否提供某些函数,供编译器拿来作为隐式转换之用;

    两种函数允许编译器执行隐式转换:

    第一种:单自变量constructors( 构造函数 ),也就是指能够以单一变量成功调用的构造函数,也可能拥有多个参数,并且除了第一个参数外都有默认值

class Name{
public:
    Name(const string& s);    //可以把string 转换为 Name
    ...
};
class Rational{
public:
 Rational(int numerator = 0,    //可以把int转换为Rational,此处为 int numerator = 0, 笔者感觉是不是应该是int numerator,
          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然后执行乘法操作

   当存在这些函数的时候,会在意料之外的时候被调用;

Rational r(1, 2);    //r为 1/2;
cout<< r;            //r会先被转换为double,然后再调用operator<<(因为Rational没有定义operator<<, 所以编译器会执行转换动作匹配操作符)

   解决办法: 以功能对等的另一函数取代类型转换操作符

class Rational{
public:
    ...
    double asDouble() const;    //将Rational转换为double
};

Rational r(1, 2);
cout << r;                      //错误,Rational没有定义<<操作符
cout << r.asDouble();           //正确,以double的形式输出r

    对于单(多)参数构造函数的同样会出现意料之外的情况:

template<class T>
class Array{
public:
   Array(int lowBound, int highBound);        //构造函数,不会导致隐式转换
   Array(int size);                           //会产生隐式转换的构造函数,把int转换为Array
   T& operator[](int index);
   bool operator== (const Array<int>& lhs, const Array<int>& rhs);
   ...
};
Array<int> a(10);
Array<int> b(10);
...
for(int i == 0; i < 10; ++i){
    if(a == b[i])        //原本应该是a[i] == b[i]的,发生错误的时候并没有报错,虽然不存在这样的operator == 函数可被调用,但是 a 的类型为int可以被转换为Array<int>
       ...;
    else
       ...;
}
   所以会产生类似如下的代码
for(int i =0; i < 10; ++i){
    if(a == static_cast<array<int> >(b[i]))    //每一次的迭代都会把b[i]转换为arry<int>然后再调用对应的operator ==来比较,每次都会产生一个array<int>的数组,然后销毁数组对象
    ...
}

    解决方法:

      方法一: 使用关键词explicit ,将构造函数声明为explicit,编译器就不能因隐式类型转换的需要而调用了,不过显式类型转换仍然是被允许的;

template<class T>
class Array{
public:
  explicit Array(int size);
  ...
}
Array<int> a(10);        //没问题,正常调用单参数构造函数
Array<int> b(10);    
if(a == b[i])  ...       //错误,无法进行隐式转换
if(a == array<int>(b[i]))  ...  //没问题,调用了构造函数
if(a == static_cast<array<int> >(b[i]))  ...  //没问题,进行了显式转换
if(a == (Array<int>)b[i] )  ...                //没问题,进行了c风格的显式转换

    方法二:如果编译器不支持关键字explicit,那就把类设计得使希望拥有的“对象构造行为”合法化,并让不希望允许的隐式构造行为非法化(没有任何一个转换的程序(过程)可以内含一个以上的“用户定制的转换行为(也就是当自变量构造函数或隐式类型转换操作符)”);

template<class T>
class Array{
public:
    class ArraySize{        //内部代理类,见条款30
    public:
        ArraySize(int numElements): theSize(numElements){ } //接受一个int作为单参数构造函数
        int size() const{ return theSize; }
    private:
        int theSize;
    };
    Array(int lowBound, int highBound);
    Array(ArraySize size);        //自变量为内部类的构造函数
    ...
};
Array<int> a(10);   //当编译器被要求调用自变量为int的构造函数时,并不存在这样的构造函数,但是,编译器知道能将int自变量转换为一个临时的ArraySize对象,于是执行了转换动作,所以构造函数以及附随的对象构造行为成功调用

    现在隐式转换的行为会被阻止了,因为没有单一自变量为int的构造函数,而且编译器不能将int转换为一个临时的ArraySize对象,然后再把这个临时对象产生必要的Array<int>对象,那将调用两个用户定制转换行为,一个将int转换为ArraySize,一个将ArraySize转换为Array<int>,这种转换是被禁止的;

bool operator ==(const Array<int>& lhs, const Array<int>& rhs);
Array<int> a;
Array<int> b;
...
for(int i = 0; i < 10; ++i)
    if(a == b[i]) ...        //错误,其中的隐式转换涉及到了两个转换动作,这是不被允许的,从结果上来说,隐式转换被阻止了

条款 6:区别 increment/decrement 操作符的前置(prefix)和后置(postfix)形式

    重载函数是通过参数类型来区分彼此的,然而无论是 increment 或 decrement 操作符的前置或是后置形式,都没有参数,为了填平语言学上的漏洞,所以,后置式会有一个int 自变量,并在调用时,编译器默认为int指定一个0值:

class UPInt{
public:
    UPInt& operator++ ();    //前置式(prefix)++
    UPInt& operator-- ();    //前置式(prefix)--
    const UPInt operator++ (int); //后置式(postfix) ++
    const UPInt operator--(int);  //后置式(postfix) --
    UPInt& operator+= (int);     //+=操作符,结合UPInts 和 ints
    ...
};
UPInt i;
++i;  /*调用i.operator++()*/    i++;  //调用i.operator++(0)
--i;  /*调用i.operator--()*/    i--;  //调用i.operator--(0)

    所谓increment操作符的前置式意义是“increment and fetch”(累加然后取出),后置式的意义"fetch and increment"(取出之后再累加);

//前置式
UPInt& UPInt::operator++(){
    *(this) += 1;    //累加(increment)
    return this;     //取出(fetch)
}
//后置式
const UPInt UPInt::operator++(int){    //返回的对象必须是const对象,因为如果是non-cosnt对象,会被允许 UPInt i; i++++; ,这与内建类型ints的行为不一致,而且实际上只是被累加一次而已,与期待的作用不一致
    UPInt oldValue = *this;    //取出(fetch)
    ++(*this);    //累加(increment)
    return oldValue;           //返回旧的值
}
    因为后置式会产生临时对象,所以在处理用户定制类型时, 尽可能的使用前置式increment后置式increment和decrement操作符的实现应以其前置式兄弟为基础实现,如此就只需维护前置式版本,因为后置式版本会自动调整为一致的行为;
const UPInt UPint::operator++(int){
   UPInt oldValue (*this);
   ++(*this);    //以前置式为基础实现后置,保证行为一致
   return oldValue;
}

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

    C++对于“真假表达式”采用所谓的“骤死式”评估方式,就是一旦表达式的真假确定以后,即使还有部分未被检查,整个评估工作就结束了,很多程序都是依赖于“骤死式”的形式进行判断的;

char *p;
if( (p != 0) && (strlen(p) > 10)) ...    //在strlen中p是不会为null指针的,因为如果p为null指针,那么strlen将不会被调用

int rangeCheck(int index){               //如果index 小于lowBound,那么就不会和upperBound比较
    if((index < lowerBound) || (index > upperBound)) ...
}

    假如重载了 && 和 || 运算符,那么“函数调用”语义将会取代“骤死式”语义,就会导致产生重大区别,当函数调用动作被执行时,所有参数值的评估都必须完成,而且,因为语言规范并未明确定义函数调用动作中各个参数的评估顺序,所以没办法知道expression1和expression2哪个会被先评估(骤死式的是从左到右评估其自变量)

if(expression1 &&  expression2) ...
//会被编译器视为以下两者之一
if(expression1.operator&&(expression2)) ...    //假如 operator&& 是成员函数
if(operator&&(expression1, expression2)) ...   //假如 opertotr&& 是全局函数

    逗号运算符用来构成表达式,表达式先评估逗号左侧的值,然后再评估逗号右侧的值,整个表达式的值以逗号右侧的值为代表,如果把操作符写出一个非成员函数,将无法保证左侧表达式一定比右侧表达式更早被评估,即使,写出成员函数也是无法保证这一事实,所以,重载后的逗号表达式和默认版本的行为不一致,不推荐重载逗号运算符;

//将字符串s的字符顺序颠倒
void reverse(char s[]){
    for(int i = 0, j = strlen(s) - 1; i < j; ++i, --j){ //先初始化i,然后初始化j, 先++i再--j
        int c = s[i];
        s[i] = s[j];
        s[j] = c;
    }
}

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

    new有 new operator 和 operator new,new operator就是语言内建的操作符,像sizeof一样不能改变意义,动作分为两部分,第一,分配足够的内存,用来放置某类型的对象,第二,调用构造函数,在刚刚分配的内存中对那个对象设置初值;new operator 调用某个函数,执行必要的内存分配动作,我们可以重写或重载那个函数,改变其行为,这个函数的名称就叫operator new;

    用new operator获得一个对象:

string *ps = new string("Memory Management");

    函数operator new通常声明为:

void* operator new(size_t size);    //返回类型为void*, 指向一块原始,未设初值的内存;
    如果希望定制自己的operator new,可以对operator new进行重载,加上额外的产生;
void* operator(size_t size, int, ...);    //第一参数必须为size_t(保存一致性)
   和maclloc一样,operator new 的唯一任务是分配内存,取得operator new返回的内存并将之转换为一个对象,是new operator的责任,所以会产生类似如下的代码:
void *memory = operator mew (sizeof(string));
call string::string("Memory Management") on *memory;        //程序员没办法这么做,这是编译器的工作
string *ps = static_cast<string>(memory);

   Placement new

      特殊版本的operator new,称为placement new,可以允许你在分配好的原始内存上构建对象;

class Widget{
public:
    Widget(int widgetSize);
    ...
};
Wiget* constructWidgetInBuffer(void *buffer, int widgetSize){
    return new (buffer) Widget(widgetSize);
}
      constructWidgetInBuffer函数乍一看下有些奇怪,这是new operator 的用法之一,其中指定一个额外自变量(buffer)作为new operator“隐式调用 operator new”时所用,于是,被调用的operator new 除了接受“一定得有的size_t自变量“以外,还接受了void* 参数,指向一块准备用来接收构造好的对象的原始内存,这样的operator new就是所谓的placement new;
void* operator new(size_t, void* location){    //size_t因为不使用所以不赋予名称
    return location;    //operator new的目的是为对象找到一块内存,因为调用者知道对象应该放在哪里,所以placement new唯一需要做的是将它获得的指针返回
}//placement new是C++标准库的一部分,欲使用plcaement new,必须先#include<new>

    总而言之,如果希望对象产生于heap(堆内存),使用new operator,不但分配内存还调用构造函数,如果只是单纯分配内存,可以使用operator new,如果打算在heap object(堆对象)产生时决定内存分配方式,可以自己写一个operator new,并使用operator new,如果打算在已分配的内存中构造对象,可以使用placement new;

删除(Deletion)和内存释放(Deallocation)

    为了避免资源泄漏,每一个动态分配的对象必须匹配一个相应但相反的释放动作,函数operaotr delete对于内建的operator delete,就类似于operator new 对应于 new operator:

string *ps;
delete ps;        //使用delete operaotr
//会产生类似以外的代码
ps->~string();       //调用析构函数
operator delete(ps);//释放分配的内存
//operator delete 一般声明如下
void operator delete(void *memoryToBeDeallocated);

    如果只打算处理原始内存,未设初值的内存,应该使用operator new取得内存并以operator delete 归还系统,在C++中相当于调用了malloc/free;

void *buffer = operator new(50 * sizeof(char));    //分配内存
...
operator delete(buffer);                           //释放内存

    如果使用了placement new 在某内存块中产生对象,应该避免对那块内存使用delete operaotr,因为delete operator 会调用operator delete来释放内存,但是该内存内含的对象并非是由operator new分配得来的,所以指针从何而来不得而知,毕竟placement new只是返回他接受的指针而已;

void* mallocShared(size_t size);        //分配shared memory 中的内存函数
void freeShared(void* memory);          //释放shared memory 内存函数
void *sharedMemory = mallocShared(sizeof(Widget));    
Widget *pw = constructWidgetInBuffer(sharedMemory, 10); //placement new
...
delete pw;            //无定义,因为sharedMemory来自mallocShared,不是opertor new
pw->~Widget();        //可以,析构pw指向的Widget对象,但是并未释放Widget占用的内存
freeShared(pw);       //可以,释放pw所指向的内存,不调用析构函数

数组(Arrays)

    多个对象(数组)的情况,new 仍然是 new operator ,但是由于诞生的是数组所以new operator 的行为与先前产生单一对象的情况略有不同,内存不再以operator new分配,而是由其“数组版”,名为operator new[] 分配(通常称为“array new”), ”数组版“与“单一对象版本”另外一个不同的是,new operator会针对数组中每一个元素调用一个构造函数,同时,”数组版本“也可以重载而且和“单一版本”有着相同的重载限制

string *ps = new string[10];    //调用operator new[] 来分配足够的内存然后对每个元素调用string的默认构造函数
delete ps;                      //为数组每个元素调用析构函数,然后调用operator delete[] 以释放内存
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值