C++除了面向对象部分, 其他还有:
- 模板(泛型编程)
- 类型转换和RTTI
- 异常机制
- IO
本文就说这4个方面.
引子
模板, 类型参数化, 这个在STL中用的比较多, 也容易把它和重载联系在一起.
异常机制, 本来是一种很优秀的设计(更好的服务于面向对象), 结果是大多数c++程序员不太爱用这个机制.
IO, c++的标准IO, 文件IO, 串IO是在c的基础上进行封装, 扩展后的产物, 比原来的printf, scanf等更加安全(多了编译时检查), 支持更加强大的格式控制; 然而就是由于扩充了太多东西, 所以用起来稍微有点儿复杂了(好吧, 我承认引入了相当的复杂度).
正文
模板
总结性的一句话: 模板可以看成参数的静多态, 一般用于表达逻辑结构相同, 但具体数据元素类型不同的数据对象的通用行为.
实现手段: 泛型
template
关键字告诉 C++编译器 我要开始泛型了, 你不要随便报错(其实就是进行两次编译, 第二次进行的是特化编译)
具体分类
* 函数模板(和重载有重合, 但是绝对不冲突)
* 类模板
函数模板
函数模板举例:1
2
3
4
5
6
7
8template <typename T>
void myswap(T &a, T &b)
{
T t;
t = a;
a = b;
b = t;
}
代替1
2
3
4
5
6
7
8
9
10
11
12
13void myswap(int &a, int &b)
{
int t = a;
a = b;
b = t;
}
void myswap(char &a, char &b)
{
char t = a;
a = b;
b = t;
}
调用:1
2myswap<float>(a, b); //显示类型调用
myswap(a, b); //自动数据类型推导(注意是严格匹配)
当然泛型参数也可以作为函数参数, 例如:
1 | template<typename T, typename T2> |
调用模板函数和普通函数的区别:
- 普通函数可以进行自动类型转换, 但是带有泛型参数的函数模板不行(必须严格匹配)
- 函数模板可以像普通函数一样被重载(函数重载和模板有重合, 但是大部分作用不同)
- C++编译器优先考虑普通函数(先考虑最优匹配(完美类型匹配), 如果两者都匹配, 则先考虑普通函数)
- 可以通过空模板实参列表的语法限定编译器只通过模板匹配, 即调用时加上
<>
, 例如’Max<>(a, b)’, 而不是Max(a,b)
完整案例: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
using namespace std;
int Max(int a, int b)
{
cout<<"int Max(int a, int b)"<<endl;
return a > b ? a : b;
}
template<typename T>
T Max(T a, T b)
{
cout<<"T Max(T a, T b)"<<endl;
return a > b ? a : b;
}
template<typename T>
T Max(T a, T b, T c)
{
cout<<"T Max(T a, T b, T c)"<<endl;
return Max(Max(a, b), c);
}
void main()
{
int a = 1;
int b = 2;
cout<<Max(a, b)<<endl; //当函数模板和普通函数都符合调用时,优先选择普通函数
cout<<Max<>(a, b)<<endl; //若显示使用函数模板,则使用<> 类型列表
cout<<Max(3.0, 4.0)<<endl; //如果 函数模板产生更好的匹配 使用函数模板
cout<<Max(5.0, 6.0, 7.0)<<endl; //重载
cout<<Max('a', 100)<<endl; //调用普通函数 可以隐式类型转换
system("pause");
return ;
}
编译器支持
泛型参数实现的模板机制, 需要C++编译器支持.
编译机制探究:
- 为什么函数模板可以和函数重载放在一块? (重载很简单, 就是编译时改名; 函数模板可以有多个重载, 它俩不冲突)
- C++编译器是如何提供函数模板机制的?(会针对存在的每种类型进行实参话编译么?)
编译器并不是把函数模板处理成能够处理任意类的函数(代价太大), 而是对函数模板进行两次编译:
- 在声明的地方对模板代码本身进行编译
- 在调用的地方对参数替换后的代码进行编译(编译器从函数模板通过具体类型产生不同的函数)
注明: 第二次就是特化编译, 产生具体的函数.
类模板
类模板一般用在容器类中比较多(类模板在表示如数组、 表、 图等数据结构显得特别重要, 这些数据结构的表示和算法不受所包含的元素类型的影响).
举个简单的类模板的例子: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
27template<typename T>
class A
{
public:
A(T t)
{
this->t = t;
}
T& getT()
{
return t;
}
public:
T t;
};
int main(void)
{
//模板了中如果使用了构造函数,则遵守以前的类的构造函数的调用规则
A<int> a(100);
a.getT();
printAA(a);
return ;
}
类模板还涉及到一些其他的问题, 特别是它和继承, 友元等扯在一起的时候, 以及类模板定义的位置.
继承问题
例如:1
2
3
4class A {};
class B: public A<int>
{
}结论:
- 子类从模板类继承的时候,需要让编译器知道, 父类的数据类型具体是什么.
- 如果不初始化父类的参数类型, 那么实例化的时候就完全无法确定实例化时父类对象的大小, 本例中
A<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
29class B : public A<int>
{
public:
B(int i) : A<int>(i) { }
void printB()
{
cout<<"A:"<<t<<endl;
}
};
void pintBB(B &b)/*作为函数参数的情况*/
{
b.printB();
}
void printAA(A<int> &a) //类模板做函数参数
{
a.getT();
}
void main()
{
A<int> a(100);
a.getT();
printAA(a);
B b(10);
b.printB();
return ;
}总结就是: 模板可以有层次, 一个类模板可以作为基类, 派生出派生模板类.
模板函数的实现位置
大致上应该有三种情况:- 所有的类模板函数写在类的内部(.hpp文件)
- 所有的类模板函数写在类的外部, 在一个 cpp 文件中
- 所有的类模板函数写在类的外部, 在不同的.h 和.cpp中 (分开写的时候.h要包含.cpp)
最好写在同一个文件中, 命名为.hpp, 具体参考 Boost库的写法就知道了.
模板和友元函数
友元函数中带有类型参数怎么办?
案例: (用友元函数重载<<
>>
)
// friend ostream& operator<<(ostream &out, Complex &c3) ;
声明友元函数
1
2
3
4template<typename T>
class Complex;
template<typename T>
Complex<T> mySub(Complex<T> &c1, Complex<T> &c2);类的内部声明
1
friend Complex<T> mySub<T>(Complex<T> &c1, Complex<T> &c2);
外部友元函数实现
1
2
3
4
5
6
7//实现的地方, 函数名后面不要再加 <T> 泛型参数了
template<typename T>
Complex<T> mySub(Complex<T> &c1, Complex<T> &c2)
{
Complex<T> tmp(c1.a - c2.a, c1.b-c2.b);
return tmp;
}友元函数调用
1
2Complex<int> c4 = mySub<int>(c1, c2);
cout<<c4;强调一点: 如果在类模板外定义成员函数, 应写成类模板形式:
1
2
3
4
5template <class 虚拟类型参数>
函数类型 类模板名<虚拟类型参数>::成员函数名(函数形参表列)
{
//TODO
}类模板和static
3句话就可以概括:- 类模板实例化出来的模板类, 该模板类的所有实例共享该模板类的static成员.
- 所有模板类static成员只是类模板的一个副本(即下次从类模板的编译出具体的模板类, static成员值不会变)
- 初始化也要带上泛型参数, 例如:
template<typename T> int Circle<T>::num = 0
.
关于类型转换
4种类型转换
(当然Boost库也提供了自己的)
最主要的, 清楚要转的变量, 类型转换前是什么类型, 类型转换后是什么类型. 转换有什么后果.
一般情况下, 不建议进行类型转换; 避免进行类型转换.
c 风格
强制类型转换(Type Cast)很简单, 不管什么类型的转换统统是:1
TYPE b = (TYPE)a;
c++风格
类型转换提供了 4 种类型转换操作符来应对不同场合的应用:- static_cast 静态类型转换, 如 int 转换成 char (但不用于指针类型)
- reinterpreter_cast 重新解释类型(主要用于指针类型)
- dynamic_cast 命名上理解是动态类型转换,如子类和父类之间的多态类型转换
- const_cast 字面上理解就是去 const 属性(或者加上const)
格式如下:
1
TYPE B = static_cast<TYPE>(a);
好处可能是, 有编译时或者运行时检查; 运行时出错还会跑出
bad_cast
异常.
总结:
static_cast和reinterpret_cast<>
- static_cast<>() 静态类型转换, 编译时 c++编译器会做类型检查;基本类型能转换, 包括类型截断, 但是不能转换指针类型
若不同类型的指针之间, 进行强制类型转换, 用 reinterpret_cast<>() 进行重新解释
一般性结论:
- c 语言中 能隐式类型转换的, 在 c++中可用 static_cast<>()进行类型转换. 因 C++编译器在编译检查一般都能通过;
- c 语言中不能隐式类型转换的, 在 c++中可以用 reinterpret_cast<>() 进行强行类型解释. (一般就是指不同类型的指针)
总结: static_cast<>()和 reinterpret_cast<>() 基本上把 C 语言中的 强制类型转换给覆盖. 并且reinterpret_cast<>()很难保证移植性.
- dynamic_cast 和const_cast
- dynamic_cast<>() 动态类型转换, 安全的基类和子类之间转换(基类指针转换赋值给子类指针); 运行时类型检查
- const_cast<>() 去除变量的只读属性(小心引起异常; 如果原来的变量被const修饰, 去掉const就妄想改变其value, 一般会出错)
RTTI
RTTI获取父类指针真正指向哪个子类.
Circle和Square皆是由Figure所衍生出来的子类别, 它们各有自己的draw()函数.
当C++ 提供了RTTI, 就可写个函数如下:1
2
3
4
5
6
7
8
9
10
11void drawing( Figure *p )
{
if( typeid(*p).name() == "Circle" ) {
((Circle*)p)->draw();
}
if( typeid(*p).name() == "Rectangle" ){
((Rectangle*)p)->draw();
}
}
此时typeid可以是非 virtual 的函数, 子类隐藏(shadow)了父类的实现.
如果C++ 并未提供RTTI, 则程序员毫无选择必须使用虚拟函数来支持 drawing() 函数的多态性.
将draw()宣告为虚拟函数, 调用写法如下:1
2
3
4void drawing(Figure *p)
{
p->draw();
}
RTTI部分还有其他内容, 个人用的不是太多.
异常机制
这一部分至少有2个点可以说:
- 异常处理这种思想(语言层面的错误处理)
- c++异常的使用惯例
- 栈的解旋
- 抛出对象的生命周期管理
- 自定义异常类的写法
- c++标准库的继承结构
异常思想
C语言中怎么处理异常呢?
比如在写本地套接字的时候, 封装监听函数: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/*
* Create a server endpoint of a connection.
* Returns fd if all OK, <0 on error.
* name : file_name
*/
int serv_listen(const char *name)
{
int fd, len, err, rval;
struct sockaddr_un un;
/* create a UNIX domain stream socket */
if ((fd = socket(AF_UNIX, SOCK_STREAM, 0)) < 0)
return(-1);
/* in case it already exists */
unlink(name);
/* fill in socket address structure */
memset(&un, 0, sizeof(un)); //bzero ok
un.sun_family = AF_UNIX;
strcpy(un.sun_path, name);
len = offsetof(struct sockaddr_un, sun_path) + strlen(name);
/* bind the name to the descriptor */
if (bind(fd, (struct sockaddr *)&un, len) < 0) {
rval = -2;
goto errout;
}
if (listen(fd, QLEN) < 0) { /* tell kernel we're a server */
rval = -3;
goto errout;
}
return(fd);
errout:
err = errno; /*tmp store in case of close error*/
close(fd);
errno = err;
return(rval);
}
/*调用逻辑如下*/
lfd = serv_listen("foo.socket");
if (lfd < 0) {
switch (lfd) {
case -3:perror("listen"); break;
case -2:perror("bind"); break;
case -1:perror("socket"); break;
}
exit(-1);
}
不同的状态, 对应不同的返回值, 并不是语言层面的支持
erro code
这种机制, 状态流和函数控制流是混合在一起的, 来回跳转需要额外的支持:
- 从流程控制来看
以前的程序逻辑, 流程控制, 基本上是围绕函数调用展开; 也就是会所控制流程主要是依据函数调用(基于栈结构的)
面向对象编程却要求对象之间有方向, 有目的的控制传动, 从一开始, 异常就是冲着改变程序控制结构, 以适应面向对象程序更有效地工作. - 从错误处理来看
虽然errocode也是一种报告错误(一般只能层层上报给相关调用者), 可以进行错误处理的机制, 但是它的错误处理控制流程和程序(函数调用)主逻辑严重耦合.
异常是另一种控制结构, 它依附于栈结构, 却可以同时设置多个异常类型作为捕获条件, 从而以类型匹配在栈机制中跳跃回馈(一般是自动层层上报, 也可以跨级跨函数上报).
并且最重要的: 异常的引发和异常的处理不必在同一个函数中, 底层着重于处理具体的流程, 出了异常就上报给上层, 上层处理决策, 错误处理(回馈).
c++编译器通过 throw 来产生对象, c++编译器再执行对应的 catch 分支, 相当于一个函数调用, 把实参传递给形参(所以这里也存在着变量声明周期的问题). - 捕捉方式
errcode是依据上层协议好的code; 异常机制基于类型匹配(它假定你使用的是纯面向对象的思路, 通过对象实例进行相关操作, 然后捕捉出错的实体对象), 但也可以在 catch 的时候引入对象.
总结性的一句:底层着重于处理具体的流程, 出了异常就上报给上层, 上层处理决策, 错误处理(回馈).
使用惯例
栈的解旋
最基本的调用方式就是 ‘try{} catch(类型 )’方式, catch的地方有点儿类似于 函数调用, 即涉及到函数实参往形参传递, 不过唯一不同的是, 这里是(严格)类型匹配, 而不是参数匹配, 换句话说, 不要指望有什么调用参数不满足的时候可以自动向上转换(异常捕捉严格按照类型匹配(和泛型编程类似, 不允许相容类型的隐式转换, 比如抛掷 char 类型用 int 型就捕捉不到))基本调用形式:
1
2
3
4
5
6
7
8
9
10try {
// do something
throw exception;
} catch (exception declaration) {
// excepetion handling code
// throw or no
} catch (exception declaration) {
// another excepetion handling code
// throw or no
}throw
语句一旦调用, 就相当于try块作用域结束(之后进入catch块, 就相当于一次函数调用), 那么在此期间定义的所有栈对象, 自动析构(顺序和定义的顺序相反).
如果异常流走下去, 不能再回头, 是否应该把对象析构掉? 异常抛出的代码块中, 对象会被析构, 称之为堆栈反解(栈解旋)
抛出对象的生命周期管理
上面已经明确说了, try里面出现异常, 或者主动抛出异常, 如果能够被catch捕获, 那么就相当于进行了一次函数调用. 此时catch子句如果有参数, 例如:(虽然通常是不许要参数, 直接按类型捕获的)1
2
3catch(ClassA &a){
//...
}但是这里面有一些规则:
- 捕获时按照值传递, 那么除了throw是产生临时对象外, 调用catch时也会进行拷贝构造. (临时对象还是会被析构)
- 多个捕获之间, 值传递和引用兼容, 但是指针和值传递不兼容(编译器会报错 ).
- 推荐使用引用, 这样catch中使用的就是throw时产生了对象. (不推荐使用值传递的方式, 因为会额外调用拷贝构造函数)
- 使用指针时, 如果throw时没有使用new关键字, 那么产生的临时栈对象会在进入catch时析构, 那么catch的指针变量其实是野指针(而抛出时使用new关键字, 那么在catch子句中就要手动delete, 总之不推荐catch指针变量).
- 自定义异常类的写法
一般抛出的异常是一系列关联的, 有联系的异常, 此时需要自定义异常类型, 并且设计好继承机构, 方面多态捕获. 其实也是为了简化该系列类型异常的捕获逻辑, 简单的例子如下:
1 | try{ |
但是如果你设计了很好的异常结构, 并且deal
是虚函数, 那么上面三个调用完全可以是一个(运行时根据抛出的具体对象决定):
1 | class Base : public std::exception |
一般what方法也要重写, 并且std::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
25class myexception : public std::exception
{
public:
myexception(std::string s) : exception(s.c_str())
{
}
virtual const char *what() const {
return exception::what();
}
};
void Func() {
try{
int *p = NULL;
if (p == NULL) {
throw myexception("p is NULL");
}
} catch(myexception e) {
printf("%s\n", e.what());
throw; //重新抛出异常,异常类型为myexception
} catch (std::exception e) {
printf("%s", e.what());
}
}
c++标准库的继承结构
这部分只要记住,std::exception
是所有相关异常的父类, 然后根据具体的情况, 抛出的是其子类.头文件 stdexcept, C++标准库所有函数抛出异常的基类, exception的接口定义如下:
1
2
3
4
5
6
7
8
9
10
11namespace std {
class exception
{
public:
exception() throw(); //不抛出任何异常
exception(const exception& e) throw();
exception& operator= (const exception& e) throw();
virtual ~exception() throw)();
virtual const char* what() const throw(); //返回异常的描述信息
};
}其他异常类的定义如下:
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
32namespace std {
class logic_error: public exception
{
public:
explicit logic_error(const string &what_arg);
};
class invalid_argument: public logic_error
{
public:
explicit invalid_argument(const string &what_arg);
};
class out_of_range: public logic_error
{
public:
explicit out_of_range(const string &what_arg);
};
class length_error: public logic_error
{
public:
explicit length_error(const string &what_arg);
};
class domain_error: public logic_error
{
public:
explicit domain_error(const string &what_arg);
};
}总结起来就是:
- exception类派生出了几个大子类: bad_cast, bad_alloc, bad_typeid, bad_exception, ios_base::failure(C++输入输出流中的错误), runtime_error, logic_error.
- bad_alloc: 在new头文件中定义了bad_alloc异常, 用于报告new操作符不能正确分配内存的情形.
- bad_cast: 当dynamic_cast失败时, 程序会抛出bad_cast异常类
其中runtime_error和logic_error也还有很多子类:
- runtime_error: overflow_error, underflow_error, range_error.
logic_error: domain_error, ivalid_argument, out_of_range, length_error.
逻辑错误主要包括invalid_argument, out_of_range, length_error, domain_error. 当函数接收到无效的实参会抛出
invaild_argument
异常, 如果函数接收到超出期望范围的实参, 会抛出out_of_range异常等等.
使用c++标准库定义的异常类如下:1
2
3
4
5
6
7
8
9
10
try{
throw invalid_argument("haha");
} catch(invalid_argument& in) {
cout<<"we catch invalid_argument"<<in.what()<<endl;
}catch(...){
cout<<"we catch unexpected error,exit"<<endl;
return 1;
}
声明注意
- 为了加强程序的可读性, 可以在函数声明中列出可能抛出的所有异常类型, 例如:
void func() throw (A, B, C , D);
//这个函数 func() 能够且只能抛出类型 A B C D 及其子类型的异常 - 如果在函数声明中没有包含异常接口声明, 则次函数可以抛掷任何类型的异常, 例如:
void func();
(最好不要声明) - 一个不抛掷任何类型异常的函数可以声明为:
void func() throw();
- 如果匹配的处理器未找到, 则运行函数 terminate 将被自动调用, 其缺省功能是调用 abort 终止程序
- 处理不了的异常, 可以在 catch 的最后一个分支, 使用 throw 语法, 向上扔(直至程序终止)
1
2
3
4
5
6
7
8
9
10
11
12
13
14class A{};
void f(){
if(...) throw A;
}
void g(){
try{
f();
}catch(B){
cout<<“exception B\n”;
}
}
int main(){
g();
}
throw A 将穿透函数 f, g 和 main, 抵达系统的最后一道防线——激发 terminate 函数. 该函数调用引起运行终止的 abort 函数. 其他补充:
可以修改默认的终止行为(set_terminate 函数在头文件 exception 中声明, 参数为函数指针
void(*)()
)1
2
3
4
void myTerminate(){cout<<“HereIsMyTerminate\n”;}
//
set_terminate(myTerminate);构造函数没有返回类型, 无法通过返回值来报告运行状态, 所以只通过一种非函数机制的途径, 即异常机制来解决构造函数的出错问题(一般是不建议在构造函数中抛出异常的)
- 为了加强程序的可读性, 可以在函数声明中列出可能抛出的所有异常类型, 例如:
IO
个人不是太乐意去说, 因为太复杂了.(第二天我还是把这部分内容补全了, 进行了分类整理, 贴出了开发中比较实用的代码)
比c io函数强的地方时, 安全(编译时类型检查 type safe); 功能丰富, 但是使用复杂(细节多).
(如果你使用习惯了c io api, 转变到使用c++ io api就需要一些时间过渡)
- 标准IO (标准输入输出) 头文件
, 包含抽象类 iostream ; 标准io类: istream, ostream, iostream. - 文件IO 头文件
, 包含: ifstream, ofstream, fstream - 串IO 输入字符串流
, 包含: istrstream, ostrstream, strstream.
主要内容就是:
- 继承关系图
- 流对象及iomanip, 缓存问题
- 文件IO及错误处理
- 二进制, 文本文件的读写
继承关系图
1
2
3
4
5
6
7
8---> basic_istream ---> ifstream
| |
ios_base ---> basic_ios ---> iostream ---> fstream
| |
---> basic_ostream ---> ofstream
/*上面的ifstream, ofstream, fstream的位置替换成
istringstream, ostringstream, stringstream,
就变成了串IO的继承关系图 */贴一幅图, 地址参考cplusplus.com
涉及到的头文件:
- ios
- istream
- ostream
- streambuf
- iostream
- fstream
- sstream
流对象
流对象不能复制, 例如:
1
2
3fstream fs1,fs2;
fs1 = fs2; //error
fstream fs3(fs2); //error流对象带缓冲(读写都要先经过缓冲区)
- 刷新缓冲的行为: 程序正常结束, 缓冲区满, endl和flush函数的调用,
unitbuf
的设置
- 刷新缓冲的行为: 程序正常结束, 缓冲区满, endl和flush函数的调用,
- 除了使用流对象类定义的方法输入输出, 还可以使用
<<
>>
进行输出和输入 标准输入cin
常见的用法:1
2
3int a, b;
char buf[1024]; //string buf;
cin >> a >> b >> buf; //读入字符串时遇到空格则止 12 23.5 aa bb cc dd也就是说, 用标准输入读取不了字符串中的空格, 所以出现了get函数作为弥补, 其重载如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15//读入一个字符并返回(包括回车、tab、空格等空白字符)
char get();
//经典用法:
char ch;
while( (ch=cin.get()) !=EOF) {
cout.put(ch);
}
/*读入一个字符,如果读取成功则返回非 0 值(真),
如失败(遇到文件结束符),则函数返回 0 值(假) */
istream& get(char&);
//经典用法
char ch;
while(cin.get(ch)) {
cout.put(ch);
}还有一个重要的重载, 该函数遇到delimiter并不会读入自定义buf中(而是放在流中), 所以下次读要从delimiter后一个位置开始, 否则函数直接返回.
1
2
3istream& get(char *, int ,char )
// istream& get(字符数组,字符个数 n,终止字符)
//或 istream & get(字符指针,字符个数 n,终止字符)即不会越过中止符(最多读取n-1个), 读不满时, 会阻塞等待.
和该函数相似的就是 getline() :
1
istream& getline(char *, int , char dilimeter = '\n')
这个函数最多也是读取n-1个字符, 不同的是, 它默认以换行为结束符, 并且从输入流中摘取 中止符(但不放入buf), 下次再从流中读取时, 中止符就没有了. 并且读不满时也会阻塞等待.
cin对象还有3个重要的方法:
ignore(streamsize n =1, int delim = EOF)
默认忽略一个字符(中止符是EOF)int peek()
流指针不移动, 但是只是取下一个位置的字符看看.(字符还在流中)istream& putback(char c)
从流中读出来的(忽略的也算), 再退回去到流中(给下一次读取)
cout
和cout一起的还有cerr(只能在屏幕上输出, 不能输出到文件, 无缓存, 实时刷新到屏幕), clog(有缓存, 其他和cerr类似)
输出的时候可以结合iomanip
进行很多格式控制, 例如:cout << hex << n << endl;
等- hex, oct, dec
- setw(n); //设置输出域宽
- setfill(c); //域宽不足的, 用指定字符c补
- setiosflags(ios::left)(左对齐)
- setiosflags(ios::right)(右对齐)
- setiosflags(ios::showpoint);// 强制显示小数点和尾0
- setiosflags(ios::showpos); //强制显示符号
- setiosflags(ios::scientific)(科学记数法)
- setiosflags(ios::fixed)(定点数)
- setprecision(n); //设置有效位数自动四舍五入
- setiosflags(ios::uppercase);//设置大写
- resetiosflags(ios::uppercase);//重新设置成小写
成员函数大概如下:
- ostream put(char)
- write() //一般用于二进制格式读写
流异常处理是在说文件IO是遇到的fail(), eof(), bad(), good().
文件IO及异常处理
主要涉及文本文件(注意换行符10和回车换行13,10的转换)和二进制文件(内存映像文件), 一个是顺序存取, 一个是随机存取(按字节或者块儿).- c语言中的IO问题:(必不可少的借助
FILE *
指针) - c++中, 对文件的操作是由文件流类完成的, 即ifstream, ofstream 和 fstream 类的对象. 对文件的操作过程可按照一下四步进行: 即定义文件流类的对象, 打开文件, 对文件进
行读写操作, 关闭文件. 打开文件
1
2void open(const unsigned char *filename, int mode,
int access=filebuf:openprot)而一般使用的时候, 直接使用对象打开
1
ifstream ifs("xxx.txt",ios::in);
文件的打开方式
见下图:ios::in|ios::out 表示以读/写方式打开文件
ios::in|ios:: binary 表示以二进制读方式打开文件
ios::out|ios:: binary 表示以二进制写方式打开文件
ios::in|ios::out|ios::binary 表示以二进制读/写方式打开文件
/默认/
对于 ifstream 流, mode 参数的默认值为 ios::in,
对于 ofstream 流, mode 的默 认值为 ios::out|ios::trunc,
对于 fstream 流, mode 的默认值为 ios::in|ios::out|ios::app1
2
3
4
5
6
7
8
9
10
11
12
13
14/*具体案例*/
ifstream ifs("xxx.txt",ios::in);
if(!ifs) {
cout<<"open error1"<<endl;
}
char buf[100];
if(ifs>>buf) {
cout<<buf<<endl;
}
ofstream ofs("yyy.txt",ios::out|ios::app);
if(!ofs) {
cout<<"open error2"<<endl;
}
ofs<<"abcefldkj"<<endl;不指明打开方式
ios::binary
时, 默认按照文本方式读写.- 文件关闭(调用流对象的close()方法)
流对象的状态判断(ios_base), 一般用于检测cin.
- eof() 读到文件尾返回true
- bad() 读写过程中出错, 返回true.(空间不足或者不是写打开)
- fail() 包含eof()和bad(), 并且格式错误也返回true
- good() 读写正常返回true
- clear() 清除所有标志位(该函数可以有参数)
使用案例可以是这样的:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17int ival;
// read cin and test only for EOF;
//loop is executed even if there are other IO failures
while (cin >> ival, !cin.eof()) {
// input stream is corrupted; bail out
if (cin.bad()) {
throw runtime_error("IO stream corrupted");
}
if (cin.fail()) {
cerr<< "bad data, try again";
cin.clear(istream::failbit);
continue; // get next input
}
// do real work
}
关于
operator void*() const
或者operator bool() const
或者bool operator!()const
转换函数的重载.1
2
3
4
5
6
7//函数在 while(cin)或是 if(cin)时被调用, 将流对象转换成 void *类型
operator void *() const;
//函数在 while(!cin)或是 if(!cin)时被调用, 将流对象转换成 bool 类型
bool operator!() const;
/*从其函数上看, iostream*/
while(cin) // while(!cin.fail()) //while the stream is OK
while(!cin) // while(cin.fail()) //while the stream is NOT OK文本文件读写的总结
写
1
2operator<<
osream put(int);读
1
2
3
4
5operator>>()
int get();
istream& get(int);
istream & get(char*,int n, char deli);
istream& getline(char *, int n);案例
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
using namespace std;
int main()
{
fstream ifs("src.txt",ios::in);
if(!ifs) {
cout<<"open error"<<endl;
return -1;
}
fstream ofs("dest.txt",ios::out|ios::trunc);
if(!ofs) {
cout<<"open error"<<endl;
return -1;
}
char buf[1024];
while(ifs.get(buf,1024,'\n')) {
while(ifs.peek() == '\n') {
ifs.ignore();
}
ofs<<buf<<endl;
}
ifs.close();
ofs.close();
return 0;
}
二进制文件读写
流对象函数
1
2ostream & write(const char * buffer,int len);
istream & read(char * buff, int len);- 文件指针操作
1
2
3
4
5
6
7
8
9
10
11//对于输入输出文件, p和g可以任意使用
tellg();
tellp();
seekg(绝对位置);
seekg(相对位置,参照位置);
seekp(绝对位置);
seekp(相对位置,参照位置);
//关于位置的定义(相对位置的负数表示从后往前)
ios::beg = 0 相对于文件头
ios::cur = 1 相对于当前位置
ios::end = 2 相对于文件尾
- 文件指针操作
案例
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
using namespace std;
struct student
{
int num;
char name[20];
float score;
};
int main(void)
{
int i;
student stud[5]={
1001,"xxx",85,
1002,"yyy",97.5,
1004,"zzz",54,
1006,"111",76.5,
1010,"2222",96
};
fstream iofile("stud.dat",
ios::in|ios::out|ios::binary);
if(!iofile) {
cerr<<"open error!"<<endl;
abort();
}
for(i=0;i<5;i++) {
iofile.write((char *)&stud[i], sizeof(stud[i]));
}
student stud1[5];
for(i=0;i<5;i=i+2) {
iofile.seekg(i*sizeof(stud[i]), ios::beg);
iofile.read((char *)&stud1[i/2],sizeof(stud1[i]));
cout<<stud1[i/2].num<<" "
<<stud1[i/2].name<<" "<<stud1[i/2].score<<endl;
}
cout<<endl;
//modify stud[2]
stud[2].num=1012;
strcpy(stud[2].name,"Wu");
stud[2].score=60;
iofile.seekp(2*sizeof(stud[0]),ios::beg);
iofile.write((char *)&stud[2],sizeof(stud[2]));
//read again
iofile.seekg(0,ios::beg);
for(i=0; i<5; i++) {
iofile.read((char *)&stud[i],sizeof(stud[i]));
cout<<stud[i].num<<" "
<<stud[i].name<<" "<<stud[i].score<<endl;
}
iofile.close();
return 0;
}
好了, IO部分也就差不多这样了.
- c语言中的IO问题:(必不可少的借助
尾巴
几乎花了一个上午在写这部分内容(全部写完IO花了2天), 说起来不觉得多, 写起来还真有点儿多.
人家说, “写一辈子程序, 看一辈子c++书”, c++确实有点儿复杂; 你不能老想着什么都要语言机制解决, 这样语言会越来越大, 越来越复杂.Java则是有选择的扩充, 该舍弃的舍弃; 只把好用的拿出来. 毕竟你不能假设所有人都是高手吧? (为了推广, 不能总是搞很小众的东西;大众能接收的是小众的东西)
就这样吧, 其他内容请参考 《c++ primer 5e》, 《C++标准库自修手册》, 官方文档.