第55课 经典问题解析四
1. 关于动态内存分配
(1)new和malloc的区别
区别 |
new |
malloc |
是否是关键字 |
关键字,是C++的一部分。被所有的C++编译器支持。 |
由C库提供的函数,注意是函数,而不是关键字,要依赖C库(cstdlib),在某些系统开发(如嵌入式)中可能不能调用。 |
分配单位 |
以具体类型为单位 |
以字节为单位 |
内存初始化 |
申请时可进行初始化 |
仅根据需要申请定量的内存,但并不进行初始化。 |
是否触发构造函数的调用 |
会触发。对象的创建只能使用new |
不会触发,仅分配需要的内存空间,因此不适合面向对象开发 |
(2)delete/free的区别
区别 |
delete |
free |
是否是关键字 |
关键字,是C++的一部分。被所有的C++编译器支持。 |
由C库提供的函数。在某些系统开发(如嵌入式)中可能不能调用。 |
是否触发析构函数的调用 |
会触发.对象的销毁只能使用delete |
不会触发,仅归之前分配的内存空间,因此不适合面向对象开发 |
【编程实验】new和malloc的区别
#include <iostream> #include <string> #include <cstdlib> //for malloc\free函数。同时说明这两者都是函数,而不是关键字! using namespace std; class Test { int* mp; public: Test() { cout << "Test::Test()" << endl; mp = new int(100); cout <<*mp << endl; } ~Test() { delete mp; cout << "~Test::Test()" << endl; } }; int main() { Test* pn = new Test; //会调用构造函数 Test* pm = (Test*)malloc(sizeof(Test)); //不会调用构造函数 delete pn; //会调用析构函数。如果这里混用free,则会造成内存泄漏! free(pm); //如果这里误用delete来释放pm所指的空间时,会同时调用 //析构函数,但因mp指针所指空间是未知,出现误删除的bug。 return 0; }
2. 关于虚函数
(1)构造函数:
①构造函数本身不可能成为虚函数。因为创建一对象时,其类型必须是确定的。如当调用new T时,显然要创建的是T类的对象,而不可能是T子类的对象,
所以不能声明为虚函数。
②在构造函数中调用其他虚函数时,不会发生多态。一般在进入构造函数前,虚表己创建好。因此,在构造父类时,会根据父类的虚表查找虚函数,所以调用的是父类版本的虚函数,当创建子类时,由于子类虚表会覆盖父类虚表,所以调用的是子类版本的虚函数,所以不会发生多态。故不管是构造子类,还是父类时,如果调用虚函数则只会调用相应的类本身中定义的那个函数版本的虚函数。
(2)析构函数:
①析构函数本身可以成为虚函数。比如Base* p = new Child,而delete p这时使用的是父类指针,必然要求要通过父类类型的p指针调用到子类的析构函数才能正确析构对象,所以可以为虚函数。而且,建议在设计类时将析构函数声明为虚函数,特别是要作为父类的类。
②析构函数中调用其他虚函数时,也不会发生多态,因为析构阶段虚函数表指针己经被销毁。所以当调用虚函数时,只会调用本类中定义的那个函数版本,即静态绑定了。
【编程实验】构造函数中调用虚函数会发生多态吗???
//test.cpp
#include <iostream> using namespace std; class A { public: A():m_ival(0){test();} virtual void func(){cout << m_ival << " ";} void test(){func();} public: int m_ival; }; class B : public A { public: B(){test();} virtual void func() { ++m_ival; cout << m_ival << " "; } }; int main() { A* p = new B; //new B时,会先构造父类A,由于进入A构造函数之前,A的虚表己构造好。因此,调用A构造函数时,会通过查找其虚表,因此调用A::func。 //构造完父类后,开始构造子类B。在进入构造函数之前父类A的虚表会被B所修改和覆盖,因此会调用到B::func p->test(); //多态,调用B::func }
/*输出结果
0 1 2 (注意分析为什么不是0 0 1? 见上面的分析)
*/
【编程实验】构造、析构和虚函数
#include <iostream> #include <string> using namespace std; class Base { public: Base() //不能成为虚函数,哪怕加了virtual; { cout << "Base()" << endl; func(); //不会发生多态,只会调用本类中的函数版本! } virtual void func() { cout << "Base::func()" << endl; } virtual ~Base() //可以成为虚函数 { func(); //不会发生多态,只会调用本类中的函数版本! cout << "~Base()" << endl; } }; class Derived: public Base { public: Derived() { cout << "Derived()" << endl; func(); //不会发生多态,只会调用本类中的函数版本! } virtual void func() { cout << "Derived::func()" << endl; } ~Derived() { func(); //不会发生多态,只会调用本类中的函数版本! cout << "~Derived()" << endl; } }; int main() { Base* p = new Derived(); //注意是父类指针,如果这里直接声明为Derived* p = new Derived() //则delete p直接调用子类的构造函数。但我们本例的目的之一是为了演示 //析构函数的多态,所以声明为父类的针。 //... cout << endl; delete p; //delete会调用析构函数。从这行代码看,如果父类中析构函数没被声明为虚函数的话, //delete一个父类的指针,由于静态绑定,所以会调用的是父类的析构函数,从而造成 //Derived的析构函数没被调用。当然,如果父类中析构函数被声明为虚函数,根据多态 //原理,将会调用子类的析构函数,结合析构的特点,会先调用父类析构,再调用子类 //自己的析构函数,从而正确的释放内存。 return 0; }
运行结果:
/*输出结果
Base()
Base::func() //注意,并没有发生多态
Derived()
Derived::func() //注意,并没有发生多态
Derived::func() //注意,并没有发生多态
~Derived()
Base::func() //注意,并没有发生多态
~Base()
*/
3. 关于继承中的强制类型转换
(1)dynamic_cast是与继承相关的类型转换关键字
(2)dynamic_cast要求相关的类中必须有虚函数
(3)用于直接或间接继承关系的指针(引用)之间
|
指针间的转换 |
引用间的转换 |
转换成功 |
得到目标类型的指针 |
得到目标类型的引用 |
转换失败 |
得到一个空指针 |
得到一个异常操作信息 |
(4)编译器会检查dynamic_cast的使用是否正确
(5)类型转换的结果只可能在运行阶段才能得到
【编程实验】dynamic_cast的使用
#include <iostream> #include <string> using namespace std; class Base { public: Base() { cout << "Base::Base()" << endl; } virtual ~Base() { cout << "Base::~Base()" << endl; } }; class Derived: public Base { public: }; int main() { Base* p = new Base; //将用父类指针来初始化子类指针(为演示之用,本身这样用就是不完全的!) //注意,这里也提醒我们,可以用dynamic_cast来判断一个类是不是另一个类父类的方法。 // 具体方法Base* p = dynamic_cast(Base* p)(pDerived);即,子类能否转为父类。 // 如果返回值不为NULL,表示有父子关系。否则没有。 Derived* pd = dynamic_cast<Derived*>(p); //注意p所指的类中一定要有虚函数 //本例中,析构函数声明为虚函数。 if(pd != NULL) //转换成功。 { cout << "pd = " << pd << endl; } else //转换失败,返回空指针NULL { cout << "Cast Error!" << endl; } delete p; return 0; }
/*输出结果
Base::Base()
Cast Error!
Base::~Base()
*/
4. 重载->和*操作符
(1)C++中解引用运算符和箭头运算符通常用于指向类对象的指针,但我们可以通过重载这两个运算符,使其作用于类的对象而不是指针。
(2)重载要求:
①箭头运算符必须是类的成员函数。解引用运算符一般也应该声明为类的成员函数,但不是必须。
②箭头用算符返回值必须是一个指针,或者是一个重载了箭头运算符的对象。
A.如果返回的是一个指针:将调用内置的箭头运算符。执行相当于(*(p.operator->()).mem;的操作。
B.如果返回是一个对象,则继续对该对象的重载->操作符,如此递归一下,直到返回的是一个指针,再对该指针调用mem的操作。
操作相当于(*(p.operator->().operator->())).mem;
【编程实验】重载->操作符
#include <iostream> using namespace std; class A{ public: A() { x = 1;} void action(){ cout << "Action in class A!" << endl; } public: int x; }; class B{ A a; public: B() { x =2; } A* operator->(){ return &a; } void action(){ cout << "Action in class B!" << endl; } private: int x; }; class C{ B b; public: C (){ x =3; } B operator->(){ return b; } void action(){ cout << "Action in class C!" << endl; } private: int x; }; int main(int argc, char *argv[]) { C* pc = new C; pc->action();//pc为指针,直接调用pc的action C c; c->action();//因C的重载->函数返回的是一个对象B,继续调用B的->,遇至返回指针A*,递归调用结束, //则调用A的action。打印"Action in class A!" int x = c->x;//理由同上,从c的->出发,返回对象,继续调用该对象的->直到返回指针(A*),然后 //调用A的x,然的x为1 std::cout<<x;//打印1 return 0; }
5. 小结
(1)new/delete会触发构造函数或析构函数的调用
(2)构造函数不能成为虚函数,析构函数可以成为虚函数
(3)构造函数和析构函数中都无法产生多态行为
(4)dynamic_cast是与继承相关的专用转换关键字。