您的位置:首页 > 编程语言 > C语言/C++

C/C++重难点总结系列(四)

2017-01-06 22:57 148 查看
31.C++五种迭代器类型

(1)Input Iterator:只读前向遍历迭代器。

如:istream

(2)Output Iterator:只写前向遍历迭代器。

如:ostream,inserter

(3)Forward Iterator:可读写前向遍历迭代器。

如:forward_list

(4)Bidirectional Iterator:可读写双向遍历迭代器。

如:list、set、multiset、map、multimap

(5)RandomAcess Iterator:可读写随机访问迭代器。

如:vector、deque、string、array

注:在C++迭代器的实现中,上述5种迭代器为继承关系,故迭代器的能力逐级递增。

迭代器的能力也反映了容器的特性,如vector可以随机访问元素,但list不能。

32.关于shared_ptr和unique_ptr的正确声明

(1)shared_ptr<T>

常见方法两种:a.最安全的做法:使用make_shared<T>(args);b.结合new使用。如:

shared_ptr<int> p1=make_shared<int>(42);
auto p2=make_shared<string>("hello");
//make_shared<T>(args)的args参数必须与T的一个构造函数匹配,才能完成动态对象的初始化
结合new使用时,构造函数将内置指针转换为智能指针。

但是智能指针的构造是explicit的,因此内置指针不能隐式转换为智能指针,必须直接构造,故下面第一种写法是错误的!

shared_ptr<int> p1=new int(1024);//error
shared_ptr<int> p2(new int(1024));//correct


(2)unique_ptr<T>

unique_ptr<T>只能绑定到new返回的指针上,不能拷贝或赋值!(独占的含义)如:

unique_ptr<string> p1(new string("test"));//correct
unique_ptr<string> p2(p1);//error


33.关于虚析构函数
(1)为多态基类声明一个虚析构函数,即:一旦一个类出现虚成员函数就应该有一个虚析构函数。

(2)不要为非基类声明虚析构函数。

如:STL容器都是非虚析构函数,因此不要子类化STL容器。(PS:C++11提供了final关键字禁止派生,但STL容器未加入,应主动避免)

(3)为抽象基类(接口类)声明一个纯虚析构函数并给出定义!

注:抽象基类仅提供接口而在派生类中实现成员函数,纯虚析构是个特例!

34.关于C++ 类型转换与类型安全问题

C++提供了四种类型转换,相对于传统的C-style强制类型转换可读性更强,正是因为C++灵活的类型转换,所以:C++不是类型安全的!

另外,C++中应尽量避免转型,会带来效率上的折损和安全问题,如下行转换失败会返回空指针或抛出异常。

(1)const_cast:常量性去除,即const转非const(唯一手段!)

(2)static_cast:强迫隐式隐式转换(将隐式转换显化,与传统强制转换效果一样,但是更可读)如将void*转换为typedef声明的指针。

(3)dynamic_cast:安全地下行转换:将基类的指针/引用转型为派生类的指针/引用,从而间接地操作派生类的某些函数,适用于某些特殊情况。

注:上行转换(即将派生类的指针/引用转型为基类的指针/引用)本身是安全的,因为派生类包含基类的所有属性,所有可直接使用static_cast转换。但是下行转换不安全,dynamic_cast使用了RTTI机制(运行期检查),因此可以保证上行转换这种特殊转换的安全!

(4)reinterpret_cast:执行低级转换,如int*转成int(用的较少)

35.dynamic_cast何时转型失败?

34中已提到dynamic_cast会在运行期检查,那么何时会转型失败从而保证安全?

(1)当基类指针或引用指向派生类时,向下转型是安全的。

(2)当基类指针或引用指向基类时,向下转型会失败(安全检查后阻止):

(直观理解:本身是基类的指针想转派生类是不允许的!但是如果本身是派生类指针而隐式转换成了基类指针(隐式上行转换),后面想逆行转换当然可以)

a.对于指针转型失败会返回空,故可以作if检查判断是否转型成功;

b.对于引用转型失败会抛出bad_cast异常,故可用try_catch做异常处理。

示例:

//转型成功的情况
Base *base=new Derived;
if(Derived *derived = dynamic_cast<Derived*>(base))
cout<<"dynamic_cast successful"<<endl;
else cout<<"dynamic_cast failed"<<endl;
//转型失败的情况(指针)
Base *base=new Base;
if(Derived *derived = dynamic_cast<Derived*>(base))
cout<<"dynamic_cast successful";
else cout<<"dynamic_cast failed"<<endl;
//转型失败的情况(引用)
Base base1;
Base &base2=base1;
try{
Derived &base3=dynamic_cast<Derived&>(base2);
}catch(bad_cast){
cout<<"dynamic_cast failed"<<endl;
}
36.关于异常安全性问题
(1)异常安全基本条件:不泄露任何资源、不破坏任何数据

(2)三种级别的保证:基本保证<强烈保证<不抛异常保证

基本保证:若有异常抛出,所有对象仍然有效;

强烈保证:若有异常抛出,所有数据保持原样

不抛异常保证:不抛异常

(3)提高异常安全性:

a.尽量使用智能指针等RAII技术,解决资源泄露问题;

(注:RAII---核心思想是用对象管理资源:对象构造时获取资源,对象析构时释放资源,如智能指针、互斥锁类等)

b.Copy and Swap技术,解决数据破坏问题

(注:Copy and Swap---核心思想是用副本检测异常:先对需要修改的对象做出一份副本,这个副本的构造使用RAII以确保不会资源泄露,在副本上完成所需的修改,如果修改过程中出现异常,原对象仍保持不变。修改完成后,再通过non-throwing
swap将副本与原对象交换。)

c.调整语句次序,将状态、标志、统计类变量尽量放在代码异常之后;

有异常安全问题的代码示例如下:

void PrettyMenu::changeBackImage(istream &imageSource)
{
lock(&mutex);//上互斥锁
delete bgImage;//删除旧图像
++imageChanges;//图像变更统计
bgImages = new Image(imageSource);//安装新图像
unlock(&mutex);//释放互斥锁
}
//上述代码安全性很差,原因如下:
//(1)new Image若抛出异常,则
//mutex永远不会被释放,造成资源泄露
//(2)new Image若抛出异常,旧图像
//删除且统计变量也已更新,但新图像
//却未安装,造成数据破坏根据上述问题,修改后异常安全的代码如下:
void PrettyMenu::changeBackImage(istream &imageSource)
{
Lock mylock(&mutex);//使用互斥锁类,退出函数时析构函数中释放互斥锁(RAII技术)
bgImage.reset(new Image(imageSource));
//bgImage为shared_ptr<Image>类型,使用其reset成员防止数据破坏和内存泄露(RAII技术)
++imageChanges;//图像变更统计放在新图像安装之后
}

37.重载、重写(覆盖)、隐藏的区别
(1)重载(overload):函数名相同,参数类型不同的函数集(个数或类型不同)

特点:相同范围(同一个类里或同处于全局函数);函数名相同;参数不同

(2)重写(Override):OOP中派生类覆盖基类函数,

特点:不同范围(基类、派生类);函数名相同;参数相同;基类函数必须有virtual修饰

(3)隐藏(Overwrite):

规则:a.派生类与基类同名但参数不同,基类函数一定触发隐藏。(不是重载!)

    b.派生类与基类同名但参数相同,若基类成员无virtual修饰则触发隐藏。(不是覆盖!)

判断方法:若处于同一类中,同名且不同参一定是重载。(先排除重载

    若处于不同类(派生类和基类)中,出现同名先考虑是否是多态中的重写(函数名和参数完全一样而且有virtual关键字),

  一旦排除重写(再排除重写),出现同名不管参数是否一样一定会触发隐藏(剩下情况一定触发隐藏)。

触发结果:隐藏机制触发后,该成员对派生类指针不可见(会直接调用派生类的成员),对基类指针而言仍可见。因此,对象指针到底调用的哪个成员取决于指针类型。

注:规则中第二条其实应该避免!与Effective C++中第36条相违背:绝不重新实现从基类继承的非虚函数(重新定义非虚函数有违public继承的本意,对于需要重新实现的成员应当在基类声明成虚函数)

38.理解接口继承与实现继承

继承体系中分为实现继承和接口继承,由基类成员函数的性质决定:

(1)基类出现纯虚函数(接口继承):仅提供接口给派生类继承,而不提供任何缺省版本(派生类一定要重写!)

(2)基类只有普通虚函数(实现继承):基类提供接口和一个缺省版本给派生类继承(派生类可重写也可不重写,不重写时会调用基类实现的缺省版本!)

注:对于非虚函数(即普通成员函数)应直接继承基类的实现(在内存中会拷贝一份到派生类),而不应重写!否则会触发隐藏机制第二条规则,并且和Effective C++中的建议也是相违背的。若想重写(多态),请使用实现继承或接口继承。

 

39.绝不重新定义一个继承而来的缺省参数值

在继承体系中,基类虚函数中出现缺省参数值,派生类重写时无法重新定义其缺省参数值。原因:

缺省参数值静态绑定,而虚函数动态绑定!

40.关于虚继承

因果:多重继承出现同名成员--->产生二义性--->使用虚继承解决

解释:当多重继承的基类有同名成员时,派生类会产生多个拷贝的二义性问题,通过虚继承保证只有一个拷贝,同一个函数名也只有一个映射。

方法:将需要多重继承的那几个基类虚继承其基类,如D多重继承自B1和B2,则将B1和B2虚继承自它们的基类A

示例:

class B1 : virtual public A //也有一种写法是class CB : public virtual CA
{ //实际上这两种方法都可以
};
class B2 : virtual public A
{
};
class D : public B1, public B2
{
};
使用虚继承与不使用的效果如下:前者为使用虚继承后二义性被解决。



注:虚继承的存在仅在解决多重继承问题里有意义,其他情况应尽量避免(个人理解,如有不当请指正),虚继承会导致效率的降低和产生更大的空间。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  C++ C++11