==========================================================================
day11 面向对象程序设计
==========================================================================ios
1.面向对象程序设计的核心思想是数据抽象、继承、动态绑定(也叫封装、继承、多态)数组
数据抽象:将类的接口和实现分离。
继 承:能够定义类似的类型并对其类似关系建模。
动态绑定:能够在必定程度上忽略类似类型的区别,而以统一的方式使用它们的对象。安全
2.基类将它的成员函数分为两类,一种是但愿派生类直接继承而不须要改变的函数;另外一种是但愿派生类进行覆盖的函数。
对于后者,基类一般将其定义为虚函数(virtual)。任何构造函数以外的非静态函数均可以是虚函数。
当咱们使用指针或引用调用虚函数时,该调用将被动态绑定,根据指针或引用所绑定的对象的不一样,该调用可能执行基类版本,也可能执行某个派生类版本。多线程
3.若是基类把一个函数声明为virtual,则该函数在派生类中隐式地也是虚函数。ide
4.派生类能访问基类的共有成员,但不能访问私有成员。可是某些时候,基类中还有这样一些成员,基类但愿它的派生类能够访问,同时禁止其余用户访问。因而产生了protected成员。函数
5.【派生类继承基类全部成员,除了构造函数和拷贝控制成员。】this
6.派生列表中用到的访问说明符的做用是,控制派生类从基类继承来的成员是否对派生类的用户可见。
class Father{
...
};线程
class Son : private Father{ // 私有继承,继承来的基类成员对派生类的用户不可见
...
};设计
7.咱们能够将基类的指针或引用绑定到派生类对象上。 但若是基类的对象既不是指针,又不是引用就不能够。
编译器也会隐式地执行派生类到基类的转换。这种隐式特性意味着咱们能够在须要基类指针或引用的地方使用派生类来代替。指针
【须要记住的一点是,即便基类的引用或指针知道本身其实是一个子类,也不能调用基类中未定义的子类方法或者成员。】
【这是由于,当调用非虚函数时,不会发生动态绑定,实际调用的函数版本由指针或引用的静态类型决定!】
eg:
class Super{
public:
Super();
virtual void someMethod();
...
};
class Sub : public Super{
public:
Sub();
virtual void someMethod(); // 覆盖了基类的该方法
virtual void someOtherMethod(); // 子类新增的方法
};
考虑以下:
Sub mySub;
Super &ref = mySub;
mySub.someOtherMethod(); // 正确
ref.someOtherMethod(); // 错误 当调用非虚函数时,不会发生动态绑定,实际调用的函数版本由指针或引用的静态类型决定
// 引用的静态类型是基类类型,但基类中没有此方法,因此错误!
若是非引用、非指针的对象,则不具有这个特性。
eg: Sub mySub;
Super obj = mySub; // 将派生类对象直接赋值或强制转换给基类,然而这样一来,对象就丢失了子类的一些知识。也就是致使了切割(slicing)
// 切割也就是覆盖的方法和子类数据的丢失
obj.someMethod(); // 调用的是基类版本的此方法,而非派生类版本
不存在基类向派生类的隐式转换。(但能够强制转换,使用dynamic_cast能够将基类指针/引用安全地转换为派生类的指针/引用)
更特殊的是:
Bulk_quote bulk;
Quote *itemp = &bulk; // 正确,基类对象指针指向子类对象
Bulk_quote *bulkp = itemp; // 错误,不能将基类转换为派生类,即便基类实际上指向的是派生类也不行
8.派生类继承了基类后,它必须用基类的构造函数来初始化它的基类部分。
【这是C++中的一个关键概念:每一个类负责定义各自的接口。要想与类的对象交互必须使用该类的接口,即便这个对象是派生类的基类部分。】
class Quote{
public:
...
Quote(const string &book,double sales_price):bookNo(book),price(sales_price){}
..
private:
string bookNo;
protected:
double price = 0.0;
};
class Bulk_quote : public Quote{ //Bulk_quote继承了Quote
public:
...
Bulk_quote(const string &,double,size_t,double);
...
private:
size_t min_qty = 0;
double discount = 0.0;
};
eg: Bulk_quote(const string &book,double p,size_t qty,d disc):
Quote(book,p),min_qty(qty),discount(disc){} // 使用基类构造函数初始化它的基类部分
9.防止继承 (C++新标准)
有时候咱们不但愿一个类被其余类继承。为了实现这个目的,在类名后面加关键字 final
eg: class NoDerived final {...}
==================================================================================
10.dynamic_cast运算符。用于将基类的指针或引用安全地转换为派生类的指针或引用
特别适用于:咱们想使用基类的指针或引用执行某个派生类的操做而且该操做并非虚函数。
dynamic_cast<type*>(e);
dynamic_cast<type&>(e);
e必须知足:e必须是目标type的公有派生类或公有基类或自己就是type类型。
若是符合条件,则转换成功,不然转换失败,要转换的类型为指针则返回0,为引用则抛出bad_cast异常
用法:
对于指针:
if(Derived *d = dynamic_cast<Derived*>(b)){
// 使用d指向的Derived对象
}else{ // 转换失败
// 使用b指向的Base对象
}
对于引用:
void f(const Base &b){
try{
const Derived &d = dynamic_cast<Derived&>(b);
// 使用b引用的Derived对象
}catch(bad_cast){
//处理类型转换失败的状况
}
}
typeid运算符。typeid(e),e能够是任何表达式或类型的名字,返回的是一个常量对象的引用。(type_info类型的)
注意点:typeid忽略顶层const,typeid一个数组,返回数组类型,而不是指针类型。
typeid通常用来比较两个表达式的类型是否同样:if(typeid(*b) == typeid(*d)) ... // typeid应该做用于对象,所以咱们使用*b而不是b
【使用RTTI】 p734
某些状况下,RTTI很是有用,好比当咱们想为具备继承关系的类实现相等运算符时。
11. 一般状况下,若是咱们不使用某个函数,就无须为该函数提供定义。
但咱们必须为虚函数提供定义,无论它有没有被使用到。
12.一个派生类的函数若是覆盖了某个继承来的虚函数,则它的形参类型必须与被它覆盖的基类函数彻底一致。
返回类型也必须一致。但有一个例外:当类的虚函数返回类型是类自己的指针或引用时。
也就是说若是D由B派生获得,则基类的虚函数能够返回B*而派生类的对应函数能够返回D*,只不过这样的返回类型【要求从D到B的类型转换是可访问的】。
13.override说明符的使用。
实际上,派生类若是定义了一个函数与基类中虚函数名字相同但形参列表不一样,这仍然是合法的行为。编译器会认为这两个函数是独立的,也就是派生类的
函数并无覆盖掉基类中的版本。 这种声明通常意味着咱们本来想覆盖基类中的虚函数,但一不当心形参列表弄错了。而编译器并不会发现这样的错误,
因而override产生了,它被用来防止这种错误。若是咱们使用override标记了某个函数,但该函数并无覆盖虚函数,此时编译器就会报错。
14.final的使用
除了阻止类被继承以外,咱们还能够经过在形参列表后面加final来阻止函数被覆盖。
15.若是虚函数使用默认实参,则基类和派生类中定义的默认实参最好一致。
这是由于:若是虚函数调用使用了默认实参,则该实参值由本次调用的静态类型决定。
也就是说,若是咱们经过基类的指针或引用调用虚函数,即便实际运行的是派生类中的函数版本,也依旧使用的是基类中定义的默认实参。
16.派生类能够访问基类中的protected成员,但有一点必须明确:派生类的成员或友元只能经过派生类对象(而不能经过基类对象)来访问基类的受保护成员。 p543
17.类的对象不能访问类的私有成员,只能经过类成员和友元函数访问。
类的对象也不能访问类的保护成员,只能经过类的成员函数或派生类的成员函数访问。之前理解错了。。。
18.派生类向基类转换的可访问性:
1)若是D公有继承B,【用户代码】才能使用派生类向基类的转换。若是是protected或private则不行。 用户代码我理解为函数体外的地方
2)不论D以什么方式继承B,【D的成员函数和友元】均可以使用派生类向基类的转换。
3)若是D继承B的方式是公有的或保护的,则【D的派生类的成员和友元】可使用派生类向基类的转换。若是是私有的则不行。
19.默认继承
和struct成员默认为公有,class成员默认为私有同样,默认继承也是如此:
struct D1 : Base{...} // 默认为公有继承
class D2 : Base{...} // 默认为私有继承
20.改变派生类继承的某个名字的访问级别
class Base{
public:
size_t size() const {return n;}
protected:
size_t n;
};
class Derived : private Base{
public:
using Base::size();
protected:
using Base::n;
};
考虑以上代码,Derived是私有继承,它继承来的成员对它的用户来讲是私有。但咱们经过using声明改变了这些成员的访问级别,它们再也不是私有的了。
改变以后,Derived的用户可使用size()成员,而Derived的派生类可使用n
以上须要注意的一点是:派生类只能为那些它能够访问的基类成员提供using声明。 也就是基类中的private成员不能够被改变访问级别
21.派生类的成员将隐藏同名的基类成员,可使用做用域运算符来使用被隐藏的基类成员。
struct Base{
Base():mem(0){}
protected:
int mem;
};
struct Derived:Base{
Derived(int i):mem(i){}
int get_Dmem(){return mem;}
int get_Bmem(){return Base::mem;} // 0
protected:
int mem; // 隐藏了基类中的同名成员
};
Derived d(42);
cout<<d.get_Dmem()<<endl; // 42
22.经过函数调用的解析过程来理解C++继承:
假定咱们调用p->mem()
1)首先肯定p的静态类型,因为咱们调用的是一个成员,p必须是类类型。
2)在p的静态类型对应的类中查找mem。若是找不到,依次在直接基类中不断查找直到继承链的顶部。还找不到,则报错。
3)一旦找到mem,进行常规类型检查,看调用是否合法。
4)若是合法,则编译器根据调用的是不是虚函数产生不一样的代码。即动态调用仍是普通调用
23.派生类的做用域是嵌套在基类的做用域的,也能够说基类做用域是外层做用域,派生类是内层做用域。
【在C++中,名字查找发生在类型检查以前。】 重载是根据函数名、参数的个数、参数的类型
在不一样的做用域,咱们没法重载函数名。若是咱们在内层做用域声明名字,它将隐藏外层做用域中声明的同名实体。
24.虚析构函数
当咱们delete一个动态分配的对象的指针时,将执行析构函数。
若是咱们delelte一个基类的指针,而该指针实际上指向的是一个派生类对象,就会产生未定义的行为。
这时应该将基类的析构函数声明为虚函数,动态地决定调用基类版本的析构函数仍是派生类版本的。
25.若是构造函数或析构函数调用了某个虚函数,则咱们应该执行与构造函数或析构函数所属类型相对应的虚函数版本。 p556
26.多重继承是指从多个直接基类中产生派生类。多重继承的派生类继承了全部父类的属性。
多重继承的二义性问题
当一个派生类同时继承了两个基类,而两个基类中又包含了同名的成员,则派生类调用该成员时会出现二义性问题。
咱们知道,在C++中名字查找先于类型检查。因此即便派生类从两个基类继承的两个同名函数的形参列表不一样,也会致使二义性错误。此外,
即便这个同名函数在一个基类中是私有的,在另外一个基类中是公有或保护的,一样会产生错误。
“倒三角” 和 “恐怖菱形”
倒三角:当一个派生类同时继承了两个基类,而两个基类中又包含了同名的成员。
能够经过做用域运算符::来规避这种二义性错误。
要避免二义性错误,最好的办法是:在派生类中为该函数定义一个新版本。
FA FB
\ /
C
恐怖菱形:以下图所示,iostream类(间接)继承了base_io两次。
base_io
/ \
istream ostream
\ /
iostream
C++中咱们经过虚继承的机制来解决这个问题。虚继承的目的是令某个类作出声明,承诺愿意共享它的基类。
在这种机制下,无论虚基类在继承体系中出现多少次,在派生类中都只包含惟一一个共享的虚基类子对象。
假定类B定义了一个名为x的成员,D1和D2都是从B虚继承获得的,而D又继承了D1和D2. 若是咱们经过D对象来使用x,有三种可能:
1)若是x在D1和D2中都没有定义,由于虚继承的缘故,不存在二义性问题。
2)若是x是B的成员,同时是D1和D2中某一个的成员,则一样不存在二义性。派生类的x比虚基类B的x优先级更高。
3)若是D1和D2中都有x的定义,则直接访问x将产生二义性问题。 最好的解决办法是:在派生类中为成员定义新的实例。
27.调用虚函数的指针也能够是this指针,当经过子类对象调用基类中的成员函数时,该函数里面的this指针将是一个指向子类对象的基类指针,这时再经过this去调用虚函数也能够表现多态的语法特性.
eg:QT多线程实现 class QThread{//QT官方写好的类 public: void start(void){ this->run(); } protected: virtual void run(void){线程入口函数} }; class myThread:public QThread{//本身写的类 protected: //重写线程入口函数 void run(void){须要放在线程中执行的代码..} }; myThread thread; //子类重写的线程入口函数将被执行 thread.start();