C++学习笔记(二)

继承

访问限定符 public

看下面的代码:

c++ 复制代码
#include <iostream> 
using namespace std;

class Fish
{
public:
	bool isFreshWaterFish;
	
	void Swim()
	{
		if (isFreshWaterFish)
			cout << "Swims in lake" << endl;
		else
			cout << "Swims in sea" << endl;
	}
};

class Tuna : public Fish
{
public:

	Tuna()
	{
		isFreshWaterFish = false;
	}
};


class Carp : public Fish
{
public:

	Carp()
	{
		isFreshWaterFish = true;
	}
};


int main()
{
	Carp myLunch;
	Tuna myDinner;
	
	cout << "About my food:" << endl;
	
  cout << "Lunch: ";
	myLunch.Swim();
	
	cout << "Dinner: ";
	myDinner.Swim();
	
	return 0;
}

在 Visual Studio 中运行 C++ 程序时,控制台窗口默认会在程序执行完毕后立即关闭。使用调试模式运行(Ctrl+F5),可以让程序执行完后不立即退出,控制台打印如下:

yaml 复制代码
About my food:
Lunch: Swims in lake
Dinner: Swims in sea
请按任意键继续. . .

上面的代码中 Tuna 和 Carp 都使用 public 继承了 Fish,因此可以在 Tuna 和 Carp 中修改 isFreshWaterFish 的值。并且虽然 Tuna 和 Carp 中都没有 Swim() 函数,但是可以在 main() 函数使用子类(或者叫派生类)的对象来调用父类(或者叫基类)的 Swim() 函数。

访问限定符 protected

但是上面的程序存在一个缺陷:可以在 main() 函数中随意修改 isFreshWaterFish 的值:

ini 复制代码
myDinner.isFreshWaterFish = true; // but Tuna isn't a fresh water fish!

需要让基类的某些属性能在派生类中访问,但不能在继承层次结构外部访问,可以使用关键字 protected。

与 public 和 private 一样,protected 也是一个访问限定符。将属性声明为 protected 的时,相当于允许派生类和友元类访问它,但禁止在继承层次结构外部(包括 main( ) 函数)访问它。

把 Fish 类改成这样:

c++ 复制代码
class Fish
{
protected:
	bool isFreshWaterFish;

public:	
	void Swim()
	{
		if (isFreshWaterFish)
			cout << "Swims in lake" << endl;
		else
			cout << "Swims in sea" << endl;
	}
};

可以看到跟之前的 Fish 类唯一不同的地方是:isFreshWaterFish 现在是 protected 属性,这样就无法在 main() 函数中修改 isFreshWaterFish 的值了,在 VS 中会直接报红。

向基类传递参数

如果基类有重载的带参数的构造函数,如下所示:

c++ 复制代码
class Base
{
public:
    Base(int someNumber) // overloaded constructor
    {
        // Use someNumber
    }
};

派生类该如何实例化基类? 答案是使用初始化列表:

c++ 复制代码
Class Derived: public Base
{
public:
    Derived(): Base(25) // instantiate Base with argument 25
    {
        // derived class constructor code
    }
};

在派生类中隐藏基类的方法

看下面的代码:

c++ 复制代码
#include <iostream> 
using namespace std;

class Fish
{
public:	
	void Swim()
	{
		cout << "Fish swims... !" << endl;
	}


	void Swim(bool isFreshWaterFish)
	{
		if (isFreshWaterFish)
			cout << "Swims in lake" << endl;
		else
			cout << "Swims in sea" << endl;
	}
};

class Tuna : public Fish
{
public:

	void Swim()
	{
		cout << "Tuna swims real fast" << endl;
	}
};


int main()
{
	Tuna myDinner;
	
	cout << "About my food" << endl;
	
	// myDinner.Swim(false);//failure: Tuna::Swim() hides Fish::Swim(bool) 
	myDinner.Swim();
	
	return 0;
}

如果取消第 39 行的注释,VS 会直接报红。因为虽然 Tuna 以公有方式继承了 Fish,但是由于它实现了自己的 Swim() 函数,所以无法使用 Tuna 的实例调用 Swim(false)。

这里有以下解决方案:

  • 在 main( ) 中使用作用域解析运算符(::)调用基类中的 Swim() 函数:
c++ 复制代码
myDinner.Fish::Swim();
  • 在 Tuna 类中,使用关键字 using 解除对 Fish::Swim( )的隐藏:
c++ 复制代码
class Tuna: public Fish
{
public:
    using Fish::Swim; // unhide all Swim() methods in class Fish

    void Swim()
    {
        cout << "Tuna swims real fast" << endl;
    }
};
  • 在 Tuna 类中,覆盖 Fish::Swim() 的所有重载版本(如果需要,可以在重载函数中调用 Fish::Swim() 方法):
c++ 复制代码
class Tuna: public Fish
{
public:
    void Swim(bool isFreshWaterFish)
    {
        Fish::Swim(isFreshWaterFish);
    }

    void Swim()
    {
        cout << "Tuna swims real fast" << endl;
    }
};

构造和析构顺序

创建派生类对象时,基类对象在派生类对象之前被实例化。类的成员属性在构造函数之前实例化,这样就可以提供给构造函数使用。

析构函数的执行顺序则与构造函数的执行顺序相反。

私有继承

私有继承的示例代码如下:

c++ 复制代码
class Base
{
    // ... base class members and methods
};

class Derived: private Base // private inheritance
{
    // ... derived class members and methods
};

在 Derived 类中,Base 类的所有公有成员和方法都是私有的,不能从外部访问,这意味着,即便是 Base 类的公有成员和方法,也只能被 Derived 类使用,而无法通过 Derived 实例调用。

从继承层次结构外部看,私有继承并非 is-a 关系。私有继承使得只有子类才能使用基类的属性和方法,因此也被称为 has-a 关系。比如汽车(Car)有发动机(Motor):

c++ 复制代码
#include <iostream> 
using namespace std;

class Motor
{
public:
	void SwitchIgnition()
	{
		cout << "Ignition ON" << endl;
	}
	
	void PumpFuel()
	{
		cout << "Fuel in cylinders" << endl;
	 }
	
	void FireCylinders()
	{
		cout << "Vroooom" << endl;
	}
};

class Car :private Motor // private inheritance 
{
public:
	 void Move()
	{
		SwitchIgnition();
		PumpFuel();
		FireCylinders();
	}
};

int main()
{
	Car myDreamCar;
	myDreamCar.Move();
}

运行后打印如下:

vbnet 复制代码
Ignition ON
Fuel in cylinders
Vroooom
请按任意键继续. . .

如果在 main( )中插入下述代码:

scss 复制代码
myDreamCar.PumpFuel(); // cannot access base's public member

将无法通过编译,编译错误类似于下面这样:error C2247: Motor::PumpFuel not accessible because 'Car' uses 'private' to inherit from 'Motor'。

保护继承

保护继承的示例代码如下:

c++ 复制代码
class Base
{
    // ... base class members and methods
};

class Derived: protected Base // protected inheritance
{
    // ... derived class members and methods
};

保护继承与私有继承类似:

  • 它也表示 has-a 关系;
  • 它也让派生类能够访问基类的所有公有和保护成员;
  • 在继承层次结构外面,也不能通过派生类实例访问基类的公有成员。 随着继承层次结构的加深,保护继承将与私有继承有些不同,如果 Derived 还有派生类 Derived2:
c++ 复制代码
class Derived2: protected Derived
{
    // can access public & protected members of Base
};

Derived2 能够访问 Base 类的公有和保护成员,代码如下:

c++ 复制代码
#include <iostream> 
using namespace std;

class Motor
{
public:
	void SwitchIgnition()
	{
		cout << "Ignition ON" << endl;
	}
	
	void PumpFuel()
	{
		cout << "Fuel in cylinders" << endl;
	 }
	
	void FireCylinders()
	{
		cout << "Vroooom" << endl;
	}
};

class Car :protected Motor 
{
public:
	 void Move()
	{
		SwitchIgnition();
		PumpFuel();
		FireCylinders();
	}
};

class RaceCar :protected Car
{
public:
	void Move()
	{
		 SwitchIgnition(); // RaceCar has access to members of 
		 PumpFuel(); // base Motor due to "protected" inheritance 
		 FireCylinders(); // between RaceCar & Car, Car & Motor 
		 FireCylinders();
		 FireCylinders();
	}
};

int main()
{
	RaceCar myDreamCar;
	 myDreamCar.Move();
}

编译器根据最严格的访问限定符来确定访问权。注意,RaceCar 和 Car 之间的继承关系不会影响它对基类 Motor 公有成员的访问权,而 Car 和 Motor 之间的继承关系会。因此即便将第 34 行的 protected 改为 public 或 private,程序也都能正常编译运行。

注意,仅当必要时才使用私有或保护继承。对于大多数使用私有继承的情形(如 Car 和 Motor 之间的私有继承),更好的选择是,将基类对象作为派生类的一个成员属性。通过继承 Motor 类,相当于对 Car 类进行了限制,使其只能有一台发动机,同时,相比于将 Motor 对象作为私有成员,没有任何好处可言。汽车在不断发展,例如,混合动力车除电力发动机外,还有一台汽油发动机。在这种情况下,让 Car 类继承 Motor 类将成为兼容性瓶颈。

将 Motor 对象作为 Car 类的私有成员被称为组合(composition)或聚合(aggergation),这样的 Car 类类似于下面这样:

c++ 复制代码
class Car
{
private:
    Motor heartOfCar;

public:
    void Move()
    {
        heartOfCar.SwitchIgnition();
        heartOfCar.PumpFuel();
        heartOfCar.FireCylinders();
    }
};

这是一种不错的设计,让您能够轻松地在 Car 类中添加 Motor 成员,而无需改变继承层 次机构,也不用修改客户看到的设计。

切除问题

如果程序员像下面这样做,结果将如何呢?

示例 1 :

c++ 复制代码
Derived objDerived;
Base objectBase = objDerived;

示例 2 :

c++ 复制代码
void UseBase(Base input);
...
Derived objDerived;
UseBase(objDerived); // copy of objDerived will be sliced and sent

这两段代码都将派生类对象赋值给基类对象,编译器将只复制 objDerived 的 Base 部分,即不是整个对象。换句话说,Derived 的数据成员包含的信息将丢失。这种无意间裁减数据,导致 Derived 变成 Base 的行为称为切除(slicing)。

要避免切除问题,不要按值传递参数,而应以指向基类的指针或 const 引用的方式传递。

使用 final 禁止继承

从 C++11 起,编译器支持限定符 final。被声明为 final 的类不能用作基类。Platypus 类表示一种进化得很好的物种,因此您可能想将其声明为 final 的,从而禁止继承它。

c++ 复制代码
class Platypus final: public Mammal, public Bird, public Reptile
{
public:
    void Swim()
    {
        cout << "Platypus: Voila, I can swim!" << endl;
    }
};

多态

多态基础

看下面的代码:

c++ 复制代码
#include <iostream> 
using namespace std;

class Fish
{
public:
	void Swim()
	{
		cout << "Fish swims! " << endl;
	}
};

class Tuna :public Fish
{
public:
	// override Fish::Swim 
	void Swim()
	{
		cout << "Tuna swims!" << endl;
	}
};

void MakeFishSwim(Fish& inputFish)
{
	// calling Fish::Swim 
	inputFish.Swim();
 }

int main()
{
	Tuna myDinner;
	
	// calling Tuna::Swim 
	 myDinner.Swim();
	
	// sending Tuna as Fish 
	MakeFishSwim(myDinner);

	int num;
	cin >> num;
	
	return 0;
}

运行后打印如下:

复制代码
Tuna swims!
Fish swims!

Tuna 类以公有方式继承了 Fish 类并覆盖了父类的 Swim() 方法,但是在 MakeFishSwim() 方法中,虽然传入的是 Tuna 对象,MakeFishSwim(Fish&) 也将其视为 Fish,进而调用 Fish::Swim,并输出了 Fish swims!。这显然不是我们想要的结果,如果我们想要根据传入的 Tuna 对象执行对应的 Tuna::Swim(),需要将基类 Fish 中的 Swim() 声明为虚函数:

c++ 复制代码
class Fish
{
public:
    virtual void Swim()
    {
        cout << "Fish swims!" << endl;
    }
};

通过使用关键字 virtual,可确保编译器调用覆盖版本。将 Fish::Swim() 声明为虚函数后打印如下:

复制代码
Tuna swims!
Tuna swims!

因为存在覆盖版本的 Tuna::Swim(),它优于被声明为虚函数的 Fish::Swim()。这很重要,它意味着在 MakeFishSwim() 中,可通过 Fish& 参数调用派生类定义的 Swim(),而无需知道该参数指向的是哪种类型的对象。

这就是多态:将派生类对象视为基类对象,并执行派生类的 Swim() 实现。

为何需要虚构造函数

如果基类指针指向的是派生类对象,通过该指针调用运算符 delete 时,将调用哪个析构函数呢?看下面的代码:

c++ 复制代码
class Fish
{
public:
	Fish()
	{
		cout << "Constructed Fish" << endl;
	}
	~Fish()
	{
		cout << "Destroyed Fish" << endl;
	}
};

class Tuna :public Fish
{
public:
	Tuna()
	{
		cout << "Constructed Tuna" << endl;
	}
	~Tuna()
	{
		cout << "Destroyed Tuna" << endl;
	}
};

void DeleteFishMemory(Fish* pFish)
{
	delete pFish;
}

int main()
{
	cout << "Allocating a Tuna on the free store:" << endl;
	Tuna* pTuna = new Tuna;
	cout << "Deleting the Tuna: " << endl;
	DeleteFishMemory(pTuna);
	
  cout << "Instantiating a Tuna on the stack:" << endl;
	Tuna myDinner;
	cout << "Automatic destruction as it goes out of scope: " << endl;
	
	return 0;
}

运行后打印如下:

csharp 复制代码
Allocating a Tuna on the free store:
Constructed Fish
Constructed Tuna
Deleting the Tuna:
Destroyed Fish
Instantiating a Tuna on the stack:
Constructed Fish
Constructed Tuna
Automatic destruction as it goes out of scope:
Destroyed Tuna
Destroyed Fish

从打印可以看出,使用 new 在自由存储区中实例化的派生类对象,如果将其赋给基类指针,并通过该指针调用 delete,将不会调用派生类的析构函数。这可能导致资源未释放、内存泄露等问题,要避免这种问题,可将基类 Fish 的析构函数声明为虚函数:

c++ 复制代码
class Fish
{
public:
	Fish()
	{
		cout << "Constructed Fish" << endl;
	}
	virtual ~Fish() // virtual destructor!
	{
		cout << "Destroyed Fish" << endl;
	}
};

重新运行后打印如下:

csharp 复制代码
Allocating a Tuna on the free store:
Constructed Fish
Constructed Tuna
Deleting the Tuna:
Destroyed Tuna
Destroyed Fish
Instantiating a Tuna on the stack:
Constructed Fish
Constructed Tuna
Automatic destruction as it goes out of scope:
Destroyed Tuna
Destroyed Fish

这样修改后,将运算符 delete 用于 Fish 指针时,如果该指针指向的是 Tuna 对象,则编译器不仅会执行 Fish::~Fish(),还会执行 Tuna::~Tuna()。输出还表明,Tuna 对象使用 new 在自由存储区中实例化,和以局部变量的方式在栈中实例化,构造函数和析构函数的调用顺序相同。

使用虚继承解决菱形问题

鸭嘴兽具备哺乳动物、鸟类和爬行动物的特征,这意味着 Platypus 类需要继承Mammal、Bird 和 Reptile。然而,这些类都从同一个类---Animal 派生而来,代码如下:

c++ 复制代码
#include <iostream> 
using namespace std;

class Animal
{
public:
	Animal()
	{
		cout << "Animal constructor" << endl;
	}
	
	 // sample member 
	int age;

};

class Mammal :public Animal
{
};

class Bird :public Animal
{
};

class Reptile :public Animal
{
};

class Platypus :public Mammal, public Bird, public Reptile
{
public:
	Platypus()
	{
		cout << "Platypus constructor" << endl;
	}
};

int main()
{
	Platypus duckBilledP;
	
	// uncomment next line to see compile failure 
	// age is ambiguous as there are three instances of base Animal 
	// duckBilledP.age = 25; 
	
	return 0;
}

运行后打印如下:

kotlin 复制代码
Animal constructor
Animal constructor
Animal constructor
Platypus constructor

从打印可以看到创建了三个 Animal 实例,这样不仅会占用更多的内存,如果你试图通过 Platypus 的实例访问 Animal 中的 age 参数,如上面的代码第 44 行,编译器会报错:

因为这里创建了 Mammal、Bird 和 Reptile 三个对象,这三个对象都继承 Animal 基类,编译器不知道应该访问这三个派生对象中的哪一个继承的 Animal 中的 age。

要解决这个问题,可以使用虚继承:

c++ 复制代码
class Mammal : virtual public Animal  
{
};

class Bird : virtual public Animal   
{
};

class Reptile : virtual public Animal 
{
};

运行后打印如下:

kotlin 复制代码
Animal constructor
Platypus constructor

可以看到只会创建一个实例,通过 Platypus 的实例访问 Animal 中的 age 参数也不会报错了,因为 age 不再存在二义性了。

在继承层次结构中,继承多个从同一个类派生而来的基类时,如果这些基类没有采用虚 继承,将导致二义性。这种二义性被称为菱形问题(Diamond Problem)。其中的"菱形"可能源自类图的形状(如果使用直线和斜线表示 Platypus 经由 Mammal、Bird 和 Reptile 与 Animal 建立的关系,将形成一个菱形)。

override

看下面的代码:

c++ 复制代码
class Fish
{
public:
	virtual void Swim()
	{
		cout << "Fish swims!" << endl;
	}
};

class Tuna :public Fish
{
public:
	void Swim() const
	{
		cout << "Tuna swims!" << endl;
	}
};

这里 Tuna 中的 Swim() 实际并不会覆盖 Fish 中的 Swim(),因为Tuna::Swim()包含 const,导致它们的特征标不同。从 C++11 开始,可以通过使用限定符 override 来校验被覆盖的函数在基类中是否被声明为虚函数,这样编译器会做如下检查:

  • 基类函数是否是虚函数?
  • 基类中相应虚函数的特征标是否与派生类中被声明为 override 的函数完全相同?

如果检查不通过编译器会报错。

使用 final 禁止覆盖函数

被声明为 final 的类不能用作基类,同样,被声明为 final 的虚函数,不能在派生类中覆盖。

c++ 复制代码
#include <iostream> 
using namespace std;

class Fish
{
public:
	virtual void Swim()
	{
		cout << "Fish swims!" << endl;
	}
};

class Tuna :public Fish
{
public:
	void Swim() override final
	{
		cout << "Tuna swims!" << endl;
	}
};

class BluefinTuna final :public Tuna
{
public:
	void Swim() // Error: Swim() was final in Tuna, cannot override 
	{
	}
};

上面的代码第 25 行会报错。

模拟虚拷贝构造函数

在 C++ 中,构造函数(包括拷贝构造函数)不能是虚函数,因为虚函数的存在是为了实现运行时多态,它依赖于对象的动态类型(即最终创建的类型)。虚函数机制需要对象已构造完成,而构造函数的职责是创建对象。

但是我们可以定义自己的 Clone() 函数来实现与拷贝构造函数相同的功能:

c++ 复制代码
class Fish
{
public:
    virtual Fish* Clone() const = 0; // pure virtual function
};

class Tuna:public Fish
{
// ... other members
public:
    Tuna * Clone() const // virtual clone function
    {
        return new Tuna(*this); // return new Tuna that is a copy of this
    }
};

代码示例:

c++ 复制代码
#include <iostream> 
using namespace std;

class Fish
{
public:
	virtual Fish* Clone() = 0;
	virtual void Swim() = 0;
	virtual ~Fish() {};
};

class Tuna : public Fish
{
public:
	 Fish* Clone() override
	{
		return new Tuna(*this);
	}
	
	void Swim() override final
	{
		cout << "Tuna swims fast in the sea" << endl;
	}
};

class BluefinTuna final :public Tuna
{
public:
	Fish* Clone() override
	{
		return new BluefinTuna(*this);
	}
	
	// Cannot override Tuna::Swim as it is "final" in Tuna 
};

class Carp final : public Fish
{
	Fish* Clone() override
	{
		return new Carp(*this);
	}
	void Swim() override final
	{
		cout << "Carp swims slow in the lake" << endl;
	}
};


int main()
{
	const int ARRAY_SIZE = 4;
	
	Fish* myFishes[ARRAY_SIZE] = { NULL };
	myFishes[0] = new Tuna();
	myFishes[1] = new Carp();
	myFishes[2] = new BluefinTuna();
	myFishes[3] = new Carp();
	
	Fish* myNewFishes[ARRAY_SIZE];
	for (int index = 0; index < ARRAY_SIZE; ++index)
		myNewFishes[index] = myFishes[index]->Clone();
	
	// invoke a virtual method to check 
	for (int index = 0; index < ARRAY_SIZE; ++index)
		myNewFishes[index]->Swim();
	
	 // memory cleanup 
	for (int index = 0; index < ARRAY_SIZE; ++index)
	{
		delete myFishes[index];
		delete myNewFishes[index];
	}
	
	return 0;
}

打印如下:

复制代码
Tuna swims fast in the sea
Carp swims slow in the lake
Tuna swims fast in the sea
Carp swims slow in the lake

运算符

单目运算符

实现为全局函数或静态成员函数的单目运算符的典型定义如下:

c++ 复制代码
return_type operator operator_type (parameter_type)
{
    // ... implementation
}

作为类成员(非静态函数)的单目运算符没有参数,因为它们使用的唯一参数是当前类实例(*this),如下所示:

c++ 复制代码
return_type operator operator_type ()
{
    // ... implementation
}

如下是一个简单的 Date 类,通过使用运算符 ++ 和 -- 可以对日期进行递增递减:

c++ 复制代码
#include <iostream> 
using namespace std;

class Date
{
private:
	int day, month, year;
	
public:
	Date(int inMonth, int inDay, int inYear)
		: month(inMonth), day(inDay), year(inYear) {};
	
	Date& operator ++ () // prefix increment 
	{
		++day;
		return *this;
	}
	
	Date& operator -- () // prefix decrement 
	{
		--day;
		return *this;
	}
	
	
	void DisplayDate()
	{
		cout << month << " / " << day << " / " << year << endl;
	}
};

int main()
{
	Date holiday(12, 25, 2016); // Dec 25, 2016 
	
	cout << "The date object is initialized to: ";
	holiday.DisplayDate();
	
	++holiday; // move date ahead by a day 
	cout << "Date after prefix-increment is: ";
	holiday.DisplayDate();
	
	--holiday; // move date backwards by a day 
	cout << "Date after a prefix-decrement is: ";
	holiday.DisplayDate();
	
	return 0;
}

运行后打印如下:

vbnet 复制代码
The date object is initialized to: 12 / 25 / 2016
Date after prefix-increment is: 12 / 26 / 2016
Date after a prefix-decrement is: 12 / 25 / 2016

也可以使用后缀递增和递减运算符:

C++ 复制代码
// postfix differs from prefix operator in return-type and parameters
Date operator ++ (int) // postfix increment
{
    Date copy(month, day, year);
    ++day;
    return copy; // copy of instance before increment returned
}

Date operator -- (int) // postfix decrement
{
    Date copy(month, day, year); 
    --day;
    return copy; // copy of instance before decrement returned
}

使用方法如下:

ini 复制代码
holiday ++;
holiday --;

在上述后缀运算符的实现中,首先复制了当前对象,再将对当前对象执行递增或递减运算,最后返回复制的对象。换句话说,如果只想执行递增运算,可使用++ object,也可使用 object ++,但应选择前者,这样避免创建一个未被使用的临时拷贝。

转换运算符

如果在上面的 main() 函数中添加如下代码:

bash 复制代码
cout << holiday;

会导致编译错误::error: binary '<<' : no operator found which takes a right-hand operand of type 'Date' (or there is no acceptable conversion)。这种错误表明,cout 不知道如何解读 Date 实例,因为 Date 类不支持这样的运算符,即将 Date 对象的内容转换成 cout 能够接受的类型。然而,cout 能够很好地显示 const char *:

c 复制代码
std::cout << "Hello world";

此时可以在 Date 类中实现转换运算符 const char*:

c++ 复制代码
#include <iostream>
#include <sstream> // new include for ostringstream 
#include <string>
using namespace std;

class Date
{
private:
	int day, month, year;
	string dateInString;
	
 public:
	Date(int inMonth, int inDay, int inYear)
		: month(inMonth), day(inDay), year(inYear) {};
	
	operator const char*()
	{
		ostringstream formattedDate; // assists string construction 
		formattedDate << month << " / " << day << " / " << year;
		
		dateInString = formattedDate.str();
		return dateInString.c_str();
	 }
};

int main()
{
	Date Holiday(12, 25, 2016);
	
	cout << "Holiday is on: " << Holiday << endl;
	
	// string strHoliday (Holiday); // OK! 
	// strHoliday = Date(11, 11, 2016); // also OK! 
	
	return 0;
}

如上所示,实现了将 Date 转换为 const char* 的运算符,这样就可以直接使用 cout 来输出 Holiday 了。在转换为 const char* 的运算符中,使用 std::ostringstream 将整型成员转换成了一个 std::string 对象,原本也可直接返回 formattedDate.str( ),但没有这样做,而将其拷贝存储在私有成员 Date::dateInString 中,这是因为 formattedDate 是一个局部变量,将在运算符返回时被销毁,因此运算符返回时,通过 str() 获得的指针将无效。

这个运算符让您能够以新的方式使用 Date 类。现在,您甚至可以将 Date 对象直接赋给 string 对象:

ini 复制代码
string strHoliday (holiday);
strHoliday = Date(11, 11, 2016);

请注意,这样的赋值将导致隐式转换,即为了让赋值通过编译而不引发错误,编译器使用了可用的转换运算符(这里为 const char*)。为了禁止隐式转换,可在运算符声明开头使用关键字 explicit,如下所示:

c++ 复制代码
explicit operator const char*()
{
    // conversion code here
}

通过使用关键字 explicit,可要求程序员使用强制类型转换来确认转换意图:

arduino 复制代码
string strHoliday(static_cast<const char*>(Holiday));
strHoliday=static_cast<const char*>(Date(11,11,2016));

强制类型转换(包括 static-cast)将在后续讨论。

解除引用运算符(*)和成员选择运算符(->)

解除引用运算符(*)和成员选择运算符(->)在智能指针类编程中应用最广。智能指针是封装常规指针的类,旨在通过管理所有权和复制问题简化内存管理。在有些情况下,智能指针甚至能够提高应用程序的性能。智能指针将在后续讨论,这里只简要地介绍如何重载运算符,以帮助智能指针完成其工作。

c++ 复制代码
#include <iostream> 
#include <memory> // new include to use unique_ptr 
using namespace std;

class Date
{
private:
	int day, month, year;
	string dateInString;
	
public:
   Date(int inMonth, int inDay, int inYear)
		: month(inMonth), day(inDay), year(inYear) {};

   void DisplayDate()
   {
		cout << month << " / " << day << " / " << year << endl;
	}
};


int main()
{
	unique_ptr<int> smartIntPtr(new int);
	*smartIntPtr = 42;
	
	// Use smart pointer type like an int* 
	 cout << "Integer value is: " << *smartIntPtr << endl;
	
	unique_ptr<Date> smartHoliday(new Date(12, 25, 2016));
	cout << "The new instance of date contains: ";
	
	// use smartHoliday just as you would a Date* 
	smartHoliday->DisplayDate();
	
	return 0;
}

第 24 行声明了一个指向 int 的智能指针,它演示了智能指针类 unique_ptr 的模板初始化语法。同样,第 30 行声明了一个指向 Date 对象的智能指针。这里的重点是模式,请暂时不要考虑细节。

这个示例表明,可像使用普通指针那样使用智能指针,如第 25 和 34 行所示。第 25 行使用了smartIntPtr 来显示指向的 int 值,而第 34 行使用了 smartHoliday->DisplayData(),就像这两个变量的类型分别是 int 和 Date*。其中的秘诀在于,智能指针类 std::unique_ptr 实现了运算符 * 和 -> 。

双目运算符

对两个操作数进行操作的运算符称为双目运算符。以全局函数或静态成员函数的方式实现的双目运算符的定义如下:

arduino 复制代码
return_type operator_type (parameter1, parameter2);

以类成员的方式实现的双目运算符的定义如下:

arduino 复制代码
return_type operator_type (parameter);

以类成员的方式实现的双目运算符只接受一个参数,其原因是第二个参数通常是从类属性获得的。

双目加法与双目减法运算符

如果类实现了双目加法和双目减法运算符,便可将其对象加上或减去指定类型的值。再来看看日历类 Date,虽然前面实现了将 Date 递增以便前移一天的功能,但它还不支持增加 5 天的功能。为了实现这种功能,需要实现双目加法运算符:

sql 复制代码
#include <iostream> 
using namespace std;

class Date
{
private:
	 int day, month, year;
	 string dateInString;
	 
public:
	Date(int inMonth, int inDay, int inYear)
		: month(inMonth), day(inDay), year(inYear) {};
	
	Date operator + (int daysToAdd) // binary addition 
	{
		Date newDate(month, day + daysToAdd, year);
		return newDate;
	}

	Date operator - (int daysToSub) // binary subtraction 
	{
		return Date(month, day - daysToSub, year);
    }

	void DisplayDate()
	{
		cout << month << " / " << day << " / " << year << endl;
	}
};

int main()
{
	Date Holiday(12, 25, 2016);
	cout << "Holiday on: ";
	Holiday.DisplayDate();
	
	Date PreviousHoliday(Holiday - 19);
	cout << "Previous holiday on: ";
	PreviousHoliday.DisplayDate();
	
	Date NextHoliday(Holiday + 6);
	cout << "Next holiday on: ";
	NextHoliday.DisplayDate();
	
	return 0;
}

对字符串类来说,双目加法运算符也很有用。前面分析了简单的字符串包装类 MyString,它提供了内存管理、复制等功能。但这个 MyString 类不支持使用如下简单语法将两个字符串拼接起来:

scss 复制代码
MyString Hello("Hello ");
MyString World(" World");
MyString HelloWorld(Hello + World); // error: operator+ not defined

定义运算符 + 后,MyString 使用起来将非常容易,值得去实现它:

c++ 复制代码
MyString operator+ (const MyString& addThis)
{
    MyString newString;
    if (addThis.buffer != NULL)
    {
          newString.buffer = new char[GetLength() + strlen(addThis.buffer) + 1];
          strcpy(newString.buffer, buffer);
          strcat(newString.buffer, addThis.buffer);
    }
    return newString;
}

实现运算符 += 与 -=

加并赋值运算符支持语法 a += b;,这让程序员可将对象 a 增加 b。如下代码可以给 Date 对象加上一个整数:

c++ 复制代码
#include <iostream> 
using namespace std;

class Date
{
private:
	int day, month, year;
	
public:
	Date(int inMonth, int inDay, int inYear)
			 : month(inMonth), day(inDay), year(inYear) {}
	
	void operator+= (int daysToAdd) // addition assignment 
	{
		day += daysToAdd;
	}
	
	void operator-= (int daysToSub) // subtraction assignment 
	{
		day -= daysToSub;
	}
	
	void DisplayDate()
	{
		cout << month << " / " << day << " / " << year << endl;
	}
};


int main()
{
	Date holiday(12, 25, 2016);
	cout << "holiday is on: ";
	holiday.DisplayDate();
	
	cout << "holiday -= 19 gives: ";
	holiday -= 19;
	holiday.DisplayDate();
	
	cout << "holiday += 25 gives: ";
	holiday += 25;
	holiday.DisplayDate();
	
	return 0;
}

运算符 += 和 -= 接受一个 int 参数,让您能够给 Date 对象加上或减去指定的天数,就像处理的是整数一样。您还可提供运算符 += 的重载版本,让它接受一个虚构的 Days 对象作为参数:

arduino 复制代码
// operator that adds a Days to an existing Date
void operator += (const Days& daysToAdd)
{
    day += daysToAdd.GetDays();
}

重载等于运算符(==)和不等运算符(!=)

如果像下面这样将两个 Date 对象进行比较,结果将如何呢?

perl 复制代码
if (date1 == date2)
{
    // Do something
}
else
{
    // Do something else
}

由于还没有定义等于运算符(==),编译器将对这两个对象进行二进制比较,并仅当它们完全相同时才返回 true。对于包含简单数据类型的类(如现在的 Date 类),这种二进制比较是可行的。然而,如果类有一个非静态字符串成员,它包含字符串值(char *),比如前面的 MyString,则比较结果可能不符合预期。在这种情况下,对成员属性进行二进制比较时,实际上将比较字符串指针(MyString::buffer),而字符串指针并不相等(即使指向的内容相同),因此总是返回 false。为了解决这种问题,可定义比较运算符。等于运算符的通用实现如下:

vbnet 复制代码
bool operator== (const ClassType& compareTo)
{
    // comparison code here, return true if equal else false
}

实现不等运算符时,可重用等于运算符:

vbnet 复制代码
bool operator!= (const ClassType& compareTo)
{
    // comparison code here, return true if inequal else false
}

不等运算符的结果与等于运算符相反(逻辑非)。

下面的代码列出了日历类 Date 定义的比较运算符:

c 复制代码
#include <iostream> 
using namespace std;

class Date
{
private:
	int day, month, year;
	
public:
	Date(int inMonth, int inDay, int inYear)
			: month(inMonth), day(inDay), year(inYear) {}

	 bool operator== (const Date& compareTo)
	 {
			return ((day == compareTo.day)
				&& (month == compareTo.month)
				 && (year == compareTo.year));
	 }

	 bool operator!= (const Date& compareTo)
	 {
			return !(this->operator==(compareTo));
	 }

	 void DisplayDate()
	{
		  cout << month << " / " << day << " / " << year << endl;
	}
};

int main()
{
	Date holiday1(12, 25, 2016);
	Date holiday2(12, 31, 2016);
	
	cout << "holiday 1 is: ";
	holiday1.DisplayDate();
	cout << "holiday 2 is: ";
	holiday2.DisplayDate();
	
	if (holiday1 == holiday2)
		cout << "Equality operator: The two are on the same day" << endl;
    else
		cout << "Equality operator: The two are on different days" << endl;
	
	if (holiday1 != holiday2)
		cout << "Inequality operator: The two are on different days" << endl;
	else
		cout << "Inequality operator: The two are on the same day" << endl;
	
	return 0;
}

运行后打印如下:

csharp 复制代码
holiday 1 is: 12 / 25 / 2016
holiday 2 is: 12 / 31 / 2016
Equality operator: The two are on different days
Inequality operator: The two are on different days

等于运算符(==)的实现很简单,它在年、月、日都相同时返回 true,如第 12~17 行所示。实现不等运算符时,重用了等于运算符的代码。

重载拷贝赋值运算符(=)

有时候,需要将一个类实例的内容赋给另一个类实例,如下所示:

ini 复制代码
Date holiday(12, 25, 2016);
Date anotherHoliday(1, 1, 2017);
anotherHoliday = holiday; // uses copy assignment operator

如果你没有定义拷贝赋值运算符,编译器将调用默认的拷贝赋值运算符。根据类的特征,默认拷贝赋值运算符可能不可行,具体地说是它不拷贝类管理的资源,默认拷贝赋值运算符存在的这种问题与前面讨论的默认拷贝构造函数存在的问题类似。与拷贝构造函数一样,为确保进行深拷贝,您需要提供拷贝赋值运算符:

c++ 复制代码
ClassType& operator= (const ClassType& copySource)
{
    if(this != &Source) // protection against copy into self
    {
        // copy assignment operator implementation
    }
    return *this;
}

如果类封装了原始指针,比如前面的 MyString 类,则确保进行深拷贝很重要。为确保拷贝时进行深拷贝,应定义拷贝赋值运算符,代码如下:

c++ 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include <iostream> 
using namespace std;
#include <string.h>

class MyString
{
private:
	char* buffer;
	
	public:
		MyString(const char* initialInput)
		{
			if (initialInput != NULL)
			{
				buffer = new char[strlen(initialInput) + 1];
				strcpy(buffer, initialInput);

			}
			else
				buffer = NULL;
		}

		// Copy assignment operator 
		 MyString& operator= (const MyString& copySource)
		{
			if ((this != &copySource) && (copySource.buffer != NULL))
			{
				if (buffer != NULL)
					delete[] buffer;
		
				 // ensure deep copy by first allocating own buffer 
				buffer = new char[strlen(copySource.buffer) + 1];
				
				// copy from the source into local buffer 
				strcpy(buffer, copySource.buffer);
			}
			
			return *this;
		}

		operator const char*()
		{
			return buffer;
		}
			
		~MyString()
		{
			delete[] buffer;
		}
};

int main()
{
  MyString string1("Hello ");
	MyString string2(" World");
	
	cout << "Before assignment: " << endl;
	cout << string1 << string2 << endl;
	string2 = string1;
	cout << "After assignment string2 = string1: " << endl;
	cout << string1 << string2 << endl;
	
	return 0;
}

运行后打印如下:

ini 复制代码
Before assignment:
Hello World 250
After assignment string2 = string1:
Hello Hello

在这个示例中故意省略了拷贝构造函数,旨在减少代码行,但您编写这样的类时,应添加它。拷贝赋值运算符的实现与拷贝构造函数很像,它首先检查源和目标是否同一个对象。如果不是,则释放成员 buffer 占用的内存,再重新给它分配足以存储拷贝源中文本的内存,然后使用 strcpy() 进行拷贝。

要创建不允许拷贝的类,可将拷贝构造函数和拷贝赋值运算符都声明为私有的。只需这样声明(而不提供实现)就足以让编译器在遇到试图拷贝对象(将对象按值传递给函数或将一个对象赋给另一个对象)的代码时引发错误。

下标运算符

下标运算符让您能够像访问数组那样访问类,其典型语法如下:

ini 复制代码
return_type& operator [] (subscript_type& subscript);

如下代码演示了下标运算符([])让用户能够使用常规数组语法来遍历 MyString 实例包含的字符:

c++ 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include <iostream> 
#include <string>
#include <string.h>
using namespace std;

class MyString
{
private:
	char* buffer;

	// private default constructor 
	MyString() {}

public:
	// constructor 
	MyString(const char* initialInput)
	{
		if (initialInput != NULL)
		{
			buffer = new char[strlen(initialInput) + 1];
			strcpy(buffer, initialInput);
		}
		else
			buffer = NULL;
	}
		
	// Copy constructor
	MyString(const MyString& copySource);
		
	// Copy assignment operator
	MyString& operator= (const MyString& copySource);
		
	const char& operator[] (int index) const
	{
		if (index < GetLength())
			return buffer[index];
	}
			
	// Destructor 
	~MyString()
	{
		if (buffer != NULL)
			delete[] buffer;
	}
			
	int GetLength() const
	{
		return strlen(buffer);
	}
				
	operator const char*()
	{
		return buffer;
	}
};

int main()
{
	cout << "Type a statement: ";
	string strInput;
	getline(cin, strInput);
	
	MyString youSaid(strInput.c_str());
	
	cout << "Using operator[] for displaying your input: " << endl;
	for (int index = 0; index < youSaid.GetLength(); ++index)
		cout << youSaid[index] << " ";
	cout << endl;
	
	cout << "Enter index 0 - " << youSaid.GetLength() - 1 << ": ";
	int index = 0;
	cin >> index;
	cout << "Input character at zero-based position: " << index;
	cout << " is: " << youSaid[index] << endl;
	
	return 0;
}

输出:

less 复制代码
Type a statement: It's a sunny day!
Using operator[] for displaying your input:
I t ' s   a   s u n n y   d a y !
Enter index 0 - 16: 5
Input character at zero-based position: 5 is: a

实现运算符时,应使用关键字 const,这很重要。在上面的代码中,将下标运算符([])的返回类型声明成了 const char&。即便没有关键字 const,该程序也能通过编译。这里使用它旨在禁止使用下面这样的代码:

arduino 复制代码
MyString sayHello("Hello World");
sayHello[2] = 'k'; //error: operator[] is const

通过使用 const,可禁止从外部通过运算符[]直接修改成员 MyString::buffer。除将返回类型声明为 const 外,还将该运算符的函数类型设置成为 const,这将禁止它修改类的成员属性。

一般而言,应尽可能使用 const,以免无意间修改数据,并最大限度地保护类的成员属性。

也可实现两个下标运算符,其中一个为 const 函数,另一个为非 const 函数:

arduino 复制代码
char& operator [] (int index); // use to write / change buffer at index
char& operator [] (int index) const; // used only for accessing char at index

编译器将在读取 MyString 对象时调用 const 函数,而在对 MyString 执行写入操作时调用非 const 函数。因此,如果愿意,可在两个下标函数中实现不同的功能。

函数运算符 operator()

operator()让对象像函数,被称为函数运算符。函数运算符用于标准模板库(STL)中,通常是 STL 算法中,其用途包括决策。根据使用的操作数数量,这样的函数对象通常称为单目谓词或双目谓词。

下面是一个使用 operator() 实现的函数对象:

c++ 复制代码
#include <iostream> 
#include <string>
using namespace std;

class Display
{
public:
	 void operator () (string input) const
	{
		cout << input << endl;
	}
};

int main()
{
	Display displayFuncObj;
	
	// equivalent to displayFuncObj.operator () ("Display this string! "); 
	displayFuncObj("Display this string! ");
	
	return 0;
}

输出:

csharp 复制代码
Display this string!

之所以能够在第 19 行将对象 displayFuncObj 用作函数,是因为编译器隐式地将它转换为对函数 operator()的调用。因此,这个运算符也称为 operator() 函数,而 Display 对象也称为函数对象或 functor。

用于高性能编程的移动构造函数和移动赋值运算符

下面的代码让你能够使用双目加法运算符(+)轻松地将三个字符串拼接起来:

c++ 复制代码
MyString operator+ (const MyString& addThis)
{
    MyString newStr;
    if (addThis.buffer != NULL)
    {
        // copy into newStr
    }
    return newStr; // return copy by value, invoke copy constructor
}

int main()
{
    MyString Hello("Hello ");
    MyString World("World");
    MyString CPP(" of C++");

    MyString sayHello(Hello + World + CPP); // operator+, copy constructor
    MyString sayHelloAgain ("overwrite this");
    sayHelloAgain = Hello + World + CPP; // operator+, copy constructor, copy assignment operator=
}

但这个加法运算符也可能导致性能问题,创建 sayHello 时,需要执行加法运算符两次,而每次都将创建一个按值返回的临时拷贝,导致执行拷贝构造函数。拷贝构造函数执行深拷贝,而生成的临时拷贝在该表达式执行完毕后就不再存在。这一直是 C++带来的性能瓶颈,直到最近才得以解决。

C++11 解决了这个问题:编译器意识到需要创建临时拷贝时,将转而使用移动构造函数和移动赋值运算符---如果您提供了它们。

移动构造函数的声明语法如下:

c++ 复制代码
class Sample
{
private:
    Type* ptrResource;

public:
    Sample(Sample&& moveSource) // Move constructor, note &&
    {
        ptrResource = moveSource.ptrResource; // take ownership, start move
        moveSource.ptrResource = NULL;
    }

    Sample& operator= (Sample&& moveSource)//move assignment operator, note &&
    {
        if(this != &moveSource)
        {
            delete [] ptrResource; // free own resource
            ptrResource = moveSource.ptrResource; // take ownership, start move
            moveSource.ptrResource = NULL; // free move source of ownership
        }
    }

    Sample(); // default constructor

    Sample(const Sample& copySource); // copy constructor

    Sample& operator= (const Sample& copySource); // copy assignment
};

移动构造函数和移动赋值运算符的不同之处在于,输入参数的类型为 Sample&&。另外,由于输入参数是要移动的源对象,因此不能使用 const 进行限定,因为它将被修改。返回类型没有变,因为它们分别是构造函数和赋值运算符的重载版本。

在需要创建临时右值时,遵循 C++ 的编译器将使用移动构造函数(而不是拷贝构造函数)和移动赋值运算符(而不是拷贝赋值运算符)。移动构造函数和移动赋值运算符的实现中,只是将资源从源移到目的地,而没有进行拷贝。

除拷贝构造函数和拷贝赋值运算符外,还包含移动构造函数和移动赋值运算符的 MyString 类代码如下:

c++ 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include <iostream> 
#include <string.h>
using namespace std;
class MyString
{
private:
	char* buffer;
	
	 MyString() : buffer(NULL) // private default constructor 
	{
		cout << "Default constructor called" << endl;
	}

public:
	MyString(const char* initialInput) // constructor 
	{
		cout << "Constructor called for: " << initialInput << endl;
		if (initialInput != NULL)
		{
			buffer = new char[strlen(initialInput) + 1];
			strcpy(buffer, initialInput);
		}
		else
			buffer = NULL;
	}

	MyString(MyString&& moveSrc) // move constructor 
	{
		cout << "Move constructor moves: " << moveSrc.buffer << endl;
		if (moveSrc.buffer != NULL)
		{
			buffer = moveSrc.buffer; // take ownership i.e. 'move' 
			moveSrc.buffer = NULL; // free move source 
		}
	}

	MyString& operator= (MyString&& moveSrc) // move assignment op. 
	{
		cout << "Move assignment op. moves: " << moveSrc.buffer << endl;
		if ((moveSrc.buffer != NULL) && (this != &moveSrc))
		{
			delete[] buffer; // release own buffer 

			buffer = moveSrc.buffer; // take ownership i.e. 'move' 
			moveSrc.buffer = NULL; // free move source 
		}
			
		return *this;
	}

	MyString(const MyString& copySrc) // copy constructor 
	{
		cout << "Copy constructor copies: " << copySrc.buffer << endl;
		if (copySrc.buffer != NULL)
		{
			buffer = new char[strlen(copySrc.buffer) + 1];
			strcpy(buffer, copySrc.buffer);
		}
		else
			buffer = NULL;
	}

	MyString& operator= (const MyString& copySrc) // Copy assignment op. 
	{
		cout << "Copy assignment op. copies: " << copySrc.buffer << endl;
		if ((this != &copySrc) && (copySrc.buffer != NULL))
		{
			if (buffer != NULL)
				delete[] buffer;

			buffer = new char[strlen(copySrc.buffer) + 1];
			strcpy(buffer, copySrc.buffer);
		}
	
		return *this;
	}

	~MyString() // destructor 
	{
		if (buffer != NULL)
			delete[] buffer;
	}
		
	int GetLength()
	{
		return strlen(buffer);
	}

	operator const char*()
	{
		return buffer;
	}

	MyString operator+ (const MyString& addThis)
	{
		cout << "operator+ called: " << endl;
		MyString newStr;
		
		if (addThis.buffer != NULL)
		{
			newStr.buffer = new char[GetLength() + strlen(addThis.buffer) + 1];
			strcpy(newStr.buffer, buffer);
			strcat(newStr.buffer, addThis.buffer);
		}
			
		return newStr;
	}
};

int main()
{
	MyString Hello("Hello ");
	MyString World("World");
	MyString CPP(" of C++");
	
	MyString sayHelloAgain("overwrite this");
	sayHelloAgain = Hello + World + CPP;
	
	return 0;
}

没有移动构造函数和移动赋值构造函数(将第 28~50 行注释掉)时的输出:

sql 复制代码
Constructor called for: Hello
Constructor called for: World
Constructor called for: of C++
Constructor called for: overwrite this
operator+ called:
Default constructor called
Copy constructor copies: Hello World
operator+ called:
Default constructor called
Copy constructor copies: Hello World of C++
Copy assignment op. copies: Hello World of C++

添加移动构造函数和移动赋值构造函数后的输出:

sql 复制代码
Constructor called for: Hello
Constructor called for: World
Constructor called for:  of C++
Constructor called for: overwrite this
operator+ called:
Default constructor called
Move constructor moves: Hello World
operator+ called:
Default constructor called
Move constructor moves: Hello World of C++
Move assignment op. moves: Hello World of C++

不同于拷贝构造函数和拷贝赋值运算符,如果您没有提供移动构造函数和移动赋值运算符,编译器并不会添加默认实现。对于管理动态分配资源的类,可使用这项功能对其进行优化,避免在只需临时拷贝的情况下进行深拷贝。

用户定义的字面量

C++ 增大了对字面量的支持力度,让您能够自定义字面量。例如,编写热力学方面的科学应用程序时,对于所有的温度,您都可能想以卡尔文为单位来存储和操作它们。为此,您可使用类似于下面的语法来声明所有的温度:

ini 复制代码
Temperature k1 = 32.15_F;
Temperature k2 = 0.0_C;

通过使用自定义的字面量_F 和 C_,您让应用程序更容易理解和维护得多。要自定义字面量,可像下面这样定义 operator "":

arduino 复制代码
ReturnType operator "" YourLiteral(ValueType value)
{
    // conversion code here
}

下面的代码将华氏温度和摄氏温度转换为开尔文温度:

c++ 复制代码
#include <iostream> 
using namespace std;

struct Temperature
{
	double Kelvin;
	Temperature(long double kelvin) : Kelvin(kelvin) {}
};

Temperature operator"" _C(long double celcius)
{
	return Temperature(celcius + 273);
}

Temperature operator "" _F(long double fahrenheit)
{
	return Temperature((fahrenheit + 459.67) * 5 / 9);
}


int main()
{
	Temperature k1 = 31.73_F;
	Temperature k2 = 0.0_C;
	
	cout << "k1 is " << k1.Kelvin << " Kelvin" << endl;
	cout << "k2 is " << k2.Kelvin << " Kelvin" << endl;
	
	return 0;
}

输出如下:

csharp 复制代码
k1 is 273 Kelvin
k2 is 273 Kelvin

类型转换运算符

在很多情况下,类型转换是合理的需求,可解决重要的兼容性问题。C++提供了一种新的类型转换运算符,专门用于基于继承的情形,这种情形在 C 语言编程中并不存在。

4 个 C++类型转换运算符如下: • static_cast • dynamic_cast • reinterpret_cast • const_cast

这 4 个类型转换运算符的使用语法相同:

c++ 复制代码
destination_type result = cast_operator<destination_type> (object_to_cast);

static_cast

static_cast 用于在相关类型的指针之间进行转换,还可显式地执行标准数据类型的类型转换---这种转换原本将自动或隐式地进行。用于指针时,static_cast 实现了基本的编译阶段检查,确保指针被转换为相关类型。这改进了 C 风格类型转换,在 C 语言中,可将指向一个对象的指针转换为完全不相关的类型,而编译器不会报错。使用 static_cast 可将指针向上转换为基类类型,也可向下转换为派生类型,

如下面的示例代码所示:

ini 复制代码
Base* objBase = new Derived ();
Derived* objDer = static_cast<Derived*>(objBase); // ok!
// class Unrelated is not related to Base
Unrelated* notRelated = static_cast<Unrelated*>(objBase); // Error
// The cast is not permitted as types are unrelated

将 Derived* 转换为 Base* 被称为向上转换,无需使用任何显式类型转换运算符就能进行这种转换;将 Base转换为 Derived被称为向下转换,如果不使用显式类型转换运算符,就无法进行这种转换;

然而,static_cast 只验证指针类型是否相关,而不会执行任何运行阶段检查。因此,程序员可使用 static_cast 编写如下代码,而编译器不会报错:

ini 复制代码
Base* objBase = new Base();
Derived* objDer = static_cast<Derived*>(objBase); // Still no errors!

其中 objDer 实际上指向一个不完整的 Derived 对象,因为它指向的对象实际上是 Base() 类型。由于 static_cast 只在编译阶段检查转换类型是否相关,而不执行运行阶段检查,因此 objDer -> DerivedFunction() 能够通过编译,但在运行阶段可能导致意外结果。

除用于向上转换和向下转换外,static_cast 还可在很多情况下将隐式类型转换为显式类型,以引起程序员或代码阅读者的注意:

ini 复制代码
double Pi = 3.14159265;
int num = static_cast<int>(Pi); // Making an otherwise implicit cast, explicit

在上述代码中,使用 num = Pi 将获得同样的效果,但使用 static_cast 可让代码阅读者注意到这里使用了类型转换,并指出编译器根据编译阶段可用的信息进行了必要的 调整,以便执行所需的类型转换。对于使用关键字 explicit 声明的转换运算符和构造函数,要使用它们,也必须通过 static_cast。

使用 dynamic_cast 和运行阶段类型识别

顾名思义,与静态类型转换相反,动态类型转换在运行阶段执行类型转换。可检查 dynamic_cast 操作的结果,以判断类型转换是否成功。使用 dynamic_cast 运算符的典型语法如下:

scss 复制代码
destination_type* Dest = dynamic_cast<class_type*>(Source);
if(Dest) // Check for success of the casting operation
    Dest->CallFunc ();

例如:

c++ 复制代码
Base* objBase = new Derived();
// Perform a downcast
Derived* objDer = dynamic_cast<Derived*>(objBase);
if(objDer) // Check for success of the cast
    objDer->CallDerivedFunction ();

如上述代码所示,给定一个指向基类对象的指针,程序员可使用 dynamic_cast 进行类型转换,并在使用指针前检查指针指向的目标对象的类型。

下面的代码使用动态转换判断 Fish 指针指向的是否是 Tuna 对象或 Carp 对象:

c++ 复制代码
#include <iostream> 
using namespace std;

class Fish
{
public:
	virtual void Swim()
	{
		cout << "Fish swims in water" << endl;
	}

	// base class should always have virtual destructor 
   virtual ~Fish() {}
};

class Tuna : public Fish
{
public:
	void Swim()
	{
		cout << "Tuna swims real fast in the sea" << endl;
	}

	void BecomeDinner()
	{
		cout << "Tuna became dinner in Sushi" << endl;
	}
};

class Carp : public Fish
{
public:
	void Swim()
	{
		cout << "Carp swims real slow in the lake" << endl;
	}

	void Talk()
	{
		cout << "Carp talked Carp!" << endl;
	}
};

void DetectFishType(Fish* objFish)
{
	Tuna* objTuna = dynamic_cast <Tuna*>(objFish);
	if (objTuna) // check success of cast 
	{
		cout << "Detected Tuna. Making Tuna dinner: " << endl;
		objTuna->BecomeDinner();
	}

	Carp* objCarp = dynamic_cast <Carp*>(objFish);
	if (objCarp)
	{
		cout << "Detected Carp. Making carp talk: " << endl;
		objCarp->Talk();
	}

	cout << "Verifying type using virtual Fish::Swim: " << endl;
	objFish->Swim(); // calling virtual function Swim 
}

int main()
{
	Carp myLunch;
	Tuna myDinner;
	DetectFishType(&myDinner);
	cout << endl;
	DetectFishType(&myLunch);
}

输出:

arduino 复制代码
Detected Tuna. Making Tuna dinner:
Tuna became dinner in Sushi
Verifying type using virtual Fish::Swim:
Tuna swims real fast in the sea

Detected Carp. Making carp talk:
Carp talked Carp!
Verifying type using virtual Fish::Swim:
Carp swims real slow in the lake

务必检查 dynamic_cast 的返回值,看它是否有效。如果返回值为 NULL,说明转换失败。

使用 reinterpret_cast

reinterpret_cast 是 C++中与 C 风格类型转换最接近的类型转换运算符。它让程序员能够将一种对象类型转换为另一种,不管它们是否相关;也就是说,它使用如下所示的语法强制重新解释类型:

c++ 复制代码
Base* objBase = new Base ();
Unrelated* notRelated = reinterpret_cast<Unrelated*>(objBase);
// The code above compiles, but is not good programming!

这种类型转换实际上是强制编译器接受 static_cast 通常不允许的类型转换,通常用于低级程序(如驱动程序),在这种程序中,需要将数据转换为 API(应用程序编程接口)能够接受的简单类型(例如,有些 OS 级 API 要求提供的数据为 BYTE 数组,即 unsigned char*):

c++ 复制代码
SomeClass* object = new SomeClass();
// Need to send the object as a byte-stream...
unsigned char* bytesFoAPI = reinterpret_cast<unsigned char*>(object);

上述代码使用的类型转换并没有改变源对象的二进制表示,但让编译器允许程序员访问 SomeClass 对象包含的各个字节。由于其他 C++类型转换运算符都不允许执行这种有悖类型安全的转换,因此除非万不得已,否则不要使用 reinterpret_cast 来执行不安全(不可移植)的转换。

使用 const_cast

const_cast 让程序员能够关闭对象的访问修饰符 const。您可能会问:为何要进行这种转换?在理想情况下,程序员将经常在正确的地方使用关键字 const。不幸的是,现实世界并非如此,像下面这样的代码随处可见:

csharp 复制代码
class SomeClass
{
public:
// ...
    void DisplayMembers(); //problem - display function isn't const
};

在下面的函数中,以 const 引用的方式传递 object 显然是正确的。毕竟,显示函数应该是只读的,不应调用非 const 成员函数,即不应调用能够修改对象状态的函数。然而,DisplayMembers() 本应为 const 的,但却没有这样定义。如果 SomeClass 归您所有,且源代码受您控制,则可对 DisplayMembers() 进行修改。然而,在很多情况下,它可能属于第三方库,无法对其进行修改。在这种情况下,const_cast 将是您的救星。

csharp 复制代码
void DisplayAllData (const SomeClass& object)
{
    object.DisplayMembers (); // Compile failure
    // reason: call to a non-const member using a const reference
}

在这种情况下,调用 DisplayMembers()的语法如下:

csharp 复制代码
void DisplayAllData (const SomeClass& object)
{
    SomeClass& refData = const_cast<SomeClass&>(object);
    refData.DisplayMembers(); // Allowed!
}

除非万不得已,否则不要使用 const_cast 来调用非 const 函数。一般而言,使用 const_cast 来修改 const 对象可能导致不可预料的行为。

另外,const_cast 也可用于指针:

scss 复制代码
void DisplayAllData (const SomeClass* data)
{
    // data->DisplayMembers(); Error: attempt to invoke a non-const function!
    SomeClass* pCastedData = const_cast<SomeClass*>(data);
    pCastedData->DisplayMembers(); // Allowed!
}

C++类型转换运算符存在的问题

并非所有人都喜欢使用 C++ 类型转换,即使那些 C++ 拥趸也如此。其理由很多,从语法繁琐而不够直观到显得多余。

来比较一下下面的代码:

arduino 复制代码
double Pi = 3.14159265;

// C++ style cast: static_cast
int num = static_cast <int>(Pi); // result: Num is 3

// C-style cast
int num2 = (int)Pi; // result: num2 is 3

// leave casting to the compiler
int num3 = Pi; // result: num3 is 3. No errors!

在这 3 种方法中,程序员得到的结果都相同。在实际情况下,第 2 种方法可能最常见,其次是第 3种,但几乎没有人使用第 1 种方法。无论采用哪种方法,编译器都足够聪明,能够正确地进行类型转换。这让人觉得类型转换运算符将降低代码的可读性。

同样,static_cast 的其他用途也可使用 C 风格类型转换进行处理,且更简单:

arduino 复制代码
// using static_cast
Derived* objDer = static_cast <Derived*>(objBase);

// But, this works just as well...
Derived* objDerSimple = (Derived*)objBase;

因此,使用 static_cast 的优点常常被其拙劣的语法所掩盖。Bjarne Stroustrup 准确地描述了这种境况:"由于 static_cast 如此拙劣且难以输入,因此您在使用它之前很可能会三思。这很不错,因为类型转换在现代 C++ 中是最容易避免的。"

再来看其他运算符。在不能使用 static_cast 时,可使用 reinterpret_cast 强制进行转换;同样,可以使用 const_cast 修改访问修饰符 const。因此,在现代 C++中,除 dynamic_cast 外的类型转换都是可以避免的。仅当需要满足遗留应用程序的需求时,才需要使用其他类型转换运算符。在这种情况下,程序员通常倾向于使用 C 风格类型转换而不是 C++类型转换运算符。重要的是,应尽量避免使用类型转换;而一旦使用类型转换,务必要知道幕后发生的情况。

相关推荐
博笙困了2 小时前
AcWing学习——归并排序
c++·排序算法
爱吃KFC的大肥羊3 小时前
C++三大特性之“继承”
开发语言·c++
星眸2293 小时前
C++/QT 1
c++
先知后行。3 小时前
线程的创建.销毁
开发语言·c++·算法
DdduZe3 小时前
9.11作业
c++·qt
鱼嘻3 小时前
西嘎嘎学习 - C++ 继承 - Day 10
开发语言·c++·学习·算法
孤廖3 小时前
从 “模板” 到 “场景”,用 C++ 磨透拓扑排序的实战逻辑
开发语言·c++·程序人生·算法·贪心算法·动态规划·学习方法
老歌老听老掉牙3 小时前
OpenCascade几何建模:平面创建与法向拉伸的工程实现
c++·平面·opencascade
-凌凌漆-3 小时前
【Qt】【C++】虚析构函数及 virtual ~Base() = default
java·c++·qt