C++系统学习之六:函数

1、函数基础

  典型的函数定义包括:返回类型、函数名、由0个或多个形参组成的列表以及函数体。

2、参数传递

  形参初始化的机理和变量初始化一样。

  有两种方式:引用传递和值传递

2.1 传值参数

  当形参是非引用类型时,形参初始化和变量初始化一样,将实参的值拷贝给形参。

指针形参

  当执行指针拷贝操作时,拷贝的是指针的值,拷贝之后,两个指针是不同的指针。但通过指针可以修改它所指的对象。

2.2 传引用参数

使用引用避免拷贝

  拷贝大的类类型对象或者容器对象比较低效,甚至有的类类型根本就不支持拷贝操作。当某种类型不支持拷贝操作时,函数只能通过引用形参访问该类型的对象。

使用引用形参返回额外信息

2.3 const形参和实参

  当用实参初始化const形参时会忽略顶层const。因此,当形参有顶层const时,传给它常量对象或者非常量对象都是可以的。

void fun1(const int i){.......}

void fun2(int i){.....}

  上述两个函数不能算是重载,两个函数是一样的,程序会报错,fun2重复定义了fun1.

指针或引用形参与const

  可以使用非常量初始化一个底层const对象,但是反过来不行。同时一个普通的引用必须用同类型的对象初始化。

尽量使用常量引用

2.4 数组形参

数组有两个重要的特性:

  • 不允许拷贝
  • 使用数组时会转换成指针

尽管不能以值传递的方式传递数组,但是可以将形参写成类似数组的形式

void print(const int*);
void print(const int[]);
void print(const int[10]);

以上三种形式的声明等价

NOTE:当函数不需要对数组元素执行写操作的时候,数组形参应该是指向const的指针。只有当函数确实要改变元素值的时候,才把形参定义成指向非常量的指针。

当数组作为函数形参时,因此应该提供一些额外信息来确定数组的确切尺寸,管理数组形参有三种常用的技术:

使用标记指定数组长度

  要求数组本身包含一个结束标记。例如C风格字符串以空字符结尾。

使用标准库规范

  传递指向数组首元素和尾元素的指针。

void print(const int *beg,const int *end)
{
     while(beg!=end)
    {
         cout<<*beg++<<endl;   
    }  
}    
int arr[2]={0,1};
print(begin(arr),end(arr));

显式传递一个表示数组大小的形参

  专门定义一个表示数组大小的形参。

void print(const int ia[], size_t size);

int j[]={0,1};

print(j, end(j)-begin(j));

数组引用形参

  形参可以是数组的引用,此时,引用形参绑定到对应的实参上,也就是绑定到数组上。

void print(int (&arr)[10])
{
    for(auto elem:arr)
    {   
        cout<<elem<<endl;
    }
}
形参是数组的引用,维度是类型的一部分

NOTE:arr两端的括号必不可少

f(int &arr[10]);    //错误,将arr声明成了引用的数组
f(int (&arr)[10]);    //正确,arr是具有10个整数的整型数组的引用

传递多维数组

  数组第二维的大小都是数组类型的一部分,不能省略。传递多维数组传递的是指向数组的指针,实际还是指向首元素的指针。(多维数组就是数组的数组,数组的首元素还是数组,所以是指向数组的指针)。

void print(int (*matrix)[10],int size);  matrix是一个指针,指向有10个整数的数组

也可以用:

void print(int matrix[][10],int size);  matrix和上面一样的意义

2.5 含有可变形参的函数

C++提供两种方法:

实参类型相同,可以传递一个名为initializer_list的标准库类型

initializer_list形参

lnitializer_list和vector一样都是模板类型,不同的是initializer_list对象中的元素永远是常量值,不能改变。

void error_msg(initializer_list<string> ls)
{
	for (auto beg = ls.begin(); beg != ls.end(); ++beg)
	{
		cout << *beg << "   ";
	}
	cout << endl;
}

error_msg({ "hello" });
error_msg({ "hello!", "world!!" });  //注意值的传递要放在花括号里

省略符形参

  省略符形参是为了便于C++程序访问某些特殊的C代码而设置的。通常,省略符形参不应用于其他目的。省略符形参只能出现在形参列表的最后一个位置。

实参类型不同,使用可变参数模板

3、返回类型和return语句

3.1 无返回值函数

  返回类型是void类型的函数

3.2 有返回值函数

值是如何被返回的

  返回一个值的方式和初始化一个变量或形参的方式完全一样:返回的值用于初始化调用点的一个临时量,该临时量就是函数调用的结果。

 不要返回局部对象的引用或指针

返回类类型的函数和调用运算符

auto sz=getstring().size();    //getstring返回的string对象再调用size函数

 引用返回左值

调用一个返回引用的函数得到左值,其他返回类型得到右值。

 列表初始化返回值

函数可以返回花括号包围的值的列表。

vector<string>  process()
{
    return {"ni","hao"};
}

 递归

如果一个函数调用了它自身,不管这种调用是直接还是间接的,都称该函数为递归函数。

int factorial(int val)
{
    if(val>1)
        return factorial(val-1)*val;
    return 1;
}
求1x2x3x4......

 3.3 返回数组指针

因为数组不能被拷贝,所以函数不能返回数组。不过,函数可以返回数组的指针或引用。

最直接的方法是使用类型别名

typedef int arrT[10];
using arrT=int[10];

 声明一个返回数组指针的函数

int arr[10];    //arr是一个含有10个整数的数组
int *p1[10];    //p1是一个含有10个整型指针的数组
int (*p2)[10]=&arr;    //p2是一个指针,其指向一个有10个整数的数组

 如果要定义一个返回数组指针的函数,则数组的维度必须跟在函数名字之后,并且函数的形参列表应该先于数组的维度。

int (*func(int a,int b))[10];

 此函数返回的是一个指向有10个整数数组的指针。

 使用尾置返回类型

任何函数的定义都能使用尾置返回,但是这种形式对于返回类型比较复杂的函数最有效,比如返回类型是数组的指针或引用。

尾置返回类型跟在形参列表后面并以一个->符号开头。为了表示函数真正的返回类型跟在形参列表之后,我们在本应该出现返回类型的地方放置一个auto。

auto func(int i)->int (*)[10];

 使用decltype

4、函数重载

如果同一作用域内的几个函数名字相同但形参列表不同,称为函数重载。注意必须是形参列表不同,仅仅只是返回类型不同不可以称为重载。

重载和const形参

顶层const不影响传入函数的对象。一个拥有顶层const的形参无法和另一个没有顶层const的形参区分开来。

int f1(int i);
int f1(const int i);    //不构成重载,重复声明了f1

int f2(int *i);
int f2(int *const i);    //不构成重载,重复声明了f2

但底层const不同,可以构成重载

int f1(int &i);
int f1(const int &i);    //重载,新函数

int f2(int *i);
int f2(const int *i);    //重载,新函数

NOTE:最好只重载那些确实非常相似的操作。

const_cast和重载

const_cast在重载函数的情景中最有用。

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

string &shorterString(string &s1, string &s2)
{
	auto &r = shorterString(const_cast<const string&>(s1), const_cast<const string&>(s2));

	return const_cast<string&>(r);
}

4.1 重载与作用域

如果在内层作用域中声明名字,它将隐藏外层作用域中声明的同名实体。

void func()
{    
}

int main()
{    
    int func=0;
    func();    //错误,此时func是int类型的变量,不是函数,隐藏了外层的函数定义
    return 0;
}

5、特殊用途语言特性

  默认实参、内联函数和constexpr函数。

5.1 默认实参

一旦某个形参被赋予了默认值,它后面的所有形参都必须有默认值。

string  screen(int i=10, int a=1, stirng s=" ");

使用默认实参调用函数

在调用函数的时候省略该实参就可以。

默认实参声明

在给定的作用域中一个形参只能被赋予一次默认实参。

默认实参初始值

局部变量不能作为默认实参。除此之外,只要表达式的类型能转换成形参所需的类型,该表达式就能作为默认实参。

5.2 内联函数和constexpr函数

调用函数一般比求等价表达式的值要慢一些。

内联函数可避免函数调用的开销

在函数的返回类型前面加上关键字inline。

一般来说,内联机制用于优化规模较小、流程直接、频繁调用的函数。

constexpr函数

constexpr函数是指能用于常量表达式的函数。

定义constexpr函数要遵循:函数的返回类型及所有形参的类型都得是字面值类型,而且函数体中必须有且只有一条return语句。

constexpr int new_sz()
{    
    return 42;
}

constexpr int foo=new_sz();    //foo是一个常量表达式

为了能在编译过程中随时展开,constexpr函数被隐式地指定为内联函数。

NOTE:constexpr函数不一定返回常量表达式。

把内联函数和constexpr函数放在头文件内

和其他函数不一样,内联函数和constexpr函数可以在程序中多次定义。不过,对于某个给定的内联函数或者constexpr函数来说,它的多个定义必须完全一致。因此,内联函数和constexpr函数通常定义在头文件中。

5.3 调试帮助

两项预处理功能:assert和NDEBUG

assert预处理宏

assert宏常用于检查“不能发生”的条件。

assert(expr);

 如果expr为假,assert输出信息并终止程序执行,如果为真,assert什么也不做。

 NDEBUG预处理变量

assert的行为依赖于一个名为NDEBUG的预处理变量的状态。如果定义了NDEBUG,则assert什么也不做,默认情况下没有定义NDEBUG。

可以使用#define语句定义NDEBUG,从而关闭调试状态。

 6、函数指针

函数指针指向的是函数而非对象。和其他指针一样,函数指针指向某种特定类型。函数的类型由它的返回类型和形参类型共同决定,与函数名无关。

int func(int a, string s);

该函数的类型是int(int , string).要想声明一个可以指向该函数的指针,只需要用指针替换函数名即可。

int (*p)(int ,string )  //未初始化

 NOTE:*p的括号必须加上

使用函数指针

当把函数名作为一个值使用时,该函数自动地转换成指针。

int (*p)(int ,string )=func;

 可以使用函数指针直接调用该函数,而不需要解引用该指针。

指向不同函数类型的指针之间不存在相互转换,可以给函数指针赋值nullptr和0,表示指针没指向任何一个函数。

 重载函数的指针

如果定义了指向重载函数的指针,编译器通过指针类型决定选用哪个函数,指针类型必须与重载函数中的某一个精确匹配。

函数指针形参

和数组类似,虽然不能定义函数类型的形参,但是形参可以是指向函数的指针。可以直接把函数作为实参使用,此时它会自动转换成指针。

返回指向函数的指针

将auto和decltype用于函数指针类型

注意将decltype用于函数名时,返回的是函数类型,而非指针类型,如果要表示函数指针,需要自己加上*。

 

posted @ 2018-08-13 17:29  jameshater  阅读(731)  评论(0编辑  收藏  举报