类与对象练习题
类和对象习题
4-1 解释public和private的作用,公有类型成员与私有类型成员有哪些区别?
在面向对象编程(OOP)中,
public
和private
是用于定义类成员(属性和方法)可见性的重要访问修饰符。它们的主要作用是控制类内部的实现细节对外部代码的可见性和可访问性,以实现数据封装和信息隐藏。下面是对它们的详细解释:
public
修饰符
- 可访问性:使用
public
修饰的类成员可以被任何代码访问,无论是类内部还是类外部,甚至是不同的包或模块中的代码都可以访问。- 使用场景:通常用于那些需要被外部代码频繁使用的接口、方法或属性。例如,getter和setter方法,提供对对象状态的控制访问。
private
修饰符
- 可访问性:使用
private
修饰的类成员只能被类内部的其他成员(包括方法和属性)访问。外部代码(包括子类)无法直接访问这些成员。- 使用场景:通常用于那些不希望被外部代码直接访问和修改的实现细节。例如,内部状态、辅助方法等。这种封装机制有助于保护数据的完整性和一致性,避免外部对其进行不适当的操作。
公有类型成员与私有类型成员的区别
- 访问权限:
- 公有成员(public):可以被任何地方的代码访问。
- 私有成员(private):只能被该类内部的代码访问,无法被类外部或者子类的代码访问。
- 数据封装:
- 公有成员(public):提供开放的接口,容易被外部滥用,可能导致数据不一致或意外修改。
- 私有成员(private):隐藏了类的内部实现细节,只通过公开的接口(例如
public
方法)与外部交互,增强了数据的安全性和完整性。- 继承关系:
- 公有成员(public):在子类中可以直接访问。
- 私有成员(private):即使在子类中也不能直接访问。
- 维护性:
- 公有成员(public):由于接口是公开的,更改时需要考虑到对外部代码的影响,维护成本较高。
- 私有成员(private):更改只需在类内部进行,外部代码无须修改,维护成本较低。
示例
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 class MyClass {
public:
int publicValue; // 公有成员
MyClass(int value) {
publicValue = value;
privateValue = value;
}
// 公有方法
void setPrivateValue(int value) {
privateValue = value;
}
int getPrivateValue() {
return privateValue;
}
private:
int privateValue; // 私有成员
};
int main() {
MyClass obj(10);
// 直接访问公有成员
obj.publicValue = 20;
// 通过公有方法访问私有成员
obj.setPrivateValue(30);
int value = obj.getPrivateValue();
// 直接访问私有成员(编译错误)
// obj.privateValue = 40;
return 0;
}在上述示例中:
publicValue
是公有成员,可以直接通过对象访问和修改。privateValue
是私有成员,无法直接通过对象访问,只能通过公有方法setPrivateValue
和getPrivateValue
进行访问和修改。这样确保了对私有数据的控制,防止了不合适的直接修改。
4-2 protected关键字有何作用?
在面向对象编程中,
protected
关键字用于定义类成员(属性和方法)的访问权限。与public
和private
不同,protected
主要用于在类继承中控制成员的可见性和访问权限。以下是对protected
关键字的详细解释:
protected
修饰符
- 可访问性:使用
protected
修饰的类成员可以被以下代码访问:
- 类内部的代码。
- 该类的派生类(子类)的代码。
- 但不能被类外部的非派生类代码直接访问。
protected
的作用和使用场景
- 继承和访问控制:
- 作用:允许派生类访问基类的成员,但不允许非派生类直接访问。这种机制使得基类能够将一些实现细节暴露给派生类,但对外部类保持隐藏。
- 使用场景:当基类中的某些成员需要在子类中使用,但不希望它们被外部代码直接访问时,可以使用
protected
修饰这些成员。- 数据封装和继承机制的结合:
- 作用:
protected
关键字在数据封装和继承机制之间提供了一个平衡点。它允许子类继承和使用基类的部分实现细节,从而促进代码重用,同时保持了对外部代码的封装性。- 使用场景:适用于需要在多个子类中共享的通用功能或数据,但这些功能或数据又不应该成为公共接口的一部分。
示例
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 class Base {
protected:
int protectedValue; // 保护成员
public:
Base(int value) : protectedValue(value) {}
// 公有方法
void setProtectedValue(int value) {
protectedValue = value;
}
int getProtectedValue() {
return protectedValue;
}
};
class Derived : public Base {
public:
Derived(int value) : Base(value) {}
void display() {
// 访问基类的保护成员
std::cout << "Protected Value: " << protectedValue << std::endl;
}
};
int main() {
Derived obj(10);
// 通过公有方法访问保护成员
obj.setProtectedValue(20);
std::cout << "Protected Value through public method: " << obj.getProtectedValue() << std::endl;
// 直接访问保护成员(编译错误)
// std::cout << obj.protectedValue << std::endl;
obj.display(); // 调用派生类的方法,间接访问保护成员
return 0;
}在上述示例中:
protectedValue
是基类Base
中的保护成员。- 在派生类
Derived
中,可以直接访问和使用protectedValue
,例如在display
方法中。- 在
main
函数中,通过setProtectedValue
和getProtectedValue
公有方法间接访问和修改protectedValue
。- 尝试在
main
函数中直接访问protectedValue
会导致编译错误,因为它在main
函数的上下文中不可访问。总结
protected
关键字在面向对象编程中的主要作用是:
- 控制继承中的访问权限:允许派生类访问基类的保护成员,但不允许非派生类直接访问。
- 平衡数据封装和继承机制:既能促进代码重用,又能保持对外部代码的封装性。
- 适用于共享功能和数据:在需要多个子类共享的通用功能或数据的场景中,使用
protected
可以实现这种共享,同时保持适当的封装。通过使用
protected
关键字,开发者可以更好地设计类的继承结构,确保实现细节只暴露给需要的子类,同时保持对外部代码的良好封装性。
4-3 构造函数和析构函数有什么作用?
构造函数(constructor)和析构函数(destructor)是类的重要成员函数,分别用于对象的初始化和清理工作。它们在对象的生命周期中扮演着关键角色,确保资源的正确分配和释放。下面是对构造函数和析构函数详细作用的解释:
构造函数(Constructor)
作用
- 对象初始化:构造函数在创建对象时被自动调用,用于初始化对象的成员变量。
- 资源分配:构造函数可以分配资源,如内存、文件句柄、网络连接等。
- 执行初始操作:构造函数可以执行任何需要在对象创建时进行的初始操作,如设定默认值、建立数据连接等。
特点
- 名称与类名相同:构造函数的名称必须与类名相同,且没有返回类型(包括
void
)。- 可以重载:一个类可以有多个构造函数,只要它们的参数列表不同(重载)。
- 可以带有参数:构造函数可以带有参数,用于灵活初始化对象。
示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 class MyClass {
public:
int value;
// 默认构造函数
MyClass() {
value = 0;
std::cout << "Default Constructor called" << std::endl;
}
// 带参数的构造函数
MyClass(int val) {
value = val;
std::cout << "Parameterized Constructor called" << std::endl;
}
};
int main() {
MyClass obj1; // 调用默认构造函数
MyClass obj2(10); // 调用带参数的构造函数
return 0;
}在这个示例中:
MyClass
有两个构造函数:一个是默认构造函数,一个是带参数的构造函数。- 创建
obj1
时,调用了默认构造函数;创建obj2
时,调用了带参数的构造函数。析构函数(Destructor)
作用
- 清理工作:析构函数在对象销毁时被自动调用,用于清理对象的资源。
- 释放资源:析构函数用于释放构造函数或其他成员函数分配的资源,如内存、文件句柄、网络连接等。
- 执行清理操作:析构函数可以执行任何需要在对象销毁时进行的操作,如关闭文件、断开网络连接等。
特点
- 名称与类名相同,但前面有
~
符号:析构函数的名称必须与类名相同,但前面有~
符号,且没有返回类型(包括void
)和参数(不能重载)。- 自动调用:析构函数在对象的生命周期结束时被自动调用,无需手动调用。
示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 class MyClass {
public:
int* ptr;
// 构造函数
MyClass(int val) {
ptr = new int(val);
std::cout << "Constructor called" << std::endl;
}
// 析构函数
~MyClass() {
delete ptr;
std::cout << "Destructor called" << std::endl;
}
};
int main() {
MyClass obj(10); // 调用构造函数
// 对象生命周期结束,自动调用析构函数
return 0;
}在这个示例中:
MyClass
的构造函数分配了一个int
类型的动态内存。- 析构函数释放了这块内存,避免了内存泄漏。
总结
- 构造函数的作用:用于初始化对象的成员变量、分配资源和执行初始操作,确保对象在创建时处于有效状态。
- 析构函数的作用:用于清理对象的资源、释放资源和执行清理操作,确保对象在销毁时不会留下未释放的资源。
构造函数和析构函数的正确实现和使用,有助于确保对象在其生命周期内资源的正确分配和释放,避免资源泄漏和未定义行为。
4-4 数据成员可以为公有的吗?成员函数可以为私有的吗?
是的,数据成员可以是公有的,成员函数也可以是私有的。下面详细解释这些情况及其使用场景和注意事项。
数据成员可以为公有的
解释
数据成员(成员变量)可以被声明为
public
,这意味着它们可以被类的外部代码直接访问和修改。使用场景
- 简单数据结构:在简单的数据结构(如C++中的
struct
)中,所有成员通常都是公有的,因为这些结构主要用于存储数据,没有复杂的行为逻辑。- 特殊需求:在某些情况下,公开数据成员可能是为了方便某些操作,减少getter和setter函数的使用,提升代码简洁性。
示例
1
2
3
4
5
6
7
8
9
10
11
12
13 struct Point {
public:
int x;
int y;
};
int main() {
Point p;
p.x = 10;
p.y = 20;
std::cout << "Point: (" << p.x << ", " << p.y << ")" << std::endl;
return 0;
}在这个示例中,
Point
结构体中的成员变量x
和y
是公有的,可以直接访问和修改。注意事项
- 封装性:公开数据成员会破坏类的封装性,使得外部代码可以随意修改内部状态,可能导致数据不一致或逻辑错误。
- 可维护性:公开数据成员使得更改类的内部实现变得困难,因为必须考虑到所有直接访问这些成员的外部代码。
成员函数可以为私有的
解释
成员函数可以被声明为
private
,这意味着它们只能被类的其他成员函数访问,不能被类的外部代码或派生类直接访问。使用场景
- 内部辅助函数:一些功能仅用于类内部实现,不需要暴露给外部代码。这些函数可以被声明为私有。
- 实现细节:将实现细节隐藏在私有成员函数中,可以防止外部代码依赖这些细节,从而增强代码的可维护性和灵活性。
示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 class MyClass {
public:
void publicMethod() {
privateMethod();
}
private:
void privateMethod() {
std::cout << "Private method called" << std::endl;
}
};
int main() {
MyClass obj;
obj.publicMethod(); // 可以调用
// obj.privateMethod(); // 编译错误,不能直接调用私有方法
return 0;
}在这个示例中,
privateMethod
是一个私有成员函数,只能通过类的公有方法publicMethod
调用。注意事项
- 可测试性:私有成员函数无法直接在单元测试中测试,通常需要通过测试公有方法来间接测试私有函数。
- 代码组织:将一些逻辑复杂的操作封装在私有成员函数中,可以使公有接口更简洁易懂,减少代码耦合。
总结
- 数据成员为公有的:可以简化代码,但会破坏封装性,增加代码维护难度。通常在简单的数据结构中使用。
- 成员函数为私有的:用于实现类的内部逻辑和细节,增强封装性和灵活性,但直接测试和访问这些函数存在一定困难。通常用于需要隐藏实现细节的辅助函数。
通过合理地设置数据成员和成员函数的访问权限,可以更好地控制类的接口和实现,提升代码的可维护性和可靠性。
4-5 已知class A中有数据成员int a,如果定义了A的两个对象a1,a2,它们各自的数据成员a的值可以不同吗?
可以
4-6 什么叫做复制构造函数?复制构造函数何时被调用?
复制构造函数(Copy Constructor)
定义
复制构造函数是一种特殊的构造函数,其参数是同类对象的引用,目的是用一个已存在的对象来初始化一个新的同类对象。复制构造函数的声明通常如下:
1 ClassName(const ClassName& other);作用
- 初始化新对象:使用已有对象的值来初始化一个新对象。
- 深拷贝和浅拷贝:在复制对象时,可以控制是进行浅拷贝(默认的成员逐字节复制)还是深拷贝(复制动态分配的资源等)。
何时被调用
复制构造函数在以下几种情况下会被自动调用:
对象初始化:
- 用一个已存在的对象初始化一个新的对象。
1 ClassName obj1 = obj2; // 调用复制构造函数对象传参:
- 将对象作为参数传递给函数时。
1
2 void function(ClassName obj); // 传值调用,调用复制构造函数
function(obj1); // obj1是ClassName类型的对象函数返回值:
- 函数返回对象类型时。
1
2
3
4 ClassName function() {
ClassName obj;
return obj; // 返回对象,调用复制构造函数
}示例
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 class MyClass {
public:
int* data;
// 默认构造函数
MyClass(int value) {
data = new int(value);
std::cout << "Constructor called" << std::endl;
}
// 复制构造函数
MyClass(const MyClass& other) {
data = new int(*other.data);
std::cout << "Copy Constructor called" << std::endl;
}
// 析构函数
~MyClass() {
delete data;
std::cout << "Destructor called" << std::endl;
}
};
int main() {
MyClass obj1(10); // 调用默认构造函数
MyClass obj2 = obj1; // 调用复制构造函数
MyClass obj3(obj1); // 调用复制构造函数
function(obj1); // 传值调用,调用复制构造函数
obj3 = function(); // 调用复制构造函数用于函数返回
return 0;
}
void function(MyClass obj) {
// 函数参数传值调用,obj是复制构造的对象
}在这个示例中:
MyClass
包含一个指向动态分配内存的指针data
。MyClass
的复制构造函数实现了深拷贝,通过分配新的内存并复制值来避免两个对象共享同一块内存。总结
复制构造函数是一个特殊的构造函数,用于用一个已存在的对象初始化一个新的同类对象。复制构造函数在以下情况下被调用:
- 用一个已存在的对象初始化一个新的对象。
- 将对象作为参数传递给函数时(传值调用)。
- 函数返回对象类型时。
合理实现复制构造函数可以避免资源管理问题,如内存泄漏和双重释放。尤其在类中包含指针等动态分配资源时,复制构造函数非常重要,用于确保对象之间的独立性和正确的资源管理。
4-7 复制构造函数与赋值运算符(=)有何不同?
复制构造函数和赋值运算符(
=
)都是用来复制对象的,但它们在用途、调用时机和实现细节上有显著区别。以下是更详细的解释:复制构造函数(Copy Constructor)
定义
复制构造函数是一种特殊的构造函数,用于创建一个新的对象,并用一个已存在的同类对象对其进行初始化。其典型定义如下:
1 ClassName(const ClassName& other);用途
- 创建新对象:复制构造函数用于在创建新对象时初始化它,使新对象成为已存在对象的副本。
调用时机
对象初始化:用一个已存在的对象初始化一个新的对象。
1
2 ClassName obj1 = obj2; // 复制构造函数被调用
ClassName obj3(obj2); // 复制构造函数被调用对象传参:将对象作为参数传递给函数时(按值传递)。
1
2 void function(ClassName obj);
function(obj1); // 复制构造函数被调用函数返回值:函数返回对象类型时。
1
2
3
4 ClassName function() {
ClassName obj;
return obj; // 复制构造函数被调用
}赋值运算符(Assignment Operator)
定义
赋值运算符用于将一个对象的值赋给另一个已经存在的同类对象。其典型定义如下:
1 ClassName& operator=(const ClassName& other);用途
- 赋值现有对象:赋值运算符用于将一个已存在对象的值赋给另一个已经存在的对象。
调用时机
对象赋值:用一个已存在的对象给另一个已存在的对象赋值。
1
2
3 ClassName obj1;
ClassName obj2;
obj1 = obj2; // 赋值运算符被调用实现细节
- 自赋值检查:赋值运算符需要检查自赋值(即一个对象赋值给自身),以防止不必要的操作。
- 释放旧资源:如果目标对象已经持有资源(如动态分配的内存),赋值运算符需要先释放这些资源,然后分配并复制新的资源。
示例
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 class MyClass {
public:
int* data;
// 默认构造函数
MyClass(int value) {
data = new int(value);
std::cout << "Constructor called" << std::endl;
}
// 复制构造函数
MyClass(const MyClass& other) {
data = new int(*other.data);
std::cout << "Copy Constructor called" << std::endl;
}
// 赋值运算符
MyClass& operator=(const MyClass& other) {
if (this != &other) { // 自赋值检查
delete data; // 释放旧资源
data = new int(*other.data); // 分配并复制新资源
}
std::cout << "Assignment Operator called" << std::endl;
return *this;
}
// 析构函数
~MyClass() {
delete data;
std::cout << "Destructor called" << std::endl;
}
};
int main() {
MyClass obj1(10); // 调用默认构造函数
MyClass obj2 = obj1; // 调用复制构造函数
MyClass obj3(20); // 调用默认构造函数
obj3 = obj1; // 调用赋值运算符
return 0;
}在这个示例中:
obj2
是通过复制构造函数用obj1
初始化的。obj3
先通过默认构造函数初始化,然后通过赋值运算符将obj1
的值赋给obj3
。总结
- 复制构造函数:用于创建并初始化一个新的对象,使其成为已有对象的副本。典型调用场景是对象初始化、传值调用和返回对象。
- 赋值运算符:用于将一个已有对象的值赋给另一个已有对象。典型调用场景是对象赋值。
尽管两者都用于对象复制,但它们的使用场景不同,处理方式也有区别。复制构造函数关注对象的初始化,而赋值运算符关注对象的重新赋值,需要处理自赋值和资源管理。
4-8 定义一个Dog类,包含了age,weight等属性,以及对这些属性操作的方法。实现并测试这个类。
1 |
|
4-10 设计一个用于人事管理的“人员”类。由于考虑到通用性,这里只抽象出所有类型人员都具有的属性:编号、性别、出生日期、身份证号等。其中“出生日期”声明为一个“日期”类内嵌子对象。用成员函数实现对人员信息的录入和显示。要求包括:构造函数和析构函数、复制构造函数、内联成员函数、带默认形参值的成员函数、类的组合。
1 |
|
4-12 定义一个DataType(数据类型)类,能处理包含字符型、整型、浮点型3种类型的数据,给出其构造函数。
1 |
|
4-15 根据例4-3中关于Circle类定义的源代码绘出该类的UML图形表示
4-16 根据下面C++代码绘出相应的UML图形,表示出类ZRF、类SSH和类Person之间的继承关系。
1 | class Person ( |
4-17 在一个大学的选课系统中,包括两个类:CourseSchedule类和Course类。其关系为:CourseSchedule 类中的成员函数add和remove的参数是Course类的对象,请通过UML方法显式表示出这种依赖关系。
4-18
在一个学校院系人员信息系统中,需要对院系(Department)和教师(Teacher)之间的关系进行部分建模,其关系描述为:每个Teacher可以属于零个或多个Department的成员,而每个Department至少包含一个Teacher作为成员。根据以上关系绘制出相应的UML类图。
4-20 定义一个负数类Complex,使得下面的代码能够工作。
1 | Complex c1(3,5); //用复数3+5i初始化c1 |
1 |
|