函数是把一个语句序列(函数体)关联到名称和零或更多函数参数列表的 C++ 实体。
// 函数名:“ isodd ”
// 参数列表拥有一个参数,有名称“ n ”和类型 int
// 返回类型是 bool
bool isodd(int n)
{ // 函数体的开始
return n % 2;
} // 函数体的结束
调用函数时,例如在函数调用表达式中,以实参(在调用场所提供或为默认)初始化形参,然后执行函数体中的语句。
int main()
{
for(int arg : {-3, -2, -1, 0, 1, 2, 3})
std::cout << isodd(arg) << ' '; // isodd 被调用 7 次,
// 每次 n 被从 arg 复制初始化
}
1.1函数定义
函数是一组一起执行一个任务的语句。
返回类型:一个函数可以返回一个值。
函数名称:这是函数的实际名称。函数名和参数列表一起构成了函数签名。
参数:参数就像是占位符。当函数被调用时,向参数传递一个值,这个值被称为实际参数。参数列表包括函数参数的类型、顺序、数量。参数是可选的,也就是说,函数可能不包含参数。
函数主体:函数主体包含一组定义函数执行任务的语句。
1.2函数优缺点
函数:
函数调用要有一定的时间和空间方面的开销,于是将影响其效率。
函数调用时,先求出实参表达式的值,然后带入形参。
函数中的实参和形参都要定义类型,二者的类型要求一致
宏:
宏替换不占运行时间,只占编译时间。
带参的宏只是进行简单的字符替换。
宏不存在类型问题,宏名无类型,它的参数也无类型。
宏不可以调用C++类中的私有或者受保护的成员。
#define square(x) (x*x)
square(5) = 25
squre (5+5) = (5+5*5+5)
int squre(x)
{
return x*x;
}
square(5) = 25
squre (5+5) = (10*10)
1.3内联函数
内联函数和宏很类似,而区别在于,宏是由预处理器对宏进行替代,而内联函数是通过编译器控制来实现的。
1.3.1内联函数工作原理
对于任何内联函数,编译器在符号表里放入函数的声明(包括名字、参数类型、返回值类型)。 如果编译器没有发现内联函数存在错误,那么该函数的代码也被放入符号表里。
在调用一个内联函数时,编译器首先检查调用是否正确 (进行类型安全检查,或者进行自动类型转换,当然对所有的函数都一样)。 如果正确,内联函数的代码就会直接替换函数调用,于是省去了函数调用的开销。
1.3.2内联函数的写法:
inline int function(int i) {return i*i;}
PS:
1. 当你定义一个内联函数时,在函数定义前加上 inline 关键字,并且将定义放入头文件。
2. 内联函数必须是和函数体声明在一起,才有效。
3. 内联函数是不能为虚函数的,但样子上写成了内联的,即隐含的内联方式。在某种情况下,虽然有些函数我们声明为了所谓“内联”方式,但有时系统也会把它当作普通的函数来处理,这里的虚函数也一样,虽然同样被声明为了所谓“内联”方式,但系统会把它当成非内联的方式来处理。
1.3.3内联函数的缺点:
- 如果函数的代码较长,使用内联将消耗过多内存 , 这种情况编译器可能会自动把它作为非内联函数处理。
- 如果函数体内有循环,那么执行函数代码时间比调用开销大。
1.4函数重载、重写
重写是发生在两个类当中的,重载是在同一个类当中。
重写函数和被重写函数参数列表一定相同,而重载函数和被重载函数参数列表一定不同。(注意:仅返回值不同不能看作重载)
重写的基类中被重写的函数必须用virtual修饰,而重载函数和被重载函数可以用virtual修饰,也可以不用。
PS:
发生在同一个类中,若新函数名和原函数名相同,参数列表不同,则为重载。
发生在不同类当中,派生类的函数与基类函数函数名相同且参数列表也相同,则为重写。
1.5多态
C++多态(polymorphism)是通过虚函数来实现的,虚函数允许子类重新定义成员函数,而子类重新定义父类的做法称为覆盖(override),或者称为重写。
1.5.1隐藏和重写的区别
隐藏函数和被隐藏函数参数列表可以相同,也可以不同,但函数名必须相同。当参数不同时,无论基类中的参数是否被virtual修饰,基类函数都是被隐藏,而不是重写。
如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数有virtual关键字。此时,基类的函数不会被“隐藏”,而是覆盖也叫重写。
1.6虚函数
虚函数的作用是允许在派生类中重新定义与基类同名的函数,并且可以通过基类指针或引用来访问基类和派生类中的同名函数。
虚函数是C++中用于实现多态的机制。核心理念就是通过基类访问派生类定义的函数。如果父类或者祖先类中函数func()为虚函数,则子类及后代类中,函数func()是否加virtual关键字,都将是虚函数。为了提高程序的可读性,建议后代中虚函数都加上virtual关键字。
通过这个示例,我们可以看到,我们可以通过强行把&b转成int *,取得虚函数表的地址,然后,再次取址就可以得到第一个虚函数的地址了,也就是Base::f()。
1.6.1纯虚函数:
纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加“=0” 。
PS:
引入纯虚函数的原因:
(1)为了方便使用多态特性,我们常常需要在基类中定义虚拟函数。
(2)在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。
1.6.2虚函数表
我们可以看到下面几点:
1)虚函数按照其声明顺序放于表中。
2)父类的虚函数在子类的虚函数前面。
Base *b = new Derive();
b->f();
我们从表中可以看到下面几点,
1)覆盖的f()函数被放到了虚表中原来父类虚函数的位置。
2)没有被覆盖的函数依旧。
由b所指的内存中的虚函数表的f()的位置已经被Derive::f()函数地址所取代,于是在实际调用发生时,是Derive::f()被调用了。这就实现了多态。
1.7引用传值
int n = 0, i = 42;
int &r = n; //r绑定了n
r = 42; //n的值为42;
r= i ; //n的值和i相同
i = r; //i的值和n相同
void func(int &i)
{
i = 0; //改变了i所引对象的值
}
1.8回调函数
简而言之,回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用为调用它所指向的函数时,我们就说这是回调函数。