C++期末考试
华南理工大学期末考试
一.单项选择题(每题2分,共20分)
1. 关于this指针的说法错误的是( )
this指针必须显示说明
当创建一个对象后,this指针就指向该对象
成员函数拥有this指针
静态成员函数不拥有this指针
关于第一个问题:
- 关于this指针的说法错误的是( A )
this指针必须显示说明 —— 错误。this指针是隐式的,不需要显式说明。
当创建一个对象后,this指针就指向该对象 —— 正确。this指针指向调用成员函数的对象。
成员函数拥有this指针 —— 正确。每个成员函数内部都有一个this指针,指向调用该函数的对象。
静态成员函数不拥有this指针 —— 正确。静态成员函数不属于任何特定对象,因此没有this指针。
因此,答案是A。
this指针是隐式的 ***
2. 下面对构造函数的不正确描述是( )
系统可以提供默认的构造函数
构造函数可以有参数,所以可以有返回值
构造函数可以重载
构造函数可以设置默认参数
关于第二个问题:
- 下面对构造函数的不正确描述是( B )
系统可以提供默认的构造函数 —— 正确。如果用户未定义任何构造函数,系统会提供一个默认的构造函数。
构造函数可以有参数,所以可以有返回值 —— 错误。构造函数可以有参数,但不能有返回值(包括void)。
构造函数可以重载 —— 正确。构造函数可以重载,即同一个类可以有多个构造函数,只要参数列表不同。
构造函数可以设置默认参数 —— 正确。构造函数可以像普通函数一样,设置默认参数。
因此,答案是B。
显然构造函数没有返回值 ***
3. 在创建派生类对象时,构造函数的执行顺序是( )
对象成员构造函数、基类构造函数、派生类本身的构造函数
派生类本身的构造函数、基类构造函数、对象成员构造函数
基类构造函数、派生类本身的构造函数、对象成员构造函数
基类构造函数、对象成员构造函数、派生类本身的构造函数
在创建派生类对象时,构造函数的执行顺序是( D )
正确的执行顺序是:
- 基类构造函数
- 对象成员构造函数
- 派生类本身的构造函数
因此,答案是D。
4. 在C++中,要实现动态联编,必须使用( )调用虚函数
- 类名 B) 派生类指针 C) 对象名 D) 基类指针
在C++中,要实现动态联编,必须使用( D )调用虚函数。
正确的解释是:
- 动态联编(也称为动态绑定或运行时多态)是通过基类指针或基类引用来调用虚函数实现的。这允许在运行时确定调用哪个函数。
因此,答案是D。
基类指针调用即可,这也是多态的实现过程 ***
5. 下面描述中,正确的是( )
虚函数是没有实现的函数 B) 纯虚函数的实现在派生类中实现
抽象类是没有纯虚函数的类 D) 抽象类指针不可以指向不同的派生类
下面描述中,正确的是( B )
解释如下:
虚函数是没有实现的函数 —— 错误。虚函数是有实现的函数,但可以被派生类重写。
纯虚函数的实现在派生类中 —— 正确。纯虚函数在基类中没有实现(仅有声明),需要在派生类中提供实现。
抽象类是没有虚函数的类 —— 错误。抽象类是包含至少一个纯虚函数的类。
抽象类指针不可以指向不同的派生类 —— 错误。抽象类指针可以指向不同的派生类对象,这是多态的基础。
很容易看出来其他三个错
因此,正确的答案是B。
6.( )不是属于面向对象程序设计的特性
- 抽象性 (B) 数据相关性 (C) 多态性 (D) 继承性
关于第六个问题:
( B ) 不是属于面向对象程序设计的特性
解释:
抽象性、多态性、继承性是面向对象程序设计的三大基本特性。
数据相关性不是面向对象程序设计的特性。
因此,答案是B。
7.要求打开文件 D:.txt,并能够写入数据,正确的语句是( )。
ifstream infile( "D:\file. txt ", ios::in ) ;
ifstream infile( "D:\file. txt ", ios::out ) ;
ofstream outfile( "D:\file. txt ", ios::in ) ;
fstream iofile( "D:\file. txt ", ios::in | ios::out ) ;
要求打开文件 D:.txt,并能够写入数据,正确的语句是( D )。
解释:
- (A)
ifstream infile( "D:\\file.txt", ios::in ) ;
:ifstream
是用于输入文件流的类,ios::in
表示以读模式打开文件。这不会满足写入数据的需求。
- (B)
ifstream infile( "D:\\file.txt", ios::out ) ;
:ifstream
仅用于读取,不能用于写入,即使指定了ios::out
。
- (C)
ofstream outfile( "D:\\file.txt", ios::in ) ;
:ofstream
是用于输出文件流的类,但ios::in
表示读模式,这与写入数据的需求不符。
- (D)
fstream iofile( "D:\\file.txt", ios::in | ios::out ) ;
:fstream
可以同时读写文件,ios::in | ios::out
表示以读写模式打开文件,这正是需要的功能。
因此,正确的答案是D。
很详细的解释 ***
8. 如果一个类的成员函数print()不修改类的数据成员值,则应将其声明为( )
void print() const; (B) const void print();
void const print(); (D) void print(const);
如果一个类的成员函数 print() 不修改类的数据成员值,则应将其声明为 ( A )
解释:
- 当成员函数不修改类的数据成员时,应将其声明为 const
成员函数。
- const
成员函数的声明方式是在成员函数的声明末尾添加
const
关键字。
因此,正确的答案是 A) void print() const;
9. 下列关于构造函数的论述中,不正确的是( )
构造函数的函数名与类名相同 (B) 构造函数可以设置默认参数
构造函数的返回类型缺省为int型 (D) 构造函数可以重载
- 下列关于构造函数的论述中,不正确的是 ( C )
解释:
- (A) 构造函数的函数名与类名相同 —— 正确。构造函数的名称必须与类名相同。
- (B) 构造函数可以设置默认参数 —— 正确。构造函数可以像其他函数一样设置默认参数。
- (C) 构造函数的返回类型缺省为int型 —— 错误。构造函数没有返回类型,甚至不返回void。
- (D) 构造函数可以重载 —— 正确。可以定义多个构造函数,只要它们的参数列表不同。
因此,正确答案是不正确的选项 C。
显而易见 ***
10. 若有以下类T声明,函数fF的错误定义是( )。
1 | class T |
void fF (T &objT,int k) { objT.i = k+1; }
void fF (T &objT,int k) { k = objT.i+1; }
void T::fF (T &objT,int k) { k += objT.i; }
void fF (T &objT,int k) { objT.i += k; }
若有以下类T声明,函数fF的错误定义是 ( C )。
解释:
- (A) void fF (T &objT, int k) { objT.i = k+1; }
——
正确。符合友元函数的定义格式,可以访问类T的私有成员。
- (B) void fF (T &objT, int k) { k = objT.i+1; }
——
正确。符合友元函数的定义格式,可以访问类T的私有成员。
- (C) void T::fF (T &objT, int k) { k += objT.i; }
—— 错误。友元函数不属于类的成员函数,因此不能用类的作用域解析运算符
::
来定义。
- (D) void fF (T &objT, int k) { objT.i += k; }
——
正确。符合友元函数的定义格式,可以访问类T的私有成员。
因此,正确答案是不正确的选项 C。
C一眼错误了,因为友元不是一部分 ***
二.简答题(每小题4分,共20分)
1.若程序员没有定义复制构造函数,则编译器自动生成一个缺省的复制构造函数,它可能会产生什么问题?
若程序员没有定义复制构造函数,则编译器自动生成一个缺省的复制构造函数,这可能会产生以下问题: 1. 浅拷贝问题:
缺省的复制构造函数执行的是浅拷贝(shallow copy),即简单地逐个复制对象的所有成员。如果对象包含指向动态内存的指针,浅拷贝会导致多个对象指向同一块动态内存区域。这样,当其中一个对象被销毁或修改时,可能会影响到其他对象,造成未定义行为或内存泄漏。
- 资源管理问题:
对于管理资源的类(如文件句柄、网络连接等),缺省的复制构造函数可能无法正确管理资源的复制和释放。例如,当两个对象共享一个文件句柄时,如果一个对象关闭了文件句柄,另一个对象会变成一个无效状态。
- 双重释放问题:
当多个对象指向同一块动态分配的内存时,如果一个对象被析构,释放了该内存,其他对象在析构时再次尝试释放同一块内存,就会导致双重释放(double free)错误,可能导致程序崩溃。
- 不一致的状态:
浅拷贝可能导致对象处于不一致的状态。如果对象的成员之间存在复杂的依赖关系,浅拷贝会破坏这种依赖,导致对象在后续操作中行为异常。
为了避免这些问题,程序员可以手动定义复制构造函数,确保对象在复制时能够正确处理其内部资源。例如,使用深拷贝(deep copy)来复制动态分配的内存或资源,确保每个对象都有自己独立的资源副本。这样可以避免浅拷贝带来的问题,确保对象在复制后能正常工作。
2、有类定义
1 | class Test |
函数Test::Test和Test::Set的参数和执行代码完全一样,它们的功能和执行时机有什么区别?请写出简单代码说明。
函数 Test::Test
和 Test::Set
的参数和执行代码虽然完全一样,但它们的功能和执行时机有很大的区别。
- 构造函数
Test::Test
:- 功能:用于初始化对象。在对象创建时自动调用,用于设置对象的初始状态。
- 执行时机:在创建对象时执行,只执行一次。
- 成员函数
Test::Set
:- 功能:用于修改对象的状态。在对象创建后可以多次调用,用于更新对象的成员变量。
- 执行时机:对象创建后,可以根据需要多次调用。
下面是一个简单的代码说明:
1 |
|
输出结果:
1 | Constructor called: a = 1, b = 2 |
解释:
- 当我们创建对象
obj
时,构造函数Test(int m, int n)
被调用,初始化成员变量a
和b
为 1 和 2。 - 调用
obj.Set(3, 4)
时,成员函数Set
被调用,更新成员变量a
和b
为 3 和 4。 - 再次调用
obj.Set(5, 6)
时,成员函数Set
被调用,更新成员变量a
和b
为 5 和 6。
总结:
- 构造函数用于对象的初始化,只在对象创建时调用一次。
- 成员函数
Set
可以在对象的生命周期内多次调用,用于修改对象的状态。
3.设有
1 | double PI=3.1415; |
请分别写出把PI的值分别写入文本文件d:1.txt和二进制文件d:2.dat当前位置的语句。这个浮点数据在两个文件中各占的字节数相同吗?为什么?
要将 PI 的值分别写入文本文件 d:1.txt 和二进制文件 d:2.dat,可以使用 ofstream 对象的输出操作符。
以下是将 PI 写入文件的语句:
1 | // 写入文本文件 |
在文本文件中,PI 的值会以文本形式被写入,例如 "3.1415";而在二进制文件中,PI 的值会以二进制形式被写入,实际上是将 PI 变量的内存表示写入文件。
浮点数在文本文件和二进制文件中占用的字节数通常不同。在文本文件中,浮点数的表示通常包含小数点、可能的指数部分、可能的正负号等,因此会占用相对较多的字节数;而在二进制文件中,浮点数通常按照 IEEE 754 标准的格式存储,通常是 4 字节或 8 字节,不包含额外的表示信息。
因此,浮点数在文本文件和二进制文件中占用的字节数通常是不同的。
4.有人定义一个教师类派生一个学生类。他认为“姓名”和“性别”是教师、学生共有的属性,声明为public,“职称”和“工资”是教师特有的,声明为private。在学生类中定义特有的属性“班级”和“成绩”。所以有
1 | class teacher |
你认为这样定义合适吗?请做出你认为合理的类结构定义。
在这个定义中,使用了继承来表示“学生”是“教师”的一种特殊情况。然而,继承表示了一种 is-a 的关系,而学生和教师并不是同一个实体,因此不适合使用继承来表示学生是教师的一种特殊情况。
此外,将教师和学生的共有属性直接放在了教师类中,这样做并不合理,因为姓名和性别是个人的基本属性,不应该将它们放在教师类中。另外,将职称和工资声明为私有成员也有问题,因为职称和工资是教师的重要属性,应该是公有的,供外部访问和修改。
下面给出一个更合理的类结构定义:
1 | class Person { |
在这个定义中,Person 类包含了姓名和性别两个共有属性,Teacher 类和 Student 类分别继承 Person 类,表示教师和学生都是人的一种。Teacher 类和 Student 类各自定义了自己特有的属性,符合面向对象设计的单一职责原则,使得类的结构更加清晰和合理。
5.在具有虚继承的类体系中,建立派生类对象时以什么顺序调用构造函数?
在具有虚继承的类体系中,建立派生类对象时,构造函数的调用顺序如下:
- 首先调用虚基类的构造函数。
- 然后调用直接基类的构造函数。
- 最后调用派生类自身的构造函数。
这样的顺序确保了虚基类的构造函数只被调用一次,而且在所有派生类构造函数之前被调用,从而避免了多次构造虚基类的问题。
以下是一个具有虚继承的类体系的简单示例:
1 |
|
在这个例子中,Base
类是一个基类,VirtualBase
类是一个虚基类,Derived
类是一个派生类。VirtualBase
类继承自 Base
类,并使用 virtual
关键字标记,表示虚继承。
当创建 Derived
类的对象时,构造函数的调用顺序如下:
- 首先调用虚基类
Base
的构造函数。 - 然后调用
VirtualBase
类的构造函数。 - 最后调用
Derived
类的构造函数。
因此,输出结果为:
1 | Base constructor called |
这个例子展示了在具有虚继承的类体系中,构造函数的调用顺序。
三.阅读下列程序,写出执行结果(每题4分,共20分)
1.
1 |
|
运行结果为:5049876321
解释:
- 初始数组为:{1, 2, 3, 4, 5, 6, 7, 8, 9, 0}
- 第一次调用
fun(p,0,3)
,将索引为0到3的元素进行反转:{4, 3, 2, 1, 5, 6, 7, 8, 9, 0} - 第二次调用
fun(p,4,9)
,将索引为4到9的元素进行反转:{4, 3, 2, 1, 0, 9, 8, 7, 6, 5} - 第三次调用
fun(p,0,9)
,将整个数组进行反转:{5, 6, 7, 8, 9, 0, 1, 2, 3, 4}
所以输出结果为:5049876321
2、分别写出文件和屏幕中的显示内容
1 |
|
这个程序主要实现了以下功能:
- 定义了两个整型变量
length
和width
,并计算它们的乘积,将结果存储在area
变量中。 - 打开一个文件 "E:\data.txt",并将
length
、width
和area
的值写入文件中。 - 关闭文件,然后再次打开相同的文件以读取数据。
- 从文件中读取数据,依次读取字符串和整数,将其存储在
str
和a
变量中。 - 输出从文件中读取的整数值
a
,即计算得到的面积值。
具体步骤如下:
首先,将
length
、width
和area
的值写入文件 "E:\data.txt" 中,格式如下:1
length: 10 width: 20 area: 200
然后,打开文件以读取数据。
从文件中依次读取字符串和整数,并存储到
str
和a
变量中。文件中的数据是按照 "length:0:0:00" 的格式写入的。依次读取每个字符串,并丢弃掉。然后将下一个整数值读取到变量
a
中。最终输出从文件中读取的整数值
a
,即计算得到的面积值为 200。
因此,程序的输出结果为 "area=200"。
3、
1 |
|
运行结果为:
1 | 9 21 |
解释:
- 首先定义了一个模板类
FF
,其中TT
是模板类型参数,表示类中的数据类型。 - 在类中定义了三个私有成员变量
a1
、a2
和a3
,它们的类型为TT
。 - 类中有一个构造函数
FF(TT b1, TT b2, TT b3)
,用于初始化这三个成员变量。 - 类中还有一个成员函数
TT Sum()
,用于计算三个成员变量的和,并返回结果。 - 在
main
函数中,定义了两个FF<int>
类型的对象x
和y
,分别初始化为 (2, 3, 4) 和 (5, 7, 9)。 - 调用对象的
Sum
方法,分别计算了x
和y
对象的成员变量之和,并将结果输出。
因此,输出结果为 9 21
。
4、
1 |
|
1 | Journal default constructor |
这是因为:
- 创建了一个
CComputerDesign
对象journal1
,它会调用CJournal
的构造函数和自己的构造函数。 journal1.subscribe()
调用了CComputerDesign
类中的虚函数subscribe()
,输出"Subscribing 《?Computer Design》¡¤"。journal1.read()
调用了CComputerDesign
类中的read()
函数,输出"Reading 《?Computer Design》¡¤"。- 将
journal1
的地址赋给了一个CJournal
指针p_journal
。 p_journal->subscribe()
调用的是CComputerDesign
类中的虚函数subscribe()
,因为是通过指针调用,所以会动态绑定到CComputerDesign
类的实现,输出"Subscribing 《?Computer Design》¡¤"。p_journal->read()
调用的是CJournal
类中的read()
函数,因为read()
函数不是虚函数,所以不会进行动态绑定,输出"Read paper"。- 最后,程序结束,会先调用
CComputerDesign
对象journal1
的析构函数,然后调用CJournal
类的析构函数。
这个代码涉及到以下几个知识点:
C++ 类与对象:代码定义了一个抽象的期刊类
CJournal
和一个继承自CJournal
的具体期刊类CComputerDesign
。对象journal1
是CComputerDesign
类的一个实例。构造函数和析构函数:每个类都有构造函数和析构函数。构造函数在对象创建时调用,析构函数在对象销毁时调用。在本例中,构造函数和析构函数被用来输出一些信息以跟踪对象的创建和销毁过程。
虚函数和纯虚函数:
subscribe()
是一个纯虚函数,在基类CJournal
中声明为纯虚函数,意味着任何继承自CJournal
的类都必须提供其实现。这允许在基类中声明接口,而在派生类中提供特定实现。在CComputerDesign
类中,subscribe()
被重写了。虚函数和动态绑定:通过使用基类指针
CJournal *p_journal
指向派生类对象journal1
,代码展示了虚函数的动态绑定机制。当通过指针调用虚函数时,会根据指针所指向的对象类型来确定调用的函数实现。命名空间:
using namespace std;
语句使得std
命名空间中的标识符可以直接使用,无需在每次使用时显式添加命名空间前缀。程序入口点:程序的入口函数是
main()
,它是程序执行的起点。输出流:代码中使用了
cout
对象和相关的流操作符<<
来输出文本信息到标准输出流。
5、
1 |
|
根据提供的代码,我们可以预期以下输出:
1 | Array a contains: |
这是因为:
printArray()
函数接受一个指向常量数组的指针array
和一个表示数组元素个数的整数count
。- 在
main()
函数中,定义了三个不同类型的数组a
、b
和c
,并分别传递给printArray()
函数。 - 在
printArray()
函数中,使用for
循环遍历数组并输出每个元素。 - 对于整型数组
a
,输出每个整数并在之间添加空格,最后换行。 - 对于双精度浮点型数组
b
,同样输出每个浮点数并在之间添加空格,最后换行。 - 对于字符数组
c
,输出每个字符并在之间添加空格,最后换行。
请注意,字符数组输出的结果可能看起来不太直观,因为它输出了字符数组的每个字符,但是每个字符之间没有添加空格。
四. 根据程序输出填空。(每空2分,共20分)
- 在下面一段类的定义中,自行车类的虚基类为车辆类,机动车类的虚基类也为车辆类,摩托车类的基类为自行车类和机动车类,类之间均为公有继承。
1 | class vehicle //车辆类 |
1 | class bicycle : public vehicle //自行车类 |
在 C++ 中,使用 public
继承关系表示派生类继承了基类的公有成员和方法。
2. 下面是一个函数模板,用于计算两个向量的和。在下面横线处填上适当字句,完成函数模板定义。
1 | # include <iostream.h> |
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
template<class T>
T* f(T* a,T* b,int n)
{
T* c = new T[n]; // (4) 动态分配内存空间
for(int i=0; i<n; i++)
c[i] = a[i] + b[i]; // (5) 对应位置元素相加
return c;
}
int main()
{
int a[5] = {1,2,3,4,5}, b[5] = {10,20,30,40,50}, *p;
p = f(a, b, 5);
for(int i=0; i<5; i++)
std::cout << p[i] << std::endl; // (6) 输出p指向的数组元素
delete[] p; // 释放动态分配的内存空间
return 0;
}
在 C++ 中,new
关键字用于动态分配内存空间,delete
用于释放动态分配的内存空间。
4. 继承和虚函数
1 |
|
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
using namespace std;
class Power
{
public:
Power(int i) { x = i; }
virtual void display() { cout << "x=" << x; } // (7) 虚函数定义
protected:
int x;
};
class Square : public Power
{
public:
Square(int n) : Power(n) { } //Square类构造函数
void display() override // (8) 公有继承Power
{
Power::display();
cout << " x square=" << x * x << endl;
}
};
class Cube : public Power
{
public:
Cube(int n) : Power(n) { }
void display() override // (9) 覆盖display函数
{
Power::display();
cout << " x cube=" << x * x * x << endl;
}
};
void fun(Power &p) { p.display(); }
int main()
{
Square squ(2);
Cube cub(3);
fun(squ);
fun(cub);
return 0;
}
在 C++ 中,虚函数允许在派生类中进行动态绑定。因此,当
fun()
函数中的 Power
类型的参数引用一个
Square
或 Cube
类型的对象时,将根据对象的实际类型来调用对应的 display()
函数。
五、完成程序。(每小题10分,共20分)
1.使用函数模板可以实现对不同类型数组求平均值的功能,给定main()函数如下:
1 |
|
它分别求一个整型数组和一个双精度浮点型数组的平均值,补充一个函数模板定义以便实现上述功能。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
template<typename T>
T average(T arr[], int size) {
T sum = 0;
for (int i = 0; i < size; ++i) {
sum += arr[i];
}
return sum / size;
}
int main() {
int a[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
double b[] = {1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8, 9.9, 10.0};
std::cout << "Average of array a: " << average(a, 10) << std::endl;
std::cout << "Average of array b: " << average(b, 10) << std::endl;
return 0;
}
这段代码定义了一个函数模板
average()
,它可以接受任意类型的数组和数组的大小作为参数,并计算数组元素的平均值。在
main()
函数中,分别传递了一个
2.已知有“d: courseFile.txt”文件如下图所示格式:
编写一个函数,读出这个文本文件中的课程号、学时和学分数据,把这些数据写入二进制文件“d: courseFile.dat”中。函数原型为:
1 | void createBinaryFile( char * filetxt, char * fileData ); |
你可以使用以下代码来实现所需的函数:
1 |
|
这段代码实现了从文本文件中读取课程数据,然后将数据写入二进制文件中。在
createBinaryFile
函数中,首先打开文本文件进行读取,然后打开二进制文件进行写入。然后,通过一个循环读取文本文件中的课程号、学时和学分数据,并将这些数据写入二进制文件中。