1 变量的初始化
1.1变量的初始化方式
C++支持两种形式的变量初始化:复制初始化和直接初始化。复制初始化的语法符号用等号(=),直接初始化的语法符号是用括号把初始化的值括起来:
int ival = 1024;//复制初始化
int ival(1024); //直接初始化
注意: C++中初始化不是赋值!初始化指创建变量并给他赋初值,而赋值则是指擦出对象的当前值并用新值代替。
直接初始化语法更灵活且效率更高。
1.2内置类型变量的初始化
内置类型的变量是否自动初始化取决与变量定义的位置,在函数体外定义的变量都会被自动初始化为0,在函数体内定义的变量则不进行自动初始化。
注意: 静态变量若没有初始化,则不管是它在函数体内还是体外,都初始化为0。
- 普通变量:
- 初始化的普通全局变量放在目标文件的.data区,该类型变量的初始值因为在代码中已经给出了,所以该值最后也会保存在目标文件(存放在磁盘上的文件)的.data区中;
- 未初始化的普通全局变量放在.bss区,该类型的变量因为没有初始值,因此在目标文件中没有存放该变量的初始值,而是在目标文件加载到内存中的时候会自动赋初值0;
- 函数体内的普通变量不在目标文件中,因为函数体内的变量是在函数调用时在内存的函数栈帧中分配的空间,且不会自动赋初值。
- 静态变量:
- 初始化的全局静态变量放在目标文件的.data区,其初始值就存在.data中。
- 未初始化的静态全局静态变量放在.bss区,其没有初始值,而是在目标文件加载到内存中的时候,系统自动赋初值0;
- 函数体类的静态变量,这是特殊情况,和函数体内的普通变量不一样,该变量会存放在目标文件的.data或者.bss区中,如果在函数体内已经初始化了,就放在.data区中,若没有则放在.bss中。
1.3变量的定义与声明
- 变量的定义
- 用于为变量分配存储空间,还可以为变量指定初始值。
变量的声明
- 用户向程序标明变量的类型和名字,通过extern关键字来声明变量。
注意:定义也是声明,但声明不是定义!
例如
int a;//是变量的定义,同时也是变量的声明
int a = 1;//变量的定义,并复制初始化,当然也是变量的声明
int a(1);//变量的定义,并直接初始化,当然也是变量的声明
extern int a;//只是变量的声明
extern int a = 1;//是变量的定义,因为赋值要分配空间所以是定义,当然也是声明;
extern int a(1);//变量的定义,并直接初始化
2 头文件
2.1 头文件中内容
头文件中一般包含的是变量和函数的声明;而不是用于定义,但有三个例外。
- 头文件中可以定义类
- 在一个给定的源文件中,一个类只能定义一次,如果在多个文件中定义一个类,那么每个文件中的类定义必须完全相同,而且都只能出现一次,所以将类的定义放在头文件中可以保证这点
- 值在编译时候就知道的const对象
- inline函数
- 同类的定义一样,所有源文件中,其定义必须要完全相同。
2.2 避免多重包含
源文件A中包含了string头文件,也包含的B头文件,但B中也包含了string头文件,这样string头文件在A中就包含了2次。(没问题,编译通过,也能正常运行,只要不出现下面粗体字的问题即可)
因此在设计头文件的时候,应该使其可以多次包含在同一源文件中,这点很重要,我们必须保证多次包含同一头文件不会引起该头文件定义的类和对象被多次定义。 使用头文件保护符可以避免在已经见到头文件的情况下重新处理该头文件的内容。
#ifndef XXX_H
#define XXX_H
头文件中的类容
#endif
3 指针和const限定符
3.1 指向const对象的指针
const int *p;
int const *p;
- const对象的地址只能赋给指向const对象的指针,不能赋给指向非const对象的指针。
- 允许把非const对象的地址赋给指向const对象的指针,但不能通过该指针来修改指向的非const对象的值。
3.2 const指针
int *const p;
- 该指针的值不能变,和const变量一样,是常量了。
- 但该指针指向的对象的值可以变。
3.3 指向const对象的const指针
const int *const p;
int const *const p;
3.4 指针与typedef
typedef string *p;
const p sp;
理解为 const string * sp;即sp是指向const string对象的指针是错的!
应该为sp 是指向string对象的const指针;
因为const p时候,p表示string * 是一个指针,即const修饰的是一个指针,代表一个const指针,所以sp为指向string对象的const指针
这个地方比较特殊,源于const限定符在类型前和类型后的位置,一般这种typedef定义容易误导人,最后从右向左读。
4 new与delete
new操作是返回一个指针!
未提供默认构造函数的对象在new的时候必须显示的初始化
普通变量和对象
new操作
int *pi = new int;//分配一个整形,未初始化
int *pi = new int();//分配一个整形,初始化为0
int *pi = new int(1024);
string *ps = new string; == new string();//分配一个string对象空间,初始化为空,调用的是string类的默认构造函数,注意与普通变量的区别
string *ps = new string(参数);//分配一个string对象空间,调用string类相应的构造函数
const int *pci = new const int(数字);//分配一个const int对象空间,并初始化delete操作
delete pi; delete ps; delete pci;
动态数组
new操作
int *pia = new int[10];//申请10个int元素的数组,元素值未知 int *pia = new int[10]();//申请10个int元素的数组,并元素值都初始化为0 int *pia = new int[10](6);//error string *psa = new string[10];//申请10个string元素的数组,并元素值都初始化为空 //分配一个二维数组,大小不定 int **pia2 = new int *[10];//申请10个int *元素的数组,元素值未知; for(...){ pia2[i] = new int[x](); } //为指向const类型的指针非配空间 const int *pcia = new const int[10];//error const int *pcia = new const int[10]();//必须初始化
delete操作
//一维数组的delete delete [] pia; delete [] psa; //二维数组的delete for(...){ delete pia2[i]; } delete [] pia2; //const对象的一维数组的delete delete pcia;
注意:
1. new后,一定要判断是否分配成功
2. new后空间,一定要用delete释放;
3. delete后的指针变量,一定要注意悬垂指针问题,即野指针!
5 指针和多维数组
5.1 多维数组
严格的说c++中没有多维数组,通常多维数组是指数组的数组。
- 与普通数组一样,使用多维数组名时,实际上将其自动转换为指向该数组第一个元素的指针。
- 二维数组(数组的数组)的第一个元素是一个一维数组。
- 指向一维数组的指针: int (*p)[10];//p是指向一个包含10个int元素的一维数组的指针。
因此函数中形参为多维数组时,形参的定义为:
void fun(int a[][10],...); <==> void fun(int (*a)[10],...);
若不确定二维数组的大小,可以用二维指针形式,即到时候可以动态分配空间;
void fun(int **a,...){
a = new int *[数字1];//new返回的是指针,
if(a == NULL)...
for(i = 0; i < 数字1; i++){
a[i] = new int[数字2];
if(a[i] == NULL)...
}
}
5.2 易混淆的指针定义
int *p;//指针,指向一个整形变量的指针
int *p[10];//指针数组,每个元素都是指向一个整形变量的指针构成的数组。(可以把&p 赋给一个二维指针,如上)
int (*p)[10];//数组指针,指向一个包含10个元素的数组的指针( p+1指向下一个数组,*p:一维数组的首址)
int **p;//二维指针,指向指针的指针
5.3 用typedef简化指向多维数组的指针
typedef可以使指向多维数组元素的指针更易读、写和理解。
typedef int array[10];
array *pa = a;
for(; pa != a+3 ; pa++){
for(int *q = *pa; q != *pa + 4; q++)
cout << *q << endl;
}
注意: int(*p)[10]; *p:表示一维数组中首元素的地址!
6 类型转换
C++中提供隐式类型转换和显示类型转换,其中隐式类型转换是由编译器自动进行的,以尽可能防止精度损失为原则。
6.1 算术转换
- 对于所有比int小的整型,包含bool,char,unsigned char,short,unsigned short,如果该类型的所有可能的值都能包容在int内,那么它们就会被提升到int型,否则都会被提升到unsigned int
- long和unsigned int的转换也是一样,只要机器上的long足够表示unsigned int型的所有值,就将unsigned int转换为long,否则都转换为unsigned long
6.2其他隐式转换
- 指针转换
- 数组在函数参数传递过程中自动转换成指向数组第一个元素的指针
- 任何指针类型都可以转换成void *类型
- 转换为bool类型
- 非0值转换成true,0值转换成false
- 转换成const对象
- 非const对象初始化const对象的引用时,非const对象转换为const对象
- 还可以将非const对象的地址转换成const类型的指针
6.3显示转换
也称强制类型转换,一般形式为 cast-name< type >(expression);
- cast-name:static_cast、dynamic_cast、const_cast 、reinterpret_cast 。
- type:为转换的目标类型。
- expression:指定在什么上面执行强制类型转换
6.3.1 dynamic_cast
支持运行时类型识别(RTTI)。和虚函数有关。
即程序能够使用基类的指针或引用来检索这些指针或引用所指向的实际派生类型。
通过以下两个操作符提供RTTI:
(1)dynamic_cast
dynamic_cast操作符,将基类类型的指针或引用安全的转换为派生类类型的指针或引用。
注意:基类包含虚函数才有效,否则编译会提示不是多态的错误,编译不通过!
与其他强制类型转换不同,dynamic_cast操作符一次执行两个操作
- 验证被请求的转换是否有效,expression必须为0或者指向一个对象,且如果绑定到引用或指针的对象(即expression)不是目标类型的对象(即type),则失败,返回0值
- 只有转换有效,操作符才实际进行转换。
例如:
class Child继承于class Base;
Base *bptr = new Child();
Child *cptr = dynamic_cast<Child *>( bptr);
或者引用
try{
Base &b = Child c;
Child &cc = dynamic_cast<Child &>( b);
}
catch(bad_cast){...}
(2)typeid
- 形式为 typeid(expression);
- 必须包含< typeinfo >头文件。
typeid操作符使程序能够问一个表达式(expression):你是什么类型,返回值是一个名为type_info的标准库类型的对象的引用。
注意:当基类包含虚函数才返回动态信息,否则就按普通的静态编译时的情况来处理
Base *bp;
Child *cp;
if(typeid(*bp) == typeid(*cp)){...};
if(typeid(*bp) == typeid(Child);
//如果基类包含虚函数,则两个if都成立
//如果基类不包含虚函数,则连个if都不成立,因为typeid(*bp)返回的是带有父类信息的type_info对象的引用。
6.3.2 const_cast
将要转换的const表达式的const性质去掉,或者给非const表达式添加const性质。即取消和设置const状态,只能是指针和引用类型。
void fun(int *pa){...}
int a = 1;
const int *pb = &a;
fun(b);//error
fun(const_cast<int*>(b));//ok
6.3.3 static_cast
该操作符作用就和老式的强制类型转换(括号里加类型)的作用一样。
double d = 3.14;
int a = (int)d;//老式强制类型转换
int a = static_cast<int>(d);
7 try和异常处理
异常就是运行时出现的不正常,要求程序立即处理。包含初错误检测(throw)和错误处理(try)两部分,其中错误检测还必须指出具体出现什么问题。
7.1 throw表达式
throw 对象;
错误检测部分使用该表达式说明正常代码流程中遇到了不可处理的错误,需要交给错误处理部分去处理。可以说throw引发了异常条件。
异常一般是在代码中显示的抛出,抛出什么样的异常也是由编写者决定,但一般是出什么问题就抛出那个问题对应的异常类型
if(....)
throw 异常类(...);//生成一个异常类的对象,然后throw出去
...
当执行了throw语句后,throw之后的所有代码将不会执行,这类似与return的功能。
7.2 try块
try{
正常处理的代码(一般包含 throw 异常对象;)
}
catch (异常类 异常对象1){...}
catch (异常类 异常对象2){...}
...
一般catch(异常对象){}的括号中会打印出出错的信息,这就用到了异常对象的what()方法,异常对象.what();
- 一个try块可能调用了包含另一个try块的函数,他的try块又调用了包含有try块的另一个函数。
- 寻找处理代码的过程与函数调用链刚好相反。抛出一个异常时,首先搜索的是抛出该异常的函数,如果没有找到匹配的catch,则直接终止该函数的执行,并在调用这个函数的函数中寻找相匹配的catch。如果仍然没有找到对应的catch,这该函数同样要终止,以此类推,直到找到相匹配的catch。
- 如果不存在处理该异常的catch,程序的运行就要转跳到名为terminate的标准库函数,该函数在exception头文件中定义,通常该函数将导致程序的非正常退出。
7.3 标准异常类
C++标准库定义了一组类,用于报告在标准库中的函数遇到的问题。我们可以在代码中使用这些标准异常类。
- stdexcept头文件定义了几种常见的异常。其中exception这个异常类最常见。
- new头文件定义了bad_alloc异常类型,提供无法非配内存而由new抛出的异常。
- type_info头文件定义了 bad_cast异常类。
< stdexcept >头文件中定义的标准异常类:
异常类型 | 对应的异常问题 |
---|---|
exception | 最常见的问题 |
runtime_error | 运行时错误:仅在运行时才能检测到的问题 |
range_error | 运行时错误:生成的结果超出了有意义的值域范围 |
overflow_error | 运行时错误:计算上溢 |
underflow_error | 运行时错误:计算下溢 |
logic_error | 逻辑错误:刻在运行签检测到的问题 |
domain_error | 逻辑错误:参数的结果值不存在 |
invalid_argument | 逻辑错误:不合适的参数 |
lengh_error | 逻辑错误:试图生成一个超出该类型最大长度的对象 |
out_of_range | 逻辑错误:使用一个超出有效范围的值 |
标准异常类的继承层次图如下:
标准库异常类只提供了很少的操作,包括创建、复制异常类型对象和异常类型对象的赋值。
- exception、bad_alloc和bad_cast类型只定义了默认构造函数(没有参数的构造函数),无法在创建这些类型的对象时为它们提供初始值。
- 其他的异常类型则只定义了一个使用string初始化式的够函数,当需要定义这些异常类型的对象时,必须提供一个string参数,以用来提供错误的信息。
- 异常类型只定义了一个名为what的操作。该函数不需要任何参数,并返回const char* 类型的值
7.4 抛出类类型的异常
(1)注意一个问题:
throw 异常类(…); 生成一个异常类的对象,然后throw出去,执行完这个操作后,如果包含有该throw语句的函数中没有对应的catch,那么该函数就终止了,也就是函数中的所有局部变量都会释放(函数中new处理的内存不释放),注意这里的异常类对象也是局部的,它也会被释放掉吗,它还没有被外面函数的catch捕获就释放了,怎么办?
- 其实不用担心,虽然说throw 异常类(…);是生成了一个局部的对象,但是编译器会把这个异常类对象特殊处理,一直会保留它,即便包含他的函数终止了,直到被对应的catch捕捉到,然后在catch处理完后释放掉。
- catch (异常类 对象名),上面提到 throw抛出的局部异常对象会一直保留,直到对应的catch捕捉到,catch捕捉到的时候,实际是采用和函数调用中值传递类似的形式,即将throw处理的局部异常对象的值赋给 catch后面表达式中的: 异常类 对象名所代表的新的异常对象中去了。
(2)抛出对像的类型
当抛出一个对象时候,被抛出对象的静态编译时的类型将决定异常对象的类型。
range_error r("error");
throw r;//抛出的异常对象的类型为range_error类型
exception *p = &r;
throw *p;//抛出的异常对象类型为指针p静态编译时的类型,即exception类型,即只抛出子类的基类部分。
7.5 捕获异常
catch(异常类 异常对象名)中括号里的内容称为异常说明符。
(1)匹配查找
在查找匹配的catch期间,找到的catch不必是与抛出的异常对象最匹配的那个catch,相反,将选择第一个找到的可以处理该抛出的异常对象的catch。因此在catch字句列表中,最匹配的catch必须最先出现。
抛出的异常与catch异常说明符之间的匹配规则比函数实参形参的匹配规则更严格,大多数转换都不允许。
- 允许从非const到const的转换。即,非const对象的throw可以与指定接受const引用的catch匹配。
- 允许从派生类类型到基类类型的转换。(即如果throw出来的是子异常类对象,而catch的异常说明符是父异常对象,那么catch匹配成功,但catch处理中如果需要使用异常说明符(catch中的异常对象)中的操作,则只能使用catch中的那个异常对象(这里是throw出来的子异常的父异常,也就是子异常类的基类部分),不能使用throw过来的子异常对象)
- 将数组转换为指向数组类型的指针,将函数转换为指向函数类型的适当指针。
- 其他的转换都不运行,如算术转换等。
(2)值传递
- 如果catch中异常说明符是引用类型,那么该catch中的异常说明符是throw处理的异常对象的别名。(完全匹配的情况下,不完全匹配下,如catch父类对象的引用和throw子类对象,则catch中引用是子类对象中基类部分的别名)
- 如果catch中异常说明符不是引用类型,则catch的异常说明符是throw出来的异常对象的副本。(完全匹配的情况下,不完全匹配下,如catch父类对象和throw子类对象,则catch中引用是子类对象中基类部分)
- catch中的异常说明符可以是引用类型,也可以是类类型,不能是指针类型,因为throw出来的是一个对象,不是对象的地址,因此catch接收的是一个对象,不是对象的地址!
7.6 自定义异常类
自定义异常类,可以继承于exception类
该类里面可以自定义一些方法,用于输出错误信息。
try{
if(...)
throw 自定义异常类(参数,可以是错误信息);//生成一个自定义异常类的对象
...
}
catch (自定义异常类 对象1){
对象1.方法(...);
}
catch(父类exception 对象2){
对象2.what();
}
catch(...){//捕捉所有异常,相当与switch中的default
...
}
7.7 重新抛出异常
有可能单个catch不能完全处理一个异常。在进行了一些校正行动后,catch可能确定该异常必须由函数调用链中更上层的函数来处理,catch可以通过重新抛出将异常传递给函数调用链中更上一层的函数,重新抛出是后面不跟类型或表达式的一个throw:
throw;
空throw语句将重新抛出异常对象,他只能出现在catch或者从catch调用的函数中。如果在catch处理代码不活动时候碰到空throw,就会调用terminate函数。
- 虽然重新抛出不指定自己的异常,但仍然将一个异常对象沿链向上传递,被抛出的异常是原来的异常,不是catch形参。
- 当catch形参是基类类型时,重新抛出的实际类型取决于异常对象的动态类型,而不是catch形参(异常说明符)的静态类型。
通常throw;与catch(…)一起配合使用
catch(...){//catch(...)可以捕获任何异常
处理代码;
throw;
}
7.8异常说明
(1)异常说明的定义
异常说明:如果函数抛出异常,被抛出的异常将是包含在该说明中的一种,或者是从说明中的异常里派生出的类型。如果为空则表示该函数不会抛出异常,这样编译器知道后,可以执行被可能抛出异常的代码所抑制的优化了。
异常说明跟在函数形参表之后。由throw 加上括号构成,括号内可能为空,也可能是一个异常对象。
void fun(形参) throw();//函数声明
void fun(形参) throw()//函数定义
{
}
void fun1() throw(异常对象1,异常对象2..);
void fun1(形参) throw(异常对象1,异常对象2..)
{
}
- 如果不确定函数会抛出什么异常,可以采用异常说明。
- 如果确定函数不会抛出异常(抛不抛出异常是在代码中显示调用throw表达式的),可以用throw()空的异常说明,以便编译器优化。
如果函数运行过程中抛出的异常与函数定义和声明时指明的异常说明不符,那么异常机制会调用标准库函数unexpected函数,该函数会调用terminate终止程序。
上面的情况发生在函数运行的时候,在编译的时候,即便函数体里面有throw 别的异常对象的代码,编译器也不会报错。
void f() throw(){
throw exception();
}
编译的时候仍然可以通过,但运行时候,该函数会抛出exception类型的异常对象,此时它与异常说明中的异常类型不一致(异常说明中说该函数不会发生异常),因此会调用unexpected最后终止程序。
(2)异常说明与成员函数
与非成员函数一样,成员函数声明的异常说明跟在函数形参之后,如果是const成员函数,异常说明要在const之后
void fun(形参) const throw(空或异常对象);
(3)异常说明与析构函数
析构函数应该从不抛出异常,因为抛出异常后,调用的函数会终止,而函数里的局部对象会调用析构函数,如果此时析构函数再抛出异常会导致整个程序的非正常退出。
- 我们可以把析构函数后面声明一个空的异常说明,但这不是必须的,不声明也行。
- 如果我们把析构函数声明了异常说明,那么继承该类的派生类中的析构函数也必须声明异常说明
(4)异常说明与虚函数
如果基类中虚函数有异常说明,派生类中对应的函数也必须有异常说明,并且它的异常说明必须是基类中的子集。
8 函数
8.1引用形参
建议使用const 引用类型当形参
- 使用引用当形参,可以避免值传递
- 使用const修饰形参,可以避免修改实参值
8.2 vector和其他容器类型的形参
- 通常函数不应该有vector或者其他标准容器类型的形参,因为调用的时候采用值传递,将复制vector中的每一个元素。
- 因此建议将形参声明为引用类型,但更倾向于通过传递指向容器中需要处理的元素的迭代器来传递容器。
eg.
void fun(vector< int >::const_iterator beg , vector< int>::const_iterator end){
while(beg != end){
cout << *beg<<endl;
}
}
//const_iterator:指向的元素值不能变,类似指向常量的指针 const int *p
//const iterator:常迭代器,迭代器值不能变,类似于常指针 int *const p
8.3 数组形参
(1)多维数组详见5.1
(2)一维数组通过引用传递
和其他类型一样,数组形参可声明为数组的引用。如果形参是数组的引用,编译器不会将数组实参转化为指针,而是传递数组的引用本身。此时数组大小成为形参和实参类型的一部分,即编译器会检查数组实参的大小和形参的大小是否匹配:
void fun(int (&array)[3]){...}
int main(){
int a[3] = {0,1,2};
int b[4] = {0,1,2,3};
fun(a);//ok
fun(b);//error
}
================================
int &array[3]:和int *p[3]类似,表示array是一个包含3个引用的数组
int (&array)[3]:和int (*p)[3]类型,表示array是一个指向有3个int型元素的数组的引用。
多维数组的引用类型好像没有
8.4 const成员函数
对于类,才有const成员函数这一说法。
在类里面的形式:
void fun(形参) const;
void fun(形参) const{...}
const成员函数 | 能访问类中任何数据成员 | 不能修改任何数据成员 |
---|---|---|
普通成员函数 | 能访问类中任何数据成员 | 能修改出const数据成员外的任何数据成员 |
为什么const成员函数不能修改类中的任何数据呢?
- 我们知道除了static成员函数外,成员函数的形参中都隐藏着一个this指针(对象的地址)。
- 非const成员函数中,this指针的类型是一个指向类类型的const指针,可以改变this所指向的对象的值,但不能改变this的值。
- 而在const成员函数中,this指针会的类型是一个指向const类类型 的const指针,既不能改变this所指向的对象的值,也不能改变this值。
同样如果在const成员函数中返回*this,那么返回的将是一个const对象。
与const成员函数经常一起出现的是const对象
const 类名 对象名;//则该对象是常对象
const对象 | 只能调用类中const成员函数 |
---|---|
普通对象 | 能调用类中任何成员函数 |
8.5 重载函数
出现在相同作用域中的两个函数,如果具有相同的名字而形参列表不同,则成为重载函数(overloaded function)
- 如果两个函数定义的返回类型、函数名和形参表完全匹配,则将第二个函数声明视为第一个的重复定义。编译通不过。
- 如果两个函数函数名和形参表完全相同,但返回类型不同,则第二个声明是错误的,编译都通不过。
注意,形参与const形参的等同性仅适合与非引用形参,有const引用的形参函数和有非const引用形参的函数是不同的。类似地,如果函数带有指向const类型的指针形参,则与带有指向相同类型的非const对象的指针形参的函数不同
void fun(const int a); void fun(int a);//两者不能重载 --------------------------------------------------- void fun(const int &a); void fun(int &a) void fun(int a);//可以重载 --------------------------------------------------- void fun(const int *p); void fun(int *p); void fun(int p);//可以重载
基于成员函数是不是const型,可以“重载”一个成员函数
因为成员函数有一个隐含的this指针参数,在const型成员函数中,该this指针是(const 类类型 )const 指针,而普通成员函数中,this类型为(类类型)const指针。而const指针也是指针,即一个是(const 类型 )修饰的指针,一个是 (普通类型)修饰的指针,符合const的等同性仅限于非引用非指针类型。
class A{
public:
void f() const;
void f();
}
A a;
a.f();//调用的是非const的f();
const A b;
b.f();//调用的是const的f();
8.6 函数指针
(1)指向函数的指针的定义
返回类型 (*fp)(形参列表);
fp = &对应函数的函数名;//ok
fp = 对应函数的函数名;//ok
(2)typedef 简化函数指针的定义
typedef 返回类型 (*别名)(形参);
例如:
typedef void (*Fun)();
void f();
Fun fp1 = f;//定义一个函数指针fp1,赋值为函数名
等级于上面定义的函数指针fp = f;
(3)通过函数指针引用函数
(*fp)(实参);//ok
fp(实参);//ok
9 标准IO库
9.1 常用的IO标准库
- istream:输入流类,提供输入操作,从标准输入中输入。
- ostream:输出流类,提供输出操作,向标准输出输出。
- cin:标准输入流类istream的对象
- cout:标准输出流类ostream的对象
- cerr:输出标准错误的ostream的对象
- >>:操作符,用于从istream对象中读入输入。
- <<:操作符,用于把输出写到ostream对象中
- getline函数:需要分别取istream类型和string类型的两个引用形参,其功能是从istream对象读取一个单词,然后写入string对象中。
9.2 面向对象的IO标准库
一般我们简称标准输入输出为输入流输出流,文件的输入输出为文件流,字符串的输入输出为字符串流,而实际上我们说的输入输出流是指这个输入输出流类型的对象。
(1)IO标准库类型与头文件
- iostream头文件中包含有:
- istream类:从输入流类中读
- ostream类:写到输出流类中去
- iostream类:对流类型进行读写,从istream类和ostream类派生而来
- fstream头文件中包含有:
- ifstream类:从文件中读取,由istream类派生而来
- ofstream类:写到文件中去,由ostream类派生而来
- fstream类:读写文件,由iostream类派生而来
- sstream头文件中包含有:
- istringstream类:从string对象中读取,由istream类派生而来
- ostringstream类:写到string对象中去,由ostream类派生而来
- stringstream类:对string对象进行读写,由iostream类派生而来
(2)IO对象不可复制或赋值
由于某些原因,标准库类型不允许做复制或赋值操作。为什么???
这个要求有两层特别重要的含义。
- 只有支持复制的元素类型才可以存储在vector或其他容器类型中去。由于流对象不能复制,因此不能存储在vector或其他容器中。
- 形参或者返回类型也不能为流类型。因为流类型中没有复制构造函数,如果需要传递或者返回IO对象,则必须传递或返回指向该对象的指针或引用,若为引用,一般为非const类型,因为可能会对流对象进行对写。
9.3 输出缓存区的管理
每个IO对象都会管理一个缓存区,用于存储程序读写的数据。
os << "hello world";
os为输出对象,可以是cout流对象,也可以是别的输出流的对象
<<为输出操作符
整个语句是:字符串hello world写入os对象中去,其实是先写入os对象的缓冲区中。
刷新缓存区:即将缓存区中的内容写入输出对象对应的真实的输出设备或文件。以下几种情况会导致缓存区的刷新。
- 程序正常结束,将清空清空缓存区,会刷新。
- 在一些不确定的时候,缓存区可能已经满了,在有数据来,会先刷新缓存区。
输出语句中使用了操作符,例如endl、ends,flush,可以显示的刷洗缓存区。
cout << "hi" <<endl;//endl会在hi后插入换行符,然后刷新缓存区 cout << "hi" <<ends;//ends会在hi后插入一个空字符'\0',然后刷新缓存区 cout << "hi" <<flush;//不添加任何东西,直接刷新缓存区
在每次输出操作执行完后,用unitbuf操作符设置流的内部状态,从而清空缓存区。
如果需要刷新所有输出,最好用unitbuf操作符,这个操作符在每次执行完写操作后都会刷新缓存区。 cout<< unitbuf <<"first"<<"second"<<nounitbuf; 等价于 cout<<"first"<<flush<<"second"<<flush; nounitbuf操作符将流恢复为使用正常的、由系统管理的缓存区刷新方式。
可将输出流与输入流关联起来(tie),此时,读输入流时候将刷新其关联的输出缓存区。
- 标准库默认将cout对象与cin对象绑定在一起了,因此cin>>ival;时候cout关联的缓存区被刷新,屏幕上就显示你的输入值。
tie函数可以用istream或ostream对象调用,使用一个指向ostream对象的指针形参。调用tie函数时,将实参流绑在调用该函数的对象上。如果一个流调用tie函数将其本身绑定到传递给tie的ostream实参对象上,那么该流上的任何IO操作都会刷新实参所关联的缓存区。
cin.tie(&cout);//将cin绑定到cout流对象上,其实默认就绑定了 ostream *old_tie = cin.tie();//获取cin所绑定的输出流对象的指针,保存到old_tie中 cin.tie(0);//取消cin的绑定,即取消cin与cout的绑定 cin.tie(&cerr);//将cin绑定到cerr上 cin.tie(0);//取消cin的绑定,即取消cin与cerr的绑定 cin.tie(old_tie);//作用是恢复cin与cout的绑定
一个ostream对象每次只能与一个istream对象绑定在一起,如果tie函数传入的实参为0,那么就是取消已存在的绑定。
注意
如果程序不正常结束,输出缓存区将不会刷新!所以有可能你在代码中添加了很多打印,但是程序崩溃时候,崩溃的地方却没有打印出来相关信息。
最好的方法是保证所有的输出操作都显示地调用了flush或endl。基于这个原因,输出时应该多使用endl而非’\n’,使用endl则不必担心程序崩溃时候输出是否悬而未决。
9.4 文件的输入输出
(1)文件流的对象
ifstream infile(路径名);//创建一个读文件的输入流对象,并与路径代表的文件关联起来,即打开文件
等价于
ifstream infile() 或 ifstream infile;//创建一个读文件的输入流对象,但是还没有和任何文件关联起来
infile.open(路径名);//将文件和已经创建的输入流对象关联起来
上两步合在一起,也称为打开文件
infile.close();//将输入流对象和已经关联的文件解关联,即关闭文件
----------------------------------------------------------
类似,写文件的ofstream流,还有读写文件的fstream流,它们创建流对象的过程是一样的。
注意路径名一定要是C风格的字符串,如果路径名采用是string对象表示,也必须转换成C风格字符串,string类的c_str()方法即可。
(2)文件操作
- 文件流与文件的重新捆绑
通过文件流对象(读文件的输入流对象,写文件的输出流对象,读写文件的流对象)重新open一个文件的时候,必须先要close掉这个文件流,再重新open。 读文件操作
ifstream infile("/home/test.c"); string s; infile >> s; //从文件流关联的文件中读出数据出来,放到s中
写文件操作
ofstream outfile("/home/text.c"); string s = "hello world"; outfile << s<<endl;//把s中数据,写到文件流关联的文件中去
(3)文件模式
打开文件时候,无论是调用open还是以文件名作为流初始化的一部分,都需要指定文件模式
由上面可知,打开文件两种方式
- fstream iofile(“C字符串的路径名”);
- fstream iofile;
iofile.open(“C字符串的路径名”);
文件模式也是整型常量,打开指定文件时,可用与或等位操作符来设置一个或多个模式。文件流的构造函数和open函数都提供了默认实参来设置文件模式。当然打开文件时候也可以显示指定文件的模式
文件模式 | 作用 |
---|---|
in | 打开文件做读操作,每次读一行,以换行符为终止 |
out | 打开文件做写操作 |
app | 在每次写之前找到文件尾 |
ate | 打开文件后立即将文件定位在文件尾,注意只在打开时候执行,与app不同,app每次写的时候都到文件尾 |
trunc | 打开文件时清空已存在的文件流 |
binary | 以二进制模式进行io操作 |
out、trunc、app模式只能用于与ofstream或fstream关联的文件;in模式只能用于与ifstream或fstream关联的文件;ate、binary模式适合所有文件。
- 默认时,与ifstream流对象关联的文件将以in模式打开,该模式允许文件做读操作;
- 与ofstream关联的文件将以out模式打开,使文件可写,并且每次写之前都清空文件中的内容;
- 与fstream关联的文件,默认将以in和out模式打开,即可读由可写,且每次写时候不清空文件,只有fstream只以out模式显示打开时候,每次写才清空文件。
打开模式的有效组合
文件模式组合 | 作用 |
---|---|
out | 打开文件做写操作,每次写之前清空文件中内容 |
app | out | 每次写都是在文件尾处写 |
out | trunc | 与out模式一样 |
in | 打开文件做读操作,每次都是接着上次的位置读,读一行 |
in | out | 打开文件做读写操作,每次都在文件头处,且不清空文件 |
in | out | trunc | 打开文件做读写操作,每次操作前清空文件 |
9.5 字符串流
头文件< sstream >
- istringstream,由istream派生而来,提供读string的功能。
- ostringstream,由ostream派生而来,提供写string的功能。
- stringstream,由iostream派生而来,提供读写string的功能。
与文件流类似,字符串流需要和一个字符串string对象关联绑定,文件流有两种打开文件的方式,一种直接构造函数,一种通过open;字符串流也有两种方式。
通过把string对象传给构造函数
string s; stringstream strm(s);
通过字符串流的str(string s)方法
string s; stringstream strm; strm.str(s); strm.str();//返回绑定的s对象
输入操作
string s1; strm >> s1;//从strm字符串流中读出数据到s1中
输出操作
string s1 = "hello"; strm << s1;//把s1中内容写入到字符串流strm中