在C ++中,有许多功能可能被认为具有潜在的危险-由于设计错误或编码不正确,它们很容易导致错误。 本文提供了一些此类功能,并提供了有关如何减少其负面影响的提示。
目录
Praemonitus,praemunitus。
预知意味着武装。 (晚)
引言
在C ++中,有许多功能可能被认为具有潜在的危险-由于设计错误或编码不正确,它们很容易导致错误。 其中一些可以归因于童年的艰难,一些归因于过时的C ++ 98标准,但其他归因于现代C ++的功能。 考虑主要因素,并就如何减少其负面影响提出建议。
1.类型
1.1。 有条件的指令和运算符
与C兼容性的需求导致这样的事实,即在if(...)
之类的语句中,您可以替换任何数值表达式或指针,而不仅仅是bool
表达式。 在bool
表达式到bool
表达式的隐式转换以及某些运算符的优先级使问题更加复杂。 例如,这导致以下错误:
if(a=b)
何时正确if(a==b)
,
if(a<x<b)
,如果正确, if(a<x && x<b)
,
if(a&x==0)
,如果正确, if((a&x)==0)
,
if(Foo)
正确, if(Foo())
,
if(arr)
正确时if(arr[0])
,
if(strcmp(s,r))
正确时if(strcmp(s,r)==0)
。
其中一些错误会引起编译器警告,但不会导致错误。 代码分析器有时也可以提供帮助。 在C#中,这样的错误几乎是不可能的, if(...)
等需要bool
类型,则不能在算术表达式中混合bool
类型和数字类型。
怎么打:
- 程序无警告。 不幸的是,这并不总是有帮助;上述某些错误不会发出警告。
- 使用静态代码分析器。
- 老式的接收技术:与常量比较时,将其放在左侧,例如
if(MAX_PATH==x)
。 它看起来很漂亮(甚至有自己的名字-“ Yoda符号”),并且在考虑的少数情况下有帮助。 - 尽可能广泛地使用
const
限定词。 同样,它并不总是有帮助。 - 习惯于编写正确的逻辑表达式:
if(x!=0)
而不是if(x)
。 (尽管您可以在此处陷入操作员优先级的陷阱,请参阅第三个示例。) - 非常专心。
1.2。 隐式转换
C ++指的是强类型语言,但是隐式类型转换被广泛用于使代码更短。 这些隐式转换在某些情况下可能导致错误。
最烦人的隐式转换是数值类型或指针到bool
以及从bool
到int
。 这些转换(与C兼容所必需)导致了1.1节中描述的问题。 隐式转换(例如,从double
精度到int
)可能会导致数值数据的准确性下降(缩小的转换)也不总是合适的。 在许多情况下,编译器会生成警告(特别是在数值数据的准确性可能会降低的情况下),但是警告不是错误。 在C#中,禁止在数字类型和bool
之间进行转换(甚至是显式的转换),并且可能导致数字数据精度下降的转换几乎总是错误。
程序员可以添加其他隐式转换:(1)使用一个参数而不使用explicit
关键字定义一个构造函数; (2)类型转换运算符的定义。 这些转换打破了基于强类型原则的其他安全漏洞。
在C#中,内置隐式转换的数量要少得多;必须使用implicit
关键字声明自定义的隐式转换。
怎么打:
- 程序无警告。
- 请特别注意上述设计,不要在没有极端需要的情况下使用它们。
2.名称解析
2.1。 在嵌套作用域中隐藏变量
在C ++中,以下规则适用。 让
根据C ++规则,
声明的变量
隐藏
声明的变量
第一个声明x
不必在块中:它可以是类的成员或全局变量,只需要在块
可见即可
现在想象一下当您需要重构以下代码时的情况
错误地进行了更改:
现在,代码“正在用来自
完成某件事”将对来自
有所帮助! 显然,一切都无法像以前那样工作,并且发现通常很难的东西。 C#中禁止隐藏局部变量(尽管类成员可以)并不是徒劳的。 请注意,几乎所有编程语言都使用一种以另一种形式隐藏变量的机制。
怎么打:
- 在尽可能小的范围内声明变量。
- 不要写长而深的嵌套块。
- 使用编码约定在视觉上区分不同范围的标识符。
- 非常专心。
2.2。 函数重载
函数重载是许多编程语言不可或缺的功能,C ++也不例外。 但是这个机会必须谨慎使用,否则会遇到麻烦。 在某些情况下,例如,当构造函数重载时,程序员别无选择,但在其他情况下,拒绝重载是合理的。 考虑使用重载函数时出现的问题。
如果尝试考虑解决过载时可能出现的所有可能选项,那么解决过载的规则就会变得非常复杂,因此很难预测。 模板功能和内置运算符的重载引入了更高的复杂性。 C ++ 11增加了右值链接和初始化列表的问题。
搜索算法可能会为候选人解决问题,以解决嵌套可见性区域中的过载问题。 如果编译器在当前范围内找到任何候选者,则进一步搜索终止。 如果找到的候选者不合适,冲突,被删除或无法访问,则会生成错误,但不会尝试进一步搜索。 并且只有在当前范围内没有候选者时,搜索才移至下一个更大的范围。 名称隐藏机制的工作原理与第2.1节中讨论的基本相同,请参见[Dewhurst]。
重载功能会降低代码的可读性,这会引发错误。
使用带有默认参数的函数看起来像使用重载函数,尽管当然,潜在问题更少。 但是可读性差和可能存在错误的问题仍然存在。
要格外小心,应使用虚拟函数的重载和默认参数,请参见第5.2节。
C#还支持函数重载,但是解决重载的规则略有不同。
怎么打:
- 不要滥用函数重载,也不要滥用带有默认参数的函数。
- 如果函数被重载,则在解决重载时使用毫无疑问的签名。
- 不要在嵌套作用域中声明相同名称的函数。
- 不要忘记,C ++ 11中出现的远程函数机制(
=delete
)可用于禁止某些重载选项。
3.构造函数,析构函数,初始化,删除
3.1。 编译器生成的类成员函数
如果程序员尚未从以下列表中定义该类的成员函数-默认构造函数,复制构造函数,复制赋值运算符,析构函数-则编译器可以为他执行此操作。 C ++ 11在此列表中添加了移动构造函数和移动赋值运算符。 这些成员函数称为特殊成员函数。 仅在使用它们并且满足每个功能特定的附加条件时才生成它们。 我们提请注意以下事实:这种用法可能被隐藏起来(例如,在实现继承时)。 如果无法生成所需的功能,则会生成错误。 (除重定位操作外,它们均由复制操作代替。)编译器生成的成员函数是公共的并且是可嵌入的。 特殊成员功能的详细信息可以在[Meyers2]中找到。
在某些情况下,来自编译器的此类帮助可能是“负担服务”。 缺少自定义特殊成员函数会导致创建琐碎的类型,而这又导致未初始化变量的问题,请参阅第3.2节。 生成的成员函数是公共的,并且这并不总是与类的设计一致。 在基类中,必须保护构造函数;有时,为了更好地控制对象的生命周期,需要一个受保护的析构函数。 如果类具有原始资源描述符作为成员并拥有此资源,则程序员需要实现一个复制构造函数,复制赋值运算符和析构函数。 所谓的“三巨头规则”是众所周知的,它指出,如果程序员定义了三个操作(复制构造函数,复制赋值运算符或析构函数)中的至少一个,则他必须定义所有三个操作。 编译器生成的move构造函数和move赋值运算符也不总是您所需要的。 在某些情况下,由编译器生成的析构函数会导致非常细微的问题,其结果可能是资源泄漏,请参见3.7节。
程序员可以禁止生成特殊的成员函数,在C ++ 11中声明时,必须使用"=delete"
构造,在C ++ 98中,声明相应的成员函数为private而不定义。
如果程序员对编译器生成的成员函数感到满意,那么在C ++ 11中,他可以明确地指出这一点,而不仅仅是删除声明。 为此,在声明时必须使用"=default"
构造,这样可以更好地阅读代码,并且出现与管理访问级别有关的其他功能。
在C#中,编译器可以生成一个默认的构造函数,通常这不会引起任何问题。
怎么打:
- 控制编译器生成特殊的成员函数。 如有必要,请自己实施或禁止实施。
3.2。 未初始化的变量
构造函数和析构函数可以称为C ++对象模型的关键元素。 创建对象时,必须调用构造函数,而在删除对象时,必须调用析构函数。 但是C的兼容性问题已经强制了一些异常,该异常称为琐碎类型。 引入它们是为了模拟sichny类型和变量的系统生命周期,而无需构造函数和析构函数的强制调用。 C代码如果在C ++中编译和执行,则应与C语言一样工作。普通类型包括数字类型,指针,枚举以及由普通类型组成的类,结构,联合和数组。 类和结构必须满足一些附加条件:缺少自定义构造函数,析构函数,复制,虚函数。 对于平凡的类,编译器可以生成默认的构造函数和析构函数。 默认构造函数将对象清零,析构函数不执行任何操作。 但是,只有在初始化变量时显式调用此构造函数时,才会生成和使用该构造函数。 如果您不使用显式初始化的某些变体,那么琐碎类型的变量将不会被初始化。 初始化语法取决于变量声明的类型和上下文。 声明时将初始化静态和局部变量。 对于类,直接基类和非静态类成员在构造函数初始化列表中初始化。 (C ++ 11允许您在声明时初始化非静态类成员,请参见下文。)对于动态对象,表达式new T()
创建一个由默认构造函数初始化的对象,而对于普通类型的new T
创建一个未初始化的对象。 创建简单类型的动态数组new T[N]
,其元素将始终未初始化。 如果创建或扩展了std::vector<T>
的实例,并且未提供用于元素的显式初始化的参数,则可以保证它们调用默认构造函数。 C ++ 11引入了新的初始化语法-使用花括号。 一对空括号表示使用默认构造函数进行初始化。 在使用传统初始化的任何地方,这种初始化都是可能的,此外,在声明时可以初始化类的非静态成员,从而代替了构造函数初始化列表中的初始化。
未初始化的变量的结构如下:如果在namespace
范围(全局)中定义,则所有位为零,如果是本地或动态创建,则将接收随机位。 显然,使用此类变量可能导致程序的行为无法预测。
没错,进展不会停滞不前,现代编译器在某些情况下会检测未初始化的变量并引发错误。 未初始化的代码分析器可以更好地检测。
C ++ 11标准库具有称为类型属性的模板(头文件<type_traits>
)。 其中之一可让您确定类型是否琐碎。 如果T
琐碎的类型,则表达式std::is_trivial<>::value
为true
否则T
false
。
收缩结构也通常称为普通旧数据(POD)。 我们可以假设POD和“琐碎的类型”几乎是等效的。
在C#中,未初始化的变量会导致错误;这是由编译器控制的。 如果未执行显式初始化,则默认情况下将初始化引用类型的对象的字段。 重要类型的对象的字段默认情况下全部初始化,或者必须显式初始化所有字段。
怎么打:
- 养成显式初始化变量的习惯。 未初始化的变量应“吸引眼球”。
- 在尽可能小的范围内声明变量。
- 使用静态代码分析器。
- 不要设计琐碎的类型。 为了确保类型不是无关紧要的,定义一个自定义构造函数就足够了。
3.3。 基类和非静态类成员的初始化过程
在实现类构造函数时,将初始化直接基类和非静态类成员。 初始化顺序由标准确定:首先按在基类列表中声明基类的顺序创建基类,然后按声明顺序依次声明该类的非静态成员。 如有必要,基类和非静态成员的显式初始化使用构造函数初始化列表。 不幸的是,该列表中的项目不需要按照初始化发生的顺序进行。 如果在初始化期间列表项使用对其他列表项的引用,则必须考虑到这一点。 错误时,链接可能指向尚未初始化的对象。 C ++ 11允许您在声明(使用花括号)时初始化非静态类成员。 在这种情况下,不需要在构造函数初始化列表中对其进行初始化,并且可以部分解决问题。
在C#中,对象的初始化过程如下:首先对字段进行初始化,从基础子对象到最后一个派生对象,然后按相同顺序调用构造函数。 不会发生所描述的问题。
怎么打:
- 按声明顺序维护构造函数初始化列表。
- 尝试使基类和类成员的初始化独立。
- 声明时,请使用非静态成员的初始化。
3.4。 静态类成员和全局变量的初始化过程
静态类成员以及在不同编译单元(文件)中的作用域namespace
(全局)中定义的变量,将按实现确定的顺序进行初始化。 如果在初始化期间此类变量使用相互引用,则应考虑到这一点。 链接可能指向未初始化的变量。
怎么打:
- 采取特殊措施来防止这种情况。 例如,使用局部静态变量(单例),它们在首次使用时被初始化。
3.5。 析构函数中的异常
析构函数不应引发异常。 如果违反此规则,则可能会出现不确定的行为,通常是异常终止。
怎么打:
3.6。 删除动态对象和数组
如果T
了某种类型T
的动态对象
T* pt = new T();
然后使用delete
运算符将其delete
delete pt;
如果创建了动态数组
T* pt = new T[N];
然后使用delete[]
运算符将其delete[]
delete[] pt;
如果不遵循此规则,则可能会得到未定义的行为,即可能发生任何事情:内存泄漏,崩溃等。 有关详细信息,请参见[Meyers1]。
怎么打:
3.7。 类声明不完整时删除
delete
运算符的杂项性可能会导致某些问题;可以将其应用于void*
类型的指针或具有不完整(抢先)声明的类的指针。 应用于类指针的delete
运算符是两阶段操作;首先,调用析构函数,然后释放内存。如果将运算符应用于delete
具有不完整声明的类的指针,则不会发生错误,编译器仅跳过对析构函数的调用(尽管会发出警告)。考虑一个例子:
class X;
即使在Dial-peer上delete
没有完整的类声明,该代码也会编译X
。Visual Studio显示以下警告:warning C4150: deletion of pointer to incomplete type 'X'; no destructor called
如果有一个执行X
和CreateX()
随后的代码链接,如果该CreateX()
函数返回一个指针由运营商创建的对象new
,调用Foo()
成功,析构函数没有被调用。显然,这可能会导致资源的消耗,因此再次需要注意警告。
, -. , . , , , , . [Meyers2].
:
4. ,
4.1。
++ , . . . , 1.1.
这是一个例子:
std::out<<c?x:y;
(std::out<<c)?x:y;
std::out<<(c?x:y);
, , .
. <<
?:
std::out
void*
. ++ , . -, , . ?:
. , ( ).
: x&f==0
x&(f==0)
, (x&f)==0
, , , . - , , , , .
. / . / , /, . , x/4+1
x>>2+1
, x>>(2+1)
, (x>>2)+1
, .
C# , C++, , - .
:
4.2。
++ , . . , , . 4.1. — +
+=
. . , : ,
(), &&
, ||
。 , (-), (short-circuit evaluation semantics), , . & ( ). & , .. .
, - (-) , . .
- , , . . [Dewhurst].
C# , , , .
:
4.3。
++ , . ( : ,
(), &&
, ||
, ?:
.) , , , . :
int x=0; int y=(++x*2)+(++x*3);
y
.
, . .
class X; class Y; void Foo(std::shared_ptr<X>, std::shared_ptr<Y>);
Foo()
:
Foo(std::shared_ptr<X>(new X()), std::shared_ptr<Y>(new Y()));
: X
, Y
, std::shared_ptr<X>
, std::shared_ptr<Y>
. Y
, X
.
:
auto p1 = std::shared_ptr<X>(new X()); auto p2 = std::shared_ptr<Y>(new Y()); Foo(p1, p2);
std::make_shared<Y>
( , ):
Foo(std::make_shared<X>(), std::make_shared<Y>());
. [Meyers2].
:
5.
5.1。
++98 , ( ), , ( , ). virtual
, , . ( ), , , . , , . , ++11 override
, , , . .
:
5.2。
. , , . . . [Dewhurst].
:
5.3。
, , . , , post_construct pre_destroy. , — . . , : ( ) . (, , .) , ( ), ( ). . [Dewhurst]. , , .
— - .
, C# , , , . C# : , , . , ( , ).
:
5.4。
, , delete
. , - .
:
6.
— C/C++, . . . « ».
C# unsafe mode, .
6.1.
/++ , : strcpy()
, strcat()
, sprinf()
, etc. ( std::vector<>
, etc.) , . (, , , . . Checked Iterators MSDN.) , : , , ; , .
C#, unsafe mode, .
:
- , .
- .
- z-terminated ,
_s
(. ).
6.2. Z-terminated
, . , :
strncpy(dst,src,n);
strlen(src)>=n
, dst
(, ). , , . . — . if(*str)
, if(strlen(str)>0)
, . [Spolsky].
C# string
.
:
6.3.
...
. printf
- , C. , , , , . , .
C# printf
, .
:
7.
7.1.
++ , , , . 这是一个例子:
const int N = 4, M = 6; int x,
:
int
;int
;N
int
;N
int
;- ,
char
int
; - ,
char
int
; - ,
char
int
; N
, char
int
;N
int
;M
N
int
;- ,
char
, long
int
.
, . ( .)
*
&
. ( .)
typedef
( using
-). , :
typedef int(*P)(long); PH(char);
, .
C# , .
:
7.2.
.
class X { public: X(int val = 0);
X x(5);
x
X
, 5.
X x();
x
, X
, x
X
, . X
, , :
X x; X x = X(); X x{};
, , , . [Sutter].
, , C++ ( ). . ( C++ .)
, , , , .
C# , , .
:
8.
8.1. inline
ODR
, inline
— . , . inline
(One Defenition Rule, ODR). . , . , ODR. static
: , , . static
inline
. , , ODR, . , . - , -. .
:
- «»
inline
. namespace
. , . - —
namespace
.
8.2.
. . , , , , .
:
- , .
- , : () , -.
using
-: using namespace
, using
-.- .
8.3. switch
— break
case
. ( .) C# .
:
8.4.
++ , — , — . ( class
struct
) , . ( , # Java.) — , .
- , . (
std::string
, std::vector
, etc.), , . - , , .
- , (slicing), , .
, , , . . , , . , . . — ( =delete
), — explicit
.
C# , .
:
8.5. 资源管理
++ . , . - ( ), ++11 , , , .
C++ .
C# , . , . (using-) Basic Dispose.
:
8.6.
«» . , , C++ , STL- - .
. . , . . «», . COM- . (, .) , C++ . — . . . , («» ) , . .
# , . — .
:
8.7.
C++ , : , , . ( !) . , . , . , , . (, .)
C ( ), C++ C ( extern "C"
). C/C++ .
-. #pragma
- , , .
, , , .
, , COM. COM-, , ( , ). COM , , .
C# . , — , C#, C# C/C++.
:
8.8.
, . , . C++ . 相反
#define XXL 32
const int XXL=32;
. inline
.
# ( ).
:
9.
- . . . , .
- .
- . ++ — ++11/14/17.
- - , - .
- .
参考文献
[Dewhurst]
, . C++. .: . . — .: , 2012.
[Meyers1]
, . C++. 55 .: . . — .: , 2014.
[Meyers2]
, . C++: 42 C++11 C++14.: . . — .: «.. », 2016.
[Sutter]
, . C++.: . . — : «.. », 2015.
[Spolsky]
, . .: . . — .: -, 2008.