1、资料清单
资料
内容
入口
教材
《C++语言程序设计(第4版)》Chap5,原书页码 162-203
抽取 Markdown 下载
课件
Chap5 课件,位于本地郑莉 C++ 资料目录
本地资料夹
例题源代码
C++V5 源代码
下载
VS2019 工程
C++V5 VS2019 solution
下载
本章的主线:Chap4 解决“类怎样封装对象”,Chap5 解决“数据在什么范围内可见、能活多久、怎样共享、怎样保护”。核心顺序是作用域 → 可见性 → 生存期 → 静态成员 → 友元 → const 保护 → 多文件结构 。
2、颜色标注
颜色
含义
使用位置
蓝色
核心术语、定义
作用域、可见性、生存期、静态成员
绿色
推荐写法、主线
const、初始化、头文件保护
黄色
易错点、边界条件
隐藏外层变量、静态成员定义、友元关系
红色
不建议做法
滥用全局变量、滥用友元、宏替代常量
3、学习目标
目标
要会到什么程度
作用域
分清函数原型、块、类、文件/命名空间作用域
可见性
知道同名标识符如何遮蔽外层标识符
生存期
分清自动、静态、动态对象的生命周期
静态成员
会定义类共享的数据成员和成员函数
友元
知道友元函数、友元类的用途和限制
const
会写常对象、常成员函数、常引用和常指针
多文件结构
会把声明放头文件,把实现放源文件
编译预处理
理解 #include、头文件保护、宏和条件编译
4、章节目录
小节
内容
本节关键词
5.1
标识符的作用域与可见性
作用域、可见性、名称遮蔽
5.2
对象的生存期
自动对象、静态对象、动态对象
5.3
类的静态成员
static 数据成员、static 成员函数
5.4
类的友元
友元函数、友元类
5.5
共享数据的保护
const、常对象、常成员函数
5.6
多文件结构和编译预处理
头文件、源文件、#include、宏
5、作用域、可见性、生存期 这三个词很像,但讨论的问题不同。
概念
讨论的问题
例子
作用域
名字在哪段代码里有效
局部变量只在块内有效
可见性
当前这个位置能不能引用到某个名字
内层同名变量会遮蔽外层变量
生存期
对象从什么时候创建,到什么时候销毁
局部变量离开块后销毁
可以这样记:作用域管“名字能不能用”,生存期管“对象还在不在” 。名字不可见时,对象不一定已经消失;对象消失后,名字即使写出来也没有意义。
6、作用域 6.1 函数原型作用域 函数原型中形参名的有效范围只在原型内部。
1 2 3 4 int maxValue (int a, int b) ;int maxValue (int x, int y) { return x > y ? x : y; }
上面原型中的 a、b 和定义中的 x、y 不需要一致。函数匹配看类型、顺序和个数,不看原型里的参数名。
6.2 块作用域 花括号内部声明的标识符通常具有块作用域。
1 2 3 4 5 6 7 8 9 10 int main () { int x = 10 ; if (x > 0 ) { int y = 20 ; std::cout << y << '\n' ; } }
6.3 类作用域 类的成员名具有类作用域,可以通过对象、指针、引用或作用域限定符访问。
1 2 3 4 5 6 7 8 9 10 11 12 13 class Counter {public : void increment () { ++value; } int getValue () const { return value; } private : int value = 0 ; };
成员函数内部可以直接访问同一个类的成员 value。
6.4 文件作用域和命名空间作用域 定义在所有函数和类之外的名字,具有较大的可见范围。
1 2 3 4 5 int globalCount = 0 ;void addOne () { ++globalCount; }
实际写项目时,不建议随意使用可修改的全局变量 。它会让数据来源和修改位置变得很难追踪。
更推荐用命名空间管理一组相关名字:
1 2 3 namespace config { constexpr int maxUserCount = 100 ; }
7、可见性与名称遮蔽 可见性的一般规则:
规则
说明
先声明,后使用
标识符通常要先声明才能引用
同一作用域不可重复声明同名标识符
同一块内不能重复定义 int x
内层同名会遮蔽外层同名
当前优先使用内层名字
例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #include <iostream> int x = 5 ;int y = 7 ;void myFunction () { int y = 10 ; std::cout << "x from myFunction: " << x << '\n' ; std::cout << "y from myFunction: " << y << "\n\n" ; } int main () { std::cout << "x from main: " << x << '\n' ; std::cout << "y from main: " << y << "\n\n" ; myFunction (); std::cout << "Back from myFunction!\n\n" ; std::cout << "x from main: " << x << '\n' ; std::cout << "y from main: " << y << '\n' ; return 0 ; }
输出:
1 2 3 4 5 6 7 8 9 10 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
myFunction() 里的局部变量 y 遮蔽了全局变量 y,但没有改变全局变量 y。
名称遮蔽不是错误,但容易让代码难读。复盘时看到同名变量,要先判断当前使用的是哪一层作用域里的名字。
8、对象的生存期 8.1 自动生存期 局部非静态对象通常具有自动生存期。进入块时创建,离开块时销毁。
1 2 3 void f () { int x = 10 ; }
8.2 静态生存期 全局对象、命名空间作用域对象、static 局部对象、类的静态数据成员通常具有静态生存期。它们在程序运行期间一直存在。
1 2 3 4 5 void countCall () { static int count = 0 ; ++count; std::cout << count << '\n' ; }
每次调用 countCall(),count 都不会重新初始化。
8.3 动态生存期 动态对象由程序控制创建和销毁。
1 2 int * p = new int (10 );delete p;
现代 C++ 更推荐用标准库对象和智能指针管理资源,减少手动 new/delete。
1 2 3 #include <memory> auto p = std::make_unique <int >(10 );
8.4 临时对象 表达式中可能产生临时对象,它们通常在完整表达式结束后销毁。
1 std::string name = std::string ("Rui" ) + "qing" ;
临时对象的生命周期和引用绑定有关,后续学到右值引用、移动语义时会继续遇到。
9、类的静态成员 9.1 静态数据成员 普通数据成员是每个对象各有一份,静态数据成员是整个类共享一份。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class Cat {public : explicit Cat (int ageValue) : age(ageValue) { ++howManyCats; } ~Cat () { --howManyCats; } static int getHowMany () { return howManyCats; } private : int age; static int howManyCats; }; int Cat::howManyCats = 0 ;
成员类型
拥有者
份数
普通数据成员
对象
每个对象一份
静态数据成员
类
全类共享一份
在 C++17 之前,类内声明的静态数据成员通常还需要在类外定义一次。否则可能出现链接错误。
C++17 之后可以使用 inline static:
1 2 3 4 class Cat {private : inline static int howManyCats = 0 ; };
教材阶段先掌握“类内声明、类外定义”的传统写法。
9.2 静态成员函数 静态成员函数属于类,不属于某个具体对象。
1 2 3 4 5 6 7 8 9 class Counter {public : static int getCount () { return count; } private : static int count; };
调用方式:
静态成员函数没有 this 指针,不能直接访问普通数据成员。
1 2 3 4 5 6 7 8 9 class Example {public : static void f () { } private : int value = 0 ; };
10、友元 友元是 C++ 给封装开的一个“受控入口”。被声明为友元的函数或类,可以访问该类的私有成员和保护成员。
10.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 class Boat ;class Car {public : explicit Car (int weightValue) : weight(weightValue) { } friend int totalWeight (const Car& car, const Boat& boat) ; private : int weight; }; class Boat {public : explicit Boat (int weightValue) : weight(weightValue) { } friend int totalWeight (const Car& car, const Boat& boat) ; private : int weight; }; int totalWeight (const Car& car, const Boat& boat) { return car.weight + boat.weight; }
totalWeight() 不是 Car 或 Boat 的成员函数,但它能访问二者私有成员。
10.2 友元类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Fuel ;class Engine { friend class Fuel ; private : int powerLevel = 0 ; }; class Fuel {public : void boost (Engine& engine) { engine.powerLevel += 10 ; } };
Fuel 是 Engine 的友元类,所以 Fuel 的成员函数可以访问 Engine 的私有成员。
10.3 友元关系的限制
特点
说明
不具有交换性
A 是 B 的友元,不代表 B 是 A 的友元
不具有传递性
A 友元 B,B 友元 C,不代表 A 友元 C
不能被继承
基类的友元关系不会自动给派生类
友元能绕过访问控制。它适合表达两个类之间确实紧密协作的情况,不适合当作“嫌 getter/setter 麻烦”的捷径。
11、共享数据的保护 共享数据越多,越要控制谁能改、谁只能看。C++ 里最基础的保护工具就是 const。
11.1 常对象 常对象创建后不能调用会修改对象状态的成员函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class Date {public : Date (int y, int m, int d) : year (y), month (m), day (d) {} void print () const { std::cout << year << '-' << month << '-' << day << '\n' ; } private : int year; int month; int day; }; const Date birthday (2000 , 9 , 1 ) ;birthday.print ();
11.2 常成员函数 成员函数后面的 const 表示它不会修改对象的可观察状态。
1 2 3 int getValue () const { return value; }
常对象只能调用常成员函数。
11.3 常数据成员 常数据成员必须通过初始化列表初始化。
1 2 3 4 5 6 7 8 9 class Student {public : Student (int idValue, std::string nameValue) : id (idValue), name (nameValue) {} private : const int id; std::string name; };
11.4 常引用和常指针 常引用常用于避免复制,同时保证函数不修改实参。
1 2 3 void printName (const std::string& name) { std::cout << name << '\n' ; }
指针里的 const 要看位置:
1 2 const int * p1 = &x; int * const p2 = &x;
写法
含义
const int* p
不能通过 p 改所指对象
int const* p
和 const int* p 等价
int* const p
p 自己不能改指向
const int* const p
指向和所指内容都不能通过 p 改
12、多文件结构和编译预处理 12.1 头文件和源文件 常见组织方式:
文件
放什么
.h / .hpp
类声明、函数声明、常量声明
.cpp
函数实现、类成员函数实现
main.cpp
程序入口和流程组织
例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 #ifndef COUNTER_H #define COUNTER_H class Counter {public : void add () ; int getValue () const ; private : int value = 0 ; }; #endif
1 2 3 4 5 6 7 8 9 10 #include "counter.h" void Counter::add () { ++value; } int Counter::getValue () const { return value; }
1 2 3 4 5 6 7 8 9 10 #include <iostream> #include "counter.h" int main () { Counter counter; counter.add (); std::cout << counter.getValue () << '\n' ; return 0 ; }
12.2 头文件保护 头文件保护防止同一个头文件被重复包含。
1 2 3 4 5 6 #ifndef COUNTER_H #define COUNTER_H #endif
也常见:
教材阶段优先掌握 #ifndef / #define / #endif。
12.3 宏与常量 传统 C/C++ 常用宏定义常量:
现代 C++ 更推荐:
1 constexpr int maxCount = 100 ;
constexpr 有类型检查,调试和作用域管理都更友好。
13、本章代码复盘 下面这个程序把静态成员、常成员函数、对象生存期和共享数据保护串起来。
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 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 #include <iostream> #include <string> class BankAccount {public : BankAccount (std::string ownerValue, double balanceValue) : owner (ownerValue), balance (balanceValue), accountId (nextId++) { ++accountCount; } BankAccount (const BankAccount& other) : owner (other.owner), balance (other.balance), accountId (nextId++) { ++accountCount; } ~BankAccount () { --accountCount; } void deposit (double amount) { if (amount > 0 ) { balance += amount; } } double getBalance () const { return balance; } int getAccountId () const { return accountId; } static int getAccountCount () { return accountCount; } private : std::string owner; double balance; const int accountId; static int accountCount; static int nextId; }; int BankAccount::accountCount = 0 ;int BankAccount::nextId = 1001 ;int main () { BankAccount a ("Rui" , 1000.0 ) ; BankAccount b = a; a.deposit (200.0 ); std::cout << "a id: " << a.getAccountId () << ", balance: " << a.getBalance () << '\n' ; std::cout << "b id: " << b.getAccountId () << ", balance: " << b.getBalance () << '\n' ; std::cout << "accounts: " << BankAccount::getAccountCount () << '\n' ; return 0 ; }
观察点:
观察点
说明
accountCount
静态数据成员,所有对象共享
nextId
静态数据成员,用于生成编号
accountId
常数据成员,只能初始化,不能后续赋值
getBalance() const
常成员函数,只读访问对象
复制构造函数
复制账户时生成新的 accountId
14、课后习题答案 以下答案按现代 C++ 重新整理,原答案中的老式 iostream.h、void main() 和明显笔误已经修正。
5-1 什么叫作用域?有哪几种类型的作用域 作用域是标识符在程序文本中有效的范围。
常见作用域:
作用域
说明
函数原型作用域
函数原型中参数名的有效范围
块作用域
{} 内声明的局部名字
类作用域
类成员名在类内有效
文件/命名空间作用域
函数和类外声明的名字
5-2 什么叫可见性?可见性的一般规则是什么 可见性讨论的是在程序某个位置能不能引用到某个标识符。
一般规则:
规则
说明
先声明,后引用
使用前要能看到声明
同一作用域不能重复声明同名标识符
避免名称冲突
内层同名标识符会遮蔽外层
当前作用域优先级更高
5-3 观察程序输出 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 #include <iostream> void myFunction () ;int x = 5 ;int y = 7 ;int main () { std::cout << "x from main: " << x << '\n' ; std::cout << "y from main: " << y << "\n\n" ; myFunction (); std::cout << "Back from myFunction!\n\n" ; std::cout << "x from main: " << x << '\n' ; std::cout << "y from main: " << y << '\n' ; return 0 ; } void myFunction () { int y = 10 ; std::cout << "x from myFunction: " << x << '\n' ; std::cout << "y from myFunction: " << y << "\n\n" ; }
输出:
1 2 3 4 5 6 7 8 9 10 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
解释:myFunction() 中定义了局部变量 y,它遮蔽了全局变量 y。x 没有被局部定义遮蔽,所以访问的是全局 x。
5-4 怎样允许 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 #include <iostream> class Fuel ;class Engine { friend class Fuel ; public : Engine () : powerLevel (0 ) {} int getPowerLevel () const { return powerLevel; } private : int powerLevel; }; class Fuel {public : void boost (Engine& engine) { engine.powerLevel += 10 ; } }; int main () { Engine engine; Fuel fuel; fuel.boost (engine); std::cout << engine.getPowerLevel () << '\n' ; return 0 ; }
输出:
5-5 什么叫静态数据成员?它有何特点 静态数据成员是用 static 声明的类成员,属于类本身,而不是某个对象。
特点:
特点
说明
全类共享一份
所有对象访问同一个静态数据成员
可用于对象间共享数据
例如统计对象数量
通常需要类外定义
如 int Cat::howManyCats = 0;
受访问控制影响
可以是 private、public、protected
5-6 什么叫静态函数成员?它有何特点 静态成员函数是用 static 声明的成员函数,属于类本身。
特点:
特点
说明
可通过类名调用
Cat::getHowMany()
没有 this 指针
不绑定具体对象
只能直接访问静态成员
不能直接访问普通数据成员
可作为类级别接口
例如查询对象总数
5-7 定义 Cat 类,用静态成员记录对象个数 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 46 47 48 49 50 51 52 53 54 55 56 #include <iostream> #include <memory> #include <vector> class Cat {public : explicit Cat (int ageValue) : age(ageValue) { ++howManyCats; } ~Cat () { --howManyCats; } int getAge () const { return age; } void setAge (int value) { if (value >= 0 ) { age = value; } } static int getHowMany () { return howManyCats; } private : int age; static int howManyCats; }; int Cat::howManyCats = 0 ;void printCatCount () { std::cout << "There are " << Cat::getHowMany () << " cats alive!\n" ; } int main () { constexpr int maxCats = 5 ; std::vector<std::unique_ptr<Cat>> cats; for (int i = 0 ; i < maxCats; ++i) { cats.push_back (std::make_unique <Cat>(i)); printCatCount (); } while (!cats.empty ()) { cats.pop_back (); printCatCount (); } return 0 ; }
输出:
1 2 3 4 5 6 7 8 9 10 There are 1 cats alive! There are 2 cats alive! There are 3 cats alive! There are 4 cats alive! There are 5 cats alive! There are 4 cats alive! There are 3 cats alive! There are 2 cats alive! There are 1 cats alive! There are 0 cats alive!
5-8 什么叫友元函数?什么叫友元类 友元函数是用 friend 在类中声明的非成员函数,它可以访问该类的私有成员和保护成员。
友元类是用 friend class 声明的类,它的成员函数都可以访问相应类的私有成员和保护成员。
类型
说明
友元函数
某个外部函数获得访问权限
友元类
某个类整体获得访问权限
5-9 友元关系是否具有交换性、传递性、继承性 都不具有。
判断
答案
原因
类 A 是类 B 的友元,类 B 是类 A 的友元吗
不是
友元关系不具有交换性
类 B 是类 C 的友元,类 C 是类 A 的友元吗
不是
友元关系不具有传递性
类 D 是类 A 的派生类,类 D 是类 B 的友元吗
不是
友元关系不能被继承
更准确地说,友元关系只在声明它的那一个类中生效,不会自动扩散。
5-10 静态成员变量可以为私有的吗?声明一个私有静态整型成员变量 可以。
1 2 3 4 5 6 class Example {private : static int value; }; int Example::value = 0 ;
静态成员同样受访问控制影响,设为 private 后只能通过类的成员函数或友元访问。
5-11 在一个文件中定义全局变量 n,另一个文件中定义函数 fn1() 可以用 extern 声明跨文件共享的全局变量。
1 2 3 4 5 6 7 #ifndef FN1_H #define FN1_H void fn1 () ;#endif
1 2 3 4 5 6 7 8 #include "fn1.h" extern int n;void fn1 () { n = 30 ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 #include <iostream> #include "fn1.h" int n = 0 ;int main () { n = 20 ; fn1 (); std::cout << "n 的值为 " << n << '\n' ; return 0 ; }
输出:
这道题是为了理解 extern 和多文件共享变量。实际项目里可修改全局变量要谨慎使用。
5-12 在 fn1() 中定义静态变量 n,主函数调用十次 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #include <iostream> void fn1 () { static int n = 0 ; ++n; std::cout << "n 的值为 " << n << '\n' ; } int main () { for (int i = 0 ; i < 10 ; ++i) { fn1 (); } return 0 ; }
输出:
1 2 3 4 5 6 7 8 9 10 n 的值为 1 n 的值为 2 n 的值为 3 n 的值为 4 n 的值为 5 n 的值为 6 n 的值为 7 n 的值为 8 n 的值为 9 n 的值为 10
解释:static int n = 0; 只初始化一次,函数调用结束后 n 不销毁,下一次调用继续使用上一次的值。
5-13 定义类 X、Y、Z 和函数 h(X*),练习友元函数与友元类 一个更完整的多文件写法如下。
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 #ifndef MY_X_Y_Z_H #define MY_X_Y_Z_H class X ;class Y {public : void g (X* x) ; }; class Z ;class X {public : X () : i (0 ) {} int getValue () const { return i; } friend void h (X* x) ; friend void Y::g (X* x) ; friend class Z ; private : int i; }; class Z {public : void f (X* x) { x->i += 5 ; } }; void h (X* x) ;#endif
1 2 3 4 5 6 7 8 9 10 #include "my_x_y_z.h" void Y::g (X* x) { ++x->i; } void h (X* x) { x->i += 10 ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #include <iostream> #include "my_x_y_z.h" int main () { X x; Y y; Z z; y.g (&x); z.f (&x); h (&x); std::cout << x.getValue () << '\n' ; return 0 ; }
输出:
这里 Y::g() 给 i 加 1,Z::f() 给 i 加 5,h() 给 i 加 10。
原答案中的 x->i =+10; 实际是把 i 赋值为正 10,不是加 10。这里已经改成 x->i += 10;。
5-14 定义 Boat 与 Car,用友元函数计算总重量 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 #include <iostream> class Boat ;class Car {public : explicit Car (int weightValue) : weight(weightValue) { } friend int totalWeight (const Car& car, const Boat& boat) ; private : int weight; }; class Boat {public : explicit Boat (int weightValue) : weight(weightValue) { } friend int totalWeight (const Car& car, const Boat& boat) ; private : int weight; }; int totalWeight (const Car& car, const Boat& boat) { return car.weight + boat.weight; } int main () { Car car (4 ) ; Boat boat (5 ) ; std::cout << totalWeight (car, boat) << '\n' ; return 0 ; }
输出:
5-15 类模板中有静态数据成员,运行中会产生多少个静态变量 类模板的每一个实例化类型都会产生一份对应的静态数据成员。
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 #include <iostream> template <typename T>class Counter {public : Counter () { ++count; } static int getCount () { return count; } private : static int count; }; template <typename T>int Counter<T>::count = 0 ;int main () { Counter<int > a; Counter<int > b; Counter<double > c; std::cout << Counter<int >::getCount () << '\n' ; std::cout << Counter<double >::getCount () << '\n' ; return 0 ; }
输出:
解释:Counter<int> 和 Counter<double> 是两个不同的实例化类,所以它们分别有自己的 count。
15、本章复盘
复盘问题
自查答案
作用域和生存期有什么区别
作用域管名字,生存期管对象
局部静态变量什么时候初始化
第一次执行到定义处时初始化一次
静态成员函数能否直接访问普通成员
不能,因为没有 this 指针
友元关系是否可交换、传递、继承
都不可以
const 成员函数的意义是什么
承诺不修改对象状态,常对象只能调用它
头文件为什么要保护
防止重复包含导致重复声明/定义问题
extern 的作用是什么
声明一个在别处定义的外部链接变量或函数
下一章 Chap6 会进入数组、指针与字符串,也是 C++ 基础里最容易出错、但复盘价值很高的一章。