特殊类设计、类型转换和IO流
- 一、特殊类设计
- 二、类型转换
-
- [1. C的类型转换](#1. C的类型转换)
- [2. C++类型转换](#2. C++类型转换)
- 三、IO流
-
- 前言
- [1. 标准IO流](#1. 标准IO流)
- [2. 标准文件流](#2. 标准文件流)
- [3. stringstream](#3. stringstream)
一、特殊类设计
only这个话太绝对,哪怕说设计一个类只能在栈或者在堆上创建对象,但是总有一些方法可能突破封锁。
1. 设计一个类,只能在堆上创建对象
eg1:析构私有。代码中的文字解释在另一篇博客C++内存管理有所介绍
cpp
class HeapOnly
{
public:
void Destroy()
{
delete this;
}
private:
~HeapOnly()
{}
};
int main()
{
// 栈对象和静态对象都会自动调用析构,析构被私有了也就创建失败
// HeapOnly hp1;
// static HeapOnly hp2;
HeapOnly* hp3 = new HeapOnly;
// delete hp3 == (这里其实是operator delete hp3)->free(hp3)+析构
// 因此也会调用析构,所以要显示调用成员函数
//delete hp3;
hp3->Destroy();
return 0;
}
eg2:构造函数私有
cpp
class HeapOnly
{
public:
static HeapOnly* CreateObj()
{
return new HeapOnly;
}
private:
HeapOnly()
{}
HeapOnly(const HeapOnly& hp) = delete;
};
int main()
{
//HeapOnly hp1;
//static HeapOnly hp2;
//HeapOnly* hp3 = new HeapOnly;
HeapOnly* hp3 = HeapOnly::CreateObj();
HeapOnly copy(*hp3); // error
return 0;
}
2. 设计一个类,只能在栈上创建对象
eg:
cpp
class StackOnly
{
public:
static StackOnly CreateObj()
{
StackOnly st;
return st;
}
private:
StackOnly()
{}
// 对一个类实现专属operator new. 这样它就不会再默认生成
void* operator new(size_t size) = delete;
};
int main()
{
//StackOnly hp1;
//static StackOnly hp2;
//StackOnly* hp3 = new StackOnly;
StackOnly hp3 = StackOnly::CreateObj();
StackOnly copy(hp3);
// new operator new + 构造
//StackOnly* hp4 = new StackOnly(hp3);
return 0;
}
3. 设计一个类,不能被拷贝/不能被继承
- 不能被拷贝,直接用delete禁了拷贝构造
- 不能被继承,直接用final禁了,在类名后面写
4. 设计一个类,只能创建一个对象(单例模式)
①设计模式
- 设计模式(Design Pattern):一套被反复使用、多数人知晓的、经过分类的、代码设计经验的总结。
- 使用设计模式的目的:为了代码可重用性、让代码更容易被他人理解、保证代码可靠性。 设计模
式使代码编写真正工程化;设计模式是软件工程的基石脉络,如同大厦的结构一样。 - 单例模式:一个类只能创建一个对象,该模式可以保证系统中该类只有一个实例。
- 单例模式有两种实现模式:在这个博客Linux线程的线程安全的单例模式 这一小节有着详细介绍
- 饿汉模式:单例对象在类加载时就被创建和初始化,无论是否被使用。可能造成资源浪费,尤其在实列化代价较高或资源消耗较大的情况。另外假如有A,B两个单例类,要求先创建A,再创建B,B的初始化依赖A。不能保证实例化顺序。(可以引入单例管理器,保证单例对象初始化顺序)
- 懒汉模式:核心思想延时加载。单例对象的实例化延迟至第一次被访问时才进行
②懒汉模式
在Linux线程这篇博客中也有对应的学习介绍,这里就不在介绍饿汉模式。只介绍懒汉模式(因为想介绍一点别的方面)
eg1:实现懒汉模式的一种方式,这个代码主要是在类内实现一个类用作回收单例(RAII思想)
cpp
#include <mutex>
namespace lazy
{
class Singleton
{
public:
// 不是线程安全的
//static Singleton& GetInstance()
//{
// // 第一次调用时创建对象
// if (_psinst == nullptr)
// {
// _psinst = new Singleton;
// }
// return *_psinst;
//}
// 线程安全的获取单例对象的接口函数
static Singleton& GetInstance()
{
// 双检查,提高效率
if (_psinst == nullptr)
{
lock_guard<mutex> lock(_mtx);
if (_psinst == nullptr)
{
_psinst = new Singleton;
}
return *_psinst;
}
return *_psinst; // 无论是否创建都返回一个值
}
// 可以显示调用释放
static void DelInstance()
{
if (_psinst)
{
delete _psinst;
_psinst = nullptr;
}
}
// 用于回收的内部类,使用了RAII思想
class GC
{
public:
~GC()
{
lazy::Singleton::DelInstance();
}
};
private:
Singleton()
{}
~Singleton()
{}
Singleton(const Singleton& s) = delete;
Singleton& operator=(const Singleton& s) = delete;
// 属于类的静态成员
static Singleton* _psinst;
static GC _gc;
static mutex _mtx; // 加锁保护
};
// 类外定义
Singleton* Singleton::_psinst = nullptr;
Singleton::GC Singleton::_gc;
mutex Singleton::_mtx;
}
eg2:实现懒汉模式的另一种方式
cpp
class Singleton
{
public:
// 提供获取单例对象的接口函数
static Singleton& GetInstance()
{
// 局部的静态对象,是在第一次调用时初始化,所以是懒汉模式
static Singleton inst;
return inst;
}
private:
Singleton()
{}
Singleton(const Singleton& s) = delete;
Singleton& operator=(const Singleton& s) = delete;
};
小节
- 构造私有:
- 栈对象、静态对象和堆创建的对象都会自动调用构造,构造私有了也就都创建失败了。但是可以写一个公用的静态成员函数,然后用类的方式调用来创建对象
- 析构私有:
- 栈对象和静态对象都会自动调用析构,析构被私有了也就创建失败。但是可以创建堆对象(也会调用析构delete时,可以显示写一个在公有成员函数中)
二、类型转换
1. C的类型转换
①C语言中的类型转换
- 类型转换:如果赋值运算符左右两侧类型不同,或者形参与实参类型不匹配,或者返回值类型与接收返回值类型不一致时,就会发生类型转化。
- 有两种形式的类型转换:
- 隐式类型转换:编译器在编译阶段自动进行,能转就转,不能转就编译失败
- 显式类型转换:需要用户自己处理,即强制类型转换
②C中类型转换有缺点
eg:volatile修饰的变量,每次都要去内存取,而不是在寄存器里直接用
cpp
int main()
{
volatile const int n = 10;
// 如果n的前面不加volatile,那么此时n输出的是10,而*p是11。但是本质上他俩都是一个地址。
// 这里涉及编译器优化问题,因为可能使用const修饰所以编译器在编译时直接就坐了替换(个人理解)。
// 所以C的类型转换有威胁
int* p = (int*)&n;
(*p)++;
return 0;
}
缺陷:转换的可视性比较差,所有的转换形式都是以一种相同形式书写,难以跟踪错误的转换
2. C++类型转换
C++为了加强类型转换的可视性,引入了四种命名的强制类型转换操作符
①static_cast
static_cast用于静态转换,编译器隐式执行的任何类型转换都可用,但它不能用于两个不相关的类型进行转换。
cpp
int main()
{
// 相关类型/相近类型
double d = 12.34;
int a = static_cast<int>(d);
return 0;
}
②reinterpret_cast
reinterpret_cast用于将一种类型转换为另一种不同的类型(即不相关类型)
cpp
int main()
{
// 不相关类型
int* p1 = &a;
// 这里使用static_cast会报错
int address = reinterpret_cast<int>(p1);
return 0;
}
③const_cast
const_cast最常用的用途就是删除变量的const属性
cpp
int main()
{
volatile const int n = 10;
int* p2 = const_cast<int*>(&n);
return 0;
}
④dynamic_cast
dynamic_cast用于将一个父类对象的指针或引用转换为子类对象的指针或引用(动态转换),即向下转型。
- 向上转型:子类对象指针/引用->父类指针/引用(不需要转换,赋值兼容规则即切片)
- 向下转型:父类对象指针/引用->子类指针/引用(用dynamic_cast转型是安全的)。有点像完美转发保持属性的作用
注意:dynamic_cast只能用于父类含有虚函数的类
eg:
cpp
class A
{
public:
virtual void f() {} // 有虚函数的父类
int _x = 0;
};
class B : public A
{
public:
int _y = 0;
};
// 用父类指针接收
void fun(A* pa)
{
// pa是指向子类对象B的,转换可以成功,正常返回地址
// pa是指向父类对象A的,转换失败,返回空指针
B* pb = dynamic_cast<B*>(pa);
if (pb)
{
cout << "转换成功" << endl;
}
else
{
cout << "转换失败" << endl;
}
}
int main()
{
A aa;
fun(&aa); // 失败
B bb;
fun(&bb); // 成功
return 0;
}
三、IO流
前言
- 在C语言阶段,习惯了使用scanf和printf进行输入输出。而C++依然可以使用C的库函数,但也引入了IO 流 (Input/Output Stream) 机制。
- C++流是指信息从外部输入设备向计算机内部输入和从内存向外部输出设备输出的过程。这种输入输出的过程被形象的比喻为"流"。C++定义了I/O标准类库,这些每个类都称为流/流类
1. 标准IO流

C++标准库提供了4个全局流对象cin、cout、cerr、clog。cout、cerr、clog是ostream类的三个不同的对象,使用场景有区别。
- cin为缓冲流。键盘输入的数据保存在缓冲区中,当要提取时,是从缓冲区中拿。
- 输入的数据类型必须与要提取的数据类型一致,否则出错。
- 空格和回车都可以作为数据之间的分格符。
- cin和cout可以直接输入和输出内置类型数据,原因:标准库已经将所有内置类型的输入和输出全部重载了
- 对于自定义类型,如果要支持cin和cout的标准输入输出,需要对
<<和>>进行重载
以上的内容基本都是常识看看即可
接下来介绍标准IO流的一些用法,用具体的例子,在代码中再进一步解释。我感觉也没必要看(可以过哈哈哈哈)
eg1:IO流中operator+类型和explicit
cpp
class A
{
public:
A(int a)
:_a(a)
{}
//explicit operator int()
operator int () // 给自定义类型转换成其他类型提供的,这个就是给A类型转int提供的
{
return _a;
}
operator bool()
{
return _a;
}
int _a;
};
int main()
{
// 内置类型->自定义类型
A aa1 = 100;
// 自定义类型->内置类型
// 如果没有explicit修饰operator int则它会调用operator int。更适合
// 有explicit修饰operator int则它会调用operator bool
int i = aa1;
cout << i << endl;
// 如果没有explicit修饰operator int则报错,二义性问题也就是调用上面两个都可以
// 有explicit修饰operator int则成功此时d=1,也就是调用了operator bool
double d = aa1;
bool ret = aa1;
cout << ret << endl;
return 0;
}
小结:explicit修饰operator+类型时禁止隐式类型转换。operator bool只要是隐式类型转换都可以调用它,所以会出现二义性问题。
eg2:单个循环输入问题
cpp
int main()
{
string str;
// Ctrl+z结束循环,此时cin.operator bool()就是返回假
// 本质是operator>>(cin, str).operator bool() <=> cin.operator>>(str).operator bool()
// operator>>(cin, str)会返回一个istream&类型,即把cin返回回去。再调用cin.operator bool()
// 库中的explicit operator bool(),会用在 if、while 这种判断真假的场合。
while (cin >> str)
{
cout << str << endl;
}
return 0;
}
小结:operator+内置类型() 此类成员函数,就是给自定义对象转内置类型提供方式。例如operator int()。就是给自定义类型对象转int提供方式
2. 标准文件流
C++根据文件内容的数据格式分为二进制文件和文本文件。采用文件流对象操作文件的一般步骤:
- 定义一个文件流对象
- ifstream ifile(只输入用)
- ofstream ofile(只输出用)
- fstream iofile(既输入又输出用) ,这个的父类是iostream
- 使用文件流对象的成员函数打开一个磁盘文件,使得文件流对象和磁盘文件之间建立联系
- 使用提取和插入运算符对文件进行读写操作,或使用成员函数进行读写
- 关闭文件
例子1:先写一个日期类在这里,下面的测试会调用这个类
cpp
class Date
{
// 友元函数在前面的博文有介绍
friend ostream& operator << (ostream& out, const Date& d);
friend istream& operator >> (istream& in, Date& d);
public:
Date(int year = 1, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
{}
operator bool()
{
if (_year == 0) // 写死犯错方式
return false;
else
return true;
}
operator string()
{
string str;
str += to_string(_year);
str += ' ';
str += to_string(_month);
str += ' ';
str += to_string(_day);
return str;
}
private:
int _year;
int _month;
int _day;
};
istream& operator >> (istream& in, Date& d)
{
in >> d._year >> d._month >> d._day;
return in;
}
ostream& operator << (ostream& out, const Date& d)
{
out << d._year << " " << d._month << " " << d._day;
return out;
}
test1:以C的方式进行读写
cpp
int main()
{
Date d(2023, 10, 14);
FILE* fin = fopen("file.txt", "w");
// C语言的方式以二进制方式向文件写入数据
//fwrite(&d, sizeof(Date), 1, fin);
// 这个会调用operator string,把d转成string类型
string str = d;
// C语言的方式以文本方式向流写入数据
fputs(str.c_str(), fin);
fclose(fin);
return 0;
}
test2:以C++的方式进行读写。自定义类型在读写的时候,用>>和<<就会调用我们写的流插入和流提取
cpp
int main()
{
Date d(2026, 10, 14);
ofstream ofs("file.txt", ios_base::out | ios_base::binary);
// 默认的打开方式:ios_base::out文件被打开用于写入。文件不存在,创建该文件
// ofstream ofs("file.txt");
// 二进制的方式向文件中写
// ofs.write((const char*)&d, sizeof(d));
// 文本的方式向文件中写
// 它会调用我们写的日期类的流插入
ofs << d;
ofs.close();
return 0;
}
例子2:封装一个类用于二进制读写和文本读写
cpp
struct ServerInfo
{
char _address[32];
// 二进制读写不能用string这样的对象存数据,因为写出去就是一个指针
// 进程结束就是野指针,另一个进程再读进来就是访问野指针
//string _address;
int _port;
Date _date;
};
struct ConfigManager
{
public:
ConfigManager(const char* filename = "file.txt")
:_filename(filename)
{}
// 二进制方式写
void WriteBin(const ServerInfo& info)
{
ofstream ofs(_filename, ios_base::out | ios_base::binary);
ofs.write((const char*)&info, sizeof(info));
}
// 二进制方式读
void ReadBin(ServerInfo& info)
{
ifstream ifs(_filename, ios_base::in | ios_base::binary);
ifs.read((char*)&info, sizeof(info)); // 读到info中
}
// 文本方式写
void WriteText(const ServerInfo& info)
{
ofstream ofs(_filename);
ofs << info._address << " " << info._port << " " << info._date;
}
// 文本方式读
void ReadText(ServerInfo& info)
{
ifstream ifs(_filename);
ifs >> info._address >> info._port >> info._date;
}
private:
string _filename;
};
// 只测试了二进制,文本同理。也可以用一下其他的成员函数测试
int main()
{
ServerInfo winfo = { "192.0.0.1111111111111111111", 80, { 2022, 4, 10 } };
ConfigManager cm("test.bin"); // 传入文件名
cm.WriteBin(winfo);
ServerInfo rbinfo;
cm.ReadBin(rbinfo); // 二进制方式读
cout << rbinfo._address << " " << rbinfo._port << " " << rbinfo._date << endl;
return 0;
}
3. stringstream
stringstream对应的是sscanf和sprintf这种操作字符串的函数,但是C的函数在转化时,都得需要先给出保存结果的空间,那空间要给多大呢,就不太好界定,而且转化格式不匹配时,可能还会得到错误的结果甚至程序崩溃。在C++中,可以使用stringstream类对象来避开此问题
eg1:将数值类型数据格式化为字符串。 str和clean一定不要忘记调用,如果连续使用
cpp
#include<sstream>
int main()
{
int a = 12345678;
string sa;
// 将一个整形变量转化为字符串,存储到string类对象中
stringstream s;
s << a; // 把a的数据流向s中
s >> sa;
// s.clear();
// 多次转换时,必须用clear将上次转换状态清空掉
// stringstream在转换结尾时(即最后一个转换后),会将其内部状态设置为badbit,
// 因此下一次转换是必须调用clear()将状态重置为goodbit才可以转换
// 但是clear()不会将stringstreams底层字符串清空掉
// s.str("");
// 将stringstream底层管理string对象设置成"",
// 否则多次转换时,会将结果全部累积在底层string对象中
s.str("");
s.clear(); // 清空s, 不清空会转化失败
double d = 12.34;
s << d;
s >> sa;
string sValue;
sValue = s.str(); // str()方法:返回stringsteam中管理的string类型
cout << sValue << endl;
return 0;
}
eg2:字符串拼接
cpp
#include<sstream>
int main()
{
int a = 10;
stringstream sstream;
// 将多个字符串放入 sstream 中
sstream << "first" << " " << a << " " << "string,";
cout << "strResult is: " << sstream.str() << endl;
// 清空 sstream
sstream.str("");
sstream << "third string";
cout << "After clear, strResult is: " << sstream.str() << endl;
return 0;
}
eg3:序列化和反序列化结构数据
cpp
#include<sstream>
struct ChatInfo
{
string _name; // 名字
int _id; // id
Date _date; // 时间
string _msg; // 聊天信息
};
int main()
{
// 结构信息序列化为字符串
ChatInfo winfo = { "张三", 12322323, { 2022, 4, 10 }, "晚上一起看电影吧"};
stringstream oss; // 数据存在对象oss中
oss << winfo._name << " " << winfo._id << " " << winfo._date << " "
<< winfo._msg;
string str = oss.str();
cout << str << endl << endl;
// 字符串解析成结构信息
ChatInfo rInfo;
stringstream iss(str); // 数据存在对象iss中
iss >> rInfo._name >> rInfo._id >> rInfo._date >> rInfo._msg; // 以空格为分隔符
cout << "-------------------------------------------------------"<< endl;
cout << "姓名:" << rInfo._name << "(" << rInfo._id << ") ";
cout << rInfo._date << endl;
cout << rInfo._name << ":>" << rInfo._msg << endl;
cout << "-------------------------------------------------------"
<< endl;
return 0;
}