使用Sal减少代码缺陷 :你不必欣赏的源代码注释语言之美

一、初识缓冲区标识符

缓冲区标识符?什么东西。好像从来没有见过,貌似也没听过啊。是不是又出自某个隐秘山谷里面的深谷幽兰。答案是否定的。缓冲区标识符更像是一种代码的锦上添花,完全不用了解,你依旧可以快乐的编写代码,但是这种快乐更像是大碗喝酒、大块吃肉的狂野;了解之后,编写代码会有种刺绣的感觉,是艺术的雕琢和精益求精,甚至而有点古典美出来,给人一见如故,再见沉迷之意。

啊!!什么玩意。真的这么玄乎。引入官方的解释:

SAL 是 Microsoft 源代码注释语言。 通过使用源代码注释,可以在代码中明确表明你的意图。 这些注释还能使自动化静态分析工具更准确地分析代码,明显减少假正和假负情况。
Microsoft 源代码注释语言 (SAL) 提供一组描述函数如何使用其参数的注释,有关注释的假设以及完成注释时的保证。 注释是在头文件 中定义的。 适用于 C++ 的 Visual Studio 代码分析使用 SAL 注释来修改其函数分析。

二、SAL基础:常用的缓冲区标识符

SAL 定义了四种基本类型的参数,这些参数按使用模式进行分类。
使用Sal减少代码缺陷 :你不必欣赏的源代码注释语言之美_第1张图片可以通过各种方式使这四个基本注释更加明确。 默认情况下,带注释的指针参数假定为必需参数,它们必须为非 NULL,函数才能成功。 最常用的基本注释变体指示指针参数是可选参数,如果它为 NULL,函数仍可成功执行其工作。

下表显示了如何区分必需参数和可选参数:
使用Sal减少代码缺陷 :你不必欣赏的源代码注释语言之美_第2张图片

三、一些基础的示例

示例:_In_ 注释

_In_ 注释表明:

1. 参数必须有效,并且不会修改。
2. 函数仅会从单元素缓冲区读取。
3. 调用方必须提供缓冲区并对其进行初始化。
4. `_In_ `指定“只读”。 常见的错误是将 _In_ 应用于应具有 _Inout_ 注释的参数。
5. 允许使用 `_In_`,但对于非指针标量,分析器将其忽略
void InCallee(_In_ int *pInt)
{
   int i = *pInt;
}

void GoodInCaller()
{
   int *pInt = new int;
   *pInt = 5;

   InCallee(pInt);
   delete pInt;
}

void BadInCaller()
{
   int *pInt = NULL;
   InCallee(pInt); // pInt should not be NULL
}

如果对此示例使用 Visual Studio Code Analysis,它将验证调用方是否将非空指针传递给 pInt 初始化的缓冲区。 在这种情况下,pInt 指针不能为 NULL。

示例:_In_opt_ 注释

_In_opt__In_ 相同,只不过允许输入参数为 NULL,因此,函数应检查该参数。

void GoodInOptCallee(_In_opt_ int *pInt)
{
   if(pInt != NULL) {
      int i = *pInt;
   }
}

void BadInOptCallee(_In_opt_ int *pInt)
{
   int i = *pInt; // Dereferencing NULL pointer 'pInt'
}

void InOptCaller()
{
   int *pInt = NULL;
   GoodInOptCallee(pInt);
   BadInOptCallee(pInt);
}

Visual Studio Code Analysis 会验证函数在访问缓冲区之前是否检查 NULL。

示例:_Out_ 注释

_Out_ 支持一种常见方案,即传入一个指向元素缓冲区的非空指针,并由函数初始化该元素。 调用方不必在调用之前初始化缓冲区;调用的函数承诺在返回结果之前对缓冲区进行初始化。

void GoodOutCallee(_Out_ int *pInt)
{
   *pInt = 5;
}

void BadOutCallee(_Out_ int *pInt)
{
   // Did not initialize pInt buffer before returning!
}

void OutCaller()
{
   int *pInt = new int;
   GoodOutCallee(pInt);
   BadOutCallee(pInt);
   delete pInt;
}

Visual Studio Code Analysis 验证此函数是否在 pInt 取消引用之前检查 NULL,如果 pInt 不是 NULL,此函数是否在返回结果之前初始化缓冲区。

示例:_Inout_ 注释

_Inout_ 用于注释函数可能更改的指针参数。 在调用之前指针必须指向有效的初始化数据,即使发生更改,也必须在返回时具有有效的值。 注释指定函数可以对单元素缓冲区进行自由地读取和写入。 调用方必须提供缓冲区并对其进行初始化。
备注:与 _Out_ 一样, _Inout_ 必须应用于可修改的值。

void InOutCallee(_Inout_ int *pInt)
{
   int i = *pInt;
   *pInt = 6;
}

void InOutCaller()
{
   int *pInt = new int;
   *pInt = 5;
   InOutCallee(pInt);
   delete pInt;
}

void BadInOutCaller()
{
   int *pInt = NULL;
   InOutCallee(pInt); // 'pInt' should not be NULL
}

Visual Studio Code Analysis 验证调用方是否将非 NULL 指针传递给 pInt 初始化的缓冲区,并且在返回前,pInt 是否仍为非 NULL 且缓冲区已初始化。

示例:_Inout_opt_ 注释

_Inout_opt__Inout_ 相同,只不过允许输入参数为 NULL,因此,函数应检查该参数。

void GoodInOutOptCallee(_Inout_opt_ int *pInt)
{
   if(pInt != NULL) {
      int i = *pInt;
      *pInt = 6;
   }
}

void BadInOutOptCallee(_Inout_opt_ int *pInt)
{
   int i = *pInt; // Dereferencing NULL pointer 'pInt'
   *pInt = 6;
}

void InOutOptCaller()
{
   int *pInt = NULL;
   GoodInOutOptCallee(pInt);
   BadInOutOptCallee(pInt);
}

Visual Studio Code Analysis 验证此函数是否在访问缓冲区之前检查 NULL,如果 pInt 不是 NULL,此函数是否在返回结果之前初始化缓冲区。

示例:_Outptr_ 注释

_Outptr_ 用于为预期返回指针的参数进行注释。 参数本身不应为 NULL,调用的函数将返回非空指针,且该指针指向已初始化的数据。

void GoodOutPtrCallee(_Outptr_ int **pInt)
{
   int *pInt2 = new int;
   *pInt2 = 5;

   *pInt = pInt2;
}

void BadOutPtrCallee(_Outptr_ int **pInt)
{
   int *pInt2 = new int;
   // Did not initialize pInt buffer before returning!
   *pInt = pInt2;
}

void OutPtrCaller()
{
   int *pInt = NULL;
   GoodOutPtrCallee(&pInt);
   BadOutPtrCallee(&pInt);
}

Visual Studio Code Analysis 验证调用方是否为 *pInt 传递非空指针,以及函数是否在返回结果之前初始化缓冲区。

示例:_Outptr_opt_ 注释

_Outptr_opt__Outptr_ 相同,只不过该参数为可选参数,调用方可以为该参数传入空指针。

void GoodOutPtrOptCallee(_Outptr_opt_ int **pInt)
{
   int *pInt2 = new int;
   *pInt2 = 6;

   if(pInt != NULL) {
      *pInt = pInt2;
   }
}

void BadOutPtrOptCallee(_Outptr_opt_ int **pInt)
{
   int *pInt2 = new int;
   *pInt2 = 6;
   *pInt = pInt2; // Dereferencing NULL pointer 'pInt'
}

void OutPtrOptCaller()
{
   int **ppInt = NULL;
   GoodOutPtrOptCallee(ppInt);
   BadOutPtrOptCallee(ppInt);
}

Visual Studio Code Analysis 验证此函数是否在 *pInt 取消引用之前检查 NULL,以及此函数是否在返回结果之前初始化缓冲区。

示例:_Success_ 注释与 _Out_ 组合

大多数对象都可应用注释。 尤其可以对整个函数进行注释。 函数最明显的特征之一是它可以成功或失败。 但如缓冲区与其大小之间的关联一样,C/C++ 无法表示函数的成功或失败。 通过使用 _Success_ 注释,可以了解函数成功的情况。 _Success_ 注释参数只是一个表达式,当它为 true 时,指示函数已成功。 表达式可以是注释分析器能够处理的任何内容。 函数返回后的注释效果仅适用于函数成功时。 此示例演示 _Success_ 如何与 _Out_ 交互执行正确的操作。 可以使用关键字 return 来表示返回值。

_Success_(return != false) // Can also be stated as _Success_(return)
bool GetValue(_Out_ int *pInt, bool flag)
{
   if(flag) {
      *pInt = 5;
      return true;
   } else {
      return false;
   }
}

_Out_ 注释使 Visual Studio Code Analysis 验证调用方是否将非空指针传递给 pInt 缓冲区,以及函数是否在返回结果之前初始化该缓冲区。

四、通过vs 代码分析工具预知代码缺陷

使用Sal减少代码缺陷 :你不必欣赏的源代码注释语言之美_第3张图片

五、什么时候使用源代码注释?

下面是一些准则:

注释所有指针参数。

提供值范围注释,以便 Code Analysis 可以确保缓冲区和指针的安全。

注释锁定规则和锁定副作用。 有关详细信息,请参阅注释锁定行为。

注释驱动程序属性和其他特定于域的属性。

六、更多

more详细资料和高级用法,请移步官方网站 https://learn.microsoft.com/zh-cn/cpp/code-quality/using-sal-annotations-to-reduce-c-cpp-code-defects?view=msvc-170

你可能感兴趣的:(开发语言,c++,源代码注释,缓冲区注释)