总言
主要内容:介绍了C++的四种类型转换(static_cast、reinterpret_cast、const_cast、dynamic_cast),以及对C++中IO流进行了简单说明。
1、C++类型转换
1.1、C语言中的类型转换
1)、前情回顾
在C语言中,如果遇到赋值运算符左右两侧类型不同,或者形参与实参类型不匹配,或者返回值类型与接收返回值类型不一致等此类情况,就需要发生类型转换。
C语言中,类型转换是一种将一种数据类型显式或隐式地转换为另一种数据类型的操作。类型转换可以分为两大类:隐式类型转换(也称为自动类型转换)和显式类型转换(也称为强制类型转换)。
1、显式类型转换: 也称为强制类型转换,是程序员通过类型转换运算符(目标类型) 表达式
,明确指定将一种数据类型转换为另一种数据类型。
int a = 5;
double b;
b = (double) a; // 将 int 类型的 a 强制转换为 double 类型,然后赋值给 b
2、隐式类型转换: 编译器在编译阶段自动进行,能转就转,不能转就编译失败。通常是在表达式求值或赋值操作中,遵循数据类型的“提升”和“转换”规则。
整数提升: 较小的整数类型(如 char 和 short)在表达式中通常会被向上提升为较大的类型(如 int 或 unsigned int )。
浮点转换: 当整数和浮点数一起进行运算时,整数会被转换为浮点数。
算术转换: 对于不同类型的算术运算,编译器会按照一定规则进行类型转换,以使得操作数类型一致。
//举例:
int a = 5;
double b = 3.14;
double c = a + b; // 隐式地将 int 类型的 a 转换为 double 类型
2)、缺陷说明
C++向前兼容,因此,在C中的类型转换,同样适用于C++中。但C语言的类型转换,特别是隐式类型转换,存在一些潜在的隐患。这些隐患可能导致程序行为变得不可预测,甚至引发错误或崩溃。
一个示例:
还记得我们学习STL时写过的insert插入吗?(详细回顾)
string& insert(size_t pos, char ch)
{
assert(pos <= _size);
//……
//数据插入:向后挪动数据,从后往前遍历
size_t end = _size;
while (end >= pos)
{
_str[end + 1] = _str[end];
--end;
}
_str[pos] = ch;
//……
}
当时我们介绍过,这种写法存在一定隐患,当pos =0
进行头插时,由于end
是无符号整型,当end=0
时,--end
得到的是一个很大的整型数据,导致while
循环继续。
我们也曾提出,将end
的类型改为int
类型,但实际上在while (end >= pos)
进行条件判断时,会发生隐式类型转换,最后还是得到size_t
类型的数据。
这种隐式类型转换,会带来一定的逻辑错误。可能连我们自己都没有意识到某个变量已经发生了类型转换,从而引发程序产生错误的行为。
其它情况简述:
精度损失: 当将一个高精度的数据类型(如double)转换为低精度的数据类型(如int)时,小数部分会被截断,导致精度损失。
#include <stdio.h>
int main() {
double pi = 3.141592653589793;
int intPi = (int)pi;
printf("Original pi: %lf\n", pi);
printf("Converted pi: %d\n", intPi);
return 0;
}
影响: 这种精度损失在一些需要高精度的场景中是不可接受的。
数据溢出: 将一个较大范围的数据类型(如long)的值转换为较小范围的数据类型(如char)时,如果源数据的值超出了目标类型的表示范围,就会发生溢出。
#include <stdio.h>
#include <limits.h>
int main() {
unsigned int maxUnsignedInt = UINT_MAX;
int signedInt = (int)maxUnsignedInt;
printf("Max unsigned int: %u\n", maxUnsignedInt);
printf("Converted signed int: %d\n", signedInt);
return 0;
}
影响:溢出可能导致程序行为变得不可预测,产生错误的结果,甚至引发未定义行为。
类型安全: 通常来讲,隐式类型转化一般用于意义相近的类型,比如浮点型和整形,能够转换是因为它们都是数值存储。但如果让原本不兼容的类型之间能够进行转换,这可能会导致程序在运行时出现类型错误或异常,引发难以查找和解决的错误。
#include <stdio.h>
int main() {
char charVar = 'A';
int *intPtr = (int*)&charVar;
*intPtr = 12345; // 尝试将整数值写入字符变量的内存地址
printf("Char after type punning: %c\n", charVar);
return 0;
}
逻辑错误: 在复杂的表达式或逻辑中,隐式类型转换可能会导致难以追踪的逻辑错误。
#include <stdio.h>
int main() {
unsigned int a = 1;
int b = -1;
if (a < b) {
printf("a is less than b\n");
} else {
printf("a is greater than or equal to b\n");
}
return 0;
}
还会存在其它各种情况。
因此,C++提出了自己的类型转化风格。注意,因为C++要兼容C语言,所以C++中还可以使用C语言的转化风格。
1.2、C++中的四种类型转换
标准C++为了加强类型转换的可视性,引入了四种命名的强制类型转换操作符:
static_cast:静态类型转换
reinterpret_cast:重新解释类型转换
const_cast:常量类型转换
dynamic_cast:动态类型转换
1.2.1、static_cast
1)、基本介绍
static_cast
用于非多态类型的转换(静态转换),编译器隐式执行的任何类型转换都可用static_cast
,但它不能用于两个不相关的类型进行转换。
type target = static_cast<type>(expression);
//其中 type 是你想要转换成的目标类型,expression 是要进行转换的表达式。
2)、static_cast 主要用于以下几种类型转换
1、基本数据类型之间的转换(常用): 可以使用 static_cast
将一种基本数据类型(如 int、float、double 等)转换为另一种基本数据类型。这种转换可能会导致数据丢失或精度下降,但编译器不会报错,因为它是在编译时进行的。
2、指针或引用之间的转换:
Ⅰ、*void
指针到具体类型指针的转换(常用):static_cast
可以用于将 void*
指针转换为其他类型的指针,反之亦然。这种转换需要我们确信转换是安全的。
Ⅱ、在类的继承体系中,static_cast
可以用于将基类指针或引用转换为派生类指针或引用(前提是这种转换是安全的,即实际对象确实是派生类类型的)。这种转换不进行运行时类型检查,因此如果转换不正确,结果将是未定义的。
相反,你也可以将派生类指针或引用转换为基类指针或引用,这是安全的,因为子类对象可以赋值给父类对象/指针/引用。
3、常量性转换(一般建议用const_cast
): static_cast
可以用在添加/移除非指针/引用类型的const
或 volatile
限定符。扩展学习:static_const和const_cast去除或添加const/volatile。
1.2.2、reinterpret_cast
1)、基本介绍
reinterpret_cast
是 C++ 中的一种强制类型转换操作符,用于在不同类型的指针或引用之间进行低级别的重新解释。 由于这种转换不检查类型之间的兼容性,因此很容易导致未定义行为。
2)、reinterpret_cast主要用于以下几种情况
1、指针或引用之间的转换:
将一种类型的指针转换为另一种类型的指针。
将一种类型的引用转换为另一种类型的引用。
2、指针与足够大的整数类型之间的转换: 在某些情况下,可能需要将指针的值视为整数进行存储或传输。例如,在与某些底层 API 或硬件接口交互时,可能需要将指针转换为整数类型(如 uintptr_t
)
int* ptr = new int(42);
uintptr_t value = reinterpret_cast<uintptr_t>(ptr);
// 现在可以使用 value 存储或传输指针值
// ...
// 当需要恢复指针时
int* recoveredPtr = reinterpret_cast<int*>(value);
错误演示:
扩展学习:探索 static_cast、reinterpret_cast、dynamic_cast 和 const_cast
1.2.3、const_cast
1)、基本介绍
const_cast
,用于去除对象的常量性或易变性。这意味着,如果你有一个被声明为 const
或 volatile
的对象,你可以使用 const_cast
来去除这些属性,使得对象可以被修改。
const_cast<type*>(expression);
//type*:目标类型,必须是指针类型、引用类型或指向成员的指针类型。
//expression:需要进行类型转换的表达式,其类型必须是 type*、type& 或指向成员的指针类型,并且 type 必须包含 const 或 volatile 限定符。
注意: 使用 const_cast 来去除 const 或 volatile 限定符可能会引入未定义行为,特别是当我们试图修改一个本不应被修改的对象时。
2)、const_cast 使用场景举例
1、去除 const
限定符: 当我们需要对一个 const 对象进行修改时,可以使用 const_cast 来去除 const 限定符。实际这种做法通常是不安全的,因为它违反了 const 的语义。然而,在某些情况下,例如在使用遗留代码或需要绕过某些 const 保护的接口时,可能会用到。
const int* ptr = &someConstInt;
int* nonConstPtr = const_cast<int*>(ptr);
*nonConstPtr = 42; // 现在可以修改这个值了
2、去除 volatile
限定符: volatile
关键字用于告诉编译器,某个变量的值可能会在程序的控制之外被改变(例如,由硬件修改)。去除 volatile
限定符通常用于与硬件编程相关的场景,但这同样是不安全的,因为它忽略了变量值可能在外部被改变的事实。
volatile int* vPtr = &someVolatileInt;
int* nonVolatilePtr = const_cast<int*>(vPtr); // 注意:这里应该使用volatile_cast(如果存在)但实际上C++标准没有定义,所以仍用const_cast作为示例
*nonVolatilePtr = 10; // 假设我们确信这样做是安全的
3)、相关演示
int main()
{
const int p = 233;
int* np = const_cast<int*>(&p);// 指针
int& rp = const_cast<int&>(p);// 引用
p = 3333;
*np = 666; // 修改值
printf("%d %d %d\n", p, *np, rp);
return 0;
}
针对上述问题的回答:编译器对const
变量进行了优化处理。具体处理方式因编译器的不同而有所差异。编译器通常会认为,被const修饰的变量,在后续代码中不会被修改。因此,一些编译器在预处理阶段,可能会直接将const变量替换为固定的值,这种优化方式类似于宏替换(VS下就采用这种做法,下图为vs2019中汇编演示)。而另一些编译器则可能会选择将被const修饰的变量直接存储在寄存器中,以便在需要时能够快速访问,而无需再到内存中查找,这种优化方式相较于从内存中读取变量能够显著提高效率。
为什么我们监视窗口看到是变化后的值? 监视窗口是另一个进程去运行的。在监视窗口中看到的值(即调试时的实时值)反映了程序运行时的实际状态。
如果我们就要正确地读取到变量的最新值,可以怎么做? 使用volatile
关键字让编译器不做优化处理。
4)、其它一些细节点
说明一:
说明二:上述我们一直在谈论 const_cast
用于去除类型的 const
或 volatile
限定符,但它也可以用于向类型添加这两种限定符。
只不过在实际编程中,向类型添加 const 或 volatile 限定符通常可以通过其他方式实现。比如,直接声明:
int a = 10;
const int* ptr = &a; // 直接声明为指向 const int 的指针
volatile int b = 20; // 声明一个 volatile int 类型的变量
volatile int* vPtr = &b; // 声明一个指向 volatile int 的指针
1.2.4、dynamic_cast
dynamic_cast
,相对于其它三者是对C的完善,这是 C++ 中的一种新的类型转换,主要用于在类的继承体系中,进行安全的多态类型转换。
在学习继承和多态章节时,我们曾介绍过,基类和派生类对象赋值转换:
1、子类对象可以赋值给父类对象/指针/引用
2、类对象不能赋值给派生类对象
3、基类的指针可以通过强制类型转换赋值给派生类的指针
当时我们遗留下第三条没介绍,实际上,这种转换正是通过dynamic_cast
进行的。dynamic_cast
用于将一个父类对象的指针/引用转换为子类对象的指针/引用(动态转换)。
1.2.4.1、向上转型(upcasting)和向下转型(downcasting)
1)、总览
向上转型:子类对象指针/引用->父类指针/引用(不需要转换,赋值兼容规则)
向下转型:父类对象指针/引用->子类指针/引用(用dynamic_cast转型是安全的)
2)、向上转型(Upcasting)
向上转型是指,将子类对象的指针或引用转换为父类的指针或引用。这种转换在C++中是自动的,并且总是安全的,因为子类对象本质上也是父类对象的一个扩展(或特例)。换句话说,父类指针或引用可以指向或引用任何子类对象,只要这些子类对象是通过父类类型的接口来访问的。
细节点:未发生显式/隐式转换,这是赋值兼容规则允许这样做的。
3)、向下转型(Upcasting)
向下转型是指将父类对象的指针或引用转换回子类(或派生类)的指针或引用。这种转换不是自动的,并且通常是不安全的,因为父类指针或引用可能实际上指向的是一个不是子类类型的对象。然而,如果确信父类指针或引用确实指向了一个子类对象,可以使用 dynamic_cast
来安全地进行向下转型。
dynamic_cast
在运行时检查对象的实际类型,如果转换是合法的(即父类指针或引用确实指向了一个子类对象),则返回正确的子类指针或引用;否则,对于指针类型,返回 nullptr
;对于引用类型,抛出 std::bad_cast
异常。
需要注意的是,dynamic_cast
只能用于父类含有虚函数的类(即多态类型),并且它的性能开销相对于其他类型转换来说可能会更高一些。因此,在使用 dynamic_cast
时应该权衡其安全性和性能开销。
class Base {
public:
virtual ~Base() {} // 必须至少有一个虚函数以支持 RTTI
};
class Derived : public Base {
public:
void fun() {
std::cout << "Derived: fun() " << std::endl;
}
};
int main() {
Base* pb = new Derived(); // 向上转型
Derived* pd = dynamic_cast<Derived*>(pb); // 向下转型
if (pd) {
pd->fun(); // 如果转换成功,调用 Derived 类的函数
}
else {
std::cout << "Downcasting failed" << std::endl;
}
delete pb; // 不要忘记释放内存
return 0;
}
1.2.4.2、为什么需要使用dynamic_cast进行向下转型?
1)、举例概述
访问子类特有的成员: 当通过父类类型的指针或引用操作对象时,你只能访问到父类中定义的成员(属性和方法)。如果对象实际上是子类的一个实例,并且你需要访问子类特有的成员(即那些在子类中新增的、不在父类中的成员),那么你就需要将父类类型的指针或引用向下转型为子类类型的指针或引用。
设计模式的实现: 在面向对象编程中,设计模式是解决常见问题的最佳实践。某些设计模式(如工厂模式、桥接模式等)可能会创建基类类型的对象,但在后续的处理中需要识别并处理这些对象的实际派生类类型。这时,向下转型是不可或缺的。
2)、一个简单演示
class A
{
public:
virtual void f() {}
int _a = 1;
};
class B : public A
{
public:
int _b = 2;
};
// 一个形参是父类指针的函数:这类传参时,可以是父类,也可以是子类(后者发生向上转型)
void fun(A* pa)
{
//B* pb = (B*)pa;//将pa强制类型转换为pb
B* pb = dynamic_cast<B*>(pa);
if (pb)
{
pb->_a++;
pb->_b++;
printf("向下转型成功:%d %d\n", pb->_a, pb->_b);
}
else
{
pa->_a++;
printf("向下转型失败:%d %p\n", pa->_a, pb);
}
}
int main()
{
A aa;
B bb;
fun(&aa);
cout << endl;
fun(&bb);
return 0;
}
1.2.4.3、延伸:多继承下的向下转换
演示如下:
class A1
{
public:
virtual void f() {}
int _a1 = 1;
};
class A2
{
public:
virtual void f(){}
int _a2 = 2;
};
class B : public A1,public A2
{
public:
int _b = 0;
};
int main()
{
B bb;
A1* ptr1 = &bb;
A2* ptr2 = &bb;
cout << ptr1 << endl;
cout << ptr2 << endl << endl;
B* bb1 = (B*)ptr1;
B* bb2 = (B*)ptr2;
cout << bb1 << endl;
cout << bb2 << endl << endl;
B* bb3 = dynamic_cast<B*>(ptr1);
B* bb4 = dynamic_cast<B*>(ptr2);
cout << bb3 << endl;
cout << bb4 << endl << endl;
return 0;
}
演示结果如下:所有转换后的指针(bb1, bb2, bb3, bb4
)都会指向相同的地址。
当进行强制类型转换(B*)ptr1
和(B*)ptr2
或使用dynamic_cast
时。这是在告诉编译器:“我知道这个A1*
或A2*
实际上是指向一个B
对象,请把它当作B*
来处理。” 由于B
对象的开始地址是唯一的,无论从A1
还是A2
的视角开始,最终转换回B*
时得到的地址都是相同的,即B
对象的起始地址。
1.3、RTTI(运行时类型识别)
1)、基本介绍
RTTI:Run-time Type identification
的简称,即:运行时类型识别。它允许程序在运行时检查对象的实际类型。
C++主要通过以下两个关键字来支持RTTI:
typeid
运算符:用于在运行时获取一个对象或类型的类型信息。typeid
返回一个std::type_info
对象,该对象包含了有关类型的特定信息,并可以用于比较不同对象的类型。
dynamic_cast
运算符:用于将基类指针或引用安全地转换为派生类指针或引用。如果转换失败,对于指针类型,dynamic_cast
将返回nullptr
;对于引用类型,它将抛出std::bad_cast
异常。dynamic_cast
只能用于含有虚函数的类(即多态类型),并且在进行类型转换时,它会检查对象的实际类型以确保转换的安全性。
扩展。
2、IO流
2.1、C、C++中输入与输出
2.1.1、什么是流,C语言下的IO流
C语言中我们用到的最频繁的输入输出方式就是scanf()
与printf()
。
scanf(): 从标准输入设备(键盘)读取数据,并将值存放在变量中。
printf(): 将指定的文字/字符串输出到标准输出设备(屏幕)。
其他常用的输入和输出函数:
getchar() 和 putchar():用于读取和输出单个字符。
fgets() 和 fputs():用于读取和输出字符串,fgets() 可以防止缓冲区溢出。
sprintf() 和 sscanf():用于将格式化的数据写入字符串和从字符串读取数据。
关于C语言的IO流,其它文件流操作,温故。
关于Linux下的IO流,文件操作,温故。
2.1.2、C++ 中的IO流
1)、基本介绍
和C语言一样,在C++中,数据的输入输出(I/O)被抽象为“流”。根据数据源和数据目的地的不同,IO流可以分为标准I/O、文件I/O和串I/O等。
标准I/O:涉及标准输入设备(如键盘)和标准输出设备(如显示器)的数据输入输出。
文件I/O:涉及在外存磁盘上文件的输入输出。
串I/O:涉及内存中指定的字符串存储空间的输入输出。
相关文档:Input/Output library。
2)、流类库的继承体系(简单了解)
如下图,C++的流类库采用了继承机制。
1、基类: C++的流类库具有两个平行的基类:streambuf
和 ios
。
streambuf类: 这个类提供对缓冲区的低级操作。
如设置缓冲区、对缓冲区指针进行操作、向缓冲区存/取字符等。它是流类库中的底层类,负责实际的字符读写操作。
ios类: 这个类继承自ios_base,记录流的状态,并支持对streambuf的缓冲区进行格式化或非格式化转换。
它提供了对流进行格式化I/O操作和错误处理的成员函数,是流类库中的核心类之一。
2、派生类: 从ios类派生出了两个主要的子类:istream
和ostream
,分别用于输入和输出操作。
istream类:用于输入操作,提供了如operator>>等重载运算符来从流中提取数据。
它还包括get、getline、read等成员函数,用于读取不同类型的输入。
ostream类:用于输出操作,提供了如operator<<等重载运算符来向流中插入数据。
它还包括put、write等成员函数,用于输出单个字符或字符序列。
3、文件流类: 为了支持对文件的操作,C++流类库在istream和ostream的基础上,又派生出了五个与文件相关的类:fstreambase
、ifstream
、ofstream
、fstream
和filebuf
。
fstreambase类:这是文件流的共同基类,程序中通常不使用该类进行文件操作。它派生自ios类,为文件流提供了基本的接口和功能。
ifstream类:这个类继承自istream和fstreambase,仅用于读文件。它提供了与cin相同的操作接口,但操作的是文件流。
ofstream类:这个类继承自ostream和fstreambase,仅用于写文件。它提供了与cout相同的操作接口,但同样操作的是文件流。
fstream类:这个类继承自iostream(而iostream又继承自istream和ostream)和fstreambase,用于对文件进行读写操作。它同时提供了输入和输出操作的接口。
filebuf类:这个类派生自streambuf类,负责管理文件操作的缓冲区。它提供了对文件缓冲区的低级操作,如设置缓冲区、读写文件等。
4、字符串流类: C++流类库还支持对字符串的操作,为此派生出了三个与字符串相关的类:istringstream
、ostringstream
和stringstream
。
istringstream类:这个类继承自istream,提供读string的功能。它可以将一个字符串视为输入流,从中提取数据。
ostringstream类:这个类继承自ostream,提供写string的功能。它可以将数据插入到一个字符串中,并返回该字符串。
stringstream类:这个类继承自iostream,提供读写string的功能。它同时支持输入和输出操作,可以将一个字符串视为双向流。
2.2、C++标准IO流
2.2.1、简单介绍4个全局流对象:cin、cout、cerr、clog
C++标准库提供了4个全局流对象:cin、cout、cerr、clog
,它们都是C++标准库的开发者创建好的内置对象,可以直接拿来使用(在使用时候必须要包含文件并引入std标准命名空间)。
1)、std::cin
std::cin
:用于标准输入,即数据通过键盘输入到程序中。
int num;
std::cin >> num; // 从键盘读取一个整数并存储在变量num中
一些说明:
cin
为缓冲流,键盘输入的数据保存在缓冲区中,当要提取时,是从缓冲区中拿。- 如果一次输入过多,数据会留在缓冲区等待提取,如果输入内容错了,必须在回车之前修改,一旦按下回车键就无法挽回,只能把输入缓冲区中的数据取完后,才要求输入新的数据。
- 输入的数据类型必须与要提取的数据类型一致,否则出错。空格和回车都可以作为数据之间的分隔符。所以多个数据可以在一行输入,也可以分行输入。但如果输入数据是字符型和字符串,则空格(ASCII码为32)无法用cin输入,字符串中也不能有空格,回车符也无法读入。
cin
可以直接输入内置类型数据,因为标准库已经将所有内置类型的输入全部重载了。对于自定义类型,如果要支持cin
的标准输入,需要对“>>
”进行重载。
2)、std::cout
std::cout
:用于标准输出,即数据从内存流向控制台(显示器)。
std::cout << "Hello, World!" << std::endl; // 向控制台输出字符串并换行
一些说明:
cout
流在内存中对应开辟了一个缓冲区,用来存放流中的数据。当向cout
流插入一个endl
时,不论缓冲区是否已满,都立即输出流中所有数据,然后插入一个换行符,并刷新流(清空缓冲区)。cout
也可以被重定向输出到磁盘文件。cout
可以直接输出内置类型数据,因为标准库已经将所有内置类型的输出全部重载了。对于自定义类型,如果要支持cout的标准输出,需要对“<<”进行重载。
3)、std::cerr、std::clog
std::cerr
:用于向标准错误输出设备(通常是显示器,但通常与标准输出不同,可能带有不同的颜色或标记)输出错误信息。std::cerr
通常不带缓冲,这意味着写入到std::cerr
的数据会立即被输出,而不会被存储在缓冲区中等待后续操作。
std::cerr << "Error: Division by zero!" << std::endl; // 向控制台输出错误信息并换行
std::clog
:也是标准错误流。用于向标准日志输出设备(通常是显示器,但可能以不同的方式处理,例如写入到日志文件或特定的控制台窗口)输出日志信息。std::clog
是带缓冲的,这意味着写入到std::clog
的数据可能会存储在缓冲区中,直到缓冲区满或显式地刷新缓冲区(例如使用std::endl
或std::flush
)。
std::clog << "Log: Starting program execution." << std::endl; // 向控制台输出日志信息并换行
2.2.2、基本演示
2.2.2.1、演示一:读取整型
#include <iostream>
int main() {
int year, month, day;
std::cout << "请输入日期: ";
while (std::cin >> year >> month >> day)
{
std::cout << "你输入的日期是: " << year << "年" << month << "月" << day << "日" << std::endl;
}
return 0;
}
说明一: 在Windows系统,C/C++程序中,Ctrl+Z
在输入流中通常被视为文件结束符(EOF)。当std::cin
在读取输入时,如果遇到Ctrl+Z
(通常是在输入结束后单独输入Ctrl+Z
并回车),它会认为输入已经结束,从而停止读取。但这一点在Linux等Unix-like系统中并不适用,因为在这些系统中,Ctrl+D
才是EOF的默认信号。
附:Linux下,在命令行中,按下Ctrl+Z
组合键的作用是将当前正在前台运行的程序挂起(暂停)并放入后台。这通常用于临时停止当前程序的执行。正在前台执行的程序会被发送一个SIGTSTP(暂停)信号,从而使其停止执行,并将控制权返回给命令行终端。被挂起的程序可以使用fg
命令将其切换回前台继续执行,或者使用bg
命令在后台继续执行。
说明二: 在命令行中,Ctrl+C
被用来中断(或终止)当前运行的程序或命令。按下Ctrl+C
会发送一个中断(SIGINT)信号给当前运行的程序,这个信号会导致程序停止执行并退出。在大多数情况下,程序会接收到中断信号后立即停止执行并退出。但有些程序可能会忽略该信号或者需要一些时间才能正确地中断和退出。
2.2.2.2、演示二:读取字符串(std::getline和std::cin.getline)
我们在学习std::string
时说明过这个问题(回顾)。
#include <iostream>
#include <string>
int main() {
std::string str;
std::cout << "请输入: ";
while (getline(cin,str))
{
std::cout << "输出>>:" << str << std::endl;
}
return 0;
}
其它介绍:注意区别std::getline
和std::cin.getline
。
std::getline
是C++标准库中的一个全局函数,用于从输入流中读取一行数据并存储到std::string
对象中。它定义在<string>
头文件中。
std::istream& getline(std::istream& is, std::string& str, char delim = '\n');
//is:输入流对象,如std::cin或文件流。
//str:用于存储读取行的std::string对象。
//delim:可选参数,指定行的分隔符,默认为换行符'\n'。
#include <iostream>
#include <string>
int main() {
std::string input;
std::cout << "请输入一行文本: ";
std::getline(std::cin, input);
std::cout << "你输入的文本是: " << input << std::endl;
return 0;
}
std::istream::getline
是std::istream
类的一个成员函数,用于从输入流中读取一行数据并存储到字符数组中。它定义在<istream>
头文件中,但通常与<iostream>
一起使用。
std::istream& getline(char* s, std::streamsize n, char delim = '\n');
//s:指向字符数组的指针,用于存储读取的字符串。
//n:要读取的最大字符数(包括空终止符'\0')。
//delim:可选参数,指定行的分隔符,默认为换行符'\n'。
#include <iostream>
int main() {
const int MAX_LENGTH = 100;
char line[MAX_LENGTH];
std::cout << "请输入一行文本: ";
std::cin.getline(line, MAX_LENGTH);
std::cout << "你输入的文本是: " << line << std::endl;
return 0;
}
扩展了解:C++ std::getline() 函数。
2.2.2.3、演示三:连续输入与输出(operator bool)
如何写连续输入?
// 单个元素循环输入
while (cin >> a)
{
// ...
}
// 多个元素循环输入
while (c >> a >> b >> c)
{
// ...
}
// 整行接收
while (cin >> str)
{
// ...
}
C语言中 while(scanf("%d",&a)!=EOF)
尚且还好理解,如何理解这里的while (cin >> a)
?
在C++中,>>
是输入流运算符,用于从输入流中提取数据。我们使用它时,实际上是在调用 cin
的成员函数 operator>>
,该函数尝试从标准输入(通常是键盘)读取数据并将其存储到对应变量中。
//istream& operator>> (int& val);
cin >> a
等价于operator>>(a);
//istream& operator>> (istream& is, string& str);
cin >> str
等价于operator>>(cin,str);
operator>>
函数返回一个 istream
类型的引用,允许进行多组输入操作链接在一起(operator<<
同理。)
c >> a >> b >> c
在if
语句或while
循环的条件中,很多时候我们需要知道流是否处于有效状态,以便决定是否继续读取输入或执行其他操作。
为了实现这一点,istream
类在C++98和C++03标准中重载了类型转换运算符operator void*
。这个运算符允许istream
对象在需要布尔值时被隐式转换为void*
类型。相关链接:ios::operator bool
转换的规则是:
1、如果流处于有效状态(即,没有遇到错误),则转换为非空指针(通常是this指针的转换,表示流对象本身的地址)。
2、如果流处于错误状态,则转换为空指针(nullptr,在C++11及以后的标准中;在C++98和C++03中,使用字面量0或NULL)。
所以,结合条件判断的规则,0为假,非0为真。当我们没有遇到错误时,经过void*()
隐式类型转换,获得一个非空指针,自然while条件为真,继续读取数据。
C++11标准引入了显式的operator bool
成员函数到istream
类中。用以评估流的状态:
如果流处于有效状态,operator bool返回true。
如果流处于错误状态,operator bool返回false。
int i;
while (std::cin >> i) {
// 如果成功从std::cin读取到i,则执行循环体
}
所以,在这个例子中,std::cin >> i
尝试从标准输入读取一个整数到变量i
中。如果读取成功,std::cin
会保持有效状态,并且operator bool
会返回true
,导致循环继续。
2.2.2.4、演示四:自定义类型转换成内置类型(类型转换运算符)
1)、引入
在以前,我们曾介绍过,单参数构造函数如果没有被声明为explicit
,其支持隐式类型转换(温故)
class A
{
public:
A(int a)
:_a(a)
{
cout << "A(int a)" << endl;
}
private:
int _a;
};
int main()
{
A aa = 1;// 1 是 int 类型,先用1构造一份临时对象(类类型 A tmp),再用它去拷贝构造 aa
// 编译器优化后:隐式类型转换+拷贝构造-->直接构造(直接用1构造aa)
return 0;
}
在那里,A aa = 1
是从内置类型转换为自定义类型。那么,我们是否可以将自定义类型转换为内置类型?
int i = aa;// 这种转换是否可行?
当然可以,只需要重载相应的类型转换运算符即可:
class A
{
public:
A(int a)
:_a(a)
{
cout << "A(int a)" << endl;
}
operator int()
{
cout << "operator int()" << endl;
return _a;
}
private:
int _a;
};
理解了上述例子,operator bool
也是同理。
2)、扩展学习
源自:C++ Primer第五版(建议自己查阅,这里仅作为补充内容)。
相关博文:重载、类型转换与运算符
隐式转换: 当重载了类型转换运算符后,如果编译器在需要另一种类型的值时遇到了该类的对象,并且这种类型与重载的运算符匹配,那么编译器会自动调用该运算符进行类型转换。这种转换是隐式的,即不需要显式地指定转换类型。
显式转换: 虽然重载了类型转换运算符后可以进行隐式转换,但在某些情况下,为了代码的清晰性和可读性,建议使用显式转换(C++的四种类型转换,比如static_cast
)
其它演示·无成员变量的情况:即使类中没有成员变量,类型转换运算符仍然可以正常工作。这是因为类型转换运算符的转换逻辑是基于类的成员函数实现的。在没有成员变量的情况下,类型转换运算符可能只是返回一个常量值、执行某种计算或调用其他成员函数来生成要转换的值。
2.3、C++文件IO流
2.3.1、文件流对象
1)、基本介绍
C++ 提供了丰富的文件输入/输出(IO)流操作,通过标准库 <fstream>
来实现文件的读写操作。文件流主要分为三种类型:输入文件流 (ifstream
)、输出文件流 (ofstream
) 和输入输出文件流 (fstream
)。
相关链接:fstream
这些类型提供的操作与我们之前已经使用过的对象cin
和cout
的操作一样。此外,由于其继承自iostream
类,因此,可以用IO运算符(<<
和>>
)来读写文件,也可以用getline
等。
实际上,C++ 中的文件读写操作与和C语言、或者Linux下学习到的都是一个套路,只不过C++中是以“对象”进行操作的。采用文件流对象操作文件的一般步骤:
1、 定义一个文件流对象。这三个文件流对象都可以通过它们各自的构造函数或 open
成员函数,以适当的模式打开文件。
ifstream ifile; // 定义了一个输入文件流对象,用于从文件中读取数据。
ofstream ofile; // 定义了一个输出文件流对象,用于向文件中写入数据。
fstream iofile; // 定义了一个输入输出流对象,同时支持文件的读写操作。
2、使用文件流对象的成员函数打开一个磁盘文件,使得文件流对象和磁盘文件之间建立联系。
3、使用提取和插入运算符对文件进行读写操作,或使用成员函数进行读写。
4、关闭文件。
2)、构造函数
这里我们简单观察一下fstream
对象的构造函数(了解一个其它两个也是类似的):
一个演示:
int main() {
ofstream outFile("output.txt");//创建一个ofstream对象,打开output.txt,用于写入
//……文件操作
outFile.close();//关闭文件
ifstream inFile;//创建一个ofstream对象
inFile.open("input.txt");// 打开input.txt,用于读取
//……文件操作
inFile.close();//关闭文件
return 0;
}
这里简单介绍一下文件打开的模式(和C/Linux中一样,如果是多模式,中间用|
进行组合),以下是一些常见的文件打开模式:
ios_base::in:以只读方式打开文件。
ios_base::out:以只写方式打开文件。如果文件已存在,则会被截断;如果文件不存在,则会创建一个新文件。
ios_base::binary:以二进制模式打开文件。在二进制模式下,读写操作会以字节为单位进行,而不会进行任何转换或格式化。
ios_base::ate:打开文件并立即将文件指针定位到文件末尾。
ios_base::app:以追加方式打开文件。写入的数据会被追加到文件末尾,而不是覆盖原有内容。
ios_base::trunc:如果文件已存在,则在打开文件时将其长度截断为0。这通常与ios_base::out模式一起使用。
3)、析构函数
说明: 当文件流对象(如ifstream、ofstream或fstream对象)被销毁时,其析构函数会被调用。析构函数会检查与该对象关联的文件是否仍然打开。如果是,析构函数会关闭文件,并释放与文件操作相关的任何资源。
自动析构: 在大多数情况下,由于文件流对象(如ifstream、ofstream和fstream)的析构函数会自动关闭与其关联的文件,因此显式调用close()
成员函数不是必需的。
手动析构与关闭文件: 在某些情况下,我们可能希望在对象生命周期结束之前手动关闭文件。这可以通过调用文件流对象的close()
成员函数来实现。一旦文件被关闭,就不能再通过该文件流对象进行读写操作,直到文件被重新打开。
2.3.2、文件流对象的读写操作
2.3.2.1、基本介绍
1)、文本读写&&二进制读写
在C++中,文件的读写操作分为文本读写和二进制读写两种。这两种方式在数据的存储格式、处理效率和适用场景上有所不同。
如何理解二进制文件和文本文件?(温故)
2)、读写操作概述
举例读取:
对于输入文件流对象,可以使用流提取运算符(如>>)来读取基本数据类型(如整数、浮点数等)。
可以使用getline成员函数来读取一行文本。
可以使用read成员函数来读取二进制数据。
举例写入:
对于输出文件流对象,可以使用流插入运算符(如<<)来写入基本数据类型和文本。
可以使用write成员函数来写入二进制数据。
2.3.2.2、相关演示
1)、文本读写举例
演示一:
int main()
{
fstream iof("text.cpp");
char ch = iof.get();
while (iof)//实际上是在调用了 iof 的 operator!()(逻辑非运算符的重载)
{
cout << ch;
ch = iof.get();
}
return 0;
}
演示二:相比于上述的调用函数,在C++中我们一般会使用流插入运算符“<<
”和流提取运算符“>>”
实现对磁盘文件的读写。这是C++文件流的优势,对于内置类型和自定义类型都使用一样的方式去流插入和流提取数据(前提是自定义类型中重载了operator >>
和 operator<<
)。
#include <iostream>
#include <fstream>
class Date {
public:
int year, month, day;
// 构造函数
Date(int y = 0, int m = 0, int d = 0) : year(y), month(m), day(d) {}
// 重载输出运算符
friend std::ostream& operator<<(std::ostream& os, const Date& d) {
os << d.year << ' ' << d.month << ' ' << d.day;
return os;
}
// 重载输入运算符
friend std::istream& operator>>(std::istream& is, Date& d) {
is >> d.year >> d.month >> d.day;
// 这里可以添加额外的错误检查逻辑
return is;
}
};
int main() {
int ii = 1001;
double dd = 3.1415926;
string ss = "俱往矣,数风流人物,还看今朝。";
Date aa(1949, 10, 01);
// 写入到文件
std::ofstream ofs("date.txt");
if (ofs) {
ofs << ii << endl;
ofs << dd << endl;
ofs << ss << endl;
ofs << aa << endl;
ofs.close();
}
else {
std::cerr << "无法打开文件以进行写入。" << std::endl;
return 1;
}
// 从文件读取
std::ifstream ifs("date.txt");
if (ifs) {
int ii2;
double dd2;
string ss2;
Date aa2;
ifs >> ii2 >> dd2 >> ss2 >> aa2;
cout << ii2 << endl;
cout << dd2 << endl;
cout << ss2 << endl;
cout << aa2.year <<"年" << aa2.month << "月" <<aa2.day << "日" << endl;
ifs.close();
}
else {
std::cerr << "无法打开文件以进行读取。" << std::endl;
return 1;
}
return 0;
}
演示结果如下:
2)、二进制读写演示
二进制文件的读写主要用成员函数 read
和 write
来实现。
文档查阅:ostream::write、istream::read
演示代码如下:
#include <iostream>
#include <fstream>
using namespace std;
struct Character
{
//string name;//昵称
char name[64];//昵称
int grade;//等级
char career[10];//角色
int id;//编号
};
class Create
{
public:
Create(const char* filename = "some.txt")
:_filename(filename)
{}
void WriteBin(const Character& info)
{
ofstream ofs(_filename, ios_base::out | ios_base::binary);
ofs.write((char*)&info, sizeof(info));//ostream& write (const char* s, streamsize n);
}
void ReadBin(Character& info)
{
ifstream ifs(_filename, ios_base::in | ios_base::binary);
ifs.read((char*)&info, sizeof(info));//istream& read (char* s, streamsize n);
}
private:
string _filename;//文件路径
};
int main()
{
//二进制写入
Character winfo = { "把酒祝东风且共从容",79,"武当",111222333 };
Create().WriteBin(winfo);
//二进制读出
Character rinfo;
Create().ReadBin(rinfo);
cout << rinfo.name << endl;
cout << rinfo.grade << endl;
cout << rinfo.career << endl;
cout << rinfo.id << endl;
//string s = "把酒祝东风且共从容";
return 0;
}
演示一: 在使用read和write成员函数时,需要确保数据缓冲区的大小足够容纳要读取或写入的数据。否则,可能会导致数据丢失或程序崩溃。此外,还需要注意文件指针的位置。在读取或写入数据之前,应该确保文件指针指向正确的位置。否则,可能会导致读取到错误的数据或写入到错误的位置。
演示二:二进制读写的一个潜在缺陷,特别是当涉及到包含指针或动态内存分配的数据结构时。
说明:在使用二进制方式读写包含STL类型(如std::string、std::vector等等)的数据结构时,需要特别注意的一个问题。这些STL类型内部通常使用动态内存分配来存储数据,因此它们包含指向这些数据的指针。当我们以 二进制方式将这些对象写入文件时,实际上写入的是对象在内存中的表示,包括那些指针的值(即内存地址)。而当程序退出并重新运行时,或者在不同的进程或不同的计算机上运行时,这些内存地址将不再有效。因此,再次从文件中读取这些对象时,它们包含的指针将指向无效的内存位置,即所谓的“野指针”。
当然,以上写法只是简单演示,在实际中,为了使二进制 I/O 正常工作,常常会采用一些方法,比如序列化和反序列化等等。
2.4、C++字符IO流
2.4.1、基本介绍
sstream
是 C++ 标准库中的一个类,用于在内存中读写字符串流。它提供了几个用于类来处理字符串流,主要包括:
std::istringstream:用于从字符串中读取数据。
std::ostringstream:用于向字符串中写入数据。
std::stringstream:同时支持读写操作。
std::istringstream
用于从字符串中读取数据,就像从输入流(如文件)中读取数据一样。
std::ostringstream
用于向字符串中写入数据,就像向输出流(如文件)中写入数据一样。
std::stringstream
结合了输入流(std::istream)和输出流(std::ostream)的功能,使得它既可以向流中写入数据,也可以从流中读取数据。这些数据是以字符串的形式存储在内存中的。
一个简单演示, 使用std::stringstream
序列化和反序列化结构数据(以下假设了输入数据格式正确,在实际应用中,可能需要添加更多的处理)。
#include <iostream>
#include <sstream>
#include <string>
// 定义结构体
struct Person {
std::string name;
int age;
// 为了方便输出,我们重载<<运算符
friend std::ostream& operator<<(std::ostream& os, const Person& person) {
os << "Name: " << person.name << ", Age: " << person.age;
return os;
}
};
// 序列化函数
std::string serialize(const Person& person) {
std::stringstream ss;
ss << person.name << " " << person.age;
return ss.str();
}
// 反序列化函数
Person deserialize(const std::string& data) {
Person person;
std::stringstream ss(data);
ss >> person.name >> person.age;
return person;
}
int main() {
// 创建一个Person对象并序列化(结构体->字符串)
Person person1{ "周百川", 25 };
std::string serializedData = serialize(person1);
std::cout << "Serialized Data: " << serializedData << std::endl;
// 从序列化数据中反序列化回Person对象 (字符串->结构体)
Person person2 = deserialize(serializedData);
std::cout << "Deserialized Person: " << person2 << std::endl;
return 0;
}