[C++] 函数

函数基础

一个典型的函数定义包括以下部分:返回类型、函数名字、由0个或多个形参组成的列表以及函数体。其中形参以逗号隔开,形参列表位于一个圆括号之内,函数指向的操作在语句块内,也就是函数体。

函数调用

使用调用运算符来执行函数,调用运算符的形式是一对圆括号,它作用于一个表达式,该表达式是函数或者指向函数的指针,圆括号内是逗号间隔开的实参列表,我们用实参初始化函数的形参。

函数的调用完成了两项工作:1、用实参初始化函数对应的形参;2、将控制权转移给被调用函数。

执行函数的第一步是定义并初始化它的形参。

形参和实参

实参是形参的初始值,实参的类型必须与对应的形参类型匹配,且数量相同。

int fact(int);

fact(3);
// 正确

fact(3.14)
// 正确
// 3,14(double) ~ 3(int)

形参列表

1、每个形参必须有声明;

2、每个形参的声明必须单独书写;

3、每个形参不能同名;

返回值类型

1、函数的返回值类型不能是数组类型或函数类型

2、函数的返回值类型可以是指向数组指针(或引用)或指向函数的指针。

局部对象

名字有作用域,对象有生命周期

1、名字的作用域是程序文本的一部分,名字在其中可见

2、对象的生命周期是程序执行过程中该对象存在的一段时间

函数的形参也是局部变量。

同时局部变量还会隐藏在外层作用域中同名的其他所有声明。

局部变量的生命周期依赖于定义的方式(static、非static)

自动对象

只存在于块执行期间的对象称为自动对象。当块执行结束后,块中创建的自动对象的值就变成未定义的了。

局部静态对象

local static object:在程序执行路径第一次经过对象定义语句时初始化,并且直到程序终止才被销毁,在此期间即使对象所在的函数结束执行也不会对它有影响。

局部静态变量没有显式的初始化,它将执行值初始化,内置类型的局部静态变量初始化为0.

函数声明

函数只能定义一次,但可以声明多次。如果一个函数永远也不会用到,那么它可以只有声明没有定义

函数原型也就是函数声明,包括函数返回类型、函数名、形参类型

在头文件中声明函数,在源文件中定义函数。

分离式编译

在分离式编译中,如果我们改变了一个源文件,只需要重新编译这个改动的文件即可。

参数传递

每次调用函数时都会重新创建它的形参,并用传入的实参对形参进行初始化。

如果形参是引用类型,它将绑定到对应的实参上也就是引用传递,否则将实参的值拷贝后赋给形参。引用形参是它对应的实参的别名。

当实参的值被拷贝给形参时,形参和实参是两个互相独立的对象,这样的实参被值传递。

传值参数

值传递时,函数对形参做的所有操作都不会影响实参。

指针形参

指针的行为和其他非引用类型一样,当执行指针拷贝操作时,拷贝的是指针的值,拷贝之后,两个指针是不同的指针,因为指针使我们可以间接的访问它所指的对象,所以指针可以修改它所指对象的值。

传引用参数

引用的操作实际上是作用在引用所引的对象上。

通过使用引用形参,允许函数改变一个或多个实参的值

当调用含有引用形参的函数时,只需要直接传入对象而无须传递对象的地址

通过使用引用来避免拷贝,因为拷贝大的类类型对象或者容器对象比较低效,甚至有的类类型不支持拷贝操作。所以通过可以引用形参来访问该类型的对象,如果不需要改变引用的对象,则可以把形参定义成对常量的引用。

如果函数无须改变引用形参的值,最好将其声明为常量的引用

通过使用引用形参可以返回额外信息

如果想让函数返回多个值,有以下两种方法:

1、引用形参为我们一次返回多个结果提供了有效的途径

2、定义一个新的数据类型,让它包含需要返回的成员。

const形参与实参

如果形参是const时,需要考虑关于顶层const的讨论,顶层const保证对象本身不变。

const int ci = 42;
// 顶层const,不能改变ci
int i = ci;
// 当拷贝ci时,会忽略它的顶层const
int *const p = &i;
// 顶层const,不能改变p
*p = 0;
// 可以通过p改变对象的内容是允许的

当实参书初始化形参时会忽略掉顶层const。

同时,当形参有顶层const时,传给它常量对象或者非常量对象都是可以的

void fcn(const int i) {}
// fcn可以读取i,但不能改变i
void fcn(int i) {}
// 这是就存在重复定义,因为函数1会 忽略顶层const,导致函数2与函数1可接收的实参类型可能相同

指针或引用形参与const

可以使用一个非常量初始化一个底层const对象

同时一个普通的引用必须用同类型的对象从初始化

允许常量引用绑定非常量的对象、字面值 、一般表达式

int i = 42;
const int *cp = &i;
// 正确、非常量初始化底层const
const int &r = i;
// 正确、常量引用绑定非常量对象
const int &r2 = 42;
// 正确、常量引用绑定字面值
int  *p = cp;
// 类型不匹配
int &r3 = r;
// 类型不匹配
int &r4 = 42;
// 不能用字面值初始化一个非常量引用
void reset(int &i);
void reset(int *ip);

int i = 0;
const int ci = i;
string::size_type  ctr = 0;

reset(i);
reset(&i);
// 正确

reset(&ci);
reset(ci);
reset(42);
reset(ctr);
// 错误、类型不匹配

应尽量使用常量引用

1、避免误导实参可修改

2、避免限制实参的类型

数组形参

因为数组存在两个性质:

1、不允许拷贝数组

2、使用数组时通常会将其转换成指针

通过性质1可知,无法以值传递的方式使用数组参数

通过性质2可知,当我们为函数传递一个数组时,实际上传递的是指向数组首元素的指针

以下三个函数等价。每个函数的形参类型都是const int*

void print(const int*);
void print(const int[]);
void print(const int[10]); 
// 这里的10表示我们期望数组含有多少元素,实际不一定

当给print函数传递一个数组时,实参自动地转换成指定数组首元素的指针,数组的大小对函数的调用没有影响。

以数组作为形参的函数也必须确保使用数组时不会越界

因为数组是以指针的形式传递给函数的,所以一开始函数并不知道数组的确切尺寸。

管理指针形参有三种常用的技术

1、使用标记指定数组的长度

void print(const char *cp) {
    if (cp)
        while (*cp)
            cout << *cp++;
}
// 处理c风格字符串时会遇到结束标记

使用标准库规范

void print(const int *beg, const int *end) {
    while (beg != end)
        cout << *beg++ << endl;
}
// 使用标准库
// begin()返回首元素指针
// end()返回尾后元素指针

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

void print(const int ia[], size_t size) {
    for (size_t i = 0; i < size; i++) 
        cout << ia[i] << endl;
}
// print(ia, end(ia) - begin(ia));

只要传递给函数的size值不超过数组实际的大小,函数就是安全的

数组形参与const

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

数组引用形参

形参可以是数组的引用。

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

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

由于数组的大小是构成数组类型的一部分,这样的引用数组无疑限制了函数的可用性。

传递多维数组

多维数组实际上是数组的数组

将多维数组传递给函数时,真正传递的是指向数组首元素的指针,因为我们处理的是数组的数组,所以首元素本身就是一个数组,指针就是一个指向数组的指针,数组第二维的大小都是数组类型的一部分,不鞥省略!

void print(int (*matrix)[10], int rowSize);
// matrix指向数组的首元素,该数组的元素由10个整数构成的数组
// matrix为指向含有10个整数的数组的指针

void print(int matrix[][10], int rowSize);
// matrix声明一个二维数组,实际上形参指向含有10个整数的数组的指针

int *matrix[10];
// 10个指针构成的数组
int (*matrix)[10];
// 指向含有10个整数的数组的指针

 含有可变形参的函数

为了编写能处理不同数量实参的函数,C++提供两种主要的方法

1、如果所有实参的类型相同,可以传递一个名为initializer_list的标准类型

2、如果实参的类型不同,我们可以编写一种特殊的函数,也就是 可变参数模板。

initializer_list形参

如果函数的实参未知但是全部实参的类型都相同,我们可以使用initializer_list类型的形参。用来表示某种特定 类型的值的数组。该类型是一个模板类。大部分操作与vector相同,但是不同之处在于initializer_list对象中的元素永远都是常量值。无法改变对象中元素的值

void error_meg(initializer_list<string> il) {
    for (auto beg = il.begin(); beg != il.end(); ++beg)
        cout << *beg << " ";
    cout << endl;
}
int list_elem(initializer_list<int> il)
{
    int sum = 0;
    for (auto it = il.begin(); it != il.end(); it++)
        sum += *it;
    return sum;
}
int main(int argc, char *argv[])
{
    cout << list_elem({ 1,2,3,4,5,6 }) << endl;
    return 0;
}
// output
21

省略符形参

省略符形参是为了便于C++程序访问某些特殊的C代码而设置的。类似于

void foo(parm_list, ...);
void foo(...);

第一种形式指定了foo函数的部分形参类型,对于这些形参的实参将会执行正常的类型检测,省略符形参所对应的实参无需类型检查

返回类型和return语句

return语句终止当前正在执行的函数并将控制权返回到调用该函数的地方。

无返回值函数

没有返回值的return语句只能用在返回类型是void的函数中,返回void的函数如果没有return语句,则函数的最后一句后会隐式执行return。

一个返回类型是void的函数也能使用return+表达式语句,不过此时表达式必须是一个返回void的函数。

void printStr()
{
    cout << "It's ok!!!" << endl;
}

void print()
{
    return printStr();
}
int main(int argc, char *argv[])
{
    print();
    return 0;
}
// output 
It's ok!!!

有返回值函数

只要函数的返回类型不是void,则该函数内每条return必须返回一个值。返回值的类型必须与函数的返回类型相同,或者能隐式转换成函数的返回类型

值是如何被返回的

返回的值用于初始化调用点的一个临时量,该临时量就是函数调用的结果。

如果函数返回一个值,返回值被拷贝到调用点、

如果函数返回一个引用,该引用仅是它所引对象的一个别名。

不要返回局部变量的指针或引用

函数完成后,它所占用的存储 空间也随之被释放掉。因此,函数终止意味着函数变量的引用将指向不再有效的区域。

如果我们想要确保返回值安全,我们需要引用哪些在函数之前就已经存在的对象

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

调用运算符()与点运算符.和箭头运算符->相同。并且符合左结合律。

引用返回左值 

函数的返回类型决定函数调用是否是左值。调用一个返回引用的函数得到左值,返回其它类型得到右值。

我们能为返回类型是非常量引用的函数的结果赋值

char& get_val(string &str, string::size_type ix)
{
    return str[ix];
}

int main(int argc, char *argv[])
{
    string s("a value");
    cout << s << endl;
    get_val(s, 0) = 'A';
    cout << s << endl;
    return 0;
}
// Output
a value
A value

 返回数组指针

如果想定义一个返回数组的指针,可以使用类型别名来重写数组名。

typedef int arrT[10];
using arrT = int[10];
arrT* func(int i);
// func返回一个指向含有10个整数的数组的指针

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

如果想在声明func时不使用类型别名,那么就要牢记被定义的名字后面数组的维度

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

返回数组指针的函数形式如下

Type (*function(parameter_list)) [dimension]

int (*func(int i))[10];

func(int i)表示调用func函数时需要一个int类型的实参

(*func(int i))意味着我们可以对函数调用的结果执行解引用操作

(*func(int i))[10]表示解引用func的调用将得到一个大小是10的数组。

int (*func(int i))[10]表示数组中的元素是int类型

尾置返回类型

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

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

func函数返回一个指针,该指针指向一个含有10个整数的数组

使用decltype

如果我们知道函数返回的指针将指向哪个数组,就可以使用decltype关键字声明返回类型。

例如,下面的函数返回一个指针,该指针根据参数i的不同指向两个已知数组的某一个

int odd[] = {1,3,5,7,9};
int even[] = {0,2,4,6,8};

decltype(odd) *arrPtr(int i)
{
    return (i % 2) ? &odd : &even;
}

decltype表示arrPtr返回类型是个指针,并且该指针所指的对象与odd类型一致,因为odd是数组,所以arrPtr返回一个指向含有5个整数的数组的指针。

decltype并不负责将数组类型转换成对应的指针,所以decltype结果是一个数组,要想表示arrPtr返回指针还必须在函数声明时加一个*符号。

// 声明一个函数,使其返回数组的引用并且该数组包含10个string对象
string (&func(string (&arrStr)[10]))[10];
// 类型别名
using arrS = string[10];
arrS& func1(arrS& arr);

// 尾置返回类型
auto func2(arrS& arr)->string(&)[10];

// decltype
string arrT[10];
decltype(arrT)& func3(arrS& arr);
// 修改arrPtr使其返回数组的引用
auto arrRef(int i)->int(&)[10];
// 实践
int odd[] = { 1,3,5,7,9 };
int even[] = { 0,2,4,6,8 };

decltype(odd) *arrPtr(int i)
{
    return (i % 2) ? &odd : &even;
}

auto arrRef(int i)->int(&)[5]
{
    return (i % 2) ? odd : even;
}

int main()
{
    int (*x)[5] = arrPtr(0);
    int (&y)[5] = arrRef(1);
    for (int i = 0; i != 5; i++)
        cout << (*x)[i] << " ";
    cout << endl;
    for (int i = 0; i != 5; i++)
        cout << y[i] << " ";
    cout << endl;
    return 0;
}
0 2 4 6 8
1 3 5 7 9
请按任意键继续. . .
原文地址:https://www.cnblogs.com/immjc/p/8076087.html