静动态类型、绑定、坑点与多态体现深谈
静动态类型、绑定、坑点与多态体现深谈
1. 静态类型和动态类型
静态类型在编译期间确定,而动态类型在运行时确定。静态类型决定了编译器如何解析代码,而动态类型决定了在运行时实际调用的函数。
静态类型
静态类型是对象定义时的类型,在编译期间就确定好的。
Base base; // 静态类型是 Base
Derived derived; // 静态类型是 Derived
Base* pbase; // 静态类型是 Base*,无论指向什么
Base* pbase2 = new Derived(); // 静态类型是 Base*
Base* pbase3 = new Derived2(); // 静态类型是 Base*
动态类型
动态类型是对象在运行时所指向的实际类型。一般来说,只有指针或引用才有动态类型的说法,通常指向父类的指针或引用。
根据上面的代码行,动态类型如下:
Base
和Derived
没有动态类型,因为它们既不是指针也不是引用。pbase2
的动态类型是Derived
。pbase3
的动态类型是Derived2
。
动态类型在执行过程中可以改变。例如:
pbase = pbase2; // pbase 的动态类型是 Derived
pbase = pbase3; // pbase 的动态类型是 Derived2
示例代码
以下是一个示例,展示了静态类型和动态类型的工作原理:
#include <iostream>
class Base {
public:
virtual void myFunction() {
std::cout << "Base::myFunction" << std::endl;
}
};
class Derived : public Base {
public:
void myFunction() override {
std::cout << "Derived::myFunction" << std::endl;
}
};
class Derived2 : public Base {
public:
void myFunction() override {
std::cout << "Derived2::myFunction" << std::endl;
}
};
int main() {
Base base; // 静态类型是 Base
Derived derived; // 静态类型是 Derived
Base* pbase; // 静态类型是 Base*
Base* pbase2 = new Derived(); // 静态类型是 Base*
Base* pbase3 = new Derived2(); // 静态类型是 Base*
// 动态类型示例
pbase = pbase2; // pbase 的动态类型是 Derived
pbase->myFunction(); // 调用 Derived::myFunction
pbase = pbase3; // pbase 的动态类型是 Derived2
pbase->myFunction(); // 调用 Derived2::myFunction
return 0;
}
解释
- 静态类型:在编译期间确定的类型。例如,
Base* pbase2
的静态类型是Base*
。 - 动态类型:在运行时确定的实际类型。例如,
pbase2
的动态类型是Derived
。
2. 静态绑定和动态绑定
静态绑定
静态绑定是指绑定的是静态类型,所对应的函数或属性依赖于对象的静态类型,发生在编译期。
动态绑定
动态绑定是指绑定的是动态类型,所对应的函数或属性依赖于对象的动态类型,发生在运行期。
示例代码
以下是一个示例,展示了静态绑定和动态绑定的工作原理:
#include <iostream>
class Base {
public:
void staticFunction() {
std::cout << "Base::staticFunction" << std::endl;
}
virtual void dynamicFunction() {
std::cout << "Base::dynamicFunction" << std::endl;
}
};
class Derived : public Base {
public:
void staticFunction() {
std::cout << "Derived::staticFunction" << std::endl;
}
void dynamicFunction() override {
std::cout << "Derived::dynamicFunction" << std::endl;
}
};
int main() {
Base base;
Derived derived;#include <iostream>
class Base {
public:
void staticFunction(int x = 10) {
std::cout << "Base::staticFunction, x = " << x << std::endl;
}
virtual void dynamicFunction(int x = 20) {
std::cout << "Base::dynamicFunction, x = " << x << std::endl;
}
};
class Derived : public Base {
public:
void staticFunction(int x = 30) {
std::cout << "Derived::staticFunction, x = " << x << std::endl;
}
void dynamicFunction(int x = 40) override {
std::cout << "Derived::dynamicFunction, x = " << x << std::endl;
}
};
int main() {
Base base;
Derived derived;
Base* pbase = &derived;
base.staticFunction(); // 静态绑定,调用 Base::staticFunction,输出 x = 10
derived.staticFunction(); // 静态绑定,调用 Derived::staticFunction,输出 x = 30
pbase->staticFunction(); // 静态绑定,调用 Base::staticFunction,输出 x = 10
base.dynamicFunction(); // 动态绑定,调用 Base::dynamicFunction,输出 x = 20
derived.dynamicFunction(); // 动态绑定,调用 Derived::dynamicFunction,输出 x = 40
pbase->dynamicFunction(); // 动态绑定,调用 Derived::dynamicFunction,但输出 x = 20
return 0;
}
Base* pbase = &derived;
base.staticFunction(); // 静态绑定,调用 Base::staticFunction
derived.staticFunction(); // 静态绑定,调用 Derived::staticFunction
pbase->staticFunction(); // 静态绑定,调用 Base::staticFunction
base.dynamicFunction(); // 动态绑定,调用 Base::dynamicFunction
derived.dynamicFunction(); // 动态绑定,调用 Derived::dynamicFunction
pbase->dynamicFunction(); // 动态绑定,调用 Derived::dynamicFunction
return 0;
}
解释
- 静态绑定:
staticFunction
是普通成员函数,绑定的是静态类型,发生在编译期。例如,pbase->staticFunction()
调用的是Base::staticFunction
,因为pbase
的静态类型是Base*
。 - 动态绑定:
dynamicFunction
是虚函数,绑定的是动态类型,发生在运行期。例如,pbase->dynamicFunction()
调用的是Derived::dynamicFunction
,因为pbase
的动态类型是Derived
。 - 缺省参数(也称为默认参数):缺省参数一般是静态绑定。例如,
pbase->dynamicFunction()
调用Derived::dynamicFunction
时,缺省参数x
的值是20
,因为pbase
的静态类型是Base*
,而Base::dynamicFunction
的缺省参数是20
。
结论
- 普通成员函数是静态绑定,而虚函数是动态绑定。
- 缺省参数(也称为默认参数)一般是静态绑定。
3. 继承的非虚函数坑
在继承关系中,不应该在子类中重新定义一个继承来的非虚成员函数。这是因为这样做会导致函数的隐藏和意外的行为,破坏代码的可维护性和可读性。
示例代码
以下是一个示例,展示了在子类中重新定义继承来的非虚成员函数所带来的问题:
#include <iostream>
class Base {
public:
void nonVirtualFunction() {
std::cout << "Base::nonVirtualFunction" << std::endl;
}
};
class Derived : public Base {
public:
void nonVirtualFunction() {
std::cout << "Derived::nonVirtualFunction" << std::endl;
}
};
int main() {
Base base;
Derived derived;
Base* pbase = &derived;
base.nonVirtualFunction(); // 调用 Base::nonVirtualFunction
derived.nonVirtualFunction(); // 调用 Derived::nonVirtualFunction
pbase->nonVirtualFunction(); // 调用 Base::nonVirtualFunction
return 0;
}
解释
- 隐藏问题:在
Derived
类中重新定义nonVirtualFunction
会隐藏Base
类中的同名函数。这意味着通过基类指针或引用调用该函数时,仍然会调用基类的实现,而不是子类的实现。 - 意外行为:在上面的示例中,
pbase->nonVirtualFunction()
调用的是Base::nonVirtualFunction
,而不是Derived::nonVirtualFunction
,因为nonVirtualFunction
不是虚函数,绑定的是静态类型。
结论
- 函数隐藏:在子类中重新定义继承来的非虚成员函数会导致基类中的同名函数被隐藏,破坏代码的可维护性和可读性。
- 意外行为:通过基类指针或引用调用该函数时,仍然会调用基类的实现,而不是子类的实现,可能导致意外行为。
为了避免这些问题,建议遵循以下原则:
- 如果需要在子类中重写基类的函数,应该将该函数声明为虚函数。
- 如果不希望子类重写某个函数,可以将该函数声明为
final
(C++11 及以上版本支持)。
4. 虚函数的动态绑定
虚函数的动态绑定是指在运行时根据对象的动态类型来决定调用哪个函数实现。这种机制允许通过基类指针或引用调用派生类的函数,实现多态性。
示例代码
以下是一个示例,展示了虚函数的动态绑定:
#include <iostream>
class Base {
public:
virtual void dynamicFunction() {
std::cout << "Base::dynamicFunction" << std::endl;
}
};
class Derived : public Base {
public:
void dynamicFunction() override {
std::cout << "Derived::dynamicFunction" << std::endl;
}
};
int main() {
Base base;
Derived derived;
Base* pbase;
pbase = &base;
pbase->dynamicFunction(); // 调用 Base::dynamicFunction
pbase = &derived;
pbase->dynamicFunction(); // 调用 Derived::dynamicFunction
return 0;
}
解释
- 虚函数:在基类中使用
virtual
关键字声明的函数。派生类可以重写该函数。 - 动态绑定:在运行时,根据对象的动态类型来决定调用哪个函数实现。例如,
pbase->dynamicFunction()
会根据pbase
指向的对象类型来调用相应的函数实现。
结论
- 多态性:虚函数的动态绑定实现了多态性,使得通过基类指针或引用可以调用派生类的函数。
- 运行时决策:动态绑定在运行时根据对象的动态类型来决定调用哪个函数实现,而不是在编译期决定。
5. 重新定义虚函数的缺省参数坑
在继承关系中,重新定义虚函数的缺省参数可能会导致意外的行为。这是因为缺省参数是静态绑定的,即在编译期就确定了,而虚函数是动态绑定的,即在运行时根据对象的动态类型来决定调用哪个函数实现。
示例代码
以下是一个示例,展示了重新定义虚函数的缺省参数所带来的问题:
#include <iostream>
class Base {
public:
virtual void dynamicFunction(int x = 20) {
std::cout << "Base::dynamicFunction, x = " << x << std::endl;
}
};
class Derived : public Base {
public:
void dynamicFunction(int x = 40) override {
std::cout << "Derived::dynamicFunction, x = " << x << std::endl;
}
};
int main() {
Base base;
Derived derived;
Base* pbase = &derived;
base.dynamicFunction(); // 调用 Base::dynamicFunction,输出 x = 20
derived.dynamicFunction(); // 调用 Derived::dynamicFunction,输出 x = 40
pbase->dynamicFunction(); // 调用 Derived::dynamicFunction,但输出 x = 20
return 0;
}
解释
- 虚函数:
dynamicFunction
是虚函数,通过基类指针或引用调用时,会根据对象的动态类型来决定调用哪个函数实现。 - 缺省参数:缺省参数是静态绑定的,即在编译期就确定了。例如,
pbase->dynamicFunction()
调用Derived::dynamicFunction
时,缺省参数x
的值是20
,因为pbase
的静态类型是Base*
,而Base::dynamicFunction
的缺省参数是20
。
结论
- 缺省参数的静态绑定:缺省参数在编译期就确定了,即使虚函数在运行时进行动态绑定,缺省参数的值仍然取决于静态类型。
- 意外行为:重新定义虚函数的缺省参数可能会导致意外的行为,因为通过基类指针或引用调用时,缺省参数的值不会根据动态类型来决定。
- 不要在子类中重新定义虚函数缺省参数的值:为了避免意外行为和代码的可维护性问题,不要在子类中重新定义继承来的虚函数的缺省参数。
6. C++ 中的多态性
多态性是面向对象编程的一个重要特性,它允许同一个接口调用不同的实现。在 C++ 中,多态性必须通过虚函数来实现,没有虚函数,就不可能存在多态性。
多态的概念
多态性可以从两个方面来讨论:
-
从代码实现上:
- 多态性通过虚函数和继承来实现。基类中声明虚函数,派生类中重写这些虚函数。
- 在运行时,通过基类指针或引用调用虚函数,根据对象的动态类型来决定调用哪个函数实现。
-
从表现形式上:
- 多态性允许通过同一个接口调用不同的实现。具体表现为,通过基类指针或引用调用派生类的函数,实现了动态绑定。
- 这种机制提高了代码的灵活性和可扩展性,使得代码可以处理不同类型的对象,而无需修改调用代码。
示例代码
以下是一个示例,展示了 C++ 中的多态性:
#include <iostream>
class Base {
public:
virtual void show() {
std::cout << "Base::show" << std::endl;
}
};
class Derived1 : public Base {
public:
void show() override {
std::cout << "Derived1::show" << std::endl;
}
};
class Derived2 : public Base {
public:
void show() override {
std::cout << "Derived2::show" << std::endl;
}
};
void display(Base* base) {
base->show();
}
int main() {
Base base;
Derived1 derived1;
Derived2 derived2;
display(&base); // 调用 Base::show
display(&derived1); // 调用 Derived1::show
display(&derived2); // 调用 Derived2::show
return 0;
}
解释
-
从代码实现上:
Base
类中声明了虚函数show
。Derived1
和Derived2
类中重写了show
函数。- 在
display
函数中,通过基类指针base
调用虚函数show
,在运行时根据对象的动态类型来决定调用哪个函数实现。
-
从表现形式上:
- 有父类有子类(有继承关系):
Base
是父类,Derived1
和Derived2
是子类,且Base
类中含有虚函数show
,子类重写了该虚函数。 - 父类指针指向子类对象:在
main
函数中,Base* pbase
可以指向Derived1
或Derived2
对象。 - 调用子类中重写的虚函数:通过
display
函数,基类指针base
调用虚函数show
,实际调用的是子类的实现。例如,display(&derived1)
调用Derived1::show
,display(&derived2)
调用Derived2::show
。 - 通过同一个接口
display
,调用了不同的实现,实现了多态性。
- 有父类有子类(有继承关系):
结论
- 多态必须存在虚函数并且调用虚函数:没有虚函数,就不可能存在多态性。
- 从代码实现上:多态性通过虚函数和继承来实现,基类中声明虚函数,派生类中重写这些虚函数。
- 从表现形式上:多态性允许通过同一个接口调用不同的实现,提高了代码的灵活性和可扩展性。