标识符作用域和可见性

作用域

作用域是标识符在程序文本中有效的有效区域

函数原型作用域

函数原型声明期间,形式参数的作用域是函数原型作用域。

在函数原型参数列表中,只有类型是重要的,标识符可以省略。为了可读性,最好包含它。

局部作用域

简单理解为在函数体内声明的变量,从声明点到声明所在块的闭合括号。

具有局部作用域的变量也称为局部变量。

类作用域

类是命名成员的集合,其成员 m 具有类作用域。访问它有三种方式:

  1. 如果成员 m 没有在成员函数中定义,并且没有被函数体遮蔽,函数可以直接访问 m;
  2. 通过表达式 x.mX::m 访问。这是最基本的方法,后者主要用于访问类的静态成员。
  3. 通过 ptr->m 访问,其中 ptr 是该类对象的指针

命名空间作用域

命名空间的目的是消除项目中不同文件可能存在的歧义,例如:当两个不同模块中的变量具有相同名称时。

语法如下:

1
2
3
namespace namespace_name{
命名空间内的各种声明(函数声明、类声明等...)
}

在命名空间内,可以直接使用当前空间中定义的标识符。如果需要使用其他命名空间中定义的标识符,则需要使用 namespace_name::identifier。为了避免冗长,提供了使用声明。

使用声明有两种形式:

1
2
using namespace_name::identifier
using namespace namespace_name

有两种特殊类型的命名空间——全局命名空间和匿名命名空间。

全局命名空间是默认命名空间,所有在显式声明的命名空间外声明的标识符都在全局命名空间中。

匿名命名空间在定义时只需省略命名空间名称,其目的是防止你定义的标识符被任何其他命名空间访问。

C++ 标准库中的所有标识符都在 std 命名空间中声明,cout、cin、endl 都是这样,因此每个程序都使用 using namespace std,否则你需要使用 std::cin

此外,命名空间允许嵌套

具有命名空间作用域的变量也称为全局变量。


可见性

内容相对简单明了,因此省略。


对象生命周期

静态生命周期

生命周期与程序运行时相同的对象称为具有静态生命周期,在声明时需要使用关键字static

特点:每次函数调用时不会创建副本,函数返回时也不会失效。变量在每次调用期间共享。赋值只执行一次,声明时的赋值语句不会多次执行。

如果在声明时未初始化,默认为 0。

动态生命周期

局部生命周期对象在声明点出生,在声明所在块执行完毕时结束。

类静态成员

对象之间也需要共享数据,静态成员解决了这个问题。

例如,如果有一个 Employee 类,我们有几个 Employee 对象,但我们如何计算有多少个 Employee 对象呢?这时可以使用静态数据成员,因为这个数据成员是所有对象共享的。

静态数据成员

当某个属性被整个类共享不属于任何特定对象时,使用static关键字将其声明为静态成员。整个类中只有一个副本,由所有对象维护和使用。

由于它不属于任何对象并且具有静态生命周期,因此通过类名访问。“class_name::identifier”

在类定义中,仅做了引用声明。必须在使用类名限定符的命名空间作用域中进行定义声明,也可以在此处进行初始化。

程序示例:

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
#include <iostream> 
using namespace std;
class Point { //Point 类定义 public: //外部接口
Point(int x = 0, int y = 0) : x(x), y(y){ //构造函数
count++; //在构造函数中递增计数,所有对象维护相同的计数
}
Point(Point &p){ //拷贝构造函数
x = p.x;
y = p.y;
count++;
}
~Point() { count--; }
int getX() { return x; }
int getY() { return y; }
void showCount() { //输出静态数据成员
cout << " 对象计数 = " << count << endl;
}
private: //私有数据成员
int x, y;
static int count; //静态数据成员声明,用于记录点的数量
};
int Point::count = 0;//静态数据成员定义和初始化,使用类名限定符
int main() { //主函数
Point a(4, 5); //定义对象 a,其构造函数将计数递增 1
cout << "Point A: " << a.getX() << ", " << a.getY(); a.showCount(); //输出对象计数
Point b(a); //定义对象 b,其构造函数将计数递增 1
cout << "Point B: " << b.getX() << ", " << b.getY(); b.showCount(); //输出对象计数
return 0;
}

静态成员函数

上面的程序实际上存在一个问题:showcount 函数需要存在一个 Point 对象才能被调用,但如果我想直接输出 count 的值呢?这时就需要静态成员函数,允许通过类名直接调用函数,而不依赖于对象。

虽然静态成员函数也可以通过对象访问,但通常习惯上通过类名访问。即使通过对象名称访问,该函数与对象没有关系。

类友元

以 Point 类为例,如果我们需要一个函数来计算两点之间的距离怎么办?

将其设置为类外的普通函数并不能反映函数与点之间的联系,也无法直接使用点坐标;

将其设置为类内的成员函数则不符合类代表一种事物特征的抽象,因为距离代表的是点之间的关系,而不是点的特征。

class composition中,有一个 Point 和 Line 类,Line 类有一个计算线段长度的函数。但如果我们面对许多点并且频繁需要计算任意两点之间的距离,我们是否每次都需要构造一个 Line 类?这显然非常麻烦。

友元关系提供了不同类或对象的成员函数之间,以及类成员函数与普通函数之间的数据共享机制。

在类中,使用关键字friend来声明函数为友元函数,类为友元类。友元类的所有函数都是友元函数

友元函数

这些是在类中用关键字friend**修饰的非成员函数。它们可以是普通函数或其他类的成员函数。在友元函数的函数体内,可以通过对象名称访问类的私有和保护成员。

在 github 上有实践源代码

友元类

类似于友元函数。如果类 A 是类 B 的友元类,则类 A 的所有成员函数都是类 B 的友元函数,可以访问类 B 的私有和保护成员。

特别注意 ⚠️:

  • 友元关系是不传递的。如果 B 是 A 的友元,C 是 B 的友元,C 不是 A 的友元,除非明确声明。
  • 友元关系是单向的。如果 B 是 A 的友元,B 可以访问 A,但 A 不能访问 B。
  • 友元关系是不继承的。如果 B 是 A 的友元,B 的派生类不会自动成为 A 的友元。一个简单的类比是:如果有人信任你的父亲,他们不一定会信任你。

共享数据保护

常量对象

常量对象的数据值成员在整个对象生命周期内不能更改。常量对象必须初始化,不能更新。

在定义时指定初始值称为初始化,通过赋值操作进行的后续更改称为赋值。不要将初始化与赋值混淆

用 const 修饰的类成员

常量成员函数

声明格式:

1
type_specifier function_name(parameter_list) const;

注意 ⚠️:

  • 如果对象是常量对象,则只能通过该常量对象调用常量成员函数,其他成员函数不能被调用!这是 C++ 对常量对象的保护,也是常量对象的唯一外部接口方法
  • 无论是否通过常量对象调用,在调用常量成员函数期间,目标对象被视为常量对象。因此,常量成员函数不能更新目标对象的数据成员,也不能调用该类中未用 const 修饰的成员函数(确保常量成员函数不修改目标对象的数据成员值)。
  • const 关键字可以用于区分重载函数(同名但有或没有 const 的函数是不同的函数)。

常量数据成员

用 const 声明的数据成员是常量数据成员,任何函数都不能给它们赋值。构造函数只能通过初始化列表为这些数据成员获取初始值。

类成员中的静态变量和常量应在类定义外部定义,但 C++ 提供了一个例外:如果类的静态常量具有整数类型或枚举类型,则可以直接在类定义中指定常量值。

常量引用

如果在声明时用 const 修饰引用,则声明的引用是常量引用,常量引用所引用的对象不能被更新。当用作函数参数时,可以防止意外更改实际参数。

对于在函数中不能更改值的参数,不适合使用普通引用传递,因为这会阻止常量对象的传递。使用值传递或传递常量引用可以避免这个问题。值传递更耗时,因此传递常量引用更好。拷贝构造函数的参数通常也选择常量引用!

多文件结构和编译预处理命令

由于 C 语言有基础,本节仅列出一些不熟悉和不易记忆的内容。

C++ 程序的一般组织结构

一个项目可以分为多个源文件:

  • 类声明文件(.h 文件)
  • 类实现文件(.cpp 文件)
  • 类使用文件(包含 main() 的 .cpp 文件)

标准 C++ 库

标准 C++ 类库是一组极其灵活和可扩展的可重用软件模块。

标准 C++ 类和组件逻辑上分为 6 种类型:

  • 输入/输出类
  • 容器类和抽象数据类型
  • 存储管理类
  • 算法
  • 错误处理
  • 运行时环境支持

综合示例——个人银行账户管理程序

程序源代码已上传至 github,并使用 makefile 编译。

严重错误:静态变量未在外部赋初值,导致我的进度停滞了两个小时,初始化赋值是在定义类成员函数的文件中完成的。