读书笔记(二十八) 《C++ Primer》#3
背景:
我为什么要重学C++?第一是巩固核心,软件编程有三大核心,语言、操作系统、框架设计,语言首当其冲,核心能力大都看不见摸不着只有你自己知道的东西。第二是将分散的知识串联起来,这样就能产生新的知识和新的创意。第三是当我们在巩固时创造出了新的知识,那么这些新的知识和旧的知识将同时变成智慧融合到身体中。
本系列基于《C++ Primer》学习,由于重点放在“重学”,我略过了滚瓜烂熟的部分,挑出以前常忽略的部分,以及记忆没有那么深刻的部分,特别是那些重要的但没有上心的部分。
开始
左值和右值
这两个名词是从C语言继承过来的,原本是为了帮助记忆即:左值可以位于赋值语句的左侧,右值则不能。 在C++语言中则没那么简单了,可以简单归纳为: 当一个对象被用作右值的时候,用的是对象的值(内容);当对象被用作左值的时候,用的是对象的身份(在内存中的位置)。
不同运算符对运算对象的要求不同,有的需要左值运算对象、有的则需要右值运算对象。 因此有个重要原则:在需要右值的地方可以用左值来代替,但不能把右值当左值(也就是位置)使用。当一个左值被当成右值使用时,实际使用的是它的内容(就是值)。
举几个例子: 赋值运算符,是左值运算,因为结果是值(内容) 取地址符,是右值运算,因为结果的是指针或地址 解引用运算符、下标运算符,是左值运算,因为运算结果是值(内容) 递增运算,是左值运算,因为运算结果是值(内容)
关于后置++
这里有个小提示,后置++版本会多浪费一个寄存器,但是编译器会帮我们优化掉,不过对于迭代器来说则不同,编译器通常不会优化迭代器的后置++,因此我们在使用迭代器后置++时要注意一下代码的优化。
cout<< *iter++ << endl;
==> 优化
count<< *iter << endl;
++iter;
sizeof运算符
sizeof有离线计算和实时运算之分,通常情况下有些值在离线时就能知道的类型的大小,这时编译器都会在编译时就将其替换成固定值。
举例说明sizeof运算符的结果取决于其作用类型:
- 对char类型表达式执行sizeof运算,结果为1。编译器离线计算
- 对引用类型执行sizeof,结果为被引用对象所占空间大小。编译器离线计算
- 对指针执行sizeof运算,结果为指针本身空间大小。编译器离线计算
- 对解引用指针执行sizeof,结果为指针指向的对象的空间大小。编译器离线计算
- 对数组执行sizeof运算,结果是整个数组所占空间大小。编译器离线计算
- 对string对象或vector对象执行sizeof运算,结果为该类型固定部分的大小。实时计算空间大小
强制类型转换
强制类型转换的根本目的就是告诉编译器,该变量的类型改变了,符合后面的逻辑运算,好让在编译器在编译时能够不误解逻辑运算,所以编译器并没有对内存内容做任何的改变,它只是为了迎合编译器。
//旧式的强制类型转换
type (expr); // 函数形式的强制类型转换
(type) expr; // c语言风格的强制类型转换
//新式的强制类型转换
static_cast<type>(expression); // 静态转换
dynamic_cast<type>(expression); // 运行时转换
const_cast<type>(expression); // 常量转换
reinterpret_cast<type>(expression); // 低阶类型转换
强制转换分为,static_cast(静态转换),dynamic_cast(运行时转换),const_cast(常量转换),reinterpret_cast(低阶类型转换)4种
- static_cast静态转换,直接将对象转换成指定类型对象。
- dynamic_cast运行时转换,通常用于继承对象,运行时转换会先通过RTTI的方式判断是否能进行父子转换,如果可以再进行执行转换。
- const_cast常量转换,只能改变运算对象的底层const无法改变具体类型,可以将const变量转换为非const变量,这样编译器就不再阻止我们对该对象进行写操作了。
- reinterpret_cast低阶类型转换,通常为运算对象的位模式提供低层次上的解释。例如将int转换为char*等低阶模式。
注意:使用reinterpret_cast比较危险的是,使用reinterpret_cast强制转换后编译器不会怀疑所转换的类型,如果类型错误则会因调用混乱而崩溃,而编译器在编译时不会发出任何警告或错误,因此reinterpret_cast通常适合比较底层转换工作。
异常处理
当异常被抛出时,首先在该函数中寻找匹配的catch子句,如果没有找到则终止该函数并在调用该函数的函数中继续寻找,以此类推沿着程序的执行路径逐层回退,直到找到适当类型的catch子句为止。 如果最终还是没能找到任何匹配的catch子句,程序会转到名为terminate的标准库函数中。该函数的行为与系统有关,一般情况下,执行该函数会导致程序非正常退出。 如果当程序没有使用try catch语句且发生了异常,系统会直接调用terminate函数并终止当前程序的执行。
分离式编译
分离式编译的意思就是,多个源文件在编译中可以只顾自己编译,最后再统一链接到执行文件或库文件中。
在分离式编译中,如果我们修改了其中一个源文件,那么只需要重新编译那个改动了的文件,再重新链接就可以。 现代大多数编译器提供了分离式编译每个文件的机制,这一过程通常会产生一个后缀名是.obj(windows)或.o(UNIX)的目标文件,再用链接器将所有目标文件链接成可执行文件或库文件。
参数传递
解释下,实参就是要传递的数据变量,形参就是传递后函数中的参数变量。
参数传递分引用传递和值传递
- 引用传递,通过传递引用的方式避免了对象拷贝。
- 值传递,则都是通过拷贝对象传递数据的。其中指针形参也是值传递,只不过传递时拷贝的是指针数据而非具体对象。
函数设置参数时,我们要注意尽量使用const常量引用或指针作为形参,因为这样既避免了对象拷贝,又可以限制对象的修改。
可变形参
使用initializer_list类型的形参可以做到传递可变数量的实参。和Vector不一样的是,initializer_list中的元素永远都是常量值。
举个例子:
void test_func(initializer_list<string> li)
{
for(auto beg = li.begin(); beg != li.end() ; ++beg)
cout<< *beg << " ";
}
test_func("1","3","4");
test_func("2","3");
test_func("2","4","6","8","10");
值返回和引用返回
与值传递和引用传一样,在函数返回时,我们也需要注意拷贝操作。
例如字符串拼接后的返回值就会造成对象拷贝的消耗:
string test_func(string &a, string &b)
{
return a + b;
}
为了避免拷贝我们会使用引用返回或指针,但要注意,使用引用返回或指针时如果返回的是局部对象则会引起内存使用错误。
string &test_func()
{
string test = "test";
return test; // 错误的返回了局部变量,导致内存使用错误
}
函数重载
函数重载的特点是函数名字相同参数列表不同且在同一个作用域。
在重载函数调用时,可能与我们想象的不同,其实编译器在离线时就区分了不同重载函数的调用地址。 当调用重载函数时,编译器会根据传递的实参类型去比较每个重载函数的参数直到匹配成功或失败。 我们知道虽然重载函数名字相同,但参数列表不同,这导致函数签名时的实际名字是不同,也就相当于调用的是不同名字的函数。
举例说明重载函数的函数签名后命名不同:
int func(int); // 函数签名后--> _Z4funci
int func(float); // 函数签名后--> _Z4funcf
func(3); // -> 匹配 _Z4funci
func(3.5F); // -> 匹配 _Z4funcf
重载函数的匹配过程为:
- 收集所有重载函数
- 比较实参与所有重载函数的形参,寻找最佳匹配函数
- 匹配规则为精准匹配和转换匹配两种,精准匹配要求实参与形参高度一致,转换则可以通过const转换、类型提升、算术类型转换来实现匹配。
- 匹配成功后替换为匹配函数的函数签名
至于函数签名的规则,其实每种编译器都不同,不过也是大同小异的。例如上述我们举例代码中的GCC编译器的规则为:
- 所有符号以“_Z”开头,
- 对于嵌套的名字后面紧跟“N”,
- 然后紧跟各个名称空间和类的名字,每个名字前是名字字符串长度,
- 再以“E”结尾,“E”后面是所有参数,
- 参数以简写代替,例如对于int类型来说就是字母“i”,float类型来说就是字母“f”,以此类推。
内联函数
我们知道函数在调用时会先保存当前寄存器、拷贝实参值、并在返回时拷贝恢复,这些都是函数调用的消耗。
内联函数则可以避免函数调用的消耗,其原理是就地展开函数,这样能省去了函数调用前和后的一系列操作。 我们可以通过内联关键字inline告诉编译器该函数需要在编译器时展开,但这也只是向编译器发出请求而已,如果编译器认为该内敛展开后性价比不高可以选择忽略这个请求。
常量表达函数constexpr,我们可以通过constrexpr定义常量表达式的函数,与普通内联函数不同的是,编译器会把constexpr函数的调用替换成实际结果,简单来说就是离线计算好结果并替换调用函数。
对于某个给定的内联函数或者constexpr函数来说,它的多个定义必须完全一致,基于这个原因,内联函数和constexpr函数通常定义在头文件中。
调试辅助指令
assert是常见的调试工具,它其实是一个预处理宏,通过判断表达式来终止程序执行。
assert的行为依赖于一个名为NDEBUG的预处理变量。如果定义了NDEBUG,则assert什么也不做。 默认状态下没有定义NDEBUG,我们可以使用#define来定义NDEBUG从而关闭调试状态,或者使用编译器的预处理指令 cc -D NDEBUG main.c
除了assert,C++编译器还定义了多个帮助我们调试的变量,例如:
- func,存放的是当前函数名字
- FILE,存放的是当前文件名
- LINE,存放的是当前代码行号
- TIME,存放的是当前文件编译时间
- DATA,存放的是当前文件编译日期
函数指针
函数指针指向的是函数地址,它的类型由它的返回值类型、形参类型共同决定,编译器会检查函数类型与函数指针类型之间的一致性。
bool (*point_func)(const string &, const string &);
// point_func前面有个*,因此point_func是指针,这里括号不能少。
// point_func右侧是形参列表,表示point_func指向的是,两个const string引用为参数、并且返回值是bool的函数类型。
bool lenCompare(const string &, const string &);
point_func = lenCompare; //指向lenCompare
point_func = &lenCompare; //等价于赋值
在指向不同函数类型的指针间不存在转换规则,也就是说,不同类型函数之间无法转换,即我们无法将一个类型函数转换到另一个类型函数指针。
在将函数指针用于形参、返回值、重载函数时,都必须遵循函数类型与函数指针一致性的原则,编译器不会自动将函数类型转换成我们想要的类型,因为转换是不允许的。
感谢您的耐心阅读
Thanks for your reading
版权申明
本文为博主原创文章,未经允许不得转载:
Copyright attention
Please don't reprint without authorize.
微信公众号,文章同步推送,致力于分享一个资深程序员在北上广深拼搏中对世界的理解
QQ交流群: 777859752 (高级程序书友会)