函数基础
一个典型的函数定义包括以下部分:返回类型、函数名字、由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 请按任意键继续. . .