1、函数的参数传递
形参的初始化与变量的初始化一样。
如果形参具有非引用类型,则复制实参的值 实现初始化。函数并没有访问调用所传递的实参本身,因此不会改变实参的值。
如果形参为引用类型,则它只是实参的别名,这时候形参改变,对应的引用实参也会改变;
如果形参为指针类型,则它可以改变指针指向的值。
2、复制实参的局限性
不适宜复制实参的情况包括:
1)当函数需要修改实参的值时;
2)当需要以大型对象作为实参传递时。对实际应用而言,复制对象所付出的时间和存储空间代价往往过大。
3)当没有办法实现对象的复制时。
3、利用const引用避免复制
1)如果函数具有普通的非const引用形参,则显然不能通过const对象进行调用。因为此时函数可以修改传递来的对象,这违背了实参的const特性。
2)如果使用引用形参的唯一目的是避免复制实参,则应该将形参定义为const引用。如果将这样的形参定义为非const引用,则毫无必要的限制了函数的使用。举例如下:
string::size_type find_char(string &s,char c){
string::size_type i = 0;
while(i != s.size() && s[i] != c)
++i;
return i;
}
上述程序在一个string对象中查找一个指定的字符。这个函数将其string类型的实参当做普通的非const引用,尽管函数没有修改这个形参的值。这样的定义带来的问题是不能通过字符串字面值来调用这个函数:
if(find_char("hello world", 'o'))//error
总结:应该将不需要修改的引用形参定义为const引用。普通的非const引用形参在使用时不太灵活。这样的形参既不能用const对象初始化,也不能用字面值或者产生右值的表达式实参初始化。
4、vector和其他容器的形参
通常,函数不应该有vector或其他标准库容器类型的形参。调用含有普通非const引用的vector形参的函数将赋值vector的每一个元素。
C++程序员倾向于通过传递指向容器中需要处理的元素的迭代器来传递容器,而不是传递容器的引用。
void print(vector<int>::const_iterator beg,
vector<int>::const_iterator end){
while(beg != end){
cout << *beg++;
if(beg != end)
cout << " ";
}
cout << endl;
}
5、数组形参
1)不能复制数组;
2)使用数组名作为形参时,数组名自动退化为指向其第一个元素的指针。
3)传递数组的三种方法:
(1) void print(int*){}
(2) void print(int []){}
(3) void print(int [10]){} //编译器会自动忽略为任何数组形参指定的长度。这里的数字10没有意义。
虽然不饿能够直接传递数组,但是函数的形参可以写成数组的形式。上述三种定义等价,形参类型都是int *。
4)数组形参可声明为数组的引用,如果形参为数组的形参,编译器不会将数组实参转化为指针,而是传递数组的引用本身。这种情况下,编译器检查数组实参的类型和大小是否与形参的类型和大小匹配。
6、不要返回局部对象的引用(指针)
当函数执行完毕时,将释放分配给局部变量的存储空间。此时,对局部对象的引用(指针)就会指向不确定的内存。
如果返回局部对象的指针或引用,可能能得到正确的结果,也有可能得不到正确的结果。这要看回收的局部对象的内存是否被覆盖。
7、默认实参
1)默认实参是通过给形参表中的形参提供明确的初始值来指定的。如果一个形参具有默认实参,那么它后面的所有形参都必须具有默认实参。
2)设计带有默认实参的函数,其中部分工作就是排列形参,使最少使用的默认实参的形参排在最前面,最可能使用的默认实参的形参排在最后面。
string screenInit(string::size_type height = 24,
string::size_type width = 18,
char background = ' ');
string screen;
screen = screenInit();
screen = screenInit(66);
screen = screenInit(66,80);
screen = screenInit(66,80,'#');
8、类成员函数
编译器隐式地将类内定义的成员函数当做内联函数。
9、构造函数
1)构造函数是特殊的成员函数,与其他成员函数不同,构造函数和类同名,而且没有返回类型。一个类可以有多个构造函数,每个构造函数必须有与其他构造函数不同数目或类型的形参。
2)构造函数是放在类的public部分的。通常构造函数会作为类的接口的一部分,希望构造函数能定义和初始化类的对象。如果将构造函数定义为private,则不能定义类 的对象,那么这个类就失去了存在的意义。
3)如果没有为类显式定义任何构造函数,编译器将自动为这个类生成默认构造函数)(即合成的默认构造函数)。合成的默认构造函数将依据和变量初始化一样的规则初始化类中的所有成员。对于具有类类型的成员,会调用改成员所属类自身的默认构造函数实现初始化。内置类型成员的初值依赖于对象如何定义。如果对象在全局作用域中定义(即不在任何函数中)或定义为静态局部对象,则这些成员将会被初始化为0,。如果对象在局部作用域中定义,则这些成员没有初始化。
合成默认构造函数一般适用于仅包含类类型成员的类。而对于含有内置类型或者复合型成员的类,则通常应该定义他们自己的默认构造函数来初始化这些成员。
10、重载函数
1)定义:出现在相同作用域中的两个函数,如果具有相同的名字而形参不同,则称为重载函数。
2)函数重载和重复声明的区别:
①如果两个函数声明的返回类型和形参表完全匹配,则将第二个函数声明视为第一个的重复声明。
②如果两个函数的形参表完全相同,但返回类型不同,则第二个声明是错误的。函数不能仅仅基于不同的返回类型而实现重载。
③默认实参并不能改变形参的个数和类型,所以默认实参不会对函数重载造成不同。下面第二个函数是第一个函数的重复声明。
int func(int i, int j = 0);
int func(int i, int j);
④const形参与非const形参具有等价性。但是const引用(指针)的形参除外,const引用形参与非const引用形参的函数是不同的。
3)重载与作用域
如果局部地声明一个函数,则该函数将屏蔽而不是重载在外层作用域声明的同名函数(只需要函数名字相同即可,返回类型以及形参类型和个数可以不一样)。局部声明函数是一种不明智的选择。
4)重载确定的三个步骤
①候选函数
函数重载确定的第一步是确定该调用所考虑的重载函数集合,该集合中的函数称为候选函数。候选函数是与被调函数同名的函数。
void f();
void f(int);
void f(int, int);
void f(double,double = 3.14);
②选择可行函数
可行函数必须满足两个条件:第一,函数形参个数与该调用的实参个数相同;第二,每一个实参的类型必须与对应形参的类型匹配,或者可被隐式转换为对应的形参类型。
若调用f(5.5),依据上述条件,候选函数剩下第二个和第四个。
③寻找最佳匹配
函数重载的第三步是确定与函数调用中使用的实际参数匹配最佳的可行函数。其原则是:实参类型与形参类型越接近则匹配越佳。
上述调用,如果调用f(int),实参需要从double转换为int型;而另一个可行函数f(double, double = 3.14)是精确匹配。
最佳匹配选择顺序如下:
(1)精确匹配。实参与形参类型相同。
(2)通过类型提升实现的匹配。
(3)通过标准转换实现的匹配。
(4)通过类类型转换实现的匹配。