C++ Data Sharing and Protection
标识符作用域和可见性
作用域
作用域是标识符在程序文本中有效的有效区域。
函数原型作用域
函数原型声明期间,形式参数的作用域是函数原型作用域。
在函数原型参数列表中,只有类型是重要的,标识符可以省略。为了可读性,最好包含它。
局部作用域
简单理解为在函数体内声明的变量,从声明点到声明所在块的闭合括号。
具有局部作用域的变量也称为局部变量。
类作用域
类是命名成员的集合,其成员 m 具有类作用域。访问它有三种方式:
- 如果成员 m 没有在成员函数中定义,并且没有被函数体遮蔽,函数可以直接访问 m;
- 通过表达式
x.m或X::m访问。这是最基本的方法,后者主要用于访问类的静态成员。 - 通过
ptr->m访问,其中 ptr 是该类对象的指针。
命名空间作用域
命名空间的目的是消除项目中不同文件可能存在的歧义,例如:当两个不同模块中的变量具有相同名称时。
语法如下:
1 | namespace namespace_name{ |
在命名空间内,可以直接使用当前空间中定义的标识符。如果需要使用其他命名空间中定义的标识符,则需要使用
namespace_name::identifier。为了避免冗长,提供了使用声明。
使用声明有两种形式:
1 | using namespace_name::identifier |
有两种特殊类型的命名空间——全局命名空间和匿名命名空间。
全局命名空间是默认命名空间,所有在显式声明的命名空间外声明的标识符都在全局命名空间中。
匿名命名空间在定义时只需省略命名空间名称,其目的是防止你定义的标识符被任何其他命名空间访问。
C++ 标准库中的所有标识符都在 std 命名空间中声明,cout、cin、endl 都是这样,因此每个程序都使用
using namespace std,否则你需要使用std::cin…
此外,命名空间允许嵌套。
具有命名空间作用域的变量也称为全局变量。
可见性
内容相对简单明了,因此省略。
对象生命周期
静态生命周期
生命周期与程序运行时相同的对象称为具有静态生命周期,在声明时需要使用关键字static。
特点:每次函数调用时不会创建副本,函数返回时也不会失效。变量在每次调用期间共享。赋值只执行一次,声明时的赋值语句不会多次执行。
如果在声明时未初始化,默认为 0。
动态生命周期
局部生命周期对象在声明点出生,在声明所在块执行完毕时结束。
类静态成员
对象之间也需要共享数据,静态成员解决了这个问题。
例如,如果有一个 Employee 类,我们有几个 Employee 对象,但我们如何计算有多少个 Employee 对象呢?这时可以使用静态数据成员,因为这个数据成员是所有对象共享的。
静态数据成员
当某个属性被整个类共享且不属于任何特定对象时,使用static关键字将其声明为静态成员。整个类中只有一个副本,由所有对象维护和使用。
由于它不属于任何对象并且具有静态生命周期,因此通过类名访问。“class_name::identifier”。
在类定义中,仅做了引用声明。必须在使用类名限定符的命名空间作用域中进行定义声明,也可以在此处进行初始化。
程序示例:
1 |
|
静态成员函数
上面的程序实际上存在一个问题: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 编译。
严重错误:静态变量未在外部赋初值,导致我的进度停滞了两个小时,初始化赋值是在定义类成员函数的文件中完成的。









