南京大学cpp复习——第二部分(继承)

继承

为什么需要继承?

  • 代码复用
  • 对事物进行分类
  • 增量开发

单继承

cpp 复制代码
```cpp
#include <iostream>
#include <cstring>

//错误声明 
// class Undergraduated_Student : public Student;
//正确声明
class Undergraduated_Student ;


using namespace std;
class Student
{       int id;
public:
    char nickname[16];
    void set_ID(int x)  { id = x; }
    void SetNickName(char *s) { strcpy(nickname,s);}
    void showInfo(){ cout << nickname << ":" << id <<endl; }
private:
    void show_private(){cout << "hi" << endl;}
};

class Undergraduate_Student : public Student
{       int dept_no;
public:
    void setDeptNo(int x) { dept_no = x; }
    void showInfo()
    { cout << dept_no << ": "<< nickname <<endl; }
    // using Student::show_private;会报错
private:
    using Student::nickname;
    using Student::set_ID;
    void SetNickName (){strcpy(nickname," ");};
};
int main(){
    Undergraduate_Student undergraduateStudent;
    // undergraduateStudent.setNickName();会报错
    // undergraduateStudent.nickname;会报错
    Student student;
    student.nickname;
    student.SetNickName("123");
    student.showInfo();
}

继承方式

public、protected、private

都是将访问权限降级。

using可以改变控制访问权限

private继承时,可以用using将某些数据成员或者函数成员权限放开

也可以将权限缩小

可否将基类private成员进行权限放宽?

派生类的内存空间

基类部分加上派生类新增部分,using不改变空间布局,只改变访问权限(这是编译器部分的检查,因此不会改变内存权限)

bash 复制代码
Undergraduate_Student 对象的内存布局:
┌─────────────────────────────────────────────┐
│             基类Student部分                   │
├─────────────────────────────────────────────┤
│ int id (4字节)      [Student私有]            │
├─────────────────────────────────────────────┤
│ char nickname[16] (16字节) [Student公有]     │
├─────────────────────────────────────────────┤
│             派生类新增部分                   │
├─────────────────────────────────────────────┤
│ int dept_no (4字节) [Undergraduate_Student私有]│
└─────────────────────────────────────────────┘
总大小:4 + 16 + 4 = 24字节

前向声明

名掩盖问题

cpp 复制代码
using namespace std;
class Base {
public:
    void func(int x) { cout << "Base::func(int)" << endl; }
    void func(double x) { cout << "Base::func(double)" << endl; }
    void func(int x, int y) { cout << "Base::func(int,int)" << endl; }
};

class Derived : public Base {
public:
    void func() {  // 只定义了一个无参版本
        cout << "Derived::func()" << endl;
    }
    // using Base::func; 解决名掩盖问题
};

int main() {
    Derived d;
    d.func();        // 正确:Derived::func()
    //d.func(1);     // 错误!Base的所有func都被隐藏了
    //d.func(1.0);   // 错误!
    //d.func(1,2);   // 错误!
}

一旦派生类中有一个同名函数,基类中任何其他同名函数都不可以再用。

如何解决:

  • 使用using将基类方法引入派生类的名空间

using和名掩盖问题的实质:

cpp 复制代码
// 当编译器看到 d.func(1) 时:
名称查找流程:
1. 进入 Derived 类作用域
2. 查找名称 "func"
3. ✅ 找到 Derived::func()  如果没找到,会到基类中继续寻找。
4. 🛑 立即停止查找!不再去Base类查找
5. 检查参数匹配:Derived::func() 需要0个参数,但提供了1个
6. ❌ 编译错误:参数不匹配

using Base::func;的作用:

  • 将Base类中所有名为func的声明注入到Derived类作用域
    using的作用实际上就是注入声明(不是注入定义)

派生类对象的初始化

由基类和派生类共同完成。

为什么需要共同完成?

  • 基类部分:包含基类的数据成员,需要基类构造函数初始化
  • 派生类部分:包含新增的数据成员,需要派生类构造函数初始化

创建Derived对象:

  • 基类构造函数 ← 第1步:基类
  • 派生类成员对象构造函数(声明顺序) ← 第2步:对象成员
  • 派生类构造函数 ← 第3步:派生类自身

Derived对象即将销毁:

  • 派生类析构函数 ← 第1步:派生类自身
  • 派生类成员对象析构函数(声明顺序) ← 第2步:对象成员
  • 基类析构函数 ← 第3步:基类

基类构造函数的调用

  • 如果缺省则执行基类默认构造函数
  • 若要执行基类的非默认构造函数,必须要派生类构造函数的成员初始化表中指出
cpp 复制代码
#include <iostream>
using namespace std;

// 基类A
class A {
    int x;
public:
    // 默认构造函数
    A() { 
        x = 0; 
    }
    
    // 带参构造函数
    A(int i) { 
        x = i; 
    }
};

// 派生类B,公有继承A
class B : public A {
    int y;
public:
    // 情况1:缺省调用基类默认构造函数
    B() { 
        y = 0; 
    }
    
    // 情况1:缺省调用基类默认构造函数
    B(int i) { 
        y = i; 
    }
    
    // 情况2:显式调用基类非默认构造函数
    B(int i, int j) : A(i) {  // 在成员初始化表中指出
        y = j;
    }
};

int main() {
    B b1;        // 执行A::A()和B::B() 
    B b2(1);     // 执行A::A()和B::B(int)
    B b3(0,1);   // 执行A::A(int)和B::B(int,int)
    return 0;
}

除此之外,还可以使用using继承构造函数

cpp 复制代码
class A {
    int x;
public:
    A() { x = 0; cout << "A默认构造" << endl; }
    A(int i) { x = i; cout << "A带参构造: " << x << endl; }
    A(int i, int j) { x = i + j; cout << "A双参构造: " << x << endl; }
};

class B : public A {
    int y;
public:
    using A::A;  // 继承A的所有构造函数
    
    // B会自动拥有以下构造函数:
    // B() : A() {}
    // B(int i) : A(i) {}  
    // B(int i, int j) : A(i, j) {}
};

虚函数------实现多态的关键

类型相容

问题:a、b是什么类型时,a = b 合法?

  • b是a的派生类对象

直接值传递会造成对象切片问题。

cpp 复制代码
class A {           // 基类
    int x, y;       // 私有数据成员
public:
    void f();       // 公有成员函数
};

class B : public A { // 派生类,公有继承A
    int z;          // 派生类新增数据成员
public:
	void f();
    void g();       // 派生类新增成员函数
};

A  a;
B  b;

a = b;     //OK, 
b = a;     //Error
a.f();      //A::f()
a.g(); 		//Error

//引用和指针也是一样
A &r_a=b;     //OK
A *p_a=&b; 	 //OK

B &r_b=a; 	 //Error
B *p_b=&a;	 //Error

动态绑定和静态绑定

调用的是A的f还是B的f。

cpp 复制代码
void func1(A& a) {
    a.f();  // 这里调用的是A::f还是B::f?
}

void func2(A* pa) {
    pa->f();  // 这里调用的是A::f还是B::f?
}

int main() {
    B b;
    func1(b);    // 实际调用:A::f
    func2(&b);   // 实际调用:A::f
    return 0;
}

为什么,普通的成员函数是静态绑定,在编译时就进行绑定。

以func1(b)为例,调用func1时,首先会进行一次传参:A& a = b,此处就将其绑定为了A类。

  • 前期绑定(默认)
    • 编译时刻、静态类型、效率高、灵活性低
  • 动态绑定
    • 运行时刻、实际类型、效率低、灵活性高

动态绑定需要加上virtual关键字,则根据实际引用和指向的对象类型。

加在哪?

cpp 复制代码
class A
{     ...
public:
	virtual void f();
};

class B
{     ...
public:
	void f();
};

如基类中被定义为虚成员函数,则派生类中对其重定义的成员函数均为虚函数。

限制:

  • 类的成员函数才可以是虚函数

  • 静态成员函数不能是虚函数(静态成员函数属于类,不能多态)

  • 内联成员函数不能是虚函数(这句话有点问题)
    这段代码能跑通,但是内联是编译期的建议,虚函数往往不会被内联

    cpp 复制代码
    #include <iostream>
    
    using namespace std;
    class Base {
    public:
        virtual inline void func() {
            cout << "Base::func()" << endl;
        }
    };
    
    int main() {
        Base base;
        base.func();
    }
  • 构造函数不能是虚函数。(虚函数根据实际对象来执行,但是构造函数执行时对象都没被创建)

  • 析构函数可以(往往)是虚函数(每一种派生类需要释放的资源可能不一样)。

虚函数的实现

cpp 复制代码
class A {
public:
    int x, y;
    virtual void f();     // 虚函数
    virtual void g();     // 虚函数
    void h();             // 非虚函数
};

class B : public A {
public:
    int z;
    void f();             // 重写 f()
    void h();             // 重写 h()
};

A a;      // 基类对象
B b;      // 派生类对象
A* p;     // 基类指针
// p = &a;
// p = &b;

每个含虚函数的类,都会有一张虚函数表

vtable 内容
A { A::f, A::g }
B { B::f, A::g }

含虚函数的对象,其内存空间会多一个指向对应类虚函数表的指针。

bash 复制代码
+-------------------+
|   vptr → A_vtable | ← 指向 A 的虚函数表
+-------------------+
|       x           |
+-------------------+
|       y           |
+-------------------+
bash 复制代码
+-------------------+
|   vptr → B_vtable | ← 指向 B 的虚函数表
+-------------------+
|       x           | ← 继承自 A
+-------------------+
|       y           | ← 继承自 A
+-------------------+
|       z           | ← B 自己的成员
+-------------------+

具体调用过程如下:

cpp 复制代码
A* p = &a;
p->f();
  • p 指向 a
  • 读取 a 的 vptr → 得到 A_vtable 地址
  • 在 A_vtable 中查找第一个函数(f)→ 获取 &A::f
  • 调用 A::f()

p->f()等价于(**((char *)p-4))(p) 这是一种过时并且错误的写法,简单了解一下

  1. (char *)p
    将 p(比如 A*)强制转换为字节指针 char*。
    目的:按字节进行地址运算。
  2. ((char *)p - 4)
    向低地址方向偏移 4 字节。获得虚函数表的地址。
  3. *((char *)p - 4) → 等价于 *(...)
    解引用,得到 vptr 的值(即 vtable 的地址)。
  4. **((char )p - 4)
    再次解引用:从 vtable 中取出 第一个函数指针(即第一个虚函数的地址)。
    此时得到的是一个 函数指针,比如 void (*)(f
    )。
  5. (**((char *)p - 4))(p)
    把 p 作为参数(即 this 指针)传给该函数并调用。

虚函数的调用规则

  • 不要在构造函数中使用虚函数。
    • 虚函数要在对象创建后才能正常使用
cpp 复制代码
class A
{    public:
	        A() { f();}
		virtual  void f();
	    void g();
		void h() { f(); g(); }
};

class B: public A
{   public:
	    void f(){g()}; //void f(B* const this){this->g()}; 
	    void g(); 
};	

B b;
A* p = &b;
p->f();  // → 调用 B::f()
p->g();  // → 调用 A::g()
p->h();  // → 调用 A::h(),B::f(),B::g(),A::g()
  • 虚函数才看实际类型,否则就看this类型。

override和final

  • override
    • 显式声明一个函数是重写一个虚函数
    • 编译器会进行检查,如果没有正确重写会报错
  • final
    • 阻止函数被重写,阻止类被继承
cpp 复制代码
struct B {
    virtual void f1(int) const ;
    virtual void f2 ();
    void f3 () ;
    virtual void f5 (int) final;

};
struct D : B {
    void f1(int) const override;      // ✅ 正确:匹配 f1 的 const 和参数
    void f2(int) override;               // ❌ 错误:B 中没有形如 f2(int) 的函数
    void f3() override;               // ❌ 错误:f3 不是虚函数,不能 override
    void f4() override;               // ❌ 错误:B 中没有名为 f4 的函数
    void f5(int);                     // ❌ 错误:f5 已被声明为 final,不能重写
};

重写虚函数也可以改变访问权限:

cpp 复制代码
struct B {
protected:
    virtual void f() {}
};

struct D : B {
public:
    void f() override {} // 放宽为 public
};

int main() {
    D d;
    d.f();               // ✅ 正确:D::f 是 public,可以直接调用
    B* pb = &d;
    pb->f();             // ❌ 错误:编译期按 B::f 检查,B::f 是 protected,类外不可访问
}

友元没有传递性

基类的友元不是派生类的友元,派生类的友元不是基类的友元

cpp 复制代码
class Base {
protected:
    int prot_mem;  // protected 成员
};

class Sneaky : public Base {
public:
    friend void clobber(Sneaky&);      // 可以访问 Sneaky 的 private 和 protected 成员
    friend void clobber(Base&);        // 只能访问 Base 的成员(但不能访问 protected)
    int j;                           // 默认 private
};

void clobber(Sneaky &s) { 
    s.j = 0;           // ✅ 正确:clobber 是 Sneaky 的友元 → 可访问 Sneaky 的成员
    s.prot_mem = 0;    // ✅ 正确:prot_mem 继承自 Base,是 protected → Sneaky 可访问
}

void clobber(Base &b) { 
    b.prot_mem = 0;    // ❌ 错误:clobber 不是 Base 的友元 → 无法访问 protected 成员
}

protected是具有传递性的。

但是protected的传递性是指派生类继承了基类的这个变量,可以访问派生类自身的这个变量,如果作为参数传入一个基类,是不能访问的。

cpp 复制代码
class Base {
protected:
    int x;
};

class Derived : public Base {
    // 可以访问 x
};

class GrandDerived : public Derived {
public:
    void foo() {
        x = 10;  // ✅ 合法!GrandDerived 可以访问 Base 的 protected 成员 x
    }
    void bad(Base& b) {
        b.x = 10;  // ❌ 编译错误!
    }
    void good(GrandDerived& gd) {
        gd.x = 20;  // ✅ 合法:gd 是同类对象(或派生类对象)
    }

    void alsoGood(Derived& d) {
        d.x = 30;   //  ❌ 编译错误!
    }
};

纯虚函数和抽象类

  • 纯虚函数
    • 声明时在函数原型后面加上 = 0
    • 必须在派生类中重写,否则派生类也是抽象类
  • 抽象类
    • 至少包含一个纯虚函数
    • 不能用于创建对象
cpp 复制代码
class AbstractClass {
public:
    virtual int f() = 0;  // 纯虚函数
};
AbstractClass a; // ❌ 错误:抽象类

虚析构函数

cpp 复制代码
class B {
public:
    virtual ~B() = default;
};

class D : public B {
public:
    mystring* name = new mystring();
    ~D() { 
        delete name; 
        cout << "D::~D()\n"; 
    }
};

int main() {
    B* p = new D;
    delete p;  // ✅ 正确:会调用 D::~D() 然后 B::~B()
}

public 继承表示"is-a"关系

正方形和矩形,鸟和企鹅。看似是is-a关系,但是正方形没有长和宽的关系,企鹅不会飞。如果只是想要代码复用,用组合!

is-a关系要能完全替代,比如工厂模式,A工厂和B工厂都能生产组件C,生产工艺可能不一样,但是两者完全可以互相替代。

"子类型必须能够替换它们的基类型,而不破坏程序的正确性。"

------ 里氏替换原则(Liskov Substitution Principle, LSP)

用组合实现代码复用

cpp 复制代码
class Square {
private:
    Rectangle r;
public:
    void setLength(int l) {
        r.setHeight(l);
        r.setWidth(l);
    }
    // 其他接口
};

不要定义与继承而来的非虚成员函数同名的成员函数

名掩盖问题。

private继承------实现代码复用的一种方式

在派生类内,将派生类当作基类的派生类,在派生类外,派生类和基类没有关系。

cpp 复制代码
class Base {
public:
    void f() {}
};

class Derived : private Base {
public:
    void test() {
        f();               // ✅ OK:在成员函数内可访问基类成员
        Base* p = this;    // ✅ OK:在成员函数内可转换为基类指针
    }
};

void func(Base& b) {}

int main() {
    Derived d;

    d.f();                 // ❌ 错误!外部不能调用 Base 的 public 成员
    func(d);               // ❌ 错误!不能将 Derived 转换为 Base&
    Base* p = &d;          // ❌ 错误!不能取地址转为 Base*

    d.test();              // ✅ OK:内部可以
}

PPT上的例子

cpp 复制代码
class CHumanBeing { ... };

class CStudent: private CHumanBeing { ... };

void eat(const CHumanBeing& h)
{ ... }//访问CHumanBeing的成员

CHumanBeing a;  CStudent b;
eat(a);
eat(b);	//出错,类型都不相容

不要重定义缺省参数值

cpp 复制代码
#include "iostream"

using namespace std;
class A {
public:
    virtual void f(int x = 0) { cout << "A::f(" << x << ")\n"; }
};

class B : public A {
public:
    virtual void f(int x = 1) { cout << "B::f(" << x << ")\n"; }
};

int main() {
    A* p = new B;
    p->f();
    B b;
    b.f();
}

为什么?

cpp 复制代码
obj.f(); // 没有传参

编译器会将其转化为:

cpp 复制代码
obj.f(0); // 假设缺省参数是 0

这个行为发生在编译时期,为静态绑定!


多继承

cpp 复制代码
class 派生类名 : [继承方式1] 基类名1, [继承方式2] 基类名2, ... {
    // 成员表
};
  • 继承方式
    • public、private 、protected
  • 继承方式及访问控制的规定同单继承
  • 派生类拥有所有基类的所有成员

多继承要点

  • 一个类可同时继承多个基类:class D : public B1, public B2 { ... };

  • 构造顺序 :按基类声明从左到右(递归调用);析构顺序相反

  • 内存分配,和单继承类似,按照声明顺序。

    • 先是基类的变量再是派生类的变量。
  • 若多个基类有同名成员 → 访问歧义 ,需用 B1::member 显式指定。

    cpp 复制代码
    #include <iostream>
    using namespace std;
    
    class A {
    public:
        int value = 10;
    };
    
    class B {
    public:
        int value = 20;
    };
    
    class C : public A, public B {
    public:
        void print() {
            // cout << value << endl;      // ❌ 错误:歧义!A::value 还是 B::value?
            cout << A::value << endl;      // ✅ 显式指定
            cout << B::value << endl;      // ✅ 显式指定
        }
    };
    
    int main() {
        C c;
        // cout << c.value << endl;       // ❌ 歧义
        cout << c.A::value << endl;       // ✅ OK
        cout << c.B::value << endl;       // ✅ OK
    }
  • 菱形继承问题:两个基类共有一个祖先 → 导致祖先被重复继承,重复构造。

    cpp 复制代码
    class A { public: int val = 0; };
    class B : public A {};
    class C : public A {};
    class D : public B, public C {};
    
    int main() {
        D d;
        d.B::val = 1;
        d.C::val = 2;
        cout << d.B::val << ", " << d.C::val; // 输出:1, 2
    }

    A会被构建几次?

    两次!不是三次!

    内存空间?

    cpp 复制代码
    +------------------+
    | B 的部分         |
    |   └─ A 的部分    |
    |        int val   |  
    +------------------+
    | C 的部分         |
    |   └─ A 的部分    |
    |        int val   |  
    +------------------+

    最顶上不会有A!

  • 解决方法 :使用 虚继承virtual public Base),确保公共基类只有一份。

    cpp 复制代码
    class A { public: int val = 0; };
    class B : virtual public A {};
    class C : virtual public A {};
    class D : public B, public C {};

    A只会被构建一次。

    内存空间?

    派生类前会多一个虚基类指针,指向共享虚基类。注意虚基类统一放在最后(即使有多个)

    cpp 复制代码
    +-----------------------------+
    | B 的部分                    |
    |   vbptr_B →──────────────┐  |  
    +--------------------------+  
    | C 的部分                 |
    |   vbptr_C →────────────┐ |  
    +------------------------+ 
    | [padding: 4 bytes]     |    
    +------------------------+
    | 共享的 A 虚基类对象      |
    |   int val              |    
    +------------------------+
  • 虚基类构造

    • 虚基类的构造函数由最新派生出的类的构造函数调用

      • 在虚继承体系中,无论中间有多少层派生类,只有最派生类(most-derived class)。中间类(如 B、C)即使写了对虚基类的初始化(如 A(10)),也会被忽略。

        cpp 复制代码
        #include <iostream>
        using namespace std;
        
        class A {
        public:
            A(int x) { cout << "A(" << x << ")\n"; }
        };
        
        class B : virtual public A {
        public:
            B() : A(10) { cout << "B()\n"; }  // ❌ 这个 A(10) 会被忽略!
        };
        
        class C : virtual public A {
        public:
            C() : A(20) { cout << "C()\n"; }  // ❌ 这个 A(20) 也会被忽略!
        };
        
        class D : public B, public C {
        public:
            D() : A(99), B(), C() {           // ✅ 只有这里的 A(99) 生效
                cout << "D()\n";
            }
        };
        
        int main() {
            D d;
        }

        输出:

        cpp 复制代码
        A(99)
        B()
        C()
        D()
    • 虚基类的构造函数优先非虚基类的构造函数执行

      • 无论声明前后,虚基类构造函数一定优先非虚基类的构造函数执行

        在下面的例子V会被优先构造:

        cpp 复制代码
        #include <iostream>
        using namespace std;
        
        class V { public: V() { cout << "V (virtual base)\n"; } };
        class N1 { public: N1() { cout << "N1 (non-virtual base)\n"; } };
        class N2 { public: N2() { cout << "N2 (non-virtual base)\n"; } };
        
        class Mid : public N1, virtual public V {
        public:
            Mid() { cout << "Mid\n"; }
        };
        
        class Final : public N2, public Mid {
        public:
            Final() { cout << "Final\n"; }
        };
        
        int main() {
            Final f;
        }

        输出

        cpp 复制代码
        V (virtual base)
        N2 (non-virtual base)
        N1 (non-virtual base)
        Mid
        Final
      • 虚函数内部则按照深度优先遍历,从左到右


相关推荐
一匹电信狗42 分钟前
【LeetCode】栈和队列进阶题目
c++·算法·leetcode·职场和发展·stl·栈和队列
WongKyunban42 分钟前
使用Valgrind检测内存问题(C语言)
c语言·开发语言
Zfox_42 分钟前
【Go】环境搭建与基本使用
开发语言·后端·golang
民乐团扒谱机43 分钟前
【微实验】携程评论C#爬取实战:突破JavaScript动态加载与反爬虫机制
大数据·开发语言·javascript·爬虫·c#
raoxiaoya44 分钟前
golang本地开发多版本切换,golang多版本管理,vscode切换多版本golang
开发语言·vscode·golang
代码游侠44 分钟前
数据结构——线性表
linux·c语言·数据结构·学习·算法
wjs20241 小时前
R Excel 文件:高效数据处理的利器
开发语言
人邮异步社区1 小时前
完全没接触过AI/NLP,如何系统学习大模型?
人工智能·学习·自然语言处理·大模型
蒋士峰DBA修行之路1 小时前
红帽练习环境介绍
linux·开发语言·bash