C ++编程中的问题


在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以及从boolint 。 这些转换(与C兼容所必需)导致了1.1节中描述的问题。 隐式转换(例如,从double精度到int )可能会导致数值数据的准确性下降(缩小的转换)也不总是合适的。 在许多情况下,编译器会生成警告(特别是在数值数据的准确性可能会降低的情况下),但是警告不是错误。 在C#中,禁止在数字类型和bool之间进行转换(甚至是显式的转换),并且可能导致数字数据精度下降的转换几乎总是错误。


程序员可以添加其他隐式转换:(1)使用一个参数而不使用explicit关键字定义一个构造函数; (2)类型转换运算符的定义。 这些转换打破了基于强类型原则的其他安全漏洞。


在C#中,内置隐式转换的数量要少得多;必须使用implicit关键字声明自定义的隐式转换。


怎么打:


  • 程序无警告。
  • 请特别注意上述设计,不要在没有极端需要的情况下使用它们。


2.名称解析



2.1。 在嵌套作用域中隐藏变量


在C ++中,以下规则适用。 让


 //   {    int x;    // ... //  ,       {        int x;        // ...    } } 

根据C ++规则, 声明的变量隐藏声明的变量 第一个声明x不必在块中:它可以是类的成员或全局变量,只需要在块可见即可


现在想象一下当您需要重构以下代码时的情况


 //   {    int x;    // ... //  ,       {    // -         } } 

错误地进行了更改:


 //   {    //  , :    int 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<>::valuetrue否则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]。


怎么打:


  • 使用正确的delete表格。


3.7。 类声明不完整时删除


delete运算符的杂项性可能会导致某些问题;可以将其应用于void*类型的指针或具有不完整(抢先)声明的类的指针。 应用于类指针的delete运算符是两阶段操作;首先,调用析构函数,然后释放内存。如果将运算符应用于delete具有不完整声明的类的指针,则不会发生错误,编译器仅跳过对析构函数的调用(尽管会发出警告)。考虑一个例子:


 class X; //   X* CreateX(); void Foo() {    X* p = CreateX();    delete p; } 

即使在Dial-peer上delete没有完整的类声明,该代码也会编译XVisual Studio显示以下警告:

warning C4150: deletion of pointer to incomplete type 'X'; no destructor called


如果有一个执行XCreateX()随后的代码链接,如果该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 , , , . .


:


  • 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 .


:


  • .
  • z-terminated , _s (. ).


6.3.


... . printf - , C. , , , , . , .


C# printf , .


:


  • . , printf - /.
  • .


7.



7.1.


++ , , , . 这是一个例子:


 const int N = 4, M = 6; int x,                // 1    *px,              // 2    ax[N],            // 3    *apx[N],          // 4    F(char),          // 5    *G(char),          // 6    (*pF)(char),      // 7    (*apF[N])(char),  // 8    (*pax)[N],        // 9    (*apax[M])[N],    // 10    (*H(char))(long);  // 11 

:


  1. int ;
  2. int ;
  3. N int ;
  4. N int ;
  5. , char int ;
  6. , char int ;
  7. , char int ;
  8. N , char int ;
  9. N int ;
  10. M N int ;
  11. , 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{};    //   C++11 

, , , . [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.) — , .


  1. , . ( std::string , std::vector , etc.), , .
  2. , , .
  3. , (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.


  1. . . . , .
  2. .
  3. . ++ — ++11/14/17.
  4. - , - .
  5. .


参考文献


[Dewhurst]
, . C++. .: . . — .: , 2012.


[Meyers1]
, . C++. 55 .: . . — .: , 2014.


[Meyers2]
, . C++: 42 C++11 C++14.: . . — .: «.. », 2016.


[Sutter]
, . C++.: . . — : «.. », 2015.


[Spolsky]
, . .: . . — .: -, 2008.




Source: https://habr.com/ru/post/zh-CN428898/


All Articles