春暖花开

C++ 中的虚函数与动态绑定杂谈

上大学之后学的第一门编程语言是 C,尽管 C 的确是一门很强大的语言,但是,在某些情况下,用 C 语言来解决问题实现起来会麻烦很多,另外,某些东西在 C 语言里面也是没有的,比如面向对象。

第一次接触面向对象大概是大二自学 Python 的时候,那时候看的一头雾水,什么构造函数,析构函数不理解它的原理,书里面讲解的也不是很清楚,更别提多态、继承等等。到后面看 C++ 的时候终于对面向对象有一些基本的理解了,虽然很多东西还是理解的不够透彻。

说到底,面向对象最重要的还是在其思想,当你理解了它的精髓和思想,那么,你再去看无论是 C++ ,还是 Python,还是 Java,其中的面向对象的内容都是一样的,只是不同语言的实现形式有所不同而已。

重载

先简单说一下 C++ 里面的重载,所谓重载,就是可以定义两个名字一样的函数,它们的参数列表不同,那么就可以把其中一个函数称为另一个函数的重载,例如:

1
2
3
4
5
6
bool Equal(int a, int b){
return a == b;
}
bool Equal(long a, long b){
return a == b;
}

需要注意的是,只有参数列表不同:个数或类型。如果只有返回类型不同,那么第二个声明是错误的。这样,当你在调用函数时,编译器会根据参数类型来选择合适的函数。显然,这儿会产生一个问题,比如上面的 Equal 函数,如果传入的参数一个为 int ,一个为 long ,那么编译器就会被迷惑,因为它无法确定哪一个是最匹配的,编程的过程中应尽量避免这样的歧义。另外,main 函数不可以被重载。

构造函数与析构函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Person {
public :
float height;
float weight;
int age;
Person(){
height = 0.0
weight = 0.0
age = 0;
};
Person(float, h, float w, int a){
height = h;
weight = w;
age = a;
};
virtual ~Person(){
std::cout << "Kill me.";
}
};

上面定义了一个类 Person ,同时定义了两个构造函数,当创建一个新的对象时,就会执行构造函数,显然,构造函数是可以被重载的,因此,编译器会根据参数列表来选择合适的构造函数,另外,初始类成员时也可以通过初始化列表,这儿不再叙述。

而析构函数则与构造函数相反,它是在一个对象消失的时候执行的,比如静态声明的对象退出作用域时,或动态分配的对象调用 delete 删除时。析构函数的函数名也和类名相同,只是前面多了一个符号 ~ 。另外,注意到上面的析构函数前面多了一个关键字 virtual 。先说一下该关键字是什么意思,它是实现动态绑定的关键,virtual 关键字允许在子类中重新定义该成员函数。下面,定义一个子类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Man : public Person{
public :
int salary;
std::string *name;
Man(char *s){
name = new string(s);
salary = 0;
};
virtual ~Man(){
if (name){
delete name;
}
};
};

在该类初始化的时候,动态分配了一个 string 存储名字,因此,需要在虚构函数中将其删除。

在 C++ 中,基类的指针是可以指向派生类的:

1
2
Person *man = new Man("Ana");
delete man;

考虑上面的代码,man 是一个 Person 类的指针,它指向了一个 Man 对象,假如上面的虚构函数没有定义成虚函数,那么 delete 的时候,只会调用 Person 的析构函数,这样,便发生了内存泄露,因为,动态分配的 string 并没有被删除。但定义成虚函数,那么编译器会根据它所指向的对象来选择对应的析构函数,从而实现动态绑定,这就是为什么基类的析构函数一般需要定义成虚函数。

最后,说一个有趣的问题,假如就像上面说的由于没有进行动态绑定导致发生内存泄露,那么,这部分内存什么时候被释放?很多人会以为需要等到关机或者重启,因为内存是易失性存储,但事实不是的,别忘了,计算机的终极大 Boss 可是操作系统,你分配出去的每一块内存它都时候监测着呢,malloc 是通过系统调用分配内存,而 new 则是通过 malloc 以及调用构造函数来实现对象分配。因此,当程序运行结束或 Crash 终止之后,操作系统会回收所有内存,无论是动态分配的还是静态分配的。但是,如果在程序运行的过程中,动态分配的内存是不会被回收的,因此,如果在运行过程不停分配内存而没有释放,那么内存就可能会被耗尽,这就是为什么要防止出现内存泄露。当然,如果一个进程分配了过多的内存超过一定限制,那么,我们的终极大 Boss 又会出场了,它会直接把你 Kill 了。

0%