《C++ Prime Plus》(6)

成员函数重载操作符的缺陷

 在上一篇笔记中提到的操作符重载函数有一个隐藏的缺陷。看似平等的位于操作符两侧的两个操作数,实际上却不平等:

1
2
3
Time time3 = time1 + time2;
// equals to
Time time3 = time1+(time2);

 也即,位于操作符左侧的操作数实际上是操作符重载函数的调用者,而右侧操作数作为函数参数传入操作符重载函数。这就带来了一个问题,试想一个操作符重载函数如下:

1
2
3
Type operator+(int n); // 函数原型
A = B * 3; // A = B.operator+(3) 有效语句!
A = 3 * B; // A = 3.operator+(B) 无效语句!

 按照思维习惯,操作符一般满足反身性,我们希望重载出来的操作符能够在交换左右操作数前后保持相同的有效的计算特性,但是上例告诉我们,还需要对操作符重载做进一步的升级才能满足我们的期望。

 一种解决方式是利用非成员函数实现操作符重载(之前的操作符重载都是基于类的,将操作符重载函数作为类的成员函数来使用,实际上大多数的操作符都支持非成员函数实现操作符重载),例如:

1
2
3
4
Type operator(int n);
Type operator(int n, Type B);
A = B * 3; // A = B.operator+(3) 有效语句!
A = 3 * B; // A = operator(3, B) 有效语句(实际并非,详见下)!

 如此,在交换操作数前后都能够的期望的结果。

友元函数

 使用非成员函数重载操作符虽然解决了操作数地位不平等问题,但同时也带来了新的问题——由于类的数据隐藏特性,非成员函数无法访问类的私有成员,因此上面语句块中的最后一条仍然不能被通过编译。为了解决这一问题,出现了可以访问类私有成员的非成员函数——友元函数。

1
2
3
4
5
6
7
8
9
class Type{
···
friend Type operator(int n, Type B); // 函数原型
···
};

Type operator(int n, Type B){
··· // 函数实现
}

 友元函数原型放在类声明中,但是它不是成员函数,因此不能使用成员函数操作符调用,在写函数实现时也不必带上Type::限定符,此外函数实现也不用写关键字friend

new和delete配套使用

 在类的定义中,如果构造函数使用了new操作符来申请内存空间,那么就需要在析构函数中配套使用delete操作符来释放相应内存以避免内存泄露。

 此外,由于可以有多个构造函数但只能有一个析构函数,因此多个构造函数中new的使用方式要保持一致。也即要带中括号都带,要不带都不带,以保证析构函数能够兼容所有构造函数。

C++编译器的故作聪明

 在C++的类定义中,存在以下几种特殊的成员函数,如果程序员没有主动定义这类函数,C++编译器就会自动生成这些默认函数用于类操作,这原本是C++编译器智能化的表现,但是在某些应用场合下,这种隐藏操作可能导致与预期不符合的结果。

 (1)默认构造函数和默认析构函数。如果程序员没有定义这两类函数,则编译器将自动添加以满足类的初始化和销毁需求。

 (2)复制构造函数。当程序中有语句利用已有对象来初始化一个新的对象时,如果程序员未提供此类函数,系统将自动提供一个原型为:

1
class_name(const class_name &);

的复制构造函数来完成要求。默认的复制构造函数将逐个复制对象的非静态成员的值,若成员本身也是类则调用这个类的复制构造函数来复制成员对象。

 (3)赋值运算符。当程序语句将相同类型的一个对象用=赋值给另一个对象时,如果程序员没有定义,则编译器将提供一个原型为:

1
class_name & class_name::operator=(const class_name &);

的赋值运算形式。

 复制构造函数和赋值运算符的区别如下:

1
2
3
4
5
class Student{···};
Student stu1(···);
Student stu2 = stu1; // 调用复制构造函数
Student stu3(···);
stu3 = stu1; // 使用赋值运算符

类的继承

 类继承用于扩展已有类的功能。被继承的类叫基类,基类通过继承派生出派生类。例如在拳头出品的游戏英雄联盟中,非常重要的一个类概念是英雄。假设英雄是基类,则英雄又分为刺客、法师、射手、坦克、战士等不同派生类,以坦克为例:

1
2
class hero{···};    // 基类
class tank: public hero{···}; // 派生类,公有继承

派生类与基类的关系

 (1)派生类可以使用基类的公有方法,但不能直接访问基类的私有成员,如果需要访问私有成员,可以通过基类的公有方法来访问。如果派生类的对象要使用基类的方法,必须通过作用域解析符::来调用,否则调用的是派生类中定义的同名方法。

 (2)在不进行显式类型转换的情况下,基类指针可以指向派生类对象;基类引用可以引用派生类对象。但是,基类指针或引用只能调用基类方法。

虚函数

 当同一个函数方法既存在于基类中,又存在于派生类中时,这个方法就具有了两种定义。当通过指针或引用调用此方法时,编译器需要决定使用哪一个方法来完成工作。

 (1)对方法定义不做任何特殊处理。此时编译器将根据引用或指针的类型来选择方法,如果引用或指针为基类则调用基类方法定义,否则调用派生类方法定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class hero{
···
public:
void damage(···); // 假设此函数接收英雄收到的伤害以修改英雄的HP值
};
class tank: public hero{
···
public:
void damage(···); // 坦克也会受到伤害,因此坦克也具有这个成员函数
};

hero h(···);
tank t(···);
hero & h_ref = h;
hero & t_ref = t;
h_ref.damage(···); // hero型引用,调用hero::damage()
t_ref.damage(···); // hero型引用,调用hero::damage()

 (2)在基类的方法定义前加virtual关键词,将成员函数变为虚函数。同时可以将派生类中相同的成员函数也用此关键词标注上。此时,编译器将根据实际对象类型而非引用或指针的类型来选择调用哪一个方法。另外需要注意,关键词virtual只需要在声明函数原型时提供,在编写函数实现时不需要加此关键词。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class hero{
···
public:
virtual void damage(···); // 假设此函数接收英雄收到的伤害以修改英雄的HP值
};
class tank: public hero{
···
public:
virtual void damage(···); // 坦克也会受到伤害,因此坦克也具有这个成员函数
};

hero h(···);
tank t(···);
hero & h_ref = h;
hero & t_ref = t;
h_ref.damage(···); // hero型对象,调用hero::damage()
t_ref.damage(···); // tank型对象,调用tank::damage()


 (3)需要注意的是,构造函数不能为虚函数,除非类不用做基类。友元也不能是虚函数,因为只有成员函数才能使虚函数,而友元不是成员函数。

多态性

 一个有关多态性的例子。对于公有继承的类,可以创建一个指针既指向基类,也指向派生类,这种现象叫类的多态性。在上面虚函数的介绍中,我们已经看到了一个多态性的例子。

虚析构函数

 按照惯例,如果类定义中有虚函数,那么也应该提供一个虚析构函数来保证基类和派生类中多个析构函数以正确的序列被调用。


转载请注明来源:©Tinshine