所以我正在刷新C ++,说实话已经有一段时间了。我做了一个控制台乒乓球游戏,作为一种复习任务,并获得了一些关于使用多态性从类“ GameObject”(具有一些将对象绘制到屏幕的基本方法)中派生出来的信息。
输入的其中一项内容是(然后我又问过)输入基类时内存如何工作。因为我还没有真正做过很多高级C ++。
例如,假设我们有一个基类,现在它只有一个“ draw”方法(为什么我们需要为此说virtual
?),因为所有其他派生对象实际上只共享一个常用方法,并且正在绘制:
class GameObject
{
public:
virtual void Draw( ) = 0;
};
例如,我们还有一个球类:
class Ball : public GameObject
我收到的输入是,在适当的游戏中,这些内容可能会保留在GameObject指针的某种矢量中。像这样的东西:std::vector<GameObject*> _gameObjects;
(所以是指向GameObjects的指针的向量)(顺便说一句,为什么我们在这里使用指针?为什么不只是纯GameObjects?)。我们将使用以下示例实例化这些游戏对象之一:
_gameObjects.push_back( new Ball( -1, 1, boardWidth / 2, boardHeight / 2 ); );
({new
返回一个指向正确对象的指针?IIRC)。根据我的理解,如果我尝试执行以下操作:
Ball b;
GameObject g = b;
事情会变得一团糟(如此处:What is object slicing?)
但是...我在执行new Ball( -1, 1, boardWidth / 2, boardHeight / 2 );
时不是简单地自己创建派生对象,还是自动将其分配为GameObject吗?我真的无法弄清楚为什么一个有效而另一个无效。它与通过new
而不是仅通过Ball ball
创建对象有关吗?
很抱歉,如果这个问题没有意义,我只是想了解如何进行此对象切片。
答案 0 :(得分:2)
将对象直接存储在容器中时会发生对象切片。当您存储指向对象的指针(或更好的智能指针)时,不会发生切片。因此,如果您将Ball
存储在vector<GameObject>
中,它将被切片,但是如果您将Ball *
存储在vector<GameObject *>
中,一切都会好的。
答案 1 :(得分:1)
对于您的问题的快速解答是,进行_gameObjects.push_back( new Ball( ... ))
时对象切片不是问题,因为new
为Ball
大小的对象分配了足够的内存。
这是解释。对象切片是编译器认为对象小于实际大小的问题。因此,在您的代码示例中:
Ball b;
GameObject g = b;
编译器已经为名为GameObject
的{{1}}保留了足够的空间,但是您正在尝试在其中放置g
(Ball
)。但是b
可能比Ball
大,然后数据将会丢失,坏的东西很可能开始发生。
但是,当您执行GameObject
或new Ball(...)
时,编译器会确切知道要分配多少空间,因为它知道对象的真实类型。然后,您存储的实际上是new GameObject(...)
或Ball*
。而且,由于指针的大小相同,因此可以安全地存储GameObject*
类型的Ball*
,因此不会发生对象切片。指向的内存可以是任意数量的不同大小,但是指针始终是相同大小。
答案 2 :(得分:1)
为什么我们要为此说
virtual
?
如果未将函数声明为虚拟函数,则无法使用虚拟调度调用该函数。当通过指针或对基类的引用虚拟调用函数时,该调用将分派到最派生类(如果存在)中的重写。换句话说,virtual
允许运行时多态。
如果该函数是非虚拟的,则该函数只能静态分派。静态调用函数时,将调用编译时类型的函数。因此,如果通过基指针静态调用函数,则将调用基函数,而不是派生的覆盖。
顺便说一句,为什么我们在这里使用指针?为什么不只是纯GameObjects?
GameObject
是一个抽象类,因此您不能具有该类型的具体对象。由于您不能拥有具体的GameObject
,因此也不能拥有它们的数组(也不能是向量)。 GameObject
实例只能作为派生类型的基类子对象存在。
new
返回一个指向正确对象的指针?
new
在动态存储中创建一个对象,并返回指向该对象的指针。
顺便说一句,如果在丢失指针值之前未能在指针上调用delete
,则可能会导致内存泄漏。哦,如果您尝试两次delete
或不是delete
的某事new
,则程序的行为将不确定。内存分配很困难,您应该始终使用智能指针进行管理。像您的示例中那样,拥有所有权指针的向量是一个非常糟糕的主意。
此外,除非基类的析构函数是虚拟的,否则通过基对象指针删除对象具有未定义的行为。 GameObject
的析构函数不是虚拟的,因此程序无法避免UB或内存泄漏。两种选择都是不好的。解决方案是将GameObject
的析构函数设为虚拟。
避免对象切片
可以通过将基类抽象化来避免意外的对象切片。由于不能有抽象类的具体实例,因此您不能意外地“切出”派生对象的基础。
例如:
Ball b;
GameObject g = b;
格式错误,因为GameObject
是抽象类。编译器可能会这样说:
main.cpp: In function 'int main()':
main.cpp:16:20: error: cannot allocate an object of abstract type 'GameObject'
GameObject g = b;
^
main.cpp:3:7: note: because the following virtual functions are pure within 'GameObject':
class GameObject
^~~~~~~~~~
main.cpp:7:18: note: 'virtual void GameObject::Draw()'
virtual void Draw( ) = 0;
^~~~
main.cpp:16:16: error: cannot declare variable 'g' to be of abstract type 'GameObject'
GameObject g = b;
答案 3 :(得分:1)
我将尝试回答您所提出的各种问题,尽管其他人的回答可能会有更多的技术解释。
virtual void Draw( ) = 0;
我们为什么要说虚拟的?
简单来说,virtual
关键字告诉C ++编译器该函数可以在子类中重新定义。当您调用ball.Draw()
时,如果Ball::Draw()
类中的Ball
而不是GameObject::Draw()
中存在std::vector<GameObject*> _gameObjects;
,编译器就会知道应该执行。
GameObject
为什么我们在这里使用指针?
这是一个好主意,因为当容器必须为其分配空间并包含对象本身时,确实会发生对象切片。请记住,无论指针指向什么,指针都是恒定大小。当您必须调整容器的大小或移动元素时,指针的移动变得更加容易和快捷。而且,如果您确定这样做是正确的话,则始终可以将指向Ball
的指针转换回指向new
的指针。
new
返回一个指向正确对象的指针?
是的,new Ball( -1, 1, boardWidth / 2, boardHeight / 2 );
的工作是在堆上构造该类的实例,然后返回指向该实例的指针。
但我强烈建议您学习如何使用smart pointers。当不再引用对象时,它们可以自动删除对象。就像垃圾收集器在Java或C#这样的语言中所做的一样。
Ball
...还是自动将其分配为GameObject?
是的,如果GameObject
继承了Ball
类,则指向GameObject
的指针也将是指向Ball
的有效指针。如您所料,您无法通过指向GameObject
的指针访问Ball
的成员。
例如与通过新建球还是仅通过球形球创建对象有关吗?
我将解释实例化Ball ballA = Ball();
Ball* ballB = new Ball();
的两种方法之间的区别:
ballA
对于ballA
,我们声明Ball
变量是Ball()
的一个实例,它将“存在”于堆栈存储器中。我们使用ballA
构造函数将Ball
变量初始化为ballA
的实例。由于这是一个堆栈变量,因此ballB
实例将在程序退出声明它的作用域后被销毁。
对于ballB
,我们声明Ball
变量是指向new Ball()
实例的指针,该实例将驻留在堆内存中。我们使用Ball
语句首先为Ball()
分配堆内存,然后使用new
构造函数构造它。最后,该ballB
语句的结果为分配给ballB
的指针。
现在,当程序退出声明Ball
的范围时,指针将被销毁,但指向的实例将保留在堆中。如果您没有在其他地方保存该指针的值,则将无法释放该wheel
实例使用的内存。这就是智能指针之所以有用的原因,因为它们在内部跟踪实例是否仍在任何地方被引用。
答案 4 :(得分:1)
基本问题是复制对象(在类为“引用类型”的语言中这不是问题,但在C ++中,默认值是按值传递事物,即进行复制)。 “切片”是指将较大的对象(类型为B
的值,该值源自A
)复制到较小的对象(类型为A
)中。由于A
较小,因此只复制了一部分。
创建容器时,其元素是它们自己的完整对象。例如:
std::vector<int> v(3); // define a vector of 3 integers
int i = 42;
v[0] = i; // copy 42 into v[0]
v[0]
是int
变量,就像i
一样。
类也发生同样的事情:
class Base { ... };
std::vector<Base> v(3); // instantiates 3 Base objects
Base x(42);
v[0] = x;
最后一行将x
对象的内容复制到v[0]
对象中。
如果我们这样更改x
的类型:
class Derived : public Base { ... };
std::vector<Base> v(3);
Derived x(42, "hello");
v[0] = x;
...然后v[0] = x
尝试将Derived
对象的内容复制到Base
对象中。在这种情况下,将发生Derived
中声明的所有成员都被忽略的情况。仅复制基类Base
中声明的数据成员,因为这v[0]
才有空间。
指针为您提供避免复制的功能。当你做
T x;
T *ptr = &x;
,ptr
不是x
的副本,它仅指向x
。
类似地,您可以
Derived obj;
Base *ptr = &obj;
&obj
和ptr
具有不同的类型(分别为Derived *
和Base *
),但是C ++仍然允许该代码。因为Derived
对象包含Base
的所有成员,所以可以让Base
指针指向Derived
实例。
这实际上为您提供了obj
的简化接口。通过ptr
访问时,它只有在Base
中声明的方法。但是因为没有进行复制,所以所有数据(包括Derived
特定部分)仍然存在并且可以在内部使用。
对于virtual
:通常,当您通过类型为foo
的对象调用方法Base
时,它将完全调用Base::foo
(即{ {1}})。即使通过使用方法的不同实现实际指向派生对象的指针进行调用(如上所述),也会发生这种情况:
Base
通过将class Base {
public:
void foo() const { std::cout << "hello from Base::foo\n"; }
};
class Derived : public Base {
public:
void foo() const { std::cout << "hello from Derived::foo\n"; }
};
Derived obj;
Base *ptr = &obj;
obj.foo(); // calls Derived::foo
ptr->foo(); // calls Base::foo, even though ptr actually points to a Derived object
标记为foo
,我们强制方法调用使用对象的实际类型,而不是通过以下方式进行调用的声明的指针类型:
virtual
class Base {
public:
virtual void foo() const { std::cout << "hello from Base::foo\n"; }
};
class Derived : public Base {
public:
void foo() const { std::cout << "hello from Derived::foo\n"; }
};
Derived obj;
Base *ptr = &obj;
obj.foo(); // calls Derived::foo
ptr->foo(); // also calls Derived::foo
对普通对象无效,因为在那里声明的类型和实际的类型始终相同。它仅影响通过指向对象的指针(和引用)进行的方法调用,因为这些对象具有引用其他对象(可能具有不同类型)的能力。
这是存储指针集合的另一个原因:当您有virtual
的几个不同的子类时,所有这些子类都实现了自己的自定义GameObject
方法时,您希望代码注意对象的实际类型,因此在每种情况下都会调用正确的方法。如果draw
不是虚拟的,则您的代码将尝试调用不存在的draw
。根据您编码的精确程度,它要么不会首先编译,要么在运行时中止。
答案 5 :(得分:0)
这与值有关。
Ball b;
GameObject g;
b
的值是其变量的不同值。
g
的值同样是其变量的不同值。
将b
分配给g
时,b
的“子对象”的变量(从GameObject
继承)被分配给g
的变量。这是切片。
现在有关功能。
对于编译器,类的成员函数是指向该函数代码所驻留的内存的指针。
非虚函数始终是恒定的指针值。
但是虚函数可以具有不同的值,具体取决于在哪个类中声明它们。
因此,为了告诉编译器应该为函数指针创建一个占位符,使用了关键字virtual
。
现在返回值的分配。
我们知道彼此分配不同类型的变量会导致切片。因此,要解决此问题,可以使用间接寻址-指向对象的指针。
对于任何类型的指针,指针始终需要相同数量的strorage。并且当分配了一个指针时,其底层结构保持不变,仅复制该指针,该指针将覆盖前一个指针。
当我们在已切片的g
上调用虚拟函数时,我们可能正在从function
调用正确的b
,但是切片的g
对象却没有具有b
函数所需的所有字段,因此可能会发生错误。
但是使用原始对象b
进行指针调用,该对象具有b
的虚函数使用的所有必填字段。