C++Primer二刷重要知识点总结

时隔半年,最近开始第二遍看C++ Primer,发现自己之前有很多理解不透彻的语法细节或STL库使用,故写下此博客以做总结。(只会写易错、生疏的知识点)

第二章

—占坑–

C++Primer二刷

第四章 表达式

  1. C++的表达式要不然是右值(rvalue),要不然是左值(lvalue)。

    • 可具名的对象是左值,临时的不可具名的对象是右值。除了常量左值对象外,左值都可以位于赋值语句左侧;而右值只能位于赋值语句右侧。左值可以用取地址符&来取地址,右值不可。
    • 使用关键字decltype时,如果表达式的结果是左值,那么将得到一个引用类型。
  2. C++规定了运算对象的组合方式和顺序,但没有规定运算表达式中运算对象本身的求值顺序。

    • 如果多个运算对象表达式涉及到同一个对象,那么可能会引起未定义行为:

      // 注意,f1() 和 f2()的调用顺序不保证
      int i = f1() * f2();
      // 错误,赋值运算符左右两侧的求值顺序不保证,最终的大写字母可能被赋给*beg或*(beg + 1)
      while (beg != s.end() && !isspace(*beg))
        	*beg = toupper(*beg++);
      
    • 只有四种运算符规定了运算对象的求值顺序。它们是&&||?:,

  3. C++规定商一律向0取整。取余运算则要满足(m/n) * + m % n == m。

    • 取余运算满足:如果m%n不等于0,则它的符号和m相同。

    • 除了-m导致溢出的特殊情况,(-m)/nm/(-n)等于-(m/n)m%(-n)等于m%n(-m)%n等于-(m%n)

      cout << 21 / 5 << " " << 21 / (-5) << " " << (-21) / 5 << " "
               << 21 % 5 << " " << 21 % (-5) << " " << (-21) % 5 << endl;
      // 输出为: 4 -4 -4  1 1 -1
      
  4. C++运算符的优先级(部分):成员选择符 > 后置递增递减运算 > 前置递增递减运算、解引用、取地址 > 算术运算 > 移位运算 > 大小比较运算 > 相等性测试符 > 位运算 > 赋值运算

    • 可以混用解引用和递增运算符,将先执行递增运算符再进行解引用运算。

      *pebg++ // 输出当前值并将pebg前移一个元素
      *++pebg // 前移pebg并输出前移后位置的元素
      
    • 赋值运算符优先级较低,在条件语句中出现时要用括号围起。

      while ((i = get_value()) != 42) // 不使用括号的话,那么i将为get_value() != 42的布尔值结果。
        ...
      
    • 条件运算符优先级很低,在ostream的输出表达式中使用条件运算符时,也要加上括号。

      cout << (flag ? "fail" : "pass"); // 不使用括号的话,cout将输出flag的值。
      
  5. C++中的移位运算符。

    • 对于有符号类型负数的左移和右移运算是未定义行为。
    • 移位运算符右侧的运算对象(即移位的数量)必须不为负,而且值必须严格小于结果的位数。
    • 左移运算符向右侧插入二进制位0,右移运算符的行为则依赖与运算对象的类型:对于无符号类型,在左侧插入二进制位0,对于带符号类型,在左侧插入符号位的副本或二进制0,这取决于具体环境。
  6. 同decltype一样,sizeof关键字不会实际计算其对象的值:

    int *p = nullptr;
    sizeof(*p); // 可以在sizeof中解引用无效指针,此语句将返回int类型的大小
    
  7. 如果算术运算的两个算术对象类型不同,那么它们将按照一定规则进行转换。

    • 对于bool, char, signed char, unsigned char, short, unsigned short等类型来说,只要它们所有可能的值都能存在int里,它们就会提升成int类型;否则,提升成unsigned int类型。而较大的char类型(wchar_t等)提升成int, unsigned int, long, unsigned long, long long, unsigned long long 中最小的一种类型。

      总的来说,就是将算术对象提升到最小的可容纳它们大小的类型(最低也要提升到int类型)。

    • 如果一个是无符号类型,另一个是有符号类型,那么如果无符号类型不小于有符号类型,那么带符号类型将转换成无符号类型。如果有符号类型大于无符号类型,那么将依赖于机器:若无符号类型对象的所有制都能存储在带符号类型中,则无符号类型对象转换为有符号类型,如果不能,带符号类型对象将转换成无符号类型。

      第一次比较的是类型本身的大小(sizeof),而当有符号类型大于无符号类型时,第二次比较将比较运算对象实际值的大小。

第五章 语句

  1. 空语句只含有一个单独的分号。

    • 如果程序的某个地方,语法上需要一条语句但是逻辑上不需要,那么就可以使用空语句。

    • 多余的空语句;;是被允许的。

    • 如果循环的工作在条件部分就可以完成时,那么我们会使用空语句,但是此时应该加上注释以向他人声明这是有意省略循环体的:

      while (cin >> s && s != sought)
        ; // 空语句
      
  2. 所谓空块,指的是内部没有任何语句的一对花括号。

    • 空块的作用等价于空语句。
  3. 每个else与离它最近的尚未匹配的if匹配。

    • 最近的if语句必须与else处于同一层中,如果if在一个块中,那么它不会被块外的else匹配:

      if (grade % 10 >= 3) { // 被else匹配
      	if (grade % 10 > 7) 
          lettergrade += '+';
      } else // 与第一个if匹配
        	lettergrade += '-';
      
  4. switch语句的case标签必须是整形常量表达式(const int或者枚举类型)。

    • 如果在case标签代码中有对变量的定义时要小心,因为:如果在某处一个带有初值的变量位于作用域之外,在另一处该变量位于作用域之内,则从前一处跳转到后一处是非法行为。

      switch (flag)
          {
          case true:
              // forbidden : crosses initialization of str
              // string str; 隐式初始化
              int jval;	// 未初始化
              // forbidden : crosses initialization of ival
              // int ival = 1; 显式初始化
              break;
          case false:
              jval = 1;
              break;
          }
      
    • 多个case标签可以合并,因为case执行结束仅当遇到break或者到达switch结尾:

      while (cin >> ch)
          {
              switch (ch)
              {
              case 'a':
              case 'e':
              case 'i':
              case 'o':
              case 'u':
                  ++tCnt;
                  break;
              }
          }
      
  5. break语句的作用范围仅限于最近的循环或者switch,而continue仅作用于最近的循环。

  6. goto语句可以跳转到同一函数内的标签位置:

    • 标签名独立于变量或其他标识符的名字,所以它可以与变量同名而不受干扰。
    • 和switch语句一样,goto语句也不能绕过变量的带有初始化的声明。
  7. 使用范围for循环遍历容器时要小心。

    • 范围for语句(auto &r : v)...等价于:

      for (auto beg = v.begin(), end = v.end(); beg != end; ++beg) {
        auto &r = *beg;
        ... // 对r执行操作
      }
      
    • 以上,如果在for循环过程中修改了容器的值,比如当v为vector类型时调用了v.push_back(),那么预存的end迭代器将指向原来的容器结尾处,如果此push_back()引起了vector内数组对象的移动,那么end迭代器将指向无效的值,引起未定义行为。

  8. C++定义了一组标准异常类。

    899685-20170219153004691-1562982459

    • 我们只能以默认初始化的方式初始化exception、bad_alloc、bad_cast对象。
    • 对于其他异常类型如runtime_error、logic_error等,我们必须使用string对象或C风格字符串为它提供一个初值。
    • 所有异常类都有一个重载的what()方法,对于有字符串初始值的异常来说,此what()返回该字符串;对于其他无初始值的异常类型来说,what()返回的内容由编译器决定。

第六章 函数

  1. 向一个函数传参有两种方式:

    • 当形参是引用类型时,形参称为实参的别名,我们说这是按引用传参。
    • 当形参是值类型(包括指针)时,实参会被拷贝给形参,我们说这是按值传参。
    • 对于大的类类型对象或者容器对象,它们的拷贝十分低效,甚至有的类类型(如ostream)不支持拷贝,那么我们应该尽可能使用按引用传递来避免对拷贝构造函数的调用。
  2. const类型参数 + 引用、指针参数。

    • 当用实参初始化形参时会忽略掉顶层const(除了const T*const T&都是顶层const)。

      // error: 对同名函数的选择存在二义性
      void fcn(const int i) {}
      void fcn(int i) {}
      
    • 如果存在函数void reset(int &i),那么以下调用都非法:

      const int i = 0;
      double d = 2.0;
      reset(i);
      reset(10);
      reset(i + 1);
      reset(d);
      // 指针同理
      

      如果函数参数为非常量的引用或指针,那么实参不能是字面值、求值结果为右值的表达式、需要转换的对象、带有底层const类型的对象。

  3. 数组作为形参、作为返回值。

    • 不能以值的方式传递数组,传递的数组会被转换为指针(自然也无法靠此指针参数获得数组的大小 )所以以下声明等价:

      void print(const int*);
      void print(const int[]);
      void print(const int[10]); // 这个10并没有什么用...
      
    • 可以将形参定义成数组的引用,这时传入的参数视为数组类型而非指针,所以它携带了数组的大小信息。

      void print(int (&arr)[10]) {
        // 支持sizeof获取数组大小,自然也支持范围for循环
        cout << sizeof(arr) << endl;
        for (auto elem : arr)
          	cout << elem << endl;
      }
      
    • 传递多维数组时,第二维(以及后面所有维度)的大小都不能省略:

      void print(int matrix[][20], int rowSize) {}
      

      这是因为如果不知道有多少列,又因为多维数组本质是线性排布的一位数组,那么形如matrix[1][0]这样的写法将无法寻址(因为不知道matrix指针要偏移多少,事实上应该偏移20 * sizeof(int))。

    • 返回值为数组类型时,。。。

  4. 含有可变形参的函数

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值