C++入门——3. 函数
关于函数,上一章介绍过,本章主要学的是自定义的函数。
用户函数
用户自定义的函数称为 用户函数,其语法如下:
返回值类型 函数名() // 函数头,告诉编译器函数的存在
{
// 函数体
}
示例:
#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前面即可正常工作了,为什么还需要前向声明呢?
- 大多情况下,前向声明用于告知编译器在其它代码中定义的函数。这种情况下,无法重新排序。
- 前向声明页可以用于不确定排序位置的方式定义函数。方便阅读
- 解决循环依赖问题。在两个函数相互调用的情况下,不可以重新排序。
| 术语 | 含义 | 样例 |
| — | ——————- | ——————— |
| 定义 | 实现函数,或者实例化变量,定义也是声明 | 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++为例:
- 新建项目

- 编写代码如上,记得添加到项目(新建时会提示)

- 直接编译(快捷键 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);
}
在独立编译这两个文件的情况下是没有问题的,但如果是在项目中(编译器链接时),那么就会产生命名冲突。
大多数命名冲突发生在两种情况下:
- 两个同名函数(或全局变量)在程序不同文件中同时存在,会导致上述链接错误。
- 两个同名函数(或全局变量)在同一文件中存在,导致编译错误。
命名空间
什么是命名空间
命名空间是一个区域,为其内声明的名称提供了一个范围区域(命名空间范围),其中声明的任何名称都不会在其他范围内的相同名称冲突。
在同一命名空间中,所有名称必须唯一,否则将导致命名冲突。
全局命名空间
在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”头文件中向前声明。
使用头文件传播前向声明
如前所述,为要使用的位于另一个文件中的函数添加前向声明是重复冗余的。
编写一个头文件来减轻负担。编写头文件非常容易,因为头文件仅由两部分组成:
- 头文件保护
- 头文件的实际内容
头文件的创建:
- 在编辑器中创建一个文件,该文件与源(.cpp)文件位于同一目录。
- 若是ide中,ide创建时,选择头文件
示例:
add.h:
// 这里需要一个头文件保护,目前简略版先不写
// 头文件内容
int add(int x, int y);
```cpp
#include <iostream>
#include "add.h"
int main() {
std::cout << add(10, 15);
}
头文件最佳实践
下面是创建使用头文件的建议。
- 始终使用头文件保护(将在下一课中介绍)。
- 不要在头文件中定义变量和函数(目前)。
- 为头文件提供与其关联的源文件相同的名称(例如,grades.h与grades.cpp成对出现)。
- 每个头文件都应该有一个特定的功能,并且尽可能独立。例如,将与功能A相关的声明放在A.h中,将与功能B相关的声明放在B.h中。如果只关心A,则可只包含A.h,而不获取与B相关的内容。
- 注意为代码文件中使用的功能显式包含对应的头文件。
- 头文件都应该能单独编译(应该#include需要的每个依赖项)。
- 仅#include 需要的内容(不要因为允许而include所有内容)。
- 不要#include .cpp文件。
- 在头文件中放置关于某段代码的作用或使用文档。它更可能在那里被看到。描述代码如何工作的文档应保留在源文件中。RedundantDefinitionProblem
头文件保护
重复定义问题
多次定义变量标识符的程序将导致编译错误:

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

虽然这些程序很容易修复(删除重复的定义),但使用头文件,很容易导致头文件中的定义被多次包含。当头文件#include另一个头文件(这是常见的)时,可能会发生这种情况。
考虑以下示例:
square.h:
int getSquareSides();
wave.h:
#include "square.h"
这样如果在main中同时引用,依然会编译异常!因此,引出我们的一种机制。
头文件保护
上述问题,可以使用头文件保护的机制来避免。头文件保护是采用以下形式的条件编译指令:
#ifndef SOME_UNIQUE_NAME_HERE
#define SOME_UNIQUE_NAME_HERE
// 在这里放置你的声明
#endif
解释如下:
ifndef:ifndefSOME_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();
- 感谢你赐予我前进的力量

