函数-Functions
说到函数,很多人觉得很简单,但如果问你重载函数的判别原理,函数返回指针的注意事项,指针函数的定义等等,很多人就头大了。函数是编程的基础,这块一定要打扎实。
函数基本知识
实参(Arguments)。形参(Parameters)。
形参初始化的顺序
尽管我们知道哪个实参初始化了哪个形参,但我们并不知道这个初始化的顺序!编译器可以以任意顺序初始化各个形参!因此,实参中不用包括改变自身的运算(如:++)。
初始化时的转换
如果我们定义了一个函数
int fact(int);
那么,在调用该函数时,实参一定要能转变成形参的type。
fact("hello");//error,string不能转成int
fact(3.14);//ok,float可以转成int
形参列表
在函数的最外层定义的变量,不能和形参重复。即:
int fact(int a)
{
int a;//error!
{
int a;//ok!
}
}
因为要使用的形参必须是有名字的,所以形参一般都会有名字。然而,如果我们在不断更新代码后,有的形参不再使用,那么我们将该形参设为没有名字的,以示区分。注意的是,即便这个形参没有名字,我们还是要通过实参给其赋值!
函数返回值
一个函数的返回值不能是数组或是函数,但是我们可以返回一个指针指向数组,或是指针指向函数。
局部变量
局部变量主要有两种,一种是automatic objects,另一种是local static objects。
automatic objects
automatic objects在函数调用时生成,在函数结束时便会消亡(每一次重新调用该函数,这个object的值不会被保留)。所有传递来的参数都是automatic objects。
local static objects
而local static objects则不同,它在函数第一次执行时,还未执行到定义该object的语句之前就被初始化,它的值在每一次调用函数时都会被保留。其定义形式为:
static type name;//ex: static int b;可以只声明不初始化,也可以直接初始化
参数传递
参数传递有两种:值传递(passing argument by value)和引用传递(passing argument by reference)。
如果函数定义时,形参是一个reference,那么参数传递便是引用传递;否则,是值传递。
简单地说,引用传递下,函数内对该参数的改变,在函数返回时依然有效,因为函数内改变的是一个对原参数的引用,它和原参数指向同一个内存地址;而值传递下,函数内对参数的改变,在函数返回后不会影响原参数,因为值传递是copy了一个一模一样的备份,然后对该备份进行运算,这个备份和原来的参数的内存地址是不同的。
必须使用值传递的情况
一些类(包括IO 类),不能被复制(cannot be copied)。这种情况下,函数必须使用引用传递!例如:
void ioNotBeCopied(ifstream fin);
void ioNotBeCopied2(ifstream &fin);
void main()
{
ifstream fin;
ioNotBeCopied(fin);//error!
ioNotBeCopied2(fin);//ok!
}
事实上IO类的三个头文件:iostream,fstream,sstream定义的类都是不能复制的,不能直接作为函数参数传递,也不能存储在vector中。
Const 参数
top-level const
当我们初始化一个形参时,top-level const会被忽略(回顾top-level const:object本身不能被改变)。
对于一个top-level const的形参,我们可以用nonconst或top-level const object来初始化它。
对于一个nonconst形参,我们也可以用nonconst或top-level const object来初始化它。
注意:这里的传递都是值传递,而非引用传递。
Pointer or Reference Parameters and const
回顾 low-level const:object指向或引用的对象不能被改变。
在pointer和reference中涉及const时,记住两个原则:
1.我们能用nonconst初始化一个low-level const,但反之不行。
2.对于plain reference(即nonconst reference,别忘了reference只存在low-level const,不存在top-level const),必须使用同样的类型(nonconst)来初始化。(这也解释了上一部分top-level const的原则只适用于值传递)。
一些例子:
void reset(int &a);
void creset(const int &a);
int c =0;
const int cc = 0;
reset(&c);//error!非常量引用的初始值必须为左值
creset(&cc);//error! const int*和const int&不兼容
reset(42);//error! 不能用plain reference绑定literal
creset(42);//ok!
这里对“非常量引用的初始值必须为左值”做一点说明,这句话的意思是int &a是一个非常量引用,所以它的初始值一定要是一个左值(lvalue),毕竟nonconst reference只能绑定object。而&c其实是c 的地址,是一个右值(rvalue),你无法对&c再次取地址。因此出现了error。
尽可能使用const reference(Use Reference to Const When Possible)
这样做有两个好处:
1.明确告诉使用函数的人,哪些变量可能会被改变,哪些不会。
2.plain reference比const reference有更多的限制,如不能绑定数字(literal),不能绑定一个const object。
数组参数(Array Parameters)
我们不能将数组copy给一个函数(数组没有拷贝构造函数),当我们使用数组时,它经常会被转换成指针。如果我们向函数传递一个数组,实际上我们传递的是指向数组第一个元素的指针。
下面的三个声明都是完全一样的:
void print (const int*);
void print (const int[]);
void print (const int[10]);
而当编译器检查时,它只会检查实参是否是一个指针:
int i=0;
print(&i);//ok!
确保数组传递的正确性
正是因为编译器只会检查是否是指针,因此传递数组很容易出错,例如我们希望得到一个int[10],而事实上传递的是一个int[5],那么在遍历数组时就会越界!C++ Primer提供了三种方法,来避免这种错误。
1.用一个标记(marker)来表示数组的结束
在C-style string中,我们知道string的结尾是一个'\0‘,当函数读到'\0'时,我们就知道数组结束了。这种方法对于有明显的结尾标记(end marker)的情况很实用。但是对于int类型往往就不那么好用了,因为任何值都是有可能的。
2.传递头元素(first element)和结尾元素(one past the last element)
借助begin()和end()函数,我们可以轻松地获得数组的起始和截止指针,将这两个指针传递到函数内,则遍历就不会出错。例如:
void print(const int*beg,const int *end)
{
while(beg!=end)
cout<< *beg++ <<endl;
}
int j[2] = {0,1};
print(begin(j),end(j));
3.传递数组同时传递数组大小
这个思想非常容易想到。值得注意的是,数组的大小一般可以用size_t来表示。
void print(const int ia[],size_t size);
Array Parameters and const
和之前提到的reference尽量声明成const一样,array和pointer都应该尽量声明成const,除非需要改变其中的值。
Array Reference(对array的引用)
我们可以定义一个reference,其指向一个array,作为参数。这样做的好处是,我们可以用range for轻松地遍历数组:
void print(int (&arr)[10])
{
for(auto elem:arr)
cout<<elem<<endl;
}
注意:这里的(&arr)两侧的括号是很有必要的!如果没有括号,则是一个由10个reference构成的array。
但是,这样做的缺点是,我们在初始化形参时,必须明确传递一个int[10]:
int j[2] = {0,1};
int k[10];
print(j);//error!
print(k);//ok!
事实上,我们也有办法传递任意大小的数组,这将在很以后学到。
Passing a Multidimensional Array
传递多维数组(本质是有数组构成的数组)时,第二维即以后的数组大小必须被明确定义。
void print(int (*matrix) [10]);//()不能省略!
void print(matrix[][10]);//equivalent defination
命令行参数处理
我们知道,main函数其实可以接受命令行的参数,基本形式如下:
int main(int argc,char *argv[]);
int main(int argc,char **argv);//equivalent
argc表示包括函数名和参数在内的总个数,argv则是包括了函数名和参数在内的具体内容,最后以0结尾。举例说明:
program -d
argc = 2;
argv[0]="program";
argv[1] = "-d";
argv[2] = 0;
不知道参数个数的函数(Functions with Varying Parameters)
如果我们事先不确定具体的函数参数个数,那么有两种主要方法来解决。
1.如果参数的类型都相同,那么可以使用initializer_list类来完成。
2.如果参数的类型也不同,那么我们用一种叫做variadic模板的特殊函数来完成(将在很以后介绍)。
另外,有一种参数类型叫做ellipsis(省略)也能完成这一功能,但只有当我们的程序需要和C语言兼容时我们才应该使用它,即在C++中不推荐使用ellipsis参数!
initializer_list参数
initializer_list<T> lst;//empty list of elements of type T
initializer_list<T> lst{a,b,c...};//elements are copies of the corresponding initializer!!!elements are const!!!
lst2(lst);//copy
lst2 = lst;//assign. Warning:copy or assign an initializer_list does not copy the elements in the list! The original and the copy share the elements!
lst.size();
lst.begin();
lst.end();
注意的是,initializer_list内的元素都是const,无法改变。void error_msg(initializer_list<string>il);
//expected,actual are strings
error_msg({"functionX",expected,actual});//ok!
其实,我没有完全想明白initializer_list和vector的差别。initializer_list全是const,这一点vector可以很容易做到。唯一的差别是,initializer_list的copy和assign都是相当于别名(alias),这样的好处是什么呢?疑惑。
我想,可能的解释是:initializer_list的构造只能通过{},这就限制了它的使用范围。正如它的名字的意思,这就是用来初始化一个函数的特殊类,用vector也能实现,但是用initializer_list实现感觉分工更明确。
ellipsis parameters
void foo(parm_list, ...);
具体如何使用我并不清楚,大家可以自己上网学习,但如果是C++程序员,ellipsis parameter不那么重要。
Function Return Types
Never Return a Reference or Pointer to a Local Object
const string& manip()
{
string ret;
if(!ret.empty())
return ret;//WRONG!ret是一个local object
else
return "EMPTY";//WRONG!"EMPTY"也是一个local object。当然,如果函数返回的是const string,而非const string&,那么这个是可以的
}
Reference Returns Are Lvalues
函数的返回类型,只有当是reference时是lvalue,其它情况都是rvalue。
如果函数的返回参数是一个reference(当然该reference不指向local object),那么我们可以把这个返回参数当做一般的lvalue使用。
char& getValue(string &str,int ix)
{
return str[ix];
}
int main()
{
string s("a value");
get_val(s,0) = 'A';
}
List Initializing the Return Value(使用list initializer来定义返回变量)
这个很好理解,当我们的return type是vector等类型时,我们可以使用list initializer的形式定义返回变量。
vector<int> process()
{
return {1,2,3,4};
}
注意的是:如果返回的是一个built-int type(如int,float等),那么{}中只能有一个值,且这个值的类型和返回类型必须完全一样,不能有任何转换。而如果返回一个类,那么{}内的内容由该类决定。返回数组的指针(Return a Pointer to an Array)
因为数组不能被复制,因此我们无法直接返回一个数组。然而,我们可以返回一个指向数组的指针。
要返回这样的一个指针,有四种办法:
1.用type alias
typedef int arrT[10];
using arrT = int[10];//equivalent
arrT* func();//func()返回一个指向int[10]的指针
2.直接定义
基本格式是:
Type (*function(parameter_list))[dimension]
一个例子:
int (*func())[10];//()不能省略!和上面的func等价
3.使用Trailing Return Type(拖尾返回类型)
C++ 11新特性,trailing return type可以定义任何函数,但是在函数的返回类型很复杂时尤其有用。
基本格式是:
auto function(parameter_list) -> return_type
一个例子:
auto func() -> int(*)[10];//和上面的func等价
4.使用decltype
int odd[] = {1,3,5,7,9};
decltype(odd) *func();//和上面的func不同,返回一个int(*)[5]。decltype(odd)得到的是一个int[5]
返回数组的引用(Return a Reference to an Array)
和上一节完全一样,四种方法可以分别使用到数组的引用上。
函数重载
重载函数必须在参数的个数,或者类型上有所区别!如果两个函数仅仅是返回类型不同,那么重载这样两个函数将是错误的。
Overloading and const Parameters
void lookup(phone);
void lookup(const phone);//error!redeclaration of lookup(phone)
void lookup(phone*);
void lookup(phone* const);//error!redeclaration of lookup(phone*);
另一方面,如果两个函数是low-level const的差别,那么就可以重载。void lookup(account&);
void lookup(const account&);//new function
void lookup(account*);
void lookup(const account*);//new function
const_cast and Overloading
const string& shorterString(const string &s1,const string &s2)
{
return s1.size()<=s2.size()?s1:s2;
}
这时,如果我们向函数传递两个nonconst string,那么得到的结果依旧是const string。现在,我们希望在这种情况下,能得到一个nonconst string,除了重新写一个新的函数外,我们可以这么做:string& shorterString(string&s1,string&s2)
{
auto &r = shorterString(const_cast<const string&>(s1),
const_cast<const string&>(s2));
return const_cast<string&>(r);
}
这里auto &r中的&的作用是保证返回的内容就是s1或s2中的一个,而不是重新创造的string。Calling an Overloaded Function
Overloading and Scope
string read();
void print(const string &);
void print(double);
void foo(int ival)
{
string s = read();//ok!
bool read = false;//hides the outer declaration of read
string s = read();//error!read is a bool variable,not a function
void print(int);//hides previous instances of print
print("Value: ");//error!
print(ival);//ok!print(int) is visible
print(3.14);//ok!calls print(int);print(double) is hidden
}
缺省参数(Default Arguments)
Default Argument Declaration
<pre name="code" class="cpp">string screen(int,int,char=' ');//ok
string screen (int,int,char='*');//error! redeclaration
string screen (int = 24,int =80,char);//ok! adds default arguments,现在两个screen的缺省值是一样的!都是int=24,int=80,char=' '
string screen(int =24,int=80,char=' ');//error! 最后一个char不能再次定义!
注意:我在VS2013测试时,输入如下:string screen(int, int=20);
string screen(int = 10, int);
在编辑器内,第二行的screen下面会有红色波浪线,显示“错误:默认实参不在形参列表的结尾”,但是编译过程没有报错,也可以正常运行。诡异。。Default Argument Initializers
int a=1;
int b();
string screen(int =a,int =b());//现在的默认调用是screen(1,b());
void func()
{
a=2;
screen();//调用screen(2,b());
}
Inline和constexpr函数
inline Functions
const string& shorterString(const string &s1,const string &s2)
{
return s1.size()<=s2.size()?s1:s2;
}
定义这种短小的函数有如下四个好处:inline Functions Avoid Function Call Overhead
inline const string& shorterString(const string &s1,const string &s2)
{
return s1.size()<=s2.size()?s1:s2;
}
应该注意的是:inline函数只应该用来定义那些简短小巧的代码。inline只是一个request(申请),编译器可以选择忽略这个申请。大部分编译器对递归函数的inline将忽略,对于超过75行的函数将几乎肯定被忽略。constexpr Functions
constexpr int scale(int cnt){return 3*cnt;}//只要cnt是一个const,那么scale返回的便是一个常量
再看个列子:int arr[scale(2)];//ok!scale(2)是常表达式
int i=2; //i is not a constant expression
int a2[scale(i)];//error!
最后要说的是!constexpr目前还没有被Visual Studio所接受!晕,具体原因涉及到编译器的实现细节,和类模板似乎有一定关系。我也不是很懂,大家可以参考这篇文章:Put inline and constexpr Functions in Header Files
Aids for Debugging
the assert Preprocessor Macro
assert(expression);
如果语句运算的结果为真,那么assert不做任何动作;如果结果为假,那么将中断程序,在终端上会显示debug结果(在哪个具体位置遇到了assert失败的情况)。the NDEBUG Preprocessor Variable
$ CC -D NDEBUG main.C #linux
$ CC /D NDEBUG main.C #microsoft
配合NDEBUG,我们还可以实现比assert更复杂的debug功能。例如:void print()
{
#ifndef NDEBUG
cerr<<__FILE__<<endl;
#endif
}
即通过宏定义来控制cerr<<__FILE__<<endl这段代码是否执行。__FILE__:string, name of file
__LINE__:int, current line number
__TIME__:string,time the file was compiled
__DATE__:string,date the file was compiled
Function Matching
void f();
void f(int);
void f(int,int);
void f(double,double=3.14);
f(5.6);//calls void f(double,double)
Candidate and Viable Functions
寻找最佳匹配函数
Argument Type Conversions
Matches Requiring Promotion or Arithmetic Conversion
void ff(int);
void ff(long);
void ff(short);
void ff(long long);
ff('a');//调用ff(int)
另一方面,如果没有int类型,可能就无法判断。void ff(long);
void ff(short);
void ff(long long);
ff('a');//报错!无法判断
所有的算数转换都是一样的!从int到unsigned int不比从int到double来的优先级更高。另外,literal中,整数一般默认是int,浮点数默认是double。
void manip(int);
void manip(float);
manip(3.14);//error!无法判断
一个特殊的例子:void ff(int);
void ff(long long);
ff(999999999999999);//调用ff(long long)
Function Matching and Const Arguments
void lookup(account&);
void lookup(const account&);
const account a;
account b;
lookup(a);//调用lookup(const account&)
lookup(b);//调用lookup(account&);
函数指针(Pointers to Functions)
Define a Function Pointer
bool lengthCompare(const string&,const string&);//该函数的类型是bool(const string&,const string&)
bool (*pf)(const string&,const string&);//定义的函数指针
注意定义函数指针的()是非常必要的!否则你就定义了一个函数!Using a Function Pointer
pf = lengthCompare;
pf = &lengthCompare;//equivalent!
我们也可以直接用函数指针来调用该函数,具体的:bool b1 = pf("hello","goodbye");
bool b2 = (*pf)("hello","goodbye");//equivalent!
bool b3 = lengthCompare("hello","goodbye");//equivalent!
任意两个不同的function type之间不存在转换方法(废话么。。。),当然我们可以对任何function pointer赋值nullptr或0,来表示他们没有指向任何函数。Function Pointer Parameter
void f(int,bool pf(int));//ok!第二个参数被认为是函数指针
void f(int,bool (*pf)(int));//equivalent!显式表明第二个参数是函数指针
使用type aliases和decltype能帮我们简化代码。bool lengthCompare(int);
//Func,Func2是函数类型
typedef bool Func(int);
type decltype(lengthCompare) Func2;//equivalent
//FuncP,FuncP2是函数指针类型
type bool (*FuncP)(int);
type decltype(lengthCompare) *FuncP2;//equivalent
void f(int,Func);//这四个定义和之前的f()完全一样
void f(int,Func2);
void f(int,FuncP);
void f(int,FuncP2);
Returning a Pointer to Function
using F = int(int*,int);//F是函数类型,不是指针
using PF = int(*)(int*,int);//PF函数指针类型
记住和参数列表中不同,参数列表中一个函数类型会被自动转换成函数指针类型,但是返回类型却不会!我们必须使用一个显式的函数指针类型来定义函数的返回类型。PF f(int);//ok
F f(int);//error!
F* f(int);//ok!
当然,我们也可以显式地定义这样的函数。int (*f(int)) (int*,int);
另外,也可以用拖尾返回类型(trailing return)来定义。auto f(int) ->int(*)(int*,int);
Using auto or decltype for Function Pointer Types
int sumLength(string,string);
decltype(sumLength) *f(int);
需要注意的是,decltype返回的是函数类型,所以我们需要在函数名前加一个*来表示返回函数指针,上面例子中如果缺少了*,则会报错。auto f(int) -> decltype(sumLength)*;
同样,这里最后的*也不能省略。