C++函数基础

179 阅读2分钟

函数基础

return_type function_name( parameter list )
{
   body of the function
}
int max(int num1, int num2) 
{
   // 局部变量声明
   int result;
   if (num1 > num2)
      result = num1;
   else
      result = num2;
   return result; 
}

执行函数的第一步是(隐式地)定义并初始化它的形参。因此,对于值传递来说,当调用函数时,首先创建一个名为val的参数类型变量,然后将它初始化为调用时所用的实参。

函数体是一个语句块。块构成一个新的作用域,可以在其中定义变量。形参和函数体内部定义的变量统称为局部变量(local variable)。它们对函数而言是“局部”的,仅在函数的作用域内可见。

static

static用于声明静态变量或静态对象,可以将局部变量定义成static类型,局部静态对象(local static object)在程序的执行路径第一次经过对象定义语句时初始化,并且直到程序终止才被销毁,在此期间即使对象所在的函数结束执行也不会对它有影响。

也就是说,static对象无论定义在哪都是初始化执行,程序结束后销毁。

size_t count_calls() {
    static size_t ctr=0; // 调用结束后,这个值仍然有效
    return ++ctr;
} 
int main() {
   for (size_t i=0;i!=10;++i) 
       cout << count_call1s() << endl; // 1-10
   return 0;
}

函数声明

函数声明会告诉编译器函数名称及如何调用函数。函数的实际主体可以单独定义,在函数声明中,参数的名称并不重要,只有参数的类型是必需的。

return_type function_name( parameter list );
int max(int num1, int num2);
int max(int, int);

在分离式编译中,函数声明于头文件(.h) ,定义于源文件(.cpp)。

参数默认值

可以指定参数默认值

int sum(int a, int b=20){
  int result = a + b;
  return (result);
}

如果函数的某个参数已经有了默认值,那么从该位置往后从左到右都必须有默认值。

如果函数声明有默认参数,那么实现就不能有默认参数,因此一般在函数声明时指定默认参数。

函数指针

函数指针是指向某个函数的地址,可以使用函数指针来调用函数,当把函数名作为值使用时会自动转换为指针

double cal(int);   // 函数
double (*pf)(int);   // 指针pf指向具有一个参数,其类型为int的函数, 返回值为double 
pf = cal;    // 指针赋值,等价于pf = &cal ,会自动进行转换

void estimate(int lines, double (*pf)(int));  // 函数指针作为参数传递 

// 使用指针调用函数
double y = cal(5);   // 函数调用
double y = (*pf)(5);   // 推荐写法
double y = pf(5);     // 这样也对, 但不推荐

配合typedef类型定义表示函数的别名,常用于函数回调。

typedef int (*Ptr)(int,int);
int add(int a,int b){
    return (a+b);
}
int main()
{
    Ptr pInt = add;
    cout<<pInt(3,5)<<endl;
    return 0;
}

参数传递

值传递与引用传递

值传递是最基础的传递,将实参的拷贝值传递给函数。

为加快性能同时可以改变参数,传递引用参数可以直接改变参数对象,为实参创建引用对象并交给函数使用。

void swap(int &x, int &y)
{
   int temp = x; 
   x = y;    
   y = temp; 
   return;
}
int main(){
   int a = 100;
   int b = 200;
   swap(a, b); // 相当于初始化创建a,b的引用类型,然后进行交换,直接改变引用参数对象
}

指针传递

与引用传递一样直接传递实际参数指针给函数使用,建议使用引用传递,使用指针传递多用于配合函数指针传递函数实现回调。

const引用参数

对参数加上const表示函数只能读取参数值,不能修改值,建议所有只读的引用参数都加上const,这样可以传递字面值常量,否则会导致编译错误

string::size_type find_char(string &s,char c,string::size_type &occurs);
find_char("He11o World", 'o', &ctr); // 编译错误,形参应该设置为const &string

数组参数

数组不允许拷贝传值,因此只能传递指针,传递指针是无法得知数组长度的,因此只能再传递一个数组长度参数或者在函数内部直接迭代数组(指针) 。但是如果明确知道是一个数组,可以使用sizeof获取数组长度:

constexpr size_t sz= sizeof(ia)/sizeof(*ia)

main函数

程序的主入口

int main(int argc,char *argv[]){} // int main(int argc,char **argv){} 
  • argc:函数参数数量
  • argv:字符串数组,参数数组,第一个字符串为程序名称,因此传递给main的参数总是从argv[1]开始

在可执行文件后面输入空格分开的传入参数即可:exe argc1 argc2

函数重载

函数名称相同,参数类型,个数,顺序不同,提高复用性,函数重载的原理是编译器在编译.c文件的时候会给函数进行简单的重命名,在原函数名称前面加上_,如原函数名叫做add,编译后就叫做_add 。 c++在底层编译.cpp文件时,虽然函数名称一样,但是在后面添加了一些其他东西导致名称不一样,其中表示开始,@@YA表示参数的开始,后面三个字符HHHMMM分别表示函数返回值类型,两个参数类型,@Z表示函数名称结束,所以由于函数生成的符号表中的名称不一样,可以编译通过。

编译器通过把所使用的参数类型与定义中的参数类型进行比较,决定选用最合适的定义。

void print(int i) {
  cout << "整数为: " << i << endl;
}

void print(double  f) {
  cout << "浮点数为: " << f << endl;
}

inline内联函数

使用关键字inline声明内联函数,编译器会把内联函数里的内容写在调用内联函数处,相当于不用执行进入函数的步骤,直接执行函数体。

  • 相当于宏,却比宏多了类型检查,真正具有函数特性;
  • 编译器一般不内联包含循环、递归、switch 等复杂操作的内联函数;
  • 在类声明中定义的函数,除了虚函数的其他函数都会自动隐式地当成内联函数

其主要作用是将简单的函数声明为内联函数可以加快速度。

// 声明1(加 inline,建议使用)
inline int functionName(int first, int second,...);

// 声明2(不加 inline)
int functionName(int first, int second,...);

// 定义
inline int functionName(int first, int second,...) {/****/};

// 类内定义,隐式内联
class A {
    int doA() { return 0; }         // 隐式内联
}

// 类外定义,需要显式内联
class A {
    int doA();
}
inline int A::doA() { return 0; }   // 需要显式内联

优点

  1. 内联函数同宏函数一样将在被调用处进行代码展开,省去了参数压栈、栈帧开辟与回收,结果返回等,从而提高程序运行速度
  2. 内联函数相比宏函数来说,在代码展开时,会做安全检查或自动类型转换(同普通函数),而宏定义则不会
  3. 在类中声明同时定义的成员函数,自动转化为内联函数,因此内联函数可以访问类的成员变量,宏定义则不能

缺点

  1. 代码膨胀。内联是以代码膨胀(复制)为代价,消除函数调用带来的开销。如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收获会很少。另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间
  2. inline 函数无法随着函数库升级而升级。inline函数的改变需要重新编译,不像 non-inline 可以直接链接
  3. 是否内联,程序员不可控。内联函数只是对编译器的建议,是否对函数内联,决定权在于编译器

函数回调

回调函数:作为参数被传递给另一个函数的函数叫做回调函数(区别就在这,一般函数的形参接收的是一个基本类型的变量,而这个函数接受的参数是一个"函数",这个作为参数的函数就叫回调函数),回调函数是作为另一个函数的形参出现的

  • 一般函数:function a(int a, string b),接收的参数是一般类型
  • 回调函数:function b(function c),接收的参数是一个函数c,这个函数就叫回调函数

回调函数是一种新的设计思路:自定义回调函数交给其他模块执行,其他模块不需要知道函数具体细节,只需要调用即可,如排序函数中传入的比较函数

回调函数更重要的用法在于异步调用,节省时间,为什么前端代码里满地都是回调?程序员说用户点击按键后需要弹窗Hello。这里包裹alert的匿名函数就是一个回调。问题是用户什么时候点击无法知道。只好把函数整个传过去,用户什么时候点,什么时候调用。

button.addEventListener('click', () => alert('Hello!'));

传统调用方式:

res = request(); 
handle(res);

异步调用方式可以使用回调实现:

request(handle);

此时根本不用关心request什么时候真正获取结果,只需要把获取到结果后该怎么处理告诉request就可以了,因此request函数可以立刻返回,真正获取结果的处理可能是在另一个线程、进程、甚至另一台机器上完成。

函数指针实现

C风格的函数回调,首先定义函数指针,配合typedef传参直接传入函数指针实现回调

typedef int (*Ptr)(int, int);
int CallBack(Ptr pInt, int a, int b) {
  return pInt(a, b);
}
int add(int a, int b) {
  return (a + b);
}
int main()
{
  cout << CallBack(&add, 3, 5) << endl;
}