前言:
随着前面几篇的文章我不会C++,没问题,跟凯哥一起学(五),相信大家大概知道c++怎么运行了,大概可以看得懂一些小的程序了,接下来我们继续:
数据抽象
数据抽象是指对外只提供基本信息并且隐藏他们的背景细节,即只呈现程序中所需的信息而没有提供细节。 数据抽象是一种编程(和设计)技术,依赖于接口和实现的分离。
让我们举一个现实生活中的例子。一个电视,你可以打开和关闭,改变频道,调整音量,并添加外部组件,比如 扬声器,录像机,以及 DVD 播放器。但是你不知道它的内部细节,也就是说,你不知道它如何通过无线技术或者 通过电缆接收信号,如何转化信号,以及最后将信号显示在屏幕上。
因此,我们可以说电视机实现了外部接口与内部实现的清晰分离,你可以无需知道它的内部具体实现,就可以通
过其外部接口比如电源按钮,更换频道,音量控制。
现在,如果我们谈论的是 C++ 编程, C++ 类提供了大量数据抽象的例子。他们提供了大量的针对外部世界的公 有函数来满足对象的功能或者操作对象数据,即外部函数不知道类在内部是如何实现的。
例如,你的程序可以在不知道函数实际使用什么算法来对给定的值进行排序的情况下调用 sort() 函数。事实 上,排序功能的底层实现可以在不同版本之间变化,只要接口保持不变,你的函数调用将仍然起作用。
在 C++ 中,我们使用类来定义自己的抽象数据类型( ADT )。您可以使用类 ostream 的 cout 对象对流数据进行 标准输出如下:
#include <iostream>
using namespace std;
int main( ) {
cout << "Hello C++" <<endl;
return 0; }
在这里,你不需要了解 cout 如何在用户的屏幕上显示文本。你只需要知道的公共接口和的 cout 底层实现是可 以自由改变的。
访问标号实施抽象
在 C++ 中,我们使用访问标号定义抽象接口类。一个类可以包含零个或多个访问标签:
• 成员定义了一个公有标号,程序的所有部分都可以访问这个公共标号。类型的数据抽象视图由其公有成员定 义。
• 使用类的代码不可以访问带有私有标号的成员。对于使用类的代码,私有部分隐藏了类的实现细节。
一个访问标号可以出现的次数通常是没有限制的。每个访问标号指定了随后的成员定义的访问级别。这个指定的
访问级别持续有效,知道遇到下一个访问标号或看到类定义提的右花括号为止。
数据抽象的好处
数据抽象提供了两个重要的优势:
• 避免内部出现无意的,可能破坏对象状态的用户级错误。
• 随着时间的推移类实现可能会根据需求或缺陷报告来做出修改,但是这种修改无需改变用户级代码。
通过只在类的私有部分定义数据成员,类作者可以自由的对数据进行更改。如果实现更改,只需要检查类的代码
看看这个改变可能造成什么影响。如果数据是公开的,那么任何可以直接访问旧的数据成员的函数都可能遭到破
坏。
数据抽象举例
任何一个用公有和私有成员实现一个类的 C++ 程序都是数据抽象的一个例子。考虑下面的例子:
#include <iostream>
using namespace std;
class Adder{
public:
// constructor
Adder(int i = 0)
{
total = i; }
// interface to outside world
void addNum(int number)
{
total += number;
}
// interface to outside world
int getTotal()
{
return total;
};
private:
// hidden data from outside world
int total; };
int main( ) {
Adder a;
a.addNum(10);
a.addNum(20);
a.addNum(30);
cout << "Total " << a.getTotal() <<endl;
return 0; }
编译和执行上面的代码时,它产生以下结果:
Total 60
上面的类实现了把数字加起来,并且返回总和。公有成员 addNum 和 getTotal 是对外的接口,用户需要知道他 们才能使用的类。私有成员 total 是用户不需要知道的,但是它是为保证程序正常运行类必要的。
设计策略
抽象使代码分离成接口和实现。所以在设计你的组件的时候,你必须保持接口独立于实现,因此,你才能做到在
改变底层实现时,界面将保持不变。
在这种情况下,无论任何程序使用这些接口,他们不会受到影响,只需要重新编译最新的实现。
所有的 C++ 程序是由以下两个基本要素组成:
• 程序语句(代码):这是程序执行行为的一部分,他们被称为函数。 • 程序数据:数据是受程序函数影响的信息。
封装是一个面向对象编程的概念,它将数据和操作数据的函数结合在一起,并使其免受外部干扰和误用。数据封 装是数据隐藏的重要面向对象编程概念。
数据封装是一种将数据和使用数据的函数结合在一起的机制;数据抽象是一种只将接口公开并且向用户隐藏实现
细节的机制。
C++ 支持封装的属性和通过创建用户定义类型实现的数据隐藏,这称为类。我们已经研究过,一个类可以包含私 有、保护和公有成员。默认情况下,所有定义在一个类中的成员是私有的。例如:
class Box {
public:
double getVolume(void)
{
return length * breadth * height;
}
private:
double length; // Length of a box
double breadth; // Breadth of a box
double height; // Height of a box
};
变量 length 、breadth 和 height 都是私有的。这意味着只有 box 类的其他成员可以访问它们,而程序的任何 其它的部分不能访问它们。这是一个封装的实现方式。
要想使类的某个部分成为共有的(即访问您的程序的其他部分),你必须在 public 关键字后声明它们。公有说明 符后定义的所有变量或函数可以被程序中的其它函数访问。
使一个类成为其它类的友元类就可以获得实现细节,降低封装。这个思想就是获得尽可能多的每个类的对其它类
隐藏的细节。
抽象类样例
考虑下面的例子,父类为基类提供了一个接口来实现函数 getArea() :
#include <iostream>
using namespace std;
// Base class
class Shape
{
public:
// pure virtual function providing interface framework.
virtual int getArea() = 0;
void setWidth(int w)
{
width = w; }
void setHeight(int h)
{
height = h;
} protected:
int width;
int height;
};
// Derived classes
class Rectangle: public Shape
{
public:
int getArea()
{
return (width * height);
} };
class Triangle: public Shape
{
public:
int getArea()
{
return (width * height)/2;
} };
int main(void)
{
Rectangle Rect;
Triangle Tri;
Rect.setWidth(5);
Rect.setHeight(7);
// Print the area of the object.
cout << "Total Rectangle area: " << Rect.getArea() << endl;
Tri.setWidth(5);
Tri.setHeight(7);
// Print the area of the object.
cout << "Total Triangle area: " << Tri.getArea() << endl;
return 0; }
上面的代码编译和执行后,将产生以下结果:
Total Rectangle area: 35
Total Triangle area: 17
设计策略
一个面向对象的系统可能使用一个抽象基类提供一个普遍的和适合所有的外部应用程序的标准化接口。然后, 通 过继承的抽象基类, 形成派生类。
功能(即,公共函数),即由外部应用程序提供的,作为抽象基类里面的纯虚函数。 那些纯虚函数是由派生类实现 的,派生类对应于应用的特定类型。
即使在系统已经定义之后, 这种架构还允许添加新的应用程序到系统中。
好了讲了这么久的基础,我们现在可以接触到功能部分了,这部分还是比较有意思的,最起码你尝试些demo的时候会有那么稍稍的成就感。
文件和流
到目前为止,我们一直在使用 iostream 标准库,它提供了 cin 及 cout方法,分别用于读取标准输入以及写入标 准输出。
本教程将教你如何从文件中读取和写入。这需要另一个称为 fstream 的标准 C++ 库,它定义了三个新的数据类 型:
数据类型
1️⃣ofstream:这个数据类型表示输出文件流,用于创建文件以及将信息写入文件。
2️⃣ifstream:这个数据类型表示输入文件流,用于从文件读取信息。
3️⃣fstream:这个数据类型通常表示该文件流,兼备有 ofstream 和 ifstream 功能,这意味着它可以创建文件,编写文件,以及读文件。
使用 C++ 执行文件处理时,头文件 <iostream> 和 <fstream> 必须包含在你的 C++ 源文件里面。
打开文件
需要对一个文件进行读写操作时必须先打开该文件。 ofstream 或 fstream 对象可以用来打开一个文件并且写 入; ifstream 对象用于以读入打开一个文件。
下面是 open() 函数的标准语法,它是 fstream,ifstream,ofstream 对象的成员。
void open(const char *filename, ios::openmode mode) ;
在这里,第一个参数指定文件的名称和打开位置,open()成员函数的第二个参数定义了文件应该以哪种模式被打 开。
模式标志
1️⃣ios::app 追加模式。所有输出文件附加到结尾。
2️⃣ios::ate 为输出打开一个文件并将读/写控制移动到文件的末尾。
3️⃣ios::in 打开一个文件去读。
4️⃣ios::out 打开一个文件去写。
5️⃣ios::trunc 如果文件已经存在,打开该文件前文件中的内容将被清空。 。
您可以通过逻辑运算将两个或更多的这些值组合到一起。例如, 如果你想以写方式打开一个文件, 并且想在其打 开之前清空内容,以防它已经存在的话,使用一下语法规则:
ofstream outfile;
outfile.open("file.dat", ios::out | ios::trunc );
同样,你可以以读入和写入目的打开一个文件如下:
fstream afile;
afile.open("file.dat", ios::out | ios::in );
关闭文件
一个 C++ 程序终止时它会自动关闭所有流,释放所有分配的内存并关闭所有打开的文件。但在终止之前,程序员 应该关闭所有打开的程序文件始终是一个很好的实习惯。
下面是标准的 close() 函数语法,它是一个 fstream,ifstream,以及 ofstream对象的成员。
void close();
写文件
在使用 C++ 编程时,你通过程序使用流插入操作符 (< <) 将信息写入文件,使用流插入操作符 (< <) 就像你使用 键盘输入将信息输出到屏幕上。唯一的区别是, 你使用一个 ofstream 或 fstream 对象而不是 cout。
读文件
您使用留提取符 ( >> ) 将文件中的信息读入程序就像你使用该运营商从键盘输入信息。唯一的区别是,你使用一 个 ifstream 或 fstream 对象而不是 cin 的对象。
读取与写入样例
下面是一段 C++ 程序,以读取和写入方式打开一个文件。将用户输入的信息写入文件后以 afile.dat 命名文 件。程序从文件中读取信息并输出在屏幕上:
#include <fstream>
#include <iostream>
using namespace std;
int main () {
char data[100];
// open a file in write mode.
ofstream outfile;
outfile.open("afile.dat");
cout << "Writing to the file" << endl;
cout << "Enter your name: ";
cin.getline(data, 100);
// write inputted data into the file.
outfile << data << endl;
cout << "Enter your age: ";
cin >> data;
cin.ignore();
// again write inputted data into the file.
outfile << data << endl;
// close the opened file.
outfile.close();
// open a file in read mode.
ifstream infile;
infile.open("afile.dat");
cout << "Reading from the file" << endl;
infile >> data;
// write the data at the screen.
cout << data << endl;
// again read the data from the file and display it.
infile >> data;
cout << data << endl;
// close the opened file.
infile.close();
return 0; }
当上面的代码被编译并执行,将产生如下的样本和输出:
$./a.out
Writing to the file
Enter your name: Zara
Enter your age: 9
Reading from the file
Zara
9
上面的例子利用 cin 对象额外的功能,如利用 getline()函数来从外部读取线,用 ignor()函数忽略先前读取语句 留下的额外字符。
文件位置指针
istream 和 ostream 是用来重新定位文件位置的指针成员函数。这些成员函数有 seekg (“seek get”) istrea m 和 seekp 上 ostream (“seek put”)。
seekg 和 seekp 通常的参数是一个长整型。可以指定第二个参数为寻找方向。寻找方向可以 ios: :beg (默 认)定位相对于流的开始, io: :scur 定位相对于当前位置的流或 ios::end 定位相对于流的结束。
文件位置指针是一个整数值,它指定文件的位置作为一个从文件的开始位置的字节数。 文件位置指针是一个整数值,它指定文件的位置。定位 “get” 文件位置指针的一些示例如下:
// position to the nth byte of fileObject (assumes ios::beg)
fileObject.seekg( n );
// position n bytes forward in fileObject
fileObject.seekg( n, ios::cur );
// position n bytes back from end of fileObject
fileObject.seekg( n, ios::end );
// position at end of fileObject
fileObject.seekg( 0, ios::end );
异常处理
异常是一个程序执行过程中出现的问题。C++ 异常是对程序运行过程中产生的例外情况作出的响应,比如试图除以 零。
异常提供一种方法将程序控制从一个程序的一部分转移到另一部分。C++ 异常处理是建立在三个关键词: 尝试,捕 获和抛出之上的。
• throw: 程序运行出现问题时抛出异常。这是使用一个 throw 关键字实现的。
• catch: 程序用异常处理器在你想要处理问题的地方捕获异常。catch 关键字显示异常的捕获。 • try: 一个 try 块标识一个可能会产生异常的代码块。紧随其后的是一个或多个 catch 块。
假设一个代码块将产生一个异常,结合使用 try 和 catch 关键词的方法捕获了一个异常。一个 try / catch 块 放置在可能生成一个异常的代码周围。在一个 try / catch 块里面的代码被称为保护代码, try / catch 的语法 规则如下:
try {
// protected code
}catch( ExceptionName e1 )
{
// catch block
}catch( ExceptionName e2 )
{
// catch block
}catch( ExceptionName eN )
{
// catch block
}
你可以列出多个捕捉语句捕获不同类型的异常, 以防你的 try 代码块在不同的情况下产生了不止一个异常。
抛出异常
异常可以在代码块的任何地方使用抛出语句抛出。把语句的操作数确定类型的异常,可以是任何表达式,表达式的 结果的类型决定了类型的异常抛出。
下面是一个例子, 在除以零条件发生时,抛出异常:
double division(int a, int b)
{
if( b == 0 )
{
throw "Division by zero condition!";
}
return (a/b);
}
捕获异常
try 块后的 catch 块可以捕获任何异常。您可以指定你需要捕获何种类型的异常,这是由出现在关键字 catch 后 边的括号中的异常声明确定的。
try {
// protected code
}catch( ExceptionName e )
{
// code to handle ExceptionName exception
}
上面的代码将捕获到一个 ExceptionName 类型的异常。如果您想要指定一个 catch 块可以应该处理任何在 try 代码中产生的异常,你必须将一个省略号...放在 catch 后的括号中,异常声明如下:
try {
// protected code
}catch(...)
{
// code to handle any exception
}
下面是一个例子,这个例子抛出会除零异常, 我们在 catch 块里面捕获它
#include <iostream>
using namespace std;
double division(int a, int b)
{
if( b == 0 )
{
throw "Division by zero condition!";
}
return (a/b);
}
int main () {
int x = 50;
int y = 0;
double z = 0;
try {
z = division(x, y);
cout << z << endl;
}catch (const char* msg) {
cerr << msg << endl;
}
return 0; }
因为上例中提出了一个 const char * 类型的异常, 所以捕捉这个异常时,我们必须在 catch 块中使用 r * 。如果我们编译和运行上面的代码, 这将产生以下结果:
const cha
Division by zero condition!
C++ 标准异常
C++ 在 <exception> 中提供了一系列标准的异常,我们可以用在我们的程序中。这些异常使用父-子分层结构展 示如下:
定义新异常
你可以采用继承及重写异常类来。下面是示例, 显示如何使用 std::exception 类以标准的方式实现自己的异 常:
#include <iostream>
#include <exception>
using namespace std;
struct MyException : public exception
{
const char * what () const throw ()
{
return "C++ Exception";
} };
int main() {
try
{
throw MyException();
}
catch(MyException& e)
{
std::cout << "MyException caught" << std::endl;
std::cout << e.what() << std::endl;
}
catch(std::exception& e)
{
//Other errors
}
}
这将产生如下的结果:
MyException caught
C++
这里,what() 是一个异常类提供的公共方法,所有子异常类都覆盖了该方法。这将返回一个异常的原因。
动态内存
很好地理解动态内存到底如何在 C++ 中发挥作用是成为一个好的 C++ 程序员所必需的。 C++ 程序中的内存分为 两个部分:
• 栈:所有函数内部声明的变量会占用栈的内存。
• 堆:这是程序中未使用的内存,可以在程序运行时动态地分配内存。
很多时候,你事先不知道你在一个定义的变量中需要多少内存来存储特定的信息以及在程序运行时所需内存的大
小。
你可以在运行时为指定类型的变量分配堆内存,并且可以使用 C++ 中特殊操作符返回分配空间的地址。这个操作 符被称为 new 操作符。
如果你不再需要动态分配内存了,你可以使用 delete 操作符来释放之前用 new 操作符分配的内存。
new 和 delete 操作符
下面是使用 new 操作符为任意数据类型动态地分配内存的通用的语法。
new data-type;
这里, data-type 可以是任何内置数据类型,包括数组或任何用户定义的数据类型包括类或结构。让我们先看看 内置的数据类型。例如,我们可以定义一个 double 类型的指针然后在程序执行时请求分配内存。我们可以使用 new 操作符来完成它,程序语句如下:
double* pvalue = NULL; // Pointer initialized with null
pvalue = new double; // Request memory for the variable
如果自由存储区都已经被占用,内存可能就不能被成功分配。因此检查 new 操作符是否返回空指针是一种很好的 做法,并且要采取适当的措施如下:
double* pvalue = NULL;
if( !(pvalue = new double ))
{
cout << "Error: out of memory." <<endl;
exit(1);
}
C 语言中的 malloc() 函数在C++中仍然存在,但是建议避免使用 malloc() 函数。相对于 malloc() 函数 new 操作符的主要优势是 new 操作符不仅分配内存,它还可以构造对象,而这正是 C++ 的主要目的。
在任何时候,当你觉得一个变量已经不再需要动态分配,你可以用 delete 操作符来释放它在自由存储区所占用 的内存,如下:
delete pvalue;// Release memory pointed to by pvalue
让我们把理解一下这些概念,并且用下面的例子来说明 new 和 delete 是如何起作用的:
#include <iostream>
using namespace std;
int main () {
double* pvalue = NULL; // Pointer initialized with null
pvalue = new double; // Request memory for the variable
*pvalue = 29494.99; // Store value at allocated address
cout << "Value of pvalue : " << *pvalue << endl;
delete pvalue; // free up the memory.
return 0; }
如果我们编译和运行上面的代码,这将产生以下结果:
Value of pvalue : 29495
数组的动态内存分配
考虑到你想要为字符数组分配内存,即 20 个字符的字符串。使用与上面相同的语法我们可以动态地分配内 存,如下所示。
char* pvalue = NULL; // Pointer initialized with null
pvalue = new char[20]; // Request memory for the variable
应该像这样删除我们刚刚创建的数组声明:
delete [] pvalue;// Delete array pointed to by pvalue
学习过 new 操作符的类似通用语法,你可以为一个多维数组分配内存如下:
double** pvalue = NULL; // Pointer initialized with null
pvalue 、= new double [3][4]; // Allocate memory for a 3x4 array
然而,释放多维数组内存的语法仍然同上:
delete [] pvalue;// Delete array pointed to by pvalue
对象的动态内存分配
对象与简单的数据类型并无不同。例如,考虑下面的代码,我们将使用一个对象数组来解释这个概念:
#include <iostream>
using namespace std;
class Box {
public:
Box() {
cout << "Constructor called!" <<endl;
}
~Box() {
cout << "Destructor called!" <<endl;
} };
int main( ) {
Box* myBoxArray = new Box[4];
delete [] myBoxArray; // Delete array
return 0; }
如果你为四个 Box 对象数组分配内存,一个简单的构造函数将被调用四次,同样的删除这些对象时,析构函数也 被调用相同的次数。
如果我们编译和运行上面的代码,这将产生以下结果:
Constructor called!
Constructor called!
Constructor called!
Constructor called!
Destructor called!
Destructor called!
Destructor called!
Destructor called!
命名空间
考虑一个情况,在同一个班有两个同名的人,都叫 Zara 。每当我们需要区分他们的时候,除了它们的名字我们 肯定会使用一些额外的信息,就像如果他们住在不同的区域或他们的母亲或父亲的名字,等等。
同样的情况会出现在你的 C++ 应用程序中。例如,你可能会编写一些代码,有一个名为 xyz() 的函数,在另一 个库中也有同样的函数 xyz() 。现在编译器不知道在你的代码中指定的是哪个版本的 xyz() 函数。
namespace就是用来克服这个困难的,而且作为附加信息来区分在不同的库中具有相同名称的函数,类、变量 等。使用命名空间,你可以定义名字已经定义的上下文。从本质上讲,一个命名空间定义了一个范围。
定义命名空间
一个命名空间的定义由关键字 namespace 加它的名称组成,如下所示:
namespace namespace_name {
// code declarations
}
调用任何函数或变量的命名空间启用版本,前面加上命名空间名字如下:
name::code; // code could be variable or function.
#include <iostream>
using namespace std;
// first name space
namespace first_space{
void func(){
cout << "Inside first_space" << endl;
} }
// second name space
namespace second_space{
void func(){
cout << "Inside second_space" << endl;
} }
int main ()
{
return 0; }
// Calls function from first name space.
first_space::func();
// Calls function from second name space.
second_space::func();
return 0; }
如果我们编译和运行上面的代码,这将产生以下结果:
Inside first_space
Inside second_space
using 指令
你可以通过使用 using namespace 指令来避免在头部添加命名空间。这个指令告诉编译器,随后代码要使用指定 命名空间中的名称。因此名称空间隐含下面的代码:
#include <iostream>
using namespace std;
// first name space
namespace first_space{
void func(){
cout << "Inside first_space" << endl;
} }
// second name space
namespace second_space{
void func(){
cout << "Inside second_space" << endl;
} }
using namespace first_space;
int main ()
{
// This calls function from first name space.
func();
return 0; }
如果我们编译和运行上面的代码,这将产生以下结果:
Inside first_space
using 指令也可以用来指一个名称空间中的特定的项目。例如,如果你打算只是用 std 名称空间的一部分cou t,你可以进行如下操作:
using std::cout;
后续的代码可以调用 cout 而不用在前面加上命名空间名字,但命名空间中的其他项目仍需要作如下说明:
#include <iostream>
using std::cout;
int main () {
cout << "std::endl is used with std!" << std::endl;
return 0; }
如果我们编译和运行上面的代码,这将产生以下结果:
std::endl is used with std!
using 指令引入的名字遵循正常的检测规则。这个名字相对于从 using 指令的指针到范围的结束是可见的,并且 在这个范围中指令可以被找到。定义在外部范围的有相同名字的实体是被隐藏的。
嵌套的命名空间
命名空间可以被嵌套,你可以在一个命名空间内定义另一个命名空间,如下:
namespace namespace_name1 {
// code declarations
namespace namespace_name2 {
// code declarations
}
}
在上面的语句中如果你使用的是 namespace _ name1 ,那么它将使 namespace _ name2 的元素在整个范围内可 用,如下:
#include <iostream>
using namespace std;
// first name space
namespace first_space{
void func(){
cout << "Inside first_space" << endl;
}
// second name space
namespace second_space{
void func(){
cout << "Inside second_space" << endl;
} }
}
using namespace first_space::second_space;
int main ()
{
// This calls function from second name space.
func();
return 0; }
如果我们编译和运行上面的代码,这将产生以下结果:
Inside second_space