高精度计时器项目学习笔记
StopWatch.h实现
守卫机制
// 包含守卫机制防止头文件多次被包含,使用预处理器指令(宏)实现
#ifndef __STOPWATCH_H__
#define __STOPWATCH_H__
...
#endif // __STOPWATCH_H__
在遇到的规范的.h
文件中,一般都会在全局加上上述宏定义;或者加上如下宏定义:
// 不同编译器的处理不一定相同(不是标准的一部分,但是广泛支持)
#pragma once
// 头文件内容
以上两种都实现了一个功能就是:保证代码中头文件只被导入一次(防止头文件被多次包含的机制);
.h
文件,通常称为头文件,在 C 和 C++ 编程中扮演着重要角色。以下是 .h
文件的主要功能和特点:
- 声明接口:
.h
文件用于声明类、函数、变量、宏、模板等的接口。这些接口定义了如何与这些实体交互,但不包含它们的实现细节。 - 代码重用:通过将接口声明放在
.h
文件中,多个源文件(.cpp
文件)可以包含(include)同一个头文件,从而重用相同的接口声明。 - 编译效率:编译器在编译源文件时只需要处理一次头文件的内容,即使多个源文件包含了相同的头文件。这提高了编译效率。
- 命名空间管理:
.h
文件有助于管理命名空间,防止不同源文件中的名称冲突。 - 类型检查:编译器使用头文件来检查类型匹配性,确保例如函数调用时参数类型正确。
- 前置声明:在某些情况下,可以使用头文件进行类和函数的前置声明(forward declaration),这有助于解决编译时的依赖问题。
- 模板实例化:对于模板类和函数,头文件是必要的,因为模板的实例化通常在包含模板声明的源文件中进行。
- 跨平台和跨编译器兼容性:头文件通常不包含特定于平台或编译器的代码,这使得它们可以在不同的开发环境中重用。
- 模块化设计:头文件支持模块化编程,每个模块可以有自己的头文件,清晰地定义模块的公共接口。
- 文档和规范:头文件可以作为代码的文档,展示类和函数的预期用途和行为规范。
但是使用头文件必须注意的是防止头文件内容被多次包含,导致编译错误。所以要设置守卫来进行保证头文件只会被包含一次;
传统的做法就是第一种,用一个条件编译加宏来实现,将文件全部内容都放在条件编译中;
最新规范中,可以使用#pragma once
来代替上述实现;
如果多次包含可能出现以下错误:
- 宏定义重复:不允许宏重新定义,所以导致编译错误;
- 重复定义类、函数、变量,导致编译器错误;
- 多次实例化导致违反模板使用规则;
- 编译效率低下,内存使用增加;
- 产生“依赖地狱”,即依赖关系及其复杂;
为了避免多次包含带来的不可预知的错误,所以一定要设置守卫机制;
根据不同的编译平台选择不同的头文件
#if defined(_MSC_VER) || defined(__MINGW32__) || defined(WIN32)
#include <Windows.h>
#else
#include <chrono>
#endif
一般如果有宏_MSC_VER
或者__MINGW32__
或者WIN32
,说明在windows
平台下编译,导入头文件#include <Windows.h>
,如果没有则导入头文件#include <chrono>
;由于C++语言贴近底层系统架构,所以不同系统下所需的头文件也可能不同,用预编译语句处理比全部导入更好;
StopWatch类及其接口声明
class StopWatch {
public:
StopWatch();
~StopWatch();
//开启计时
void Start();
//暂停计时
void Stop();
//重新计时
void ReStart();
//微秒
double Elapsed();
//毫秒
double ElapsedMS();
//秒
double ElapsedSecond();
private:
long long elapsed_;
#if defined(_MSC_VER) || defined(__MINGW32__) || defined(WIN32)
LARGE_INTEGER start_;
LARGE_INTEGER stop_;
LARGE_INTEGER frequency_;
#else
typedef std::chrono::high_resolution_clock Clock;
typedef std::chrono::microseconds MicroSeconds;
std::chrono::steady_clock::time_point start_;
std::chrono::steady_clock::time_point stop_;
#endif
};
依旧判断编译平台,根据不同的编译平台选择不同的方案;
StopWatch.cpp实现
StopWatch.h
仅仅定义了实现的类以及接口声明,具体的实现依旧在StopWatch.cpp
中实现;
导入.h
文件:#include "StopWatch.h"
;
#if defined(_MSC_VER) || defined(__MINGW32__) || defined(WIN32)
StopWatch::StopWatch():elapsed_(0) // 列表初始化,eplased_变量初始为0
{
elapsed_ = 0;
start_.QuadPart = 0;
stop_.QuadPart = 0;
QueryPerformanceFrequency(&frequency_);
}
#else
StopWatch::StopWatch():elapsed_(0),start_(MicroSeconds::zero()),stop_(MicroSeconds::zero()) {}
#endif
StopWatch::~StopWatch() {} // 析构函数
开始计时Start()
与停止计时Stop()
;
void StopWatch::Start()
{
#if defined(_MSC_VER) || defined(__MINGW32__) || defined(WIN32)
QueryPerformanceCounter(&start_); // 类似于获取当前时间保存到start_中
#else
start_ = Clock::now();
#endif
}
void StopWatch::Stop()
{
#if defined(_MSC_VER) || defined(__MINGW32__) || defined(WIN32)
QueryPerformanceCounter(&stop_); // 获取当前时间到stop_中
elapsed_ += (stop_.QuadPart - start_.QuadPart) * 1000000 / frequency_.QuadPart;
#else
stop_ = Clock::now();
elapsed_ = std::chrono::duration_cast<MicroSeconds>(stop_ - start_).count();
#endif
}
思路:在Start()
中,获取当前时间信息到start_
成员变量中;在Stop()
中,获取当前时间到stop_
成员变量中,并计算stop_-start_
,即为计时总时长,和计时器初始值相加,保存在elapsed_
成员变量中;
重启计时器:
void StopWatch::ReStart()
{
elapsed_ = 0; // 更新计时器初始值设定为0
Start(); // 重新开始计时
}
获取计时器的值(必须停止之后获取):
double StopWatch::Elapsed()
{
return static_cast<double>(elapsed_);
}
double StopWatch::ElapsedMS()
{
return elapsed_ / 1000.0;
}
double StopWatch::ElapsedSecond()
{
return elapsed_ / 1000000.0;
}
elapsed_
本来是long long
类型,在返回时,根据返回的时间单位做不同处理;
static_cast
是四种类型转换操作符之一,用于执行显式的类型转换。 static_cast<double>(elapsed_)
表示将 elapsed_
变量的值显式转换为 double
类型;
static_cast
的关键点:
- 显式类型转换:编译时将一个类型转换为另一个类型;
- 不安全:与
const_cast
和reinterpret_cast
不同,static_cast
可以执行大多数类型的转换,包括那些可能会导致数据丢失或行为改变的转换。这意味着使用static_cast
时需要小心,以避免潜在的错误。 - 不进行运行时检查:转换速度更快,但是可能产生非法行为;
- 一般用于基本数据类型的转换;
- 也可以用于类层次结构中的向上转型:在类继承中,
static_cast
可以安全地将派生类指针或引用转换为基类指针或引用。
注意,转换之后,再使用elapsed_
就会变成转换之后的类型(即会覆盖);
四种类型转换操作符
C++ 中提供了四种类型转换操作符,每种操作符都有其特定的用途和转换规则(内置,不需要引入特定的头文件):
static_cast<type>(expression)
- 用于基本的非多态类型转换。
- 可以用于内置类型之间的转换,如将
int
转换为double
,或者类层次结构中的向上转型(从派生类到基类)。 - 不进行运行时类型检查,因此是最快的转换方式,但需要程序员确保转换的安全性。
const_cast<type>(expression)
- 用于移除或添加
const
属性。 - 可以用于将
const
类型的值转换为非const
类型,或反之。 - 这种转换通常用于编译器警告或错误时,但不应该用于绕过
const
的意图。
- 用于移除或添加
reinterpret_cast<type>(expression)
- 用于低级别的重新解释转换,可以将任何指针类型转换为任何其他指针类型。
- 可以转换指针和整数类型,甚至可以转换函数指针。
- 这种转换可能导致未定义行为,因为它允许重新解释内存的字节,而不关心原始数据的语义。
dynamic_cast<type>(expression)
- 用于类层次结构中的向下转型(从基类到派生类)和多态性相关的转换。
- 进行运行时类型检查,以确保转换的安全性。
- 如果转换失败,对于指针类型,会返回
nullptr
;对于引用类型,会抛出std::bad_cast
异常。
每种类型转换操作符的使用场景和特点如下:
static_cast
:适用于明确知道类型转换安全的场合,不需要运行时检查。const_cast
:主要用于修改const
属性,但在多态性场景中应该谨慎使用。reinterpret_cast
:用于指针的低级转换,需要程序员非常清楚自己在做什么,因为可能会破坏类型安全。dynamic_cast
:适用于运行时多态性,确保转换的安全性,但性能开销较大。
使用类型转换时,应该考虑转换的安全性和必要性,避免不必要的类型转换,以减少程序中的错误和潜在的未定义行为。
多态类型
多态是一个重要的概念,它允许同一个接口接受不同的数据类型。在面向对象编程(OOP)中,多态主要有两种形式:编译时多态(也称为静态多态或方法重载)和运行时多态(也称为动态多态或方法重写)。
编译时多态(静态多态)
编译时多态主要通过函数重载和模板实现。函数重载是指在同一个作用域内可以有多个同名函数,但参数的类型或数量不同,编译器在编译时根据调用的参数来确定调用哪个函数。
运行时多态(动态多态)
运行时多态通常通过虚函数实现。它允许程序在运行时确定调用哪个函数,这通常用于类的继承结构中。以下是运行时多态的关键点:
- 基类和派生类:在类继承中,派生类可以继承基类的属性和方法。
- 虚函数:在基类中,将某些函数声明为虚函数(使用
virtual
关键字),这告诉编译器这些函数将在派生类中可能被重写。 - 重写(Override):派生类可以重写基类中的虚函数,即使用相同的函数名和参数列表提供新的实现。
- 多态引用或指针:通过基类的引用或指针调用虚函数时,程序会在运行时确定应该调用哪个类中的函数版本。如果指向的是派生类对象,就调用派生类的版本;如果是基类对象,就调用基类的版本。
- 动态类型识别:运行时多态依赖于运行时类型识别(RTTI),这允许程序在运行时询问对象的实际类型。
dynamic_cast
:dynamic_cast
是 C++ 中实现运行时多态的一种机制,它允许安全的向下转型,即从基类指针或引用转换为派生类指针或引用。- 设计原则:运行时多态支持里氏替换原则(Liskov Substitution Principle),即派生类应该能够替换其基类而不影响程序的正确性。
总结
在.h
文件中声明了类、类内的成员函数、成员变量等;而在.cpp
文件中进行具体实现;
比如.h
文件中声明了类StopWatch
,并且声明了类内的方法Start()
,则cpp
文件中,通过StopWatch::Start()
来写方法的具体实现;
// .h文件声明
class StopWatch
{
public:
void Start();
}
// .cpp文件实现
void StopWatch::Start() {
// 具体实现
}
通过域作用符::
来访问类内的公有方法;
对于不同平台,引入的头文件,和实现的方式都不同,所以可以使用条件编译宏来实现不同平台执行不同语句引入不同头文件;
#if defined(_MSC_VER) || defined(__MINGW32__) || defined(WIN32)
// 满足上面条件时,执行的语句
#else
// 不满足上面条件时,执行的语句
#endif
计时器的逻辑实现很简单,无法就是调用Start()
函数时,更新计时器计时时间为0,然后记录当前时间为开始时间;调用Stop()
时,记录当前时间为结束时间;结束时间减去开始时间加上计时器初值,就是计时器时间;