类型安全 与 线程安全 、异常安全、事务安全

(1)类型安全

类型安全很大程度上可以等价于内存安全类型安全的代码不会试图访问自己没被授权的内存区域。“类型安全”常被用来形容编程语言,其根据在于该门编程语言是否提供保障类型安全的机制;有的时候也用“类型安全”形容某个程序,判别的标准在于该程序是否隐含类型错误。类型安全的编程语言与类型安全的程序之间,没有必然联系。好的程序员可以使用类型不那么安全的语言写出类型相当安全的程序,相反的,差一点儿的程序员可能使用类型相当安全的语言写出类型不太安全的程序。绝对类型安全的编程语言暂时还没有。

C语言的类型安全   相当多的操作不是类型安全的

C只在局部上下文中表现出类型安全,比如试图从一种结构体的指针转换成另一种结构体的指针时,编译器将会报告错误,除非使用显式类型转换。然而,C中相当多的操作是不安全的(例如printf函数和malloc函数)。以下是两个十分常见的例子:

(1)printf格式输出


 int main()

    {

    printf("%d\n",10);

    system("pause");

    return 0;

    }


上面的代码很简单,printf函数中,%d与10匹配,结果正确。

稍作修改:

    int main()

    {

    printf("%f\n",10);

    system("pause");

    return 0;

    }

%f浮点数与10并不匹配,但是编译通过,执行也没报错,但是结果却是:

0.000000

请按任意键继续. . .

更进一步,把%f修改为%s,编译通过,执行将报错Access Violation。

(2)malloc函数的返回值

malloc是C中进行内存分配的函数,它的返回类型是void*即空类型指针,常常有这样的用法char*pStr=(char*)malloc(100*sizeof(char)),这里明显做了显式的类型转换。类型匹配尚且没有问题,但是一旦出现int*pInt=(int*)malloc(100*sizeof(char))就很可能带来一些问题,而这样的转换C并不会提示错误。

 

C++的类型安全   有些操作也不是类型安全的(两个不同类型的指针之间可以强制转换(用reinterpret cast),C#、Java是类型安全的)

如果C++使用得当,它将远比C更有类型安全性。相比于C,C++提供了一些新的机制保障类型安全:

(1)操作符new返回的指针类型严格与对象匹配,而不是void*;

(2)C中很多以void*为参数的函数可以改写为C++模板函数,而模板是支持类型检查的;

(3)引入const关键字代替#define constants,它是有类型、有作用域的,而#define constants只是简单的文本替换;

(4)一些#define宏可被改写为inline函数,结合函数的重载,可在类型安全的前提下支持多种类型,当然改写为模板也能保证类型安全;

(5)C++提供了dynamic_cast关键字,使得转换过程更加安全,因为dynamic_cast比static_cast涉及更多具体的类型检查。

即便如此,C++也不是绝对类型安全的编程语言。如果使用不得当,同样无法保证类型安全。比如下面两个例子:

    int i=5;

    void* pInt=&i;

    double d=(*(double*)pInt);

    cout<< d << endl;

输入结果不是5,而意想不到的结果:-9.25596e+061。又比如:

帮助


#include 

using namespacestd; 

class Parent 

}; 

class Child1:publicParent 

public: 

    int i; 

    Child1(int e):i(e) 

    { 

    } 

}; 

class Child2:publicParent 

public: 

    double d; 

    Child2(double e):d(e) 

    { 

    } 

}; 

int main() 

    Child1 c1(5); 

    Child2 c2(4.1); 

    Parent* pp; 

    Child1* pc1; 

  

    pp=&c1; 

    pc1=(Child1*)pp;  //#1 强制转换,由于类型仍然为Child1*,不造成错误 

    cout<i<

  

    pp=&c2; 

    pc1=(Child1*)pp;  //#2 强制转换,且类型发生变化,将造成错误 

    cout<i<

    system("pause"); 

    return 0; 

}


结果如下:

5

1717986918

请按任意键继续. . .

上面两个例子之所以引起类型不安全的问题,是因为程序员使用不得当。第一个例子用到了空类型指针void*,第二个例子则是在两个类型指针之间进行强制转换。因此,想保证程序的类型安全性,应尽量避免使用空类型指针void*,尽量不对两种类型指针做强制转换。

综上所述:CC++是非类型安全的,JavaC#是类型全的。

(2)线程安全

如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。线程安全问题都是由全局变量静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。

(3)异常安全

当异常抛出时,带有异常安全的函数会:

1>   不泄露任何资源

一般采用RAII技术,即以对象管(智能指针)理资源来防止资源泄漏。

2>   不允许数据被破坏(例如正常指针变野指针)

3>   少些try catch,因为大量的try catch会影响代码逻辑。导致代码丑陋混乱不优雅

第一个是造成资源泄漏的例子。一个类Type,内含一个互斥锁成员Mutex mutex,以及一个成员函数voidFunc()。假设Func函数的实现如下所示:

    void Type::Func() 

   { 

       Lock(&mutex); 

       DoSomething(); 

       UnLock(&mutex); 

   } 

首先是获得互斥锁,中间是做该做的事,最后释放互斥锁。从功能上来讲很完整,没任何问题。但从异常安全角度来说,它却不满足条件。因为一旦DoSomething()函数内部导致异常,UnLock(&mutex)将不会被执行,于是互斥器将永远不会被释放了。换句话说即造成了资源泄漏。

    再来看第二个造成数据破坏的例子。这个例子是我们很熟悉的重载 ‘=’ 操作符的成员函数。依然假设一个类Type,其中一个成员是一个指向一块资源(假设类型为T)的指针。这时候我们一般就需要来自定义复制构造函数和重载复制操作符以及析构函数了。(绝大多数情况下,这三个成员总是要么同时存在,要么都不用定义,因为编译器默认定义了,即C++中所谓的“三原则”规则。这里不作详细介绍)。这里我们只考虑重载复制操作符的问题,其部分代码假设如下:

class Type 

   { 

   public: 

       .... 

       Type& operator = (const Type &t) 

       { 

            if(this == &t) 

                return *this; 

            else 

            { 

                delete m_t; 

                m_t = new T(t->m_t); 

                return *this; 

            } 

       } 

       .... 

   private: 

       T *m_t; 

    };  

首先来判断是否是自我复制,如果是,则直接返回自己。如果不是,则安全释放当前指向的资源,再创建一块与被复制的对象资源一模一样的资源并指向它,最后返回复制好的对象。同样,抛开异常安全来看,没问题。但是考虑到异常安全性时,一旦“newT(t->m_t)"时抛出异常,m_t将指向一块已被删除的资源,并没有真正指向一块与被复制的对象一样的资源。也就是说,原对象的数据遭到破坏。

C++中的异常安全函数提供了三种安全等级

1、函数提供基本保证(thebasic guarantee)(不会发生内存泄漏并且程序内的每个对象都处在合法的状态,没有流错位,没有野指针,但是不是每个对象的精确状态是可以预期的,如可能发生异常后,指针处在空指针状态,或者某种默认状态,但是客户无法预知到底是哪一个),对于达成基本保证可以多使用智能指针来管理资源

2、函数提供强力保证(thestrong guarantee),强力保证含义是,成功或者回滚保证,发生异常的函数对程序来说,没有任何改动,提供发生异常时候的回滚机制。

调用提供强力保证的函数之后,仅有两种可能的程序状态:像预期一样成功执行了函数,或者函数回滚继续保持函数被调用时当时的状态。与之相比,如果调用只提供基本保证的函数引发了异常,程序可能存在于任何合法的状态。

函数提供强力保证的有效解决办法是copy-and-swap:先做出一个你要改变的对象的copy,然后在这个copy上做出全部所需的改变。如果改变过程中的某些操作抛出了异常,最初的对象保持不变。在所有的改变完全成功之后,将被改变的对象和最初的对象在一个不会抛出异常的操作中进行swap。

1.     函数有不抛出保证(the nothrow guarantee),对于所有对内建类型(例如,ints,指针,等等)的操作都是不抛出(nothrow)的(也就是说,提供不抛出保证)。这是异常安全代码中必不可少的基础构件。

注意现在我们来一个个解决上面两个问题。

    对于资源泄漏问题,解决方法很容易,即用对象来管理资源。RAII技术之前介绍过,这里就不再赘述。我们在函数中不直接对互斥锁mutex进行操作,而是用到一个管理互斥锁的对象MutexLockml。函数的新实现如下:

  void Type::Func() 

   { 

       MutexLock ml(&mutex); 

       DoSomething(); 

   } 

对象ml初始化后,自动对mutex上锁,然后做该做的事。最后我们不用负责释放互斥锁,因为ml的析构函数自动为我们释放了。这样,即时DoSomething()中抛出异常,ml也总是要析构的,就不用担心互斥锁不被正常释放的问题了。

    对于第二个问题,一个经典的策略叫“copy and swap"。原则很简单:即先对原对象做出一个副本(copy),在副本上做必要的修改。如果出现任何异常,原对象依然能保证不变。如果修改成功,则通过不抛出任何异常的swap函数将副本和原对象进行交换(swap)。函数的新实现如下:

    Type& Type::operator = (constType &t) 

   { 

       Type tmp(t); 

       swap(m_t,tmp->m_t); 

         

       return *this; 

   } 

先创建一个被复制对象t的副本tmp,此时原对象尚未有任何修改,这样即使申请资源时有异常抛出,也不会影响到原对象。如果创建成功,则通过swap函数对临时对象的资源和原对象资源进行交换,标准库的swap函数承诺不抛出异常的,这样原对象将成功变成对象 t 的复制版本。对于这个函数,我们可以认为它是”强烈保证“异常安全的。

    当然,提供强烈保证并不是总是能够实现的。一个函数能够提供的异常安全性等级,也取决于它的实现。考虑以下例子:

void Func() 

   { 

       f1(); 

       f2(); 

   } 

如果f1和f2都提供了”强烈保证“,则显然Func函数是具有”强烈保证“的安全等级。但是如果f1或f2中有一个不能提供,则Func函数将不再具备”强烈保证“等级,而是取决于f1和f2中安全等级最低的那个。

注意事项:

异常安全 最关键的是:swapctor dctor 不发生异常保证,只有成功或者终止程序两种状态。

一个函数的异常安全等级,是取决于它所调用的函数中最低异常安全等级的函数(详解上例中fun的安全等级)。

从异常安全的观点看,不抛出的函数(nothrowfunctions)是极好的,但是在 C++ 的 C 部分之外部不调用可能抛出异常的函数简直就是寸步难行。使用动态分配内存的任何东西(例如,所有的 STL 容器)如果不能找到足够的内存来满足一个请求,在典型情况下,它就会抛出一个bad_alloc 异常。只要你能做到就提供不抛出保证,但是对于大多数函数,选择是在基本的保证和强力的保证之间的。

但是,不是所有函数都能做出异常保证的,考虑这样一个函数,函数内部的函数内是一个对数据库的操作,一旦异常发生,难以撤销对数据库的更改。如果想对这样的函数做到异常的strongguarantee保证,就是非常困难度事情。

所以对于只对局部变量改变的函数保证异常安起会相对比较容易。如果函数的操作中牵扯到全局变量等等,就变得困难的多。

 

解决异常安全的好办法

1,多使用RAII,使用智能指针来管理内存。由于unwind机制的保证,当异常发生时,函数栈内已构造的局部对象的析构函数会被一一调用,在析构函数内释放资源,也就杜绝了内存泄漏的问题。

2,做好程序设计。特别是异常发生时的回滚机制的正确使用,copy-and-swap是有效的方法。

3,注意需要异常保证的函数内部的调用函数,异常安全等级是以有最低等级异常保证的函数确定的。

一个系统即使只有一个函数不是异常安全的,那么系统作为一个整体就不是异常安全的,因为调用那个函数可能发生泄漏资源和恶化数据结构。

4,对于一些需要保证性能的程序,在提供基本的异常安全时,要注意,栈解退机制只是调用析构函数,对于内置类型的操作不会被回滚,所以。像起累加器作用的一些内置类型变量,应该注意在函数成功执行后再进行累加。避免数据结构恶化。重新分配资源给原本已经持有资源的变量,应该先清空释放变量的资源,指针再设置为null ptr,防止资源重新分配过程中抛出异常,导致指针变为野指针的问题。

5,流对象,资源对象,new对象,不应该直接作为参数,一旦抛出异常,就可能会导致严重的问题,函数也许会被错误的执行,资源也许会泄漏。对于函数参数和函数内使用的全局变量,应该保证在进入函数内部是是正常状态。

6, 减少全局变量的使用,对包含全局变量的函数做异常安全是比较困难的事情,栈解退也只对局部变量起效果。

7,如果不知道如何处理异常,就不要捕获异常,直接终止比吞掉异常不处理要好

8.保证 构造 析构 swap不会失败

注意:在构造函数中,如果抛出异常,是不会调用当前正在构造的类的析构函数的,因为当前正在构造的类没有构造完成,只会析构已经构造完成成员和父类,So,极易导致内存泄漏,这里要谨慎处理,使用RAII,智能指针,noexcept保证不会抛出异常和恶化数据。

(4)事务安全

某些操作支持所谓的“交付或恢复(commit-or-rollback)”,二择一的行为,称为“事务安全”。

你可能感兴趣的:(C++,编程出现的异常)