C++ Primer笔记(十三)面向对象编程

本文深入探讨面向对象编程的核心概念,包括继承、动态绑定与抽象等,通过C++实例详细解释如何利用类进行数据抽象、访问基类成员以及实现多态性。重点阐述了虚函数在C++中的应用,动态绑定的机制以及如何通过基类的引用或指针调用虚函数。此外,文章还讨论了派生类如何访问基类成员、虚函数的声明与重定义、以及如何避免常见陷阱,旨在为读者提供全面的面向对象编程实践指南。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1、继承和动态绑定与抽象一起成为面向对象编程的基础。

2、C++中用类进行数据抽象,用派生从一个类继承另一个类,动态绑定是编译器能够在运行时决定,是使用基类中定义的函数还是派生类中定义的函数。通过基类的引用(或指针)调用虚函数,引用(或指针)既可以指向基类对象也可以指向派生类对象。被调用的函数是引用(或指针)所指对象的实际类型所定义的。

3、面向对象编程的关键思想是多态性。在C++中,多态性仅用于通过继承而相关联的类型的引用或指针。


4、除构造函数外,任意非static成员函数都可以是virtual函数。继承层次的基类一般都要定义virtual的析构函数。

virtual 只能在类内部的声明中出现,不能出现在类外部的定义上。

5、派生类可以访问基类的public和protected成员,不能访问private成员。用户代码只能访问public成员。但类的友元都可以访问。protected成员可以被派生类对象访问但不能被该类型的普通用户访问。

6、派生类型必须对想要重新定义的每个继承成员进行声明,且必须与基类的定义方式完全匹配。

但有一个例外:返回对基类型的引用(指针)的虚函数。派生类中虚函数可以返回基类函数返回的类型的派生类的引用或指针。


7、一旦函数在基类被定义为虚函数,它就一直为虚函数,派生类无法改变这一事实,在派生类中重定义虚函数时,可以使用virtual关键字,也可以不使用。

注意:用作基类的类必须是已定义的。

每个派生类对象都包含基类部分的对象。类可以访问其基类的public和protected成员,就好像那些成员是派生类自己的成员一样。


8、C++中的函数调用默认不使用动态绑定。要触发动态绑定,必须满足两个条件:

1)只有指定为虚函数的成员函数才能进行动态绑定,成员函数默认为非虚函数,非虚函数不进行动态绑定。

2)必须通过基类类型的引用或指针进行函数调用。

因为每个派生类对象都包含基类部分,所以可将基类类型的引用绑定到派生类对象的基类部分,也可以用指向基类的指针指向派生类对象:

double print_total(const Item_base&,  size_t);

Item_base item;

print_total(item, 10);

Item_base *p = &item;

Bulk_item bulk;

print_total(bulk, 10);

p = &bulk;

这段代码使用同一基类类型指针指向基类类型的对象和派生类型的对象,该代码还传递基类类型和派生类型的对象来调用需要基类类型引用的函数,两种使用都是正确的,因为每个派生类对象都包含基类部分。


9、派生类虚函数调用基类版本时,必须显式使用作用域操作符。如果没有使用作用域说明符将会导致无穷递归。

10、虚函数也可以有默认实参,但它们是在编译时确定,与对象的动态类型无关。

在同一虚函数的基类和派生类的版本指定不同的默认实参,会导致麻烦。

如果通过基类的引用或指针调用虚函数,但实际执行的是派生类中定义的版本,这时传递给派生类虚函数的实参是在基类的虚函数指定的默认实参。

通过基类的引用或指针调用虚函数时,默认实参为在基类虚函数声明的值。通过派生类的引用或指针调用虚函数,默认实参为在派生类虚函数声明的值。


11、对类所继承的成员的访问,由基类中的成员访问级别和派生类派生列表中使用的访问标号共同控制。

派生类派生列表决定在派生类中的访问级别:

1)如果是public继承,基类成员保持自己的访问级别。基类的public成员为派生类的public成员,基类的protected员为派生类的protected成员。

2)如果是protected继承,基类的public和protected成员在派生类中为protected成员。

3)如果是private继承,基类的public和protected成员为派生类的private成员。

无论访问标号是什么,派生类都可以访问基类对象的public和protected成员。派生列表的访问标号主要是限制使用派生类对象的用户代码对派生类的使用。


12、派生类使用protected和private继承,限制了基类在派生类的访问级别。某些成员被限制过后,还可以在派生类中恢复,但是只能恢复到原来的级别。
如class base
{
  public:
   int a;
   protected:
    int b;

   private:

};

class derived:private base

{
  public:
   using base ::a;
   using base b;//错误,b原来为protected.只能恢复为protected。
  protected:

     using base ::b;

};

在对应的位置,使用using声明,可以将其恢复在基类的访问级别。注意使用using声明的位置。


13、如果不指定派生列表中的访问标号,使用class关键字定义的派生类默认具有private继承。使用struct关键字定义的派生类默认具有public继承。这是class和struct的第二个区别。

14、友元关系不能继承。

基类的友元对派生类的成员没有特殊访问权限。

反过来一样。每个类控制自己的友元关系。

15、如果基类定义了static成员,则整个继承层次中只有一个这样的成员。

static成员遵循常规访问控制。既可以通过基类访问static成员,也可以通过派生类访问成员。既可以使用作用域操作符也可以使用点或箭头成员访问操作符。


16、因为派生类对象也是基类的对象,所以存在从派生类型引用(指针)到基类类型的引用(指针)的自动转换。反过来不成立,基类对象不是派生类对象,因为派生类对象拥有基类对象不用有的某些特征。虽然一般可以使用派生类型的对象对基类对象进行初始化或赋值,但编译器不会自动将派生类对象转换为基类对象。

17、由于基类的复制构造函数和赋值操作符函数的形参均为基类型的引用,因此可以通过传递派生类的对象对基类进行初始化和赋值。要特别明白派生类对象转换为基类对象的理论基础。

18、从基类对象到派生类对象的转换是不存在的。

原因是基类对象的引用不能转换为派生类对象的引用。也就是说,不能将派生类类型的引用绑定到基类对象上。但是可以显式强转。


19、构造函数和复制控制函数不能继承,每个类定义自己的构造函数和复制控制函数,如果不定义将使用合成版本。

构造函数可以为protected或private,某些类如果需要只希望派生类使用的特殊构造函数,应将其声明为protected。

20、派生类的构造函数除了初始化自己的成员外,还要初始化基类。

派生类的初始化列表只能初始化派生类的成员,不能直接初始化继承的成员。应该使用基类构造函数初始化基类部分的成员。派生类的构造函数首先初始化基类(初始化列表调用基类构造函数),然后按照声明次序初始化派生类的成员。

注意: 一个类只能初始化自己的直接基类。

21、派生类构造函数不能初始化(初始化列表)基类的成员,也不应该对基类成员赋值(构造函数内),虽然可以在构造函数内对基类的成员赋值,但这样做会违反基类的接口。派生类应通过使用基类构造函数(初始化列表),尊重基类初始化意图。


22、如果派生类定义了自己的复制构造函数和赋值操作符,那么派生类就负责对基类部分以及类自己的成员进行复制或赋值。
例如派生类定义自己的复制构造函数,那么应该显式使用基类复制构造函数初始化对象的基类部分。如:
class Base 
{
   };
class Derived:public Base
{
  public:
   Derived(const Derived &d)
    :Base(d)//注意此处。
    {
    }
};

如果缺少调用基类的复制构造函数的步骤,那么效果就是Base的默认构造函数被用来初始化对象的基类部分。与调用复制构造函数复制的目标大相径庭。

新构造的对象将非常奇怪:它的Base部分保存默认值,而Derived部分是另一对象的副本。


23、赋值操作符与复制构造函数类似:如果派生类定义了自己的复制构造函数,则该操作符必须对基类部分进行显式赋值。
如:Derived &Derived::operator=(const Derived &rhs)
{
  if(this!=&rhs)//防止自我赋值。
      Base::operator=(rhs);//显式调用基类赋值操作符函数。
/

    }


24、析构函数的工作与复制构造函数和赋值操作符不同:

派生类析构函数不负责撤销基类对象的成员,编译器总是显式调用派生类对象的基类部分的析构函数。每个析构函数只负责清除自己的成员。

对象的撤销顺序与构造顺序相反:首先运行派生类析构函数,然后按继承层次依次向上调用各基类析构函数。

25、如果析构函数为虚函数,那么通过指针调用时,运行哪个析构函数将因指针所指向的对象类型不同而不同。尤其是当指针为基类类型,而指向派生类类型时,调用delete函数删除指针时,将调用派生类的虚析构函数。

注意:与其他虚函数一样,析构函数的虚函数性也会被继承。


26、即使析构函数没有工作要做,继承层次的根类也应该定义一个虚析构函数。

27、在运行构造函数和析构函数时,对象都不是完整的。

28、如果在它们中调用虚函数,那么运行的是构造函数或析构函数自身类型定义的版本,而不是派生类的版本。如在基类的构造函数调用派生类版本,派生类版本很可能会访问派生类的成员,而派生类的成员此时还并未初始化。如果允许这样的访问,程序很可能会崩溃。

29、与基类成员同名(同名即可)的派生类成员,将屏蔽对基类成员的直接访问。无论是成员函数还是成员变量。如在派生类访问被派生类成员屏蔽的基类成员,可以使用基类的作用域说明符访问。如Base ::a;

注意: 要尽量避免在派生类中定义与基类同名的成员。


30、在派生类作用域定义的同名派生类成员将屏蔽基类成员,即使函数原型不同。

31、当基类定义了一个重载函数集,派生类如果想通过自身类型使用所有的重载版本,那么它要么重定义所有的重载版本,要么一个也不定义。

原因:如果在派生类定义一个重载函数,由于它与基类的重载函数集同名,因此将屏蔽基类函数集中的所有函数,无法达到重载其中一部分函数的目的。解决方法是使用using声明,将该函数的所有重载实例加到派生类的作用域。

注意:一个using声明只能指定一个名字,不能指定形参表。

using Base::func;这一句话会将函数的所有重载实例加到派生类作用域,此后派生类就可以只定义确实需要重定义的重载函数,其他版本可以使用继承过来的函数的定义。


32、派生类重定义基类的虚函数时,要与基类声明的原型完全一致(返回值可以是基类返回值的派生类的指针或引用),否则派生类将屏蔽基类的虚函数。

此时通过基类的引用或指针绑定到派生类对象,调用函数时,将调用基类的虚函数,而不是派生类屏蔽后的函数。因为虚函数是在运行时确定的,而派生类没有定义与基类类型原型一致的虚函数,所以将调用基类的版本。

33、当希望使用容器保存因继承而相关联的对象。不能将容器类型定义为基类型,当加入派生类对象时,派生类对象会被切掉,只保存基类的部分。也不能将容器定义为派生类对象。

唯一可行的方法就是将容器定义为基类对象的指针。但是用户必须保证,只要容器存在,指针所指的对象就存在。用户必须保证在容器消失时释放对象。引用更不可能。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值