第六章:函数
0.前导:
由多条语句组成的有自身作用域的代码块就是函数,它可以被反复调用。
学习路径:函数组成——参数传递——返回类型——重载——函数指针
总结:在学习完本章函数后,我明白了:参数的传递与返回类型与函数指针本质上都是对象的定义,带着这样的眼光去看就不会显得很难了,只要第一章学的好。
1.基础
定义一个函数:函数不能在另一个函数内部定义。这是因为函数的定义必须在外部作用域中进行,以便编译器可以在需要时找到它们。
返回类型 函数名 (形参列表)
{
语句;
}
int count (int a,int b)
{
int c = a+b;
return c;
}
调用函数过程: 主函数调用函数,主函数执行被打断,被调函数开始执行——被调函数的形参通过实参进行初始化——执行函数中的语句——当遇到return语句,返回return语句中的值,将控制权交还给主函数
形参与实参: 形参与实参的关系相当于对象的初始化,形参作为左值即对象,实参作为右值即数值。函数中的形参相当于给出一个接口,使得主函数可以通过初始化不同的对象来调用函数,达到函数的重复利用性。
int count (int a,int b) 相当于 //形参列表中的定义不可以复合 如(int a,b)
{ {
int c = a+b; int a = //将a与b的初始化开放出来,可以自由初始化
return c; int b =
} int c = a+b
int main() return c; }
{
int d = 4;
int e = 8;
int f = count(d,e); //传入实参对形参(即对象)初始化
}
局部对象: 名字(对象名):有作用域,不同作用域可以有相同的名字。 对象:有生命周期(程序执行中对象存在的时间),位于局部的普通对象在对应块执行完成后就销毁 局部静态对象:位于块内的,不会随块执行完成就被销毁的自动对象
int count (int a,int b = 4) //必须为每一个形参传入对应的实参
{
static int e = 0; //定义在函数中,在块执行完之后不会被销毁,保留内存空间
int c = a+b; //局部对象,会被销毁,在主函数中无法调用。
++e;
return e;
}
int main()
{
int d = 4;
int f = count(d); //如果有形参函数定义时已经被初始化,可以不为它传入参数,
for(int i = 0 ; i<4;++i)
{
cout<< count(a) <<endl; //虽然静态局部对象不会被销毁,但是其作用域还是局部的,不能其它域中直接调用
}
}
函数声明: 如同外部对象的声明一样,可以实施头文件声明的格式,方便函数被其它文件调用。
//源文件
函数定义
//头文件
函数声明
返回类型 函数名 (形参列表);
void print();
3.参数传递:
本质: 参数传递本质就是初始化形参对象,与第二章的定义变量的操作相同。(意识到这点后面很好理解) 形参是按照传入的实参顺序初始化的,除非指定传入实参的形参。
传值参数: 相当于初始化定义形参对象
int count (int *a,int b)
{
return *a;
}
int main()
{
int d = 4;
int *p = &d;
cout<<count(&d,d)<<endl; //初始化指针对象,初始化整形对象
*p = 5;
cout<<count(&d,d)<<endl;
}
传引用参数:相当于初始化引用形参对象,适用于在大数据类型(不易拷贝)或想要直接改变传入对象。
void count (int &a,int b = 8)
{
++a;
return;
}
int main()
{
int d = 4;
count(d);
cout<<d<<endl; //对象d的制备改变,输出5
}
特殊形参:
const形参与实参: 在使用传引用参数时,只要不准备直接改变传入对象的值,尽量使用常量引用参数,因为非常量引用参数在初始化时有诸多限制,如不能使用字面值,常量等引用。
int count (const int *a,const int &b,int &c)
{
return *a+b+c;
}
int main()
{
int d = 4;
count(&d,b,4); //错误,引用初始化不能使用字面值、常量。常量引用可以
count(&d,b,b);
cout<<d<<endl; //对象d的制备改变,输出5
}
数组形参:
//包含传值形参,引用形参(常量引用,非常量引用).引用形参数组时形参与实参的数组大小必须相同。
void count ( const int *a,const int b[],const int c[10], const int (&g)[3],int (&h)[3] )
{
cout<<*a<<*b<<*c<<*f<<endl;
}
int main()
{
int d[] = {3,4,6,7};
count(d,d,d,d);
int z[] = {{3,4,6,7},{2,4,6,7}}; //当数组是多维数组时
count(d,d,d,d);
//传入的是指向数组首个元素的指针,在多维数组中,元素就是数组,这里就是指向数组中首个数组的指针。
}
可变形参函数: 当输入函数的实参的数量不确定时,有俩种方法解决
initializer_list形参: 适用于实参数量未知但是实参数据类型统一的情况。是标准库中定义的模板。
void erro(initializer_list<int> a) //数据类型永远为常量值
{
for(auto i : a)
{
cout<<i<<endl;
}
}
int main()
{
int a =4 , b=5;
erro({a});
erro({a,b});
}
4.返回
函数中与返回相关的:返回数据类型与return语句 返回的过程:初始化一个临时对象,数据类型为返回数据类型,初始化的值为return语句返回的值。这个临时对象就是函数给主函数的一个临时对象。理解了这个后面就很好理解了。
int * count(int *&b)
{
return b ; //返回时等于 int *temp = b;
}
int main()
{
int b = 4;
int *a = &b;
int *p = count(a); //接收时等于 int *p = temp
b = 5;
cout << *p << endl; //输出5
}
无返回值函数:
void count() //无返回值的return语句只能用在void返回数据类型中
{
return; //在void返回数据类型中,可以隐式执行return
}
有返回值函数: 不返回局部对象的引用或指针:局部对象在函数块执行完就被销毁,此时的引用或指针都将是无效的
int * count(int *&b)
{
int v = 8,*e = &v;
return e ; //返回的是指向局部变量的指针
}
int main()
{
int b = 4;
int *a = &b;
int *p = count(a); //进入主函数,局部变量销毁,指向局部变量的指针失效(存放的内存地址失效了)
b = 5;
cout << *p << endl; //输出5
}
返回数据类型: 类类型:类类型一般有成员,在调用函数返回类后可以通过运算符使用其成员
auto n = ShorterString(s1,s2).size() //返回string类型,直接调用temp对象中的成员对n初始化
引用与指针:当返回数据类型是引用时,这个函数可以作为左值,通常作为右值
int & count(int &b) //int &b = a
{
return b ; //返回 int &temp = b
}
int main()
{
int a = 4;
count(a) = 7; //引用可以作为左值
cout<<a<<endl;
}
return语句: 列表:使用列表作为返回的值,如果返回数据类型是容器类型,花括号中可以返回多个值,其余不行。
vector<int> count()
{
return {2,2,4,6};
}
int main()
{
int a = 4;
vector<int> v = count();
for (auto i : v)
{
cout << i << endl;
}
}
返回数组指针: 由于不能拷贝数组,函数不能数组作为返回数据类型,但可以将数组的指针与引用作为返回数据类型。
using arrint = int[10]; //设置一个数组类型别名
arrint* count(); //返回数据类型为数组指针
//声明一个返回数组指针的函数
type (*function(parameter_list))[dimension] ; //方法一
auto function() -> type(*)[dimension]; //方法二,尾置返回类型
decltype(object) * function(); //方法三,使用decltype的结果是数组,不是指针,所以要带*
注意:初始化数组指针与初始化指向数组首元素的指针不同
int main()
{
int arr1[] = {3,4,4};
int (*arr2)[3] = &arr1;
int *arr3 = arr1;
cout << **arr2 << endl;
cout << *(arr2[0]) << endl;
cout << arr3 << endl;
}
5.函数重载
定义: 在同一作用域里几个相同函数名,相同返回数据类型,但是不同形参数量与不同形参数据类型的函数成为重载函数
特殊: 顶层const形参数据类型:不能将仅在有无顶层const作为区分的函数作为重载函数
int count(const int i);
int count(int i); //重复了,不能重载
指针或引用形参数据类型:可以通过指向或引用的是否为常量来进行重载
int count(const int * i); //可以重载
int count(int * i); //当输入为非常量时,俩个函数都可以传参,编译器优先使用非常量形参的函数
int count(const int &i); //可以重载
int count(int &i) //当输入为非常量时,俩个函数都可以传参,编译器优先使用非常量形参的函数
//使用const_cast优化指针与引用的重载函数
每理解到有什么太大用处,展示先不写
作用域: 最好让所以重载函数在同一个作用域。当多个重载函数不在同一个作用域时,优先使用在最近作用域中的重载函数。
void print(int i)
void print(int i,char c)
int count()
{
void print(int i,string s);
print(3); //错误,不在同一作用域
print(3,'r'); //错误,不在同一作用域
print(3,"efg"); //正确,在同一作用域,但最好不要再函数中声明函数
return 3;
}
6.特殊函数
默认实参: 对于函数的形参列表,调用函数时每一个形参都要初始化。而使用默认实参,可以在定义或声明函数的时候直接在形参列表中初始化。在这些情况下(形参过多时;有些形参经常为固定值时等)使用默认参数方便了函数的调用。
有默认实参的形参必须位于没有默认实参的形参后面;在一个作用域中给一个函数默认实参只能进行一次;局部变量不能作为默认实参
int count(int *arr, int n) //有默认实参的形参必须位于没有默认实参的形参后面
{
return n;
}
int a = 4;
int *p = &a;
int count (int *arr = p, int n = a); //正确,在一个作用域中给一个函数默认实参只能进行一次
int main()
{
int count ( int n = 76); //错误,局部变量不能作为默认实参
for (int i = 0; i < 4; i++)
{
int a = 4;
int *p = &a;
int count (int *arr = p, int n = a); //错误,局部变量不能作为默认实参
}
}
内联函数与constexpr函数: 俩者通常都被放在头文件中。 内联函数: 对于简单常用的函数,调用该函数效率低于直接在主函数中写对应的语句。因此,使用inline前缀的函数,在编译时将被调用函数的代码直接插入到调用函数的位置,从而避免了函数调用的开销。
inline int add(int a, int b) {
return a + b;
}
constexpr函数: 当函数的返回数据类型为constexpr时,返回的temp的数据类型是带constexpr的。并且constexpr函数为隐式内联函数。 必须使用常量初始化形参列表。
constexpr int multiply_by_two(int x) {
return x * 2; // constexpr int temp = x*2
}
int main()
{
int x = 10;
int y = multiply_by_two(x); //错误int类型不能接受constexpr int
constexpr int k = multiply_by_two(x); //错误,实参为对象是变量
constexpr int u = multiply_by_two(4); //正确
}
总结:带constexpr的表达式或函数,在编译时就会被执行计算,而在编译时对象都是未被初始化的,所以在constexpr表达式中只能使用常量。
调试帮助: 在开发程序时,经常需要写调试程序对程序进行测试,而在程序正式使用时,这些调试程序需要被屏蔽掉。 assert预处理宏:
assert(expression); //常用于程序的测试
NDEBUG预处理变量: 如果定义了NDEBUG,则表示程序不在测试中,所有测试程序段都会被忽略。
#define NDEBUG
int main()
{
#ifndef NDEBUG //定义了NDEBUG,一直到#endif中的语句被忽略
{
constexpr int u = count(4);
cout << u << endl;
}
#endif
int a = 4;
cout<< a <<endl;
}
7.函数指针
可以使用一个指针指向函数,在调用该指针就相当于调用函数,这就是函数指针。 本质:将函数看成对象,函数指针看作普通指针,之前所学的操作在这里都相似。
知识点一:定义 函数由:返回数据类型;函数名;形参列表组成。将函数作为为指针对象进行初始化,就要确定他的数据类型。 函数指针的数据类型由:返回数据类型与形参列表组成;名字由函数名组成。 由于函数指针的数据类型比较长,通常可以使用别名来简短数据类型的长度。 使用定义好的函数的函数名或取址函数名都可以对函数指针进行初始化。 在使用重载函数初始化函数指针时,要注意指针只能指向一个函数。
知识点二:调用 使用解引用符或不使用都可以使用函数指针,效果等价于调用函数指针指向的函数
知识点三:传参 由于函数本身与数组一样无法作为参数进行传递。所以与数组相同的使用函数指针作为形参来调用函数,并且当形参的数据类型为函数指针的数据类型,可以传入函数名作为实参。
知识点四:返回 函数不能返回一个函数,但可以返回函数指针。在定义函数或声明函数时,可以使用直接写、类型别名、尾置返回等方式来写返回类型。与数组同样的在使用decltype时返回的是函数类型而非指针 回调函数:在一个函数中调用了另外一个函数。
typedef int (*bieming)(int*, int); //知识点三
int count (int* a, int n)
{
int cn = *a + n ;
return cn;
}
auto total(int(*p)(int*, int),bieming p1) -> int (*)(int*, int) //知识点四
{
return p;
}
int main()
{
int (*p)(int*, int) = &count; //知识点一
int (*p1)(int*, int) = count; //与数组类似,使用函数名相当于取址
int a = 1,*b = &a;
int i = p(b,a); //知识点二
cout << i << endl;
int(*p2)(int*, int) = total(p,count); //知识点三
int j = p2(b,a);
cout << j << endl;
}