C++Primer笔记(第四、五、六章)

关键词: 左值, 右值, 运算符, 强制类型转换, 函数重载, 函数指针等

第四章主要描述了运算符以及相关的优先级; 第五章讲了一些基本的语句以及流程控制符; 第六章讲了函数相关的内容, 重点在于参数, 重载函数以及函数指针等.

第四章

左值和右值 :

当一个对象被用作右值的时候, 用的是对象的值(内容); 当对象被用作左值的时候, 用的是对象的身份(内存中的位置). 在需要右值的地方可以用左值来代替, 但不能把右值当做左值来使用, 当一个左值被用作右值时, 实际使用的是它的值.

复合运算符 :

使用复合运算符只求值一次 , 使用普通的运算符则求值两次 . (区别除了对程序性能有些许影响外几乎可以忽略不计)

1
2
int a += 1;
int a = a + 1; // 基本等价

递增和递减运算符 :

此前只知道这里的区别在于先递增(减)还是后递增(减) , 还有一个求值结果的细节.

1
2
3
4
int i = 0, j;
j = ++i; // j = 1, i = 1; 前置版本得到递增后的值(即改变后的对象作为求值结果)
j = i++; // j = 1, i = 2; 后置版本得到递增之前的值(即运算对象改变之前的副本作为求值结果)
/* 前置版本把对象本身作为左值返回, 后置版本则将对象原始值的副本作为右值返回 */

后置递增运算符的优先级高于解引用运算符:

1
2
3
auto pbeg = v.begin();
while(pbeg != v.end() && *pbeg >=0)
cout << *pbeg++ << endl; // 等价于*(pbeg++);

sizeof运算符 :

sizeof运算符返回一条表达式或一个类型名字所占的字节数 , 所得的值类型为size_t.

sizeof并不实际计算其运算对象的值.对数组执行sizeof运算得到整个数组所占空间的大小, 等价于对数组中所有的元素各执行一次sizeof运算并将所得结果求和.

强制类型转换 :

  • static_cast :

    任何具有明确定义的类型转换, 只要不包含底层const, 都可以使用static_cast.

  • const_cast :

    const_cast只能改变运算对象的底层const (移除or增加对象的const性质). 如果对象本身不是一个常量, 使用强制类型转换获得写权限是合法的行为, 如果对象是一个常量, 再使用const_cast执行写操作就会产生未定义的后果.

  • reinterpret_cast :

    通常为运算对象的位模式提供较低层次上的重新解释.(想像不出对应的应用场景)

第五章

switch语句 :

  • 为了安全起见, 最好在最后一个标签后面加上break, 这样新增case就不需要加了.

  • 如果在某处一个带有初值的变量位于作用域之外, 在另一处该变量位于作用域之内, 则从前一处跳转到后一处的行为是非法行为.

范围for语句 :

如果需要对序列中的元素进行写操作, 循环变量必须声明成引用类型.

第六章 : 函数

局部静态对象 :

可以将局部变量定义成static类型从而获得局部静态对象, 在程序的执行路径第一次经过对象定义语句时初始化, 并且直到程序终止才被销毁 ; 在此期间即使对象所在的函数结束执行也不会对它有影响.

1
2
3
4
5
6
7
8
9
10
11
12
13
size_t count_calls()
{
static size_t ctr = 0; // 调用结束后, 这个值仍然有效
return ++ctr;
}
int main()
{
for(size_t i = 0; i != 10; ++i)
{
cout << count_calls() << endl;
}
return 0;
}

参数传递 :

  • 拷贝大的类类型对象或者容器对象比较低效, 有的类烈性不支持拷贝操作, 出于这两种原因, 函数只能通过引用形参访问该类型的对象.
  • 当函数无需修改引用形参的值时, 最好使用常量引用.
  • 通过引用形参, 可以突破函数一次只能返回一个值的限制 (C#里可以使用out关键字)

const形参和实参 :

当用实参初始化形参时会忽略掉顶层const, 可以传递给它常量对象与非常量对象. 这个初始化方式和变量的初始化方式相同.

1
void fcn(const int i)	{/* fcn能够读取i, 但是不能向i写值 */}

数组形参 :

因为不能拷贝数组, 所以我们无法以值传递的方式使用数组参数. 当为函数传递一个数组时, 实际上传递的是指向数组首元素的指针.

1
2
3
void print(const int*);
void print(const int[]);
void print(const int[10]); // 三种写法是等价的,这里的10仅仅是希望的维度,实际不一定

数组引用形参 :

1
2
3
f(int &arr[10])		// 第三章提过: 这是引用的数组, 但不存在引用的数组
f(int (&arr)[10]) // 正确, arr是具有10个整型的整形数组的引用
f(int (&arr)[]) // 错误:无法将参数 1 从“int [10]”转换为“int (&)[]”

传递多维数组:

1
2
void print(int (*matrix)[10], int rowSize) {}
void print(int (*matrix)[][10], int rowSize) {} // 两种等价, 注意第二维开始的维度都是数组类型的一部分, 不可缺少

含有可变形参的函数 :

  • initializer_list形参 :

    如果函数的实参数量未知但是全部实参的类型都相同, 我们可以使用initializer_list类型的形参. initializer_list是一种标准库类型, 用于表示某种特定类型的值的数组.

    与vector不一样的是, initializer_list对象中的元素永远是常量值, 我们无法改变其中元素的值.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    void error_msg(initializer_list<string> il)
    {
    for(auto beg = il.begin(); beg != il.end(); ++beg)
    cout << *beg << " ";
    cout << endl;
    }

    void error_msg3(const vector<string> il)
    {
    for(auto beg = il.begin(); beg != il.end(); ++beg)
    cout << *beg << " ";
    cout << endl;
    }
    // 两个函数输出相同
    error_msg({"moss1", "moss2", "moss3"});
    error_msg3({"moss1", "moss2", "moss3"});

    为什么不能用const vector<T>?

    这个问题在网上搜了下, 发现8年前就有人问过了 : Why use initializer_list instead of vector in parameters?

    看了下里面的回答, 大概是说vector不支持列表初始化? 不过上面的代码是可以正常运行不报错的.
    同样的问题在2015年也有人问过 :Initializer list vs. vector , 此时的最高赞回答大意为initializer_list的实现更加高效, 而vector由于需要动态分配内存导致此处开销可能增大.

    image-20211231163925916

    具体实现逻辑以及根据笔者暂时没有能力考究, 欢迎读者在本文的评论区附上相关讨论.
    另附拓展阅读 : The cost of std::initializer_list

    省略符形参 :

    省略符形参是为了便于C++程序访问某些特殊的C代码而设置的, 这些代码使用了名为varargs的C标准库功能(详见C编译器文档).
    省略符形参应该仅仅用于C和C++通用的类型, 应当特别注意的是: 大多数类型的对象在传递给省略符形参时都无法正确拷贝, 省略符形参只能出现在形参列表的最后一个位置.

    1
    2
    void foo(parm_list, ...);
    void foo(...);

函数的返回值 :

返回一个值的方式和初始化一个变量或者形参的方式完全一样: 返回的值用于初始化调用点的一个临时量, 该临时量就是函数调用的结果.

不要返回局部对象的引用或指针, 因为函数终止之后局部变量的引用将不再指向有效的内存区域.

返回数组指针 :

因为数组不能被拷贝, 所以函数不能返回数组, 但可以返回数组的指针或引用, 可以通过以下几种方法进行定义:

  • 类型别名:

    1
    2
    3
    4
    typedef int arrT[10];		// 类型别名: 含有10个整数的数组
    using arrT = int[10]; // 等价于第一行

    arrT* func(int i); // func返回一个指向含有10个整数的数组的指针
  • 直接声明:

    1
    int (*func(int i))[10];		// 最外层的括号必须存在, 不然将返回指针的数组
  • 尾置返回类型:

    1
    auto func(int i)-> int(*)[10];		// 跟在形参列表后, 并以一个->开头
  • 使用decltype:

    1
    2
    3
    4
    5
    6
    int odd[] ={1,3,5,7,9};
    int even[] = {2,4,6,8,10};
    decltype(odd) *arrPtr(int i)
    {
    return (i % 2) ? &odd : &even; // 返回一个指向数组的指针
    }

函数重载 :

同一作用域内的几个函数名字相同但形参列表不同.

1
2
3
4
5
6
7
8
9
10
// top-level const 无法用来区分形参
Record lookup(Phone);
Record lookup(const Phone); // 重复声明了

// low-level const 可以用来区分形参
Record lookup(Account&); // 作用于Account的引用
Record lookup(const Account&); // 作用于常量引用

Record lookup(Account*); // 作用于Account的指针
Record lookup(const Account*); // 作用域指向常量的指针

const_cast和重载 :

1
2
3
4
5
6
7
8
9
10
11
12
// 参数和返回类型都是const string, 传入非const时, 返回的依然为const string
const string &shorterString(const string &s1, const string &s2)
{
return s1.size() <= s2.size() ? s1 : s2;
}

// 当实参不是常量时, 得到的结果依然是一个普通的引用
string &shorterString(string &s1, string& s2)
{
auto &r = shorterString(const_cast<const string&>(s1), const_cast<const string&>(s2));
return const_cast<string&>(r);
}

特殊用途语言特性:

  • 默认实参 :

    1
    2
    3
    4
    string screen(int ht = 24, int wid = 80. char backgrnd = ' ');
    // 为每个形参提供了默认实参, 一旦某个形参被赋予了默认值, 它后面的所有形参都必须有默认值
    // 在给定的作用域中一个形参只能被赋予一次默认实参
    // 局部变量不能作为默认实参
  • 内联函数 :

    背景: 调用函数一般比求等价表达式的值要慢一些 , 将函数指定为内联函数可避免函数调用的开销.

    在函数的返回类型前面加上关键字inline, 将其声明为内联函数, 适用于规模较小, 流程直接, 频繁调用的函数. (感觉类似#define的功能)

    1
    2
    3
    4
    5
    6
    7
    8
    inline const string& shorterString(const string &s1, const string &s2)
    {
    return si.size() <= s2.size() ? s1 : s2;
    }

    cout << shorterString(s1, s2) << endl;
    // 在编译过程中展开如下(原书此处为<号, 没道理吧......)
    cout << s1.size() <= s2.size() > s1 : s2;
  • constexpr函数 :

    能用于常量表达式的函数, 函数的返回值类型以及所有形参的类型都得是字面值类型, 而且函数体中必须有且仅有一条return语句(注: 在C++14标准中这条被删除了, 参考来源:constexpr specifier).

    当constexpr函数的实参是常量表达式时, 返回值也是常量表达式, 反之不然.

    书中这里特意强调了 :

    constexpr函数不一定返回常量表达式.

    在不需要返回常量表达式的上下文, constexpr函数可以不返回常量表达式, 编译器不会检查函数的结果是否是一个常量表达式; 但在需要返回常量表达式的上下文中, 如果结果不是常量表达式, 编译器会报错.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    #include <iostream>
    constexpr size_t scale(size_t cnt) { return 32 * cnt; } // scale是一个constexpr函数
    int main()
    {
    int num = 10; // num非常量表达式
    constexpr int a = scale(10); // 正确,实参是一个字面值常量
    //constexpr int b = scale(num); // 错误, 实参非常量表达式, 返回值也不是
    int c = scale(num); // 正确, 上下文未要求返回常量表达式, 可以不返回
    }

    相关讨论 :

调试帮助 :

  • assert预处理宏 :

    1
    assert(expr);	// 如果表达式为假, assert输出信息并终止程序的执行; 如果为真则什么也不做

    感觉应用的场景不多, 是debug模式下的一些错误保护函数, 相比自己添加保护可能就就是NDEBUG可以关闭它比较方便吧……

  • NDEBUG预处理变量 :

    assert的行为依赖于一个名为NDEBUG的预处理变量的状态, 如果定义了NDEBUG, 则assert什么也不做, 默认状态下没有定义NDEBUG, 此时assert将执行运行时检查.

函数匹配 :

对重载函数进行匹配 :

  1. 候选函数 : 与被调用的函数同名; 其声明在调用点可见
  2. 可行函数: 形参数量和提供的实参相等; 实参类型与对应的形参类型相同, 或者可以转换成形参的类型
  3. 最佳匹配: 实参类型与形参类型越接近, 匹配的越好

多个形参时, 如果有且只有一个函数满足以下条件:

  1. 该函数每个实参的匹配都不劣于其他可行函数需要的匹配
  2. 至少有一个实参的匹配优于其他可行函数提供的匹配

如果没有函数满足, 编译器将报错并返回二义性调用的信息.

函数指针 :

指针指向函数, 函数的类型由它的返回类型和形参类型共同决定. (我以为这个集合叫函数签名, 但并不是, 函数签名的定义要比这个范围广的多)

1
2
3
4
bool lengthCompare(const string &, const string &);

bool (*pf)(const string &, const string &); // 未初始化
// 括号必不可少, 少了括号将是一个返回bool指针的函数

当把函数名作为一个值使用时, 该函数自动地转换成指针.

1
2
pf = lengthCompare;
pf = &lengthCompare; // 取地址符是可选的

可以直接调用指向函数的指针调用该函数, 无需解引用指针.

1
2
bool b1 = pf("hello", "bye");
bool b2 = (*pf)("hello", "bye"); // 解引用也是可选的

在指向不同函数类型的指针间不存在转换规则.

指针类型必须与重载函数中的某一个精确匹配.

将decltype作用于某个函数是, 它返回函数类型而非指针类型

函数指针形参 :

形参可以是指向函数的指针.

直接使用函数指针类型显得冗长而繁琐, typedef和decltype可以简化这个步骤.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
bool lengthCompare(const string&, const string&);
// 函数类型
typedef bool Func(const string&, const string&);
typedef decltype(lengthCompare) Func2;

// 指向函数的指针
typedef bool (*FuncP)(const string&, const string&);
typedef decltype(lengthCompare) *FuncP2;

// 等价
void useBigger(const string&, const string&, Func); // 编译器自动将Func表示的函数类型转换为指针
void useBigger(const string&, const string&, FuncP2);
// 在Clion中验证该种写法也未报错
void useBigger(const string&, const string&, Func2); // decltype的结果是函数类型, 在结果前加上*才是指针

返回指向函数的指针

我的建议是用类型别名, 直接写看着太乱了.

1
2
3
4
5
using PF = int(*)(int*, int);

PF f1(int); // 返回指向函数的指针
int (*f1(int))(int*, int); // 等价
auto f1(int)->int(*)(int*, int); // 尾置返回类型, 等价