C++ 类模板与文件输入输出详解
1. 嵌套类的类模板
在 C++ 中,一个类可以在其定义内部包含另一个嵌套类。类模板定义同样可以包含嵌套类,甚至是嵌套类模板。嵌套类模板具有独立的参数化特性,在另一个类模板内部使用时,能产生二维的类生成能力。下面以实现一个栈为例,详细介绍嵌套类的类模板。
1.1 栈的基本概念
栈是一种“后进先出”(Last In, First Out,LIFO)的存储机制,类似于自助餐厅中的一摞盘子。栈有两个基本操作:入栈(push),即在栈顶添加一个元素;出栈(pop),即移除栈顶的元素。理想情况下,栈的实现应该能够存储任意类型的对象,因此使用模板来实现栈是很自然的选择。
1.2 栈模板的初始定义
栈模板的参数是一个类型参数,用于指定栈中对象的类型。其初始模板定义如下:
template <typename T>
class Stack
{
// 栈定义的详细内容...
};
1.3 以链表实现栈
若要让栈的容量能自动增长,就不能使用固定存储来存放栈内的对象。一种可行的方法是将栈实现为链表。链表的节点可在自由存储区创建,栈只需记录栈顶节点即可。
当创建一个空栈时,链表头指针为
nullptr
,可借此判断栈是否为空。而且,只有栈对象需要访问栈内的节点对象,节点对象只是用于封装栈中存储的对象,因此栈类外部的代码无需知道
Node
类型的存在。
在
Stack
模板的每个实例中,都需要一个嵌套类来定义链表节点。由于节点必须持有一个类型为
T
(即
Stack
模板的参数类型)的对象,所以可将其定义为基于
T
的嵌套类。下面是添加了嵌套类后的
Stack
模板初始框架:
template <typename T>
class Stack
{
private:
// 嵌套类
class Node
{
public:
T* pItem {}; // 指向存储对象的指针
Node* pNext {}; // 指向下一个节点的指针
Node(T& item) : pItem {&item} {} // 从对象创建一个节点
};
// 栈类定义的其余部分...
};
Node
类被声明为私有,因此可将其所有成员设为公共的,以便
Stack
模板的成员函数能直接访问。
Node
对象仅存储一个指向类型为
T
的对象的指针,对象的所有权归
Stack
类的用户。当对象入栈时,会调用
Node
类的构造函数,构造函数的参数是一个类型为
T
的对象的引用,该对象的地址用于初始化新
Node
对象的
pItem
成员。
完整的
Stack
类模板如下:
template <typename T>
class Stack
{
private:
// 嵌套的 Node 类定义如前...
Node* pHead {}; // 指向栈顶的指针
void copy(const Stack& stack); // 复制栈的辅助函数
void freeMemory(); // 释放自由存储区内存的辅助函数
public:
Stack() = default; // 默认构造函数
Stack(const Stack& stack); // 拷贝构造函数
~Stack(); // 析构函数
Stack& operator=(const Stack& stack); // 赋值运算符
void push(T& item); // 将对象压入栈
T& pop(); // 从栈中弹出对象
bool isEmpty() {return !pHead;} // 检查栈是否为空
};
Stack
对象只需记录栈顶节点,因此只有一个类型为
Node*
的数据成员
pHead
。它包含默认构造函数、拷贝构造函数、析构函数和赋值运算符函数。析构函数很重要,因为节点是使用
new
动态创建的,其地址存储在原始指针中。
push()
和
pop()
成员函数用于在栈和外部之间转移对象,
isEmpty()
函数用于判断栈是否为空。
Stack
类的私有成员
copy()
会在拷贝构造函数和赋值运算符函数内部使用,以执行两者共有的操作,从而减少代码行数和可执行文件的大小。类似地,私有函数
freeMemory()
是一个辅助函数,供析构函数和赋值运算符函数使用。
2. 栈成员的函数模板
接下来详细介绍栈成员函数模板的定义。
2.1 copy() 成员函数模板
template <typename T>
void Stack<T>::copy(const Stack& stack)
{
if(stack.pHead)
{
pHead = new Node {*stack.pHead}; // 复制原栈的栈顶节点
Node* pOldNode {stack.pHead}; // 指向原栈的栈顶节点
Node* pNewNode {pHead}; // 指向新栈的节点
while(pOldNode = pOldNode->pNext) // 如果为 nullptr,则是最后一个节点
{
pNewNode->pNext = new Node {*pOldNode}; // 复制节点
pNewNode = pNewNode->pNext; // 移动到刚创建的节点
}
}
}
该函数将
stack
参数所代表的栈复制到当前的
Stack
对象(假设当前对象为空)。它先复制参数对象的
pHead
,然后遍历
Node
对象序列,逐个复制节点,直到复制完
pNext
成员为
nullptr
的节点。
2.2 freeMemory() 辅助函数模板
template <typename T>
void Stack<T>::freeMemory()
{
Node* pTemp {};
while(pHead)
{ // 当当前指针不为空时
pTemp = pHead->pNext; // 获取下一个节点的指针
delete pHead; // 删除当前节点
pHead = pTemp; // 将下一个节点设为当前节点
}
}
此函数用于释放当前
Stack
对象所有
Node
对象占用的堆内存。它使用一个临时指针
pTemp
来保存
Node
对象的
pNext
成员中的地址,然后删除当前节点。循环结束后,所有属于当前
Stack
对象的
Node
对象都会被删除,
pHead
会变为
nullptr
。
2.3 拷贝构造函数模板
template <typename T>
Stack<T>::Stack(const Stack& stack)
{
copy(stack);
}
拷贝构造函数通过调用
copy()
函数来复制
Stack<T>
对象。
2.4 赋值运算符模板
template <typename T>
Stack<T>& Stack<T>::operator=(const Stack& stack)
{
if (this != &stack) // 如果对象不相同
{
freeMemory(); // 释放左操作数节点的内存
copy(stack); // 将右操作数复制到左操作数
}
return *this // 返回左操作数对象
}
赋值运算符与拷贝构造函数类似,但需要额外做两件事:一是检查参与操作的对象是否相同;二是在复制右操作数之前,释放左操作数节点的内存。
2.5 析构函数模板
template <typename T>
Stack<T>::~Stack()
{
freeMemory();
}
析构函数只需调用
freeMemory()
函数来释放内存。
2.6 push() 操作模板
template <typename T>
void Stack<T>::push(T& item)
{
Node* pNode {new Node(item)}; // 创建新节点
pNode->pNext = pHead; // 指向原栈顶节点
pHead = pNode; // 使新节点成为栈顶
}
入栈操作通过将对象引用传递给
Node
构造函数来创建封装该对象的
Node
对象。新节点的
pNext
成员指向原栈顶节点,新节点的地址存储在
pHead
中,成为新的栈顶。
2.7 pop() 操作模板
template <typename T>
T& Stack<T>::pop()
{
T* pItem {pHead->pItem}; // 获取栈顶节点对象的指针
if(!pItem) // 如果栈为空
throw std::logic_error {"Stack empty"}; // 出栈操作无效,抛出异常
Node* pTemp {pHead}; // 保存栈顶节点的地址
pHead = pHead->pNext; // 使下一个节点成为栈顶
delete pTemp; // 删除原栈顶节点
return *pItem; // 返回栈顶对象
}
出栈操作可能会在栈为空时执行,由于该函数返回引用,无法通过返回值来表示错误,所以此时需要抛出异常。函数先将栈顶节点中对象的指针存储在局部变量
pItem
中,然后删除栈顶节点,将下一个节点提升为栈顶,最后返回对象的引用。
3. 栈模板的使用示例
将所有模板收集到一个头文件
Stacks.h
中,就可以使用以下代码进行测试:
// Ex16_04.cpp
// 使用嵌套类模板定义的栈
#include "Stacks.h"
#include <iostream>
#include <string>
using std::string;
int main()
{
const char* words[] {"The", "quick", "brown", "fox", "jumps"};
Stack<const char*> wordStack; // 一个存储 C 风格字符串的栈
for (size_t i {}; i < sizeof(words)/sizeof(words[0]) ; ++i)
wordStack.push(words[i]);
Stack<const char*> newStack {wordStack}; // 创建栈的副本
// 逆序显示单词
while(!newStack.isEmpty())
std::cout << newStack.pop() << " ";
std::cout << std::endl;
// 将 wordStack 中的单词逆序存入 newStack
while(!wordStack.isEmpty())
newStack.push(wordStack.pop());
// 按原顺序显示单词
while(!newStack.isEmpty())
std::cout << newStack.pop() << " ";
std::cout << std::endl;
std::cout << std::endl << "Enter a line of text:" << std::endl;
string text;
std::getline(std::cin, text); // 读取一行文本到字符串对象
Stack<const char> characters; // 一个存储字符的栈
for (size_t i {}; i < text.length(); ++i)
characters.push(text[i]); // 将字符串中的字符压入栈
std::cout << std::endl;
while(!characters.isEmpty())
std::cout << characters.pop(); // 从栈中弹出字符并输出
std::cout << std::endl;
}
以下是该程序的输出示例:
jumps fox brown quick The
The quick brown fox jumps
Enter a line of text:
Never test for errors that you don't know how to handle.
.eldnah ot woh wonk t'nod uoy taht srorre rof tset reveN
程序首先定义了一个包含五个以空字符结尾的字符串的数组,然后创建一个可以存储
const char*
对象的空栈
wordStack
,通过
for
循环将数组元素压入栈中。接着创建
wordStack
的副本
newStack
以测试拷贝构造函数。之后,通过
while
循环将
newStack
中的单词逆序输出,再将
wordStack
中的单词逆序存入
newStack
,最后按原顺序输出
newStack
中的单词。
程序的下一部分使用
getline()
函数将一行文本读入字符串对象,然后创建一个存储字符的栈
characters
。通过
for
循环将字符串中的字符逐个压入栈,最后从栈中弹出字符并逆序输出。
4. 类模板的总结
理解类模板的定义和使用方法,有助于掌握和应用标准模板库的功能。定义类模板的能力也是对 C++ 基本语言类定义功能的强大扩展。以下是类模板的一些要点总结:
- 类模板定义了一族类类型。
- 类模板的实例是编译器根据模板和代码中指定的一组模板参数生成的类定义。
- 类模板的隐式实例化源于类模板类型对象的定义。
- 类模板的显式实例化是为模板参数的给定参数集定义一个类。
- 类模板中对应类型参数的参数可以是基本类型、类类型、指针类型或引用类型。
- 非类型参数的类型可以是整数或枚举类型、指针类型或引用类型。
- 类模板的部分特化定义了一个新模板,用于原模板参数的特定受限子集。
- 类模板的完全特化定义了一个新模板,用于原模板参数的特定完整参数集。
- 类模板的友元可以是函数、类、函数模板或类模板。
- 普通类可以将类模板或函数模板声明为友元。
5. 相关练习
以下是一些相关练习,帮助你巩固所学知识:
-
练习 16 - 1
:定义一个一维稀疏数组的模板,该模板能存储任意类型的对象,且只有数组中存储的元素占用内存。模板实例可存储的元素数量应无限制。例如,可以使用以下语句定义一个包含指向
double
类型元素指针的稀疏数组:
SparseArray<double> values;
为该模板定义下标运算符,使元素值的获取和设置与普通数组类似。若某个索引位置不存在元素,下标运算符应返回对象类默认构造函数创建的对象。在
main()
函数中测试该模板,将 20 个随机的
int
类型元素值存储在索引范围为 32 到 212 的稀疏数组中(数组索引范围为 0 到 499),并输出存在元素的值及其索引位置。
-
练习 16 - 2
:定义一个链表类型的模板,该链表允许从链表末尾反向遍历,也能从链表开头正向遍历。在一个程序中应用该模板,将一些散文或诗歌中的单个单词作为
std::string
对象存储在链表中,然后按顺序和逆序每行显示五个单词。
-
练习 16 - 3
:使用链表和稀疏数组模板编写一个程序,将散文或诗歌样本中的单词存储在最多 26 个链表的稀疏数组中,每个链表包含以相同首字母开头的单词。输出这些单词,每组以给定首字母开头的单词从新行开始。(注意:在指定模板参数时,要在连续的
>
字符之间留空格,否则
>>
会被解释为右移运算符。)
-
练习 16 - 4
:为
SparseArray
模板添加一个
insert()
函数,用于在数组的最后一个元素之后添加一个元素。使用该函数和一个元素为
SparseArray
对象(存储
string
对象)的
SparseArray
实例,完成与上一个练习相同的任务。
通过这些练习,你可以更深入地理解和掌握类模板的使用,提高编程能力。
5. 文件输入输出
在 C++ 中,语言本身并未提供输入输出的功能,输入输出(I/O)能力由标准库提供,它支持与设备无关的输入输出操作。在前面的示例中,我们已经使用了这些功能的一部分,如从键盘读取数据和向屏幕输出数据。下面将详细介绍如何读写磁盘文件。
5.1 输入输出概述
在程序中,我们需要多种不同类型的 I/O 能力。例如,应用程序可能需要在数据库中存储和检索数据、创建应用程序窗口并在屏幕上显示图形、通过电话线或网络进行通信等。这些操作大多超出了 C++ 及其标准库的范围,但 C++ 提供的 I/O 功能仍然非常重要,它是一个功能丰富的标准库,除了文件 I/O 能力外,还提供了基于字符串的 I/O 数据格式化功能。
5.2 学习要点
通过学习文件输入输出,我们将掌握以下内容:
- 什么是流
- 标准流有哪些
- 二进制流与文本流的区别
- 如何创建和使用文件流
- 流操作中的错误如何记录以及如何处理
- 如何使用无格式流操作
- 如何将数值数据以二进制形式写入文件
- 如何将对象写入流和从流中读取对象
- 如何为自己的类重载插入和提取运算符
- 如何创建字符串流
在后续的学习中,我们将逐步深入了解这些内容,掌握 C++ 中的文件输入输出操作。
C++ 类模板与文件输入输出详解
6. 流的基本概念
在 C++ 的输入输出体系中,流是一个核心概念。流可以看作是数据的序列,它可以从一个源(如文件、键盘)流向一个目标(如文件、屏幕)。可以将流想象成一个管道,数据在其中流动。
6.1 标准流
C++ 标准库提供了几个标准流对象,它们在程序启动时自动创建,并且可以直接使用。主要的标准流有:
| 标准流对象 | 描述 |
| — | — |
|
std::cin
| 标准输入流,通常与键盘关联,用于从用户那里读取数据。 |
|
std::cout
| 标准输出流,通常与屏幕关联,用于向用户输出数据。 |
|
std::cerr
| 标准错误流,通常也与屏幕关联,用于输出错误信息。它是无缓冲的,意味着数据会立即输出。 |
|
std::clog
| 也是标准错误流,但它是有缓冲的,数据会先存储在缓冲区,在合适的时候再输出。 |
例如,我们可以使用
std::cin
读取用户输入的整数,使用
std::cout
输出信息:
#include <iostream>
int main() {
int num;
std::cout << "Please enter an integer: ";
std::cin >> num;
std::cout << "You entered: " << num << std::endl;
return 0;
}
6.2 二进制流与文本流
在 C++ 中,流可以分为二进制流和文本流。
-
文本流
:文本流以字符的形式处理数据,它会对数据进行一些转换,例如换行符在不同操作系统中的表示可能不同,文本流会进行相应的转换。文本流适合处理文本数据,如普通的文本文件。
-
二进制流
:二进制流以字节的形式处理数据,不会对数据进行任何转换。它适合处理二进制数据,如图片、音频、视频等文件。
下面是一个简单的流程图,展示了二进制流和文本流的区别:
graph LR
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
A(数据):::process --> B{流类型}:::process
B -->|文本流| C(字符处理与转换):::process
B -->|二进制流| D(字节处理,无转换):::process
C --> E(输出):::process
D --> E
7. 文件流的创建和使用
要进行文件的输入输出操作,我们需要使用文件流对象。C++ 标准库提供了三个主要的文件流类:
-
std::ifstream
:用于从文件中读取数据,即输入文件流。
-
std::ofstream
:用于向文件中写入数据,即输出文件流。
-
std::fstream
:既可以用于读取文件,也可以用于写入文件,即输入输出文件流。
7.1 打开文件
在使用文件流之前,需要打开文件。可以在构造函数中指定文件名和打开模式,也可以使用
open()
函数。打开模式有多种,常见的有:
| 打开模式 | 描述 |
| — | — |
|
std::ios::in
| 以输入模式打开文件,用于读取数据。 |
|
std::ios::out
| 以输出模式打开文件,用于写入数据。如果文件不存在,则创建文件;如果文件已存在,则清空文件内容。 |
|
std::ios::app
| 以追加模式打开文件,用于写入数据。数据会追加到文件末尾。 |
|
std::ios::binary
| 以二进制模式打开文件。 |
以下是一个打开文件并写入数据的示例:
#include <iostream>
#include <fstream>
int main() {
std::ofstream outFile("example.txt", std::ios::out);
if (outFile.is_open()) {
outFile << "Hello, World!" << std::endl;
outFile.close();
} else {
std::cerr << "Unable to open file." << std::endl;
}
return 0;
}
7.2 读取文件
使用
std::ifstream
可以从文件中读取数据。可以使用
>>
运算符逐词读取,也可以使用
getline()
函数逐行读取。
#include <iostream>
#include <fstream>
#include <string>
int main() {
std::ifstream inFile("example.txt", std::ios::in);
if (inFile.is_open()) {
std::string line;
while (std::getline(inFile, line)) {
std::cout << line << std::endl;
}
inFile.close();
} else {
std::cerr << "Unable to open file." << std::endl;
}
return 0;
}
8. 流操作中的错误处理
在进行流操作时,可能会出现各种错误,如文件打开失败、读取数据错误等。C++ 流对象提供了一些状态标志来表示流的状态。
8.1 状态标志
主要的状态标志有:
-
good()
:如果流处于正常状态,返回
true
;否则返回
false
。
-
eof()
:如果到达文件末尾,返回
true
。
-
fail()
:如果发生非致命错误,如读取数据格式错误,返回
true
。
-
bad()
:如果发生致命错误,如文件损坏,返回
true
。
以下是一个检查流状态的示例:
#include <iostream>
#include <fstream>
int main() {
std::ifstream inFile("nonexistent.txt", std::ios::in);
if (inFile.good()) {
std::cout << "File opened successfully." << std::endl;
} else {
if (inFile.fail()) {
std::cerr << "Non - fatal error occurred." << std::endl;
}
if (inFile.bad()) {
std::cerr << "Fatal error occurred." << std::endl;
}
}
return 0;
}
8.2 错误处理方法
当流操作出现错误时,可以根据状态标志进行相应的处理。例如,可以使用
clear()
函数清除错误标志,然后继续操作。
#include <iostream>
#include <fstream>
int main() {
std::ifstream inFile("example.txt", std::ios::in);
int num;
while (inFile >> num) {
std::cout << num << std::endl;
}
if (inFile.fail()) {
inFile.clear(); // 清除错误标志
std::string junk;
inFile >> junk; // 读取错误的数据
}
return 0;
}
9. 无格式流操作
除了使用
>>
和
<<
进行格式化输入输出外,C++ 还提供了无格式流操作。无格式流操作直接处理字节,不进行任何格式化。
9.1 读取和写入单个字符
可以使用
get()
函数读取单个字符,使用
put()
函数写入单个字符。
#include <iostream>
#include <fstream>
int main() {
std::ofstream outFile("char.txt", std::ios::out);
if (outFile.is_open()) {
outFile.put('A');
outFile.close();
}
std::ifstream inFile("char.txt", std::ios::in);
if (inFile.is_open()) {
char ch;
inFile.get(ch);
std::cout << "Read character: " << ch << std::endl;
inFile.close();
}
return 0;
}
9.2 读取和写入块数据
可以使用
read()
和
write()
函数读取和写入块数据。
#include <iostream>
#include <fstream>
int main() {
char data[] = "Hello, Binary!";
std::ofstream outFile("binary.bin", std::ios::out | std::ios::binary);
if (outFile.is_open()) {
outFile.write(data, sizeof(data));
outFile.close();
}
std::ifstream inFile("binary.bin", std::ios::in | std::ios::binary);
if (inFile.is_open()) {
char buffer[sizeof(data)];
inFile.read(buffer, sizeof(data));
std::cout << "Read data: " << buffer << std::endl;
inFile.close();
}
return 0;
}
10. 二进制数据的文件写入
在某些情况下,我们需要将数值数据以二进制形式写入文件,这样可以节省存储空间,并且在读取时可以直接恢复数据。
#include <iostream>
#include <fstream>
int main() {
int numbers[] = {1, 2, 3, 4, 5};
std::ofstream outFile("binary_numbers.bin", std::ios::out | std::ios::binary);
if (outFile.is_open()) {
outFile.write(reinterpret_cast<const char*>(numbers), sizeof(numbers));
outFile.close();
}
std::ifstream inFile("binary_numbers.bin", std::ios::in | std::ios::binary);
if (inFile.is_open()) {
int readNumbers[5];
inFile.read(reinterpret_cast<char*>(readNumbers), sizeof(readNumbers));
for (int i = 0; i < 5; ++i) {
std::cout << readNumbers[i] << " ";
}
std::cout << std::endl;
inFile.close();
}
return 0;
}
11. 对象的读写操作
在 C++ 中,我们可以将对象写入流和从流中读取对象。为了实现这一点,需要为对象的类重载插入和提取运算符。
11.1 重载插入运算符
#include <iostream>
#include <fstream>
class Point {
public:
int x;
int y;
Point(int x = 0, int y = 0) : x(x), y(y) {}
};
std::ostream& operator<<(std::ostream& os, const Point& p) {
os << p.x << " " << p.y;
return os;
}
int main() {
Point p(3, 4);
std::ofstream outFile("points.txt", std::ios::out);
if (outFile.is_open()) {
outFile << p;
outFile.close();
}
return 0;
}
11.2 重载提取运算符
#include <iostream>
#include <fstream>
class Point {
public:
int x;
int y;
Point(int x = 0, int y = 0) : x(x), y(y) {}
};
std::istream& operator>>(std::istream& is, Point& p) {
is >> p.x >> p.y;
return is;
}
int main() {
Point p;
std::ifstream inFile("points.txt", std::ios::in);
if (inFile.is_open()) {
inFile >> p;
std::cout << "Read point: (" << p.x << ", " << p.y << ")" << std::endl;
inFile.close();
}
return 0;
}
12. 字符串流的创建和使用
字符串流允许我们将字符串作为流进行操作,就像操作文件流一样。C++ 提供了
std::ostringstream
用于输出字符串流,
std::istringstream
用于输入字符串流。
#include <iostream>
#include <sstream>
#include <string>
int main() {
// 输出字符串流
std::ostringstream oss;
oss << "Hello, " << "World!";
std::string result = oss.str();
std::cout << "Output string: " << result << std::endl;
// 输入字符串流
std::string input = "123 456";
std::istringstream iss(input);
int num1, num2;
iss >> num1 >> num2;
std::cout << "Read numbers: " << num1 << " " << num2 << std::endl;
return 0;
}
通过以上内容的学习,我们全面了解了 C++ 中的类模板和文件输入输出操作。掌握这些知识,能够让我们更高效地编写 C++ 程序,处理各种类型的数据和文件。希望大家通过不断练习,熟练运用这些技术。
超级会员免费看
1436

被折叠的 条评论
为什么被折叠?



