面向对象的方法就是利用抽象、封装等机制,借助于对象、类、继承、消息传递等概念进行软件系统构造的软件开发方法。
这一章主要解决之前遗留的一些问题,作为对上述章节的补充。很多补充的内容直接会写在之前的文档里,所以这里很大程度上是作为补充材料的。
在此之前积累的一些问题
- 变量和对象可以定义在不同的位置:函数体内、类体内、函数原型参数表内、所有函数和类之外,使用的时候分别有什么不同、访问和共享有什么限制呢?
- 不同位置定义的变量和对象,其
作用域
、可见性
、生存期
都不同。如果要在不同的程序模块间共享数据,就需要了解变量和对象的作用域、可见性、生存期。
- 不同位置定义的变量和对象,其
- 如何在同一个类的所有对象之间共享数据?比如需要记录一个类的对象总数。
- 定义属于整个类而不是对象的数据成员——
静态数据成员
- 定义用于处理静态数据成员的函数——
静态成员函数
- 定义属于整个类而不是对象的数据成员——
- 类的私有成员在类外不能直接访问,这是为了保护数据的安全性和隐藏细节。但是需要频繁访问私有数据时,调用接口函数的开销比较大。
- 对一些类外的函数、其他的类,给预授权,使之可以访问类的私有成员——
友元
- 提高了效率,但是带来一些安全隐患,需要权衡、慎用
- 对一些类外的函数、其他的类,给预授权,使之可以访问类的私有成员——
- 共享数据的安全性如何保证
- 通过
const
关键字,限制对共享数据的修改,使共享的数据在被共享时,是只读的。
- 通过
- 在编译之前,需要进行预处理,例如包含头文件,选择在不同情况下编译程序的不同部分
编译预处理
- 当程序的规模略大些的时候,就不能将所有代码放在一个文件里了
- 多文件结构
学习建议
- 完成练习题和编程作业
- 编写程序(或者修改例题)观察和验证变量的作用域、可见性、生存期
- 除了完成作业,希望
- 能自己举出一些需要定义静态成员的例子,并编写程序实现
- 找出以前的例题、习题,看看可否改写,将一些类的成员函数改为常函数;将一些函数的参数定义为常引用。
- 尝试在程序中使用编译预处理命令,并观察效果。
5.1 标识符的作用域与可见性
作用域是一个标识符在程序正文中有效的区域。可用{ }
进行划分
作用域分类:
1、函数原型作用域(声明)
函数原型中的参数,其作用域始于”(“,结束于”)”
所以声明中可以只给出类型,不用形参名,但一般写上好读
2、局部/块 作用域
比如说,函数定义中函数的形参、在块中声明的标识符;
其作用域自声明处起,限于块中。
3、类作用域
类的成员具有类作用域,其范围包括类体和非内联成员函数的函数体。
如果在类作用域以外访问类的成员,要通过类名(访问静态成员),或者该类的对象名、对象引用、对象指针(访问非静态成员)。
4、文件作用域
不在前述各个作用域中出现的声明,就具有文件作用域,这样声明的标识符其作用域开始于声明点,结束于文件尾。
5、命名空间作用域
namespace xxx { }
可见性
可见性表示从内层作用域向外层作用域“看”时能看见什么。
如果某个标识符在外层中声明,且在内层中没有同一标识符的声明,则该标识符在内层可见。
对于两个嵌套的作用域,如果在内层作用域内声明了与外层作用域中同名的标识符,则外层作用域的标识符在内层不可见。
代码说明:
#include<iostream>
using namespace std; //引入命名空间
int i; //全局变量,文件作用域
struct Aclass{
int a=8; // 类作用域
void showElem1(int a); //a 可有可无,这是函数原型作用域
void showElem2();
};
void Aclass::showElem1(int a ){ //a 不可少 这里是块作用域 ,::是取作用域符
std::cout<<this->a<<" <--Class a,Input a--> "<< a <<std::endl;
}
void Aclass::showElem2(){ //a 不可少 这里是块作用域 ,::是取作用域符
std::cout<<a<<" <--Class a "<<std::endl;
}
int main() {
i = 5; //为全局变量i赋值
{
int i; //局部变量,局部作用域
i = 7;
cout << "i = " << i << endl;//输出7
}
cout << "i = "<< i << endl;//输出5
Aclass aa;
aa.showElem2(); //默认情况类数据是可见的 输出:8 <--Class a
//当形参与类数据冲突时,可以用this指针让类数据可见。输出:8 <--Class a,Input a--> 5
aa.showElem1(i);
return 0;
}
5.2 对象的生存期
对象生存期就是对象诞生开始到它结束这段时间
在对象生存期内,对象将保存它的值,直到被更新
1、静态生存期
生存期与整个程序运行期相同
声明一个静态变量:添加关键字 static ,
静态变量再初始化时被赋值,只会进行一次;如果没有赋值,int型默认是0
2、动态生存期
块作用域中声明的,没有用static修饰的对象是动态生存期的对象(习惯称局部生存期对象)。
开始于程序执行到声明点时,结束于命名该标识符的作用域结束处。
#include<iostream>
using namespace std;
int i = 1; // i 为全局变量,具有静态生存期。
void other() {
static int a = 2;
static int b;
// a,b为静态局部变量,具有全局寿命,局部可见。
//只第一次进入函数时被初始化。
int c = 10; // C为局部变量,具有动态生存期,
//每次进入函数时都初始化。
a += 2; i += 32; c += 5;
cout<<"---OTHER---\n";
cout<<" i: "<<i<<" a: "<<a<<" b: "<<b<<" c: "<<c<<endl;
b = a;
}
int main() {
static int a;//静态局部变量,有全局寿命,局部可见。
int b = -10; // b, c为局部变量,具有动态生存期。
int c = 0;
cout << "---MAIN---\n";
cout<<" i: "<<i<<" a: "<<a<<" b: "<<b<<" c: "<<c<<endl;
c += 8; other();
cout<<"---MAIN---\n";
cout<<" i: "<<i<<" a: "<<a<<" b: "<<b<<" c: "<<c<<endl;
i += 10; other();
return 0;
}
运行结果
---MAIN---
i: 1 a: 0 b: -10 c: 0
---OTHER---
i: 33 a: 4 b: 0 c: 15
---MAIN---
i: 33 a: 0 b: -10 c: 8
---OTHER---
i: 75 a: 6 b: 4 c: 15
5.3 类的静态成员
1、静态数据成员
为整个类所共享 ; 具有静态生存周期;
必须在类外定义和初始化;
2、静态函数成员
用于处理静态数据;
不一定要在类外定义;
代码说明:
#include <iostream>
#include <string>
using namespace std;
class Point{
int x,y;
static int count;
public:
void showPosition(){cout<<"X:"<<x<<" Y:"<<y<<endl;}
static void showCount(){cout<<"Count:"<<count<<endl;}
Point(int x =0, int y = 0):x(x),y(y){ ++count;}
Point(Point &p):Point(p.x,p.y){}
~Point(){--count;}
};
int Point::count = 0;
int main(){
Point::showCount();
Point a;
a.showPosition(); a.showCount();
Point b(4,7);
b.showPosition(); Point::showCount();
a.~Point();
Point::showCount();
return 0;
}
----------输出结果----------
Count:0
X:0 Y:0
Count:1
X:4 Y:7
Count:2
Count:1
5.4 类的友元
友元是C++提供的一种破坏数据封装和数据隐藏的机制。
通过将一个模块声明为另一个模块的友元,一个模块能够引用到另一个模块中本是被隐藏的信息。
可以使用友元函数和友元类。
为了确保数据的完整性,及数据封装与隐藏的原则,建议尽量不使用或少使用友元。
1、友元函数
友元函数是在类声明中由关键字friend修饰说明的非成员函数,在它的函数体中能够通过对象名访问 private 和 protected成员
作用:增加灵活性,使程序员可以在封装和快速性方面做合理选择。
友元包括声明与定义。友元声明默认为了extern
,就是说友元类或友元函数作用域已扩展至包含该类定义的作用域,即便在类的内部定义友元函数也没有关系。
访问对象中的成员必须通过对象名。
#include <iostream>
#include <cmath>
using namespace std;
class Point { //Point类声明
public: //外部接口
Point(int x=0, int y=0) : x(x), y(y) { }
int getX() { return x; }
int getY() { return y; }
friend float dist(const Point &a, const Point &b);
private: //私有数据成员
int x, y;
};
float dist( const Point& a, const Point& b) {
double x = a.x - b.x;
double y = a.y - b.y;
return static_cast<float>(sqrt(x * x + y * y));
}
int main() {
Point p1(1, 1), p2(4, 5);
cout <<"The distance is: ";
cout << dist(p1, p2) << endl;
return 0;
}
这里模拟了一个大规模调用点数据的场景:当使用get函数来获取点的数据的时候会增加程序的时间空间开销。同样为了节省开销,会看到友元函数和友元类的参数都是引用。所以为了保护被传入对象不被修改,会加以const修饰符。
2、友元类
若一个类为另一个类的友元,则此类的所有成员都能访问对方类的私有成员。
这种情况很像继承关系中的protect
声明语法:将友元类名在另一个类中使用friend修饰说明。
class A {
friend class B; //B是A的友元,A不一定是B的友元
public:
void display() {cout << x << endl;}
private:
int x;
};
class B { //组合类
public:
void set(int i);
void display();
private:
A a; //部件类
};
void B::set(int i) {a.x=i;}
void B::display() {a.display();}
类的友元关系是单向的
如果声明B类是A类的友元,B类的成员函数就可以访问A类的私有和保护数据,但A类的成员函数却不能访问B类的私有、保护数据。
5.5 共享数据的保护 const
对于既需要共享、又需要防止改变的数据应该声明为常类型(用const进行修饰)或者常引用
对于不改变对象状态的成员函数应该声明为常函数。
1、常对象
必须进行初始化,不能被更新。
const 类名 对象名
2、常引用
被引用的对象不能被更新。
const 类型说明符 &引用名
3、常函数
常成员函数不更新对象的数据成员。
类型说明符 函数名(参数表)const;
这里,const是函数类型的一个组成部分,因此在实现部分也要带const关键字。
const关键字可以被用于参与对重载函数的区分
l 通过常对象只能调用它的常成员函数。所以一般对于不改变对象状态的函数都声明为常函数
4、常成员
用const修饰的对象成员
常数据成员和常函数成员
常成员是不能被放在构造函数函数体中赋值的,它必须在初始化列表中初始化
5、常数组
数组元素不能被更新
类型说明符 const 数组名[大小]...
6、常指针
指向常量的指针
5.6 多文件结构和编译预处理命令
1、C++程序一般组织结构
一个工程可以分为多个源文件
- 类声明文件(.h文件)
- 类实现文件(.cpp文件)
- 类使用文件(main()入口函数所在的.cpp文件)
- 利用工程(vs)来结合各个文件(编译单元)
过程:编译-连接-生成可执行文件
2、外部函数和外部变量
一个C++工程中,如果main.cpp需要调用在functions.cpp中定义的函数,须在头文件common.h中加入函数的声明,在main.cpp和functions.cpp中都需要加入
#include<common.h>
如果需要多个文件共享的全局变量,则在头文件common.h中用extern关键字声明变量(但不能初始化),在需要用到该变量的文件中定义该变量
extern int g_flag; //common.h
int g_flag = 0; // main.cpp
外部变量
一个变量除了在定义它的源文件中可以被使用,还可以被其他文件使用
文件作用域下的变量默认都是外部变量,但如果其他文件需要使用它,需要用extern关键字加以声明。
外部函数
在所有类之外声明的函数(非成员函数),都是据哟文件作用域的
文件作用域下的函数默认都是外部函数,但如果其他文件需要使用它,只需要进行声明,extern关键字修饰是可选的。
将变量和单元限制在编译单元内
使用匿名命名空间的函数和变量,是不会暴露给其他编译单元的。
namespace { /* ....... */ }
结合以下代码了解外部函数的使用:在B中使用A的变量以及函数
//linkA.cpp
#include <iostream>
#include "common.h"
using namespace std;
int flag = 22;
void func0( int n){
cout<< n<<"from linkA" <<endl;
}
----------------------------
//common.h
#ifndef COMMON_H
#define COMMON_H
void func0( int n);
extern int flag;
#endif
----------------------------
//linkB.cpp
#include <iostream>
#include <string>
#include "common.h"
using namespace std;
void func1( int n){
cout<< n;
}
int main(){
func1(flag);
func0(10);
return 0;
}
----------------------------
//输出
2210from linkA
注意 这里不能用常规的编译方法,要手动编译链接:
g++ -c linkA.cpp #编译
g++ -c linkB.cpp #编译
g++ -o linkB linkB.o linkA.o #连接生成 linkB.exe
./linkB.exe #运行程序
如果按照我们平时的默认方式,第三行其实是 g++ -o linkB linkB.o
这时候报错:
main.o: In function `main':
main.c:(.text+0x7): undefined reference to `test'
collect2: ld returned 1 exit status
说明是连接时缺失相关目标文件
其实一行代码解决问题:g++ -o linkB linkB.cpp linkA.cpp #连接生成 linkB.exe
3、标准C++库
逻辑上分为6种类型
- 输入/输出类
- 容器类与抽象数据类型
- 存储管理类
- 算法
- 错误处理
- 运行环境支持
4、编译预处理命令
#include包含指令
头文件中<>与“ ”的区别
1、标准库的文件用<>
,不用写拓展名。非标准库的头文件用“”
,一般以h、hpp、hxx结束。
2、检索路径不一样,前者只搜索includePath下,后者首先检索当前目录然后才是标准搜索的includePath下。
#include <iostream>
#include "seal_statistic.h"
#define 宏定义指令
定义符号常量,很多时候被const代替了
定义带参数的宏,已被内联函数取代
undef 删除定义的宏,使之不再起作用
条件编译指令
#if 常量表达式1
程序正文1 //当“ 常量表达式1”非零时编译
#elif 常量表达式2
程序正文2 //当“ 常量表达式2”非零时编译
#else
程序正文3 //其他情况下编译
#endif
#ifndef 标识符
#defin 标识符
程序段
#endif