C++ Primer 学习笔记12 函数(return语句、函数声明、局部对象、内联函数、类的成员函数、重载函数)

来源:互联网 发布:淘宝网app官方下载 编辑:程序博客网 时间:2024/06/05 18:10

1、return语句

1、没有返回值的函数

    在返回值类型为void的函数中,return返回语句不是必需的,隐式的return发生在函数的最后一个语句完成时。

    一般情况下,返回类型是void的函数使用return语句是为了引起函数的强制结束,这种return的用法类似于循环结构中的break语句的作用。

void do_swap(int &v1,int &v2){int tmp = v1;v1 = v2;v2 = tmp;}void swap(int &v1,int &v2){if (v1 == v2)return;return do_swap(v1,v2);}

2、具有返回值的函数
任何返回类型不是void的函数都必须返回一个值,而且这个返回值的类型必须和函数的返回类型相同,或者能隐式转化为函数的返回类型。
bool str_subrange(const string &str1,const string &str2){    if (str1.size() == str2.size())        return str1 == str2;    string::size_type size = (str1.size() > str2.size() ? str1.size() : str2.size());    string::size_type i = 0;    while (i != size)    {        if (str1[i] != str2[i])            return;//Error    return没有返回值        ++i;<pre name="code" class="cpp" style="color: rgb(0, 0, 153);">     }    //此处应有return语句
 }
在含有return语句的循环后没有提供return语句是很危险的,因为大部分的编译器不能检测出这个漏洞,运行时会出现什么问题是不确定的。
返回类型不是void的函数必须返回一个值,但此规则有一个例外情况:允许主函数main没有返回值就可结束。
可将主函数main返回的值视为状态指示器,返回0表示程序运行成功,其他大部分返回值则表示失败。
返回非引用类型:如果函数返回非引用类型,则其返回值可以是局部对象,也可以是求解表达式的结果
返回引用:当函数返回引用类型时,并没有复制返回值,相反,函数返回的是对象本身!

千万不要返回:

    1)局部对象的引用

    2)指向局部对象的指针(悬垂指针!)

const string &manip(const string &s){string ret = s;return ret;//Warning} 

这个函数在运行时出错,因为它返回了局部对象的引用。当函数执行完毕,字符串ret占用的存储空间被释放,函数返回值指向了对于这个程序来说不再有效的存储空间。
同样,返回局部对象的指针也是错误的,一旦函数结束,局部对象被释放,返回的指针就变成了指向不再存在的对象的悬垂指针。
3、递归
 直接或间接调用自己的函数称为递归函数。
阶乘
int factorial(int val){if (val > 1)return val * factorial(val - 1);return 1;}
递归函数必须定义一个终止条件:否则,函数就会“永远”递归下去,直到程序栈耗尽!
2、函数声明
函数必须在被调用前声明。一个函数只能定义一次,但可声明多次。
函数声明由函数返回类型、函数名和形参列表组成。形参列表必须包括形参类型,但不必对形参命名。
在函数声明中的形参名会被忽略,如果在声明中给出了形参的名字,它应该只用作辅助说明文档:
void print(int *arr,size_t size);

应该在头文件中提供函数声明:

    在源文件中声明函数的方法比较呆板而且容易出错。解决的方法是把函数声明放在头文件中,这样可以确保对于指定函数其所有声明保持一致。如果函数接口发


生变化,则只要修改其唯一的声明即可。


    定义函数的源文件应该包含声明该函数的头文件。将提供函数声明头文件包含在定义该函数的源文件中,可使编译器能检查该函数的定义和声明时是否一致。

 如果有一个形参具有默认实参,那么,它后面所有的形参都必须有默认实参。因此,设计带有默认实参的函数,其中部分工作就是排列形参,应该使最少使用默认实参的形参排在最前面,最可能使用默认实参的形参排在最后。

既可以在函数声明也可以在函数定义中指定默认实参。但是,在一个文件中,只能为一个实参指定默认实参一次:
//ff.hint ff(int = 0);//ff.cc#include "ff.h"int ff(int a = 0)//Error{//...}

通常,应该在函数声明中指定默认实参,并将该声明放在合适的头文件中
3、局部对象
 C++语言中,每个名字都有作用域,每个对象都有生命期;名字的作用域指的是该名字的程序文本区,对象的生命期是在程序的执行过程中对象的存在时间。

静态局部对象

    一个变量如果位于函数的作用域内,但生命期跨越了这个函数的多次调用,这种变量往往很有用。则应该将这样的对象定义为static(静态的)

    static局部对象确保不迟于在程序执行流程第一次经过该对象的定义语句时进行初始化。这种对象一旦被创建,程序结束前都不会撤销。当定义静态局部对象的函数结束时,静态局部对象不会撤销。在该函数被多次调用的过程中,静态局部对象会持续存在并保持它的值

/**在第一次调用函数count_calls之前,cnt就已经创建并赋初值为0*在执行函数 count_calls 时, 变量 ctr 就已经存在并且保留上次调用该函数时的值。 */size_t count_calls(){    static size_t cnt = 0;    return ++cnt;}int main(){    for (int i = 0; i != 10; ++i)    {        cout << count_calls() << endl;    }    return 0;} 
4、内联函数

使用短小函数的好处:

    1)阅读和理解函数的调用,要比读一条用等价的表达式要容易得多。

    2)如果需要做任何修改,修改函数要比找出并修改每一处等价表达式容易得多。

    3)使用函数可以确保统一的行为,每个测试都保证以相同的方式实现。

    4)函数可以重用,不必为其他应用重写代码。

但是这样的函数也存在者一些缺陷,调用函数比求解等价表达式要慢得多。在大多数的机器上,调用函数都要做很多工作:

    1)调用前先保存寄存器,并且要在返回时恢复

    2)复制实参

    3)程序必须转向一个新的位置执行

1、使用内联函数避免函数调用的开销

   将函数指定为inline函数,就是将它在程序中每个调用点上“内联地”展开。从而消除了把表达式写成函数的额外执行开销。

inline const string &shorterString(const string &s1,const string &s2){return (s1.size() < s2.size() ? s1 : s2);}

内联说明(inline)对于编译器来说只是一个建议,编译器可以选择忽略这个建议;一般来说,内联机制使用与优化小的,只有几行而且经常被调用的函数。

2、把内联函数写入头文件

  内联函数要在头文件中定义,这一点不同于其他函数!

   在头文件中加入或修改内联函数时,使用了该头文件的所有源文件都必须重新编译!因为修改此处的头文件相当于修改了各个源文件!

5、类的成员函数

函数原型必须在类中定义,而函数体既可以在类中定义,也可以在类外定义,一般比较短小的函数定义在类的内部。

    编译器隐式的将在类内定义的成员函数当作内联函数。

    类的成员函数可以访问该类的private成员。

class Sales_item{public:double avg_price() const;bool same_isbn(const Sales_item &rhs) const{return isbn == rhs.isbn;}private:std::string isbn;unsigned int units_sold;double revenue;};
每个成员函数都有一个额外的、隐含的形参将该成员函数与调用该函数的类对象绑定在一起。而这个形参就是this指针!
const成员函数的引入

bool Sales_item::same_isbn(const Sales_item *const this,                           const Sales_item &rhs) const{    return (this -> isbn == rhs.isbn);}
使用这种方式的const函数称为常量成员函数。由于this是指向const对象的指针,const成员函数不能修改调用该函数的对象,因此,函数same_isbn只能读取而不能修改调用他们的对象的数据成员。
const对象、指向const对象的指针或者引用只能用于调用其const成员函数,如果尝试用它们来调用非const成员函数则是错误的。
在类外定义成员函数时,必须指明它们是类的成员:
double Sales_item::avg_price() const{    if(units_sold)        return   revenue/units_sold;    else         return  0;}
使用作用域操作符指明函数avg_price是在类Sales_itemd的作用域范围内定义的。
构造函数
在定义类时没有初始化它们的数据成员,而是通过构造函数来初始化其数据成员。
构造函数是特殊的成员函数,与其他成员函数不同,构造函数与类同名,而且没有返回类型。一个类可以有多个构造函数,每个构造函数必须有与其他构造函数不同数目或者类型的形参。
Sales_item::Sales_item():units_sold(0),revenue(0){};

    1)通常构造函数会作为类的接口的一部分,因此必须将构造函数定义为public的。

    2)在冒号和花括号之间的代码称为构造函数的初始化列表。构造函数的初始化列表为类的一个或多个数据成员指定初值。它跟在构造函数的形参表之后,以冒号开头。构造函数的初始化式是一系列成员名,每个成员后面是括在圆括号中的初始值。多个成员的初始化用逗号分隔。

    3)如果没有为一个类显式定义任何构造函数,编译器将自动为这个类生成默认构造函数。由编译器创建的默认构造函数通常称为合成的默认构造函数。

对于具有类类型的成员,如isbn,则会调用该成员所属类自身的默认构造函数实现初始化。而内置类型成员的初值却要依赖于对象如何定义,如果对象定义为全局对象,或定义为静态局部对象,则将这些成员初始化为0,不然,则不提供这些成员的初始化工作,这些成员没有初始化!

  合成的默认构造函数一般适用于仅包含类类型成员的类。而对于含有内置类型或复合类型成员的类,则通常应该定义他们自己的默认构造函数初始化这些成员。

类代码文件的组织

   通常情况下,将类的声明放置在头文件中,在类外定义的成员函数放置在源文件中,C++程序员习惯使用一些简单的规则给头文件及其关联的类定义代码命名。类定义应置于名为type.htype.H的文件 中,type指在该文件中定义的类的名字成员函数的定义则一般存储在与类同名的源文件中。依照这些规则,我们将类Sales_item放在名为Sales_item.h的文件中定义。任何需使用这个类的程序,都必须包含这个头文件。而Sales_item的成员函数的定义则应该放在名为Sales_item.cc的文件中。这个文件同样也必须包含Sales_item.h头文件

6、重载函数
出现在相同作用域中的两个函数,如果具有相同的名字而形参表不同,则称为重载函数。
如果局部的声明一个函数,则该函数将屏蔽而不是重载在外层作用域中声明的同名函数。
如果两个函数声明的返回类型和形参表完全匹配,则将第二个函数视为第一个的重复声明。如果两个函数的形参表完全相同,但返回类型不同,则第二个声明是错误的。
Record lookup(const Account&);bool lookup(const Account&);    //error
函数不能仅仅基于不同的返回类型而实现重载。
有些看起来不相同的形参,实质上是相同的:
<span style="font-family: Arial, Helvetica, sans-serif;"></span><pre code_snippet_id="545516" snippet_file_name="blog_20141208_15_6347098" name="code" class="cpp" style="color: rgb(85, 85, 85); line-height: 35px;"><span style="font-family: Arial, Helvetica, sans-serif;">const phone&,const Name&);</span>
Record lookup(const phone&,const Name&=""); //默认实参不同,并未改变形参的个数和类型。
Record lookup(phone);Record lookup(const phone);
复制实参时并不考虑形参是否为const,函数操纵的只是实参的副本,函数无法修改实参。结果既可以将const对象传递给const形参,也可以传递给非const形参,这两种形式并无区别。
形参与const形参的等价性仅仅适用于非引用形参。有const引用形参的函数和有非const引用形参的函数是不同的。类似的,如果函数带有指向const类型的指针形参,则与指向相同类型的非const对象的指针形参的函数不相同。
如果局部的声明一个函数,则该函数将屏蔽而不是重载在外层作用域中声明的同名函数。
void print(const string &);  void print(double);   void fooBar(int ival)  {      void print(int);     print("Value: ");   //error    <span style="font-family: SimSun;">print(const string &)已经被隐藏</span>    print(ival);        //OK       调用<span style="font-family: SimSun;">void print(int);</span>    print(3.14);        //OK       <span style="font-family: SimSun;">调用</span><span style="font-family: SimSun;">void print(int)</span>}  
重载确定的三个步骤

  1)候选函数

     第一步是确定该调用所考虑的重载函数集合,该集合中的函数称为候选函数,候选函数是与被调用函数同名的函数。

    2)选择可行函数

    选择可行函数必须满足两个条件:函数的形参个数与该调用的实参个数相同;每一个实参的类型必须与对引形参的类型匹配,或者可以被隐式转换成为对应的形参类型。

    如果没有找到可行函数,则该调用错误。

   3)寻找最佳匹配

    实参类型与形参类型越接近则匹配越佳,因此,实参类型与形参类型之间的精确匹配比需要转换的匹配要好。

为了确定最佳匹配,编译器将实参类型到相应形参类型转换划分等级。转换等级以降序排列如下:

    1)精确匹配。实参与形参类型相同。

    2)通过类型提升实现的匹配

    3)通过标准转换实现的匹配

    4)通过类类型转换实现的匹配


需要类型提升或转换的匹配

    1较小的整型提升为int:对于任意整型的实参值,int型版本都是由于short版本的较佳匹配,即使从形式看short版本匹配较佳

void ff(int);void ff(short);ff('a');//匹配ff(int);
      2通过提升实现的转换要优于其他标准转换。如:对于char型实参来说,有int型形参的函数是优于有double型形参的函数较佳匹配。




0 0
原创粉丝点击