Functions are C++ entities that associate a sequence of statements (a function body) with a name and a list of zero or more function parameters.
函数是 C++ 中的实体,它将一系列语句(一个函数体)与一个名称和零个或多个函数参数列表相关联。
这句话相当直白,深刻揭示了函数的实质。更加通俗的说法是函数是被设计用来执行任务的实体。函数包含函数头和一组用花括号括起来的语句。
它们只是描述函数的不同说法,但是函数本身没有改变,但是我们往往可能倾向于后者,因为这足够形象。类似的,C语言的函数已然足够匹配它的作用——作为一个可复用的功能模块,那么如果C++还需要改变什么,那只能说明一点,那就是——C语言的函数还不够好用。
一、改进C函数
假如,现在有一个小物理测试,测试小车在不同物理状态下的位移量,是否满足位移公式:
鉴于若干次计算,将其整理成一个函数
double displacement(double v, double a, double t) {return v * t + 0.5 * a * t * t;
}
作为物理测试差不多了,但是,从一个程序员的角度看,它太小了,怎么说呢,看一个程序,这个程序用到的库,后面会介绍,为了控制变量,先将速度、加速度和时间固定
#include <iostream>
#include <random>
#include <chrono>
double displacement(double v, double a, double t) {return v * t + 0.5 * a * t * t;
}int main() {std::chrono::high_resolution_clock::time_point start_time = std::chrono::high_resolution_clock::now();// 创建一个随机数发生器对象std::random_device rd; // 用来获取随机种子std::mt19937 gen(rd()); // 使用 Mersenne Twister 作为随机数生成器// 定义分布std::uniform_real_distribution<double> distVelocity(0.0, 10.0); // 初始速度范围 [0.0, 10.0]std::uniform_real_distribution<double> distAccel(-5.0, 5.0); // 加速度范围 [-5.0, 5.0]std::uniform_real_distribution<double> distTime(1.0, 10.0); // 时间范围 [1.0, 10.0]for (int i = 0; i < 1000; i++) {double v = 0; // 随机生成初始速度double a = 1; // 随机生成加速度double t = 10; // 随机生成时间double s = displacement(v, a, t); // 计算位移std::cout << "No." << i + 1 << " displacement: " << s << std::endl;}std::cout << "The program took " << std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::high_resolution_clock::now() - start_time).count() << " milliseconds to run." << std::endl;return 0;}
如果使用函数
如果直接替换为函数内容
虽然差距不足一秒,但是我们知道,调用一个函数要进行一系列操作,大概为保护现场(存储当前执行指令地址,为了返回),参数传递(如果存在),地址跳转(跳转到调用函数里面),执行函数体,返回。为了调用函数的其它操作称之为开销(overhead),如果函数本身的执行时间,比开销还小,这就很不划算。但是显然,将其封装为一个整体的功能又是必要的(存在很多这样类似的情况),所以我们需要改进
(一)“小”函数少开销—内联函数
考虑到是调用函数需要开销,那么或许我们可以使用宏函数,这样就不用开销了,但是C 函数中的宏函数存在诸多问题。
首先,宏函数缺乏类型安全检查。由于宏只是简单的文本替换,不会对参数的类型进行检查,这可能导致在使用宏函数时传递了不恰当的参数类型,从而产生难以预料的错误。例如,对于一个简单的加法宏 #define ADD(a,b) (a + b) ,如果传递的参数是浮点数,可能会得到不符合预期的结果。
其次,宏函数调试困难。因为宏函数在预处理阶段就被替换为相应的代码,所以在调试时无法像函数那样设置断点和单步跟踪,难以准确地确定问题所在。
另外,宏函数容易引起一些边界性的问题和歧义。例如,#define SQUARE(X) ((X)*(X)) ,当使用 SQUARE(3.5 + 1.5) 时,得到的结果可能并非期望的 25,而是由于文本替换导致的计算错误。
鉴于此,C++提出了内联函数
1、内联函数
在 C++ 中,内联函数是一种特殊的函数。其基本概念是在函数调用点直接展开函数体的代码,从而减少函数调用的开销,这种展开,也叫内联展开(inline expansion)。内联函数的定义方式通常是在函数返回类型前加上inline关键字。
这正是解决了开销问题,这样看来问题被解决了,不过且慢,这也导致了另外的开销,因为内联展开也要开销,所以内联展开值不值得呢?我们无法得知,所以,是否内联的决定权在编译器手里
The original intent of the inline keyword was to serve as an indicator to the optimizer that inline substitution of a function is preferred over function call, that is, instead of executing the function call CPU instruction to transfer control to the function body, a copy of the function body is executed without generating the call.
inline关键字的初衷是向优化器指示函数的内联替换优先于函数调用,也就是说,并非执行函数调用CPU指令将控制权转移到函数体,而是执行函数体的副本而不生成调用。
This avoids overhead created by the function call (passing the arguments and retrieving the result) but it may result in a larger executable as the code for the function has to be repeated multiple times.
这避免了函数调用产生的开销(传递参数和检索结果),但它可能会导致更大的可执行文件,因为函数的代码必须重复多次。
Since inline substitution is unobservable in the standard semantics, compilers are free to use inline substitution for any function that's not marked inline, and are free to generate function calls to any function marked inline.
由于内联替换在标准语义学中是不可观察的,编译器可以自由地对任何未标记为内联的函数使用内联替换,并且可以自由地生成对任何标记为内联的函数的函数调用。
所以,最终的优化还是交给了编译器 ,这很好,因为并非小就需要内联,也并非所有程序员都能保证眼前这个函数是需要内联的。
C语言也有内联,但是这是来源于C++:
The inline keyword was adopted from C++, but in C++, if a function is declared inline, it must be declared inline in every translation unit, and also every definition of an inline function must be exactly the same (in C, the definitions may be different, and depending on the differences only results in unspecified behavior).
内联关键字是从C++采用的,但是在C++中,如果一个函数被声明为内联,它必须在每个翻译单元中被声明为内联,并且内联函数的每个定义必须完全相同(在C中,最多允许一个定义不同,根据差异只会导致未指定的行为)。
OK,继续我们的测试,由于计算参数有点多,我们采用小车,并设置不同的斜坡来先使得初始速度为0,加速度是变量,时间也固定,像这样
for (int i = 0; i < 20; i++) {double v = 0; // 随机生成初始速度double a = distAccel(gen); // 随机生成加速度double t = 10; // 随机生成时间double s = displacement(v, a, t); // 计算位移std::cout << "No." << i + 1 << " displacement: " << s << std::endl;
}
这样,一个三参的函数现在我们只使用一个,剩下两个还一直是一样的,不太好,能不能提供默认呢?可以,C++提供了默认参数。
2、默认参数
C++中的默认参数是指在函数定义时为参数提供一个默认的值,这样在调用函数时如果没有传入该参数的值,就会使用默认值来代替。像下面这样
double displacement(double v = 0.0, double a = 10.0, double t = 10.0) {return v * t + 0.5 * a * t * t;
}
不过,注意 如果一个参数是默认参数,那么它的右边参数也需要是默认参数,除非
void func(int a = 1, ...); // 右边参数是可变参数
void func2(int a , int b = 1);
void func2(int a =1 , int b);
// 在同一作用域的前面定义的相同名字的函数的右边是默认参数,
// 注意不能void func2(int a =1 , int b = 1); 这属于重定义
这下,我们的代码看起来就好多了。接下来,同样的公式,我们或许可以计算重力加速度
设计函数如下
double calculateGraviationalAcceleration(double height = 1.0, float t = 1.0) {return 2 * height / t * t;
}
不过有个问题,当我想通过固定高度计算重力加速度时,我必须给时间参数也设置为默认参数,倘若交换位置,那我想固定时间呢?可见,在某些时候,某些相同功能的函数可能仅仅在参数上有所不同,比如整数的加法,浮点数的加法。在C语言中,我们必须设置不同的函数,取不同的名字,即使功能一样。显然不能使用auto,所以我们需要重载。
(二)“同”函数多利用—函数重载
函数重载(function overload)允许函数重名,只要函数签名(参数列表)不同
double calculateGraviationalAcceleration(double height , float t = 1.0) {return 2 * height / t * t;
}
double calculateGraviationalAcceleration( float t, double height = 1.0 ) {return 2 * height / t * t;
}
函数重载的基本规则
- 函数名称相同:所有被重载的函数都必须具有相同的名称。
- 参数列表不同:参数列表必须有所不同,这可以是参数的数量、类型或者顺序上的差异。如果两个函数只有返回类型不同而参数列表完全相同,则无法构成重载。默认参数的存在不会影响函数重载,但是当两个函数除了默认参数外其他部分完全相同时,它们会被视为同一函数。
- 编译器的选择:
- 编译器会根据传入的实际参数类型和数量来决定调用哪个重载版本。
- 编译器会尝试通过类型转换来匹配函数签名,如果多个重载版本都可行,它会选择最合适的那个。
- 如果没有一个重载版本能够被完美匹配,编译器会选择一个最接近的版本,并可能进行隐式类型转换。
二、一些库函数
(一) 数学函数
C++定义了一组可以在程序中调用的数学函数。这些函数都是预定义的并且收集在头文件中。 头文件名称中的前缀“c” 强调它是从C语言继承来的(但是有一些更改)。
1. 数值函数
数值函数用于数值计算。参数通常是一个或者多个数值,返回结果也是一个数值。下表显示了该类别中的一些常见函数以及函数声明。
函数声明 | |
type abs(type x); | 返回 x 的绝对值 |
返回大于或等于 x 的最小值 | |
返回小于或等于 x 的最大值 | |
返回 x 的自然(以e 为底) 对数 | |
返回x 的常用(以10为底) 对数 | |
返回eˣ | |
返回x³, 注意在这种情况下 y也可以为 int类型 | |
返回 x 的平方根(x[MISSING IMAGE: , ]) |
表中的 type是 float、double 或者 long double。对于 pow 函数, 第二个参数也可以是整数。所有这些函数都可以接收不同类型的参数,并且可以返回不同类型的值。
2. 三角函数
注意前三个函数的参数必须是弧度, 其中180度=π弧度(π近似等于 3.141592653589793238462).
函数声明 | 参数单位 | ||
type cos(type x); | 返回余弦值 | [-1, +1] | |
返回正弦值 | [-1,+1] | ||
返回正切值 | 任意 | ||
返回反余弦值 | [0,π] | ||
返回反正弦值 | [0,π] | ||
返回反正切值 |
(二) 字符函数
所有字符处理函数都包含函数库中(C character type的缩写),它们继承自 C 语言。
1. 字符分类函数
所有字符分类函数名都以前缀 is开头,例如 iscontrol、isupper等。如果参数属于由函数名定义的类别,则函数返回1(可以解释为真),否则,函数返回0(可以解释为假)。下表显示了这些函数的列表。注意,每个函数中的参数都是 int类型,但我们总是传递一个字符作为参数(字符是小于短整数的整数)。
函数声明 | |
int isalnum(int x); | 判断参数是否为字母数字字符 |
判断参数是否为字母字符 | |
判断参数是否为控制字符 | |
判断参数是否为十进制数字字符(0到9) | |
判断参数是否为除空格之外的可打印字符 | |
判断参数是否为小写字母(a到z) | |
判断参数是否为可打印字符(包含空格) | |
判断参数是否为标点符号 | |
判断参数是否为空白字符(空格、回车或者制表符) | |
判断参数是否为大写字母(A 到Z) | |
2. 字符转换函数
函数声明 | |
int tolower(int x) | 返回参数所对应的小写字母 |
C++语言包含两个函数,它们用于将字符从一类转换为另一类。这些函数以前缀to开头,并返回一个整数(即转换后的字符的值)。注意,这些函数的返回值被定义为一个整数,但是我们总是可以将返回值(隐式或者显式地)转换为字符,
(三) 处理时间
在C++中处理时间,可以使用一些内置函数和库,如(对应于C中的)和库。
1. ctime库
可以使用time()函数来获取当前时间,这个函数返回一个time_t类型的值,代表当前时间。例如:
time_t now = time(nullptr);
time_t now = time(nullptr); // 获取当前时间戳// 将time_t类型的时间转换成一个以字符串表示的本地时间std::cout << "The current local time is: " << ctime(&now) << std::endl; // 打印本地时间// 将time_t类型的时间转换成一个以本地时间为基准的tm结构体tm *ltm = localtime(&now); // 转换为本地时间// 将time_t类型的时间转换成一个以UTC时间为基准的tm结构体tm *gmtm = gmtime(&now); // 转换为UTC时间std::cout << "The current local time is: " << asctime(ltm) << std::endl; // 打印本地时间std::cout << "The current UTC time is: " << asctime(gmtm) << std::endl; // 打印UTC时间// 修改时间ltm->tm_year = 2022 - 1900; // 年份, 以1900年为基准ltm->tm_mon = 10; // 月份, 0-11ltm->tm_mday = 1; // 日期time_t new_time = mktime(ltm); // 转换为时间戳std::cout << "The new local time is: " << ctime(&new_time) << std::endl; // 打印新时间// 将tm结构体表示的时间根据指定格式转换成一个字符串char buffer[80];strftime(buffer, 80, "%Y-%m-%d %H:%M:%S", ltm);std::cout << "The formatted local time is: " << buffer << std::endl; // 打印格式化时间
2.chrono库
<chrono>
库是C++11标准引入的,旨在提供一种类型安全的方式来处理时间间隔、时间点(时间点指的是从某个固定点(例如epoch)开始计算的持续时间)和时钟。
由于 <chrono>
涉及到比较高级的C++特性,这里简要介绍
<chrono>
库的主要组成部分
- 持续时间(Durations):用于表示时间间隔。持续时间可以是正数或负数,表示时间的流逝或回溯。
- 时间点(Time Points):时间点表示一个特定的时间瞬间,通常是基于某个固定时间点(如epoch)的持续时间偏移。
- 时钟(Clocks):时钟是用于测量时间的设备或系统的抽象。
<chrono>
库定义了多种时钟,如系统时钟(std::chrono::system_clock
)、稳定时钟(std::chrono::steady_clock
)和高精度时钟(std::chrono::high_resolution_clock
)。 - 操作(Operations):
<chrono>
库提供了一系列的操作来处理和计算时间,如获取当前时间点(now()
函数)、持续时间加减、时间点和持续时间的比较等。
以下是一个简单的示例,展示了如何使用 <chrono>
库来获取当前的系统时间,并计算从现在起经过了一段时间后的时间点:
#include <iostream>
#include <chrono>
#include <thread> // 用于演示等待 int main() { // 获取当前时间点 auto now = std::chrono::system_clock::now(); // 将当前时间点转换为time_t类型,然后输出 std::time_t now_c = std::chrono::system_clock::to_time_t(now); std::cout << "Current time: " << std::ctime(&now_c); // 等待一段时间 std::this_thread::sleep_for(std::chrono::seconds(5)); // 再次获取当前时间点 auto later = std::chrono::system_clock::now(); // 计算两个时间点之间的持续时间 auto duration = later - now; // 输出持续时间 std::cout << "Elapsed time: " << std::chrono::duration_cast<std::chrono::seconds>(duration).count() << " seconds.\n"; return 0;
}
(四) 随机生成数
1.C语言的简单随机数
在 C 语言中,生成随机数主要依靠 rand() 和 srand() 这两个函数。rand() 函数用于生成一个介于 0 到 RAND_MAX 之间的伪随机整数,RAND_MAX 通常是一个较大的整数。
为了生成指定范围的随机数,我们可以使用取模运算。例如,要生成 0 到 49 的随机数,可以使用 rand() % 50;如果要生成 100 到 150 的随机数,可以使用 rand() % 51 + 100 。
下面是一个简单的示例代码:
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main() {srand(time(NULL)); for(size_t i = 0 ; i < 10; i++){printf("random No-%d:%d\n", i, rand() % 50);}return 0;}
rand() 函数生成的随机数实际上是根据一个固定的算法和初始种子计算出来的,所以如果不设置不同的种子,每次运行程序得到的随机数序列会相同。srand() 函数的作用就是设置这个初始种子,通常我们会使用当前系统时间作为种子,通过 time(NULL) 获取,这样每次运行程序时,由于时间不同,种子不同,从而生成不同的随机数序列。
C 语言的随机数生成机制使用相对简单,只需包含<stdlib.h>头文件,并调用rand()和srand()函数,就能实现随机数的生成,对于初学者和简单的应用场景来说,易于理解和上手。其次,它能够在一定程度上满足随机性需求,对于一些对随机性要求不是特别高的应用,如简单的游戏、模拟实验等,能够提供一定的随机效果。
然而,C 语言随机数生成机制也存在诸多缺点。其一,生成的是伪随机数,这意味着它们并非真正的随机,而是遵循一定的数学规律生成的,可能会影响到对随机性要求极高的应用,如加密、密码学等领域。其二,种子的控制不易,若不精心设计种子的设置方式,可能导致生成的随机数不够随机或可预测性较强。其三,在多线程环境下,可能会出现竞争条件和随机数序列冲突的问题。由于多个线程可能同时调用rand()和srand()函数,如果处理不当,可能导致随机数的生成不符合预期,影响程序的正确性和稳定性。
2. C++现代化随机数
C++ 在随机数生成方面相对于 C 语言有了显著的改进和变化。C++11 引入了全新的随机数库,提供了更强大、更灵活的随机数生成机制。
下面是一个简单的示例
#include <iostream>#include <random>int main() {std::random_device rd;std::mt19937 engine(rd());std::uniform_int_distribution<int> dist(1, 6);for (int i = 0; i < 10; ++i) {std::cout << dist(engine) << " ";}std::cout << std::endl;return 0;}
分步来看,首先是选择随机数引擎:<random>
库提供了多种随机数引擎,如std::mt19937
(基于Mersenne Twister算法),std::default_random_engine
(默认随机数引擎,通常基于系统或编译器实现的某个好的随机数引擎)等。你需要选择一个引擎来生成随机数。
std::mt19937 gen(std::random_device{}());
这里,我们使用std::random_device来初始化std::mt19937引擎,这样每次运行程序时都会得到一个不同的随机数序列。
定义随机数分布:需要定义一个随机数分布,它告诉引擎如何生成随机数。<random>
库提供了多种分布,如均匀分布std::uniform_int_distribution
,正态分布std::normal_distribution
等。
std::uniform_int_distribution<> dis(1, 100); // 定义一个1到100之间的均匀整数分布
生成随机数:现在,可以使用定义好的引擎和分布来生成随机数了。
int rand_num = dis(gen); // 使用引擎gen和分布dis生成一个随机数
std::cout << rand_num << std::endl;