C++Primer笔记(第四、五、六章)
关键词: 左值, 右值, 运算符, 强制类型转换, 函数重载, 函数指针等
第四章主要描述了运算符以及相关的优先级; 第五章讲了一些基本的语句以及流程控制符; 第六章讲了函数相关的内容, 重点在于参数, 重载函数以及函数指针等.
第四章
左值和右值 :
当一个对象被用作右值的时候, 用的是对象的值(内容); 当对象被用作左值的时候, 用的是对象的身份(内存中的位置). 在需要右值的地方可以用左值来代替, 但不能把右值当做左值来使用, 当一个左值被用作右值时, 实际使用的是它的值.
复合运算符 :
使用复合运算符只求值一次 , 使用普通的运算符则求值两次 . (区别除了对程序性能有些许影响外几乎可以忽略不计)
1 | int a += 1; |
递增和递减运算符 :
此前只知道这里的区别在于先递增(减)还是后递增(减) , 还有一个求值结果的细节.
1 | int i = 0, j; |
后置递增运算符的优先级高于解引用运算符:
1 | auto pbeg = v.begin(); |
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 | size_t count_calls() |
参数传递 :
- 拷贝大的类类型对象或者容器对象比较低效, 有的类烈性不支持拷贝操作, 出于这两种原因, 函数只能通过引用形参访问该类型的对象.
- 当函数无需修改引用形参的值时, 最好使用常量引用.
- 通过引用形参, 可以突破函数一次只能返回一个值的限制 (C#里可以使用out关键字)
const形参和实参 :
当用实参初始化形参时会忽略掉顶层const, 可以传递给它常量对象与非常量对象. 这个初始化方式和变量的初始化方式相同.
1 | void fcn(const int i) {/* fcn能够读取i, 但是不能向i写值 */} |
数组形参 :
因为不能拷贝数组, 所以我们无法以值传递的方式使用数组参数. 当为函数传递一个数组时, 实际上传递的是指向数组首元素的指针.
1 | void print(const int*); |
数组引用形参 :
1 | f(int &arr[10]) // 第三章提过: 这是引用的数组, 但不存在引用的数组 |
传递多维数组:
1 | 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
16void 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
由于需要动态分配内存导致此处开销可能增大.具体实现逻辑以及根据笔者暂时没有能力考究, 欢迎读者在本文的评论区附上相关讨论.
另附拓展阅读 : The cost of std::initializer_list省略符形参 :
省略符形参是为了便于C++程序访问某些特殊的C代码而设置的, 这些代码使用了名为varargs的C标准库功能(详见C编译器文档).
省略符形参应该仅仅用于C和C++通用的类型, 应当特别注意的是: 大多数类型的对象在传递给省略符形参时都无法正确拷贝, 省略符形参只能出现在形参列表的最后一个位置.1
2void foo(parm_list, ...);
void foo(...);
函数的返回值 :
返回一个值的方式和初始化一个变量或者形参的方式完全一样: 返回的值用于初始化调用点的一个临时量, 该临时量就是函数调用的结果.
不要返回局部对象的引用或指针, 因为函数终止之后局部变量的引用将不再指向有效的内存区域.
返回数组指针 :
因为数组不能被拷贝, 所以函数不能返回数组, 但可以返回数组的指针或引用, 可以通过以下几种方法进行定义:
类型别名:
1
2
3
4typedef 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
6int odd[] ={1,3,5,7,9};
int even[] = {2,4,6,8,10};
decltype(odd) *arrPtr(int i)
{
return (i % 2) ? &odd : &even; // 返回一个指向数组的指针
}
函数重载 :
同一作用域内的几个函数名字相同但形参列表不同.
1 | // top-level const 无法用来区分形参 |
const_cast
和重载 :
1 | // 参数和返回类型都是const string, 传入非const时, 返回的依然为const string |
特殊用途语言特性:
默认实参 :
1
2
3
4string screen(int ht = 24, int wid = 80. char backgrnd = ' ');
// 为每个形参提供了默认实参, 一旦某个形参被赋予了默认值, 它后面的所有形参都必须有默认值
// 在给定的作用域中一个形参只能被赋予一次默认实参
// 局部变量不能作为默认实参xxxxxxxxxx1 1vector
ivec(begin(ia), end(ia)); // 同理, 可以计算指针使用部分ia内容初始化ivecc++ 背景: 调用函数一般比求等价表达式的值要慢一些 , 将函数指定为内联函数可避免函数调用的开销.
在函数的返回类型前面加上关键字
inline
, 将其声明为内联函数, 适用于规模较小, 流程直接, 频繁调用的函数. (感觉类似#define的功能)1
2
3
4
5
6
7
8inline 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
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 | bool lengthCompare(const string &, const string &); |
当把函数名作为一个值使用时, 该函数自动地转换成指针.
1 | pf = lengthCompare; |
可以直接调用指向函数的指针调用该函数, 无需解引用指针.
1 | bool b1 = pf("hello", "bye"); |
在指向不同函数类型的指针间不存在转换规则.
指针类型必须与重载函数中的某一个精确匹配.
将decltype作用于某个函数是, 它返回函数类型而非指针类型
函数指针形参 :
形参可以是指向函数的指针.
直接使用函数指针类型显得冗长而繁琐, typedef和decltype可以简化这个步骤.
1 | bool lengthCompare(const string&, const string&); |
返回指向函数的指针
我的建议是用类型别名, 直接写看着太乱了.
1 | using PF = int(*)(int*, int); |