flash 网站 收费,小企业网站建设服务,重庆做个网站多少钱,网络推广方案的内容C中的主要问题之一是存在大量行为未定义或对程序员来说意外的构造。我们在使用静态分析器检查各种项目时经常会遇到这些问题。但正如我们所知#xff0c;最佳做法是在编译阶段尽早检测错误。让我们来看看现代C中的一些技术#xff0c;这些技术不仅帮助编写简单明了的代码中的主要问题之一是存在大量行为未定义或对程序员来说意外的构造。我们在使用静态分析器检查各种项目时经常会遇到这些问题。但正如我们所知最佳做法是在编译阶段尽早检测错误。让我们来看看现代C中的一些技术这些技术不仅帮助编写简单明了的代码还能使代码更加安全可靠。
1、什么是现代C “现代C”这一术语在C11发布后变得非常流行。那么它是什么意思呢首先现代C是一套模式和惯用法旨在消除老式“带类的C”中的缺点特别是对于那些从C语言起步的C程序员来说。C11看起来更加简洁明了这一点非常重要。 当人们谈论现代C时通常会想到什么并行编程、编译时计算、RAII、lambda表达式、范围、概念、模块以及标准库中的其他同样重要的组件例如文件系统操作API。这些都是非常酷的现代化改进我们期待在下一套标准中看到它们。然而我想特别关注新标准如何帮助编写更安全的代码。在开发静态分析器时我们看到许多各种各样的错误有时我们忍不住会想“但在现代C中这些问题本可以避免。”因此我建议我们检查一下PVS-Studio在各种开源项目中发现的一些错误并看看如何修复这些错误。
2、自动类型推断
在C中引入了关键字 auto 和 decltype 。当然你已经知道它们是如何工作的。
std::mapint, int m;
auto it m.find(42);
//C98: std::mapint, int::iterator it m.find(42); 这非常方便可以缩短长类型同时不影响代码的可读性。然而这些关键字与模板一起变得相当广泛使用 auto 和 decltype 不需要指定返回值的类型。
但让我们回到主题。这是一个 64位错误的示例
string str .....;
unsigned n str.find(ABC);
if (n ! string::npos)在64位应用程序中std::string::npos 的值大于 UINT_MAX无符号类型变量能够表示的最大值。这看起来是一个 auto 可以解决的问题的例子n 变量的类型对我们来说并不重要主要的是它能够容纳 std::string::find 的所有可能值。 事实上如果我们使用 auto 重写这个示例错误就会消失
string str .....;
auto n str.find(ABC);
if (n ! string::npos)但并非所有事情都这么简单。使用auto并不是万灵药它的使用有很多陷阱。例如你可以这样写代码:
auto n 1024 * 1024 * 1024 * 5;
char* buf new char[n];auto 无法解决整数溢出问题而且分配的缓冲区内存会少于5GiB。
auto 在处理一个非常常见的错误时也帮不上太大忙写错的循环。让我们来看一个例子
std::vectorint bigVector;
for (unsigned i 0; i bigVector.size(); i)
{ ... }对于大型数组这种循环会变成一个无限循环。这种错误在代码中并不少见它们在非常罕见的情况下暴露出来而这些情况通常没有测试。
我们可以使用 auto 重写这个代码片段吗
std::vectorint bigVector;
for (auto i 0; i bigVector.size(); i)
{ ... }不能。错误不仅仍然存在而且变得更糟了。
在简单类型的情况下auto 的行为很糟糕。是的在最简单的情况下例如 auto x y它能正常工作但一旦有额外的构造行为可能变得更加不可预测。更糟糕的是错误会变得更难发现因为变量的类型一开始并不明显。幸运的是这对于静态分析器来说不是问题它们不会感到疲倦也不会失去注意力。但对我们这些普通人来说最好还是显式地指定类型。我们还可以通过其他方法避免窄化转换但我们稍后会讨论这些方法。
3、危险的 countof
在 C 中“危险”的类型之一是数组。程序员经常在将数组传递给函数时忘记它是作为指针传递的并尝试使用 sizeof 来计算元素的数量。
#define RTL_NUMBER_OF_V1(A) (sizeof(A)/sizeof((A)[0]))
#define _ARRAYSIZE(A) RTL_NUMBER_OF_V1(A)
int GetAllNeighbors( const CCoreDispInfo *pDisp,int iNeighbors[512] ) {....if ( nNeighbors _ARRAYSIZE( iNeighbors ) ) iNeighbors[nNeighbors] pCorner-m_Neighbors[i];....
} 注意这段代码摘自 Source Engine SDK。
PVS-Studio 警告V511 sizeof() 运算符返回的是指针的大小而不是数组的大小这在 sizeof (iNeighbors) 表达式中出现。Vrad_dll disp_vrad.cpp 60
这种混淆可能是因为在参数中指定了数组的大小这个数字对编译器没有意义仅仅是对程序员的提示。
问题在于这段代码被编译了而程序员可能不知道其中存在问题。显而易见的解决方案是使用元编程
template class T, size_t N
constexpr size_t countof(const T (array)[N]) {return N;
}
countof(iNeighbors); // 编译时错误如果传递给这个函数的不是数组我们会得到编译错误。在 C17 中可以使用 std::size。
在 C11 中std::extent 函数被引入但它不适合作为 countof因为它对不适当的类型返回 0。
std::extentdecltype(iNeighbors)(); // 0你不仅会在 countof 中犯错也可能在 sizeof 中出现错误
VisitedLinkMaster::TableBuilder::TableBuilder(VisitedLinkMaster* master,const uint8 salt[LINK_SALT_LENGTH]): master_(master),success_(true) {fingerprints_.reserve(4096);memcpy(salt_, salt, sizeof(salt));
}注意这段代码摘自 Chromium。
PVS-Studio 警告
V511 sizeof() 运算符返回的是指针的大小而不是数组的大小这在 sizeof (salt) 表达式中出现。browser visitedlink_master.cc 968 V512 memcpy 函数的调用将导致 salt_ 缓冲区的下溢。browser visitedlink_master.cc 968
正如你所见标准 C 数组有很多问题。这就是为什么你应该使用 std::array 的原因在现代 C 中它的 API 类似于 std::vector 和其他容器并且在使用时更难出错。
void Foo(std::arrayuint8, 16 array)
{array.size(); // 16
}4、如何在一个简单的for中犯错误
另一个错误来源是简单的 for 循环。你可能会想“在哪里会出错呢是和复杂的退出条件或节省代码行数有关吗”不程序员在最简单的循环中也会犯错误。让我们来看一下项目中的代码片段
const int SerialWindow::kBaudrates[] { 50, 75, 110, .... };SerialWindow::SerialWindow() : ....
{....for(int i sizeof(kBaudrates) / sizeof(char*); --i 0;){message-AddInt32(baudrate, kBaudrateConstants[i]); ....}
}注这段代码取自 Haiku 操作系统。
PVS-Studio 警告V706 可疑的除法sizeof (kBaudrates) / sizeof (char *)。kBaudrates 数组中每个元素的大小与除数不相等。SerialWindow.cpp 162
我们在前面的章节中详细检查过这种错误数组大小没有正确计算。我们可以通过使用 std::size 来轻松修复它
const int SerialWindow::kBaudrates[] { 50, 75, 110, .... };SerialWindow::SerialWindow() : ....
{....for(int i std::size(kBaudrates); --i 0;) {message-AddInt32(baudrate, kBaudrateConstants[i]); ....}
}但是有一个更好的方法。让我们再看一个片段。
inline void CXmlReader::CXmlInputStream::UnsafePutCharsBack(const TCHAR* pChars, size_t nNumChars)
{if (nNumChars 0){for (size_t nCharPos nNumChars - 1;nCharPos 0;--nCharPos)UnsafePutCharBack(pChars[nCharPos]);}
}注这段代码取自 Shareaza。
PVS-Studio 警告V547 表达式 nCharPos 0 始终为真。无符号类型的值总是大于等于 0。BugTrap xmlreader.h 946
这是编写反向循环时的典型错误程序员忘记了无符号类型的迭代器检查总是返回真。你可能会想“怎么会这样只有新手和学生才会犯这样的错误。我们专业人员不会。”不幸的是这并不完全正确。当然每个人都知道 (unsigned 0) 的结果为真。这样的错误通常在哪里出现它们常常是在重构的过程中发生的。假设项目从 32 位平台迁移到 64 位。之前使用了 int/unsigned 进行索引后来决定将它们替换为 size_t/ptrdiff_t。但在某个片段中他们不小心使用了无符号类型而不是有符号类型。
为了避免这种情况你的代码中可以采取什么措施有些人建议使用有符号类型例如C# 或 Qt 中的方式。也许这是一种解决方案但如果我们要处理大量数据那么就无法避免使用 size_t。有没有更安全的方式在 C 中迭代数组当然有。我们从最简单的方法开始非成员函数。标准库中有用于处理集合、数组和 initializer_list 的标准函数它们的原理应该对你来说很熟悉。
char buf[4] { a, b, c, d };
for (auto it rbegin(buf);it ! rend(buf);it) {std::cout *it;
}很好现在我们不需要记住直接循环和反向循环之间的区别了。也不必考虑我们使用的是简单数组还是数组——循环在任何情况下都会有效。使用迭代器是一种避免麻烦的好方法但即便如此有时也不够理想。最佳的做法是使用基于范围的 for 循环
char buf[4] { a, b, c, d };
for (auto it : buf) {std::cout it;
}当然基于范围的 for 循环也有一些缺陷它不允许灵活地管理循环如果需要对索引进行更复杂的操作那么这种 for 循环帮助不大。但这种情况应该另行讨论。我们现在面对的是一个比较简单的情况我们需要以反向顺序遍历元素。然而在这个阶段已经出现了一些困难。标准库中没有额外的类来支持基于范围的 for 循环。我们来看看如何实现它
template typename T
struct reversed_wrapper {const T _v;reversed_wrapper (const T v) : _v(v) {}auto begin() - decltype(rbegin(_v)){return rbegin(_v);}auto end() - decltype(rend(_v)){return rend(_v);}
};template typename T
reversed_wrapperT reversed(const T v)
{return reversed_wrapperT(v);
}在 C14 中你可以通过去掉 decltype 来简化代码。你可以看到 auto 如何帮助你编写模板函数——reversed_wrapper 将同时适用于数组和 std::vector。
现在我们可以将代码片段重写如下
char buf[4] { a, b, c, d };
for (auto it : reversed(buf)) {std::cout it;
}这段代码有什么好处呢首先它非常易于阅读。我们立即可以看到元素数组是以反向顺序排列的。其次出错的可能性较小。第三它适用于任何类型。这比之前的做法要好得多。
在 Boost 中你可以使用 boost::adaptors::reverse(arr)。
但让我们回到最初的例子。那里数组是通过一对指针大小传递的。显然我们的 reversed 方法对于这种情况是不适用的。我们应该怎么做使用像 span/array_view 这样的类。在 C17 中我们有 string_view我建议使用它
void Foo(std::string_view s);
std::string str abc;
Foo(std::string_view(abc, 3));
Foo(abc);
Foo(str);std::string_view 不拥有字符串实际上它是一个 const char* 和长度的包装器。这就是为什么在代码示例中字符串是通过值传递的而不是通过引用传递的。string_view 的一个关键特性是它与各种字符串表示方式的兼容性const char*、std::string 和非空终止的 const char*。
因此函数的形式如下
inline void CXmlReader::CXmlInputStream::UnsafePutCharsBack(std::wstring_view chars)
{for (wchar_t ch : reversed(chars))UnsafePutCharBack(ch);
}在将值传递给函数时需要记住 string_view(const char*) 的构造函数是隐式的因此我们可以像这样写
Foo(pChars);而不是这样
Foo(wstring_view(pChars, nNumChars));string_view 指向的字符串不需要是以 null 结尾的这个名字 string_view::data 就暗示了这一点。在使用 string_view 时必须记住这一点。当将其值传递给一个期望 C 字符串的 cstdlib 函数时可能会出现未定义的行为。如果在大多数测试用例中使用的是 std::string 或以 null 结尾的字符串这种问题可能会被忽略。
5、枚举
让我们暂时抛开 C来看看老旧的 C 语言。那么 C 语言的安全性如何呢毕竟它没有隐式构造函数调用和操作符也没有类型转换的问题也没有各种类型字符串的问题。在实际应用中错误往往发生在最简单的构造中最复杂的构造因为引起怀疑而经过仔细审查和调试。与此同时程序员们往往会忘记检查简单的构造。以下是一个来自 C 语言的危险结构的例子
enum iscsi_param {....ISCSI_PARAM_CONN_PORT,ISCSI_PARAM_CONN_ADDRESS,....
};enum iscsi_host_param {....ISCSI_HOST_PARAM_IPADDRESS,....
};
int iscsi_conn_get_addr_param(....,enum iscsi_param param, ....)
{....switch (param) {case ISCSI_PARAM_CONN_ADDRESS:case ISCSI_HOST_PARAM_IPADDRESS:....}return len;
}这是一个 Linux 内核的例子。PVS-Studio 警告V556 不同枚举类型的值进行比较switch(ENUM_TYPE_A) { case ENUM_TYPE_B: … }。libiscsi.c 第 3501 行。
请注意 switch-case 中的值其中一个命名常量来自不同的枚举。在原始代码中当然有更多的代码和可能的值错误并不那么明显。这是因为枚举的类型松散——它们可能会隐式地转换为 int这留下了很多错误的空间。
在 C11 中您可以并且应该使用 enum class这样的技巧在那里行不通错误会在编译阶段显示出来。结果以下代码无法编译这正是我们所需要的
enum class ISCSI_PARAM {....CONN_PORT,CONN_ADDRESS,....
};enum class ISCSI_HOST {....PARAM_IPADDRESS,....
};
int iscsi_conn_get_addr_param(....,ISCSI_PARAM param, ....)
{....switch (param) {case ISCSI_PARAM::CONN_ADDRESS:case ISCSI_HOST::PARAM_IPADDRESS:....}return len;
}以下片段与枚举不完全相关但具有类似的症状
void adns__querysend_tcp(....) {...if (!(errno EAGAIN || EWOULDBLOCK || errno EINTR || errno ENOSPC ||errno ENOBUFS || errno ENOMEM)) {...
}注意这段代码来自 ReactOS。
是的errno 的值被声明为宏这在 C 中是不好的做法在 C 中也是如此但即使程序员使用了枚举也不会更容易解决这个问题。失去的比较在枚举中不会显现出来特别是在宏的情况下。同时使用 enum class 不会允许这种情况因为不会有隐式转换为 bool。
6 、构造函数中的初始化
回到原生 C 的问题。其中一个问题在于当需要在多个构造函数中以相同的方式初始化对象时会显现出来。一个简单的情况是有一个类两个构造函数其中一个调用另一个。这看起来很合逻辑将公共代码放入一个单独的方法中——没有人喜欢重复代码。那么陷阱是什么呢
Guess::Guess() {language_str DEFAULT_LANGUAGE;country_str DEFAULT_COUNTRY;encoding_str DEFAULT_ENCODING;
}
Guess::Guess(const char* guess_str) {Guess();....
}注意这段代码来自 LibreOffice。
PVS-Studio 警告V603 对象被创建了但没有被使用。如果您希望调用构造函数应使用 this-Guess::Guess(....)。guess.cxx 第 56 行。
问题在于构造函数调用的语法。经常会忘记这一点程序员会创建一个额外的类实例然后立即销毁它。也就是说原始实例的初始化没有发生。当然有很多方法可以解决这个问题。例如我们可以通过 this 显式调用构造函数或者将所有内容放入一个单独的函数中
Guess::Guess(const char * guess_str)
{this-Guess();....
}Guess::Guess(const char * guess_str)
{Init();....
}顺便提一下显式地重复调用构造函数例如通过 this是一种危险的做法我们需要了解发生了什么。使用 Init() 的变体更好且更清晰。
但在这里最好使用构造函数的委托。这样我们可以以以下方式显式地从一个构造函数调用另一个构造函数
Guess::Guess(const char * guess_str) : Guess()
{....
}
这种构造函数有几个限制。首先委托构造函数对对象的初始化负有全部责任。也就是说它无法在初始化列表中初始化另一个类字段
Guess::Guess(const char * guess_str): Guess(), m_member(42)
{....
}当然我们必须确保委托不会创建循环否则将无法退出。遗憾的是这段代码会被编译
Guess::Guess(const char * guess_str): Guess(std::string(guess_str))
{....
}Guess::Guess(std::string guess_str): Guess(guess_str.c_str())
{....
}7、关于虚函数
虚函数会带来潜在的问题派生类中函数签名错误很容易发生结果可能不会重写函数而是声明了一个新函数。我们来看以下例子
class Base {virtual void Foo(int x);
}
class Derived : public Base {void Foo(int x, int a 1);
}通过指向 Base 的指针或引用无法调用 Derived::Foo。不过这是一个简单的例子你可能会说没人会犯这样的错误。通常人们会以以下方式出错
class DBClientBase : .... {
public:virtual auto_ptrDBClientCursor query(const string ns,Query query,int nToReturn 0int nToSkip 0,const BSONObj *fieldsToReturn 0,int queryOptions 0,int batchSize 0 );
};
class DBDirectClient : public DBClientBase {
public:virtual auto_ptrDBClientCursor query(const string ns,Query query,int nToReturn 0,int nToSkip 0,const BSONObj *fieldsToReturn 0,int queryOptions 0);
};注意这段代码取自 MongoDB。 PVS-Studio 警告V762 请检查虚函数参数。请查看派生类 DBDirectClient 和基类 DBClientBase 中函数 query 的第七个参数。文件 dbdirectclient.cpp 第 61 行。
函数的参数很多并且在继承类的函数中没有最后一个参数。这些是不同的、不相关的函数。这样的错误经常发生在具有默认值的参数上。
在下面的代码片段中情况会更加复杂。如果编译为 32 位代码这段代码会正常工作但在 64 位版本中则无法正常工作。最初在基类中参数是 DWORD 类型但后来被更正为 DWORD_PTR。同时继承类中的参数没有相应地更改。愿不眠的夜晚、调试和咖啡长存
class CWnd : public CCmdTarget {....virtual void WinHelp(DWORD_PTR dwData, UINT nCmd HELP_CONTEXT);....
};
class CFrameWnd : public CWnd { .... };
class CFrameWndEx : public CFrameWnd {....virtual void WinHelp(DWORD dwData, UINT nCmd HELP_CONTEXT);....
};
你可以以更为离奇的方式犯错比如忘记函数的 const 限定符或参数的 const 限定符或者忽略基类函数是否为虚函数或者混淆有符号与无符号类型。
在 C 中添加了几个关键字来规范虚函数的重写。override 关键字将大有帮助。这样代码将无法编译。
class DBDirectClient : public DBClientBase {
public:virtual auto_ptrDBClientCursor query(const string ns,Query query,int nToReturn 0,int nToSkip 0,const BSONObj *fieldsToReturn 0,int queryOptions 0) override;
};8、NULL vs nullptr
使用 NULL 来表示空指针可能会导致许多意想不到的情况。NULL 实际上是一个普通的宏它展开成 0其类型为 int。这就是为什么在以下示例中选择第二个函数的原因
void Foo(int x, int y, const char *name);
void Foo(int x, int y, int ResourceID);
Foo(1, 2, NULL);虽然原因很清楚但这种情况很不符合逻辑。这也是为什么需要 nullptr 的原因nullptr 具有自己的类型 nullptr_t。这就是为什么在现代 C 中不能使用 NULL更不要说 0。
另一个例子是NULL 可以与其他整数类型进行比较。假设有一个 WinAPI 函数返回 HRESULT。该类型与指针无关因此与 NULL 的比较毫无意义。nullptr 通过引发编译错误来强调这一点而 NULL 则不会
if (WinApiFoo(a, b) ! NULL) // 不好
if (WinApiFoo(a, b) ! nullptr) // 好 编译错误9、va_arg
在某些情况下需要传递不确定数量的参数。一个典型的例子是格式化输入/输出函数。虽然可以编写成不需要可变数量参数的方式但我认为没有理由放弃这种语法因为它更方便且更易于阅读。旧的 C 标准提供了什么它们建议使用 va_list。这会带来哪些问题对于这种参数传递错误类型的参数并不那么容易也有可能完全没有传递参数。让我们仔细看看这些片段。
typedef std::wstring string16;
const base::string16 relaunch_flags() const;int RelaunchChrome(const DelegateExecuteOperation operation)
{AtlTrace(Relaunching [%ls] with flags [%s]\n,operation.mutex().c_str(),operation.relaunch_flags());....
}注意这段代码取自 Chromium。
PVS-Studio 警告V510 AtlTrace 函数不应接收类类型变量作为第三个实际参数。delegate_execute.cc 第 96 行
程序员想打印 std::wstring 字符串但忘记调用 c_str() 方法。因此wstring 类型在函数中会被解释为 const wchar_t*。当然这样做是无济于事的。
cairo_status_t
_cairo_win32_print_gdi_error (const char *context)
{....fwprintf (stderr, L%s: %S, context,(wchar_t *)lpMsgBuf);....
}注意这段代码取自 Cairo。
PVS-Studio 警告V576 格式不正确。请检查 fwprintf 函数的第三个实际参数。期望的是指向 wchar_t 类型符号的字符串指针。cairo-win32-surface.c 第 130 行
在这段代码中程序员混淆了字符串格式说明符。问题在于在 Visual C 中wchar_t* 和 %S 都期望 wprintf 的 %s 格式说明符。值得注意的是这些错误出现在用于错误输出或调试信息的字符串中——这些是较少见的情况因此被忽略了。
static void GetNameForFile(const char* baseFileName,const uint32 fileIdx,char outputName[512] )
{assert(baseFileName ! NULL);sprintf( outputName, %s_%d, baseFileName, fileIdx );
} 注意这段代码取自 CryEngine 3 SDK。
PVS-Studio 警告V576 格式不正确。请检查 sprintf 函数的第四个实际参数。期望的是有符号整数类型参数。igame.h 第 66 行
整数类型也很容易混淆尤其是当它们的大小依赖于平台时。然而在这里情况要简单得多有符号和无符号类型被混淆了。大的数字将被打印为负数。
ReadAndDumpLargeSttb(cb,err)
int cb;
int err;
{....printf(\n - %d strings were read, %d were expected (decimal numbers) -\n);....
}
注意这段代码取自 Word for Windows 1.1a。
PVS-Studio 警告V576 格式不正确。调用 printf 函数时实际参数的数量不匹配。期望3 个。实际1 个。dini.c 第 498 行
这个字符串预期有三个参数但实际没有提供。可能程序员打算打印栈上的数据但我们不能假设栈上有什么内容。显然我们需要显式地传递这些参数。
BOOL CALLBACK EnumPickIconResourceProc(HMODULE hModule, LPCWSTR lpszType, LPWSTR lpszName, LONG_PTR lParam)
{....swprintf(szName, L%u, lpszName);....
}注意这段代码取自 ReactOS。
PVS-Studio 警告V576 格式不正确。请检查 swprintf 函数的第三个实际参数。打印指针的值应该使用 %p。dialogs.cpp 第 66 行
这是一个 64 位错误的示例。指针的大小依赖于架构使用 %u 来打印指针是不合适的。我们应该使用什么呢分析器提示正确的格式符是 %p。如果指针用于调试时打印这是非常有用的。如果之后尝试从缓冲区读取它并使用它那就更有意思了。
对于具有可变参数的函数几乎一切都有可能出错你无法检查参数的类型或数量。稍有偏差就会出现未定义行为。
幸运的是现在有更可靠的替代方案。首先变参模板就是其中之一。借助变参模板我们可以在编译期间获取所有传递类型的信息并按需使用它。举个例子我们可以使用一个更安全的 printf
void printf(const char* s) {std::cout s;
}
templatetypename T, typename... Args
void printf(const char* s, T value, Args... args) {while (s *s) {if (*s% *s!%) {std::cout value;return printf(s, args...);}std::cout *s;}
} 当然这只是一个示例在实际应用中其使用意义不大。但在变参模板的情况下你的限制仅限于你的想象力而不是语言特性。
另一种可以用来传递可变参数的构造是 std::initializer_list。它不允许传递不同类型的参数但如果这足够你可以这样使用
void Foo(std::initializer_listint a);
Foo({1, 2, 3, 4, 5});它也很方便遍历因为我们可以使用 begin、end 和范围 for 循环。
10、窄化转换
窄化转换给程序员的生活带来了很多麻烦。特别是当迁移到 64 位架构变得更加必要时这种问题显得尤为突出。理想情况下代码中应该只有正确的类型。但是实际情况往往不是如此程序员常常使用各种“黑科技”和一些奇特的方法来存储指针。找到这些代码片段需要消耗大量的时间和精力
char* ptr ...;
int n (int)ptr;
....
ptr (char*) n;不过我们暂时不讨论 64 位错误。这儿有一个更简单的例子程序员想要找出两个整数值的比例。这样做的代码如下
virtual int GetMappingWidth( ) 0;
virtual int GetMappingHeight( ) 0;void CDetailObjectSystem::LevelInitPreEntity()
{....float flRatio pMat-GetMappingWidth() / pMat-GetMappingHeight();....
}注意这段代码取自 Source Engine SDK。
PVS-Studio 警告V636 表达式被隐式地从 ‘int’ 类型转换为 ‘float’ 类型。请考虑使用显式类型转换以避免丢失小数部分。示例double A (double)(X) / Y;。客户端 (HL2) detailobjectsystem.cpp 1480
不幸的是无法完全防止这种错误——总会有某种方式隐式地将一种类型转换为另一种类型。但是好消息是C11 引入的新初始化方法具有一个很好的特性它禁止狭义转换。在这种代码中错误将在编译阶段被发现可以轻松地加以修正。
float flRatio { pMat-GetMappingWidth() / pMat-GetMappingHeight() };11、没有消息就是好消息
管理资源和内存的错误方式有很多种。现代语言在工作便利性方面有很高的要求。现代 C 也不落后提供了多种自动资源控制工具。尽管这些错误在动态分析中很常见但有些问题可以通过静态分析来发现。以下是其中一些问题的示例
void AccessibleContainsAccessible(....)
{auto_ptrVARIANT child_array(new VARIANT[child_count]);...
}注意这段代码取自 Chromium。
PVS-Studio 警告V554 错误使用了 auto_ptr。使用 new [] 分配的内存将通过 delete 清理。interactive_ui_tests accessibility_win_browsertest.cc 171
当然智能指针的理念并不新鲜例如曾经有一个类 std::auto_ptr。我使用过去式谈论它因为它在 C11 中被声明为弃用并在 C17 中被移除。在这个代码片段中错误是由于错误使用了 auto_ptr该类没有数组的专门化因此将调用标准的 delete而不是 delete[]。unique_ptr 替代了 auto_ptr并且它对数组有专门化支持还可以传递一个删除器函数对象该对象将在 delete 代替调用并且完全支持移动语义。看起来这里似乎没有什么问题。
void text_editor::_m_draw_string(....) const
{....std::unique_ptrunsigned pxbuf_ptr(new unsigned[len]);....
}注意这段代码取自 nana。
PVS-Studio 警告V554 错误使用了 unique_ptr。使用 new [] 分配的内存将通过 delete 清理。text_editor.cpp 3137
结果发现其实你也会犯同样的错误。是的只需写 unique_ptrunsigned[]错误就会消失但代码在这种形式下仍然能编译。因此这种方式也可能出错实践表明只要可能人们就会这样做。这段代码就是证明。因此使用 unique_ptr 管理数组时务必小心比想象中更容易出错。也许使用 std::vector 会更符合现代 C 的规范
我们来看另一个事故类型。
templateclass TOpenGLStage
static FString GetShaderStageSource(TOpenGLStage* Shader)
{....ANSICHAR* Code new ANSICHAR[Len 1];glGetShaderSource(Shaders[i], Len 1, Len, Code);Source Code;delete Code;....
}注意这段代码取自 Unreal Engine 4。
PVS-Studio 警告V611 内存是使用 new T[] 操作符分配的但却使用 delete 操作符释放。请考虑检查这段代码。最好使用 delete[] Code;。openglshaders.cpp 1790
没有智能指针时同样的错误也很容易出现使用 new[] 分配的内存通过 delete 释放。
bool CxImage::LayerCreate(int32_t position)
{....CxImage** ptmp new CxImage*[info.nNumLayers 1];....free(ptmp);....
}注意这段代码取自 CxImage。
PVS-Studio 警告V611 内存是使用 new 操作符分配的但却使用 free 函数释放。请考虑检查 ptmp 变量背后的操作逻辑。ximalyr.cpp 50
在这个片段中malloc/free 和 new/delete 被混用。这可能发生在重构过程中C 语言的函数需要被替换结果导致了未定义行为。
int settings_proc_language_packs(....)
{....if(mem_files) {mem_files 0;sys_mem_free(mem_files);}....
}注意这段代码取自 Fennec Media。
PVS-Studio 警告V575 空指针被传递给 free 函数。检查第一个参数。settings interface.c 3096
这是一个更有趣的例子。有一种做法是在释放内存后将指针置为零。程序员有时甚至会为此编写特殊的宏。从某种程度上来说这是一个很好的技术你可以防止对同一块内存的再次释放。但是在这里表达式的顺序被搞错了因此 free 得到了一个空指针这一点被分析器注意到了。
ETOOLS_API int __stdcall ogg_enc(....) {format open_audio_file(in, enc_opts);if (!format) {fclose(in);return 0;};out fopen(out_fn, wb);if (out NULL) {fclose(out);return 0;}
}但这个问题不仅仅涉及内存管理还涉及资源管理。例如你可能忘记关闭文件如上面的代码片段所示。在这两种情况下RAII 关键字概念都适用。这一概念也支持智能指针。结合移动语义RAII 有助于避免许多与内存泄漏相关的 bugs。以这种风格编写的代码可以更直观地识别资源所有权。
作为一个小例子我将提供一个基于 unique_ptr 的 FILE 封装器
auto deleter [](FILE* f) {fclose(f);};
std::unique_ptrFILE, decltype(deleter) p(fopen(1.txt, w), deleter);尽管如此你可能会希望有一个更具函数式的封装来处理文件具有更易读的语法。值得记住的是在 C17 中将添加一个用于处理文件系统的 API —— std::filesystem。但是如果你对这个决定不满意并且希望使用 fread/fwrite 而不是 i/o 流你可以从 unique_ptr 中获得一些灵感编写自己的 File 类这样可以根据你的个人需求进行优化使其更方便、可读和安全。
结果是什么呢现代 C 提供了许多工具帮助你更安全地编写代码。许多用于编译时评估和检查的构造也已出现。你可以切换到更方便的内存和资源管理模型。
但没有任何技术或编程范式可以完全保护你免于错误。与功能的增加相伴随C 也会引入新的 bugs这些 bugs 可能只有 C 特有。这就是为什么我们不能单纯依赖一种方法我们应该始终结合代码审查、优质代码和良好的工具这些可以帮助节省你的时间和精力这些时间和精力可以用在更好的地方。