C++练习题

5-1 什么是作用域? 有哪几种类型的作用域 标识符的作用范围

作用域是指程序中变量或其他标识符的可见范围。在不同的作用域内,同名的标识符可以有不同的含义。C++ 中主要有以下几种类型的作用域:

1. 函数原型作用域 (Function Prototype Scope)

函数原型作用域指的是函数声明中参数名称的作用范围。参数名称在函数原型作用域内仅用于函数声明本身,在其他地方不可见。例如:

1
void function(int x, int y); // x 和 y 仅在此作用域内有效

在函数原型中,参数名称 xy 仅用于参数说明,编译器并不需要它们来进行匹配。

2. 块作用域 (Block Scope)

块作用域是最常见的作用域类型,指的是在一对花括号 {} 中声明的标识符的作用范围。它包括函数体、循环体、条件语句等。块作用域内声明的变量在块结束时被销毁。示例如下:

1
2
3
4
5
6
7
8
void function() {
int x = 10; // x 的作用域在整个函数体内

if (x > 5) {
int y = 20; // y 的作用域在 if 语句块内
}
// y 在这里是不可见的
}

3. 类作用域 (Class Scope)

类作用域指的是类内部声明的成员(包括成员变量和成员函数)的作用范围。类作用域内的成员在类的任何成员函数中都是可见的。示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
class MyClass {
private:
int x; // x 的作用域在整个类内

public:
void setX(int value) {
x = value; // 这里可以访问 x
}

int getX() {
return x; // 这里可以访问 x
}
};

4. 文件作用域 (File Scope)

文件作用域(也称为全局作用域)指的是在文件级别声明的标识符的作用范围。文件作用域的标识符在文件的整个范围内都是可见的。全局变量和函数具有文件作用域。示例如下:

1
2
3
4
5
int globalVar = 0; // globalVar 的作用域在整个文件内

void function() {
globalVar = 10; // 这里可以访问 globalVar
}

全局变量和函数在文件中任意位置都可以被访问,但需要注意多文件项目中的变量命名冲突问题,可以使用 extern 关键字来声明跨文件的变量。

详细解释

  • 函数原型作用域:这是最局限的作用域类型,参数名称在函数原型中只是用来解释参数的类型,不参与实际计算。

  • 块作用域:变量在其定义的块内有效,块外不可见。块作用域在花括号内开始,在花括号结束时结束。它适用于局部变量、循环变量、条件语句等。

  • 类作用域:类成员在类的所有成员函数内可见。类作用域使得类的内部实现细节对类外部的代码隐藏,只暴露类的接口部分。

  • 文件作用域:文件作用域允许在一个文件中声明全局变量和函数,这些标识符在文件的任何地方都可以被访问。全局变量在程序生命周期内一直存在,而局部变量在其作用域结束时被销毁。

通过理解这些不同类型的作用域,可以更好地控制变量和函数的可见性和生命周期,从而编写出更高质量、更易维护的代码。


5-2 什么叫做可见性? 可见性的一般规则是什么? 可见性是指在程序中访问变量或其他标识符的能力。一般来说,可见性遵循以下一般规则:

  1. 内层可见外层:在程序的嵌套结构中,内层作用域的代码可以访问外层作用域中声明的变量或其他标识符,但外层不能访问内层的变量或标识符。

  2. 使用前需要定义或声明:在使用变量或函数之前,需要确保其已经被定义或者声明过。如果尝试使用一个未定义或未声明的变量或函数,编译器会报错。

下面详细解释一下这两条规则:

内层可见外层

在程序的嵌套结构中,内层作用域的代码可以访问外层作用域中的变量或其他标识符。这意味着,内部块中声明的变量可以访问外部块中声明的变量,但反过来是不行的。这种设计有助于封装和隐藏变量,提高程序的可维护性和安全性。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>

int main() {
int x = 5; // 外层作用域中的变量

{
int y = x + 2; // 内层作用域可以访问外层作用域中的变量
std::cout << "y: " << y << std::endl; // 输出 y 的值
}

// 外层作用域无法访问内层作用域中的变量 y
// std::cout << "y: " << y << std::endl; // 编译错误:y 未定义

return 0;
}

在这个示例中,内层作用域中的变量 y 可以访问外层作用域中的变量 x,但是外层作用域无法访问内层作用域中的变量 y

使用前需要定义或声明

在使用变量或函数之前,需要确保其已经被定义或者声明过。这是因为编译器需要在编译时确定标识符的类型和含义。如果尝试使用一个未定义或未声明的变量或函数,编译器会报错,因为它无法确定该标识符的含义。

示例:

1
2
3
4
5
6
7
8
9
#include <iostream>

int main() {
// 在使用变量 x 之前,需要先定义或声明
int x = 5;
std::cout << "x: " << x << std::endl;

return 0;
}

在这个示例中,变量 x 在使用之前先进行了定义,因此可以正常使用。如果不进行定义,编译器会报错。


5-3 下面程序运行结果是什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include<iostream>
using namespace std;

int x = 5, y = 7;

void myFunction() {
int y = 10;
cout << "x from myFunction: " << x << "\n";
cout << "y from myFunction: " << y << "\n\n";
}

int main() {
cout << "x from main: " << x << "\n";
cout << "y from main: " << y << "\n\n";
myFunction();
cout << "Back from myFunction!\n\n";
cout << "x from main: " << x << "\n";
cout << "y from main: " << y << "\n";
return 0;
}

x from main: 5 y from main: 7

x from myFunction: 5 y from myFunction: 10

Back from myFunction!

x from main: 5 y from main: 7


5-4 假设有两个无关系的类Engine和Fuel,使用时,如何使Fuel成员访问Engine中的私有和保护的成员? 设置友元类

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
class Fuel;
class Engine {
private:
int powerlevel;
public:
Engine()
{
this->powerlevel = 0;
}
void engine_fn(Fuel &f)
{
// TODO
}
};

class Fuel {
private:
int powerlevel;
public:
Fuel()
{
this->powerlevel = 0;
}
void fuel_fn(Engine &e)
{
// TODO
}
};

在这种情况下,如果要使 Fuel 类的成员能够访问 Engine 类的私有和保护成员,可以将 Fuel 类声明为 Engine 类的友元类。这样一来,Fuel 类的成员就能够在其成员函数中直接访问 Engine 类的私有和保护成员了。

下面是修正后的代码:

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 Engine;

class Fuel {
private:
int powerlevel;

public:
Fuel() {
this->powerlevel = 0;
}

// 声明 Engine 类为 Fuel 类的友元类
friend class Engine;

void fuel_fn(Engine &e) {
// 在 Fuel 类的成员函数中可以直接访问 Engine 类的私有和保护成员
e.powerlevel = 100;
}
};

class Engine {
private:
int powerlevel;

public:
Engine() {
this->powerlevel = 0;
}

// 声明 Fuel 类为 Engine 类的友元类
friend class Fuel;

void engine_fn(Fuel &f) {
// 在 Engine 类的成员函数中可以直接访问 Fuel 类的私有和保护成员
f.powerlevel = 50;
}
};

在修正后的代码中,Fuel 类和 Engine 类相互声明为友元类,这样它们的成员函数就可以互相访问彼此的私有和保护成员了。


5-5 什么叫做静态数据成员?它有何特点? 静态数据成员是一种使用 static 关键字声明的类成员变量。它属于整个类,而不是类的某个特定对象,因此只有一份拷贝,被类的所有对象共享。

静态数据成员有以下几个特点:

  1. 只有一份拷贝:静态数据成员属于类本身,而不是类的对象。因此,无论创建多少个类的对象,静态数据成员都只有一份拷贝。

  2. 类的所有对象共享:静态数据成员被类的所有对象所共享,它们的值在类的所有对象之间是相同的。

  3. 可以直接通过类名访问:静态数据成员可以直接通过类名加作用域解析运算符 :: 访问,也可以在类的成员函数中访问,但不能直接使用对象名访问。

  4. 可以用于实现类范围内的数据共享:静态数据成员适合用于需要在类的所有对象之间共享的数据,例如计数器、全局配置等。

  5. 初始化和声明必须在类外部进行:静态数据成员在类内部只能声明,不能初始化,其初始化必须在类外部进行。

下面是一个示例,演示了静态数据成员的使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>

class MyClass {
public:
static int count; // 声明静态数据成员

MyClass() {
count++; // 每创建一个对象,count 加 1
}
};

// 初始化静态数据成员
int MyClass::count = 0;

int main() {
MyClass obj1, obj2, obj3;

// 通过类名访问静态数据成员
std::cout << "Number of objects created: " << MyClass::count << std::endl;

return 0;
}

在这个示例中,MyClass 类有一个静态数据成员 count,它记录创建的 MyClass 对象的数量。无论创建多少个 MyClass 对象,count 的值都会相应地增加,因为它被所有对象共享。


5-6 什么叫做静态函数成员?它有何特点? 使用static关键字声明的函数成员, 其属于整个类, 同一个类的所有对象共同维护与共享

静态函数成员是一种使用 static 关键字声明的类成员函数。与静态数据成员类似,静态函数成员也属于整个类,而不是类的某个特定对象,因此同一个类的所有对象共享同一份静态函数成员。

静态函数成员有以下几个特点:

  1. 属于整个类:静态函数成员不依赖于类的任何对象,它属于整个类。因此,它不与任何特定对象关联,而是属于类本身。

  2. 同一个类的所有对象共同维护与共享:静态函数成员只有一份,它被同一个类的所有对象所共享。因此,所有对象共享相同的函数代码。

  3. 只能直接访问静态成员数据成员:静态函数成员只能直接访问同一个类的静态数据成员和静态函数成员。它不能直接访问非静态成员数据成员和非静态函数成员。

  4. 只维护一份函数实现:同一个类的所有对象共享同一份静态函数成员的实现代码。这使得静态函数成员适合用于不依赖于特定对象的操作,例如实用函数或工具函数。

下面是一个示例,演示了静态函数成员的使用:

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
#include <iostream>

class MyClass {
private:
static int count; // 静态数据成员

public:
static void incrementCount() {
count++; // 只能直接访问同一个类的静态数据成员
}

static int getCount() {
return count; // 只能直接访问同一个类的静态数据成员
}
};

// 初始化静态数据成员
int MyClass::count = 0;

int main() {
MyClass::incrementCount(); // 调用静态函数成员
MyClass::incrementCount(); // 调用静态函数成员
MyClass::incrementCount(); // 调用静态函数成员

std::cout << "Count: " << MyClass::getCount() << std::endl; // 调用静态函数成员

return 0;
}

在这个示例中,MyClass 类有一个静态数据成员 count 和两个静态函数成员 incrementCount()getCount()。静态函数成员 incrementCount() 用于增加 count 的值,而静态函数成员 getCount() 用于获取 count 的值。由于它们都是静态函数成员,可以直接通过类名调用,而不需要创建类的对象。


5-7 定义一个Cat类,拥有静态数据成员numOfCats,记录Cat的个体数目;静态成员函数getNumOfCats(),读取numOfCats.设计程序测试这个类,体会静态数据成员和静态成员函数的用法。

下面是一个定义了 Cat 类的示例,其中包含了静态数据成员 numOfCats 和静态成员函数 getNumOfCats(),并通过一个简单的测试程序展示了它们的用法:

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
#include <iostream>

class Cat {
private:
static int numOfCats; // 静态数据成员,记录 Cat 的个体数目

public:
Cat() {
numOfCats++; // 每次创建对象时,numOfCats 增加 1
}

~Cat() {
numOfCats--; // 每次销毁对象时,numOfCats 减少 1
}

// 静态成员函数,读取 numOfCats
static int getNumOfCats() {
return numOfCats;
}
};

// 初始化静态数据成员
int Cat::numOfCats = 0;

int main() {
// 创建几个 Cat 对象
Cat cat1;
Cat cat2;
Cat cat3;

// 输出 Cat 的个体数目
std::cout << "Number of cats: " << Cat::getNumOfCats() << std::endl;

// 创建一个新的 Cat 对象
Cat cat4;

// 再次输出 Cat 的个体数目
std::cout << "Number of cats: " << Cat::getNumOfCats() << std::endl;

return 0;
}

在这个示例中,Cat 类有一个静态数据成员 numOfCats 用于记录 Cat 的个体数目。每次创建 Cat 对象时,numOfCats 增加 1;每次销毁 Cat 对象时,numOfCats 减少 1。Cat 类还有一个静态成员函数 getNumOfCats(),用于读取 numOfCats 的值。

main() 函数中,创建了几个 Cat 对象,并通过调用 Cat::getNumOfCats() 输出了 Cat 的个体数目。


5-8 什么叫做友元函数?什么叫做友元类?

友元函数和友元类都涉及到使用 friend 关键字声明的关系,在 C++ 中它们的作用分别如下:

友元函数(Friend Function):

友元函数是在类外部声明并且使用 friend 关键字声明为某个类的友元的函数。这意味着该函数可以访问该类的私有成员和保护成员。友元函数的特点包括:

  • 使用 friend 关键字声明:在声明友元函数时,需要使用 friend 关键字将其声明为某个类的友元。

  • 可以访问类的私有和保护成员:被声明为某个类的友元函数可以访问该类的私有成员和保护成员,即使这些成员在函数所在的类外部是不可访问的。

  • 不属于类的成员:友元函数并不是类的成员函数,它们在声明时不属于任何类,但是通过 friend 关键字的声明,它们被授权可以访问类的私有和保护成员。

友元类(Friend Class):

友元类是在某个类中使用 friend 关键字声明的另一个类,被声明为友元的类的所有成员函数都可以访问声明为友元的类的私有成员和保护成员。友元类的特点包括:

  • 使用 friend 关键字声明:在某个类的声明中,使用 friend 关键字将另一个类声明为友元类。

  • 所有成员函数都是友元函数:被声明为某个类的友元类的所有成员函数都可以访问声明为友元的类的私有成员和保护成员,即使这些成员在函数所在的类外部是不可访问的。

  • 不是继承关系:友元类并不意味着类之间的继承关系,而是一种访问授权关系。被声明为友元的类的成员函数可以访问友元类的私有和保护成员,但并不继承其成员函数或数据成员。


5-9 如果类A是类B的友元,类B是类C的友元,类D是类A的派生类,那么类B是类A的友元吗?类C是类A的友元吗?类D是类B的友元吗?

这些情况都不成立,因为友元关系不具有交换性、传递性以及继承性。让我详细解释一下:

  1. 友元关系不具有交换性:即如果类 A 是类 B 的友元,不意味着类 B 自动成为类 A 的友元。在题目中,虽然类 A 是类 B 的友元,但并不意味着类 B 是类 A 的友元。

  2. 友元关系不具有传递性:即如果类 A 是类 B 的友元,类 B 是类 C 的友元,不意味着类 A 自动成为类 C 的友元。在题目中,虽然类 A 是类 B 的友元,类 B 是类 C 的友元,但并不意味着类 A 是类 C 的友元。

  3. 友元关系不能被继承:即如果类 A 是类 B 的友元,类 D 继承自类 A,不意味着类 D 自动成为类 B 的友元。在题目中,即使类 D 继承自类 A,但并不意味着类 D 是类 B 的友元。

友元关系的特性决定了它们的限制,确保了类的封装性和数据的安全性。友元关系通常用于特定的情况,允许一些外部函数或类访问类的私有成员,但需要谨慎使用,避免滥用破坏封装性。


5-10 静态成员变量可以为私有的吗?声明一个私有的静态整型成员变量。 是的,静态成员变量可以是私有的。在类的访问控制权限中,私有(private)是最高级别的访问权限,只有该类的成员函数可以直接访问私有成员,外部代码无法直接访问。

声明一个私有的静态整型成员变量的语法如下所示:

1
2
3
4
class MyClass {
private:
static int a;
};

在这个示例中,a 是一个私有的静态整型成员变量。它只能被该类的成员函数访问,其他类或函数无法直接访问。私有的静态成员变量通常用于实现类的内部逻辑或数据处理,限制了外部代码对其直接访问,增强了类的封装性和安全性。


5-11 在一个文件中定义一个全局变量n,主函数main(),在另一个文件中定义函数fnl(),在main()中对n赋值,再调用fnl(),在fnl()中也对n赋值,显示n最后的值。

fn1.main.h 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include<iostream>
#include<stdlib.h>
# include"fn1.h"
using namespace std;

int n;

int main()
{
n = 20;
fn1();
cout << "n的值为: " << n << endl;


system("pause");
return 0;
}

fn1.h 文件

1
2
3
4
5
6
7
8
9
#pragma once
extern int n;

void fn1()
{
n = 30;
}



5-12 在函数fnl()中定义一个静态变量n,fnl()中对n的值加1,在主函数中,调用nl()十次,显示n的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include<iostream>
#include<stdlib.h>

using namespace std;

void fn1()
{
static int n = 0;
n++;
cout << "n的值为: " << n << endl;
}

int main()
{
int i;
for (i = 0; i < 10; i++)
{
fn1();
}
system("pause");
return 0;
}

5-13 定义类X,Y,Z,函数 h(X),满足:类X有私有成员i,Y的成员函数 g(X) 是X的友元函数,实现对X的成员i加1;类Z是类X的友元类,其成员函数 f(X) 实现对X的成员i加5;函数 h(X) 是X的友元函数,实现对X的成员i加10,在一个文件中定义和实现类,在另一个文件中实现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
#pragma once
#ifndef MYHEADER_H
#define MYHEADER_H

class X;
class Y {
public:
void g(X* x);
};

class X {
private:
int i;
public:
X() :i(0) {}
friend void h(X* x);
friend void Y::g(X* x);
friend class Z;
};

class Z {
public:
void f(X* x)
{
x->i += 5;
}
};

void h(X* x)
{
x->i += 10;
}

void Y::g(X* x)
{
x->i++;
}

#endif // !MYHEADER_H
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include<iostream>
#include<stdlib.h>
#include"myheader.h"
using namespace std;



int main()
{
X x;
Y y;
Z z;
z.f(&x);
h(&x);
y.g(&x);

system("pause");
return 0;
}

5-14 定义Boat与Car两个类,二者都有weight属性,定义二者的一个友元函数getTotalWeight().计算二者的重量和。

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
#include<iostream>
#include<stdlib.h>

using namespace std;

class Car; // 向前声明,告诉编译器 Car 类的存在
class Boat {
private:
double weight; // 船的重量
public:
Boat(int weight) // 构造函数,初始化船的重量
{
this->weight = weight;
}
friend double getTotalWeight(Boat &b, Car &c); // 声明 getTotalWeight() 为友元函数
};

class Car {
private:
double weight; // 汽车的重量
public:
Car(int weight) // 构造函数,初始化汽车的重量
{
this->weight = weight;
}
friend double getTotalWeight(Boat &b, Car &c); // 声明 getTotalWeight() 为友元函数
};

// 友元函数,用于计算汽车和船的总重量
double getTotalWeight(Boat &b, Car &c)
{
return b.weight + c.weight; // 返回汽车和船的总重量
}

int main()
{
Car c(4.5); // 创建一个汽车对象,重量为4.5
Boat b(5.5); // 创建一个船对象,重量为5.5
cout << "getTotalWight(b, c) = " << getTotalWeight(b, c) << endl; // 输出汽车和船的总重量

system("pause"); // 暂停程序,等待用户操作
return 0; // 返回执行结果
}


5-15 在函数内部定义的普通局部变量和静态局部变量在功能上有何不同?计算机底层对这两类变量做了怎样的不同处理,导致了这种差异? 普通局部变量和静态局部变量在功能上有几个重要的区别:

  1. 生命周期不同:普通局部变量的生命周期仅限于其所在函数的执行期间。每次函数调用时,都会创建一个新的副本,当函数执行结束时,该变量的内存空间会被释放。而静态局部变量的生命周期在程序整个运行期间都是存在的,它不会随着函数的返回而失效,只有在程序结束时才会被销毁。

  2. 存储位置不同:普通局部变量通常存储在栈区(stack),而静态局部变量则存储在静态数据存储区(static data storage area)或全局数据区(global data area)。

  3. 初始化不同:普通局部变量不会被自动初始化,其值是未知的,需要手动初始化才能使用。而静态局部变量会被默认初始化为0或者指定的初始值,无需手动初始化。

底层上的区别主要体现在变量的存储位置和生命周期上:

  • 普通局部变量存储在栈区,每次函数调用时在栈上分配内存,函数返回时释放内存。由于栈的特性,普通局部变量的生命周期与函数的调用关系密切相关,超出作用域即销毁,内存被回收。

  • 静态局部变量存储在静态数据存储区或全局数据区,它的内存空间在程序启动时就已经分配,并且在整个程序运行期间都存在。静态局部变量的作用域仍然是局部作用域,但是它的生命周期不会受到函数的调用关系影响,因此可以保留其值并在多次函数调用之间共享。


5-16 编译和连接这两个步骤的输入输出分别是什么类型的文件?两个步骤的任务有什么不同? 在以下几种情况下,在对程序进行编译、连接时是否会报错?会在哪个步骤报错? (1) 定义了一个函数 void f(intx,inty),以f(1)的形式调用。 (2) 在源文件起始处声明了一个函数void f(intx),但未给出其定义,以f(1)的形式调用。 (3) 在源文件起始处声明了一个函数voidf(intx),但未给出其定义,也未对其进行调用。 (4) 在源文件a.cpp中定义了一个函数void f(intx),在源文件b.cpp中也定义了一个函数void f(intx),试图将两源文件编译后连接在一起。

编译和连接是编译器的两个主要步骤,它们的输入输出以及任务有一些不同:

编译(Compilation):

  • 输入类型的文件:编译器的输入是源文件(例如 .cpp 文件),其中包含了源代码的文本形式。

  • 输出类型的文件:编译器的输出是目标文件(例如 .o 文件或 .obj 文件),其中包含了源代码经过编译器翻译后的机器语言形式的代码。

  • 任务:编译器的任务是将源文件中的源代码转换为目标文件,即将高级语言源代码转换为可执行的机器语言代码。

链接(Linking):

  • 输入类型的文件:连接器的输入是目标文件,以及可能的库文件,其中包含了已编译的目标代码。

  • 输出类型的文件:连接器的输出是可执行文件,即最终的程序文件,它包含了所有的目标代码以及可能的库文件中的代码。

  • 任务:连接器的任务是将各个编译单元的目标文件以及库文件中的代码合并在一起,生成最终的可执行文件。

对于给定情况的解释:

  1. 情况一:在定义了函数 void f(int x, int y) 的情况下,调用 f(1) 会在编译阶段报错,因为调用参数不匹配。

  2. 情况二:在源文件起始处声明了函数 void f(int x),但未给出定义,在调用 f(1) 时,编译阶段不会报错,因为编译器认为该函数可能在其他地方定义了。但在连接阶段会报错,因为找不到 f(int) 的定义。

  3. 情况三:在源文件起始处声明了函数 void f(int x),但未给出定义,也未对其进行调用。在这种情况下,既不会在编译阶段报错,也不会在连接阶段报错。

  4. 情况四:在不同的源文件中定义了相同名称的函数 void f(int x),编译阶段不会报错,因为每个源文件中的函数定义是独立的。但在连接阶段会报错,因为连接器无法确定应该使用哪个定义,从而导致函数重复定义的错误。