《Essential C++》读书笔记(二)

来源:互联网 发布:网络上 蛤是啥意思 编辑:程序博客网 时间:2024/05/01 05:08

第二章面向过程的编程风格

这一章主要讲的就是函数。应抽取共同的操作行为,将他们实现为独立的函数。在我看来,函数就像是生活中工具。接收原材料,返回加工后的结果。

2.1如何撰写函数

函数必须定义4个部分:函数的返回型别、函数的名称、函数的参数列表、函数主体。
函数必须先声明才能被调用。函数的声明让编译器得以检查后继出现的使用方式是否正确。函数声明时不必提供其主体部分,就是形参表中参数名称都可以不提供,仅提供形参类型便可。注意函数的地位是平等的,不能在一个函数内定义另一个函数。
函数只能返回一个值,若想从函数中返回多个值的话,就应该将形参类型定义为引用reference类型。这样,函数中操纵的便是实参本身,而不是一个函数中的局部变量。本质上一个reference是以一个指针实现的。
如果函数的返回型别不为void,那么它必须在每个可能的退出点上将值返回。要注意函数的隐性退出点,不过编译器一般可以捕捉到这个错误。

2.2调用一个函数

函数有两种调用方式:传值调用和传址调用。
传值调用的时候,我们看一下程序的执行流程。“函数调用做了两件事情:用对应的实参初始化函数的形参,并将控制权转移给被调用的函数。”,“函数的运行以形参的(隐式)定义和初始化开始。”。所以,函数中操纵的不是实参本身,而是在函数内定义的局部对象,当函数调用结束时,该局部对象便会被释放,函数外便无法引用该对象了。
而传址调用则不同,应为引用的内部实现是以指针为基础的,所以函数内操纵的是实参本身,只不过就换了个名而已。将参数声明为reference的理由之一是,希望得以直接对所传入的对象进行修改。之二是,为了降低复制大型对象的负担。
引用虽然是以指针实现的,但引用又与指针有很大的不同。pointer参数和reference参数二者之间重要的差异是,pointer可能(也不可能)指向某个实际对象。当我们提领pointer时,一定要先确定其值并非为0.至于reference则必定会代表某个对象,所以不须做此检查。而且定义了一个reference型变量后,不能再改变该变量所引用的对象,而指针则不同。
当我们调用一个函数时,会在内存中建立起一块特殊区域,称为“程序栈”。这块特殊区域提供了每个函数参数的储存空间。它也提供函数所定义的每个对象的内存空间——我们将这些对象称为local object(局部对象)。一旦函数完成,这块内存就会被释放掉,或者说是从程序堆栈中被pop出来。所以函数运行完毕便不能在使用该对象。所以函数不应该返回指向局部对象的指针或引用。
下面是我copy来的关于c++的内存区域:

  1.栈,就是那些由编译器在需要的时候分配,在不需要的时候自动清楚的变量的存储区。里面的变量通常是局部变量、函数参数等。

  2.堆,就是那些由new分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个new就要对应一个delete.如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。

  3.自由存储区,就是那些由malloc等分配的内存块,他和堆是十分相似的,不过它是用free来结束自己的生命的。

  4.全局/静态存储区,全局变量和静态变量被分配到同一块内存中,在以前的C语言中,全局变量又分为初始化的和未初始化的,在C++里面没有这个区分了,他们共同占用同一块内存区。

  5.常量存储区,这是一块比较特殊的存储区,他们里面存放的是常量,不允许修改(当然,你要通过非正当手段也可以修改)

2.3提供默认参数值

“一般的程序撰写法则是,以‘参数传递’作为函数间的沟通方式,比‘直接将对象定义于file scope’更合适。理由之一是,函数如果过度依赖定义于file scope内的对象,就比较难以在其它环境中被重复使用,与比较难以修改——我们不仅需要了解该函数的运行逻辑,也必须了解定义于file scope中的那些个对象的运行逻辑。”
关于默认实参的提供,有两个规则:一是默认值的决议操作由最右边开始进行。如果我们为某个参数提供了默认值,那么这个参数右侧的所有参数都必须也具有默认参数值才行。二是,默认值只能够指定一次,可以在函数声明处,亦可以在定义处,但不能够在两个地方都指定。

2.4使用局部静态对象

为了节省函数间的通信问题而将对象定义于file scope内,永远都是一种冒险。通常,file scope对象会打乱不同函数间的独立性,使它们难以理解。
2.3节中的c++内存区域可已看出,静态对象是和全局对象在同一个内存区域的。所以即使在不同的函数调用过程中,其依然持续存在。虽然持续存在,但其作用域并未改变,依然是局部的。

2.5声明一个inling函数

一些函数体积很小,但是经常被使用。这样每当调用该函数时程序做的工作太多,有点浪费时间。这时便可以将这样的函数定义为inline函数。声明为inline,表示要求编译器在每个函数调用点上,将函数的内容展开。这样可以改善效率。但这只是对编译器提出的一种请求,具体怎么做还得看编译器。

2.6提供重载函数

“函数由函数名以及一组操作数类型唯一的表示。”这是primer中的原话。而重载函数的函数名一定都相同,所以函数重载的依据便是不同的形参列表。函数调用时,编译器会将实参与各个重载函数的形参作比较,找出其中最适当的。

2.7定义并使用Template Functions

function template将参数表中指定的所有(或部分)参数的型别信息(或数据信息)抽离出来。既然是template,模板嘛。一提这个模板,我就会想起活字印刷术。
当传递的实参能够决定模板的类型时,便不用单独声明类型,若不然则需要,如:

#include<iostream>

using namespace std;

template<typename type>

void kjk(type &os){

    int n=9;

    os<<n<<endl;

}

int main(){

    kjk(cout);

}

这时只要传递实参就行了。

#include<iostream>

using namespace std;

template<typename type,int n>

void kjk(type &os){

    os<<n<<endl;

}

int main(){

    kjk<ostream,9>(cout);

}

这时就得这样了。

当然,function template同时也可以是重载函数。如:

#include<iostream>

using namespace std;

template<typename type,int n>   //1

void kjk(type &os){

    os<<n<<endl;

}

template<typename type>          //2

void kjk(type &os){

    int n=9;

    os<<n<<endl;

}

template<typename type,int n>     //3

void kjk(type &os,int i){

    i=n;

    os<<i<<endl;

}


int main(){

    kjk<ostream,9>(cout);         //1

    kjk(cout);                      //2

    kjk<ostream,9>(cout,8);         //3

}

反正,模板形参表和函数形参表不能同时一样。别让编译器蒙就行。


2.8函数指针带来更大的弹性

函数指针的定义是最为闹心的
const vector<int>* (*seq_ptr)(int);
首先,seq_ptr是一个指针,然后跟右边的括号结合,说明它指向的是一个函数,而该函数有一个int型的形参,该函数的返回值类型是一个指针,该指针指向一个const vector<int>。
函数指针seq_ptr的使用与正常的函数名相同,直接加个括号,给个实参,就是调用其所指向的函数了。如:
seq_ptr(9);
对函数指针赋值很简单,把想要赋的函数的函数名直接给这个指针就行了。当然加个取址&也可以。
这一节最让我有收获的不是函数指针,而是我懂得了enum到底该怎么用了。那就是用来当数组的索引值,假如一个指针数组存储的是各个函数指针,将各个枚举成员的名定义成各个函数名就善了,省得还得记。

2.9设定头文件

头文件中放的应该是声明而不是定义。因为同一个程序的多个代码文件可能都会含入这个头文件。若是有定义的话,便会造成重复定义了。不过这有几个例外:inline函数的定义。为了能够扩展inline函数的内容,在每个调用点上,编译器都取得其定义。还有const型变量,因为const型变量默认情况下为该文件的局部变量,其它文件不可使用。看下面这个:
const vector<int>* (*seq_array[seq_cnt])(int);
注意了,这是一个定义,而不是声明,而且虽然前面有一个const,但是这依然不可放入头文件中。
嗯,具体原因,不解释了没时间了,请看我资源里的指针详解。
对于#include“ ”和#include<>二者的区别是:标准库的用<>,自定义的用“”。也就是:“如果表头文件和含入此文件的程序代码文件位于同一个驱动器目录下,我们便用双引号。否则便用尖括号。