菜鸟笔记
提升您的技术认知

C++内功修炼----函数

 

1.参数传递 

 介绍

值传递:

形参是实参的拷贝,改变形参的值并不会影响外部实参的值。从被调用函数的角度来说,值传递是单向的(实参->形参),参数的值只能传入,不能传出。当函数内部需要修改参数,并且不希望这个改变影响调用者时,采用值传递。

指针传递:
形参为指向实参地址的指针,当对形参的指向操作时,就相当于对实参本身进行的操作

引用传递:
形参相当于是实参的“别名”,对形参的操作其实就是对实参的操作,在引用传递过程中,被调函数的形式参数虽然也作为局部变量在栈中开辟了内存空间,但是这时存放的是由主调函数放进来的实参变量的地址。被调函数对形参的任何操作都被处理成间接寻址,即通过栈中存放的地址访问主调函数中的实参变量。正因为如此,被调函数对形参做的任何操作都影响了主调函数中的实参变量
区别
指针传递与引用传递的区别:

指针参数传递本质上是值传递,它所传递的是一个地址值。值传递过程中,被调函数的形参作为被调函数的局部变量处理,会在栈中开辟内存空间以存放由主调函数传递进来的实参值,从而形成了实参的一个副本(替身)。值传递的特点是,被调函数对形参的任何操作都是作为局部变量进行的,不会影响主调函数的实参变量的值(形参指针变了,实参指针不会变)。

引用参数传递过程中,被调函数的形参也作为局部变量在栈中开辟了内存空间,但此时存放的是由主调函数放进来的实参变量的地址。被调函数对形参(本体)的任何操作都被处理成间接寻址,即通过栈中存放的地址访问主调函数中的实参变量(根据别名找到主调函数中的本体)。因此特点,被调函数对形参的任何操作都会影响主调函数中的实参变量。

【注:】指针与引用的另一个重要的不同是指针可以被重新赋值以指向另一个不同的对象。但是引用则总是指向在初始化时被指定的对象,以后不能改变。

应用场景 

在C++中,函数的形参,什么时候使用引用?什么时候使用指针?什么时候使用按值传递?

1  针对函数内部只使用传递的值而不做修改
(1)数据对象较小,如内置数据类型或小型结构体,则按值传递
  void func(int );
(2)数据对象是数组,则只能使用指针,并将指针const指针
  void func(const int *,int);//第二个参数为数组长度
(3)数据对象是较大的结构体,则const指针或const引用都行
  struct struc{…};
  void func(const struc *);
  或void func(const struc &);
(4)数据对象是类,则使用const引用
  void func(const string &,);
2  针对函数内部不仅使用传递的值且要做修改的
(1)数据对象是内置数据类型,则使用指针
  void func(int *);
(2)数据对象是数组,则只能使用指针
  void func(int *,int);//第二个参数为数组长度
(3)数据对象是结构体,则使用引用或指针
  struct struc{…};
  void func(struc *);
  或void func(struc &);
(3)数据对象是类,则使用引用
  void func(ostream &);

2.函数重载 

我们在平时写代码中会用到几个函数但是他们的实现功能相同,但是有些细节却不同。例如:交换两个数的值其中包括(int, float,char,double)这些个类型。在C语言中我们是利用不同的函数名来加以区分。

void Swap1(int* a, int* b);
void Swap2(float* a, float* b);
void Swap3(char* a, char* b);
void Swap4(double* a, double* b);
我们可以看出这样的代码不美观而且给程序猿也带来了很多的不便。于是在C++中人们提出了用一个函数名定义多个函数,也就是所谓的函数重载。

一.函数重载定义

 函数重载是一种特殊情况,C++允许在同一作用域中声明几个类似的同名函数,这些同名函数的形参列表(参数个数,类型,顺序)必须不同,常用来处理实现功能类似数据类型不同的问题。

    在C++中不仅函数可以重载,运算符也可以重载。例如:

 运算符<<,>>。既可以做移位运算符,也可以做输出,输入运算符。

注意:重载函数的参数个数,参数类型或参数顺序三者中必须有一个不同

#include<Windows.h>
#include<iostream>
using namespace std;
 
int Add(int a, int b)
{
 
    return a + b;
 
}
 
double Add(double a, double b)
{
 
    return a + b;
}
 
float Add(float a, float b)
{
 
    return a + b;
 
}
int main()
{
 
    cout<<Add(1,2)<<endl;
    cout<<Add(3.5, 4.5)<<endl;
    cout << Add(2.22, 3.33) << endl;
    
    system("pause");
    return 0;
}
我们可以看到定义了一个Add函数来求三个不同类型数的和,在调用过程中系统会自动根据其实参的类型不同来实现准确调用。

#include<iostream>
#include<Windows.h>
using namespace std;
 
int main()
{
    int max(int a, int b, int c);
    int max(int a, int b);
    int a = 10;
    int b = 20;
    int c = 30;
 
    cout << max(a, b, c) << endl;
    cout << max(a, b) << endl;
    system("pause");
    return 0;
}
int max(int a, int b, int c)
{
    if (b > a)
        a = b;
    if (c > a)
        a = c;
    return a;
 
}
 
int max(int a, int b)
{
 
    return (a > b) ? a : b;
}
从上边代码可以看出函数重载除了允许函数类型不同以外,换允许参数个数不同。

函数重载的规则:

函数名称必须相同。
参数列表必须不同(个数不同、类型不同、参数排列顺序不同等)。
函数的返回类型可以相同也可以不相同。
仅仅返回类型不同不足以成为函数的重载。
二、函数重载的作用:
重载函数通常用来在同一个作用域内 用同一个函数名 命名一组功能相似的函数,这样做减少了函数名的数量,避免了名字空间的污染,对于程序的可读性有很大的好处。

三、函数重载是一种静态多态:

(1)多态:用同一个东西表示不同的形态; 
(2)多态分为: 
         静态多态(编译时的多态); 
         动态多态(运行时的多态); 
(3)函数重载是一种静态多态;

四.面试题

1.C语言中为什么不能支持函数重载?

从上图可知编译器在编译.c文件时,只会给函数进行简单的重命名;具体的方法是给函数名之前加上”_”;所以加入两个函数名相同的函数在编译之后的函数名也照样相同;调用者会因为不知道到底调用那个而出错;

2.C++中函数重载底层是如何处理的?

 在.cpp文件中,虽然两个函数的函数名一样,但是他们在符号表中生成的名称不一样。

 
‘?’表示名称开始,‘?’后边是函数名“@@YA”表示参数表开始,后边的3个字符分别表示返回值类型,两个参数类型。“@Z”表示名称结束。 
由于在.cpp文件中,两个函数生成的符号表中的名称不一样,所以是可以编译通过的。

3.C++中能否将一个函数按照C的风格来编译?

#include<iostream>
#include<Windows.h>
using namespace std;
 
 
extern "C" int Add(int a, int b)
{
 
    return a + b;
}
int main()
{
    cout << Add(10, 20) << endl;
    system("pause");
    return 0;
}
可以按照C风格来编译,只需在函数名前加extern "C" 就可以完成按照C风格来编译

其他特性

 

内联函数

1.函数调用原理

"编译过程的最终产品是可执行程序--由一组机器语言指令组成。运行程序时,操作系统将这些指令载入计算机内存中,因此每条指令都有特定的内存地址。计算机随后将逐步执行这些指令。有时(如有循环和分支语句时),将跳过一些指令,向前或向后跳到特定地址。常规函数调用也使程序跳到另一个地址(函数的地址),并在函数结束时返回。下面更详细地介绍这一过程的典型实现。执行到函数调用指令时,程序将在函数调用后立即存储该指令的内存地址,并将函数参数复制到堆栈(为此保留的内存块),跳到标记函数起点的内存单元,执行函数代码(也许还需将返回值放入寄存器中),然后跳回到地址被保存的指令处(这与阅读文章时停下来看脚注,并在阅读完脚注后返回到以前阅读的地方类似)。来回跳跃并记录跳跃位置意味着以前使用函数时,需要一定的开销。"

2.内联函数
内联函数提供了另一种选择。编译器将使用相应的函数代码替换函数调用。因此,内联函数的运行速度比常规函数稍快,但代价是需要占用更多内存。

3.内联函数的使用
在函数声明前加上关键字inline;
在函数定义前加上关键字inline。
示例如下:

#include<iostream>
inline double square(double x){return x*x;}
int main()
{
    using namespace std;
    double a,b;
    double c = 13.0;
    a = square(5.0);
    b = square(4.5+7.5);
    cout<<"a="<<a<<",b="<<b<<endl;
    cout<<"c="<<c<<endl;
    cout<<"c squared="<<square(c++)<<endl;
    cout<<"now c="<<c<<endl;
    return 0;
}

程序输出结果如下:
a=25,b=144
c=13
c square=169
now c=14

4.内联函数与宏定义的区别
C语言使用预处理器语句#define来提供宏。如下例所示:
#define SQUARE(X) X*X
宏定义时通过文本替换开实现的--X是参数的符号标记。
a = square(5.0);->a=5.0*5.0;
b = square(4.5+7.5);->b=4.5+7.5*4.5+7.5
d = square(c++);->d=c++*c++
可以看出,对于b,需要使用括号才能正常运算。
#define SQUARE(X) ((X)*(X))
对于c,却仍递增了两次。
因此,宏定义和内联函数存在本质的区别,转换的时候应考虑是否转换后功能是否正常。

5.什么时候使用内联函数
如果执行函数代码的时间比处理函数调用机制的时间长,则节省的时间占比很小。若代码执行时间很短,则内联函数就可以节省函数调用的时间。

 const expr函数

constexpr变量
将变量声明为constexpr以便于由编译器检测一个表达式是否为一个常量表达式,而const没有此功能:

int a = 3;
int b = 4;
scanf("%d", &a);
constexpr int value1 = a + b;//编译器报错,表达式a + b不是常量表达式
const int value2 = a + b;//正常初始化,不会报错
因此,如果你想用一个你认为是常量的表达式来初始化一个变量,不妨将这个变量声明为constexpr,让编译器为你检测一下。

只有字面值类型才能声明为constexpr变量。(基本算数类型(bool,int...),引用,指针...)。

当一个指针声明为constexpr时,相当于:

#include <iostream>
using namespace std;
int a = 3;
int main()
{
    constexpr int *p1 = &a;
    //两者等价,表示指针为常量,对象的值可以修改。
    int * const p1 = &a;
    system("pause");
    return 0;
}
所以,如果想要声明一个指针常量指向一个整型常量,则可以有如下操作:

#include <iostream>
using namespace std;
int a = 3;
int main()
{
    constexpr const int *p1 = &a;
    //两者等价,指针为常量,指向一个整型常量
    const int *const p3 = &a;
    system("pause");
    return 0;
}
constexpr指针和引用只能指向所有函数体之外的变量(全局变量)或者函数体内的静态变量。

constexpr函数
可以实现编译期函数(即函数在编译期执行完毕,并在调用处进行替换):

#include <iostream>
using namespace std;
//运算n的阶乘
constexpr int factorial(int n)
{
    return n == 0 ? 1 : n * factorial(n - 1);
}
int main()
{
    cout << factorial(10) << endl;
    system("pause");
    return 0;
}
该函数也可以在运行期执行:

#include <iostream>
using namespace std;
constexpr int factorial(int n)
{
    return n == 0 ? 1 : n * factorial(n - 1);
}
int main()
{
    int a = 3;
    scanf_s("%d", &a);
    cout << factorial(a) << endl;
    system("pause");
    return 0;
}
可以对constexpr变量进行初始化:

#include <iostream>
using namespace std;
constexpr int factorial(int n)
{
    return n == 0 ? 1 : n * factorial(n - 1);
}
int main()
{
    constexpr int value = factorial(10);
    system("pause");
    return 0;
}
规定:

函数的返回值以及参数都必须为字面值类型;
函数只能有一条return语句(C++14后无该要求);
函数不一定返回常量表达式,但如果要初始化一个constexpr变量,则必须返回常量表达式(参数也必须为常量或常量表达式);
函数被隐式的声明为内联函数;
函数内部可以声明变量(声明之后是运行期还是编译期?),可以用using声明,空语句,类型别名,循环,判断语句等,但cout不行。

static静态函数

 
  在函数的返回类型前加上static关键字,函数即被定义为静态函数。静态函数与普通函数不同,它只能在声明它的文件当中可见,不能被其它文件使用。
静态函数的例子:

#include<iostream>
using namespace std;

static void fn();   //声明静态函数

int main(void)
{
    fn();
    return 0;
}

void fn()     //定义静态函数
{
    int n = 10;
    cout<<n<<endl;
}

定义静态函数的好处:

       静态函数不能被其它文件所用;
       其它文件中可以定义相同名字的函数,不会发生冲突;