文件与流
也就是iostream
标准库,里面提供了常用的cin
和cout
方法,分别用于 标准输入读取流 和 标准输出写入流, 也就是对流操作要用这两个方法.
这两个流基本上是用在终端上的,与文件交互则需要另一个标准库fstream
, 这里面定义了三种新的数据类型:
数据类型 |
描述 |
ofstream |
表示输出文件流,用于创建文件并向文件中写入信息 |
ifstream |
表示输入文件流,用于从文件中读取信息 |
fstream |
表示文件流,既可以作为输入文件流,也可以作为输出文件流 |
因此,C++中要处理文件就必须在代码中包含和这两个头文件。
打开文件
ofstream
和 fstream
都可以打开文件进行写操作,如果只需要打开文件并执行读操作,则使用 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);
|
这样打开的文件就既可以读也可以写
关闭文件
在打开文件之后一定要记得把文件对象关闭
读取文件
我们使用流提取运算符(>>
)来从文件流对象中读取信息,就像使用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文件
文件位置指针
istream
和 ofstream
都提供了重新定位文件指针位置的成员函数,像是
1 2
| basic_istream& seekg( pos_type pos );
|
以及
1 2
| 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( ExceptionName e2 ) { }catch( ExceptionName eN ) { }
|
抛出异常(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
Case 2
由于代码在抛出异常时的类型是字符串,我们在捕获该异常的时候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::vector 和 std::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
运算符分配的内存。
new
与delete
的使用方法
1 2
| int* num = new int; delete num;
|
定义一个指向 double
类型的指针,然后请求内存,该内存在执行时被分配。
1 2
| double* pValue = NULL; 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; pvalue = new double;
*pvalue = 29494.99; cout << "pvalue= " << *pvalue << endl;
delete pvalue;
return 0; }
|
运行结果
数组的动态内存分配
1 2 3
| char* pvalue = NULL; pvalue = new char[20]; delete [] pvalue;
|
多维数组的动态内存分配
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| int **array
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; }
|
运行结果
可以使用 using namespace
,这样在使用命名空间时就可以不用在前面加上命名空间的名称。这个指令会告诉编译器,后续的代码将使用指定的命名空间中的名称。
using
指令也可以用来指定命名空间中的特定项目。例如,我只打算使用 std
命名空间中的 cout
和 cin
部分:
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; Stack<string> stringStack; intStack.push(7); cout << intStack.top() <<endl; 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 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 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; }
|
运行结果
当 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; }
|
运行结果
可以看到,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
|
信号中断
信号是由操作系统传给进程的中断,会提早终止一个程序。在 UNIX
、LINUX
、Mac OS X
或 Windows
系统上,Ctrl+C
就是一种中断。
下表所列信号可以在程序中捕获,并可以基于信号采取适当的动作。这些信号是定义在 C++ 头文件 <csignal>
中。
信号 |
描述 |
SIGABRT |
程序的异常终止,如调用 abort 。 |
SIGFPE |
错误的算术运算,比如除以零或导致溢出的操作。 |
SIGILL |
检测非法指令。 |
SIGINT |
接收到交互注意信号。 |
SIGSEGV |
非法访问内存。 |
SIGTERM |
发送到程序的终止请求。 |
C++ 信号处理库提供了 signal
函数,用来捕获突发事件。以下是 signal()
函数的语法:
1 2 3
|
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>
#include <windows.h>
#include <cstdlib> using namespace std; void signalHandler( int signum ) { cout << "接收到中断信号 (" << signum << ")" << endl; exit(signum); } int main () { signal(SIGINT, signalHandler); while(1){ cout << "即将休眠...." << endl; _sleep(1); } return 0; }
|
现在,按 Ctrl+C
来中断程序,会看到程序捕获信号,程序打印如下内容并退出:
1 2 3 4
| 即将休眠.... 即将休眠.... 即将休眠.... 接收到中断信号 (2)
|
可以使用函数 raise()
生成信号,语法如下:
1 2
| 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 <cstdlib> using namespace std; void signalHandler( int signum ) { cout << "接收到中断信号 (" << signum << ")" << endl; exit(signum); } int main () { int i = 0; signal(SIGINT, signalHandler); while(++i){ cout << "即将休眠...." << endl; if( i == 3 ){ raise( SIGINT); } Sleep(1); } return 0; }
|
运行结果
1 2 3 4
| 即将休眠.... 即将休眠.... 即将休眠.... 接收到中断信号 (2)
|
Ref