【C++探索之旅】第一部分第八课:传值引用,文件源头



内容简介

1、第一部分第八课传值引用,文件源头

2、第一部分第九课预告:数组威武,动静合一



传值引用,文件源头


这一课的标题有点怪。其实是由这一课的几个重点内容结合起来取的名,慢慢学习就知道啦。


上一课《【C++探索之旅】第一部分第七课:函数效应,分而治之》中,我们初步认识了函数。


不过不要高兴得太早,你以为函数就这样离你远去了嘛?怎么可能,函数将伴随一生好吗,只要你继续编程的话。哈哈,所以你是跑不掉了~


【小编,都跟你签了协议了,没吃药不要随便出来溜达】


这一课我们就继续深入学习与函数相关的几个知识点。不过函数我们会一直深入学习的,以后有了类,对象等面向对象的知识,到时函数还会换一个称呼。



值传递和引用传递


值传递


我们首先来学习在函数范围内操作系统是怎样管理内存的。


再拿我们之前的addTwo函数举例。这个函数很简单,就是在参数的基础上加上2,然后返回其值。如下:


int addTwo(int a)
{
    a += 2;
    return a;
}


是不是觉得 a+= 2; 这一句有点多余呢?完全可以直接 return a + 2; 啊。接下来会知道这里为什么添加这一句。


写个小程序测试一下此函数:


#include <iostream>
using namespace std;

int addTwo(int a)
{
    a+=2;
    return a;
}

int main()
{
    int number(4), result;
    result = addTwo(number);
    
    cout << "number的值是 : " << number << endl;
    cout << "调用函数后结果是 : " << result << endl;
    return 0;
}


运行这个程序,输出:


number的值是 : 4

调用函数后结果是 : 6


程序中最关键的一句当然就是


result = addTwo(number);


当addTwo函数被调用时,其实发生了很多事情:


  1. 程序获取number的值,发现是4。

  2. 申请了内存中的一块地址(好像一个抽屉),抽屉上的标签是a(名字),类型是int,抽屉里存放的值等于number的值,为4。

  3. 程序进入到函数体中。

  4. 将变量a加上2,a变为6。

  5. a的值被作为函数的返回值赋给变量result。result的值也变为6了。

  6. 跳出函数。


重要的一点是:变量number被拷贝到了内存的一个新的地址上(新的抽屉),这个新的抽屉的标签是a。我们说参数a是通过值来传递的(number的值拷贝给了a),叫做值传递。当我们的程序在addTwo函数里时,内存中的情况大致如下图所示:




因此我们在内存中使用了三个"抽屉"。注意:number变量并没有被改变。


所以在程序中操作变量a的时候,已经与number没什么关系了,只是操作number的一份值的拷贝。


引用传递


我们之前的课程初步介绍了引用(reference)的概念。其实引用这个名词太抽象,一般第一次接触引用的朋友都觉得:哇,好高深的感觉。其实一点也不高深,引用应该更确切地被称为"别名"。


比如小编叫谢恩铭,有的人可能会戏称为小铭。那小铭就是我的别名啦,这两个名字是不是指同一个人呢?是,都是指向略微顽皮,但在编程技术上绝不马虎的小编。


除了前面说的值传递的方式,也就是将变量number的值拷贝到变量a中。


除了值传递,我们也有其他的方式。可以给内存中名为number的抽屉再贴一个标签,叫做a。等于给number变量取了一个别名,称为a。此时函数的参数就要使用引用了。如下:


int addTwo(int& a)  // 注意 & 这个表示引用的符号
{
    a+=2;
    return a;
}


当我们调用函数时,就没有之前那样的值拷贝了。程序只是给了number变量一个别名而已。当我们的程序在addTwo函数里时,内存中的情况大致如下图所示:




这次,变量a和变量number是指向同一块内存地址(同一个抽屉),抽屉里存放的值是4,a和number只是这个抽屉的两个不同标签而已。我们说变量a是通过引用传递的,称为引用传递。


引用很特别,从上图中我们看到,我们在内存中并没有给a这个引用变量分配新的内存地址,它是指向它所引用的number这个变量的内存地址。所以引用变量和它所指向的变量的内存地址是一样的。我们可以来测试一下:


#include <iostream>
using namespace std;

int main()
{
    int number(4);
    int &refNumber = number;
    cout << "number的内存地址是 : " << &number << endl;
    cout << "refNumber的内存地址是 : " << &refNumber << endl;
    return 0;
}


运行,输出:



如上图中所示,变量number和引用变量refNumber的内存地址是完全一样的。


既然值传递可以帮我们解决问题,为什么要用引用传递呢?既生瑜何生亮呢?引用有什么好处呢?


首先,从上例中我们知道了:引用不需要在内存中新开辟一块地址,减少开销。


C语言没有引用的概念,但是C++有。引用传递可以让我们的函数addTwo直接修改参数。继续使用之前的测试程序,只不过这次在函数参数中的是一个引用:


#include <iostream>
using namespace std;

int addTwo(int &a)
{
    a+=2;
    return a;
}

int main()
{
    int number(4), result;
    result = addTwo(number);
    
    cout << "number的值是 : " << number << endl;
    cout << "调用函数后结果是 : " << result << endl;
    return 0;
}


运行这个程序,输出:


number的值是 : 6

调用函数后结果是 : 6


为什么number的值变成了6呢?之前在值传递的例子中,number的值是不变的。


很简单,我们之前说了,引用其实就是别名。a就是number的一个别名。那么其实它们指向的是同一个内存地址,因此对a做加2操作,也就是对number做加2操作,当然会改变number的值。


因此,引用的使用要谨慎,因为它会改变所引用的那个对象。


对于引用的使用,经典的例子也就是swap函数了,用于交换两个参数的值。


#include <iostream>
using namespace std;

void swap(double& a, double& b)
{    
    double temporary(a);  // 将变量a的值保存到变量temporary中    
    a = b;           // 用b的值替换a的值    
    b = temporary;   // 将temporary的值赋给b,就是用a的旧值替换b的值
}

int main()
{    
    double a(2.3), b(5.4);    
    cout << "a的值是 " << a << "  b的值是 " << b << endl;    
    swap(a,b);   // 使用swap函数    
    cout << "a的值是 " << a << " b的值是 " << b << endl;    
    return 0;
​}


运行程序,输出:


a的值是 2.3 b的值是 5.4

a的值是 5.4 b的值是 2.3


可以看到a和b这两个变量的值被交换了。


假如我们不用引用传递,而是用传统的值传递,那么我们交换的就只是变量的拷贝而已,并不是变量本身。


暂时,引用的概念对大家可能还是有些抽象,但是不要担心,我们之后的课程会经常用到引用的,关于引用还有不少要说的呢,慢慢来,熟能就生巧了嘛。


之后,学到指针那章,还会讲解引用和指针的异同。


不可改写的引用传递


既然说到了引用,就需要介绍一个引用的惯常用法。这个用法在接下来的课程中会很有用,我们先来一窥堂奥。


我们说引用传递相比值传递有一个优势:不进行任何拷贝。


设想一下,如果我有一个函数,其中一个参数是string类型的字符串。假如你的这个字符串变量里面的内容是很长的一串字符串,例如有一本小书那么多的字符。那么拷贝这么长的一个字符串的开销可是很大很费时的,即使拷贝是在内存中进行。这样的拷贝完全没什么意义,因此我们就要避免使用值传递的方式。


当然,你会自豪地对我说:我们可以用引用传递啊。是的,好主意。使用引用传递的话,就不用拷贝了。但是,引用传递有一个小缺陷:可以修改所指向的对象。不过这也不能说是缺陷吧,毕竟正是因此引用也才显得有用啊。


void f1(string text);  // 进行耗时的拷贝
{
}
void f2(string& text);  // 不进行拷贝,函数可以直接修改string变量的值
{
}


解决办法就是使用:不可改写的引用传递。


之前的课程我们介绍过const这个关键字,被它修饰的变量就变成不可改变的变量。


我们用引用来避免了拷贝,再用const变量修饰引用就可以使引用不能被修改了。


void f1(string const& text); // 不进行拷贝,函数也不能修改string变量的值
{
}


目前来说这个用法对我们貌似没太大用处,但是本课程的第二部分会学习面向对象编程,到时会经常用到这个技术。



头文件和源文件,合理安排


在上一课介绍函数时,我们已经说过函数是为了可以重用(重复使用)已经创建的"砖块"(代码块)。


目前,我们已经学习了如何创建自定义的函数,但是这些函数暂时还是和main函数位于同一个文件中。我们还没有真正很好地重用它们。


和C语言一样,C++也允许我们将程序分割成不同的源文件。在这些源文件中我们可以定义自己的函数。


在要用到这些函数时,引入文件的内容,也就引入了这些函数。这样我们的函数就可以为不同的项目所使用了。借此我们就可以真正意义上地重用这些被分割开的砖块来建造房屋了。


必要的文件


但是为了更好地组织程序,计算机先驱们用了两种文件,而不是一种文件(为何"多此一举",学下去就知道了):


  • 源文件(source file):以.cpp结尾(也可以由.cc,.cxx,.C结尾),包含函数的具体源代码。

  • 头文件(header file):以.h结尾(也可以由.hxx,.hpp结尾),包含函数的描述,术语称为函数的原型(prototype)。


这下知道为什么今天这课的标题里有"文件源头"了吧?就是指头文件和源文件。


那么我们还是以addTwo函数为例,来分别创建源文件和头文件吧。


int addTwo(int number)
{
    int value(number + 2);
    return value;
}


我们会用CodeBlocks这个IDE(集成开发环境)来演示。如果你是用文本编辑器(比如Vim,Emacs,Sublime,等)加gcc(编译器)来编写和编译代码的,那么直接创建文件就好了。


而且,我们默认你的Codeblocks的C++项目已经创建好了,也就是说已经有了一个自带main.cpp文件的项目,名称随便取。


如下图:




我们只是演示如何创建函数的源文件和头文件。


源文件


首先,我们按照以下顺序打开菜单栏:File > New > File。




然后在以下窗口中选择C/C++ source:




然后,点击Go这个按钮。进入下图的窗口:




选择C++,点击Next按钮。进入下图所示窗口:




填写要创建的源文件的完整路径(选择目录,然后填写文件名)。我们把文件的目录选择为和main.cpp一样的目录。


文件名字尽量做到见名知意,不要来个 1.cpp,以后都不知道这文件干什么的。


我们的演示中使用了math.cpp,因为我们的addTwo函数进行的是数学运算,就用math(mathematics的缩写)来命名。


然后可以把Debug和Release都选上。


点击Finish。你的源文件就创建好了。我们接着来创建头文件。


头文件


开头和之前创建源文件是一样的。依次打开菜单:File > New > File,然后在下图窗口中选择C/C++ header:




点击Go进入下一步:



建议头文件的名字和源文件的名字一样,只是后缀名不同。


头文件的也放在和main.cpp一样的目录。那个MATH_H_INCLUDED是自动生成的,不需要改动。


点击Finish。你的头文件就创建好了。


一旦源文件和头文件都创建好之后,你的项目应该是类似下图这样的:




现在文件有了,我们就要往里面填写内容了,就是来定义我们的函数。


完成源文件


之前说过,源文件包含了函数的具体定义。这是一点。


另外还有一点理解起来略为复杂:编译器需要获知源文件和头文件之间存在联系。


目前,我们的源文件是空的,我们首先需要往里面添加这样的一行:


#include "math.h"


你应该对这行代码的格式不陌生,我们之前说过。


#include <iostream>


这一行我们熟悉的代码,是为了引入C++的标准iostream(输入输出流)库。


那么,#include "math.h" 就是为了引入math.h 这个文件中定义的内容。


但是,为什么C++标准库的头文件是包括在尖括号中,而我们自己定义的math.h是包括在双引号中呢?


C++有一些编写好的头文件(比如标准函数库等等),它们存放在系统的include文件夹里。当我们使用#include <文件名> 命令时,编译器就到这个文件夹里去找对应的文件。


显然,用这种写法去包含一个我们自己编写的头文件(不在系统include文件夹里)就会出错了。


所以包含C++提供的头文件时,应该使用尖括号。


相反地,#include "文件名" 命令则是先在当前文件所在的目录搜索是否有符合的文件,如果没有再到系统include文件夹里去找对应的文件。


现在我们就来完成math.cpp的内容,其实很简单,如下:


#include "math.h"

int addTwo(int number)
{
    int value(number + 2);
    return value;
}


完成头文件


我们可以看到,math.h这个头文件的初始内容不是空的。而是:


#ifndef MATH_H_INCLUDED
#define MATH_H_INCLUDED

#endif // MATH_H_INCLUDED

这几句指令又是什么意思呢?


它们是为了防止编译器多次引入这个头文件。编译器并不总是那么智能的,有可能会循环引入同样的头文件。


前面说过,头文件包含函数的原型。我们要把原型写在上面第二句话和第三句话之间,如下所示:


#ifndef MATH_H_INCLUDED
#define MATH_H_INCLUDED

int addTwo(int number);

#endif // MATH_H_INCLUDED


上面的三个以#开头的语句是如何防止重复引入头文件内容的呢?


这几句语句被称为条件编译语句。


首先,如果是第一次引入头文件,那么,就会读到第一句命令:


#ifndef MATH_H_INCLUDED


ifndef是if not defined的缩写,表示"如果没有定义"。整句的意思就是:假如MATH_H_INCLUDED没有被定义。


那如果是第一次引入此头文件,MATH_H_INCLUDED确实还没被定义。#ifndef MATH_H_INCLUDED 成立。


所以就进入此条件编译语句中,执行 #define MATH_H_INCLUDED,定义(define是英语"定义"的意思)MATH_H_INCLUDED


然后引入


int addTwo(int number);


最后,来到第三句条件编译指令:


#endif // MATH_H_INCLUDED


endif是end if的缩写,表示"结束if条件编译",后面的 // MATH_H_INCLUDED 是注释,会被编译器忽略。


然后,假如之后我们又引入此头文件,编译器读到第一句条件编译指令,


#ifndef MATH_H_INCLUDED


发现之前已经定义过了 MATH_H_INCLUDED,所以#ifndef MATH_H_INCLUDED不成立。因此不进入条件编译的语句中。直接跳出。就不会再引入


int addTwo(int number);


了。很妙吧。


当然了,我们因为用的是CodeBlocks这样的IDE,它帮你自动生成了这三句条件编译指令。假如我们用的是文本编辑器,那是没有这三句自动生成的语句的。就需要我们自己手工写了。


MATH_H_INCLUDED可以改成其他任何文本,只需要保证这三句条件指令中这个文本是一样的就行,不过也要保证没有两个头文件是使用同样的文本。


注意。我们以上的函数原型其实就是源文件中函数定义的头一行,只不过没有了大括号包括起来的函数体,而且在参数列表的之后多了一个分号。


以上演示的是简单的情况,是在函数的参数是普通变量类型时。假如我们要使用诸如string这样的类型。那么头文件有些许改动哦,如下:


#ifndef MESSAGE_H_INCLUDED
#define MESSAGE_H_INCLUDED

#include <string>

void displayMessage(std::string message);

#endif // MESSAGE_H_INCLUDED


可以看到,需要在函数原型之前加 #include <string>这句指令,表示要引入string这个C++标准库的头文件。而且,在参数列表中,string类型前也要加上std::


为什么呢?


还记得我们之前在使用cout,cin时,并没有再前面加上std:: 吗?


那是因为我们在使用cout,cin前,已经用了


using namespace std;


表示使用std命名空间(关于命名空间,之后的课程会学习)。


因为cout和cin是位于命名空间std中,因此就可以省略了std::


但是,这里的头文件中并没有 using namespace std; 这句指令(绝对不建议把using namespace std; 放在头文件中,要放到源文件中),因此我们需要在string前加上std::,因为string也位于std命名空间。


现在,我们来测试一下addTwo函数吧。


我们只需要在main.cpp中加入一行 #include "math.h",表示引入math.h头文件,而其定义是在math.cpp中,但是因为math.cpp中已经有了#include "math.h",因此我们在main.cpp中就不要指定math.cpp来引入了。(是否已经晕了...)


#include <iostream>
#include "math.h"
using namespace std;

int main()
{
    int a(2), b(3);
    cout << "a的值是 : " << a << endl;
    cout << "b的值是 : " << b << endl;
    b = addTwo(a);        // 函数调用
    cout << "a的值是 : " << a << endl;
    cout << "b的值是 : " << b << endl;
    return 0;
}


运行,显示:


a的值是 : 2

b的值是 : 3

a的值是 : 2

b的值是 : 4




好了,现在我们真正做到了将可重用的砖块分开存放了。如果之后你想要在另一个项目中使用addTwo函数,只需要拷贝math.h和math.cpp这两个文件过去就可以了。然后在要使用的源文件中写上 #include "math.h文件所在的路径/math.h"


我们也可以在同一个文件中定义多个函数。一般来说,我们都会把同一类函数放到同样的文件中,比如那些数学运算的函数放到一个文件中,那些用于显示菜单的函数放到另一个文件中,等等。


编程,就要有条理,有规划。


好的注释是代码的一半


最烦看到大型项目中代码不怎么写注释的了,特别是那种许多人合作开发的项目,大家的命名规范和编程风格不尽相同,如果没有注释,看起来相当累。


一般不写注释的程序员不是好程序员,只顾自己写代码写得爽,完全不关心后来人是不是被他的代码拍死在沙滩上。应该被拖出去高速揉脸五分钟(这是什么呆萌的刑罚,那画面太美我不敢看):



写注释对于函数特别有用。因为你很可能会用到别的程序员写的函数。那么如果已经有了对函数的作用等的注释,你一般就不需要读函数的所有代码了(程序员是会"偷懒"的)。


对于一个函数,一般我们的注释要包含三方面:

  1. 函数的功用

  2. 参数列表

  3. 返回值


我们就来给addTwo函数写个注释吧:


#ifndef MATH_H_INCLUDED
#define MATH_H_INCLUDED

/*
 * 对参数进行加2操作
 * - number : 要进行加2操作的数
 * 返回值 : number + 2
 */
int addTwo(int number);

#endif // MATH_H_INCLUDED


写注释的格式随个人爱好不尽相同。不顾一般常用的注释格式是这样(doxygen的格式):


/** 
 * \brief 对参数进行加2操作
 * \param number 要进行加2操作的数
 * \return number + 2
 */
 int addTwo(int number);



参数的默认值


函数的参数,我们已经学习过了。如果一个函数有三个参数,那么我们需要向函数提供这三个参数的值,才能让函数被正常调用。


但是,也并不一定。我们用以下函数来学习一下默认函数参数的用法:


int secondNumber(int hours, int minutes, int seconds)
{
    int total = 0;
    total = hours * 60 * 60;
    total += minutes * 60;
    total += seconds;
    return total;
}


这个函数的作用就是根据给出的小时数,分钟数,秒数,计算出一共有多少秒。非常好理解。


因此,我们说hours, minutes, seconds是函数secondNumber的三个参数。在函数被调用时,我们须要给它这三个参数一定的值。



这个我们之前早就学过了。


参数默认值


我们要学习新知识点,就是我们其实可以给函数的参数指定默认值。假如函数在被调用时这些参数没有被指定值,那么就用默认的值。


首先来看没有指定默认值的情况:


#include <iostream>
using namespace std;

// 函数原型
int secondNumber(int hours, int minutes, int seconds);

// 主函数
int main()
{
    cout << secondNumber(1, 10, 25) << endl;
    return 0;
}

// 函数定义
int secondNumber(int hours, int minutes, int seconds)
{
    int total = 0;
    total = hours * 60 * 60;
    total += minutes * 60;
    total += seconds;
    return total;
}


运行,显示:


4225


因为 1小时等于3600秒,10分钟等于600秒,25秒等于... 25秒。所以 3600 + 600 + 25 = 4225


现在,假如我们要将secondNumber函数的部分参数设为有默认值的参数。例如,我指定分钟数minutes和seconds默认都为0,因为一般我们都用整点比较多,就只有小时数。


我们须要改写函数的原型,如下所示:


int secondNumber(int hours, int minutes = 0, int seconds = 0);


看到了吗?在minutes和seconds后面,都多了


= 0


这个就是函数的默认参数值。此处都设为0。测试如下:


#include <iostream>
using namespace std;

// 函数原型
int secondNumber(int hours, int minutes = 0, int seconds = 0);

// 主函数
int main()
{
    cout << secondNumber(1) << endl;
    return 0;
}

// 函数定义,没有写参数默认值
int secondNumber(int hours, int minutes, int seconds)
{
    int total = 0;
    total = hours * 60 * 60;
    total += minutes * 60;
    total += seconds;
    return total;
}


运行,显示:


3600


这是因为hours为1,而minutes和seconds没指定,默认为0。1小时等于3600秒。


请注意:函数参数的默认值只能写在函数的原型中,假如写在函数的定义中,编译器会报错。


假如改为:


cout << secondNumber(1,10) << endl;


则输出:


4200


因为hours为1;minutes为10;seconds没指定,默认为0


所以3600 + 600 = 4200


特殊情况


函数的默认参数的使用中,会出现多种情况,有些比较特殊,我们举例如下(还是以secondNumber函数为例):


1.假如我指定了hours和seconds的值,但是minutes的值没指定,那么如何做呢?


你不可以使用这样的形式:


cout << secondNumber(1,,25) << endl;


这样是会出错的。在C++中,我们不能跳过参数,即使它有默认值。如果我们给开头和结尾的参数赋了值,那么中间的参数也需要赋值。


还是得这么写:


cout << secondNumber(1, 0, 25) << endl;


2.这样写可以吗?


int secondNumber(int hours = 0, int minutes, int seconds);


不可以,会出错。因为参数的默认值必须要靠右写。也就是说,假如只有hours这个参数有默认值,那么必须要把hours参数写到最右边,如下所示:


int secondNumber(int minutes, int seconds, int hours = 0);


这样就没有错误了。


3.我能否将所有参数都设为有默认值?


可以。举例如下:


int secondNumber(int hours = 0, int minutes = 0, int seconds = 0);
cout << secondNumber() << endl;


输出就是0了。


设置默认参数的两条主要规则


  1. 只有函数原型才能包含参数的默认值。

  2. 参数的默认值是写在参数列表之后。而且是靠右边写。



总结


  1. 一个函数可以接收数据(通过参数),也可以返回数据(通过retur函数可以接收引用作为参数,以直接修改内存中的信息。

  2. 当你的程序渐渐多起来时,建议将其分为不同的文件。每个文件中存放特定的函数,而且文件是成对组织的:.cpp文件存放函数的定义,.h文件存放函数的原型。

  3. 一定要写好注释。



第一部分第九课预告


今天的课就到这里,一起加油吧!

下一课我们学习:数组威武,动静合一

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值