面向对象的方法就是利用抽象、封装等机制,借助于对象、类、继承、消息传递等概念进行软件系统构造的软件开发方法。

这一章主要解决之前遗留的一些问题,作为对上述章节的补充。很多补充的内容直接会写在之前的文档里,所以这里很大程度上是作为补充材料的。

在此之前积累的一些问题

  1. 变量和对象可以定义在不同的位置:函数体内、类体内、函数原型参数表内、所有函数和类之外,使用的时候分别有什么不同、访问和共享有什么限制呢?
    • 不同位置定义的变量和对象,其作用域可见性生存期都不同。如果要在不同的程序模块间共享数据,就需要了解变量和对象的作用域、可见性、生存期。
  2. 如何在同一个类的所有对象之间共享数据?比如需要记录一个类的对象总数。
    • 定义属于整个类而不是对象的数据成员——静态数据成员
    • 定义用于处理静态数据成员的函数——静态成员函数
  3. 类的私有成员在类外不能直接访问,这是为了保护数据的安全性和隐藏细节。但是需要频繁访问私有数据时,调用接口函数的开销比较大。
    • 对一些类外的函数、其他的类,给预授权,使之可以访问类的私有成员——友元
    • 提高了效率,但是带来一些安全隐患,需要权衡、慎用
  4. 共享数据的安全性如何保证
    • 通过const关键字,限制对共享数据的修改,使共享的数据在被共享时,是只读的。
  5. 在编译之前,需要进行预处理,例如包含头文件,选择在不同情况下编译程序的不同部分
    • 编译预处理
  6. 当程序的规模略大些的时候,就不能将所有代码放在一个文件里了
    • 多文件结构

学习建议

  • 完成练习题和编程作业
  • 编写程序(或者修改例题)观察和验证变量的作用域、可见性、生存期
  • 除了完成作业,希望
    1. 能自己举出一些需要定义静态成员的例子,并编写程序实现
    2. 找出以前的例题、习题,看看可否改写,将一些类的成员函数改为常函数;将一些函数的参数定义为常引用。
    3. 尝试在程序中使用编译预处理命令,并观察效果。

5.1 标识符的作用域与可见性

作用域是一个标识符在程序正文中有效的区域。可用{ } 进行划分

作用域分类:

1、函数原型作用域(声明)

函数原型中的参数,其作用域始于”(“,结束于”)”

所以声明中可以只给出类型,不用形参名,但一般写上好读

2、局部/块 作用域

比如说,函数定义中函数的形参、在块中声明的标识符;

其作用域自声明处起,限于块中。

image-20200324155208342.png

3、类作用域

类的成员具有类作用域,其范围包括类体和非内联成员函数的函数体。

如果在类作用域以外访问类的成员,要通过类名(访问静态成员),或者该类的对象名、对象引用、对象指针(访问非静态成员)。

4、文件作用域

不在前述各个作用域中出现的声明,就具有文件作用域,这样声明的标识符其作用域开始于声明点,结束于文件尾。

5、命名空间作用域

  1. namespace xxx { }

可见性

可见性表示从内层作用域向外层作用域“看”时能看见什么。

如果某个标识符在外层中声明,且在内层中没有同一标识符的声明,则该标识符在内层可见。

对于两个嵌套的作用域,如果在内层作用域内声明了与外层作用域中同名的标识符,则外层作用域的标识符在内层不可见。

代码说明:

  1. #include<iostream>
  2. using namespace std; //引入命名空间
  3. int i; //全局变量,文件作用域
  4. struct Aclass{
  5. int a=8; // 类作用域
  6. void showElem1(int a); //a 可有可无,这是函数原型作用域
  7. void showElem2();
  8. };
  9. void Aclass::showElem1(int a ){ //a 不可少 这里是块作用域 ,::是取作用域符
  10. std::cout<<this->a<<" <--Class a,Input a--> "<< a <<std::endl;
  11. }
  12. void Aclass::showElem2(){ //a 不可少 这里是块作用域 ,::是取作用域符
  13. std::cout<<a<<" <--Class a "<<std::endl;
  14. }
  15. int main() {
  16. i = 5; //为全局变量i赋值
  17. {
  18. int i; //局部变量,局部作用域
  19. i = 7;
  20. cout << "i = " << i << endl;//输出7
  21. }
  22. cout << "i = "<< i << endl;//输出5
  23. Aclass aa;
  24. aa.showElem2(); //默认情况类数据是可见的 输出:8 <--Class a
  25. //当形参与类数据冲突时,可以用this指针让类数据可见。输出:8 <--Class a,Input a--> 5
  26. aa.showElem1(i);
  27. return 0;
  28. }

5.2 对象的生存期

对象生存期就是对象诞生开始到它结束这段时间

在对象生存期内,对象将保存它的值,直到被更新

1、静态生存期

生存期与整个程序运行期相同

声明一个静态变量:添加关键字 static ,

静态变量再初始化时被赋值,只会进行一次;如果没有赋值,int型默认是0

2、动态生存期

块作用域中声明的,没有用static修饰的对象是动态生存期的对象(习惯称局部生存期对象)。

开始于程序执行到声明点时,结束于命名该标识符的作用域结束处。

  1. #include<iostream>
  2. using namespace std;
  3. int i = 1; // i 为全局变量,具有静态生存期。
  4. void other() {
  5. static int a = 2;
  6. static int b;
  7. // a,b为静态局部变量,具有全局寿命,局部可见。
  8. //只第一次进入函数时被初始化。
  9. int c = 10; // C为局部变量,具有动态生存期,
  10. //每次进入函数时都初始化。
  11. a += 2; i += 32; c += 5;
  12. cout<<"---OTHER---\n";
  13. cout<<" i: "<<i<<" a: "<<a<<" b: "<<b<<" c: "<<c<<endl;
  14. b = a;
  15. }
  16. int main() {
  17. static int a;//静态局部变量,有全局寿命,局部可见。
  18. int b = -10; // b, c为局部变量,具有动态生存期。
  19. int c = 0;
  20. cout << "---MAIN---\n";
  21. cout<<" i: "<<i<<" a: "<<a<<" b: "<<b<<" c: "<<c<<endl;
  22. c += 8; other();
  23. cout<<"---MAIN---\n";
  24. cout<<" i: "<<i<<" a: "<<a<<" b: "<<b<<" c: "<<c<<endl;
  25. i += 10; other();
  26. return 0;
  27. }
  28. 运行结果
  29. ---MAIN---
  30. i: 1 a: 0 b: -10 c: 0
  31. ---OTHER---
  32. i: 33 a: 4 b: 0 c: 15
  33. ---MAIN---
  34. i: 33 a: 0 b: -10 c: 8
  35. ---OTHER---
  36. i: 75 a: 6 b: 4 c: 15

5.3 类的静态成员

1、静态数据成员

为整个类所共享 ; 具有静态生存周期;

必须在类外定义和初始化;

2、静态函数成员

用于处理静态数据;

不一定要在类外定义;

代码说明:

数据共享与保护 - 图2

  1. #include <iostream>
  2. #include <string>
  3. using namespace std;
  4. class Point{
  5. int x,y;
  6. static int count;
  7. public:
  8. void showPosition(){cout<<"X:"<<x<<" Y:"<<y<<endl;}
  9. static void showCount(){cout<<"Count:"<<count<<endl;}
  10. Point(int x =0, int y = 0):x(x),y(y){ ++count;}
  11. Point(Point &p):Point(p.x,p.y){}
  12. ~Point(){--count;}
  13. };
  14. int Point::count = 0;
  15. int main(){
  16. Point::showCount();
  17. Point a;
  18. a.showPosition(); a.showCount();
  19. Point b(4,7);
  20. b.showPosition(); Point::showCount();
  21. a.~Point();
  22. Point::showCount();
  23. return 0;
  24. }
  25. ----------输出结果----------
  26. Count:0
  27. X:0 Y:0
  28. Count:1
  29. X:4 Y:7
  30. Count:2
  31. Count:1

5.4 类的友元

友元是C++提供的一种破坏数据封装和数据隐藏的机制。

通过将一个模块声明为另一个模块的友元,一个模块能够引用到另一个模块中本是被隐藏的信息。

可以使用友元函数友元类

为了确保数据的完整性,及数据封装与隐藏的原则,建议尽量不使用或少使用友元。

1、友元函数

友元函数是在类声明中由关键字friend修饰说明的非成员函数,在它的函数体中能够通过对象名访问 private 和 protected成员

作用:增加灵活性,使程序员可以在封装和快速性方面做合理选择。

友元包括声明与定义。友元声明默认为了extern,就是说友元类或友元函数作用域已扩展至包含该类定义的作用域,即便在类的内部定义友元函数也没有关系。

访问对象中的成员必须通过对象名。

  1. #include <iostream>
  2. #include <cmath>
  3. using namespace std;
  4. class Point { //Point类声明
  5. public: //外部接口
  6. Point(int x=0, int y=0) : x(x), y(y) { }
  7. int getX() { return x; }
  8. int getY() { return y; }
  9. friend float dist(const Point &a, const Point &b);
  10. private: //私有数据成员
  11. int x, y;
  12. };
  13. float dist( const Point& a, const Point& b) {
  14. double x = a.x - b.x;
  15. double y = a.y - b.y;
  16. return static_cast<float>(sqrt(x * x + y * y));
  17. }
  18. int main() {
  19. Point p1(1, 1), p2(4, 5);
  20. cout <<"The distance is: ";
  21. cout << dist(p1, p2) << endl;
  22. return 0;
  23. }

这里模拟了一个大规模调用点数据的场景:当使用get函数来获取点的数据的时候会增加程序的时间空间开销。同样为了节省开销,会看到友元函数和友元类的参数都是引用。所以为了保护被传入对象不被修改,会加以const修饰符。

2、友元类

若一个类为另一个类的友元,则此类的所有成员都能访问对方类的私有成员。

这种情况很像继承关系中的protect

声明语法:将友元类名在另一个类中使用friend修饰说明。

  1. class A {
  2. friend class B; //B是A的友元,A不一定是B的友元
  3. public:
  4. void display() {cout << x << endl;}
  5. private:
  6. int x;
  7. };
  1. class B { //组合类
  2. public:
  3. void set(int i);
  4. void display();
  5. private:
  6. A a; //部件类
  7. };
  1. void B::set(int i) {a.x=i;}
  2. void B::display() {a.display();}

类的友元关系是单向的

如果声明B类是A类的友元,B类的成员函数就可以访问A类的私有和保护数据,但A类的成员函数却不能访问B类的私有、保护数据。

5.5 共享数据的保护 const

对于既需要共享、又需要防止改变的数据应该声明为常类型(用const进行修饰)或者常引用

对于不改变对象状态的成员函数应该声明为常函数

1、常对象

必须进行初始化,不能被更新。

  1. const 类名 对象名

2、常引用

被引用的对象不能被更新。

  1. const 类型说明符 &引用名

3、常函数

常成员函数不更新对象的数据成员。

  1. 类型说明符 函数名(参数表)const;

这里,const是函数类型的一个组成部分,因此在实现部分也要带const关键字。

  1. const关键字可以被用于参与对重载函数的区分

l 通过常对象只能调用它的常成员函数。所以一般对于不改变对象状态的函数都声明为常函数

4、常成员

用const修饰的对象成员

  1. 常数据成员和常函数成员

常成员是不能被放在构造函数函数体中赋值的,它必须在初始化列表中初始化

5、常数组

数组元素不能被更新

  1. 类型说明符 const 数组名[大小]...

6、常指针

指向常量的指针

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

1、C++程序一般组织结构

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

  • 类声明文件(.h文件)
  • 类实现文件(.cpp文件)
  • 类使用文件(main()入口函数所在的.cpp文件)
  • 利用工程(vs)来结合各个文件(编译单元)

1195561228676763648.png

过程:编译-连接-生成可执行文件

2、外部函数和外部变量

一个C++工程中,如果main.cpp需要调用在functions.cpp中定义的函数,须在头文件common.h中加入函数的声明,在main.cpp和functions.cpp中都需要加入

  1. #include<common.h>

如果需要多个文件共享的全局变量,则在头文件common.h中用extern关键字声明变量(但不能初始化),在需要用到该变量的文件中定义该变量

  1. extern int g_flag; //common.h
  2. int g_flag = 0; // main.cpp

外部变量

一个变量除了在定义它的源文件中可以被使用,还可以被其他文件使用

文件作用域下的变量默认都是外部变量,但如果其他文件需要使用它,需要用extern关键字加以声明。

外部函数

在所有类之外声明的函数(非成员函数),都是据哟文件作用域的

文件作用域下的函数默认都是外部函数,但如果其他文件需要使用它,只需要进行声明,extern关键字修饰是可选的。

将变量和单元限制在编译单元内

使用匿名命名空间的函数和变量,是不会暴露给其他编译单元的。

  1. namespace { /* ....... */ }

结合以下代码了解外部函数的使用:在B中使用A的变量以及函数

  1. //linkA.cpp
  2. #include <iostream>
  3. #include "common.h"
  4. using namespace std;
  5. int flag = 22;
  6. void func0( int n){
  7. cout<< n<<"from linkA" <<endl;
  8. }
  9. ----------------------------
  10. //common.h
  11. #ifndef COMMON_H
  12. #define COMMON_H
  13. void func0( int n);
  14. extern int flag;
  15. #endif
  16. ----------------------------
  17. //linkB.cpp
  18. #include <iostream>
  19. #include <string>
  20. #include "common.h"
  21. using namespace std;
  22. void func1( int n){
  23. cout<< n;
  24. }
  25. int main(){
  26. func1(flag);
  27. func0(10);
  28. return 0;
  29. }
  30. ----------------------------
  31. //输出
  32. 2210from linkA

注意 这里不能用常规的编译方法,要手动编译链接:

  1. g++ -c linkA.cpp #编译
  2. g++ -c linkB.cpp #编译
  3. g++ -o linkB linkB.o linkA.o #连接生成 linkB.exe
  4. ./linkB.exe #运行程序
  5. 如果按照我们平时的默认方式,第三行其实是 g++ -o linkB linkB.o
  6. 这时候报错:
  7. main.o: In function `main':
  8. main.c:(.text+0x7): undefined reference to `test'
  9. collect2: ld returned 1 exit status
  10. 说明是连接时缺失相关目标文件
  11. 其实一行代码解决问题:g++ -o linkB linkB.cpp linkA.cpp #连接生成 linkB.exe

3、标准C++库

逻辑上分为6种类型

  1. 输入/输出类
  2. 容器类与抽象数据类型
  3. 存储管理类
  4. 算法
  5. 错误处理
  6. 运行环境支持

4、编译预处理命令

#include包含指令

头文件中<>与“ ”的区别

1、标准库的文件用<>,不用写拓展名。非标准库的头文件用“”,一般以h、hpp、hxx结束。

2、检索路径不一样,前者只搜索includePath下,后者首先检索当前目录然后才是标准搜索的includePath下。

  1. #include <iostream>
  2. #include "seal_statistic.h"

#define 宏定义指令

定义符号常量,很多时候被const代替了

定义带参数的宏,已被内联函数取代

undef 删除定义的宏,使之不再起作用

条件编译指令

  1. #if 常量表达式1
  2. 程序正文1 //当“ 常量表达式1”非零时编译
  3. #elif 常量表达式2
  4. 程序正文2 //当“ 常量表达式2”非零时编译
  5. #else
  6. 程序正文3 //其他情况下编译
  7. #endif
  1. #ifndef 标识符
  2. #defin 标识符
  3. 程序段
  4. #endif