Skip to content

18.x 十二章理解和练习

我们的 C++ 旅程在继承和虚函数这块已经结束了。不要担心,亲爱的读者,因为还有大量其他 C++ 的领域等待我们前进,探索!

章节总结

C++ 允许你设置基类指针和引用到派生对象。这是很有用的,当我们想要写一个函数或者数组,可以和任何类型的从基类派生的对象搭配。(This is useful when we want to write a function or array that can work with any type of object derived from a base class.)

如果没有虚函数,基类指向派生类的指针和引用将会只能访问基类成员,和函数。

一个虚函数,是一个特殊类型的函数能解析到继承末端版本的函数(叫做重写)存在于基类和派生类之间。想要被认为是一个重写,派生类必须和基类虚函数有同样的签名和返回类型。一个例外就是covariant return types,能够允许一个重写返回一个指派生类的针或引用如果基类函数返回一个基类的指针或引用。

一个函数函数想要被重写,应该添加重写指示符来确保它确实重写了。

final 指示符能够被用作阻止从一个该类的函数被重写。

如果你想要使用一个虚函数,你应该使你的析构函数为一个虚函数,如果你删除一个基类指针,这样做可以使合适的析构函数被调用。

你可以忽略一个虚函数解析通过使用 scope resolution opearator 来直接指定那个类的函数你想要调用:例如 base.Base::getName()

Early binding 早绑定发生在编译时直接检测到函数调用,编译器或者连接器可以直接解析这些函数调用。 Late binding 发生在当一个函数指针被调用是。在这种情况下,哪个函数将会被调用不可能在运行之前确定。虚函数使用了 late binding 和一个虚表来决定哪个版本的函数将被调用。

使用虚函数有一定的花费,虚函数的调用将花费更多时间,并且虚表会占用一定的空间,在每个包含虚函数的对象中增加了一个指针。

一个虚函数可以被定义为纯虚函数(抽象函数)通过添加 =0 在虚函数的定义后方。包含纯虚函数的类被叫做抽象类,这种类不能被实例化。一个类继承于抽象类,那么纯虚函数必须被正确的定义,否则该类也将被认为是抽象类。纯虚函数也可以有一个函数体,但是他们仍然被认为是抽象的。

接口类是一个没有成员变量并且全是纯虚函数的类。这样的类的名称通常以大写的I 开头。

一个虚基类在一个对象中只能被包含一次,无论被一个类继承了多少次。

当一个派生类被赋值到基类对象时,基类只能接收到派生类中基类部分的拷贝。这被叫做对象切割。

动态转换可以被用来转换一个指向基类的指针成为一个指向派生类的指针,这被叫做向下转换 (downcasting)。一个失败的转换会返回一个空指针。

派生类最简单的重载 << 运算符的方式是写一个重载 << 为最顶部的基类,然后添加一个虚成员函数来打印。

// 译者添加的代码片段:
virtual std::ostream &print(std::ostream &in) const = 0;
friend std::ostream & operator<<(std::ostream &out, const Base &b) {
    return b.print(out);
}
// 继承对象都只需要重写print即可。

Quiz time

Quiz1

以下的每个程序都有一些缺陷,检查每个程序(用眼睛检查,不要编译)并且诊断出这个程序有什么问题。每个程序的输出都应该是 Derived

译者注:原网站是响应式的隐藏,折叠了答案,本网站无法做到,因此请先完成代码阅读再往下看答案描述,每个题的答案都在代码描述下方。

1a)

#include <iostream>

class Base
{
protected:
 int m_value;

public:
 Base(int value)
  : m_value{value}
 {
 }

 const char* getName() const { return "Base"; }
};

class Derived : public Base
{
public:
 Derived(int value)
  : Base{value}
 {
 }

 const char* getName() const { return "Derived"; }
};

int main()
{
 Derived d{5};
 Base &b{ d };
 std::cout << b.getName() << '\n';

 return 0;
}

答案1a Base::getName() 不是虚函数, 因此 b.getName() 不会解析并调用 Derived::getName(). 1b)

#include <iostream>

class Base
{
protected:
 int m_value;

public:
 Base(int value)
  : m_value{value}
 {
 }

 virtual const char* getName() { return "Base"; }
};

class Derived : public Base
{
public:
 Derived(int value)
  : Base{value}
 {
 }

 virtual const char* getName() const { return "Derived"; }
};

int main()
{
 Derived d{5};
 Base &b{ d };
 std::cout << b.getName() << '\n';

 return 0;
}

答案1b Base::geetName() 并没有定义为const,而 Derived::getName() 是一个const函数,因此 Derived::getName() 不是一个重写(override)

1c)

#include <iostream>

class Base
{
protected:
 int m_value;

public:
 Base(int value)
  : m_value{value}
 {
 }

 virtual const char* getName() { return "Base"; }
};

class Derived : public Base
{
public:
 Derived(int value)
  : Base{value}
 {
 }

 virtual const char* getName() override { return "Derived"; }
};

int main()
{
 Derived d{5};
 Base b{ d };
 std::cout << b.getName() << '\n';

 return 0;
}

答案1c d 是值赋值到 b,导致 d 被切割。

1d)

#include <iostream>

class Base final
{
protected:
 int m_value;

public:
 Base(int value)
  : m_value{ value }
 {
 }

 virtual const char* getName() { return "Base"; }
};

class Derived : public Base
{
public:
 Derived(int value)
  : Base{ value }
 {
 }

 virtual const char* getName() override { return "Derived"; }
};

int main()
{
 Derived d{5};
 Base &b{ d };
 std::cout << b.getName() << '\n';

 return 0;
}

答案1d Base 被定义为 final,因此 Derived 不能从它继承。这将会造成编译错误。

1e)

#include <iostream>

class Base
{
protected:
 int m_value;

public:
 Base(int value)
  : m_value{value}
 {
 }

 virtual const char* getName() { return "Base"; }
};

class Derived : public Base
{
public:
 Derived(int value)
  : Base{value}
 {
 }

 virtual const char* getName() = 0;
};

const char* Derived::getName()
{
    return "Derived";
}

int main()
{
 Derived d{5};
 Base &b{ d };
 std::cout << b.getName() << '\n';

 return 0;
}

答案1e Derived::getName() 是一个纯虚函数(无函数体),并且因此 Derived 是一个抽象类 (abstract class) 因此不能被实例化。

1f)

#include <iostream>

class Base
{
protected:
 int m_value;

public:
 Base(int value)
  : m_value{value}
 {
 }

 virtual const char* getName() { return "Base"; }
};

class Derived : public Base
{
public:
 Derived(int value)
  : Base(value)
 {
 }

 virtual const char* getName() { return "Derived"; }
};

int main()
{
 auto *d{ new Derived(5) };
 Base *b{ d };
 std::cout << b->getName() << '\n';
 delete b;

 return 0;
}

答案1f 这个程序实际上的确可以得到正确的输出,但是又不同的的问题。当我们 delete b 是,b 是一个 Base 指针,但是从来没添加一个虚析构函数 (virtual destructor) 到基类。因此,这个程序删除了 Derived 对象中的 Base 部分,并且 Derived 部分留在内存中(泄露)。

Quiz2

2a) 创建一个叫做形状的抽象类,这个类应该有三个函数:一个纯虚函数 print传入并且返回一个 std::ostream,重载 <<操作符 operator<< 和一个虚析构函数。

答案

class Shape
{
public:
// virtual const char* getShapeName() const = 0;
 virtual std::ostream& print(std::ostream &out) const = 0;

 friend std::ostream& operator<<(std::ostream &out, const Shape &p)
 {
  return p.print(out);
 }
 virtual ~Shape() {}
};

2b) 从 Shape 派生两个类:一个 Triangle 和一个 CircleTriangle 应该有三个 Point 作为成员。Circle 应该有一个 Point 圆心,和一个整数半径 radius,重载 print() 函数使得以下程序能够运行。

int main()
{
    Circle c{ Point{ 1, 2, 3 }, 7 };
    std::cout << c << '\n';

    Triangle t{Point{1, 2, 3}, Point{4, 5, 6}, Point{7, 8, 9}};
    std::cout << t << '\n';

    return 0;
}

样例应该打印:

Circle(Point(1, 2, 3), radius 7)
Triangle(Point(1, 2, 3), Point(4, 5, 6), Point(7, 8, 9))

这是 Point 类:

class Point
{
private:
 int m_x{ 0 };
 int m_y{ 0 };
 int m_z{ 0 };

public:
 Point(int x, int y, int z)
  : m_x{x}, m_y{y}, m_z{z}
 {

 }

 friend std::ostream& operator<<(std::ostream &out, const Point &p)
 {
  out << "Point(" << p.m_x << ", " << p.m_y << ", " << p.m_z << ')';
  return out;
 }
};

答案:

#include <iostream>

class Point
{
private:
 int m_x{ 0 };
 int m_y{ 0 };
 int m_z{ 0 };

public:
 Point(int x, int y, int z)
  : m_x{x}, m_y{y}, m_z{z}
 {

 }

 friend std::ostream& operator<<(std::ostream &out, const Point &p)
 {
  out << "Point(" << p.m_x << ", " << p.m_y << ", " << p.m_z << ')';
  return out;
 }
};

class Shape
{
public:
 virtual std::ostream& print(std::ostream &out) const = 0;

 friend std::ostream& operator<<(std::ostream &out, const Shape &p)
 {
  return p.print(out);
 }
 virtual ~Shape() = default;
};

class Triangle : public Shape
{
private:
 Point m_p1{};
 Point m_p2{};
 Point m_p3{};

public:
 Triangle(const Point &p1, const Point &p2, const Point &p3)
  : m_p1{p1}, m_p2{p2}, m_p3{p3}
 {
 }

 virtual std::ostream& print(std::ostream &out) const override
 {
  out << "Triangle(" << m_p1 << ", " << m_p2 << ", " << m_p3 << ')';
  return out;
 }
};

class Circle: public Shape
{
private:
 Point m_center;
 int m_radius;

public:
 Circle(const Point &center, int radius)
  : m_center{center}, m_radius{radius}
 {
 }

 virtual std::ostream& print(std::ostream &out) const override
 {
  out << "Circle(" << m_center << ", radius " << m_radius << ')';
  return out;
 }
};

int main()
{
    Circle c{Point{1, 2, 3}, 7};
    std::cout << c << '\n';

    Triangle t{Point{1, 2, 3}, Point{4, 5, 6}, Point{7, 8, 9}};
    std::cout << t << '\n';

    return 0;
}

2c) 在以上类(Point, Shape, Circle and Triangle)的基础上,完成以下程序:

#include <vector>
#include <iostream>

int main()
{
    std::vector<Shape*> v{
      new Circle{Point{1, 2, 3}, 7},
      new Triangle{Point{1, 2, 3}, Point{4, 5, 6}, Point{7, 8, 9}},
      new Circle{Point{4, 5, 6}, 3}
    };

    // 打印 vector 中的每个 `Shape` ,在这里。

    std::cout << "The largest radius is: " << getLargestRadius(v) << '\n'; // write this function

    // 删除每个vector中的对象,在这里。

 return 0;
}

提示:你将会需要添加一个 getRadius() 函数到 Circle 类,并且向下转化 Shape* 成为一个 Circle* 来访问它。

答案:

#include <vector>
#include <iostream>

class Point
{
private:
 int m_x{};
 int m_y{};
 int m_z{};

public:
 Point(int x, int y, int z)
  : m_x{x}, m_y{y}, m_z{z}
 {

 }

 friend std::ostream& operator<<(std::ostream &out, const Point &p)
 {
  out << "Point(" << p.m_x << ", " << p.m_y << ", " << p.m_z << ')';
  return out;
 }
};

class Shape
{
public:
 virtual std::ostream& print(std::ostream &out) const = 0;

 friend std::ostream& operator<<(std::ostream &out, const Shape &p)
 {
  return p.print(out);
 }
 virtual ~Shape() = default;
};

class Triangle : public Shape
{
private:
 Point m_p1{};
 Point m_p2{};
 Point m_p3{};

public:
 Triangle(const Point &p1, const Point &p2, const Point &p3)
  : m_p1{p1}, m_p2{p2}, m_p3{p3}
 {
 }

 virtual std::ostream& print(std::ostream &out) const override
 {
  out << "Triangle(" << m_p1 << ", " << m_p2 << ", " << m_p3 << ')';
  return out;
 }
};


class Circle: public Shape
{
private:
 Point m_center{};
 int m_radius{};

public:
 Circle(const Point &center, int radius)
  : m_center{center}, m_radius{radius}
 {
 }

 virtual std::ostream& print(std::ostream &out) const override
 {
  out << "Circle(" << m_center << ", radius " << m_radius << ')';
  return out;
 }

 int getRadius() const { return m_radius;  }
};

// h/t to reader Olivier for this updated solution
int getLargestRadius(const std::vector<Shape*> &v)
{
 int largestRadius { 0 };

 // Loop through all the shapes in the vector
 for (const auto* element : v)
 {
  // // Ensure the dynamic cast succeeds by checking for a null pointer result
      if (auto *c{ dynamic_cast<Circle*>(element) })
  {
   if (c->getRadius() > largestRadius)
    largestRadius = c->getRadius();
  }
 }

    return largestRadius;
}
int main()
{
 std::vector<Shape*> v{
      new Circle{Point{1, 2, 3}, 7},
      new Triangle{Point{1, 2, 3}, Point{4, 5, 6}, Point{7, 8, 9}},
      new Circle{Point{4, 5, 6}, 3}
    };

 for (const auto* element : v) // element will be a Shape*
  std::cout << *element << '\n';

 std::cout << "The largest radius is: " << getLargestRadius(v) << '\n';

 for (const auto* element : v)
  delete element;

 return 0;
}