《C++ Primer Plus》第十三章复习题和编程练习

一、复习题**

1. 派生类从基类那里继承了什么?

答:在类的继承和派生中,C++中的派生类能够继承基类的所有数据成员和大部分成员函数。但是基类中不同访问控制权限的成员在派生中的访问权限也不相同。公有成员直接成为派生类的公有成员,派生类的对象可以直接访问;基类的保护成员成为派生类的保护成员,派生类的对象也可以直接访问;基类的私有成员被派生类继承,但是派生类对象不能直接访问,只能通过基类的公有方法间接访问。

2. 派生类不能从基类那里继承什么?

答:派生类不能继承基类的构造函数、析构函数、赋值运算符、友元函数和友元类。但是派生类在调用构造函数时会调用基类的默认构造函数,也可以在初始化列表中传参指定使用基类的哪一个构造函数。在调用派生类对象的析构函数时,系统先调用派生类的析构函数,再调用基类的构造函数。

3. 假设baseDMA::operator=()函数的返回类型为void,而不baseDMA&,将会产生什么后果?如果返回类型为baseDMA,而不是baseDMA&,又将有什么后果?

答:如果赋值运算符重载的返回类型为void,仍然可以单个赋值,但是不能连续赋值,即可以使用 baseDMA a = b,但不能使用 baseDMA a = b = c。因为b = c 的结果为void。
  如果返回类型为baseDAM,则与baseDAM&相比,在返回值时会创建一个临时对象,然后给其赋值,再返回这个临时对象。因此运行效率降低。

4. 在创建和删除派生类对象时,构造函数和析构函数调用的顺序是怎么样的?

答:创建派生类对象时,如果不在派生类的构造函数的初始化列表中显式指定基类的构造函数,则系统自动调用基类的默认构造函数,然后再对派生类新增的成员变量进行初始化,总之,创建派生类对象时,先调用基类的构造函数,再调用派生类的构造函数。删除派生类对象时,编译器先调用派生类的析构函数,然后再调用基类的析构函数。由于基类的指针和引用可以指向派生类,所以基类的析构函数最好使用虚函数。

5. 如果没有为派生类添加任何数据成员,它是否需要构造函数?

答:需要。派生类不会继承基类的构造函数,而在C++中每一个类都必须要有自己的构造函数,即使这个类没有数据成员和自定义的构造函数,编译器也会自动生成一个默认构造函数,并在创建对象时调用。

6. 如果基类和派生类定义了同名的方法,当派生类对象调用该方法时,被调用的将是哪个方法?

答:如果派生类中定义了与基类中同名的方法,则派生类中定义的方法会隐藏基类中同名的方法,被调用的是派生类中的方法。通常情况下,派生类的对象只会调用派生类中新定义的方法,不会调用基类中同名的方法。只有当派生类没有重新定义同名方法或使用基类的作用域运算符时,派生类才会调用基类的方法。

7. 在什么情况下,派生类应定义赋值运算符?

答:如果派生类中存在动态存储,即在派生类的构造函数中使用new或new[]运算符来初始化类的某些指针类型的数据成员,则该类必须提供赋值运算符重载、析构函数和复制构造函数的定义。因为默认的赋值运算符重载使用的是浅拷贝,单纯地赋值,比如两个指针指向同一块空间,并不会开辟属于自己的空间,这就导致使用析构函数的时候对一块空间释放两次,导致未知的错误。如果派生类中不存在动态存储,则不需要定义赋值运算符。因为默认的赋值运算符在对基类中的成员进行赋值时,调用的是基类中的赋值运算符,而调用析构函数时,也会调用基类的析构函数,析构基类的成员。

8. 可以将派生类对象的地址赋值给基类的指针吗?可以将基类对象的地址赋值给派生类指针吗?

答:由于派生类继承了基类的所有成员变量和大部分成员函数,所以基类的指针和引用都可以指向派生类。但是在使用时,对于非虚函数的同名函数,编译器将根据指针和引用的类型调用基类的同名函数而不是派生类的同名函数。对于虚函数,则编译器将根据指针和引用指向的对象的类型进行调用,指向基类对象调用基类的虚函数,指向派生类对象调用派生类的虚函数。基类对象的地址可以通过强制类型转换,赋值给派生类的指针(向下转换),但是这样使用指针很不安全,因为派生类中可能新增了基类没有的变量,在调用函数时由于没有那个变量而无效,所以不推荐使用。

9. 可以将派生类对象赋值给基类对象吗?可以将基类对象赋值给派生类对象吗?

答:C++中可以将派生类对象赋值给基类对象,因为派生类和基类的的关系是IS-A关于,即派生类也是基类的一种。且派生类对象拥有基类对象的所有成员变量,而且赋值过程中不会把派生类中新增的成员变量赋值给基类对象。通常情况下,基类的对象是不能够给派生类对象赋值的,但是当派生类中定义了转换运算符(即类型转换,将包含以基类引用作为唯一参数的构造函数)或使用基类作为参数的赋值运算符时(重新定义赋值运算符),这种赋值才是可行的。

10. 假设定义了一个函数,它以基类对象的引用作为参数。为什么该函数也可以使用派生类对象作为参数?

答:在C++中基类的指针和引用可以在不显式转换的情况下指向派生类对象。因为在C++中派生类和基类的关系为IS-A,可以把派生类对象看作一种基类对象。

11. 假设定义了一个函数,它以基类对象作为参数(即函数按值传递基类对象)。为什么该函数也可以使用派生类对象作为参数?

答:在按值传递的过程中,会调用基类的构造函数,而参数通常为const引用,而基类的引用和指针可以指向派生类,所以会根据派生类对象中的基类成员变量创建一个临时的基类对象,然后传递给函数。

12. 为什么通常按引用传递对象比按值传递对象的效率更高?

答:在C++中按值传递对象会调用该对象的复制构造函数创建一个临时对象,其优点是保护原数据不被修改,但是会消耗时间和空间,效率降低。而使用引用传递对象,在该函数中使用的就是原来的对象,效率更高,但是可能会修改原数据,若不希望原数据被修改,在函数的参数列表中需要加上const修饰。

13. 假设 Corporation 是基类,PublicCorporation 是派生类。在假设这两个类都定义了head()函数,ph 是指向 Corporation 类型的指针,且指向一个 PublicCorporation 对象的地址。如果基类将head()函数定义为以下两种方法,则ph->head()将如何解释?
  a. 常规非虚方法。
  b. 虚方法。

答: a. 基类中使用常规非虚方法,则编译器会根据指针或引用的类型来判断,所以调用基类的head()函数。
   b. 基类中使用虚方法,则编译器根据指针或引用指向的类型来判断,所以调用派生类的head()函数。

14. 下述代码有什么问题?

// 基类声明
class kitchen
{
private:
	double kit_sq_ft;
public:
	kitchen() { kit_sq_ft = 0; }  // 默认构造函数称为内联函数
	virtual double area() const { return kit_sq_ft * kit_sq_ft; }  // 该虚函数为内联函数
};

// 派生类声明
class House : public kitchen
{
private:
	double all_sq_ft;
public:
	House() { all_sq_ft += kit_sq_ft; }  // 无法直接访问基类中的私有成员,只能通过公有成员访问
	double area(const char* s) const { cout << s; return all_sq_ft; }
};

答:首先,从派生类的基类的IS-A关系角度来说,房子根本不是厨房的一种,两个根本不搭边。可以把厨房类放入房子类中。其次,房子类是厨房类的派生类,不能直接访问厨房类的私有变量 kit_sq_ft 。只能通过基类的公有成员函数来进行访问。而且由于同名函数area()的参数列表不同,虚函数标签(virtual)并没有发挥作用。

二、编程练习

1. 以下面的类声明为基础,派生出一个Classic类,并添加一组char成员,用于存储指出Cd中主要作品的字符串。修改上述声明,使基类的所有函数都是虚的。如果上述定义声明的某个方法并不需要,则请删除它。请使用下面的程序测试你的代码。

// Cd基类声明
class Cd
{
private:
	char performers[50];  // 演员
	char label[20];  // 标签
	int selections;  // 选择
	double playtime;  // 游戏时间
public:
	// 构造函数
	Cd(char* sl, char* s2, int n, double x);
	Cd(const Cd& d);
	Cd();
	// 析构函数
	~Cd();
	// 成员函数
	void Report() const;
	// 运算符重载
	Cd& operator=(const Cd& d);
};

测试代码和结果我都放在下面的解答里面了,结果我都对过一遍了,正确的。

答:三个文件。

classic.h 头文件 :当涉及动态存储或者成员变量里面包含指针类型都需要深度拷贝,程序员都需要自己提供构造函数和赋值运算符重载的定义,动态存储还需提供析构函数的定义。基类中的析构函数最好声明为虚函数,不管是否设计动态存储。

#pragma once

// 头文件
#include <iostream>

// Cd基类声明
class Cd
{
private:
	char performers[50];  // 演员
	char label[20];  // 标签
	int selections;  // 选择
	double playtime;  // 表演时间
public:
	// 构造函数
	Cd(const char* s1, const char* s2, int n, double x);
	Cd(const Cd& d);
	Cd();
	// 析构函数
	virtual ~Cd();  // 基类的析构函数最好定义为virtual,不管是否动态存储
	// 成员函数
	virtual void Report() const;
	// 运算符重载
	Cd& operator=(const Cd& d);
};

// Classic派生类声明
class Classic : public Cd
{
private:
	char main_works[50];  // 主要作品
public:
	// 构造函数
	Classic(const char* nm, const char* lb, const char* works, int st, double pt);
	Classic(const Classic& c);
	Classic();
	// 析构函数
	~Classic();
	// 成员函数
	virtual void Report() const;
	// 运算符重载
	Classic& operator=(const Classic& c);
};

test1.cpp 测试文件 :若使用基类的指针或引用指向派生类调用与基类中的同名方法,需把基类中的该方法声明为虚函数,不然编译器会根据指针或引用的类型去调用基类的该函数。设置为虚函数之后,编译器会根据指针或引用指向的对象的类型调用属于该类型的函数。

// 头文件
#include "classic.h"

// using 声明
using std::cout;
using std::endl;

// 函数声明
void Bravo(const Cd& d);

int main()
{
	Cd c1("Beatles" , "Capital", 14, 35.5);
	Classic c2 = Classic("Piano Sonata in B flat, Fantasia in C",
		"Alfred Brendel", "Philips", 2, 57.17);
	Cd* pcd = &c1;  // 基类指针和引用可以直接指向派生类(向上转换)
	cout << "Using object directly:\n";
	c1.Report();
	c2.Report();
	cout << "Using type cd *pointer to objects:\n";
	pcd->Report();
	pcd = &c2;
	pcd->Report();
	cout << "Calling a function with a Cd reference argument:\n";
	Bravo(c1);
	Bravo(c2);
	cout << "Testing assignment: ";
	Classic copy;
	copy = c2;
	copy.Report();

	return 0;
}

// 函数定义
void Bravo(const Cd& d)
{
	d.Report();
}

classic.cpp 方法定义文件

// 头文件
#include "classic.h"
#include <cstring>

// using声明
using std::cout;
using std::endl;

// Cd基类方法定义
 
// 构造函数
Cd::Cd(const char* s1, const char* s2, int n, double x)
{
	strcpy(performers, s1);
	strcpy(label, s2);
	selections = n;
	playtime = x;
}

Cd::Cd(const Cd& d)
{
	strcpy(performers, d.performers);
	strcpy(label, d.label);
	selections = d.selections;
	playtime = d.playtime;
}

Cd::Cd()
{
	performers[0] = label[0] = '\0';
	selections = 0;
	playtime = 0;
}

// 析构函数
Cd::~Cd()
{
	// 空
}

// 成员函数
void Cd::Report() const
{
	cout << "Performers: " << performers << endl;
	cout << "Label: " << label << endl;
	cout << "Selections: " << selections << endl;
	cout << "Playtime: " << playtime << endl;
}

// 运算符重载
Cd& Cd::operator=(const Cd& d)
{
	strcpy(performers, d.performers);
	strcpy(label, d.label);
	selections = d.selections;
	playtime = d.playtime;

	return *this;
}

// Classic派生类方法定义

// 构造函数
Classic::Classic(const char* nm, const char* lb, const char* works, int st, double pt)
	: Cd(nm, lb, st, pt)
{
	strcpy(main_works, works);
}

Classic::Classic(const Classic& c)
	: Cd(c)
{
	strcpy(main_works, c.main_works);
}

Classic::Classic()
	: Cd()
{
	main_works[0] = '\0';
}

// 析构函数
Classic::~Classic()
{

}

// 成员函数
void Classic::Report() const
{
	// 先调用基类的Report显示基类信息,再显示派生类新增信息
	Cd::Report();
	cout << "Works: " << main_works << endl;
}

// 运算符重载
Classic& Classic::operator=(const Classic& c)
{
	// 先调用基类的赋值运算符给基类成员变量赋值
	Cd::operator=(c);
	strcpy(main_works, c.main_works);

	return *this;
}

运行结果如下:
在这里插入图片描述

2. 重做编程练习1,使两个类使用动态内存分配而不是长度固定的数组来记录字符串。

答:基类和派生类都使用动态内存分配,则基类的析构函数必须声明为虚函数,不然基类指针指向派生类生命周期结束时,只会调用基类的析构函数,而派生类新增成员不会被析构。测试程序和编程练习1一样未改变,所以本题只提供修改后的头文件和方法定义文件。

classic2.h 头文件

#pragma once
#pragma once

// 头文件
#include <iostream>

// Cd基类声明
class Cd
{
private:
	char* performers;  // 演员
	char* label;  // 标签
	int selections;  // 选择
	double playtime;  // 表演时间
public:
	// 构造函数
	Cd(const char* s1, const char* s2, int n, double x);
	Cd(const Cd& d);
	Cd();
	// 析构函数
	virtual ~Cd();  // 基类的析构函数最好定义为virtual,不管是否动态存储
	// 成员函数
	virtual void Report() const;
	// 运算符重载
	Cd& operator=(const Cd& d);
};

// Classic派生类声明
class Classic : public Cd
{
private:
	char* main_works;  // 主要作品
public:
	// 构造函数
	Classic(const char* nm, const char* lb, const char* works, int st, double pt);
	Classic(const Classic& c);
	Classic();
	// 析构函数
	~Classic();
	// 成员函数
	virtual void Report() const;
	// 运算符重载
	Classic& operator=(const Classic& c);
};

classic2.cpp 方法定义文件

#define _CRT_SECURE_NO_WARNINGS

// 头文件
#include "classic2.h"
#include <cstring>

// using声明
using std::cout;
using std::endl;

// Cd基类方法定义

// 构造函数
Cd::Cd(const char* s1, const char* s2, int n, double x)
	: performers(new char[strlen(s1) + 1])
	, label(new char[strlen(s2) + 1])
	, selections(n)
	, playtime(x)
{
	strcpy(performers, s1);
	strcpy(label, s2);
}

Cd::Cd(const Cd& d)
	: performers(new char[strlen(d.performers) + 1])
	, label(new char[strlen(d.label) + 1])
	, selections(d.selections)
	, playtime(d.playtime)
{
	strcpy(performers, d.performers);
	strcpy(label, d.label);
}

Cd::Cd()
	: performers(new char[1] { 0 })
	, label(new char[1] { 0 })
	, selections(0)
	, playtime(0)
{
	
}

// 析构函数
Cd::~Cd()
{
	// 释放两个指针的空间
	delete[] performers;
	delete[] label;
}

// 成员函数
void Cd::Report() const
{
	cout << "Performers: " << performers << endl;
	cout << "Label: " << label << endl;
	cout << "Selections: " << selections << endl;
	cout << "Playtime: " << playtime << endl;
}

// 运算符重载
Cd& Cd::operator=(const Cd& d)
{
	// 检查是否自身赋值
	if (&d != this)
	{
		// 释放之前的空间
		delete[] performers;
		delete[] label;
		// 申请新的空间
		performers = new char[strlen(d.performers) + 1];
		label = new char[strlen(d.label) + 1];
		// 拷贝内容
		strcpy(performers, d.performers);
		strcpy(label, d.label);
		selections = d.selections;
		playtime = d.playtime;
	}

	return *this;
}

// Classic派生类方法定义

// 构造函数
Classic::Classic(const char* nm, const char* lb, const char* works, int st, double pt)
	: Cd(nm, lb, st, pt)
	, main_works(new char[strlen(works) + 1])
{
	strcpy(main_works, works);
}

Classic::Classic(const Classic& c)
	: Cd(c)
	, main_works(new char[strlen(c.main_works) + 1])
{
	strcpy(main_works, c.main_works);
}

Classic::Classic()
	: Cd()
	, main_works(new char[1] { 0 })
{
	
}

// 析构函数
Classic::~Classic()
{
	delete[] main_works;
}

// 成员函数
void Classic::Report() const
{
	// 先调用基类的Report显示基类信息,再显示派生类新增信息
	Cd::Report();
	cout << "Works: " << main_works << endl;
}

// 运算符重载
Classic& Classic::operator=(const Classic& c)
{
	// 检查是否自身赋值
	if (&c != this)
	{
		// 先调用基类的赋值运算符给基类成员变量赋值
		Cd::operator=(c);
		// 释放之前的空间
		delete[] main_works;
		// 申请新的空间
		main_works = new char[strlen(c.main_works) + 1];
		// 拷贝内容
		strcpy(main_works, c.main_works);
	}

	return *this;
}

3. 修改 baseDMA-lacksDMA-hasDMA 类的层次,使三个类都从一个ABC派生而来,然后使用与程序清单13.10相似的程序对结果进行测试。也就是说,它应使用ABC指针数组,并由用户决定要创建的对象类型。在类定义中添加virtual View()方法以显示数据。

答:ABC(抽象基类)必须有一个纯虚成员,且该类不能创建对象,只能用作基类,其析构函数应为纯虚函数。实际上把原来的baseDMA变成抽象基类就行。

DMA.h 头文件

#pragma once

// 头文件
#include <iostream>

// DMA 抽象基类声明
class DMA
{
private:
	char* label;
	int rating;
public:
	// 构造函数
	DMA(const char* lb = "null", int r = 0);
	DMA(const DMA& d);
	// 析构函数
	virtual ~DMA() = 0 { delete[] label; }
	// 虚函数
	virtual void View() const;
	// 运算符重载
	DMA& operator=(const DMA& d);
	// 友元函数
	friend std::ostream& operator<<(std::ostream& os, const DMA& dma);
};

// 三个派生类声明

class baseDMA : public DMA
{
public:
	// 构造函数
	baseDMA(const char* lb = "null", int r = 0);
	// 虚函数
	virtual void View() const;
};

class lacksDMA : public DMA
{
private:
	enum { COL_LEN = 40 };
	char color[COL_LEN];
public:
	// 构造函数
	lacksDMA(const char* c = "blank", const char* lb = "null", int r = 0);
	lacksDMA(const char* c, const baseDMA& dma);
	// 虚函数
	virtual void View() const;
	// 友元函数
	friend std::ostream& operator<<(std::ostream& os, const lacksDMA& ld);
};

class hasDMA : public DMA
{
private:
	char* style;
public:
	// 构造函数
	hasDMA(const char* sl = "none", const char* lb = "null", int r = 0);
	hasDMA(const char* sl, const DMA& dma);
	hasDMA(const hasDMA& hd);
	// 析构函数
	virtual~hasDMA();
	// 虚函数
	virtual void View() const;
	// 运算符重载
	hasDMA& operator=(const hasDMA& hd);
	// 友元函数
	friend std::ostream& operator<<(std::ostream& os, const hasDMA& hd);
};

test3.cpp 测试文件

// 头文件
#include "DMA.h"

// using 声明
using std::cout;
using std::endl;
using std::cin;

// 符号常量声明
const int SIZE = 3;
const int LEN = 40;

int main()
{
	// 基类指针数组声明
	DMA* pdma[SIZE] = { 0 };
	// 所需变量
	char label[LEN];
	int rating = 0;
	// 循环赋值
	for (int i = 0; i < SIZE; ++i)
	{
		cout << "No." << i+1 << endl;
		cout << "Please enter the label: ";
		cin.getline(label, 39);
		label[39] = '\0';  // 保证为字符串
		cout << "Please enter the rating: ";
		cin >> rating;
		cin.get();  // 读取后面的换行符
		cout << "Please select:\n";
		cout << "1) baseDMA    2) lacksDMA\n";
		cout << "3) hasDMA\n";
		int select = 0;
		cin >> select;
		cin.get();
		switch (select)
		{
		case 1 :
			pdma[i] = new baseDMA(label, rating);
			break;
		case 2 :
			char color[LEN];
			cout << "Please enter the color: ";
			cin.getline(color, 39);
			color[39] = '\0';
			pdma[i] = new lacksDMA(color, label, rating);
			break;
		case 3 : 
			char style[LEN];
			cout << "Please enter the style: ";
			cin.getline(style, 39);
			style[39] = '\0';
			pdma[i] = new lacksDMA(style, label, rating);
			break;
		}
		cout << endl;
	}
	// 遍历输出
	for (int i = 0; i < SIZE; ++i)
	{
		cout << "No." << i + 1 << endl;
		pdma[i]->View();
	}
	// 释放空间
	for (int i = 0; i < SIZE; ++i)
	{
		delete pdma[i];
	}

	return 0;
}

DMA.cpp 方法定义文件

// 头文件
#include "DMA.h"
#include <cstring>

// using 声明
using std::cout;
using std::endl;

// DMA 抽象类方法定义

// 构造函数
DMA::DMA(const char* lb, int r)
	: label(new char[strlen(lb) + 1])
	, rating(r)
{
	strcpy(label, lb);
}

DMA::DMA(const DMA& d)
	: label(new char[strlen(d.label) + 1])
	, rating(d.rating)
{
	strcpy(label, d.label);
}

// 虚函数
void DMA::View() const
{
	cout << "Now, it is the DMA.\n";
	cout << *this;
}

// 运算符重载
DMA& DMA::operator=(const DMA& d)
{
	// 检查是否自身赋值
	if (&d != this)
	{
		// 释放之前空间
		delete[] label;
		// 申请新的空间
		label = new char[strlen(d.label) + 1];
		// 拷贝内容
		strcpy(label, d.label);
		rating = d.rating;
	}
	return *this;
}

// 友元函数
std::ostream& operator<<(std::ostream& os, const DMA& dma)
{
	os << "Label: " << dma.label << endl;
	os << "Rating: " << dma.rating << endl;

	return os;
}

// baseDMA 类方法定义

// 构造函数
baseDMA::baseDMA(const char* lb, int r)
	: DMA(lb, r)
{

}

// 虚函数
void baseDMA::View() const
{
	cout << "Now, it is the baseDMA.\n";
	cout << (const DMA&)*this;
}

// lacksDMA 类方法定义

// 构造函数
lacksDMA::lacksDMA(const char* c, const char* lb, int r)
	: DMA(lb, r)
{
	strcpy(color, c);
}

lacksDMA::lacksDMA(const char* c, const baseDMA& dma)
	: DMA(dma)
{
	strcpy(color, c);
}

// 虚函数
void lacksDMA::View() const
{
	cout << "Now, it is the lacksDMA.\n";
	cout << *this;
}

// 友元函数
std::ostream& operator<<(std::ostream& os, const lacksDMA& ld)
{
	os << (const DMA&)ld;
	cout << "Color: " << ld.color << endl;

	return os;
}

// hasDMA 类方法定义

// 构造函数
hasDMA::hasDMA(const char* sl, const char* lb, int r)
	: DMA(lb, r)
	, style(new char[strlen(sl) + 1])
{
	strcpy(style, sl);
}

hasDMA::hasDMA(const char* sl, const DMA& dma)
	: DMA(dma)
	, style(new char[strlen(sl) + 1])
{
	strcpy(style, sl);
}

hasDMA::hasDMA(const hasDMA& hd)
	: DMA(hd)
	, style(new char[strlen(hd.style) + 1])
{
	strcpy(style, hd.style);
}

// 析构函数
hasDMA::~hasDMA()
{
	delete[] style;
}

// 虚函数
void hasDMA::View() const
{
	cout << "Now, it is the hasDMA.\n";
	cout << *this;
}

// 运算符重载
hasDMA& hasDMA::operator=(const hasDMA& hd)
{
	// 检查是否自身赋值
	if (&hd != this)
	{
		// 调用基类赋值运算符重载函数,赋值基类数据
		DMA::operator=(hd);
		// 赋值派生类新增数据
		// 释放之前的空间
		delete[] style;
		// 申请新的空间
		style = new char[strlen(hd.style) + 1];
		// 拷贝内容
		strcpy(style, hd.style);
	}
	return *this;
}

// 友元函数
std::ostream& operator<<(std::ostream& os, const hasDMA& hd)
{
	os << (const DMA&)hd;
	os << "Style: " << hd.style << endl;

	return os;
}

测试结果:
在这里插入图片描述

4. Benevolent Order of Programmers (BOP) 用来维护瓶装葡萄酒箱。为描述它,BOP 的 PortMaster 设置了一个Port 类,其声明如下。

// Port 基类声明
class Port
{
private:
	char* brand;
	char style[20];
	int bottles;
public:
	// 构造函数
	Port(const char* br = "none", const char* st = "none", int b = 0);
	Port(const Port& p);
	// 析构函数
	virtual ~Port();
	// 运算符重载
	Port& operator=(const Port& p);
	Port& operator+=(int n);
	Port& operator-=(int n);
	// 获取成员变量函数
	int BottleCount() const;
	// 虚函数
	virtual void show() const;
	// 友元函数
	friend std::ostream& operator<<(std::ostream& os, const Port& p);
};

show()方法按照下面的格式显示信息。
Brand: Gallo
Kind: tawny
Bottles: 20
operator<<()函数按下面的格式显示信息(末尾没有换行符)。
Gallo, tawny, 20
PortMaster 完成了 Port 类方法的定义后,派生了 VintagePort 类,然后被解雇——因为不小心将一瓶45度的科佰恩酒泼到了正在准备烤肉调料的人身上。VintagePort 类如下所示。

// VintagePort 派生类声明
class VintagePort : public Port
{
private:
	char* nickname;
	int year;
public:
	// 构造函数
	VintagePort();
	VintagePort(const char* br, int b, int nm, int y);
	VintagePort(const VintagePort& vp);
	// 析构函数
	~VintagePort() { delete[] nickname; }
	// 运算符重载
	VintagePort& operator=(const VintagePort& vp);
	// 显示信息函数
	void show() const;
	// 友元函数
	friend std::ostream& operator<<(std::ostream& os, const VintagePort& vp);
};

你负责完成 VintagePort。
a. 第一个任务是重新创建 Port 方法的定义,因为 PortMaster 在离职时销毁了方法的定义。
b. 第二个任务是解释为什么有的方法重新定义了,而有些没有重新定义。
c. 第三个任务是解释为何没有将operator=()和operator<<()声明为虚方法。
d. 第四个任务是提供 VintagePort 中各个方法的定义。

答:

a. Port 基类方法定义

// Port 基类方法定义

// 构造函数
Port::Port(const char* br, const char* st, int b)
	: brand(new char[strlen(br)+1])
	, bottles(b)
{
	strcpy(brand, br);
	strcpy(style, st);
}

Port::Port(const Port& p)
	: brand(new char[strlen(p.brand) + 1])
	, bottles(p.bottles)
{
	strcpy(brand, p.brand);
	strcpy(style, p.style);
}

// 析构函数
Port::~Port()
{
	delete[] brand;
}

// 运算符重载
Port& Port::operator=(const Port& p)
{
	// 检查是否自身赋值
	if (&p != this)
	{
		// 释放之前的空间
		delete[] brand;
		// 申请新的空间
		brand = new char[strlen(p.brand) + 1];
		// 拷贝内容
		strcpy(brand, p.brand);
		strcpy(style, p.style);
		bottles = p.bottles;
	}

	return *this;
}

Port& Port::operator+=(int n)
{
	bottles += n;
	return *this;
}

Port& Port::operator-=(int n)
{
	bottles -= n;
	return *this;
}

// 获取成员变量函数
int Port::BottleCount() const
{
	return bottles;
}

// 虚函数
void Port::show() const
{
	cout << "Brand: " << brand << endl;
	cout << "Kind: " << style << endl;
	cout << "Bottles: " << bottles << endl;
}

// 友元函数
std::ostream& operator<<(std::ostream& os, const Port& p)
{
	os << p.brand << ", " << p.style << ", " << p.bottles;

	return os;
}

b. 解释:首先构造函数,析构函数,赋值运算符重载函数,友元函数和友元类这些是不会被派生类继承的,其次派生类 VintagePort 中使用了动态内存,所以上述函数都需要重新定义。再者需要使用派生类新增的成员变量也需要重新定义该函数,如果与基类函数同名且参数列表相同,需要把基类同名函数声明为虚函数(virtual)。而只涉及基类中成员变量的处理函数一般不需要重新定义,直接调用即可。

c. 解释:在C++中,将赋值运算符重载函数声明为虚函数没有意义,因为赋值运算符不会像构造函数或析构函数那样影响多态性,也不会像其他函数那样与对象的生命周期直接相关。赋值运算符是一个二元运算符,它的左操作数是当前对象的一个引用,右操作数是另一个对象。赋值运算符的目的是将右操作数的值复制到左操作数所指向的对象中。由于C++的运行时多态性主要是通过虚函数实现的,而赋值运算符不涉及虚函数的动态绑定,因此不需要将它声明为虚函数。如果你试图通过基类的引用或指针来对派生类对象进行赋值,C++将使用基类的赋值运算符来处理这个操作,这可能不是你想要的结果。如果你需要在派生类中自定义赋值操作,你应该使用成员函数重载而不是虚函数。友元函数不是类的成员函数,不能成为虚函数。

d. VintagePort 派生类方法定义

// VintagePort 派生类方法定义

// 构造函数
VintagePort::VintagePort()
	: Port()
	, nickname(new char[1] { 0 })
	, year(0)
{

}

VintagePort::VintagePort(const char* br, const char* st, int b, const char* nm, int y)
	: Port(br, st, b)
	, nickname(new char[strlen(nm) + 1])
	, year(y)
{
	strcpy(nickname, nm);
}

VintagePort::VintagePort(const VintagePort& vp)
	: Port(vp)
	, nickname(new char[strlen(vp.nickname) + 1])
	, year(vp.year)
{
	strcpy(nickname, vp.nickname);
}

// 运算符重载
VintagePort& VintagePort::operator=(const VintagePort& vp)
{
	// 检查是否自身赋值
	if (&vp != this)
	{
		// 先调用基类的赋值运算符
		Port::operator=(vp);
		// 释放之前空间
		delete[] nickname;
		// 申请新的空间
		nickname = new char[strlen(vp.nickname) + 1];
		// 拷贝内容
		strcpy(nickname, vp.nickname);
		year = vp.year;
	}
	return *this;
}

// 显示信息函数
void VintagePort::show() const
{
	Port::show();
	cout << "NickName: " << nickname << endl;
	cout << "Year: " << year << endl;
}

// 友元函数
std::ostream& operator<<(std::ostream& os, const VintagePort& vp)
{
	os << (const Port&)vp;
	os << ", " << vp.nickname << ", " << vp.year;

	return os;
}

相关推荐

  1. C++Primer Plus编程练习4

    2024-06-10 15:00:04       7 阅读
  2. 数据治理练习

    2024-06-10 15:00:04       8 阅读

最近更新

  1. TCP协议是安全的吗?

    2024-06-10 15:00:04       8 阅读
  2. 【Python教程】压缩PDF文件大小

    2024-06-10 15:00:04       9 阅读
  3. 通过文章id递归查询所有评论(xml)

    2024-06-10 15:00:04       10 阅读

热门阅读

  1. C-Linux: 题集

    2024-06-10 15:00:04       6 阅读
  2. 【DevOps】开源的sd-wan软件介绍和对比分析

    2024-06-10 15:00:04       10 阅读
  3. 词向量对模型performance的影响

    2024-06-10 15:00:04       7 阅读
  4. Linux磁盘与文件系统管理

    2024-06-10 15:00:04       7 阅读
  5. 需求记录(共享元素)

    2024-06-10 15:00:04       9 阅读
  6. Vue3 组合式 API:依赖注入(四)

    2024-06-10 15:00:04       4 阅读
  7. 贪心算法03(leetcode1005,134,135)

    2024-06-10 15:00:04       9 阅读
  8. web前端三大主流框架

    2024-06-10 15:00:04       4 阅读
  9. C++中的if constexpr

    2024-06-10 15:00:04       7 阅读
  10. 驱动开发MISC 杂项驱动

    2024-06-10 15:00:04       7 阅读
  11. 自建 Docker 镜像

    2024-06-10 15:00:04       7 阅读
  12. Linux常见命令

    2024-06-10 15:00:04       5 阅读
  13. C++——时间复杂度

    2024-06-10 15:00:04       8 阅读