标识符的作用域与可见性

作用域

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

函数原型作用域

在函数原型声明时形式参数的作用范围就是函数原型作用域。

函数原型的形参列表中起作用的只是类型,而非标识符,允许省去,为了可读性,最好写上。

局部作用域

简单理解就是函数体内声明的变量,从声明处开始,一直到声明所在块结束的大括号为止。

具有局部作用域的变量也叫做局部变量。

类作用域

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

  1. 成员函数中没有定义m标识符,成员m没有被函数体屏蔽,该函数可以直接访问到m;
  2. 通过表达式x.m或者X::m这是最基本的方法,后者主要用来访问类的静态成员。
  3. 通过ptr->m访问,其中ptr为指向该类对象的一个指针

命名空间作用域

命名空间的作用是为了消除一个项目中可能具有歧义的不同的文件,例如:两个不同的模块之前变量的命名重名了。

语法形式如下:

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

在命名空间的内部可以直接使用当前空间所定义的标识符,如果需要使用其他命名空间中所定义的标识符,需要使用命名空间名::标识符。为了避免冗长,又提供了using语句。

using语句有两种形式

1
2
using 命名空间名::标识符
using namespace 命名空间名

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

全局命名空间是默认的命名空间,在显式声明的命名空间之外声明的标识符都是在一个全局命名空间里。

匿名命名空间只需要在定义时省去命名空间名即可,作用就是不想把自己定义的标识符给任何其他的命名空间访问的机会。

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

另外,命名空间允许嵌套

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


可见性

内容比较简单浅显,故略去。


对象的生存期

静态生存期

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

特点:不会随着每次函数的调用都产生一个副本,也不随着函数的返回而失效。变量在每次的调用期间时共享的。同时只进行一次赋值,不会多次执行声明时的赋值语句。

若不在声明的时候初始化,则默认为0。

动态生存期

局部生存期对象诞生于声明点,结束于声明所在的块执行完毕之时。

类的静态成员

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

例如有一个雇员类,我们有若干个雇员对象,但我需要统计有多少个雇员对象怎么办呢?这时候就可以用静态数据成员,这个数据成员被所有的对象共享。

静态数据成员

某个属性为整个类所共有不属于任何一个具体的对象。则采用static关键字声明为静态成员,整个类中只有这一份副本,由所有的对象共同管理维护和使用。

因为它不属于任何对象,具有静态生存期,所以通过类名对其进行访问。“类名::标识符”。

在类的定义中仅仅进行引用性声明,必须在命名空间作用域的某个地方使用类名限定定义性声明,这时也可以进行初始化。

程序实例

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++; //在构造函数中对count累加,所有对象共同维护同一个count
}
Point(Point &p){ //复制构造函数
x = p.x;
y = p.y;
count++;
}
~Point() { count--; }
int getX() { return x; }
int getY() { return y; }
void showCount() { //输出静态数据成员
cout << " Object count = " << count << endl;
}
private: //私有数据成员
int x, y;
static int count; //静态数据成员声明,用于记录点的个数
};
int Point::count = 0;//静态数据成员定义和初始化,使用类名限定
int main() { //主函数
Point a(4, 5); //定义对象a,其构造函数回使count增1
cout << "Point A: " << a.getX() << ", " << a.getY(); a.showCount(); //输出对象个数
Point b(a); //定义对象b,其构造函数回使count增1
cout << "Point B: " << b.getX() << ", " << b.getY(); b.showCount(); //输出对象个数
return 0;
}

静态成员函数

上面的程序中其实存在一个问题,就是showcount函数的调用前提是必须要有Point对象的存在,但是如果我想要直接输出count的值怎么办,这时候就需要使用静态成员函数,通过类名去直接进行函数的调用,而不依赖对象。

虽然静态成员函数也可以使用对象访问,但是一般习惯是通过类名访问,即使通过对象名访问,函数也和对象之间没有关系。

类的友元

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

设置成类外的普通函数,不能体现函数与点的联系,而且不能直接使用点的坐标;

设置成类内成员函数,却不符合类是一类事物特征的抽象,因为距离表示的是点和点之间的关系,而非点的特征。

类的组合中,有一个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
类型说明符 函数名(参数表) const;

注意⚠️:

  • 如果一个对象是常对象,则通过该常对象只能调用常成员函数,不能够调用别的成员函数!这是C++对于常对象的保护,也是常对象唯一的对外接口方式
  • 无论是否通过常对象调用常成员函数,在常成员函数的调用期间,目的对象都被视作常对象,因此常成员函数不能更新目的对象的数据成员,也不能针对目的对象调用该类中没有用const修饰的成员函数(保证了常成员函数不会修改目的对象的数据成员的值)
  • const关键字可以作为重载函数的区分(同名函数是否加const是不同的函数)。

常数据成员

使用const说明的数据成员为常数据成员,任何函数不能对其赋值。构造函数通过对该数据成员进行初始化,只能通过初始化列表获取初值。

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

常引用

如果在声明引用时用const修饰,被声明的引用就是常引用,常引用所引用的对象不能被更新。用来作为函数的形参时,并不会意外的发生对实参的更改。

对于在函数中无法改变其值的参数,不宜使用普通引用方式传递,因为会使得常对象无法被传入,采用传值的方式或传递常引用的方式可以避免这一问题,传值耗时较多,传递常引用为好。复制构造函数的参数一般也选用常引用!

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

由于有过C语言的基础,此部分仅挑选一些不了解和一些印象不深的内容进行罗列。

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

一个工程可以划分为多个源文件:

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

标准C++库

标准C++类库是一个极为灵活并可扩展的可重用软件模块的集合

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

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

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

程序源码上传至了github,使用makefile进行编译运行

严重错误,静态变量没有在外部进行赋初始值,导致我进度直接停滞两小时,而且是在类的成员函数定义的文件内进行初始化赋值。