C++ Primer阅读心得(第四章、第五章)

1.运算符重载时,我们可以重新定义已存在运算符的行为,但是无法修改运算对象的个数、运算符的优先级和结合律。

2.在C++中存在左值(lvalue)和右值(rvalue)的区别:左值是地址,可以出现在赋值语句的左边和右边(当它出现在右边时,实际上使用的是它的值);右值是变量的值,只可以出现在赋值语句的右边。c++11中新增的decltype作用于得到左值的表达式(注意不是变量)时返回的是引用类型,而decltype作用于右值时得到的是普通的类型。

int a, b=10;
a = 5; //a左值,5右值
a = b; //a左值,b左值(转化为右值10)
decltype(a) c; //int型,a是变量
decltype((a)) d; //int &型,(a)是一个表达式,返回a左值
3.除了&&(逻辑与)、||(逻辑或)、? :(问号运算符)和 ,(逗号运算符)之外,c++中其他运算符没有规定运算对象的求值顺序,所以如果一个子表达式修改了另外一个子表达式的变量,那么运算结果无法预计,应当予以避免。
int i = 0;
cout<<i<<" "<<++i<<endl; //i和++i不确定谁先执行,输出"0 1"和"1 1"都有可能
4.c++11中改进了结果为负数的商取整的方式,规定一律向0取整。同时也规定了当%(求模)的两个操作数一正一负时的行为,让它等于两个绝对值求模之后再加上一个负号。
int d = -20.1/3; //d向0取整,等于-6
d = -20%-3; //都是负数,得到-2
d = -20%3; //等价于d=-(20%3),得到-2
5.赋值操作符是右结合的,而算术运算符是左结合的。例如:
a+b+c
就是(a+b)+c
而a=b=c
就是a=(b=c)
6.列表赋值:c++11中允许使用{}括起来的初始值列表作为赋值语句的右侧运算对象。(注意内置类型列表初始化不能损失精度)
int k = {3.14}; //错误,精度损失
vector<int> v;
v = {1,2,3}; //ok
7.具有求值顺序的操作符:

  • &&(逻辑与):短路规则,只有在左侧为true时才对右侧求值;这个运算符的运算对象和返回值都是右值。
  • ||(逻辑或):短路规则,只有在左侧为false时才对右侧求值;这个运算符的运算对象和返回值都是右值。
  • ?:(条件运算符):先求条件表达式,再根据结果对后面的两个表达式之一进行求值;根据 : 两侧的表达式来决定返回值类型:都是左值,返回左值,否则返回右值。
  • , (逗号运算符):先求左边再求右边;它返回右侧表达式的计算结果(右侧返回左值则返回左值,反之返回右值)。

8.对于内置类型来说,前置递增++递减--,与后置递增++递减--,在只执行变量自增1或者自减1的运行效率上是一致的。但要注意在对复杂迭代器的处理上,后置++或者--效率要比前置版本低很多。所以,如非必要,在使用迭代器时,推荐使用前置版本。

9.在bitmap或者simhash中,我们要用到位操作,下面列一下常用位操作:

  • 某位置1:
    result |= 1<<x; //原数字与0..010..0或,利用或的特性保留其他位的值,只将第x位置1
  • 某位置0:
    result &= ~(1<<x); //原数字与1..101..1与,利用与的特性保留其他位的值,只将第x位设置为0;对0..010..0求反刚好得到1..101..1
  • 检查某位:
    status= result & (1<<x); //原数字与0..010..0与,利用与的特性去掉其他位的值,只保留第x位
  • 计算1的总数:
    int cnt = 0;
    while(result)
    {
        result &= result - 1;
        ++cnt;
    }

10.关于左移<<和右移>>,这里再扯个淡。c++语法规范中规定了,当右操作数大于做操作数的bit数时,运算结果是无法预测的。在intel处理器的SHL和SHR中,左移右移的计数器(counter)只有5bit(32位)或者6bit(64位),当你左移或者右移大于32或者64时,会造成计数器溢出,得到意料之外的结果。

int operand_r = 32;
unsigned int i = 1 << operand_r; //因为处理器的左移计数器溢出,所以相当于:1<<0,i等于1,而不是unsigned int溢出之后的0

11.sizeof操作符返回size_t类型的常量表达式(所以可以作为数组的维度),它并不实际计算运算对象的值(与decltype很像啊)。所以sizeof一个无效指针(未初始化或者已被delete)的解引用是安全的,能够返回正确的值。sizeof以一个byte作为1,对象占用几个byte就返回几。在c++11中,允许sizeof直接作用于类的成员,而不需要实现实例化一个类的对象。

char c; 
sizeof c; //返回1,一个byte嘛
char &rc = c; 
sizeof rc; //返回被引用值的大小,1
char *p; 
sizeof p; //返回指针的大小,一般为一个int,4
sizeof *p; //返回被指向对象的大小,为初始化也没关系,1
char ca[16] = {};
sizeof ca; //返回数组的大小,16
string s;
sizeof s; //返回固定部分大小,4
vector v;
sizeof v; //返回固定部分大小,12
sizeof myclass::member; //c++11新增,方便了
int ia[16] = {0};
size_t length = sizeof(ia)/sizeof(*ia); //length = 16,利用sizeof特性计算数组长度
12.内部算数类型隐式转换规则:

  • 对于整型:char/signed char/unsigned char/short/int  先被提升为int;如果unsigned short能被int装下,则unsigned short也被转换为int,否则unsigned short和int都被转换为unsigned int;如果表达式中包含unsigned int型,则表达式被提升为unsigned int型,如果表达式中包含long型,则表达式被提升为long型,如果表达式中有unsigned long,则表达式被提升为unsigned long型;如果unsigned int能被long装下,则unsigned int也会被提升为long型,否则unsigned int和long都会被提升为unsigned long型。
  • 对于浮点型:如果表达式中包含float型,则表达式被提升为float型,如果表达式中包含double型,则表达式被提升为double型。

13.其他类型的隐式转换:

  • 数组名转换为首元素的指针
  • 0或者nullptr转换为任意类型的指针;任意类型的指针转换为void *类型
  • 非空指针或者非零算术值转换为true,空指针或者零算术值转换为false
  • 非常量指针或者引用转换为常量指针或者引用(反之则不行)
  • 类类型的自动转换(需要重载操作符)
14.显示转换四个操作符的区别:
  • static_cast:静态转换,相当于c中的类型转换,编译过程中转换不成功将报错
  • dynamic_cast:动态转换,具有运行时类型检查的特性,只能被使用在将变量转化为类的指针/类的引用/void*时,进行父类子类之间的现实转换时更加安全
  • const_cast:去掉变量的const/volatile属性
  • reinterpret_cast:重解释转换,可以在任何两种类型之间进行转换,而不必担心编译器的检查,由程序员自己负起转换后正确使用的责任
15.空语句:只包含一个;(分号)的语句称为空语句,要注意循环语句和条件语句后面的空语句,它们容易造成逻辑上的错误而且难以发现。(血的教训啊...)
int i = 0;
while(i!=10); //错误的空语句,导致死循环
    ++i;
if (i > 9);   //错误的空语句,导致后续statment总是执行
    cout<<i<<endl;

16.复合语句:使用{}包括起来的一段语句块,被视为一个语句,它内部的变量具有块作用域。所以可以使用块语句扩展if/else/while/for/do等等后面的statement语句,达到多做很多事的目的。(原来还有这么深的道理,我一直以为if等语句后面跟这个{}是天然的呢.......汗!)

17.悬垂else:悬垂else是指,在同一语句作用域内,每个else都将与它最接近的if匹配,这有些时候会造成逻辑上的错误(与程序员预想不一致),在每个if和else之后使用{},划分清楚作用域即可解决此问题。

18.关于switch语句:switch后面的()中只能放置结果为整型的表达式;case后面只能紧跟常量表达式(constexpr出场);执行了一个case之后,如果没有break,C++将执行下一个case直到switch结束或者遇到break为止;只能在switch的最后(default或者最后一个case)中定义变量,如果想在这之前定义变量,请在该变量的外面添加{}表示语句块,否则当程序未执行到此case时,会导致这个变量在后面的语句中出现未定义的错误。

19.注意:在for语句头初始化的变量只在循环内部可见。

for(int i=0; i<10; i++)
    //do sth
int k = i; //错误,i出了作用域,已被销毁
20.c++11中引入了范围for语句(类似于Java中的for_each),来简化for循环的编写。
for (auto element : sequence)
    do sth

需要注意三点:

  • 可以在范围for中声明引用,然后使用此引用来修改序列中的值。(好东西,比Java自由一些)
    string s = "abcd";
    for (auto &c : s)
        c = toupper(c);
  • 如果不声明为引用,那么语句中声明的临时变量是序列中元素的副本,修改它不会影响到序列本身。在序列的元素很占用空间的情况下,可以使用const &来实现只读加减少复制(节省空间)的效果。
    vector<string> v = {"abcd","efgh"};
    for (const auto &e : v)
        cout<<e;
  • 在范围for语句中,不要修改序列本身(新插入一个元素或者删除一个元素),因为这会导致范围for持有的序列end()迭代器失效,进而造成越界访问等问题。
21.do while语句与while和for相比,能够让循环体先执行一次再判断条件。但是要注意在do后面的语句块中定义的变量,在while的()中是不可见的。最后说一个奇葩的do while(0)吧,这个语句有两个作用:
  • 替代goto语句:假设我们有个函数要分配一些资源,然后在中途执行过程中如果遇到错误则退出函数,在退出前先释放资源,那么可以用do while(0)代替goto。
    bool dosth()                                    bool dosth()
    {                                               {
       thing *p = new thing();                          thing *p = new thing();
       bool bRet = true;                                bool bRet = true;
                                                        do{
       // 执行并进行错误处理                                // 执行并进行错误处理
       bRet = func1();                                      bRet = func1();
       if(!bRet) goto errorhandle;                          if(!bRet) break;
    
       bRet = func2();                                      bRet = func2();
       if(!bRet) goto errorhandle;                          if(!bRet) break;
       // processing.......                                 // processing......
    
       // 执行成功,释放资源并返回                          // 执行成功,释放资源并返回
        delete p;                                           delete p;   
        p = NULL;                                           p = NULL;
        return true;                                        return true;
    errorhandle:                                         }while(0);
        delete p;                                        delete p;
        p = NULL;                                        p = NULL;
        return false;                                    return false;
    }                                                }
  • 在定义了多条语句的宏定义中使用,防止宏定义被if/else/while等语句拆开:
    #define SAFE_DELETE(p) delete p; p = NULL;
    if(NULL != p) SAFE_DELETE(p) //逻辑错误,SAFE_DELETE只被执行了一半,另外一半被扔到了if外边,必定执行
    //改成:
    #define SAFE_DELETE(p) {delete p; p = NULL}
    if(NULL != p) SAFE_DELETE(p); //语法错误,{}后面跟着;无法通过编译
    //改成:
    #define SAFE_DELETE(p) do{ delete p; p = NULL} while(0)
    //就可以避免以上的错误,既不会被拆开后面多个;也能正确执行
     
     

22.break语句和continue语句与悬垂else具有同样的工作原理,与距离最近的while/for/switch等语句匹配,注意不要产生逻辑上的错误。

23.异常的处理过程:异常发生之后,首先在本函数中查找是否有匹配的catch语句,如果没有,那么终止本函数,向上在调用这个函数的函数中查找,如果还没有则终止调用的函数继续向上...如果一直找到顶层(main)都没有匹配的catch,则调用terminate函数终止整个程序。

你可能感兴趣的:(C++ Primer阅读心得(第四章、第五章))