C++ Primer(面向对象编程 一)
2015-01-07 10:39
225 查看
第15章 面向对象编程
15.1 概述
面向对象编程基于三个基本概念:数据抽象、继承和动态绑定。在 C++ 中,用类进行数据抽象,用类派生从一个类继承另一个:派生类继承基类的成员。动态绑定使编译器能够在运行时决定是使用基类中定义的函数还是派生类中定义的函数。
1.在 C++ 中,基类必须指出希望派生类重写哪些函数,定义为 virtual 的函数是基类期待派生类重新定义的,基类希望派生类继承的函数不能定义为虚函数。
2.在 C++ 中,通过基类的引用(或指针)调用虚函数时,发生动态绑定。引用(或指针)既可以指向基类对象也可以指向派生类对象,这一事实是动态绑定的关键。用引用(或指针)调用的虚函数在运行时确定,被调用的函数是引用(或指针)所指对象的实际类型所定义的。
15.2 定义基类和派生类
1.基类成员函数:保留字 virtual 的目的是启用动态绑定。成员默认为非虚函数,对非虚函数的调用在编译时确定。除了构造函数之外,任意非 static 成员函数都可以是虚函数。保留字只在类内部的成员函数声明中出现,不能用在类定义体外部出现的函数定义上。
2.基类通常应将派生类需要重定义的任意函数定义为虚函数。
3.访问控制和继承:protected 成员可以被派生类对象访问但不能被该类型的普通用户访问。
4.protected 成员:派生类只能通过派生类对象访问其基类的 protected 成员,派生类对其基类类型对象的 protected 成员没有特殊访问权限。
void Bulk_item::memfcn(const Bulk_item &d, const Item_base &b)
{
// attempt to use protected member
double ret = price; // ok: uses this->price
ret = d.price; // ok: uses price from a Bulk_item object
ret = b.price; // error: no access to price from an Item_base
}
5.派生类和虚函数:
a.派生类一般会重定义所继承的虚函数。派生类没有重定义某个虚函数,则使用基类中定义的版本。
b.派生类型必须对想要重定义的每个继承成员进行声明。
c.派生类中虚函数的声明必须与基类中的定义方式完全匹配,但有一个例外:返回对基类型的引用(或指针)的虚函数。派生类中的虚函数可以返回基类函数所返回类型的派生类的引用(或指针)。
d.一旦函数在基类中声明为虚函数,它就一直为虚函数,派生类无法改变该函数为虚函数这一事实。派生类重定义虚函数时,可以使用 virtual 保留字,但不是必须这样做。
6.派生类中的函数可以使用基类的成员
7.用作基类的类必须是已定义的
8.每个类继承其基类的所有成员。最底层的派生类继承其基类的成员,基类又继承自己的基类的成员,如此沿着继承链依次向上。
从效果来说,最底层的派生类对象包含其每个直接基类和间接基类的子对象。
9.派生类的声明:如果需要声明(但并不实现)一个派生类,则声明包含类名但不包含派生列表。
正确的前向声明为:
// forward declarations of both derived and nonderived class
class Bulk_item;
class Item_base;
10.要触发动态绑定,满足两个条件:第一,只有指定为虚函数的成员函数才能进行动态绑定,成员函数默认为非虚函数,非虚函数不进行动态绑定;第二,必须通过基类类型的引用或指针进行函数调用。
a可将基类类型的引用绑定到派生类对象的基类部分,也可以用指向基类的指针指向派生类对象.
b.通过引用或指针调用虚函数时,编译器将生成代码,在运行时确定调用哪个函数,被调用的是与动态类型相对应的函数。
11.非虚函数总是在编译时根据调用该函数的对象、引用或指针的类型而确定。
12.覆盖虚函数机制:在某些情况下,希望覆盖虚函数机制并强制函数调用使用虚函数的特定版本,这里可以使用作用域操作符:
Item_base *baseP = &derived;
// calls version from the base class regardless of the dynamic type of baseP
double d = baseP->Item_base::net_price(42);
只有成员函数中的代码才应该使用作用域操作符覆盖虚函数机制。
派生类虚函数调用基类版本时,必须显式使用作用域操作符。
13.像其他任何函数一样,虚函数也可以有默认实参。通过基类的引用或指针调用虚函数时,默认实参为在基类虚函数声明中指定的值,如果通过派生类的指针或引用调用虚函数,则默认实参是在派生类的版本中声明的值。
14.a.如果是受保护继承,基类的 public 和 protected 成员在派生类中为 protected成员。
b.如果是私有继承,基类的的所有成员在派生类中为 private成员。
15.派生类可以恢复继承成员的访问级别,但不能使访问级别比基类中原来指定的更严格或更宽松。
在这一继承层次中,size 在 Base 中为 public,但在 Derived 中为 private。为了使 size 在 Derived 中成为 public,可以在 Derived 的 public 部分增加一个 using 声明。如下这样改变 Derived 的定义,可以使 size 成员能够被用户访问,并使 n 能够被从 Derived派生的类访问:
class Derived : private Base {
public:
// maintain access levels for members related to the size of the object
using Base::size;
protected:
using Base::n;
// ...
};
16.默认继承保护级别:使用 class 保留字定义的派生默认具有 private 继承,而用 struct 保留字定义的类默认具有 public 继承.
struct 保留字定义的类与用 class 定义的类唯一的不同只是默认的成员保护级别和默认的派生保护级别,没有其他区别.
17.友元关系与继承:友元关系不能继承。基类的友元对派生类的成员没有特殊访问权限。如果基类被授予友元关系,则只有基类具有特殊访问权限,该基类的派生类不能访问授予友元关系的类。
如果派生类想要将自己成员的访问权授予其基类的友元,派生类必须显式地这样做:基类的友元对从该基类派生的类型没有特殊访问权限。同样,如果基类和派生类都需要访问另一个类,那个类必须特地将访问权限授予基类的和每一个派生类。
18.如果基类定义 static 成员,则整个继承层次中只有一个这样的成员。无论从基类派生出多少个派生类,每个 static 成员只有一个实例。
static 成员遵循常规访问控制:如果成员在基类中为 private,则派生类不能访问它。假定可以访问成员,则既可以通过基类访问 static 成员,也可以通过派生类访问 static 成员。一般而言,既可以使用作用域操作符也可以使用点或箭头成员访问操作符。
15.3转换与继承
可以将派生类对象的引用转换为基类子对象的引用
没有从基类引用(或基类指针)到派生类引用(或派生类指针)的(自动)转换。
没有从派生类型对象到基类类型对象的直接转换。
1.派生类到基类的转换:如果有一个派生类型的对象,则可以使用它的地址对基类类型的指针进行赋值或初始化。同样,可以使用派生类型的引用或对象初始化基类类型的引用。严格说来,对对象没有类似转换。编译器不会自动将派生类型对象转换为基类类型对象。
2。用派生类对象对基类对象进行初始化或赋值:基类一般(显式或隐式地)定义自己的复制构造函数和赋值操作符,这些成员接受一个形参,该形参是基类类型的(const)引用。因为存在从派生类引用到基类引用的转换,这些复制控制成员可用于从派生类对象对基类对象进行初始化或赋值:
Item_base item; // object of base type
Bulk_item bulk; // object of derived type
// ok: uses Item_base::Item_base(const Item_base&) constructor
Item_base item(bulk); // bulk is "sliced down" to its Item_base portion
// ok: calls Item_base::operator=(const Item_base&)
item = bulk; // bulk is "sliced down" to its Item_base portion
在这种情况下,我们说 bulk 的 Bulk_item 部分在对 item 进行初始化或赋值时被“切掉”了。Item_base 对象只包含基类中定义的成员,不包含由任意派生类型定义的成员,Item_base对象中没有派生类成员的存储空间。
3.派生类到基类转换的可访问性:像继承的成员函数一样,从派生类到基类的转换可能是也可能不是可访问的。转换是否访问取决于在派生类的派生列表中指定的访问标号。
如果类是使用 private 或 protected 继承派生的,则用户代码不能将派生类型对象转换为基类对象。如果是 protected 继承,则后续派生类的成员可以转换为基类类型。
4.基类到派生类的转换:没有从基类类型到派生类型的(自动)转换,原因在于基类对象只能是基类对象,它不能包含派生类型成员。如果允许用基类对象给派生类型对象赋值,那么就可以试图使用该派生类对象访问不存在的成员。
甚至当基类指针或引用实际绑定到绑定到派生类对象时,从基类到派生类的转换也存在限制:
Bulk_item bulk;
Item_base *itemP = &bulk; // ok: dynamic type is Bulk_item
Bulk_item *bulkP = itemP; // error: can't convert base to derived
编译器在编译时无法知道特定转换在运行时实际上是安全的。编译器确定转换是否合法,只看指针或引用的静态类型。
15.4 构造函数和复制控制
构造函数和复制控制成员不能继承,每个类定义自己的构造函数和复制控制成员。
1.基类构造函数和复制控制:本身不是派生类的基类,其构造函数和复制控制基本上不受继承影响。
2.派生类构造函数:派生类的构造函数受继承关系的影响,每个派生类构造函数除了初始化自己的数据成员之外,还要初始化基类。
a.合成的派生类默认构造函数:派生类的合成默认构造函数与非派生的构造函数只有一点不同:除了初始化派生类的数据成员之外,它还初始化派生类对象的基类部分。基类部分由基类的默认构造函数初始化。
b.构造函数初始化列表为类的基类和成员提供初始值,它并不指定初始化的执行次序。首先初始化基类,然后根据声明次序初始化派生类的成员。
3.派生类构造函数通过将基类包含在构造函数初始化列表中来间接初始化继承成员。
class Bulk_item:public Item_base{
public:
Bulk_item(const std::string& book, double sales_price, std::size_t qty=0, double disc_rate=0):
Item_base(book,sales_price),min_qty(qty),discount(disc_rate){}
//as before
};
4.只能初始化直接基类:一个类只能初始化自己的直接基类。直接就是在派生列表中指定的类。
5.尊重基类接口:派生类应通过使用基类构造函数尊重基类的初始化意图,而不是在派生类构造函数函数体中对这些成员赋值。
6.定义派生类复制构造函数:如果派生类定义了自己的复制构造函数,该复制构造函数一般应显式使用基类复制构造函数初始化对象的基类部分.
7.派生类赋值操作符:如果派生类定义了自己的赋值操作符,则该操作符必须对基类部分进行显式赋值。赋值操作符必须防止自身赋值。
Derived &Derived::operator=(const Derived *rhs)
{
if(this!=&rhs){
Base::operator=(rhs);
//do whatever needed to clean up the old value in the derived part
//assign the members from the derived
}
return *this;
}
8.派生类析构函数:派生类析构函数不负责撤销基类对象的成员。
对象的撤销顺序与构造顺序相反:首先运行派生析构函数,然后按继承层次依次向上调用各基类析构函数。
9.要保证运行适当的析构函数,基类中的析构函数必须为虚函数.
a.析构函数的虚函数性质都将继承。因此,如果层次中根类的析构函数为虚函数,则派生类析构函数也将是虚函数,无论派生类显式定义析构函数还是使用合成析构函数,派生类析构函数都是虚函数。
b.即使析构函数没有工作要做,继承层次的根类也应该定义一个虚析构函数。
c.基类析构函数是三法则的一个重要例外。三法则指出如果类需要析构函数,几乎也需要其他复制控制成员。如果基类为了将析构函数设为虚函数而具有空析构函数,那么并不表示需要赋值操作符或复制构造函数。
10.构造函数和赋值操作符不是虚函数
15.1 概述
面向对象编程基于三个基本概念:数据抽象、继承和动态绑定。在 C++ 中,用类进行数据抽象,用类派生从一个类继承另一个:派生类继承基类的成员。动态绑定使编译器能够在运行时决定是使用基类中定义的函数还是派生类中定义的函数。
1.在 C++ 中,基类必须指出希望派生类重写哪些函数,定义为 virtual 的函数是基类期待派生类重新定义的,基类希望派生类继承的函数不能定义为虚函数。
2.在 C++ 中,通过基类的引用(或指针)调用虚函数时,发生动态绑定。引用(或指针)既可以指向基类对象也可以指向派生类对象,这一事实是动态绑定的关键。用引用(或指针)调用的虚函数在运行时确定,被调用的函数是引用(或指针)所指对象的实际类型所定义的。
15.2 定义基类和派生类
1.基类成员函数:保留字 virtual 的目的是启用动态绑定。成员默认为非虚函数,对非虚函数的调用在编译时确定。除了构造函数之外,任意非 static 成员函数都可以是虚函数。保留字只在类内部的成员函数声明中出现,不能用在类定义体外部出现的函数定义上。
2.基类通常应将派生类需要重定义的任意函数定义为虚函数。
3.访问控制和继承:protected 成员可以被派生类对象访问但不能被该类型的普通用户访问。
4.protected 成员:派生类只能通过派生类对象访问其基类的 protected 成员,派生类对其基类类型对象的 protected 成员没有特殊访问权限。
void Bulk_item::memfcn(const Bulk_item &d, const Item_base &b)
{
// attempt to use protected member
double ret = price; // ok: uses this->price
ret = d.price; // ok: uses price from a Bulk_item object
ret = b.price; // error: no access to price from an Item_base
}
5.派生类和虚函数:
a.派生类一般会重定义所继承的虚函数。派生类没有重定义某个虚函数,则使用基类中定义的版本。
b.派生类型必须对想要重定义的每个继承成员进行声明。
c.派生类中虚函数的声明必须与基类中的定义方式完全匹配,但有一个例外:返回对基类型的引用(或指针)的虚函数。派生类中的虚函数可以返回基类函数所返回类型的派生类的引用(或指针)。
d.一旦函数在基类中声明为虚函数,它就一直为虚函数,派生类无法改变该函数为虚函数这一事实。派生类重定义虚函数时,可以使用 virtual 保留字,但不是必须这样做。
6.派生类中的函数可以使用基类的成员
7.用作基类的类必须是已定义的
8.每个类继承其基类的所有成员。最底层的派生类继承其基类的成员,基类又继承自己的基类的成员,如此沿着继承链依次向上。
从效果来说,最底层的派生类对象包含其每个直接基类和间接基类的子对象。
9.派生类的声明:如果需要声明(但并不实现)一个派生类,则声明包含类名但不包含派生列表。
正确的前向声明为:
// forward declarations of both derived and nonderived class
class Bulk_item;
class Item_base;
10.要触发动态绑定,满足两个条件:第一,只有指定为虚函数的成员函数才能进行动态绑定,成员函数默认为非虚函数,非虚函数不进行动态绑定;第二,必须通过基类类型的引用或指针进行函数调用。
a可将基类类型的引用绑定到派生类对象的基类部分,也可以用指向基类的指针指向派生类对象.
b.通过引用或指针调用虚函数时,编译器将生成代码,在运行时确定调用哪个函数,被调用的是与动态类型相对应的函数。
11.非虚函数总是在编译时根据调用该函数的对象、引用或指针的类型而确定。
12.覆盖虚函数机制:在某些情况下,希望覆盖虚函数机制并强制函数调用使用虚函数的特定版本,这里可以使用作用域操作符:
Item_base *baseP = &derived;
// calls version from the base class regardless of the dynamic type of baseP
double d = baseP->Item_base::net_price(42);
只有成员函数中的代码才应该使用作用域操作符覆盖虚函数机制。
派生类虚函数调用基类版本时,必须显式使用作用域操作符。
13.像其他任何函数一样,虚函数也可以有默认实参。通过基类的引用或指针调用虚函数时,默认实参为在基类虚函数声明中指定的值,如果通过派生类的指针或引用调用虚函数,则默认实参是在派生类的版本中声明的值。
14.a.如果是受保护继承,基类的 public 和 protected 成员在派生类中为 protected成员。
b.如果是私有继承,基类的的所有成员在派生类中为 private成员。
15.派生类可以恢复继承成员的访问级别,但不能使访问级别比基类中原来指定的更严格或更宽松。
在这一继承层次中,size 在 Base 中为 public,但在 Derived 中为 private。为了使 size 在 Derived 中成为 public,可以在 Derived 的 public 部分增加一个 using 声明。如下这样改变 Derived 的定义,可以使 size 成员能够被用户访问,并使 n 能够被从 Derived派生的类访问:
class Derived : private Base {
public:
// maintain access levels for members related to the size of the object
using Base::size;
protected:
using Base::n;
// ...
};
16.默认继承保护级别:使用 class 保留字定义的派生默认具有 private 继承,而用 struct 保留字定义的类默认具有 public 继承.
struct 保留字定义的类与用 class 定义的类唯一的不同只是默认的成员保护级别和默认的派生保护级别,没有其他区别.
17.友元关系与继承:友元关系不能继承。基类的友元对派生类的成员没有特殊访问权限。如果基类被授予友元关系,则只有基类具有特殊访问权限,该基类的派生类不能访问授予友元关系的类。
如果派生类想要将自己成员的访问权授予其基类的友元,派生类必须显式地这样做:基类的友元对从该基类派生的类型没有特殊访问权限。同样,如果基类和派生类都需要访问另一个类,那个类必须特地将访问权限授予基类的和每一个派生类。
18.如果基类定义 static 成员,则整个继承层次中只有一个这样的成员。无论从基类派生出多少个派生类,每个 static 成员只有一个实例。
static 成员遵循常规访问控制:如果成员在基类中为 private,则派生类不能访问它。假定可以访问成员,则既可以通过基类访问 static 成员,也可以通过派生类访问 static 成员。一般而言,既可以使用作用域操作符也可以使用点或箭头成员访问操作符。
15.3转换与继承
可以将派生类对象的引用转换为基类子对象的引用
没有从基类引用(或基类指针)到派生类引用(或派生类指针)的(自动)转换。
没有从派生类型对象到基类类型对象的直接转换。
1.派生类到基类的转换:如果有一个派生类型的对象,则可以使用它的地址对基类类型的指针进行赋值或初始化。同样,可以使用派生类型的引用或对象初始化基类类型的引用。严格说来,对对象没有类似转换。编译器不会自动将派生类型对象转换为基类类型对象。
2。用派生类对象对基类对象进行初始化或赋值:基类一般(显式或隐式地)定义自己的复制构造函数和赋值操作符,这些成员接受一个形参,该形参是基类类型的(const)引用。因为存在从派生类引用到基类引用的转换,这些复制控制成员可用于从派生类对象对基类对象进行初始化或赋值:
Item_base item; // object of base type
Bulk_item bulk; // object of derived type
// ok: uses Item_base::Item_base(const Item_base&) constructor
Item_base item(bulk); // bulk is "sliced down" to its Item_base portion
// ok: calls Item_base::operator=(const Item_base&)
item = bulk; // bulk is "sliced down" to its Item_base portion
在这种情况下,我们说 bulk 的 Bulk_item 部分在对 item 进行初始化或赋值时被“切掉”了。Item_base 对象只包含基类中定义的成员,不包含由任意派生类型定义的成员,Item_base对象中没有派生类成员的存储空间。
3.派生类到基类转换的可访问性:像继承的成员函数一样,从派生类到基类的转换可能是也可能不是可访问的。转换是否访问取决于在派生类的派生列表中指定的访问标号。
如果类是使用 private 或 protected 继承派生的,则用户代码不能将派生类型对象转换为基类对象。如果是 protected 继承,则后续派生类的成员可以转换为基类类型。
4.基类到派生类的转换:没有从基类类型到派生类型的(自动)转换,原因在于基类对象只能是基类对象,它不能包含派生类型成员。如果允许用基类对象给派生类型对象赋值,那么就可以试图使用该派生类对象访问不存在的成员。
甚至当基类指针或引用实际绑定到绑定到派生类对象时,从基类到派生类的转换也存在限制:
Bulk_item bulk;
Item_base *itemP = &bulk; // ok: dynamic type is Bulk_item
Bulk_item *bulkP = itemP; // error: can't convert base to derived
编译器在编译时无法知道特定转换在运行时实际上是安全的。编译器确定转换是否合法,只看指针或引用的静态类型。
15.4 构造函数和复制控制
构造函数和复制控制成员不能继承,每个类定义自己的构造函数和复制控制成员。
1.基类构造函数和复制控制:本身不是派生类的基类,其构造函数和复制控制基本上不受继承影响。
2.派生类构造函数:派生类的构造函数受继承关系的影响,每个派生类构造函数除了初始化自己的数据成员之外,还要初始化基类。
a.合成的派生类默认构造函数:派生类的合成默认构造函数与非派生的构造函数只有一点不同:除了初始化派生类的数据成员之外,它还初始化派生类对象的基类部分。基类部分由基类的默认构造函数初始化。
b.构造函数初始化列表为类的基类和成员提供初始值,它并不指定初始化的执行次序。首先初始化基类,然后根据声明次序初始化派生类的成员。
3.派生类构造函数通过将基类包含在构造函数初始化列表中来间接初始化继承成员。
class Bulk_item:public Item_base{
public:
Bulk_item(const std::string& book, double sales_price, std::size_t qty=0, double disc_rate=0):
Item_base(book,sales_price),min_qty(qty),discount(disc_rate){}
//as before
};
4.只能初始化直接基类:一个类只能初始化自己的直接基类。直接就是在派生列表中指定的类。
5.尊重基类接口:派生类应通过使用基类构造函数尊重基类的初始化意图,而不是在派生类构造函数函数体中对这些成员赋值。
6.定义派生类复制构造函数:如果派生类定义了自己的复制构造函数,该复制构造函数一般应显式使用基类复制构造函数初始化对象的基类部分.
7.派生类赋值操作符:如果派生类定义了自己的赋值操作符,则该操作符必须对基类部分进行显式赋值。赋值操作符必须防止自身赋值。
Derived &Derived::operator=(const Derived *rhs)
{
if(this!=&rhs){
Base::operator=(rhs);
//do whatever needed to clean up the old value in the derived part
//assign the members from the derived
}
return *this;
}
8.派生类析构函数:派生类析构函数不负责撤销基类对象的成员。
对象的撤销顺序与构造顺序相反:首先运行派生析构函数,然后按继承层次依次向上调用各基类析构函数。
9.要保证运行适当的析构函数,基类中的析构函数必须为虚函数.
a.析构函数的虚函数性质都将继承。因此,如果层次中根类的析构函数为虚函数,则派生类析构函数也将是虚函数,无论派生类显式定义析构函数还是使用合成析构函数,派生类析构函数都是虚函数。
b.即使析构函数没有工作要做,继承层次的根类也应该定义一个虚析构函数。
c.基类析构函数是三法则的一个重要例外。三法则指出如果类需要析构函数,几乎也需要其他复制控制成员。如果基类为了将析构函数设为虚函数而具有空析构函数,那么并不表示需要赋值操作符或复制构造函数。
10.构造函数和赋值操作符不是虚函数
相关文章推荐
- C++ Primer 学习笔记_68_面向对象编程 -构造函数跟复制控制[续]
- 《面向对象编程》--c++ primer 第15章
- 《C++ Primer》 第15章:面向对象编程
- C++ Primer 十五章 面向对象编程概述 要点笔记
- C++ Primer 学习笔记_72_面向对象编程 --句柄类与继承[续]
- C++ Primer(十五) 面向对象编程
- C++ Primer 第十五章 面向对象编程
- C++ Primer 学习笔记_69_面向对象编程 -继承景况下的类作用域
- 《面向对象编程》--c++ primer 第15章
- [C/C++] 第15章:面向对象编程 《 C++ Primer 》
- C++ Primer 面向对象编程
- C++ Primer(第4版)-学习笔记-第4部分:面向对象编程与泛型编程
- c++ primer学习之-----面向对象编程(继承与动态绑定)
- [C/C++] 第15章:面向对象编程 《 C++ Primer 》
- 重读C++ Primer,记录一些之前不易觉察的知识点(15. 面向对象编程)
- [C++ Primer] 面向对象编程
- C++ Primer(面向对象编程 二)
- C++ Primer 学习笔记_71_面向对象编程 --句柄类与继承
- C++ Primer 学习笔记_72_面向对象编程 -句柄类与继承[续]