关于函数,上一章介绍过,本章主要学的是自定义的函数。

用户函数


用户自定义的函数称为 用户函数,其语法如下:

返回值类型 函数名() // 函数头,告诉编译器函数的存在
{
    // 函数体
}

示例:

#include<iostream>

using namespace std;

// 用户自定义函数
void print() 
{
    cout << "doPrint()" << "\n";
}

// 主函数
int main() 
{
    print();
    return 0;
}

不支持嵌套函数


和其他主流编程语言不同的是,C++不支持函数的嵌套:

错误定义:

int main()
{
    void foo()
    {
        cout << "foo";
    }
    foo();
    return 0;
}

函数返回值


main函数 中就使用了一个 return 0;,这个就是函数的返回值,将函数的结果返回给了计算机,计算机判断程序是否正确运行。

返回值

用户自定义函数时,函数能将返回值返回给调用方。创建方法如下:

定义函数返回值类型 函数名()
{
    // 函数主体

    return 对应函数返回值类型的值;
}

使用示例:

int returnTest()
{
    return 100;
}

int main()
{
    cout << returnTest();
    return 0;
}

最后会输出 100

无返回值函数

在一些函数中,我们只需要执行函数体,而不需要将结果返回,这时就可以使用无返回值函数:

void 函数名()
{
    // 函数体
}

使用示例:

#include<iostream>

using namespace std;

void voidTest()
{
    cout << "None";
}

int main()
{
    voidTest();
    return 0;
}

只会输出 None

函数参数


函数参数是函数头中定义的变量。只能作用于函数体中,使用方式如下:

#include<iostream>

using namespace std;

void voidTest(int n)
{
    cout << n << "None";
}

int main()
{
    voidTest(1);
    return 0;
}

输出: 1None

当然也可以同时社多个参数:

int add(int x, int y)
{
    return x + y;
}

int main()
{
    cout << add(4, 5) << "\n";
    return 0;
}

使用函数返回值作为参数

这算是一种使用技巧,我们可以将函数返回值作为另一个函数的参数

#include<iostream>

using namespace std;

int getValueFromUser()
{
    cout << "Enter an integer: ";
    int input {};
    cin >> input;

    return input;
}

void printDouble(int value)
{
    cout << value << " doubled is: " << value * 2 << "\n";
}

int main()
{
    printDouble(getValueFromUser());
    return 0;
}

输出结果:

Enter an integer: 12
12 doubled is: 24

局部变量


函数体中定义的变量称为局部变量,只能作用在该函数中。

int add(int x, int y)
{
    int z{ x + y }; // z 是局部变量
    return z;
}
  • 实际上,参数也算是一个局部变量

生命周期

变量既然被实例化(创建),那么就会被销毁的时候,我们将一个变量从创建到销毁的过程,称为“生命周期”。

#include <iostream>

using namespace std;

void doSomething()
{
    cout << "Hello!\n";
}

int main()
{
    int x{ 0 }; // x 生命周期开始
    doSomething(); // 调用函数时,x 仍然存活
    return 0;
    // x 生命周期结束
}

前向声明


介绍前向声明前,先看一个示例程序:

#include <iostream>

using namespace std;

int main()
{
    cout << "The sum of 3 and 4 is" << add(3, 4) << "\n";
    return 0;
}

int add(int x, int y)
{
    return x + y;
}

运行效果:

提示: add 没有定义?
为什么呢,因为main函数只能识别在它之前声明的函数,要怎么解决呢?以下有两种解决方法

解决办法一:重新排序函数定义

将函数重新排序:

#include <iostream>

using namespace std;

int add(int x, int y)
{
    return x + y;
}

int main()
{
    cout << "The sum of 3 and 4 is" << add(3, 4) << "\n";
    return 0;
}

解决方法二:使用前向声明

什么是前向声明呢,就是在原函数不改变的情况下,再main函数智商单独声明,只声明,没有函数体。

#include <iostream>

using namespace std;

int add(int x, int y);

int main()
{
    cout << "The sum of 3 and 4 is" << add(3, 4) << "\n";
    return 0;
}

int add(int x, int y)
{
    return x + y;
}

为什么需要前向声明?

如果重排或者一开始就写在main前面即可正常工作了,为什么还需要前向声明呢?

  1. 大多情况下,前向声明用于告知编译器在其它代码中定义的函数。这种情况下,无法重新排序。
  2. 前向声明页可以用于不确定排序位置的方式定义函数。方便阅读
  3. 解决循环依赖问题。在两个函数相互调用的情况下,不可以重新排序。

| 术语 | 含义 | 样例 |
| — | ——————- | ——————— |
| 定义 | 实现函数,或者实例化变量,定义也是声明 | int x; void foo() { } |
| 声明 | 告诉编译器标识符的信息 | void foo(); int x; |
| 纯声明 | 非定义的声明 | void foo(); |
| 初始化 | 给一个对象初始值 | int x { 2 }; |

多代码文件程序


在同一个目录中,示例如下:

add.cpp

int add(int x, int y)
{
    return x + y;
}

main.cpp

#include <iostream>

using namespace std;

int main()
{
    cout << "The sum of 3 and 4 is: " << add(3, 4) << "\n";
    return 0;
}

无法编译:

这时候我们就需要使用之前的前向声明了,修改代码如下:

main.cpp

#include <iostream>

using namespace std;

int add(int x, int y);

int main()
{
    cout << "The sum of 3 and 4 is: " << add(3, 4) << "\n";
    return 0;
}

编译时指定编译的cpp文件。

g++ main.cpp add.cpp -o main
  • g++ xxx.cpp xxx.cpp:要编译的文件
  • -o xxx:输出到的文件名称

手动执行exe文件:

有IDE的话可以简单不少。

以dev-c++为例:

  1. 新建项目
  2. 编写代码如上,记得添加到项目(新建时会提示)
  3. 直接编译(快捷键 ctrl+9)

命名冲突和命名空间简介


C++要求所有标识符都是不模糊的。如果编译器或链接器无法区分它们,将两个相同标识符引入同一程序,则编译器或链接器会提示程序出错。此错误称为命名冲突

示例:

a.cpp

#include <iostream>

void myFcn(int x)
{
    std::cout << x;
}

main.cpp

#include <iostream>

using namespace std;

void myFcn(int x)
{
    cout << 2 * x;
}

int main() {
    myFcn(10);
}

在独立编译这两个文件的情况下是没有问题的,但如果是在项目中(编译器链接时),那么就会产生命名冲突。

大多数命名冲突发生在两种情况下:

  1. 两个同名函数(或全局变量)在程序不同文件中同时存在,会导致上述链接错误。
  2. 两个同名函数(或全局变量)在同一文件中存在,导致编译错误。

命名空间


什么是命名空间

命名空间是一个区域,为其内声明的名称提供了一个范围区域(命名空间范围),其中声明的任何名称都不会在其他范围内的相同名称冲突。

在同一命名空间中,所有名称必须唯一,否则将导致命名冲突。

全局命名空间

在C++中,任何未在类、函数或命名空间内定义的名称都被认为是全局命名空间(有时也称为全局范围)的隐式定义命名空间的一部分。

全局命名空间只能出现声明和定义语句。因此如表达式语句等不能放在全局命名空间中(全局变量的初始值设定项除外)

#include <iostream> // 被预处理器处理

// 下面的语句都是在全局命名空间中的
void foo(); // 前向函数声明
int x;         // 可以编译通过但不推荐,全局命名空间中的未初始化变量
int y { 5 };   // 可以编译通过但不推荐,全局命名空间中的初始化变量
x = 5;         // 编译失败,可执行语句

int main()     // 函数定义
{
    return 0;
}

void goo();    // 前向函数声明

std命名空间

前面需要使用 cout 时,引入了命名空间 std

using namspace std

如果不使用 using 依然可以使用 std::cout 表示,

为什么std要使用命名空间?
因为C++为了防止命名冲突,为标准库的所有代码移到了命名空间 std 中了。

避免使用using指令

避免在程序顶部或头文件中使用using指令(例如使用命名空间std)。它们违反了最初添加名称空间的目的。

预处理器简介


编译项目时,编译器并不会直接编译文件,而是先经过预处理。

预处理器主要做了一些优化的操作,如去除注释,将代码文件以换行结束等。最重要的是,它会处理 #inclue 指令。

预处理器完成对代码文件的处理,处理结果称为翻译单元。翻译单元是编译器随后编译的基本单元。

预处理指令

预处理指令是以 # 开头、以 换行符 结尾的指令。

常见的预处理指令如下:

include

#include 文件时,预处理器将 #include 指令替换为所包含文件的内容。然后对包含文件内容进行预处理,最后处理文件神域部分,如下:

#include <iostream>

int main()
{
    std::cout << "Hello, world!\n";
    return 0;
}

预处理器运行此程序时,预处理器将名为 “iostream”的文件内容替换 #include,然后处理引用的内容和文件其余部分。

宏定义

#define 指令用于创建宏。C++中,宏是一条规则,定义如何将输入文本转换为替换输出文本。

宏有两种基本类型:类对象类函数宏

  • 类函数宏:类似函数,通常使用它们都是不安全的,因为它所做的是都可由正常功能完成
  • 类对象宏:默认用法

类对象宏定义方法:

#define 标识符
#define 标识符 替换温拌

第一个定义没有替换文本,第二个有。这些是预处理指令(不是语句),请注意,两种形式都没有以分号结尾。

宏的标识符与普通标识符使用相同的命名规则:可以使用字母、数字和下划线,不能以数字开头,且不以下划线开头。按照惯例,宏名称通常都是大写的,由下划线分隔

类型对象宏示例程序

#include <iostream>

#define MY_NAME "Fly"

int main()
{
    std::cout << "My name is: " << MY_NAME << "\n";
    return 0;
}

最后输出的是

My name is: Fly
关于无替换文本的类对象宏

如:

#define USE_YEN

这种形式的宏工作方式将会将标识符任何进一步出现都会被删除,并且不能替换任何内容

目前(本章为止)还没有该指令的使用场景。

条件编译

条件编译预处理指令运行在指定条件下编译或不编译。
目前只介绍其中三个:

  • #ifdef
  • #ifndef
  • #endif

ifdef

#ifdef 预处理其指令允许预处理器检查以前是否 # 定义了标识符。如果是,则编译 #ifdef#endif 之间的代码。如果不是,则忽略代码 。

#include <iostream>

#define PRINT_JOE

int main()
{
#ifdef PRINT_JOE
    std::cout << "Joe\n"; 
#endif

#ifdef PRINT_BOB
    std::cout << "Bob\n";
#endif

return 0;
}

输出 Joe

ifndef

#ifdef 相反,如果不符合则编译,符合则不编译

#include <iostream>

#define PRINT_JOE

int main()
{
#ifdef PRINT_JOE
    std::cout << "Joe\n"; 
#endif

#ifndef PRINT_BOB
    std::cout << "Bob\n";
#endif

return 0;
}

输出:

Joe
Bob

if 0

使用 #if 0 可以排除不需编译的代码块,效果和 注释一样

#include <iostream>


int main()
{
    std::cout << "Joe\n";

#if 0
    std::cout << "Bob\n";
    std::cout << "Steve\n"; 
#endif

    return 0;
}

类对象宏不影响其他预处理器指令

预处理器的最终输出不包含预处理指令——会在编译之前解析,因为编译器不知道如何处理。

#define FOO 9 // 宏定义

#ifdef FOO // 这里的预处理指令不会受影响
    std::cout << FOO << '\n'; // FOO 被替换为 9,因为这里是普通代码 
#endif

#define的作用范围

#define 在编译之前解析,从文件中,从上到下,逐个文件进行处理。

考虑以下程序:

#include <iostream>

void foo()
{
#define MY_NAME "Fly"
}

int main()
{
    std::cout << "My name is: " << MY_NAME << '\n';

    return 0;
}

尽管#define MY_NAME “Fly” 是在函数foo中定义的,但预处理器不理解C++概念(如函数)。因此,该程序的行为与#define MY_NAME “Fly” 在函数foo之前或之后定义的程序相同。为了可读性,通常需要在函数外部设置 #define

一旦预处理器处理完成,该文件中定义的所有#define定义的标识符将被丢弃。意味着指令仅从定义点到定义它们的文件末尾有效。在一个代码文件中定义的指令不会影响同一项目中的其他代码文件。

考虑以下示例:

function.cpp:

#include <iostream>

void doSomething()
{
#ifdef PRINT
    std::cout << "Printing!\n";
#endif
#ifndef PRINT
    std::cout << "Not printing!\n";
#endif
}

main.cpp:

void doSomething(); // 前向声明 doSomething()

#define PRINT

int main()
{
    doSomething();

    return 0;
}

上述程序将打印:

Not printing!

PRINT是在main.cpp中定义的,对function.cpp的代码没有任何影响(PRINT只是从定义点到main.cpp末尾定义有效)。

头文件


头文件及其用途

随着程序愈加复杂(使用更多文件),在不同cpp文件中要使用大量重复的前向声明。

C++中提供了一种文件类型,名为头文件,其拓展名大多为 .h.hpp,亦或者没有拓展名,它可以将声明传播到cpp代码中。

使用标准库头文件

回到HelloWorld程序,我们如果没有 include iostream 头文件,那么将无法使用 std::cout

原因是std::cout已在“iostream”头文件中向前声明。

使用头文件传播前向声明

如前所述,为要使用的位于另一个文件中的函数添加前向声明是重复冗余的。

编写一个头文件来减轻负担。编写头文件非常容易,因为头文件仅由两部分组成:

  1. 头文件保护
  2. 头文件的实际内容

头文件的创建:

  • 在编辑器中创建一个文件,该文件与源(.cpp)文件位于同一目录。
  • 若是ide中,ide创建时,选择头文件

示例:

add.h

// 这里需要一个头文件保护,目前简略版先不写

// 头文件内容
int add(int x, int y);
```cpp
#include <iostream>
#include "add.h"

int main() {
    std::cout << add(10, 15);
}

头文件最佳实践

下面是创建使用头文件的建议。

  1. 始终使用头文件保护(将在下一课中介绍)。
  2. 不要在头文件中定义变量和函数(目前)。
  3. 为头文件提供与其关联的源文件相同的名称(例如,grades.h与grades.cpp成对出现)。
  4. 每个头文件都应该有一个特定的功能,并且尽可能独立。例如,将与功能A相关的声明放在A.h中,将与功能B相关的声明放在B.h中。如果只关心A,则可只包含A.h,而不获取与B相关的内容。
  5. 注意为代码文件中使用的功能显式包含对应的头文件。
  6. 头文件都应该能单独编译(应该#include需要的每个依赖项)。
  7. 仅#include 需要的内容(不要因为允许而include所有内容)。
  8. 不要#include .cpp文件。
  9. 在头文件中放置关于某段代码的作用或使用文档。它更可能在那里被看到。描述代码如何工作的文档应保留在源文件中。RedundantDefinitionProblem

头文件保护


重复定义问题

多次定义变量标识符的程序将导致编译错误:

类似,如果多次定义相同函数也会导致编译错误:

虽然这些程序很容易修复(删除重复的定义),但使用头文件,很容易导致头文件中的定义被多次包含。当头文件#include另一个头文件(这是常见的)时,可能会发生这种情况。

考虑以下示例:

square.h:

int getSquareSides();

wave.h:

#include "square.h"

这样如果在main中同时引用,依然会编译异常!因此,引出我们的一种机制。

头文件保护

上述问题,可以使用头文件保护的机制来避免。头文件保护是采用以下形式的条件编译指令:

#ifndef SOME_UNIQUE_NAME_HERE
#define SOME_UNIQUE_NAME_HERE

// 在这里放置你的声明

#endif

解释如下:

  • ifndefifndef
    • SOME_UNIQUE_NAME_HERE:首先检查是否定义改保护,实际上的名字可以自定义。
  • #define宏定义
    • SOME_UNIQUE_NAME_HERE:定义一个宏,通过和ifdef的联动,先检查是否存在-> 不存在 -> 定义 -> 执行下方的头文件内容。这样子就可以保证只会定义一次。从而达到头文件保护的效果。

推荐命名方式:大写头文件,如:SQUARE_h

当然,如果是在引用中,我们依然推荐使用头文件保护,这样可以避免多个头文件引用同一个头文件导致的内存浪费。

#ifndef WAVE_H
#define WAVE_H

#include "square.h"
#endif

#pragma once

现代编译器使用#pragma预处理器指令支持更简单的替代形式的头文件保护:

#pragma once

// your code here

由于#pragma once不是由C++标准定义的,因此一些编译器可能不会实现它。

比如:

#pragma once

int getSquareSides();