小白学习大型C++源码项目系列之函数

函数的定义

函数就是一段封装好的,可以重复使用的代码,它使得我们的程序更加模块化,不需要编写大量重复的代码。

函数可以提前保存起来,并给它起一个独一无二的名字,只要知道它的名字就能使用这段代码。
函数还可以接收数据,并根据数据的不同做出不同的操作,最后再把处理结果反馈给我们。

对于函数的定义来说,使用return语句可以向外提供该函数执行的一下结果;
对于函数的调用者来说,是否可以使用函数中执行的一些操作结果,就在于函数是否使用return语句返回了对应的执行结果。

函数的的使用方法

从表面上看,函数在使用时必须带上括号,有必要的话还要传递参数,函数的执行结果也可以赋值给其它变量。
例如,strcmp() 是一个用来比较字符串大小的函数,它的用法如下:

#include <stdio.h>
#include <string.h>
int main(){
char str1[] = “http://c.biancheng.net”;
char str2[] = “http://www.baidu.com”;
//比较两个字符串大小
int result = strcmp(str1, str2);
printf(“str1 - str2 = %d\n”, result);
return 0;
}

str1 和 str2 是传递给 strcmp() 的参数,strcmp() 的处理结果赋值给了变量 result。

函数的返回值

既然函数可以处理数据,那就有必要将处理结果告诉我们,所以很多函数都有返回值(Return Value)。所谓返回值,就是函数的执行结果。例如:

char str1[] = “C Language”;
int len = strlen(str1);

strlen() 的处理结果是字符串 str1 的长度,是一个整数,我们通过 len 变量来接收。

函数返回值有固定的数据类型(int、char、float等),用来接收返回值的变量类型要一致。

引用是 C++ 的新增内容,在实际开发中会经常使用;
C++ 用的引用就如同C语言的指针一样重要,但它比指针更加方便和易用,有时候甚至是不可或缺的。

同指针一样,引用能够减少数据的拷贝,提高数据的传递效率。

我们知道,参数的传递本质上是一次赋值的过程,赋值就是对内存进行拷贝。
所谓内存拷贝,是指将一块内存上的数据复制到另一块内存上。

对于像 char、bool、int、float 等基本类型的数据,它们占用的内存往往只有几个字节,对它们进行内存拷贝非常快速。
而数组、结构体、对象是一系列数据的集合,数据的数量没有限制,可能很少,也可能成千上万,对它们进行频繁的内存拷贝可能会消耗很多时间,拖慢程序的执行效率。

C/C++ 禁止在函数调用时直接传递数组的内容,而是强制传递数组指针,而对于结构体和对象没有这种限制,调用函数时既可以传递指针,也可以直接传递内容;为了提高效率,我曾建议传递指针,这样做在大部分情况下并没有什么不妥。

但是在 C++ 中,我们有了一种比指针更加便捷的传递聚合类型数据的方式,那就是引用(Reference)。

在 C/C++ 中,我们将 char、int、float 等由语言本身支持的类型称为基本类型,将数组、结构体、类(对象)等由基本类型组合而成的类型称为聚合类型。

引用(Reference)是 C++ 相对于C语言的又一个扩充。引用可以看做是数据的一个别名,通过这个别名和原来的名字都能够找到这份数据。引用类似于 Windows 中的快捷方式,一个可执行程序可以有多个快捷方式,通过这些快捷方式和可执行程序本身都能够运行程序;引用还类似于人的绰号(笔名),使用绰号(笔名)和本名都能表示一个人。

引用的定义方式类似于指针,只是用&取代了*,语法格式为:type &name = data;
type 是被引用的数据的类型,name 是引用的名称,data 是被引用的数据。
引用必须在定义的同时初始化,并且以后也要从一而终,不能再引用其它数据,这有点类似于常量(const 变量)。

凡是有引用类型的成员变量的类,不能有缺省构造函数。默认构造函数没有对引用成员提供默认的初始化机制,也因此造成引用未初始化的编译错误。

构造函数的形参必须为引用类型,暂时还不知道该怎么解释,牵涉到引用的机制。

C++函数的三种传递方式为:值传递、指针传递和引用传递

值传递

void fun(int x){
    x += 5;
    //修改的只是y在栈中copy x,
    // x只是y的一个副本,在内存中重新开辟的一块临时空间把y的值送给了x;
    // 这样也增加了程序运行的时间,降低了程序的效率。
}

void main(void)
{
    int y = 0;
    fun(y);
    cout<<"y = \"<<y<<endl; //y = 0;
}

指针传递

void fun(int *x){
    *x += 5; //修改的是指针x指向的内存单元值
}
void main(void){
    int y = 0;
    fun(&y);
    cout<<<<\"y = \"<<y<<endl; //y = 5;
}

引用传递

void fun(int &x){
    x += 5; //修改的是x引用的对象值 &x = y;
}
void main(void){
    int y = 0;
    fun(y);
    cout<<<<\"y = \"<<y<<endl; //y = 5;
}

1.值传递:有一个形参向函数所属的栈拷贝数据的过程,如果值传递的对象是类对象或是大的结构体对象,将耗费一定的时间和空间。

2.指针传递:同样有一个形参向函数所属的栈拷贝数据的过程,但拷贝的数据是一个固定为4字节的地址。

3.引用传递:同样有上述的数据拷贝过程,但其是针对地址的,相当于为该数据所在的地址起了一个别名。
注意,引用在定义时需要添加&,在使用时不能添加&,使用时添加&表示取地址。

效率上讲,指针传递和引用传递比值传递效率高。一般主张使用引用传递,代码逻辑上更加紧凑、清晰。

引用传递做函数参数”是C++的特性,C语言不支持。

例如:数据结构带&与不带&

带&的是引用型参数,它是地址传递,其实参会随着形参的改变而改变;
不带&的参数是一般参数,是值传递,其实参不会随着形参的改变而改变。
所以,结构改变,并且需要传回这种改变的要用引用型参数,否则用一般参数。
GetElem(L,i)只是找到第i个元素的值,线性表的结构并未发生任何改变,所以参数L前面不用加&。
ListInsert(&L,i,e)是在线性表L的第i个元素处插入一个数值为e的元素,线性表L的结构发生了改变,长度增加了,所以在L前必须加上&。如果不加,显示L时,新增元素就显示不出来,显示L的长度,也仍然是增加以前的值,比实际长度少1.

C++引用作为函数返回值

引用除了可以作为函数形参,还可以作为函数返回值,请看下面的例子:

    #include <iostream>
    
    using namespace std;
    
    int &plus10(int &r) {
        r += 10;
        return r;
    }
    
    int main() {
        int num1 = 10;
        int num2 = plus10(num1);
        cout << num1 << " " << num2 << endl;
        return 0;
    }

运行结果:
20 20

在将引用作为函数返回值时应该注意一个小问题,就是不能返回局部数据(例如局部变量、局部对象、局部数组等)的引用,
因为当函数调用完成后局部数据就会被销毁,有可能在下次使用时数据就不存在了,C++ 编译器检测到该行为时也会给出警告。

更改上面的例子,让 plus10() 返回一个局部数据的引用:

 #include <iostream>
    using namespace std;
    int &plus10(int &r) {
        int m = r + 10;
        return m;  //返回局部数据的引用
    }
    int main() {
        int num1 = 10;
        int num2 = plus10(num1);
        cout << num2 << endl;
        int &num3 = plus10(num1);
        int &num4 = plus10(num3);
        cout << num3 << " " << num4 << endl;
        return 0;
    }

在 GCC 下的运行结果:

20
30 30

plus10() 返回一个对局部变量 m 的引用,这是导致运行结果非常怪异的根源,因为函数是在栈上运行的,并且运行结束后会放弃对所有局部数据的管理权,后面的函数调用会覆盖前面函数的局部数据。

函数重载(Function Overloading)

所谓重载,就是赋予新的含义。函数重载(Function Overloading)可以让一个函数名有多种功能,在不同情况下进行不同的操作。运算符重载(Operator Overloading)也是一个道理,同一个运算符可以有不同的功能。

实际上,我们已经在不知不觉中使用了运算符重载。例如,+号可以对不同类型(int、float 等)的数据进行加法操作;<<既是位移运算符,又可以配合 cout 向控制台输出数据。C++ 本身已经对这些运算符进行了重载。

运算符重载(Operator Overloading)

运算符重载其实就是定义一个函数,在函数体内实现想要的功能,当用到该运算符时,编译器会自动调用这个函数。也就是说,运算符重载是通过函数实现的,它本质上是函数重载。

运算符重载的格式为:
返回值类型 operator 运算符名称 (形参表列){
//TODO:
}
operator是关键字,专门用于定义重载运算符的函数。我们可以将operator 运算符名称这一部分看做函数名。

运算符重载函数除了函数名有特定的格式,其它地方和普通函数并没有区别。

以成员函数的形式重载

Complex & operator+=(const Complex &c);
Complex & operator-=(const Complex &c);
Complex & operator*=(const Complex &c);
Complex & operator/=(const Complex &c);
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章