文件与流

也就是iostream标准库,里面提供了常用的cincout方法,分别用于 标准输入读取流 和 标准输出写入流, 也就是对流操作要用这两个方法.

这两个流基本上是用在终端上的,与文件交互则需要另一个标准库fstream, 这里面定义了三种新的数据类型:

数据类型 描述
ofstream 表示输出文件流,用于创建文件并向文件中写入信息
ifstream 表示输入文件流,用于从文件中读取信息
fstream 表示文件流,既可以作为输入文件流,也可以作为输出文件流

因此,C++中要处理文件就必须在代码中包含这两个头文件。

打开文件

ofstreamfstream 都可以打开文件进行写操作,如果只需要打开文件并执行读操作,则使用 ifstream 对象。

以下是open函数的使用方法

1
void open(const char* filename, ios::openmode mode);

其中,filename参数表示要打开的文件名,mode参数表示打开文件的模式,可以是以下值之一:

模式标志 描述
ios::app 追加模式,所有写入都追加到文件末尾
ios::ate 文件打开后会直接定位到文件末尾
ios::in 打开文件用于读取操作
ios::out 打开文件用于写入操作
ios::trunc 如果文件已存在,则截断文件,即删除文件内容(把文件长度设置为0)

模式可以结合使用,例如

1
2
ifstream afile;
afile.open("file.dat", ios::in | ios::out);

这样打开的文件就既可以读也可以写

关闭文件

1
void close();

在打开文件之后一定要记得把文件对象关闭

读取文件

我们使用流提取运算符(>>)来从文件流对象中读取信息,就像使用cin从键盘输入信息一样

写入文件

我们使用流插入运算符(<<)来从文件流对象中读取信息,就像使用cout从键盘输入信息一样

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
#include <fstream>
#include <iostream>
using namespace std;

int main ()
{
char data[100];

// 以写模式打开文件
ofstream outfile;
outfile.open("afile.dat");
// 显示提示并从标准输入流获取键盘的输入
cout << "写入文件" << endl;
cout << "键入你的名称: ";
cin.getline(data, 100);
// 向文件写入用户输入的数据
outfile << data << endl;
// 上一步操作执行完之后开始写入其他信息
cout << "输入你的年龄: ";
cin >> data;
cin.ignore();
// 再次向文件写入用户输入的数据
outfile << data << endl;
// 关闭打开的文件
outfile.close();
// 以读模式打开文件
ifstream infile;
infile.open("afile.dat");
// 用流运算提取数据
cout << "对文件执行读操作" << endl;
infile >> data;
// 在屏幕上写入数据
cout << data << endl;
// 再次从文件读取数据,并显示它
infile >> data;
cout << data << endl;
// 关闭打开的文件
infile.close();

return 0;
}

代码运行的结果如下

1
2
3
4
5
6
写入文件
键入你的名称: F10atingHeart
输入你的年龄: 23
对文件执行读操作
F10atingHeart
23

同时目录下会生成afile.dat文件

文件位置指针

istreamofstream 都提供了重新定位文件指针位置的成员函数,像是

1
2
/// @param pos 设置输入位置指示器到的绝对位置。
basic_istream& seekg( pos_type pos );

以及

1
2
/// @param pos 	要设置输出位置指示器到的绝对位置。
basic_ostream& seekp( pos_type pos );

异常处理

C++中提供了有关异常处理的机制,也就是出现异常是转移程序的控制权.

  • throw 当问题出现时,程序会通过这个抛出一个异常.
  • catch 被用在可能会出现问题的地方,用于捕获抛出的异常
  • try 用于标记可能出现异常的代码块,与catch一同使用

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
try
{
// 保护代码
}catch( ExceptionName e1 )
{
// catch 块
}catch( ExceptionName e2 )
{
// catch 块
}catch( ExceptionName eN )
{
// catch 块
}

抛出异常(throw)

可以使用 throw 语句在代码块中的任何地方抛出异常。throw 语句的操作数可以是任意的表达式,表达式的结果的类型决定了抛出的异常的类型。

1
2
3
4
5
6
7
8
9
// 抛出零除异常的示例
double division(int a, int b)
{
if( b == 0 )
{
throw "Division by zero condition!";
}
return (a/b);
}

捕获异常(catch)

catch 块跟在 try 块后面,用于捕获异常。可以指定想要捕捉的异常类型,这是由 catch 关键字后的括号内的异常声明决定的。让 catch 块能够处理 try 块抛出的任何类型的异常,则必须在异常声明的括号内使用省略号 ...

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;

double division(int a, int b)
{
// 在这里抛出零除异常
if( b == 0 )
{
throw "出现了零除异常!";
}
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;
}

代码的运行结果如下:

Case 1

1
2
3
x=5
y=10
result=0

Case 2

1
2
3
x=50
y=0
出现了零除异常!

由于代码在抛出异常时的类型是字符串,我们在捕获该异常的时候catch的参数就要相应的使用const char*

标准异常

C++在 <Exception> 标准库中定义了一些列标准的异常,结构如下:

标准异常定义

异常 描述
std::exception 该异常是所有标准 C++ 异常的父类。
std::bad_alloc 该异常可以通过 new 抛出。
std::bad_cast 该异常可以通过 dynamic_cast 抛出。
std::bad_exception 这在处理 C++ 程序中无法预期的异常时非常有用。
std::bad_typeid 该异常可以通过 typeid 抛出。
std::logic_error 理论上可以通过读取代码来检测到的异常。
std::domain_error 当使用了一个无效的数学域时,会抛出该异常。
std::invalid_argument 当使用了无效的参数时,会抛出该异常。
std::length_error 当创建了太长的 std::string 时,会抛出该异常。
std::out_of_range 该异常可以通过方法抛出,例如 std::vectorstd::bitset<>::operator[]()
std::runtime_error 理论上不可以通过读取代码来检测到的异常。
std::overflow_error 当发生数学上溢时,会抛出该异常。
std::range_error 当尝试存储超出范围的值时,会抛出该异常。
std::underflow_error 当发生数学下溢时,会抛出该异常。

不可通过读取代码 即 Runtime(运行时)

可以通过继承重载Exception类来创建自定义异常类

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
#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)
{
//其他的错误
}
}

在这里,what() 是异常类提供的一个公共方法,它已被所有子异常类重载。这将返回异常产生的原因。

运行结果

1
2
MyException caught
C++ Exception

动态内存分配

C++程序中的内存主要是以下的形式:

  • :在函数内部声明的所有变量都将占用栈内存。
  • :这是程序中未使用的内存,在程序运行时可用于动态分配内存。

可以使用特殊的运算符为给定类型的变量在运行时分配堆内的内存,这会返回所分配的空间地址。这种运算符即 new 运算符。如果不再需要动态分配的内存空间,可以使用 delete 运算符,删除之前由 new 运算符分配的内存。

newdelete的使用方法

1
2
int* num = new int;
delete num;

定义一个指向 double 类型的指针,然后请求内存,该内存在执行时被分配

1
2
double* pValue = NULL;  // 初始化为NULL的指针(其实nullptr更常用)
pValue = new double; // 请求内存并生成对象

如果自由存储区(Heap)已被用完,可能无法成功分配内存。所以建议检查 new 运算符是否返回 NULL 指针,并采取以下适当的操作:

1
2
3
4
5
6
7
double* pvalue  = NULL;
if( !(pvalue = new double ))
{
// 这里甚至还能再抛出个自定义异常
cout << "Error: out of memory." <<endl;
exit(1);
}

C语言中我们常用malloc()分配内存, 但是到了C++,还是推荐使用 new 来对内存进行分配,这样做的优势是在分配内存的同时也会创建内存对象,对于内存的管理也会更安全.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
using namespace std;

int main ()
{
double* pvalue = NULL; // 初始化为 null 的指针
pvalue = new double; // 为变量请求内存

*pvalue = 29494.99; // 在分配的地址存储值
cout << "pvalue= " << *pvalue << endl;

delete pvalue; // 释放内存

return 0;
}

运行结果

1
pvalue= 29495

数组的动态内存分配

1
2
3
char* pvalue  = NULL;   // 初始化为 null 的指针
pvalue = new char[20]; // 为变量请求内存
delete [] pvalue; // 删除 pvalue 所指向的数组

多维数组的动态内存分配

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int **array
// 假定数组第一维长度为 m, 第二维长度为 n
// 动态分配空间
array = new int *[m];
for( int i=0; i<m; i++ )
{
array[i] = new int [n] ;
}
//释放
for( int i=0; i<m; i++ )
{
delete [] arrary[i];
}
delete [] array;

本质上来说,多维数组的地址是一个地址数组存放了一系列数组的地址,就形成了多维数组

对于对象的内存分配其实与基础的数据类型差不多

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
using namespace std;

class Box
{
public:
Box() {
cout << "调用构造函数!" <<endl;
}
~Box() {
cout << "调用析构函数!" <<endl;
}
};

int main( )
{
cout << "开始对Box进行实例化" << endl;
Box* myBoxArray = new Box[4];
cout << "开始删除Box实例" << endl;
delete [] myBoxArray; // 删除数组
return 0;
}

在面向对象的概念中,实例化一个对象,执行构造函数的过程其实就是给这个对象分配一块内存地址。而删除对象,执行析构函数就是释放这个对象的内存.

运行结果

1
2
3
4
5
6
7
8
9
10
开始对Box进行实例化
调用构造函数!
调用构造函数!
调用构造函数!
调用构造函数!
开始删除Box实例
调用析构函数!
调用析构函数!
调用析构函数!
调用析构函数!

命名空间

命名空间作为附加信息来区分不同库中相同名称的函数、类、变量等。使用了命名空间即定义了上下文。本质上,命名空间就是定义了一个范围。

命名空间的定义使用关键字 namespace,后跟命名空间的名称,如下所示:

1
2
3
4
5
namespace myspace {
// 代码声明
int variable;
void function();
}

要调用命名空间中声明的变量与函数,就要加上命名空间前缀

1
2
myspace::variable = 10;
myspace::function();

示例

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>
using namespace std;

// 第一个命名空间
namespace first_space{
void func(){
cout << "在第一个命名空间中" << endl;
}
}
// 第二个命名空间
namespace second_space{
void func(){
cout << "在第二个命名空间中" << endl;
}
}
int main ()
{

// 调用第一个命名空间中的函数
first_space::func();

// 调用第二个命名空间中的函数
second_space::func();

return 0;
}

运行结果

1
2
在第一个命名空间中
在第二个命名空间中

可以使用 using namespace,这样在使用命名空间时就可以不用在前面加上命名空间的名称。这个指令会告诉编译器,后续的代码将使用指定的命名空间中的名称。

using 指令也可以用来指定命名空间中的特定项目。例如,我只打算使用 std 命名空间中的 coutcin 部分:

1
2
using std::cout;
using std::cin;

命名空间可以嵌套。可以通过使用 :: 运算符来访问嵌套的命名空间中的成员。

模板

模板是泛型编程的基础,泛型编程即以一种独立于任何特定类型的方式编写代码。模板是创建泛型类或函数的蓝图或公式。

下面的代码展示了模板的定义与使用:

函数模板

1
2
3
4
template <class type> ret-type func-name(parameter list)
{
// 函数的主体
}

类模板

1
2
3
template <class type> class class-name {
// ...
}

下面是一个定义stack的例子

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
64
65
66
67
68
69
70
#include <iostream>
#include <vector>
#include <cstdlib>
#include <string>
#include <stdexcept>

using namespace std;

template <class T>
class Stack {
private:
vector<T> elems; // 元素

public:
void push(T const&); // 入栈
void pop(); // 出栈
T top() const; // 返回栈顶元素
bool empty() const{ // 如果为空则返回真。
return elems.empty();
}
};

template <class T>
void Stack<T>::push (T const& elem)
{
// 追加传入元素的副本
elems.push_back(elem);
}

template <class T>
void Stack<T>::pop ()
{
if (elems.empty()) {
throw out_of_range("Stack<>::pop(): 空栈");
}
// 删除最后一个元素
elems.pop_back();
}

template <class T>
T Stack<T>::top () const
{
if (elems.empty()) {
throw out_of_range("Stack<>::top(): 空栈");
}
// 返回最后一个元素的副本
return elems.back();
}

int main()
{
try {
Stack<int> intStack; // int 类型的栈
Stack<string> stringStack; // string 类型的栈

// 操作 int 类型的栈
intStack.push(7);
cout << intStack.top() <<endl;

// 操作 string 类型的栈
stringStack.push("He110");
cout << stringStack.top() << std::endl;
stringStack.pop();
stringStack.pop();
}
catch (exception const& ex) {
cerr << "Exception: " << ex.what() <<endl;
return -1;
}
}

运行结果

1
2
3
7
He110
Exception: Stack<>::pop(): 空栈

预处理器

预处理器是一些指令,指示编译器在实际编译之前所需完成的预处理。#include就是一种预处理

define 预处理指令用于创建符号常量。该符号常量通常称为

1
#define PI 3.14159

可以定义带参数的宏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
using namespace std;

#define MIN(a,b) (a<b ? a : b)

int main ()
{
int i, j;
i = 100;
j = 30;
cout <<"较小的值为:" << MIN(i, j) << endl;

return 0;
}

运行结果

1
较小的值为:30

条件编译

有几个指令可以用来有选择地对部分程序源代码进行编译。这个过程被称为条件编译。

1
2
3
#ifdef NULL
#define NULL 0
#endif

###预处理运算符

C++ANSI/ISO C 中都是可用的。# 运算符会把 replacement-text 令牌转换为用引号引起来的字符串。

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>
using namespace std;

#define MKSTR( x ) #x

int main ()
{
cout << MKSTR(HELLO C++) << endl;

return 0;
}

运行结果

1
HELLO C++

CONCAT 出现在程序中时,它的参数会被连接起来,并用来取代宏。例如,程序中 CONCAT(HELLO, C++) 会被替换为 "HELLO C++"

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>
using namespace std;

#define concat(a, b) a ## b
int main()
{
int xy = 100;

cout << concat(x, y);
return 0;
}

运行结果

1
100

可以看到,concat 宏被替换为变量 xy

其他的预定义宏

描述
__LINE__ 这会在程序编译时包含当前行号。
__FILE__ 这会在程序编译时包含当前文件名。
__DATE__ 这会包含一个形式为 month/day/year 的字符串,它表示把源文件转换为目标代码的日期。
__TIME__ 这会包含一个形式为 hour:minute:second 的字符串,它表示程序被编译的时间。

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
using namespace std;

int main ()
{
cout << "Value of __LINE__ : " << __LINE__ << endl;
cout << "Value of __FILE__ : " << __FILE__ << endl;
cout << "Value of __DATE__ : " << __DATE__ << endl;
cout << "Value of __TIME__ : " << __TIME__ << endl;

return 0;
}

运行结果

1
2
3
4
Value of __LINE__ : 6
Value of __FILE__ : D:\Hexo\MemeryPalace\test.cpp
Value of __DATE__ : Jul 27 2024
Value of __TIME__ : 17:11:44

信号中断

信号是由操作系统传给进程的中断,会提早终止一个程序。在 UNIXLINUXMac OS XWindows 系统上,Ctrl+C就是一种中断。

下表所列信号可以在程序中捕获,并可以基于信号采取适当的动作。这些信号是定义在 C++ 头文件 <csignal> 中。

信号 描述
SIGABRT 程序的异常终止,如调用 abort
SIGFPE 错误的算术运算,比如除以零或导致溢出的操作。
SIGILL 检测非法指令。
SIGINT 接收到交互注意信号。
SIGSEGV 非法访问内存。
SIGTERM 发送到程序的终止请求。

C++ 信号处理库提供了 signal 函数,用来捕获突发事件。以下是 signal() 函数的语法:

1
2
3
/// @param sig 信号的编号
/// @param func 指向信号处理函数的指针。
void (*signal (int sig, void (*func)(int)))(int);

我们可以用以下代码来测试中断

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
#include <iostream>
#include <csignal>
// Windows环境下要包含的头文件是这个
#include <windows.h>
// #include <unistd.h>
#include <cstdlib>

using namespace std;

void signalHandler( int signum )
{
cout << "接收到中断信号 (" << signum << ")" << endl;

// 清理并关闭
// 终止程序

exit(signum);

}

int main ()
{
// 注册信号 SIGINT 和信号处理程序
signal(SIGINT, signalHandler);

while(1){
cout << "即将休眠...." << endl;
// 不同环境下要调用的函数也不一样
_sleep(1);
}

return 0;
}

现在,按 Ctrl+C 来中断程序,会看到程序捕获信号,程序打印如下内容并退出:

1
2
3
4
即将休眠....
即将休眠....
即将休眠....
接收到中断信号 (2)

可以使用函数 raise() 生成信号,语法如下:

1
2
/// @param sig 信号的编号,包括SIGINT、SIGABRT、SIGFPE、SIGILL、SIGSEGV、SIGTERM、SIGHUP。
int raise (signal sig);

可以用这段代码来验证一下

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
#include <iostream>
#include <csignal>
#include <Windows.h>
// #include <unistd.h>
#include <cstdlib>

using namespace std;

void signalHandler( int signum )
{
cout << "接收到中断信号 (" << signum << ")" << endl;

// 清理并关闭
// 终止程序

exit(signum);
}

int main ()
{
int i = 0;
// 注册信号 SIGINT 和信号处理程序
signal(SIGINT, signalHandler);

while(++i){
cout << "即将休眠...." << endl;
if( i == 3 ){
raise( SIGINT);
}
Sleep(1);
}

return 0;
}

运行结果

1
2
3
4
即将休眠....
即将休眠....
即将休眠....
接收到中断信号 (2)

Ref