CPP静态检查常见问题

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


CPP静态检查常见问题

静态检查是比较好的一种自动检查代码工具,可以发现一些隐藏问题,当然更多是让你的代码更加规范,更加在可控范围内。

以下常见的错误,也是对C++进一步的思考

构造函数显式调用问题

`提示:

Class ‘CLBTimeSpan’ has a constructor with 1 argument that is not explicit. Such constructors should in general be explicit for type safety reasons. Using the explicit keyword in the constructor means some mistakes when using the class can be avoided.

翻译:

类’CLBTimeSpan’有一个带有1个参数的构造函数,它不是显式的。 出于类型安全的原因,这些构造函数通常应该是明确的。 在构造函数中使用explicit关键字意味着可以避免在使用类时出现一些错误。

具体源码:
在这里插入图片描述

原因分析:

1.构造只有一个参数时,容易被隐式调用,这样容易导致初始化错误。

例如:

  CLBTimeSpan dtSpan = 0;  //隐式调用其构造函数,默认调用
  1. 要使用“explicit” 关键字进行约束

改进要点:

explicit CLBTimeSpan(const time_t &t);

改成这样之后

CLBTimeSpan dtSpan = 0; //编译错误,不能隐式调用其构造函数
CLBTimeSpan dtSpan(0);  //显式调用成功

扩展阅读:

C++中,一个参数的构造函数(或者除了第一个参数外其余参数都有默认值的多参构造函数),承担了两个角色。1 是个构造器 ,2 是个默认且隐含的类型转换操作符。

所以,有时候在我们写下如 AAA a = XXX,这样的代码,且恰好XXX的类型正好是AAA单参数构造器的参数类型,这时候编译器就自动调用这个构造器,创建一个AAA的对象。

这样看起来好象很酷,很方便。但在某些情况下,却违背了我们(程序员)的本意。这时候就要在这个构造器前面加上explicit修饰,指定这个构造器只能被明确的调用/使用,不能作为类型转换操作符被隐含的使用。

构造函数直接赋值
提示:
When an object of a class is created, the constructors of all member variables are called consecutively in the order the variables are declared, even if you don’t explicitly write them to the initialization list. You could avoid assigning ‘m_timeSpan’ a value by passing the value to the constructor in the initialization list.

翻译:
创建类的对象时,即使您没有将它们显式写入初始化列表,也会按声明变量的顺序连续调用所有成员变量的构造函数。 您可以通过将值传递给初始化列表中的构造函数来避免为’m_timeSpan’赋值。

对应的源码:
在这里插入图片描述

原因分析:

这个性能改进项,主要意思就是通过构造函数对成员变量进行初始化的,建议使用直接初始化,不用调用=进行初始化。
当然输入参数,改成引用效果更佳

改进要点:

	CLBTimeSpan::CLBTimeSpan(consttime_t &t): m_timeSpan(t)
    {
       //m_timeSpan = t;
    }

扩展阅读:

其实在构造函数里面调用等于号并不是真正意义上的“初始化”(未改进之前的方式)。这个过程相当于:

  1. 系统创建成员变量;

  2. 创建完后再进行赋值操作。

而在构造函数后面跟冒号,就相当于:

  1. 系统创建成员变量并且初始化。也就是系统为成员变量分配了一块内存并且把相应的数据给填了进去。而构造函数里面调用等于号的方式是分配好后再进行赋值,多了一个步骤。

虚函数的作用范围

提示:

Virtual function ‘Open’ is called from constructor ‘CBackFile(char*strFileName,uint nOpenFlags)’ at line 53. Dynamic binding is not used.

翻译:

虚函数’Open’在第53行从构造函数’CBackFile(char * strFileName,uint nOpenFlags)'调用。不使用动态绑定。

对应源码位置:

virtual bool Open(char *strFileName, uint nOpenFlags);  

原因分析:

Open是虚函数,如果不是用于接口定义的话(也就是不存在动态绑定)且本身是有实现的话,建议是不用使用虚函数。
这个虚函数本身是作用为了将来派生用的(派生类会)。但实际代码,的确未使用动态绑定的特性。另外虚函数是会占一定空间,
导致编译之后的可执行文件体积变大,效率方面也是有所降低,这里不推荐使用虚函数。

改进要点:
在这里插入图片描述

扩展阅读:

一般虚函数是用于“实现多态性(Polymorphism),多态性是将接口与实现进行分离”还有就是析构函数这里经常要用到虚函数。

但是我们更应该要知道虚函数的缺点:

如果某个类不包含虚函数,那一般是表示它将不作为一个基类来使用。当一个类不准备作为基类使用时,使析构函数为虚一般是个坏主意。因为它会为类增加一个虚函数表,对象增加一个虚指针,使得对象的体积增大。所以基本的一条是:无故的声明虚析构函数和永远不去声明一样是错误的。

同理,也可以知道如果不是当基类用途的类,析构函数也是不需要使用虚函数的。

实验证明,如果程序里减少虚函数,可以缩减可执行文件体积大小。

构造函数里未完全初始化成员变量

提示:

Member variable ‘CLBTime::m_status’ is not initialized in the constructor.

翻译:

成员变量’CLBTime :: m_status’未在构造函数中初始化。

也有可能这样提示:(同一个意思)

When an object of a class is created, the constructors of all member variables are called consecutively in the order the variables are declared, even if you don‘t explicitly write them to the initialization list. You could avoid assigning ‘m_strHrefOnPanReady‘ a value by passing the value to the constructor in the initialization list.

翻译:

创建类的对象时,即使您没有将它们显式写入初始化列表,也会按声明变量的顺序连续调用所有成员变量的构造函数。 您可以通过将值传递给初始化列表中的构造函数来避免为“m_strHrefOnPanReady”赋值。

对应源码位置:

在这里插入图片描述

原因分析:

这个问题比较直接,就是成员变量未初始化。

改进要点:
在这里插入图片描述

扩展阅读:

一般来说构造函数用来初始化成员变量(这是个好习惯),但如果其中有个变量未初始化,也存在很大风险。所以要核对是否所有的成员变量都初始化(比较安全的数)

成员变量是需要动态分配内存

标题可能有点问题,大概意思是成员变量有指针,那么要重载=

提示:

Class ‘CLogFile’ does not have a operator= which is recommended since it has dynamic memory/resource allocation(s).

翻译:

类’CLogFile’没有operator =,因为它具有动态内存/资源分配,所以建议使用它。

对应源码位置:
在这里插入图片描述

原因分析:

意思是构造函数有动态分配内存,所以建议该类增加operator =函数。为什么呢?

因为是存在动态分配,因此如果默认调用A=B的时候,会默认调用浅拷贝,就好比只是B里成员变量的指针拷贝给A里成员变量,而不是拷贝“成员变量指针指向内容”,这样就会带来指针被多次引用,指向地方又是同一个地方,存在很大风险。

改进要点:

增加CLogFile 的operator =函数

或者在确定不需要使用 operator= 的情况下 使用delete 禁用operator=

CLogFile& operator=(const GLogFile& other) = delete;

扩展阅读:

1.浅拷贝: 将原对象或原数组的引用直接赋给新对象,新数组,新对象/数组只是原对象的一个引用

2.深拷贝: 创建一个新的对象和数组,将原对象的各项属性的“值”(数组的所有元素)拷贝过来,是“值”而不是“引用”

这个重载“=”,使用

CExample theObjthree;

theObjthree.init(60);//现在需要一个对象赋值操作,被赋值对象的原内容被清除,并用右边对象的内容填充。

theObjthree = theObjone; //此时调用“=”操作符,若CExample内有申请操作,此时 theObjthree里的指针是指向 theObjone,从而忘记里对theObjthree内空间释放导致内存的泄露。

指针类型缺少强制转换

提示:

C-style pointer casting detected. C++ offers four different kinds of casts as replacements: static_cast, const_cast, dynamic_cast and reinterpret_cast. A C-style cast could evaluate to any of those automatically, thus it is considered safer if the programmer explicitly states which kind of cast is expected. See also: https://www.securecoding.cert.org/confluence/display/cplusplus/EXP05-CPP.+Do+not+use+C-style+casts.

翻译:

检测到C风格的指针转换。 C ++提供了四种不同类型的转换作为替换:static_cast,const_cast,dynamic_cast和reinterpret_cast。 C风格的强制转换可以自动评估任何一个,因此如果程序员明确说明预期哪种类型的强制转换,则认为它更安全。 另请参阅:https://www.securecoding.cert.org/confluence/display/cplusplus/EXP05-CPP.+Do+not+use+C-style+casts。

原因分析:

不要用C风格进行强制转换

改进要点:

static_cast,const_cast,dynamic_cast和reinterpret_cast 替换原先

扩展阅读:

实际上static_cast,const_cast,dynamic_cast和reinterpret_cast都是用于C++类型强制转换,主要用于继承。

static_cast 应该比较频繁,static_cast主要是一些类型转换。static_cast相当于传统的C语言里的强制转换,该运算符把expression转换为new_type类型,用来强迫隐式转换,例如non-const对象转为const对象,编译时检查,用于非多态的转换,可以转换指针及其他,但没有运行时类型检查来保证转换的安全性。它主要有如下几种用法:

① 用于类层次结构中基类(父类)和派生类(子类)之间指针或引用的转换。

进行上行转换(把派生类的指针或引用转换成基类表示)是安全的;

进行下行转换(把基类指针或引用转换成派生类表示)时,由于没有动态类型检查,所以是不安全的。

②用于基本数据类型之间的转换,如把int转换成char,把int转换成enum。这种转换的安全性也要开发人员来保证。

③把空指针转换成目标类型的空指针。

④把任何类型的表达式转换成void类型。

注意:static_cast不能转换掉expression的const、volatile、或者__unaligned属性。

缺少拷贝构造函数

这里是指当该类存在指针的成员变量时,需要增加拷贝构造函数

提示:

Class ‘CLocalSocket’ does not have a copy constructor which is recommended since it has dynamic memory/resource allocation(s).

翻译:

类’CLocalSocket’没有建议(默认)的复制构造函数,因为它具有动态内存/资源分配。

原因分析:

首先确定的是C++中每个类一定有一个拷贝构造函数(好习惯)。它可以用来创建一个对象,并用另一个对象的数据初始化新建对象。
你的代码中如果没有显示定义拷贝构造函数(用来实现你自己的一些特殊需求),则C++将为每个类隐式地提供一个缺省的拷贝构造函数。
缺省的拷贝构造函数简单地将参数对象的每个数据域复制给新建对象中相应的副本(注意如果数据域是一个指向其他对象的指针,
那么仅简单地复制指针保存的地址值,而不是复制对象的内容)(会产生浅拷贝,因此成员变量有动态内容)

改进要点:

增加CLocalSocket 的拷贝构造函数

扩展阅读:

拷贝构造函数,又称复制构造函数,是一种特殊的构造函数,它由编译器调用来完成一些基于同一类的其他对象的构建及初始化。

在C++中,下面三种对象需要调用拷贝构造函数(有时也称“复制构造函数”):

  1. 一个对象作为函数参数,以值传递的方式传入函数体;

例如:BOOL testfunc(CExample obj);

  1. 一个对象作为函数返回值,以值传递的方式从函数返回;
CTest func()

{

	CTest theTest;

	return theTest;

}
  1. 一个对象用于给另外一个对象进行初始化(常称为赋值初始化);

    CExample theObjtwo = theObjone; //注意此时调用的不是重载“=”

还有:CExample theObjthree(theObjone);//显式调用

减少变量的作用域

其实的意思,有些变量没有必要在前面就申请。建议用到之前申明即可

提示:

The scope of the variable ‘byDir’ can be reduced.

翻译:

可以减少变量’byDir’的范围。
在这里插入图片描述

原因分析:
byDir 实际上就是在for循环内部使用,建议在for循环进行初始化。(个人认为这个不是很好的建议,会增加构造函数调用次数,实际情况请适当改进)
改进要点:

  for (int i = 0; i < 0x0040; i++)
    {
        byte byDir = (byte)((i >> 4) & 0x0F);     //高4,表 abc
        byte byNum = (byte)(i & 0x0F);              //低3,表 数字
        sprintf(strUPath, "/media/sd%c%d/", strUDiskDir[byDir],byNum);
        if (Utility::IsExist_Dir(strUPath))
        {
            nRes = i;
            break;
        }
    }

迭代器使用后置叠加或减运算

增加效率的问题

提示:

Prefix ++/-- operators should be preferred for non-primitive types. Pre-increment/decrement can be more efficient than post-increment/decrement. Post-increment/decrement usually involves keeping a copy of the previous value around and adds a little extra code.

翻译:

前缀++ / - 运算符应该是非基本类型的首选。 预增量/减量可以比后增量/减量更有效。 后递增/递减通常涉及保留前一个值的副本并添加一些额外的代码。

原因分析:

迭代器前置++和后置++的运行效率是不同的,**前置++效率更高**,因为后置运算符需要做一个临时的类对象拷贝。

这里是不是比较坑?

如果是后置:

调用过程(从汇编语言可以分析)

a. 先拷贝this内容到_temp中;b. 调用前置++操作符,自增this的内容;c. 返回_temp;

b. 从过程就可以轻易看出,后置++在前置++的基础上,至少多了步骤a和c;

如果是前置:

调用过程(从汇编语言可以分析)

a. a.直接递增;c. 返回(*this);

前置式迭代器不需要传回旧值,所以不需要花费一个临时对象来保存旧值。因此,面对任何迭代器(以及任何抽象数据型别),应该优先使用前置式。这条规则对递减操作也同样适用。

直接在函数参数中使用C风格字符串

提示:

The conversion from const char* as returned by c_str() to std::string creates an unnecessary string copy. Solve that by directly passing the string.

翻译:

从c_str()返回到std :: string的const char *的转换会创建一个不必要的字符串副本。 通过直接传递字符串来解决这个问题。

原因分析:

比如一个函数的参数类型是string,调用时实参直接使用了C风格(c_str())的字符串,
于是就会有以上提示,主要还是因为这里会造成一个不必要的字符串拷贝,降低运行效率。

这里也有坑:大家可以度娘下 c_str()

扩展阅读:

char* c;

string s="1234";

c = s.c_str(); //c最后指向的内容是垃圾,因为s对象被析构,其内容被处理,同时,
//编译器也将报错——将一个const char *赋与一个char *。野指针无意之中出现,很尴尬。

应该这样用:

char c[20];

string s="1234";

strcpy(c,s.c_str());

这样才不会出错,c_str()返回的是一个临时指针,不能对其进行操作

这样说吗c_str()会产生一个临时空间,而且调用完c_str()就无效了。存在效率问题,也存在使用风险,好在大部分编译器能报错

使用无用的find

提示:

Either inefficient or wrong usage of string::find(). string::compare() will be faster if string::find’s result is compared with 0, because it will not scan the whole string. If your intention is to check that there are no findings in the string, you should compare with std::string::npos.

翻译:

string :: find()的低效或错误用法。 如果将string :: find的结果与0进行比较,则string :: compare()会更快,因为它不会扫描整个字符串。 如果您打算检查字符串中没有发现,则应与std :: string :: npos进行比较。

在这里插入图片描述

原因分析:

find() 效率低,要用compare

代码本身不会出错,但是效率上是不被认可的,如cppcheck所说,如果你希望检查某个字符串是不是某个期望的字符串,那你应该使用compare函数,因为这样更快。

扩展阅读:

很多时候,我们会写的find代码,值不等于-1则说明找到了该字符串,这样做效率比较低,可以用compare比较

C++标准库里面的string::rfind和string:find不是用快速匹配算法实现的,效率不是一般的差

在实践中还发现,string::find(char)比较string::find(string)慢很多

函数参数使用传值而不是传引用

提示:

Parameter ‘strShowTime’ is passed by value. It could be passed as a const reference which is usually faster and recommended in C++.

翻译:

参数’strShowTime’按值传递。 它可以作为const引用传递,通常更快并且在C ++中推荐。

原因分析:

值传递会产生赋值操作,会产生临时变量,因此效率比较低(对于超过4字节的结构体、字符串)

可以改成 const & 方式

使用memset清空含有string(wstring)类型成员的结构体

提示:

Using ‘memset’ on struct that contains a ‘std::wstring’. [memsetClass]

翻译:

原因分析:

在C语言中,使用memset清空内存是常用操作,在C++中,和malloc一样,
使用memset直接操作内存是有很大风险的,因为它们都只是在内存层面做了改动,
但是对类对象本身却没有任何行动,
也就是说,他们不会调用类对象的构造或者析构函数,如果我们的结构体中含有string类型的成员,
直接使用memset很可能导致内存泄漏!

这里涉及到一个POD参考的概念。如果一个类不是POD的,那么就不应该使用如mem*,malloc等内存操作函数,否则,我们将得不到我们想要的东西,甚至引发错误。

扩展阅读:
在这里插入图片描述

测试程序以memcpy函数为例,运行本程序,虽然tt2可以正常的将str1拷贝进来,但是最后程序奔溃了!!

想想原因是为何?

程序崩溃在tt的析构函数中,因为无法探究memcpy到底做了哪些具体操作,所以我们可以猜测它是将tt的内存区域完整的拷贝到了tt2内,但是,tt2中的string类型的成员str1并没有完成自己的构造,而是通过内存拷贝的方式完整的使用了tt的数据,那么执行完delete ,tt2的str1也会析构自身,这时其析构的其实是tt的str1。等到tt再去析构自己时,奔溃就发生了!以上只是自己的猜测。可能不是事实,但是,当我把delete tt2注释后,崩溃就消失了,这也能证明自己上面的论述。

在C++中,除非是明确的POD类型,否则放弃使用mem*系包括malloc等传统C内存操作函数。

首先,我们需要弄明白,我们为什么需要使用memet,因为我们需要将这个结构体的数据清零,所以我们真正需要的是一个结构体(类)的构造函数!在类中写一个结构体,将类里的所有成员变量进行列表初始化就可以解决这个问题了。
话说回来,就好像,我们在写C代码时, 如果结构体某个成员类型需要是结构体类型,我们使用了该结构体指针一样,我们同样可以使用string类型的指针来表示该成员类型。毕竟在VS2010环境下,一个string类型的内存占用就是32byte,而一个string*只有4byte。如果担心hold不住指针类型,可以使用智能指针来折中,如shared_ptr,内存的占用将减小到8。事实上,使用智能使用已经是一个最优方案了。

Ps:string不是常规类型,使用时不能简单等同于“int”之类进行操作,否则很多坑的

size() 判断是否为空,效率低

提示:

Checking for ‘m_listSendBuffer’ emptiness might be inefficient. Using m_listSendBuffer.empty() instead of m_listSendBuffer.size() can be faster. m_listSendBuffer.size() can take linear time but m_listSendBuffer.empty() is guaranteed to take constant time.

翻译:

检查’m_listSendBuffer’空虚可能效率低下。 使用m_listSendBuffer.empty()而不是m_listSendBuffer.size()可以更快。 m_listSendBuffer.size()可以占用线性时间,但m_listSendBuffer.empty()保证需要恒定的时间。

在vocter里empty()的效率要高于size

扩展阅读:

size_type size() const 
{
      size_type __result = 0; 
      distance(begin(), end(), __result); 
      return __result; 
}
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值