《C++ Primer》笔记(第05-08章)

第5章 表达式

  1. 若两个操作数都是负数则求模结果为负。
  2. 逻辑非:若操作数非零值则结果为false
  3. 如果val不是bool类型,不要这样比较:if (val == true)
  4. 如果操作数为负数,则位操作符如何处理其操作数的符号位依赖于机器,不可移植
  5. 移位操作的右操作数不可为负且必须严格小于左操作数位数值,否则结果未定义
  6. sizeof是一个操作符,其结果是编译时常量。三种用法:
    • sizeof(type name)
    • sizeof(expr)
    • sizeof exprsizeof用于expr时,并没有计算expr的值
  7. 逗号表达式从左向右计算,结果是其最右边表达式的值。如果最右边的操作数的左值,则逗号表达式的值也是左值。
  8. 值初始化的()语法必须置于类型名后面,而不是变量后。如:

    int x();
    

    上式声明了一个名为x、没有参数而返回int值的函数,而不是定义一个变量。

  9. c++保证:删除0值的指针是安全的,但这样做没有任何意义。
  10. 删除指针后,该指针编程悬垂指针,不再有效。
  11. 动态创建const对象的例子:const int *pci = new cons tint(10);
  12. 删除动态分配的内存失败称为“内存泄露”
  13. 类类型可以定义由编译器自动执行的类型转换,如:while (cin >> s) ...
  14. 显式转换也称为强制类型转换(cast),包括:dynamic_cast,const_cast,static_cast,reinterpret_cast。
  15. char *p = "hello";在编译时会出现警告,改成const char *就没问题。
  16. sizeof(func());的结果是func函数的返回类型的size,且不执行func。注意,不能这样用:

    sizeof(func);
    

    。因为func是函数类型,而sizeof不支持函数类型,func()才是数据类型。

  17. string str();的作用是声明一个函数而不是定义一个变量(如果想定义一个使用默认构造函数的对象,正确写法是string str;)。但如果是string str("abc");则相反,因为编译器看到了"abc"是一个量而不是一个变量声明。另外可以这么干:string str = string();
  18. for (;;);是一个死循环;for语句只要中间不写东西就是,这是合法的。省略循环条件等效于其永远为true
  19. char *p = "hello"; p[0] = 'x';会导致segment fault。
  20. 大多数操作符没有定义左右操作数的求值顺序,故不能依赖它来编写程序。

第6章 语句

  1. 空语句,如:while (expr);;又如:a = b + c;;。后者由两条语句组成:一条表达式语句和一条空语句。
  2. 在条件表达式中定义的变量必须初始化,该条件检验的就是初始化对象的值。如:

    if (int x = val) ...;
    switch (int x = func()) ...;
    
  3. switch语句的每个case的标号必须是整型常量表达式,浮点数也不行!任两个case标号不能有相同的值,否则compile error。
  4. switch结构中,除非使用块语句,否则只能在它的最后一个casedefault标号后才能定义变量。
  5. do...while中的continue会继续求解循环条件(和while一样)
  6. 使用预处理器进行调试:$ cc -DNDEBUG main.c

    这等效于在main.c的开头提供#define NDEBUG预处理命令。

第7章 函数

  1. 函数由函数名以及一组操作数类型唯一地表示
  2. 在c语言中具有const形参或非const形参的函数并无区别(引用和指针除外),故:

    void fcn (const int i) {...}
    void fcn (int i) {...}
    

    会产生编译错误:重定义。c++为了兼容也保持这点。

  3. 由于数组不能复制,所以无法编写使用数组类型形参的函数,而函数也不允许返回一个数组!
  4. 由于使用数组名字时,数组会被自动转化为指针,所以处理数组的函数通常通过操纵指向数组中的元素的指针来处理数组。
  5. 虽然不能直接传递数组,但形参可以写成数组形式,下面三种定义是等价的,形参类型都是int *

    void func(int *) {...}
    void func(int []) {...}
    void func(int [10]) {...}
    

    编译器只会检查实参是不是指针、指针类型是否匹配,而不会检查数组的长度。故对于以上第三种定义可以传个int[2]进去。

  6. 一般用法:传非引用指针:void func (const int *) {...}。 也可通过引用传递,此时编译器将检查数组实参的大小与形参大小是否匹配:

    void func (int (&arr)[10]) {...}
    
  7. 多维数组的传递:c++中没有多维数组,所谓多维数组实际上是一个一维数组,其元素是由数组组成。因此,除了第一维以外的所有维的长度都是元素类型的一部分,必须明确指定:

    void f(int (m*)[10]) {...}
    void f(int m[][10]) {...}
    void f(int m[100][10]) {...}
    

    上述三种定义是等价的。100,类似地,将会被忽略。

  8. 关于可变形参:对于c++程序,只能将简单数据类型传递给含有省略符形参,实际上,当需要传递给省略符形参时,大多数类类型对象都不能正确地复制
  9. 返回类型为void的函数可以return另一个返回类型同样是void的函数的调用结果
  10. 函数的返回值用于初始化在调用函数处创建的临时对象。当返回引用类型时,没有复制返回值,而是返回对象本身。
  11. 函数声明时需提供:返回类型、函数名、形参列表(不必对形参命名)。这三个元素成为函数原型。
  12. 默认实参可以是任何适当类型的表达式(包括函数调用)。
  13. 可在函数声明也可在函数定义中指定默认实参,但在一个文件中只能为一个形参指定默认实参一次(因此h和cc只能有一个这么干,因为cc包含了h)。如果在函数定义中指定默认实参,那么只有在包含该函数定义的源文件中调用该函数时(被指定的默认实参)才有效。在声明时指定默认实参可以类似于这么干:

    int f(int = 0);
    
  14. 只有当定义它的函数被调用时才存在的对象成为自动对象。
  15. 内联函数应该在头文件中定义,这一点不同于其他函数。这是因为,内联函数的定义对编译器而言必须是可见的,以便编译器能够在调用点内联展开该函数的代码。
  16. 由编译器自动生成的默认构造函数对内置类型成员的初始化规则:
    1. 若该类的对象被定义于全局作用域或定义为局部静态对象,这些成员被初始化为0
    2. 否则没有初始化

    另可参考《深度探索c++对象模型》

  17. 重载函数:出现于相同作用域中(当作用域不同时会发生名字屏蔽而不是重载),名字相同而形参表不同(默认实参不改变形参表)。注意,重载函数可以有不同的返回类型。
  18. 在c++中,名字查找发生在类型检查之前。
  19. 形参与const形参的等价性仅适用于非引用形参,有const引用的形参与有非const引用形参的函数是不同的,对于const指针和非const指针也是不同的。(即:仅当形参是引用或指针时,形参是否为const才有影响)
  20. 局部声明的名字会屏蔽全局声明的同名名字(包括函数名,此时,只要同名就会屏蔽,不管形参如何)
  21. 重载确定的寻找最佳匹配方法:若仅有一个函数满足以下条件则ok,否则编译错误:
    • 其每个实参的匹配都不劣于其他可行函数需要的匹配
    • 至少有一个实参的匹配优于其他可行函数提供的匹配
  22. 重载确定(overload resolution)的三个步骤:
    1. 确定候选函数(candidate function)集:与被调函数同名的函数的集合(声明要在调用点可见;如果调用的是类的成员函数则不考虑访问控制符如publicprivate等,而是将该类的所有同名函数都取出拿来用)
    2. 选择可行函数(viable function),两个条件:
      • 函数的形参个数与该调用的实参个数相同
      • 每一个实参的类型必须与对应形参的类型匹配,或者可被隐式转换为对应的形参类型
    3. 寻找最佳匹配:先为每个实参划分等级,再按照第下述方法做。
  23. 为了确定最佳匹配,编译器将实参到形参的类型转换划分等级:
    1. 精确匹配
    2. 通过类型提升(promotion)实现的匹配,包括整形提升:charsigned charunsigned charshortunsigned short提升为int,其次是提升为unsigned int。如,对于 func('a');来说,void func(int);要优于void func(short);
    3. 通过标准转换实现的匹配
    4. 数组->指针,指针->void*0->指针
    5. 算术值和指针->bool
    6. 算术值<->bool
    7. enum->int或更高
    8. const->const引用;非const指针->const指针
    9. 由标准库类型定义的转换
    10. 通过类类型转换实现的匹配
  24. 不能通过基于指针本身是否为const来实现函数重载(这符合第19点):

    void f(int *);
    void f(int *const);  // 错误,重声明
    
  25. 函数指针:在引用函数名但又没有调用该函数时,函数名将被自动解释为指向函数的指针,故以下两句赋值含义相同:

    funcptr p1 = func;
    funcpty p2 = &func;
    

    故调用时也相同:p1 (params);等价于(*p1)(params);

  26. 指向不同函数类型(包含返回类型的定义)的指针指针之间不存在转换。
  27. 函数指针作为形参时,以下两种形式等价:

    void f(bool (int *));
    void f(bool (*)(int *));
    
  28. 返回类型中的函数指针的例子:

    int (*ff(int))(int *, int);
    

    。其中,ff(int)是一个函数,它返回一个函数指针,其类型为int (*)(int *, int);

  29. 函数的返回类型不能是一个函数,最多只能是一个函数指针!!
  30. 若类A有个带一个int参数的ctor,则以下函数定义是合法的:

    A func() { return 10; }
    
  31. 如果一个类定义了一个非const函数以及对应的const函数,是合法的,C++将其看成重载,不会引起编译错误。对于这点,我的理解是,C++会将成员函数重写为包含指向对象本身的隐含this指针的函数,这样,const成员函数就会变成const对象指针,根据第19点这是可以被重载的。

第8章 标准I/O库

  1. IO对象不可复制或赋值,这表示着:
    • 不能存储在容器中
    • 函数的形参或返回类型不能是非指针或引用的流类型
  2. 刷新输出缓冲区的方法:

    cout << "hi" << flush;  // 不增加任何数据
    cout << "hi" << ends;  // 增加null字符
    cout << "hi" << endl;
    
  3. 如果一个流调用tie函数将其本身绑在传递给tieostream实参对象上,则该流上任何IO操作都会刷新实参所关联的缓冲区。
lambda /
Published under (CC) BY-NC-SA in categories 程序&设计  tagged with c++  读书笔记