c++ primer 第五版学习笔记-第六章 函数

本文详细介绍了函数的基础概念,包括函数定义、参数传递、返回类型等,并深入探讨了函数重载、内联函数、constexpr函数及函数指针等高级特性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

6.1 函数基础

 

1.函数是一个命名了的代码块,我们通过调用函数执行相应的代码。函数可以有0个或者多个参数,而且(通常)会产生一个结果。可以重载,也就是说,同一个名字可以对应几个不同的名字。

 

2.一个典型的函数定义包括以下部分:返回类型,函数名字,由0个或多个形参组成的列表以及函数体。其中,形参以逗号隔开,形参的列表位于一对圆括号之内。函数执行的操作在语句块中说明,该语句块成为函数体。

 

3.我们通过调用运算符来执行函数。调用运算符的形式是一对圆括号,它作用于一个表达式,该表达式是函数或者指向函数的指针:圆括号之内是一个用括号给的实参列表,我们用实参初始化函数的形参。调用表达式的类型就是函数的返回类型。

void func(int a, char c) // 形参
 {
 }
 
 int main()
 {
     func(1, 'a'); // 实参,与形参的类型、数量相匹配
     return 0;
 }

 

4.名字有作用域,对象有生命周期。

    名字的作用域是程序文本的一部分,名字在其中可见。

    对象的生命周期是程序执行过程中该对象存在的一段时间。

局部变量:形参和函数体内部定义的变量统称为局部变量。仅在函数的作用域内可见,同时,局部变量还会隐藏在外层作用域中同名的其他所有声明中。

 

5.自动对象:我们把只存在于块执行期间的对象称为自动对象。当块执行结束后,块中创建的自动对象的值就变成未定义的了。形参就是一种自动对象。

 

6.局部静态对象在程序的执行路径第一次经过对象定义语句时初始化,并且直到程序终止才被销毁,在此期间即使对象所在的函数结束执行也不会对它有影响。

int count()
{
    static int cnt=0;
    return ++cnt;
}
//函数会自动统计被调用多少次
//对于局部静态对象,若cnt没有显式初始化,那么cnt会默认初始化

 

       ①内置基本类型的局部静态变量如果没有显示地初始化,就会被初始为 0,且只会初始化一次

 

       ②局部静态变量会被存储到存储器的全局量区,生命周期贯穿函数调用以及之后的实际直到程序终止才会被销毁

       ③局部变量的生命周期超出函数的作用域范围,但是局部变量只能在其所在函数的作用域范围内使用

#include <iostream> 
using namespace std;
   int counter()
   {
       static int static_cnt = 0;
       int stack_i = ++static_cnt;
       return stack_i;
   }
   int main()
   {
       for (int i = 0; i < 5; ++i) {
           cout << counter() << endl;
       }
       return 0;
   }

7.函数声明

声明(函数原型,)不包含函数体(返回值类型还是需要的),且形参列表可以只保留参数的类型而省略参数名称,最后需要以";"结束。函数在被调用之前一定要被声明或被定义。

// 函数的声明,必须要写在函数第一次被调用之前。
 // 这部分的代码可放到头文件中,用的时候include进来就可以了(分离式编译)
 int func(char, int);
 
 // 只要函数在被调用前声明了,那函数的定义可以写在声明之后的任意的位置,
 // 如这里的func就可以在声明之后、main函数之前定义;
 // 如果func没有在调用前声明,则必须要在被调用前定义(相当于把main函数后面的func那段代码放在这里实现)
 
 int main() {
     return func('a', '1'); // 函数的调用,虽然func在main后定义,但是因为之前对func进行了声明,所以编译器知道这个函数的三要素是啥
 }
 
 /*
  * 函数的定义的参数列表中各参数的类型、数量以及位置等需要和声明时的相匹配
  */
 int func(char c, int i)
 {
     // do something
     return 0;
 }

8.分离式编译:允许我们把程序分割到几个文件中,每个文件独立编译。

 

6.2 参数传递

 

1.当形参是引用类型时,我们说它对应的实参被引用传递或者函数被传引用调用

当实参的值被拷贝给形参时,形参和实参是两个相互独立的对象。我们说这样的实参被值传递或者函数被传值调用

 

  • 值传递传的是变量的拷贝,而不是原始的值
  • 在修改指针参数指向的内存空间存储的数据时,应使用操作符*
  • 操作引用参数实际操作的是参数所引用的对象,使用引用传递可以避免拷贝,比如说如果vector对象中存储了大量的元素,而其作为参数传入时不需要进行修改,则可以传递引用,这样就避免了拷贝其中的大量元素,提高效率
  • 使用引用参数可以让函数返回额外的信息
  • 函数的顶层const形参会被忽略掉,重载时需注意

#include <iostream>
 
   using namespace std;
 
   /**
    * 传进来的是main函数中变量a和b的值的拷贝 c1, c2
    * 交换的也是值的拷贝,并没有改变a和b内存空间中的数据,
    * 所以此函数并不能交换a和b的值
    */
   void swap1(char c1, char c2)
   {
       char tmp = c1;
       c1 = c2;
       c2 = tmp;
   }
 
   /**
    * 传进来的是main函数中变量a和b的内存地址的拷贝 cp1, cp2
    * 交换的也是内存地址的拷贝,并没有改变a和b内存空间中的数据,
    * 所以此函数并不能交换a和b的值
    */
   void swap2(char *cp1, char *cp2)
   {
       cout << "in swap2: *cp1=" << *cp1 << ", *cp2=" << *cp2 << endl;
 
       char *tmp = cp1;
       cp1 = cp2;
       cp2 = tmp;
 
       cout << "out swap2: *cp1=" << *cp1 << ", *cp2=" << *cp2 << endl;
   }
 
   /**
    * 传进来的是main函数中变量a和b的内存地址的拷贝 cp1, cp2
    * 然后通过操作符*来更新内存地址中的数据,所以此函数可以成功交换a和b的值
    */
   void swap3(char *cp1, char *cp2)
   {
       cout << "in swap3: *cp1=" << *cp1 << ", *cp2=" << *cp2 << endl;
 
       // 取c1指向的内存空间中的数据赋值给临时变量tmp
       char tmp = *cp1;
       // 取cp2指向的内存空间中的数据赋值到cp1指向的内存空间中,覆盖原有数据
       *cp1 = *cp2;
       // 将tmp的值赋值到cp2指向的内存空间中,覆盖原有数据
       *cp2 = tmp;
 
       cout << "out swap3: *cp1=" << *cp1 << ", *cp2=" << *cp2 << endl;
   }
 
   /**
    * 传进来的是main函数中变量a和b的引用cr1, cr2
    * cr1, cr2直接操作的是其引用的对象,所以此函数可以成功交换a和b的值
    */
   void swap4(char &cr1, char &cr2)
   {
       char tmp = cr1;
       cr1 = cr2;
       cr2 = tmp;
   }
 
   /**
    * 交换指针,过程是一样的,只是会这里需要传指针的指针进来
    */
   void swap_pointer(char **cp1, char **cp2)
   {
       char *tmp = *cp1;
       *cp1 = *cp2;
       *cp2 = tmp;
   }
 
   int main()
   {
       char a = 'A';
       char b = 'B';
       cout << "origin: a=" << a << ", b=" << b << endl << endl;
 
       swap1(a, b);
       cout << "after swap1: a=" << a << ", b=" << b << endl << endl;
 
       swap2(&a, &b);
       cout << "after swap2: a=" << a << ", b=" << b << endl << endl;
 
       swap3(&a, &b);
       cout << "after swap3: a=" << a << ", b=" << b << endl << endl;
 
       // 重置 a, b 为swap3交换之前的原值
       a = 'A';
       b = 'B';
       swap4(a, b);
       cout << "after swap4: a=" << a << ", b=" << b << endl << endl;
 
       // 重置 a, b 为swap4交换之前的原值
       a = 'A';
       b = 'B';
       char *pa = &a;
       char *pb = &b;
       printf("before swap_pointer: pa=%p, pb=%p\n", pa, pb);
       swap_pointer(&pa, &pb);
       printf("after swap_pointer: pa=%p, pb=%p\n", pa, pb);
 
       return 0;
   }

2.当形参是const时,当用实参初始化形参时,顶层const被忽略,也就是说传给它常量对象和非常量对象就是可以的。

 

3.如果函数无需改变引用形参的值,最好将其声明为常量引用。

int L(int *A){...}
int R(int &B){...}
int i=0;const int j=0;
L(&i)   
L(&j)   //错误,不能用指向常量的指针初始化变量指针
R(i)    
R(j)    //错误,不能把普通引用绑定到常量引用上
R(42)   //错误,不能把普通引用绑定到字面值上

 

4.数组形参 

数组具有两个性质:①不允许拷贝数组,②使用数组时通常会将其转化为指针。 

                                因此,为函数传递数组,不能使用值传递(因为不能拷贝数组),所以实际是传递了指向数组首元素的指针。

 

当我们想把数组作为函数的实参时候,有以下三种方式:

//以下三种定义方法等价,每个函数的唯一形参都是const int*类型的
void print(const int *a);//声明为指针
void print(const int a[]);//声明为不限维度的数组
void print(const int a[10]);//这里维度表示我们期望数组含有多少元素,实际不一定
  • 1
  • 2
  • 3
  • 4
  • 5

①:不控制指针的边界:void print1(const int *p)

②:调用者指定数组的维度:void print2(const int *p,const int sz)//sz为数组维度

③:c++新规定的begin ,end函数限定数组的边界void print3(const int *beg,const int *end)

 

数组引用形参写法如下:

void print(int (&arr)[10])
//这样可以限制以下几种非法写法
int i=0,j[2]={1,2};
print(&i);//错误
print(j);//错误
  • 1
  • 2
  • 3
  • 4
  • 5

传递高维数组,例如m[20][10]

void print(int (*matrix)[10])
//正确,指向数组的首元素(10个int构成的数组)
void print(int *matrix[10])//错误
void print(int matrix[][10])//正确
  • 1
  • 2
  • 3
  • 4

main函数传递数组:

int main(int argc,char *argv[])//第2个参数是个数组,是指向c风格字符串的指针,第1个形参表示数组中字符串的数量或者:int main(int argc,char **argv)
//当运行文件时,例如
./ex hello world
/*
此时,argc==3 
argv[0]="./ex"
argv[1]="hello"
argv[2]="world"
argv[3]=0
*/当使用argv[]中的实参时,一定记得可选的实参从argv[1]开始,因为argv[0]保存的是程序的名字,而非用户输入

 

5.为了处理不同数量实参的函数,但是实参类型都相同,initializer_list是一种标准库类型,用于表示某种特定类型的值的数组

initializer_list:当我们要传递不同数量的实参(实参类型都相同)时,我们可以使用initializer_list,与vector类似,但值都为常量,无法更改。

使用时包含头文件:#include<initializer_list>

initializer_list提供的操作:

initializer_list<T>1st:                   默认初始化;T类型元素的空列表
initializer_list<T>1st{a,b,c...}:         1st的元素数量和初始值一样多;1st的元素是对应初始值的副本;列表中的元素是const
1st2(1st):                                拷贝或赋值一个initializer_list对象不会拷贝列表中的元素;拷贝后,原始列表和副本共享元素
1st2 = 1st:                               同上
1st.size():                               列表中的元素数量
1st.begin():                              返回指向1st中首元素的指针
1st.end():                                返回指向1st中尾元素下一位置的指针
#include <initializer_list>
void error(inializer_list<string>ls){...}
if(opt==1)error({"Ass","We","Can"});//三个参数
else error({"Thankyou","Sir"});//两个参数

 

6.3 返回类型和return语句

 

 

1.没有返回值的return语句只能用在返回类型时void的函数中。

只要函数的返回类型不是void,则该函数内的每条return语句必须返回一个值。return语句返回值的类型必须与函数的返回类型相同,或者能隐式的转换成函数的返回类型。

         动态参数列表:void func(string msg, ...); 常见的应用场景:日志的打印

        c++11新标准允许返回花括号包围的值列表:

 vector<string> test()
 {
     return {"hello", "world"};
 }

 

2.如果一个函数调用了它自身,不管这种调用时直接的还是间接的,都称该函数为递归函数。

 

        递归,调用函数自身,main函数不能调用它自己。应用场景:虽然很多书的例子都是用的阶乘或者是斐波拉数列,但是实际工作中除了就是做算法的人,因为涉及到性能的问题,平时应该都很少用递归,我自己也只在遍历文件目录的时候用过递归。

 // 伪代码
 void list_dir(string dir)
 {
     files = traverse_current_dir(dir); // 遍历当前目录下的文件或者目录
     for path in files:
         print path
         if path is directory: // 如果文件是目录,则调用自身遍历这个目录
             list_dir(path);
 }

 

 

返回左值的引用:调用一个返回引用的函数得到左值

int &get_val(int a[]){return a[0];}
int main()
{
    get_val(x)=10;//函数调用放在左侧,是因为函数返回的是引用的函数,是左值
}
  • 1
  • 2
  • 3
  • 4
  • 5

初始化返回值

vector<int> P{return {1,2,3};}
  • 1

main函数返回值

#include <cstdlib>
int main()
{
    return EXIT_SUCCESS;//EXIT_SUCCESS==0
    return EXIT_FAILURE;//EXIT_FAILURE!=0,但具体值依机器定
    //这两个均为预处理变量
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

因为数组不能被拷贝,所以函数不能返回数组,但是函数可以返回数组的指针或引用

//方法一:使用别名
typedef int arr10[10];//using arr10=int[10];
arr10 *func(int x)

//方法二:直接声明
int (*(func(int x)))[10] //对比声明指向数组的指针 int (*p)[10],必须有数组维度

//方法三:尾置返回
auto func(int x) -> int (*)[10]//箭头后面表示真正返回的类型

//方法四:decltype
int odd[]={1,3,5},even[]={2,4,6};
decltype(odd) *func(int x)//返回一个大小为3的数组的指针

6.4 函数重载

 

1.如果同一作用域内的几个函数名字相同但形参列表不同,我们称之为重载函数。

不允许两个函数除了返回类型外其他所有的要素都相同。

 

顶层const与非顶层const无法区分开,所以不允许重载,所以const int和int一样,不算重载。

但是形参是指针或引用等底层引用时,可以重载

int lookup(int &x)          //1
int lookup(const int &x)    //2

int lookup(int *x)          //3
int lookup(const int *x)    //4
//以上四种均不相同
lookup(y)
//当y为常量,只有函数2可以调用
//当y为变量,优先函数1,函数1不存在时调用函数2
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

不同作用域下函数的重载

void print(string &s);
void print(double a);
int main()
{
    void print(int a);
    print("XTX");//错误,print(string)被覆盖
    print(3.14);//错误,print(double)被覆盖
    print(3);//正确,调用print(int)
}

 

2.函数匹配是指一个过程,在这个过程中我们把函数调用与一组重载函数中的某一个关联起来,函数匹配也叫做重载确定。

 

 

3.调用重载函数时有三种可能的结果:

        编译器找到一个与实参最佳匹配的函数,并生成调用该函数的代码。

        找不到任何一个函数与调用的实参匹配,此时编译器发出无匹配的错误信息。

        有多于一个函数可以匹配,但是每一个都不是最佳选择。此时也将发生错误,称为二义性调用。

int get_user(int id);
 int get_user(string name, string mail);
 
 int main()
 {
     get_user(100); // 调用形参列表是 (int) 的
     get_user("cat", "littlefawn@163.com"); // 调用形参列表是 (string, string)的
 
     return 0;
 }

 

6.5 特殊用途语言特性

 

 

1.某些函数有这样一种形参,在函数的很多次调用中它们都被赋予一个相同的值,此时,我们把这个反复出现的值称为函数的默认实参。调用含有默认实参的函数时,可以包含该实参,可以省略该实参。

一旦某个形参被赋予了默认值,它后面所有形参都必须有默认值。

 

尽管多次声明可行,但是对于有默认实参的函数声明,之后同一函数声明只能对没有默认值的形参添加默认值,已有默认值不变

string screen(int a,int b,int c=10);
string screen(int a,int b,int c=5);//错误!
string screen(int a=1,int b=3,int c);//正确

 

在声明时候使用的默认参数不能是局部变量。

局部变量不能作为默认实参,否则,只要表达式的类型能转化为默认实参,就能作为默认实参

int wd=80;
int f(int x=wd);
int x=f();//调用f(80)
void f2()
{
    int wd=100;
    int y=f();//调用f(80),外层定义隐藏,但是没改变默认值
}

 

2.内联说明只是向编译器发出的一个请求,编译器可以选择忽略这个请求。

 

一般来说,内联机制用于优化规模较小、流程直接、频繁调用的函数,很多编译器都不支持内联递归函数。

#include <iostream>
 
using namespace std;
 
// 在函数声明的返回值之前加上关键字 inline即可
inline string short_str(const string &str1, const string &str2) {
    return str1.size() > str2.size() ? str1 : str2;
}
 
int main()
{
    string s1 = "Hi";
    string s2 = "world";
    // short_str会被展开为:()
    cout << short_str(s1.size() > s2.size() ? s1 : s2) << endl;
 
    return 0;
}

 

3.constexpr函数是指能用于常量表达式的函数

 

函数返回类型及所有形参的类型都得是字面值类型,而且函数体中必须有且只有一条return语句。

#include <iostream> 
using namespace std;
constexpr int MIN_TO_SEC(int minutes)
{
    return minutes * 60;
}
 
int main()
{
    const int one_hour_minutes = 60;
    cout << one_hour_minutes << " minutes = " << MIN_TO_SEC(one_hour_minutes) << " seconds" << endl;
    return 0;
}
constexpr int scale(int x){return 2*x;}

int arr[scale(3)],i=10;//正确

int arr[scale(i)];//错误,scale(i)不是常量表达式,编译器报错

 

把内联函数(inline)和constexpr函数放在头文件中。函数声明也应该放在头文件中。

 

4.调试帮助 
assert:assrt(a),如果a为假,assrt输出信息并且终止程序,若为真,则什么也不做。#include<cassert>,由预处理器管理,不需用std:: ,不需要提供using声明。
NDEBUG:当我们定义NDEBUG时,assert函数将不执行,否则assert函数将进行调试 
其他常用函数:

_ _func_ _    输出当前调试的函数的名字 

_ _FILE_ _   存放文件名的字符串字面值

_ _LINE_ _   存放当前行号的整型字面值

_ _TIME_ _  存放文件编译时间的字符串字面值

_ _DATE_ _  存放文件编译日期的字符串字面值

 

6.6 函数匹配

 

1.函数匹配的第一步是选定本次调用对应的重载函数集,集合中的函数称为候选函数。候选函数具备两个特征:一是与被调用的函数同名,二是其声明在调用点可见。

第二步考察本次调用提供的实参,然后从候选函数中选出能被这组实参调用的函数,这些新选出的函数称为可行函数。可行函数也有两个特征:一是其形参数量与本次调用提供的实参数量相等,二是每个实参的类型与对应的形参类型相同,或者能转换成形参的类型。

  函数匹配的原则:

void f();
void f(int);
void f(int,int);
void f(double,double=3.14);

f(5.6) //调用f(double,double =3.14)
/*
1.寻找同名且调用点可见
2.找出所有可行函数
3.若存在最优匹配,寻找最优,即实参与形参类型越接近,结果越好
*/

f(2,2.56)//错误!
/*
寻找最优匹配,有且仅有一个可行函数满足下列条件才可行
1.每个实参匹配均不劣于其他可行函数
2.至少一个优于其他
对于2,f(int,int)更优,对于2.56,f(double,double=3.14)更优,因此,调用具有二义性,错误!
*/

 

2.为了确定最佳匹配,编译器将实参类型到形参类型的转换划分成几个风机,具体排序如下所示:

 

a.精确匹配,包括以下情况:

        实参类型和形参类型相同

        实参从数组类型或函数类型转换成对应的指针类型

        向实参添加顶层const或者从实参中删除底层const

b.通过const转换实现的匹配

c.通过类型提升实现匹配

d.通过算术类型转换

e.通过类类型转换实现匹配

 

6.7 函数指针

 

1.函数指针指向的是函数而非对象。和其他指针一样,函数指针指向某种特定类型。函数的类型由它的返回类型和形参类型共同决定,与函数名无关。

bool judge(int x);
bool (*J)(int x);//未初始化的函数指针

J=judge;
J=&judge;//两种写法等价,均为将judge与J绑在一起
//若定义重载函数,则函数指针的形参必与某一个函数形参精确匹配

J(1);(*J)(1);judge(1);//三种等价调用方法

//函数指针也可以作为形参,以下两种等价定义
void dy(bool u(int x));
void dy(bool (*u)(int x));
//可以直接调用函数,自动转为指针
dy(judge);

//可以用decltype,typedef,using简化定义
typedef bool Func(int x);
/*
等价定义 
typedef decltype(judge) Func;
using Func=bool(int);
*/
void dy(Func u);
void dy(Func *u);
//若需要返回一个函数指针,我们还可以这样
auto ddy(int x)-> int (*)(int);个函数形参精确匹配

J(1);(*J)(1);judge(1);//三种等价调用方法

//函数指针也可以作为形参,以下两种等价定义
void dy(bool u(int x));
void dy(bool (*u)(int x));
//可以直接调用函数,自动转为指针
dy(judge);

//可以用decltype,typedef,using简化定义
typedef bool Func(int x);
/*
等价定义 
typedef decltype(judge) Func;
using Func=bool(int);
*/
void dy(Func u);
void dy(Func *u);
//若需要返回一个函数指针,我们还可以这样
auto ddy(int x)-> int (*)(int);

 

2.当我们使用重载函数时,上下文必须清晰地界定到底应该选用哪个函数。如果定义了指向重载函数的指针,编译器通过指针类型决定选用哪个函数,指针类型必须与重载函数中的某一个精确匹配。

 

#include <iostream> 
using namespace std; 
string short_str(const string &str1, const string &str2) {
    return str1.size() > str2.size() ? str1 : str2;
} 
string short_str(const string &str1) {
    return str1;
}
 
// 参数p为函数指针
void show(const string &str1, const string &str2, string (*p)(const string &, const string &)) {
    cout << p(str1, str2) << endl;
}
 
// 返回指向函数的指针
using SSP = string (*)(const string &);
SSP get_function()
{
    return &short_str;
}
 
int main()
{
    string (*ssp)(const string &, const string &);
 
    ssp = &short_str; // 如果只有一个short_str时,只需要直接赋值即可
    cout << ssp("Hi", "world") << endl;
 
    string (*ssp2)(const string &) = &short_str; // 存在重载函数时,上下文需明确指定应选用哪个函数
    cout << ssp2("Hi") << endl;
 
    show("my", "god", &short_str);
 
    cout << get_function()("good") << endl;
 
    return 0;
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值