函数基础
1、调用函数: 函数的调用主要完成两项工作,一是用实参初始化函数对应的形参,二是将控制权转移给被调用函数。当遇到一条return语句时,和函数一样return语句也完成两项工作,一是返回return语句中的值(如果有的话),二是将控制权从被调函数转移回主调函数。
2、形参和实参: 尽管实参和形参存在对应关系,但是并没有规定实参的求值顺序,编译器能以任意可行的顺序对实参求值。
3、函数返回类型: 函数的返回类型不能是数组类型或函数类型,但可以是指向数组或函数的指针。
4、自动对象: 对于普通局部变量对应的对象来说,当函数的控制路径经过变量定义语句时创建该对象,当到达定义所在的块末尾时就销毁它。我们把只存在于块执行期间的对象称为自动对象
。当块的执行结束后,块中创建的自动对象的值就变成未定义的了。
形参是一种自动对象,函数开始时为形参申请存储空间,因为形参定义在函数体作用域之内,所以一旦函数终止,形参也就被销毁。
5、局部静态对象: 局部静态对象仅在程序的执行路径第一次经过对象定义语句时初始化,并且直到程序终止才被销毁,在此期间即使对象所在的函数结束执行也不会对它有影响:
#include<iostream>
using namespace std;
inline unsigned myCnt();
int main(){
for(int i=0; i<=10; ++i)
cout<<myCnt()<<" "; // 结果:0 1 2 3 4 5 6 7 8 9 10
return 0;
}
unsigned myCnt(){
static unsigned iCnt = -1; //静态变量只分配一次内存(仅第一次初始化有效)
++iCnt;
return iCnt;
}
全局变量和静态全局变量的区别: 全局变量具有外部连续性,即同一工程中其他文件中的也可引用。而静态全局变量不具有外部连续性,即同一工程中其他文件中不可以引用:
//cpp1.cpp
extern int x=0;
static int y=2;
//cpp2.cpp
#include<iostream>
using namespace std;
int main(){
extern int x;
extern int y;
cout<<x<<endl;
cout<<y<<endl;
return 0;
}
将上面两个文件放在同一工程中,会发现其实每个文件单独编译能通过,但作为工程不能构成.exe文件运行。若将变量y的行注释后(或将static换成extern)就可以了。这是因为静态全局变量的作用域只在本文件内,不能扩充到其他文件。其作用就是当多人合作开发一个工程的时候,仅在自己的文件内只用的全局变量换成静态全局变量这样就不会与其他人用的变量混淆,这是标识符的一致性。
变量可以分为全局变量、静态全局变量、静态局部变量和局部变量:
按存储区域划分: 全局变量、静态全局变量和静态局部变量都存放在内存的全局数据区,局部变量存放在内存的栈区。
按作用域分: 全局变量在整个工程内都有效;静态全局变量只在定义它的文件内有效;静态局部变量只在定义它的函数内有效,只是程序仅分配一次内存 (仅第一次初始化有用,后面的重复初始化将失效),函数返回后,该变量不会消失;局部变量在定义它的函数内有效,但是函数返回后失效。
注意: (1)全局变量和静态变量如果没有手动初始化,则由编译器初始化为0,局部变量的值不可知。(2)在c++中,名字有作用域
,对象有生命周期
理解这两个概念非常重要。
参数传递
1、指针形参: 指针的行为和其他非引用类型一样。当执行指针拷贝操作时,拷贝的是指针的值。拷贝之后两个指针是不同的指针:
//该函数接受一个指针,然后将指针所指的值置为0
void reset(int *p){
*p = 0; //改变指针p所指对象的值
p = 0; //只改变了p的局部拷贝,实参未被改变!
}
2、使用引用避免拷贝: 拷贝大的类类型对象或者容器对象比较低效,甚至有的类类型(包括IO类型在内)根本就不支持拷贝操作。当某种类型不支持拷贝操作时,函数只能通过引用形参访问该类型的对象。
当参数是较大的数据结构类型的时,比如void fun(vector< int> a),假如我们不需要改变原变量中的值,那么我们为什么要使用void fun(vector< int> &a),而不使用void fun(vector< int> a)呢,因为void fun(vector< int> a)需要对原变量进行一次复制的操作,即使两个变量名是相同的由于作用域不同,所以其实是两个变量,需要一次复制操作,因此数据大小较大时是非常浪费时间的,那么自然的就引出了这样的问题,我们使用void fun(vector< int> &a),不需要进行复制操作,但是不小心在函数中改变了参数的值不就得不偿失了。所以就有了void fun( const vector< int> &a),这样的结构,我们都知道const关键字定义的变量是不可以被改变的,所以当我们进行常量引用时既不会进行复制操作,当误操作时又不能编译通过,两全其美。
参考: http://blog.csdn.net/hk_john/article/details/72459549
3、使用引用形参返回额外信息: 一个函数只能返回一个值,然而有时函数需要同时返回多个值,引用形参为我们返回多个结果提供了有效的途经。
4、const形参和实参: 顶层const作用于对象本身,和其他初始化过程一样,当用实参初始化形参时会忽略掉顶层const。换句话说,形参的顶层const被忽略掉了。当形参有顶层const时,传给它的常量对象或者非常量对象都是可以的:
const int ci=42; //不能改变ci,const是顶层的
int i=ci; //正确:当拷贝ci时,忽略了它的顶层const
int *const p=&i; //const是顶层的,不能给p赋值
*p = 0; //正确:通过p改变对象的内容是允许的,现在i变成了0
void fcn(const int i){ /* fcn能够读取i,但是不能向i写值*/}
忽略掉形参的顶层const可能会产生意想不到的结果:
void fcn(const int i){ /* fcn能够读取i,但是不能向i写值*/}
void fcn(int i){ /* fcn也可以读取顶层const变量 */} //错误:重复定义了fcn(int)
注意: c++允许我们用字面值
来初始化常量引用,对于函数不会改变的形参应该尽量使用常量引用
。
5、数组形参: 数组的两个特殊性质
对我们定义和使用作用在数组上的函数有影响,这两个性质分别是:不允许拷贝数组以及使用数组时(通常)会将其转换成指针。因为不能拷贝数组,所以我们无法以值传递的方式使用数组参数。因为数组会被转换成指针,所以当我们函数传递一个数组时,实际上传递的是指向数组首元素的指针,这个时候是可以对数组内的元素进行修改的!
注意: 当函数不需要对数组元素执行写操作的时候,数组形参应该是指向const的指针。
6、管理数组实参:
- 使用标记指定数组长度:要求数组本身包含一个结束标记,使用这种方法的典型示例是C风格字符串,在最后一个后面跟着一个空字符' '。
- 使用标准库规范:传递指向数组首元素和尾后元素的指针,这种方法受到了标准库技术的启发。
- 显示传递一个表示数组大小的形参:专门定义一个表示数组大小的形参,在C程序和过去的C++程序中常常使用这种方法。
7、数组引用形参: C++语言允许将变量定义成数组的引用,基于同样的道理,形参也可以是数组的引用。此时,引用形参绑定到对应的实参上,也是绑定到了数组上:
//正确:形参是数组的引用,维度是类型的一部分
void print(int (&arr)[10]){
for(auto elem : arr)
cout<<elem<<endl;
}
//&arr两端的括号必不可少
void f(int &arr[10]) //错误: 将arr声明成了引用的数组
void f(int (&arr)[10]) //正确: arr是具有10个整数的整型数组的引用
8、传递多维数组: 和所有数组一样,当将多维数组传递给函数时,真正传递的是指向数组首元素的指针。因为我们处理的是数组的数组,所以首元素本身就是一个数组,指针就是一个指向数组的指针。数组第二排(以及后面所有维度)的大小都是数组类型的一部分,不能省略:
//matrix指向数组的首元素,该数组的元素是由10个整数构成的数组
void print(int (*matrix)[10], int rowSize){/*.....*/}
//*matrix两端的括号必不可少
int *matrix[10]; //10个指针构成的数组
int (*matrix)[10] //指向含有10个整数的数组的指针
9、含有可变形参的函数: 为了编写能处理不同数量实参的函数,C++11新标准
提供了两种主要的方法:如果所有的实参类型相同,可以传递一个名为initializer_list
的新标准库类型;如果实参的类型不同,我们可以编写一种特殊的函数,也就是所谓的可变参数模板
。
initializer_list是一种标准库类型,用于表示某种特定类型的值的数组,定义在同名的头文件中,它提供了如下几种操作:
- initializer_list<T> lst; 默认初始化,T类型元素的空列表
- initializer_list< T> lst{a, b, c...}; lst的元素数量和初始值一样多;lst的元素是对应初始值的副本,列表中的元素是const。
- lst2(lst); 拷贝或赋值一个initializer_list对象不会拷贝列表中的元素,拷贝后,
lst2=lst; 原始列表和副本共享元素。- lst.size(); 列表中的元素数量
- lst.begin() ; 返回指向lst中首元素的指针
- lst.end(); 返回指向lst中
尾元素下一位置
的指针
和vector一样,initializer_list也是一种模板类型,定义对象时,必须说明列表中所含元素的类型。不过和vector不一样的是,initializer_list对象中的元素永远是常量值 ,我们无法改变initializer_list对象中元素的值。
#include<iostream>
using namespace std;
int iCount(initializer_list<int> il){
int count = 0;
//遍历il的每一个元素
for(auto val : il)
count+=val;
return count;
}
int main(){
//使用列表初始化的方式构建initializer_list<int> 对象
cout<<"1,6,9的和是:"<< iCount({1, 6, 9})<<endl;
cout<<"4,5,9,11,12的和是:"<< iCount({4, 5, 9, 11, 12})<<endl;
return 0;
}
返回类型
1、值是如何被返回的: 返回一个值的方式和初始化一个变量或形参方式完全一样:返回的值用于初始化调用点
的一个临时量,该临时量就是函数调用的结果。
2、不要返回局部对象的引用或指针: 函数完成后,它所占用的存储空间也随之被释放掉,因此,函数终止意味着局部变量的引用将指向不再有效的内存区域:
//严重错误:这个函数调用试图返回局部对象的引用
const string &manip(){
string ret;
//以某种方式改变以下ret
if(!ret.empty())
return ret; //错误:返回局部对象的引用!
else
return "Empty"; //错误: "Empty"是一个局部临时变量
}
注意: 如果想要确保返回值安全,我们可以不妨提问:引用所引的是在函数之前已经存在的哪个对象?
3、引用返回左值: 函数的返回类型决定函数调用是否是左值,调用一个返回引用的函数得到左值
,其他返回类型得到右值。可以像使用其他左值那样来使用返回引用的函数的调用,特别是,我们能为返回类型是非常量引用的函数的结果赋值:
char &get_val(string &str, string::size_type ix){
return str[ix]; //get_val假定索引值是有效的
}
int main(){
string s("a value");
cout<<s<<endl;
get_val(s, 0)='A'; //将s[0]的值改为A
cout<<s<<endl;
return 0;
}
4、列表初始化返回值: C++11新标准
规定,函数可以返回花括号包围的列表。类似于其他返回结果,此处的列表也用来对表示函数返回的临时量
进行初始化
。如果列表为空,临时量执行值初始化,否则,返回的值有函数的返回类型决定:
vector<string> process(){
//...
//expected和actual是string对象
if(expected.empty())
return {}; //返回一个空vector对象
else if(expected == actual)
return {"functionX", "okay"}; //返回列表初始化的vector对象
else
return {"functionX", expected, actual};
}
如果函数返回的是内置类型
,则花括号包围的列表最多包含一个值,而且该值所占空间不应该大于目标类型的空间。如果函数返回的是类类型
,由类本身定义初始化如何使用。
5、递归: 在递归函数中,一定有某条路径是不包含递归调用的;否则,函数将“永远”递归下去,换句话说,函数将不断地调用它自身直到程序栈空间耗尽为止:
//计算val的阶乘,即1*2*3*.............*val
int factorial(int val){
if(val>1)
return factorial(val-1)*val;
return 1;
}
6、返回数组指针: 因为数组不能被拷贝,所以函数不能返回数组,不过,函数可以返回数组的指针或引用。虽然从语法上来说,要想定义一个返回数组的指针或引用的函数比较繁琐,但是有一些方法可以简化这一任务,其中最直接的方法是使用类型别名
:
typedef int arrT[10]; //arrT是一个类型别名,它表示的类型是含有10个整数的数组
using arrT =int[10]; //arrT的等价声明
arrT* func(int i); //func返回一个指向含有10个整数的数组的指针
- 声明一个返回数组指针的函数:要想在声明func时不使用类型别名,我们必须牢记被定义的函数后面数组的维度:
int arr[10]; //arr是一个含有10个整数的数组
int *p1[10]; //p1是一个含有10个指针的数组
int (*p2)[10] = &arr; //p2是一个指针,它指向含有10个整数的数组
和这些声明一样,如果我们想定义一个返回数组指针的函数,则数组的维度必须跟在函数名字之后,然而,
函数的形参也跟在函数名字后面且形参列表应该先于数组的维度。返回数组指针的函数形式如下所示:
Type (*function(parameter_list)) [dimension]
- 使用尾置返回类型: 在
C++11新标准
中还有一种可以简化上述func声明的方法,就是使用尾置返回类型。任何函数的定义都能使用尾置返回,但是这种形式对于返回类型比较复杂的函数最有效,比如返回类型是数组的指针或数组的引用:
//func接受一个int类型的实参,返回一个指针,该指针指向含有10个整数的数组
auto func(int i) -> int(*) [10];
- 使用decltype:还有一种情况,如果我们知道函数返回的指针指向哪个数组,就可以使用decltype关键字声明返回类型:
int odd[] = {1, 3, 5, 7, 9};
int even[] = {0, 2, 4, 6, 8};
//返回一个指针,该指针指向含有5个整数的数组
decltype(odd) *arrPtr(int i){
return (i % 2) ? &odd : &even; //返回一个指向数组的指针
}
注意: decltype并不负责把数组类型转换成对应的指针,所以decltype的结果是个数组,想要表示arrPtr返回指针还必须在函数声明时加一个*符号。
函数重载
1、定义重载函数: 不允许两个函数除了返回类型外其他所有的要素都相同。假设有两个函数,它们的形参列表一样但是返回类型不同,则第二个函数的声明是错误的:
Record lookup(const Account&);
bool lookup(const Account&); //错误:与上一个函数相比只有返回类型不同
2、重载和const形参: 一个拥有顶层const的形参无法和另一个没有顶层const的形参区分开来:
Record lookup(Phone);
Record lookup(const Phone); //错误:当用实参初始化形参时会忽略掉顶层const,所以重复声明了。
Record lookup(Phone*);
Record lookup(Phone* const); //错误:重复声明了Record lookup(Phone*);
另一方面,如果形参是某种类型的指针或引用,则通过区分其指向的是常量对象还是非常量对象可以实现函数重载,此时的const是底层的:
//对于接受引用或指针的函数来说,对象是常量还是非常量对应的形参不同
//定义了4 个独立的重载函数
Record lookup(Account&); //函数作用于Account 的引用
Record lookup (const Account&); //新函数.作用于常量引用
Record lookup(Account* ); //新函数,作用于指向Account 的指针
Record lookup(const Account*); //新函数,作用于指向常量的指针
注意: 当我们传递一个非常量对象或者指向非常量对象的指针时,编译器会优先选用非常量版本的函数。
3、重载和作用域: 一般来说将函数声明置于作用域内不是一个明智的选择。在C++中,名字查找发生在类型检查之前。
特殊用途语言特性
1、默认参数: 我们可以为一个或多个形参定义默认值,不过需要注意的是,一旦某个形参被赋予了默认值,它后面的所有形参都必须有默认值。因此,当设计含有默认实参的函数时,其中一项任务是合理设置形参的顺序,尽量让不怎么使用默认值的形参出现在前面,而让那些经常使用默认值的形参出现在后面。
-
默认实参声明: 对于函数的声明来说,通常的习惯是将其放在头文件中,并且一个函数只声明一次,但是多次声明同一个函数也是合法的迈不过有一点需要注意,在给定的作用域中一个形参只能被赋予一次默认实参。换句话说,函数的后续声明只能为之前那些没有默认值的形参添加默认实参,而且该形参右侧的所有形参必须都有默认值:
//表示高度和宽度的形参没有默认值 string screen(sz,sz,char = ' '); //我们不能修改一个已经存在的默认值: string screen(sz,sz,char = '*'); //但是可以按照如下形式添加默认实参 string screen(sz=24,sz=80,chsr);
-
默认实参初始值: 局部变量不能作为默认参数。除此之外,只要表达式的类型能转换成形参所需的类型该表达式就能作为默认参数:
//wd 、def 和ht的声明必须出现在函数之外
sz wd = 80;
char def = ' ';
sz ht();
string screen(sz = ht() , sz = wd , char = def) ;
string window = screen (); // 调用screen (ht() , 80,' ' )
void f2 (){
def = ' * '; //改变默认实参的位
sz wd = 100; //隐藏了外层定义的wd. 但是没有改变默认位
window = screen (); //调用screen (ht() , 80 ,' * ' )
}
2、内联函数和constexpr函数:
- 内联函数可避免函数调用的开销: 内联说明只是向编译器发出的一个请求,编译器可以选择忽略这个请求
//内联版本:寻找两个string对象中较短的那个
inline const string &shorterStrung(const string &s1,const string &s2){
return s1.size()<=s2.size() ? s1 : s2;
}
- constexpr函数: 指能用于常量表达式的函数。定义constexpr函数的方法与其他函数类似,不过要遵循几项约定:函数的返回类型及所有形参的类型都得是
字面值类型
,而且函数体中必须有且只有一条return语句:
constexpr int new_sz() {return 42;}
constexpr int foo=new_sz(); //正确:foo是一个常量表达式
执行该初始化任务时,编译器把对constexpr函数的调用替换成其结果值。为了能在编译过程中随时展开,constexpr函数被隐式地指定为内联函数。
-
把内联函数和constexpr函数放在头文件内: 和其他函数不一样,内联函数和constexpr函数可以在程序中多次定义。不过,对于某个给定的内联函数或者constexpr函数来说,它的多个定义必须完全一致,内联函数和constexpr函数通常定义在头文件中。
-
把类的成员函数定义成内联函数的途经: 主要有两种途经,一种是直接把函数定义放在类的内部,第二种是把函数定义在类的外部,并且在定义之前显式地指定inline。