C++ Primer Plus 读书笔记(第8、9章)
第八章 函数探幽
内联函数的选择
如果执行函数的编译代码的时间比处理函数调用机制的时间长,则节省的时间将只占整个过程的很小一部分。如果代码执行时间很短,则内联调用就可以节省非内联调用使用的大部分时间。总之:内联用在定义那些被经常调用且短小的函数,例如在某个循环中调用一个函数。
内联函数声明是在函数原型或者是定义前加上inline。通常的做法省略函数的原型,将整个定义放置在调用函数之前,这样形式上更加说明这个函数的特别。
引用变量
C++相比C增加了一种复合类型:引用变量。引用变量就是定义某个变量的一个别名,可以看做是一个伪装的指针。通常用在函数的参数和函数的返回值,前者能够使得大型数据的传递不产生一个副本且较指针编码更加简洁;后者能够确保类似cin>>a>>b;这样的语句变得合乎语法规则,通过先计算”cin>>a”返回一个cin的引用,那么这个函数”cin>>a”就是cin的一个别名,因此在这之后又可以接上>>b。
正由于引用变量的存在,因此C++的函数参数传递就不完全是按值传递,多了按引用传递,当一个函数的参数声明为引用类型应尽可能的将这个声明加上const,因为单单引用可能改变原来变量的值,加上const将更加保险,如果修改的话,能够在编译时就发现错误。当我们将一个表达式传递给引用时将报错:传递的值不是一个左值,然而加上const后就可以了,编译器将自动定义一个匿名的变量来保存这个表达式,再将这个变量传递给函数。
const引用变量在两种情况下将产生临时变量:
1、实参的类型正确,但不是左值;
2、实参的类型不正确,但可以转换为正确的类型。
两个相同定义的匿名结构体不能够视为相同的变量类型。
如果定义了这样的一个函数 int & fn(int &x, int y) {return x;}那么就存在这样的调用 fn(a, b) = 3;那么等价于a = 3。返回引用的函数实际上是被引用变量的别名。
不应让一个函数返回一个临时变量的引用,这样非常危险。同样如果一个函数的引用返回值能加上一个const又不影响结果的话,最好加上。通常,应避免在设计中添加模糊的特性,因为模糊特性增加了犯错的机会。
如果某个对象被声明为常量,那么只用常成员函数能够被调用。
C++在调用函数时,单凭从调用函数的语句看无法判断函数原型是否为引用,这也算一个小小的缺憾。
函数重载
函数重载时C++的一个重要特性,也正是存在函数重载,后面就又会出现函数的匹配选择问题。C++允许程序员定义名称相同的多个函数,只要这些函数的特性不同(在名称相同的情况下,参数不相同)即可。
一个一劳永逸的方法是定义个函数模板(参数化类型 parameterized types),那些复杂的库函数大部分都是利用模板实现的,且模板套模板相当复杂。正是由于存在着这么多重名的函数所以在调用时,编译器将进行一个重载解析,让最适合的被调用,如果有两个或以上的函数都是“最优的”,那么编译器将报错。
函数模板并不生成函数的定义,而只是在调用的时候再根据调用提供的类型生成相应的函数。也就是给编译器看的,编译器机智的帮助我们减少代码量。
关于函数模板又有几个名词要知晓:实例化、显式具体化。前者有分为隐式和显式。三者共称为具体化。对于一个函数模板例如:
template<typename T>
T max(T a, T b) { if (a > b) return a; else return b; }
如果直接调用max(4, 5)那么就是隐式实例化,而max<double>(4, 5)就是显示实例化。
如果重新定义如下函数template<> max<int>(int a, int b) {return 10000;},那么max(4, 5)就是显式具体化,因为重新给定了这个调用的一个函数定义,而不仅仅是模板的实例化。
名称修饰
这么多的同名函数在编译器看来其实是不一样,函数名在编译后将发生变化,函数参数列表的所有信息都将被加入到新的函数名中,这个规则各个不同编译器实现不同。
重载解析:
重载解析分为以下几步:
第1步:创建候选函数列表。其中包含与被调用函数的名称相同的函数的模板函数。
第2步:使用候选函数列表创建可行函数列表。这些都是参数数目正确的函数,为此有一个隐式转换序列,其中包括实参类型与相应的形参类型完全匹配的情况。例如,使用float参数的函数调用可以讲该参数转换为double,从而与double形参匹配,而模板可以为float生成一个实例。
第3步:确定是否有最佳的可行函数。如果有,则使用它,否则该函数调用出错。
最复杂的过程就是确定是否有最佳的可行函数的过程。
对于函数模板C++98有如下的规定:非模板函数由于具体化和常规模板,具体化优先于常规模板。另有:(优先级从高到低)
1、完全匹配,但常规函数由于模板。
2、提升转换(例如char和short自动转换为int,float自动转换为double)。
3、标准转换(例如,int转换为char,long转换为double)。
4、用户定义的转换,如类声明中定义的转换。
其中完全匹配中又有一些是无关紧要的转换:
Type -> Type &
Type & -> Type
Type [] -> Type *
Type (argument-list) -> Type (*) (argument-list)
Type -> const Type
Type -> volate Type
Type * -> const Type *
Type * -> volate Type *
之所以叫做无关紧要的转换就是因为左边的实参代入进去的话,那么声明为左右两边两种参数的形参的函数时在调用的优先级是相同的。例如Type -> Type & 当我们定义有fn(int x)和fn(int &x),此时调用fn(a),那么编译器将报错,因为从Type -> Type &的转换时无关紧要的。它们都是完全匹配。
然而这里又有存在两个函数都完全匹配时,仍可完成重载解析。
1、指向非const数据的指针和引用优先于非const指针和引用参数匹配。
2、如果一个函数时非模板函数而另一个不是,在这种情况下,非模板函数将优于模板函数(包括现实具体化)。
3、如果两个完全匹配的函数都是模板函数,则较具体的模板函数优先。例如:
存在template<typename T> T fn(T a , T b) {...} 和 template<typename T> T * fn(T * a, T * b){...} 则如果传递一个两个指针来调用函数时,则后一个模板更加具体,将会被调用。
第九章 内存模型和名称空间
该章详细介绍了关于一个程序的内存管理机制、代码是如何对内存进行影响以及程序员如何利用语言来达到预期内存分配目的。
项目
一个项目将由多个单元(文件)组成,各个文件能够单独编译,最后IDE将这些文件进行一个链接,多文件的引入带来了更多的麻烦。文件可以这样组织:
头文件:包含结构声明和使用这些结构的函数的原型。
源代码文件:包含与结构有关的函数的代码。
源代码文件:包含调用与结构相关的函数的代码。
这似乎和调用库函数非常相似,我们通过包含头文件(头文件里有我们使用函数的原型)使得所写的源程序能够通过编译,然后通过链接库函数来调用函数的定义。
头文件
头文件应该仅包含以下内容:
函数原型。
使用#define或const定义的符号常量。
结构声明。
类声明。
内联函数。
头文件也不会被编译,所以也不会有*.o的文件产生,头文件就是用来包含在另外一个.cpp的文件中为其提供调用函数的原型,让文件通过编译,然后再通过链接程序在其他.cpp文件生成*.o文件中找到某个函数的定义。
程序与内存变化的几个名词
存储持续性、作用域、连接性
存储持续性
存储持续性分为:自动存储持续性、静态存储持续性、动态存储持续性、线程存储持续性(C++11)。其强调的是某块内存的生命周期(从被使用到被释放)。
作用域
即一个变量在一个文件中能够使用的范围。先介绍声明区域:声明区域分为文件和代码块,这个很好理解。潜在作用域:从定义该变量位置开始到声明区域结束为潜在作用域。那么作用域就是排除那些被覆盖了的潜在作用域。
链接性
链接性就是一个变量能否通过链接被其他文件中的引用申明所使用。
单定义规则
单定义规则是指一个变量只能够被定义声明一次,但是可以被引用声明多次。引用声明的形式是extern type typename 的形式,其余形式都视为定义声明,包括extern type typename = value;的这种带有迷惑性的形式。
5种变量存储方式
C++11 auto register
C++11中auto和register关键字的意义发生了改变,后者取代了前者的含义,而前者变为自动类型推断。
cv-限定符、mutable
const 以及 volatile就是cv-限定符,前者不必多说,后者的意义在于告诉编译器该变量可能在代码中没有修改内存的情况下发生变化,防止编译器做相关的优化。
mutable,用来修饰成员变量。使用mutable修饰的成员变量在整个结构为const限定后仍可以进行更改。
函数的链接性
函数也具有链接性,且默认为外部变量,链接性为外部,须加上static显示的声明链接性为内部。这个特性也就满足了头文件中只要有一个引用声明一个函数即可,也就是我们常说的函数原型。
new运算符和定位new运算符
new运算符通过指针来灵活使用动态内存。定位new运算符需要包含<new>头文件。其能够指定一个开始地址然后声明一块内存,可以用来再次初始化已经开辟出来的内存块。
名称空间
名称的出现将使变量的命名更加的放心,名称空间可以是全局的,也可以位于另一个名称空间中,但是不能位于代码块中。通过名称非代码块中定义变量都有了一个名称空间,全局变量为::,因此通过加上作用域解析运算符能够方位各个不同名称空间定义的相同名字的变量。C#这门语言干脆为每个类都要定义一个名称空间。另外使用using声明比使用using编译指令更安全。using声明断言该处一定可以使用名称直接访问,若有冲突则报错,而using编译指令在发生冲突时将被覆盖。using声明同样将导入某个函数的所有重载版本。