中南大学考试试卷

一、填空题(每空 2 分,共 20 分)

  1. 将一个函数声明为一个类的友元函数,必须使用关键字 (1) 。
  2. 若已经定义了一个int类型的变量a,现要求定义a的引用变量ref_a,ref_a的定义方式为 (2) 。
  3. 假定A为一个类,则执行语句:A a(2), b[3], *p[4];时,调用该类构造函数 (3) 次。
  4. 假设ip为已经定义好的整型指针,为动态得到一个包含10个整数的数组,并由ip指向该数组,应使用的语句为 (4) ,当要释放ip指向的动态数组对象时,使用的语句为: (5) 。
  5. 当一个函数的代码较少且需要频繁调用时,可以将其定义为内联函数,此时需在函数前面使用关键字 (6) 说明该函数为内联函数。
  6. 一个抽象类的派生类可以实例化的必要条件是实现了所有的 (7) 。
  7. 假定类student中有一个公用属性的静态数据成员static float score;在类外不通过对象名给该成员

score赋值为90的语句为: (8) 。

  1. 假设程序中用如下的语句定义了常量PI和变量a:

const float PI = 3.1415926; float a;

现要求定义一个指向常量的指针p1和常指针p2,分别指向PI和a,p1和p2的数据类型均为float, 则应该使用的语句分别为 (9) 和 (10)

  1. (1)friend
  2. (2)int &ref_a = a;
  3. (3)4次
  4. (4)ip = new int[10]; (5)delete[] ip;
  5. (6)inline
  6. (7)纯虚函数
  7. (8)student::score = 90;
  8. (9)const float *p1 = Π (10)float *const p2 = &a;

解释:

  • 第9题中,定义指向常量的指针p1时,使用了const float *的语法,表示p1是一个指向常量的指针,指向的对象不可通过p1修改。
  • 第10题中,定义常指针p2时,使用了float *const的语法,表示p2是一个常指针,指向的对象不可通过其他方式修改,但p2本身是可修改的。

五、 简答题(每小题 4 分,共 12 分)

1、简述友元函数重载运算符和成员函数重载运算符的异同。

2、简述 new 运算符和 delete 运算符的作用及其关系。

3、简述结构化程序设计方法与面向对象程序设计方法的基本思想。

1、友元函数重载运算符和成员函数重载运算符的异同在于:

  • 相同之处:它们都可以用来实现对运算符的重载,使得用户自定义类型能够使用类似内置类型的语法进行操作。
  • 不同之处:友元函数重载运算符是在类外定义的函数,它不属于类的成员函数,但可以通过友元声明访问类的私有成员;而成员函数重载运算符是类的成员函数,直接访问类的成员变量和函数。

2、new 运算符用于在动态存储区(堆)上分配内存空间,并返回指向该空间的指针。它允许在程序运行时动态地分配内存,适用于需要灵活管理内存的情况,如动态数组的创建。而 delete 运算符用于释放由 new 运算符分配的动态内存,防止内存泄漏。两者关系在于,new 和 delete 是一对互相配套的运算符,使用 new 分配的内存应该在不再需要时使用 delete 释放,以免造成内存泄漏。

3、结构化程序设计方法强调程序结构的模块化和层次化,通过顺序、选择和循环等结构来实现程序的设计和实现,重点在于分解问题、模块化设计、自顶向下的设计与编码。而面向对象程序设计方法则强调将问题看作是由多个对象组成的,通过定义对象及对象之间的交互关系来描述问题,以类和对象为基本单位,通过封装、继承和多态等概念来组织和管理程序,重点在于抽象、封装、继承和多态。结构化程序设计方法更适用于简单、功能相对独立的问题,而面向对象程序设计方法更适用于复杂、需要复用和扩展的问题。

六、 编程题(第 1 题 12 分,第 2 题 8 分,共 20 分)

1、关于交通工具的程序设计

(1)设计一个表示交通工具的抽象基类 vehicle,该类中至少包括速度、颜色两个属性(私有成员), 以及设置这些属性的成员函数(这些函数均为虚函数,其中至少有一个为纯虚函数)。

(2)由 vehicle 公有派生出 car、plane、ship 三个类,每个类中新增最大载客数(私有成员),并对基类中的虚函数重新定义;新增一个设置参数的成员函数,设置各交通工具的基本属性(速度、颜色、最大载客数);新增一个 print 函数,输出该交通工具的基本属性:速度、颜色、最大载客数。

(3)main 函数中,对不同类型的交通工具,进行参数设置与输出,要求实现动态多态性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
#include <iostream>
#include <string>

using namespace std;

// 基类:交通工具
class Vehicle {
protected:
double speed;
string color;

public:
Vehicle(double _speed = 0, string _color = "") : speed(_speed), color(_color) {}
virtual void setAttributes(double _speed, string _color) {
speed = _speed;
color = _color;
}
virtual void print() const = 0; // 纯虚函数
};

// 派生类:汽车
class Car : public Vehicle {
private:
int maxPassengers;

public:
Car(int _maxPassengers) : maxPassengers(_maxPassengers) {}
void setAttributes(double _speed, string _color, int _maxPassengers) {
speed = _speed;
color = _color;
maxPassengers = _maxPassengers;
}
void print() const override {
cout << "Car - Speed: " << speed << " km/h, Color: " << color << ", Max Passengers: " << maxPassengers << endl;
}
};

// 派生类:飞机
class Plane : public Vehicle {
private:
int maxPassengers;

public:
Plane(int _maxPassengers) : maxPassengers(_maxPassengers) {}
void setAttributes(double _speed, string _color, int _maxPassengers) {
speed = _speed;
color = _color;
maxPassengers = _maxPassengers;
}
void print() const override {
cout << "Plane - Speed: " << speed << " km/h, Color: " << color << ", Max Passengers: " << maxPassengers << endl;
}
};

// 派生类:船
class Ship : public Vehicle {
private:
int maxPassengers;

public:
Ship(int _maxPassengers) : maxPassengers(_maxPassengers) {}
void setAttributes(double _speed, string _color, int _maxPassengers) {
speed = _speed;
color = _color;
maxPassengers = _maxPassengers;
}
void print() const override {
cout << "Ship - Speed: " << speed << " km/h, Color: " << color << ", Max Passengers: " << maxPassengers << endl;
}
};

int main() {
Vehicle *vehicles[3];

vehicles[0] = new Car(5); // 最大载客数为 5
vehicles[1] = new Plane(200); // 最大载客数为 200
vehicles[2] = new Ship(1000); // 最大载客数为 1000

for (int i = 0; i < 3; ++i) {
vehicles[i]->setAttributes(500, "Red"); // 设置速度和颜色
vehicles[i]->print(); // 输出交通工具的属性
}

for (int i = 0; i < 3; ++i) {
delete vehicles[i]; // 释放动态分配的内存
}

return 0;
}

这段代码实现了一个基类 Vehicle,以及三个派生类 CarPlaneShip。基类包含了速度和颜色两个属性,并定义了纯虚函数 print() 用于输出交通工具的基本属性。派生类中新增了最大载客数属性,并对基类中的虚函数重新定义,以及新增了设置参数的成员函数 setAttributes()print() 函数。在 main() 函数中,通过基类指针数组实现了对不同类型的交通工具进行参数设置和输出,并展示了动态多态性的应用。


2、关于时间 Time 的程序设计

(1)设计一个表示时间的类 Time,其私有数据成员包括三个 int 类型的数:h, m, s,分别表示小时、分钟和秒钟。

(2)设计一个普通构造函数和一个拷贝构造函数,实现时间的初始化。

(3)重载运算符“-”和“<<”,实现时间的减法运算和时间的输出。注:时间的减法运算是指两个时间之间差的秒数,以非负整数表示。

下面是一个简单的C++程序,实现了上述要求的时间类(Time)的设计:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
#include <iostream>
using namespace std;

class Time {
private:
int h, m, s; // 分别表示小时、分钟和秒

public:
// 普通构造函数
Time(int hours = 0, int minutes = 0, int seconds = 0) {
h = hours;
m = minutes;
s = seconds;
normalize(); // 正常化时间
}

// 拷贝构造函数
Time(const Time& t) {
h = t.h;
m = t.m;
s = t.s;
}

// 重载减法运算符
int operator-(const Time& t) const {
int totalSeconds = (h * 3600 + m * 60 + s) - (t.h * 3600 + t.m * 60 + t.s);
if (totalSeconds < 0) {
totalSeconds = -totalSeconds; // 确保结果为非负整数
}
return totalSeconds;
}

// 重载输出运算符
friend ostream& operator<<(ostream& os, const Time& t) {
os << t.h << ":" << t.m << ":" << t.s;
return os;
}

// 私有成员函数,用于时间的正常化
void normalize() {
s += m * 60;
m += h * 60;
h = s / 3600;
m = (s % 3600) / 60;
s = s % 60;
}
};

int main() {
Time t1(2, 30, 45); // 创建时间对象 t1
Time t2(1, 25, 30); // 创建时间对象 t2

cout << "Time t1: ";
cout << t1 << endl; // 输出 t1

cout << "Time t2: ";
cout << t2 << endl; // 输出 t2

int difference = t1 - t2;
cout << "Difference in seconds: " << difference << endl; // 输出时间差

return 0;
}

这个程序定义了一个名为 Time 的类,其中包含三个私有数据成员 hms,分别代表小时、分钟和秒。程序提供了两个构造函数:一个普通构造函数和一个拷贝构造函数,用于初始化时间对象。同时,程序重载了减法运算符 - 来计算两个时间对象之间的差值(以秒为单位),并重载了输出运算符 << 以便能够方便地输出时间对象。

注意,这个程序假设输入的时间是合法的,并且没有进行错误检查。在实际应用中,可能需要添加更多的功能,比如时间的增加、错误检查等。

中南大学考试试卷

填空题

  1. 在删除一个动态对象时,将自动调用该动态对象所属类的 (1) 函数。

  2. 运算符重载函数作为类的成员函数和友元函数时,其主要区别在于形参个数,原因是其作为类的成员函数时隐含有 (2) 指针。

  3. 若要将前缀形式的自增运算符和后缀形式的自增运算符重载为类A的成员函数,则这两个重载函数的声明分别为 (3) 、 (4) 。

  4. 为了避免多重继承时,在派生类中出现公共基类成员的多个副本,则需将该公共基类声明为(5) 。

  5. 在定义成员函数时,函数体之前加上 (6) 防止函数改变数据成员的值。

  6. 假定一个类A有两个int型的数据成员a和b,且类A有如下两个构造函数:

1
2
3
A(int aa, int bb, int cc) {a = aa++; b = aa*bb +cc;}

A(int aa = 6, int bb = 3) { a = ++aa; b = aa*bb;}

则创建对象A x1(5, 6, 7), x2(4);后,x1.a和x1.b的值分别为 (7) ;x2.a和x2.b的值分别为 (8) 。

  1. 不同对象可以调用相同名称的函数,但执行完全不同行为的现象称为 (9) 。

  2. 若将一个函数定义为内联函数,则需要在函数声明加 (10) 关键字。

  3. (1)析构函数

  4. (2)this

  5. (3)A:: operator++()

  6. (4)A operator++(int)

  7. (5)虚基类

  8. (6)const

  9. (7)5、43

  10. (8)5、21

  11. (9)多态

  12. (10)inline


综合题

1、设计一个表示函数的抽象基类fun,由它派生出三个派生类:正弦函数类sin_fun、余弦函数cos_fun、正切函数类tan_fun,这三个派生类均有一个数据成员x,定义虚函数(derivative_fun)分别计算三个函数的导数。main函数中,定义如下对象:sin_fun obj1(1); cos_fun obj2(1); tan_fun obj3(1);再定义一个基类指针数组,分别用数组的每一个元素指向一个派生类的对象,分别计算三个函数sin(x)、cos(x)、tan(x)在x=1处的导数,并输出结果。

提示:三个函数的导数公式:

sin’(x) = cos(x);

cos’(x) = -sin(x);

tan’(x) = 1/cos2(x)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#include <iostream>
#include <cmath>

using namespace std;

// 抽象基类:函数
class Fun {
protected:
double x;

public:
Fun(double _x) : x(_x) {}
virtual double derivative_fun() const = 0; // 虚函数,计算导数
};

// 派生类:正弦函数
class Sin_fun : public Fun {
public:
Sin_fun(double _x) : Fun(_x) {}
double derivative_fun() const override {
return cos(x); // 导数公式:sin'(x) = cos(x)
}
};

// 派生类:余弦函数
class Cos_fun : public Fun {
public:
Cos_fun(double _x) : Fun(_x) {}
double derivative_fun() const override {
return -sin(x); // 导数公式:cos'(x) = -sin(x)
}
};

// 派生类:正切函数
class Tan_fun : public Fun {
public:
Tan_fun(double _x) : Fun(_x) {}
double derivative_fun() const override {
return 1.0 / pow(cos(x), 2); // 导数公式:tan'(x) = 1/cos^2(x)
}
};

int main() {
Sin_fun obj1(1); // 创建正弦函数对象
Cos_fun obj2(1); // 创建余弦函数对象
Tan_fun obj3(1); // 创建正切函数对象

Fun* funcs[3]; // 基类指针数组

funcs[0] = &obj1; // 指向正弦函数对象
funcs[1] = &obj2; // 指向余弦函数对象
funcs[2] = &obj3; // 指向正切函数对象

// 计算三个函数在 x=1 处的导数,并输出结果
for (int i = 0; i < 3; ++i) {
cout << "Function " << i + 1 << " derivative at x=1: " << funcs[i]->derivative_fun() << endl;
}

return 0;
}

这段代码实现了一个抽象基类 Fun,以及三个派生类 Sin_funCos_funTan_fun,分别表示正弦函数、余弦函数和正切函数。每个派生类都有一个数据成员 x,表示函数的自变量,以及虚函数 derivative_fun() 用于计算函数的导数。在 main() 函数中,创建了三个函数对象,并通过基类指针数组分别计算了这三个函数在 x=1 处的导数,并输出结果。


2、设计一个表示矩阵的类Matrix,它的私有数据成员包括:column(int类型,表示矩阵的列数)、row(int类型,表示矩阵的行数)和content(float类型的指针,表示矩阵的值);要求定义构造函数对矩阵进行初始化,重载运行符“+”和“<<”,实现矩阵的加法运算和矩阵的输出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
#include <iostream>

using namespace std;

class Matrix {
private:
int row;
int column;
float* content;

public:
// 构造函数,初始化矩阵
Matrix(int _row, int _column) : row(_row), column(_column) {
content = new float[row * column];
// 将矩阵初始化为0
for (int i = 0; i < row * column; ++i) {
content[i] = 0.0;
}
}

// 析构函数,释放内存
~Matrix() {
delete[] content;
}

// 重载运算符"+"
Matrix operator+(const Matrix& other) const {
// 检查两个矩阵的维度是否相同
if (row != other.row || column != other.column) {
cerr << "Error: Matrix dimensions don't match for addition!" << endl;
exit(1);
}

Matrix result(row, column); // 创建一个结果矩阵

// 将对应位置的元素相加
for (int i = 0; i < row * column; ++i) {
result.content[i] = content[i] + other.content[i];
}

return result;
}

// 重载运算符"<<"
friend ostream& operator<<(ostream& os, const Matrix& matrix) {
for (int i = 0; i < matrix.row; ++i) {
for (int j = 0; j < matrix.column; ++j) {
os << matrix.content[i * matrix.column + j] << " ";
}
os << endl;
}
return os;
}
};

int main() {
// 创建两个矩阵并初始化
Matrix matrix1(2, 2);
matrix1 = {1, 2, 3, 4};

Matrix matrix2(2, 2);
matrix2 = {5, 6, 7, 8};

// 计算两个矩阵的和
Matrix result = matrix1 + matrix2;

// 输出结果矩阵
cout << "Result Matrix:" << endl;
cout << result;

return 0;
}

这段代码实现了一个表示矩阵的类 Matrix。类中包含了私有数据成员 rowcolumn 表示矩阵的行数和列数,以及 content 表示矩阵的值。通过构造函数对矩阵进行初始化,并通过析构函数释放动态分配的内存。重载了运算符 + 用于矩阵的加法运算,并重载了运算符 << 用于矩阵的输出。在 main() 函数中,创建了两个矩阵并初始化,然后计算它们的和并输出结果。

中南大学考试试卷

一、 填空题

  1. 重载后的运算符保持其原有的 (1) 、 (2) 和结合性不变。
  2. 在定义成员函数时,函数体之前加上关键字 (3) 防止函数改变数据成员的值。
  3. 若要把函数:void fun(A &a)说明为类A的友元函数,则应在类A的定义中加入函数声明语句 (4) 。
  4. 设有类A,要将后缀形式的自增运算符“++”重载为其成员函数,重载函数声明为: (5) 。
  5. 抽象类中至少要有一个 (6) 函数。
  6. 静态成员函数、友元函数、构造函数和析构函数中,不属于成员函数的是 (7) 。
  7. 假定一个类A有两个int型的数据成员a和b,且类A有如下两个构造函数:

A(int aa, int bb, int cc) {a = ++aa; b = aa*bb +cc;}

A(int aa = 4, int bb = 2) { a = aa++; b = aa*bb;}

则创建对象A x1(4, 5, 6), x2(3);后,x1.a的值为 (8) ;x2.b的值为 (9) 。

  1. 在删除一个动态对象时,将自动调用该动态对象所属类的 (10) 函数。
  2. 在C++中,三种继承方式的说明符分别为 (11) 、public、 (12) ,如果不加说明,则默认的继承方式为 (13) 。
  3. 如果只想保留公共基类的一个复制,必须使用关键字 (14) 把这个公共基类声明为虚基类。
  4. 在函数前面用关键字 (15)修饰时,则表示该函数表为内联函数。

(1) 操作参数个数 (2) 优先级 (3) const (4) friend void fun(A &a)

(5) A operator++(int) (6) 纯虚 (7) 友元函数 (8) 5 (9) 8

(10) 析构 (11) private (12) protected (11和12的答案可以对调)

(13) private (14) virtual (15) inline

  1. 重载后的运算符保持其原有的优先级、操作参数个数和结合性不变。这意味着,重载后的运算符在使用时仍然具有与原始运算符相同的优先级、参数个数和结合性。

  2. 在定义成员函数时,函数体之前加上关键字 const 可以防止函数改变数据成员的值。这样做是因为在 const 成员函数中,对数据成员的修改是被禁止的,只能访问但不能修改数据成员,从而确保了 const 成员函数的只读性。

  3. 若要把函数 void fun(A &a) 说明为类 A 的友元函数,则应在类 A 的定义中加入函数声明语句 friend void fun(A &a)

  4. 设有类 A,要将后缀形式的自增运算符“++”重载为其成员函数,重载函数声明为 A operator++(int)。这样的声明表示重载的自增运算符是类 A 的成员函数,并且是后缀形式的自增运算符。

  5. 抽象类中至少要有一个纯虚函数。纯虚函数是在基类中声明但在基类中没有定义的虚函数,它的目的是强制子类提供自己的实现。

  6. 静态成员函数、友元函数、构造函数和析构函数中,不属于成员函数的是友元函数。因为友元函数虽然可以访问类的私有成员,但并不属于类的成员函数,而是定义在类外部的独立函数。

  7. 假设一个类 A 有两个 int 型的数据成员 a 和 b,且类 A 有如下两个构造函数:

1
2
3
A(int aa, int bb, int cc) {a = ++aa; b = aa * bb + cc;}

A(int aa = 4, int bb = 2) {a = aa++; b = aa * bb;}

则创建对象 A x1(4, 5, 6),x2(3) 后,x1.a 的值为 5,x2.b 的值为 8。这是因为在第一个构造函数中,a 先自增再赋值,而在第二个构造函数中,a 先赋值再自增。

  1. 在删除一个动态对象时,将自动调用该动态对象所属类的析构函数。析构函数是用于释放对象占用的资源,它会在对象销毁时自动调用。

  2. 在 C++ 中,三种继承方式的说明符分别为 publicprotectedprivate。如果不加说明,则默认的继承方式为 private。这指的是在派生类继承基类时,如果不指定继承方式,则默认为私有继承,意味着基类的公有和保护成员在派生类中都变成了私有成员。

  3. 如果只想保留公共基类的一个复制,必须使用关键字 virtual 把这个公共基类声明为虚基类。这是因为在多重继承中,如果某个基类被多个派生类直接或间接继承,并且希望在最终派生类中只保留一个基类对象,就需要将这个基类声明为虚基类,以确保只有一个共享的基类子对象。

二、 程序阅读题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
// 1.
#include<iostream>
using namespace std;

class BaseClass
{
public:
BaseClass()
{
cout << "基类构造函数" << endl;
}
virtual void OutPut1()
{
cout << "基类输出函数1" << endl;
}
void OutPut2()
{
cout << "基类输出函数2" << endl;
}
};

class DerivedClass: public BaseClass
{
public:
DerivedClass()
{
cout << "派生类构造函数" << endl;
}
virtual void OutPut1()
{
cout << "派生类输出函数1" << endl;
}
void OutPut2()
{
cout << "派生基类输出函数2" << endl;
}
};

int main()
{
BaseClass BaseObj, *pt;
DerivedClass DerivedObj;
pt = &BaseObj;
pt->OutPut1();
pt->OutPut2();
pt = &DerivedObj;
pt->OutPut1();
pt->OutPut2();
}

// 运行结果:
// 基类构造函数
// 基类构造函数
// 派生类构造函数
// 基类输出函数1
// 基类输出函数2
// 派生类输出函数1
// 基类输出函数2

// 2.
#include <iostream>
using namespace std;

class Base
{
int Y;
public:
Base(int y=0)
{
Y=y;
cout << "Base(" << y << ")\n";
}
~Base()
{
cout << "~Base()\n";
}
void print()
{
cout << Y << endl;
}
};

class Derived:public Base
{
int Z;
public:
Derived(int y, int z)
{
Z=z;
cout << "Derived(" << y << "," << z << ")\n";
}
~Derived()
{
cout << "~Derived()\n";
}
void print()
{
Base::print();
cout << Z << endl;
}
};

int main()
{
Base b(10);
Derived d(10,20);
b.print();
d.print();
}

// 运行结果:
/*
Base(10)
Base(0)
Derived(10, 20)
10
0
20
~Derived()
~Base()
~Base()

*/
// 3.
#include <iostream>
using namespace std;

class Count
{
private:
static int counter;
int id;
public:
Count()
{
counter++;
id = counter;
}
void display()
{
cout << "counter=" << counter;
cout << ", ID=" << id << endl;
}
~Count()
{
cout << "对象" << id << "的析构函数\n";
}
};

int Count::counter=0;

int main()
{
Count a1;
a1.display();
Count a2, a3;
a2.display();
a3.display();
}

// 运行结果:
// counter=1, ID=1
// counter=3, ID=2
// counter=3, ID=3
// 对象3的析构函数
// 对象2的析构函数
// 对象1的析构函数
  1. 这段代码首先创建了一个 BaseClass 类型的对象 BaseObj 和一个 DerivedClass 类型的对象 DerivedObj,并输出了它们的构造函数调用情况。然后通过基类指针 pt 分别指向 BaseObjDerivedObj,并调用它们的虚函数 OutPut1() 和非虚函数 OutPut2()。由于 OutPut1() 是虚函数,在派生类中进行了重写,因此会根据指针所指向的对象的实际类型而调用相应的函数。而 OutPut2() 不是虚函数,因此只会根据指针类型来调用,不会根据对象的实际类型来调用。

  2. 这段代码首先创建了一个 Base 类型的对象 b 和一个 Derived 类型的对象 d,并输出了它们的构造函数调用情况。然后分别调用了它们的成员函数 print()。由于在 Derived 类中重写了 print() 函数,并且在其中调用了基类的 print() 函数,因此调用 d.print() 时会先调用基类的 print() 函数再输出派生类的成员变量 Z。而 b.print() 则只调用了基类的 print() 函数。

  3. 这段代码创建了三个 Count 类型的对象 a1a2a3,并输出了它们的构造函数调用情况。每次创建对象时,静态成员变量 counter 会递增,然后将当前计数值赋给对象的 id 成员。最后输出了每个对象的 ID,并且在程序结束时会自动调用每个对象的析构函数。因为 a1a2a3 的生命周期是函数内,所以它们的析构顺序与它们的创建顺序相反。


四、 简答题

  1. 什么是this指针,其作用是什么?
  2. 什么是多态性?说明静态多态性和动态多态性的区别。
  3. 简述new运算符和delete运算符的作用及其关系。
  1. this 指针是一个隐含在每个类的非静态成员函数中的特殊指针。它指向当前对象的地址,用于在类的成员函数中访问当前对象的成员变量和成员函数。其作用是让类的成员函数能够在不同的对象中正确地访问对象的成员变量和成员函数,即解决同名成员变量和成员函数的访问冲突。

  2. 多态性是面向对象编程中的一个重要概念,它允许同一操作作用于不同的对象上时产生不同的行为。静态多态性(编译时多态性)是指在编译阶段确定调用的函数或方法,主要通过函数重载和运算符重载来实现。动态多态性(运行时多态性)是指在程序运行时确定调用的函数或方法,主要通过虚函数和继承来实现。

  3. new 运算符用于在堆(动态内存)上分配内存空间来创建一个对象,并返回该对象的地址。它的作用是动态地分配内存空间,从而可以在程序运行时动态创建对象。与之相对应的是 delete 运算符,它用于释放通过 new 运算符分配的内存空间,即销毁动态分配的对象,并释放其占用的内存空间。deletenew 是一对相互配对的运算符,用于动态内存的分配和释放,保证内存的正确管理,避免内存泄漏。


六、 综合题

1、定义抽象基类Shape,由它派生出三个派生类:圆(Circle,数据成员radius表示圆的半径)、正方形(Square,数据成员side表示边长)、三角形(Triangle,数据成员len1, len2, len3分别表示三角形的三个边长),定义虚函数分别计算圆、正方形和三角形的面积。main函数中,定义如下对象:Circle circle(2.5); Square square(4.5); Triangle triangle(2.3, 3.4, 4.5),再定义一个基类指针数组,分别用数组的每一个元素指向一个派生类的对象,计算三个对象的面积及它们的和,并输出结果。

(提示:三角形的面积计算公式:S = 1/4sqrt[(a+b+c)(a+b-c)(a+c-b)(b+c-a)], a, b, c分别为三个边长)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
#include <iostream>
#include <cmath>
using namespace std;

// 抽象基类 Shape
class Shape {
public:
// 纯虚函数,用于计算面积
virtual double area() const = 0;
};

// 圆类 Circle
class Circle : public Shape {
private:
double radius; // 圆的半径
public:
// 构造函数
Circle(double r) : radius(r) {}

// 重写基类的虚函数,计算圆的面积
double area() const override {
return 3.1415926 * radius * radius;
}
};

// 正方形类 Square
class Square : public Shape {
private:
double side; // 正方形的边长
public:
// 构造函数
Square(double s) : side(s) {}

// 重写基类的虚函数,计算正方形的面积
double area() const override {
return side * side;
}
};

// 三角形类 Triangle
class Triangle : public Shape {
private:
double len1, len2, len3; // 三角形的三个边长
public:
// 构造函数
Triangle(double a, double b, double c) : len1(a), len2(b), len3(c) {}

// 重写基类的虚函数,计算三角形的面积
double area() const override {
double s = (len1 + len2 + len3) / 2.0;
return sqrt(s * (s - len1) * (s - len2) * (s - len3));
}
};

int main() {
// 定义圆、正方形和三角形对象
Circle circle(2.5);
Square square(4.5);
Triangle triangle(2.3, 3.4, 4.5);

// 定义一个基类指针数组,分别指向这些对象
Shape *shapes[3] = {&circle, &square, &triangle};

// 计算三个对象的面积及它们的和
double totalArea = 0.0;
for (int i = 0; i < 3; ++i) {
cout << "第" << i+1 << "个图形的面积为:" << shapes[i]->area() << endl;
totalArea += shapes[i]->area();
}
cout << "总面积为:" << totalArea << endl;

return 0;
}

答案解释:

  • 在这段代码中,首先定义了一个抽象基类 Shape,其中包含了一个纯虚函数 area(),用于计算各种形状的面积。
  • 然后分别定义了三个派生类:Circle(圆)、Square(正方形)和Triangle(三角形),并重写了基类的虚函数 area(),分别计算了圆、正方形和三角形的面积。
  • main() 函数中,创建了圆、正方形和三角形的对象,并将它们存储在基类指针数组中。通过循环遍历数组,计算每个对象的面积并输出,然后累加得到总面积。

2、已知如下的类继承层次,解答后面的2个问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
class B1

{ public:

B1(){ cout << "Base1" << endl; }

~B1(){cout << "~Base1" << endl; }

};

class B2 : public B1

{ public:

B2(){ cout << "Base2" << endl; }

~B2(){ cout << "~Base2" << endl; }

};

class D1: virtual public B2

{ public:

D1(){ cout << "Derived1" << endl; }

~D1(){cout << "~Derived1" << endl; }

};



class D2 : virtual public Base2

{ public:

D2(){ cout << "Derived2" << endl; }

D2(){ cout << "~Derived2" << endl;}

};

class MI : public D1, public D2

{ public:

MI(){ cout << "MI" << endl; }

~MI(){ cout << "~MI" << endl; }

};

class Final : public MI, public B1

{ public:

Final(){ cout << "Final " << endl; }

~Final(){ cout << "~Final " << endl;}

};

这个类继承层次包含了几个类和它们的构造函数和析构函数,它们的继承关系如下:

  1. B1 是一个基类,拥有默认构造函数和析构函数,构造函数输出 "Base1",析构函数输出 "~Base1"。
  2. B2B1 的派生类,继承了 B1,拥有默认构造函数和析构函数,构造函数输出 "Base2",析构函数输出 "~Base2"。
  3. D1B2 的派生类,继承了 B2,并使用虚继承,拥有默认构造函数和析构函数,构造函数输出 "Derived1",析构函数输出 "~Derived1"。
  4. D2B2 的派生类,继承了 B2,并使用虚继承,拥有默认构造函数和析构函数,构造函数输出 "Derived2",析构函数输出 "~Derived2"。
  5. MI 是多重继承自 D1D2,拥有默认构造函数和析构函数,构造函数输出 "MI",析构函数输出 "~MI"。
  6. Final 是多重继承自 MIB1,拥有默认构造函数和析构函数,构造函数输出 "Final",析构函数输出 "~Final"。

每个类的构造函数和析构函数在对象创建和销毁时被调用,根据类的继承关系和虚继承的特点,构造函数和析构函数的调用顺序如下:

  1. 创建 Final 类的对象时,首先调用 B1 的构造函数,输出 "Base1"。
  2. 接着调用 B2 的构造函数,输出 "Base2"。
  3. 然后调用 D1 的构造函数,输出 "Derived1"。
  4. 再调用 D2 的构造函数,输出 "Derived2"。
  5. 最后调用 MI 的构造函数,输出 "MI"。
  6. 对象创建完成,开始销毁对象时,调用析构函数的顺序与构造函数相反,先调用 MI 的析构函数,输出 "~MI"。
  7. 然后调用 D2 的析构函数,输出 "~Derived2"。
  8. 接着调用 D1 的析构函数,输出 "~Derived1"。
  9. 再调用 B2 的析构函数,输出 "~Base2"。
  10. 最后调用 B1 的析构函数,输出 "~Base1"。
  1. 一个Final类对象含有多少个B1子对象?含有多少个B2子对象?
  2. 下面的程序段①-⑥行中哪3行赋值会引起编译时刻错误?解释错误原因。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
B1 *pb1, b1;

B2 *pb2, b2;

D1 *pd1;

D2 *pd2;

MI *pmi, mi;

① pb2 = &b1;

② pd2= new Final;

③ pmi = pb1;

④ pd2 = pmi;

⑤ pd1 = &mi;

⑥ pd1 = &b2;

问题1:一个Final类对象含有多少个B1子对象?含有多少个B2子对象?

  1. B1子对象数量:
    • B2B1的派生类,因此B2包含一个B1子对象。
    • D1D2都虚继承自B2,这意味着它们会共享同一个B2子对象。
    • MI继承自D1D2,由于D1D2都虚继承自B2,所以在MI中只会有一个共享的B2子对象。
    • Final继承自MIB1,由于MI中已经包含了一个通过B2传递的B1子对象,且Final直接继承了一个新的B1子对象,因此最终在Final类中会有两个B1子对象。
  2. B2子对象数量:
    • 由于D1D2虚继承自B2,它们在MI中共享同一个B2子对象。
    • 所以,Final类中包含一个B2子对象。

综上,一个Final类对象含有2个B1子对象和1个B2子对象。

问题2:编译时错误分析

1
2
3
4
5
6
7
8
9
10
11
12
B1 *pb1, b1;
B2 *pb2, b2;
D1 *pd1;
D2 *pd2;
MI *pmi, mi;

① pb2 = &b1; // 错误
② pd2 = new Final; // 正确
③ pmi = pb1; // 错误
④ pd2 = pmi; // 错误
⑤ pd1 = &mi; // 正确
⑥ pd1 = &b2; // 错误

解释:

  1. ① pb2 = &b1;
    • 错误原因:pb2是指向B2的指针,而&b1是指向B1的地址。由于B1不是B2的子类,无法进行这种类型转换。
  2. ② pd2 = new Final;
    • 正确:pd2是指向D2的指针,而FinalD2的派生类。可以将Final对象的地址赋给D2指针,这是向上类型转换。
  3. ③ pmi = pb1;
    • 错误原因:pmi是指向MI的指针,而pb1是指向B1的指针。由于B1不是MI的基类,无法进行这种类型转换。
  4. ④ pd2 = pmi;
    • 错误原因:pd2是指向D2的指针,而pmi是指向MI的指针。虽然MID2的派生类,但直接赋值需要进行显式类型转换或使用dynamic_cast
  5. ⑤ pd1 = &mi;
    • 正确:pd1是指向D1的指针,而&mi是指向MI对象的地址。由于MID1的派生类,可以进行这种类型转换。
  6. ⑥ pd1 = &b2;
    • 错误原因:pd1是指向D1的指针,而&b2是指向B2对象的地址。由于B2不是D1的子类,无法进行这种类型转换。

总结:

  • 会引起编译错误的行是: ①,③,④,⑥。