const:
C++ const 允许指定一个语义约束,编译器会强制实施这个约束,允许程序员告诉编译器某值是保持不变的。如果在编程中确实有某个值保持不变,就应该明确使用const,这样可以获得编译器的帮助。
1.
const修饰成员变量:
1)只有一个const,如果const位于*左侧,表示指针所指数据是常量,不能通过解引用修改该数据;指针本身是变量,可以指向其他的内存单元。
2)只有一个const,如果const位于*右侧,便是指针本身是常量,不能指向其他内存地址;指针所指的数据可以通过解引用修改。
3)两个const,*左右各一个,表示指针和指针所指数据都不能修改。
2.
const修饰函数参数:
传递过来的参数在函数内不可以改变,
#include <iostream>
using namespace std;
void print(const int n)
{
// n=5; //会报错!
cout<<n;
}
int main()
{
int a=1;
print(a);
return 0;
}
3.
const修饰成员函数:
1)const修饰的成员函数不能修改任何的成员变量(mutable修饰的变量除外)。
2)const成员函数不能调用非const成员函数,因为非const成员函数可能会修改成员变量。
#include <iostream>
using namespace std;
class point{
public:
point(int _x):x(_x)
{
}
void test(int _x)const{
//错误,在const成员函数中,不能修改任何类成员变量
x=_x;
//错误,const成员函数不能调用非const成员函数,因为非const成员函数可能会修改成员变量
modify_x(_x);
}
void modify_x(int _x){
x=_x;
}
int x;
};
int main()
{
}
4.
const修饰函数返回值:
1)指针传递:
如果返回 const data,non-const pointer,返回值也必须赋给 const data,non-const pointer。因为指针指向的数据是常量不能修改。
#include <iostream>
using namespace std;
const int *mallocA(){ //const data,non-const pointer
int *a=new int(2);
return a;
}
int main()
{
const int *a=mallocA(); //编译正确
int *b=mallocA(); //编译错误
return 0;
}
2)值传递:
若果函数返回值采用“值传递方式”,由于函数会把返回值复制到外部临时的存储单元中,加const修饰没有任何价值。所以,对于值传递来说,加const没有太多意义。
所以,不要把函数int a(void)写成const int a(void)。
不要把函数 A a(void)写成const A a(void)。其中A为用户自定义的数据类型。
在编程中尽可能多的使用const,这样可以获得编译器的帮助,以便写出健壮性的代码。
为什么链表的删除中,free(p)后还要加个p=NULL?
这是C语言的问题,free只是释放了p所指向的堆地址,p还是指向的这个地址。这个操作是让操作系统知道这片堆地址已经从原来的被占用,变成了可使用。这时候你其实还是可以用p去操作这个地址。但是这样操作是不安全的,因为操作系统默认这片堆地址可以随意使用。当其他程序刚好调用这个地址时,系统会毫不犹豫的把这地址分配给它。这就会导致很多问题。所以释放后,一定要把指针指向空值。不要使用这种野指针。
野指针:
野指针指向一个已删除的对象或未申请访问受限内存区域的指针。与空指针不同,野指针无法通过简单的判断是否为NULL来避免,而只能通过养成良好的编程习惯来尽力减少。对野指针进行操作很容易造成系统错误。需对指针进行初始化。
野指针主要是因为这些疏忽而出现的删除或申请访问受限内存区域的指针。
1)指针变量未初始化:
任何指针变量刚创建时不会自动成为NULL指针,他的缺省值是随机的,它会乱指一气。所以,指针变量在创建的同时应该被初始化,要么将指针设置为NULL,要么让它指向合法的内存。如果没有初始化,编译器会报错“‘point’mat be uninitializedin the function”。
2)指针释放之后未置空:
有时指针在free或delete后未赋值NULL,便会使人以为是合法的,别看free和delete的名字(尤其是delete),它们只是把指针所指的内存给释放掉,但并没有把指针本身干掉。此时指针指向的就是“垃圾”内存。释放后的指针应立即将指针置为NULL,防止产生“野指针”。
3)指针操作超越变量的作用域:
不要返回指向栈内存的指针或引用,因为栈内存在函数结束时会被释放:如
class A {
public:
void Func(void){ cout << “Func of class A” << endl; }
};
class B {
public:
A *p;
void Test(void) {
A a;
p = &a; // 注意 的生命期 ,只在这个函数Test中,而不是整个class B
}
void Test1() {
p->Func(); // p 是“野指针”
}
};
函数Test1在执行语句p->Func()时,p的值还是a的地址,对象a的内容已经被清除,所以p就成了野指针。
break与continue的区别:
break:
1)结束当前整个循环,执行当前循环下边的语句。忽略循环体中任何其它语句和循环条件测试。
2)只能跳出一层循环,如果你的循环是嵌套循环,那么你需要按照你的嵌套的层次,逐步使用break来跳出。
continue:
1)终止本次循环的执行,即跳过当前这次循环中continue语句后尚未执行的语句,接着进行下一次循环条件的判断。
2)结束当前循环,进行下一次循环判断。
3)终止当前的循环过程,但它并不跳出循环,而是继续往下判断循环条件执行语句,它只能结束循环中的一次过程,但不能终止循环继续进行。
cin与scanf的区别:
scanf是格式化输入,printf是格式化输出。
cin是输入流,cout是输出流。
格式化输出效率较高,但是写代码麻烦。
流输出操作效率稍低,但书写简便。
cout之所以效率低,正如一楼所说,是先把要输出的东西存入缓冲区,再输出,导致效率降低。
缓冲区比较抽象,举个例子:
int i;
cin>>i;
cout<<'a';
cin>>i;
cout<<'b';
运行结果什么都没看到输出,输入一个整型比如3再按回车后ab同时显示出来了。
但是这样的情况并不是经常发生,是在一些较大型的工程中偶尔出现,原因是字符a先到了缓冲区,但是没输出,等输入了i,b进入缓冲区再一并输出的。
流输入也是差不多的。
cin的实时性较差,因为它使用了缓冲区,一般情况下满了才刷新的。
对于字符:cin的输入忽略空格和回车,scanf(“%c”,&i)等价于i=getchar(),换行符和回车都会被读入。
this指针:
C++类中this指针的理解:
先要理解class的意思。class应该理解为一种类型,像int,char一样,是用户自定义的类型。用这个类型可以来声明一个变量,比如 int x,myclass my等等。这样就像变量x具有int类型一样,变量my具有myclass类型。理解了这个,就好解释了,my里的this就是指向my的指针。如果还有一个变量myclass mz,mz的this就是指向mz的指针。这样就很容易理解this的类型应该是myclass*,而对其的解引用*this就应该是一个myclass类型的常量。(解引用:返回内存地址中保存的值)
通常在class定义时要用到类型变量自身时,因为这时候还不知道变量名(为了通用也不可能固定实际的变量名),就用this这样的指针来使用变量自身。
1)this指针的用处:
一个对象的this指针并不是对象本身的一部分,不会影响sizeof(对象)的结果。
this作用域是在类内部,当在类的非静态成员函数中访问类的非静态成员时,编译器会自动将对象本身的地址作为一个隐含参数传递给函数。也就是说,即使你没有写上this指针,编译器在编译时也是加上this的,它作为非静态成员函数的隐含形参,对各成员的访问均通过this进行。例如,调用date.SetMonth(9)<==>SetMonth(&date,9),this帮助完成了这一转换。
在成员函数内部,我们可以直接使用调用该函数的对象的成员,而无需通过成员访问运算符来做到这一点,因为this所指的正是这个对象。任何对类的成员的直接访问都被看成this的隐式使用。
this的目的总是指向这个对象,所以this是一个常量指针,我们不允许改变this中保存的地址。
2)this指针的使用:
一种情况是,在类的非静态成员函数中返回类对象本身的时候,直接使用 return 相同时*this;另一种情况是当参数与成员变量名相同时,如this->n=n(不能写成n=n)。
3)this指针程序示例:
this指针是存在于类的成员函数中,指向被调用函数所在的类实例的地址。
根据以下程序说明:
#include<iostream>
#include<string>
using namespace std;
class Stu_Info_Mange
{
int sno;
string sname;
int age;
int grade;
public:
Stu_Info_Mange(int s=0,string n="",int a=0,int g=0)
{
sno=s;
sname=n;
age=a;
grade=g;
}
void Setsname(int sn) //使用this指针进行赋值
{
this->sname=sn;
}
int Setage(int a)
{
this->age=a;
return (*this).age; //使用this指针返回该对象的年龄
}
void print()
{
cout<<"the sname is "<<this->sname<<endl; //显式this指针通过箭头操作符访问
cout<<"the sno is "<<sno<<endl;//隐式使用this指针打印
cout<<"the age is "<<(*this).age<<endl;//显式使用this指针通过远点操作符
cout<<"the grade is "<<this->grade<<endl<<endl;
}
};
int main()
{
Stu_Info_Mange sim1(761,"张三",19,3);
sim1.print(); //输出信息
sim1.Setage(12); //使用this指针修改年龄
sim1.print(); //再次输出
return 0;
}
#include <iostream>
using namespace std;
class A
{
public:
int get()const
{
return i;
}
void set(int x)
{
this->i=x;
cout<<"this指针保存的内存地址为:"<<this<<endl;
}
private:
int i;
};
int main()
{
A a;
a.set(9);
cout<<"对象a所在的内存的地址为:"<<&a<<endl;
cout<<"对象a所保存的值为:"<<a.get()<<endl;
cout<<endl;
A b;
b.set(999);
cout<<"对象b所在的内存地址为:"<<&b<<endl;
cout<<"对象b所保存的值为:"<<b.get()<<endl;
return 0;
}
函数:
1)函数模板:
只适用于函数体相同,函数的参数个数相同而类型不同的情况。如果参数的个数不同则不能用函数模板。
2)函数重载:
参数的个数和类型可以都不相同。但不能只有函数的类型不同而参数的个数和类型相同。(重点在于参数的个数和类型是否相同)
3)默认参数:
实参与形参的结合是从左至右顺序进行的。因此指定默认值的参数必须放在形参表列中的最右端,否则会出错。
4)函数的声明和定义:
函数的声明是函数的原型,而函数的定义是函数功能的确立。
5)变量的声明和定义:
把建立存储空间的声明称为定义,把不需要建立存储空间的声明称为声明。
指针和数组:
1)多维数组作函数参数:
必须指定第二维(列)的大小,且与对应实参的第二维大小相同。
2)变量与指针:
变量的指针就是变量的地址,用来存放变量地址的变量就是指针变量。
3)指针变量:
一般的C++编译系统(32位平台下)都为指针变量分配4个字节的存储单元,但是在定义指针变量时必须指定基类型。
4)函数调用:
实参变量和形参变量之间的数据传递方式是“单向值传递”,调用函数时不会改变实参指针变量的值,但可以改变实参指针变量所指向变量的值。
5)函数指针:
一个函数在编译时被分配一个入口地址,这个函数入口地址就称为函数的指针。
6)指针函数:
返回指针的函数,
类型名 *函数名(参数列表);
1.指针数组 类型名 *数组名[数组长度]
2.数组指针 类型名 (*指针名)[数组长度]
7)引用:
对于习惯使用C的人,在看到C++中出现的&符号,可能会犯迷糊,因为在C语言中这个符号表示了取地址符,但是在C++中它却有着不同的用途,掌握C++的&符号,是提高代码执行效率和增强代码质量的一个很好的办法。
引用不是一种独立的数据类型,必须指定其代表某一类型的实体(如变量,类对象),不能定义引用数组,不能定义指向引用的指针。引用变量主要用作函数参数,它可以提高效率,并且保持程序良好的可读性。
引用是C++引入的新语言特性,是C++常用的一个重要内之一,正确,灵活地使用引用,可以使程序简洁,高效。
引用简介:
引用就是某一变量(目标)的一个别名,对引用的操作与对变量的操作完全一样。
引用的声明方法:类型标识符 &引用名=目标变量名;
例:int a;int &ra=a;//定义引用ra,它是变量a的引用,即别名
说明:
1)&在此处不是求地址运算,而是起标识作用。
2)类型标识符是指目标变量的类型。
3)声明引用时,必须同时对其初始化。
4)引用声明完毕后,相当于目标变量名有两个名称,即该目标原名称和引用名,且不能再把该引用名作为其他变量的别名。
ra=1;等价于a=1;
5)声明一个引用,不是新定义了一个变量,它只表示该引用名是目标变量名的一个别名,它本身不是一种数据类型,因此引用本身不占存储单元,系统也不给引用分配存储单元。故:对引用求地址,就是对目标变量求地址。&ra与&a相等。
6)不能建立数组的引用。因为数组是一个由若干个元素所组成的集合,所以无法建立一个数组的别名。
引用应用:
1.引用作为参数:
引用的一个重要作用就是作为函数的参数。以前的C语言中函数参数传递是值传递,如果有大块数据作为参数传递的时候,采用的方案往往是指针,因为这样可以避免将整块数据全部压栈,可以提高程序效率。但是现在C++中又增加了一种同样有效率的选择(在某些特殊情况下又是必须的选择),就是引用。
eg.
#include <iostream>
using namespace std;
void swap(int &p1,int &p2) //此处函数的形参p1,p2都是引用
{
int p;
p=p1;
p1=p2;
p2=p;
} //为了在程序中调用该函数,则相应的主调函数的调用点处, 直接以变量作为实参进行调用即可 ,而不需要实参变量有任何的特殊要求。
int main()
{
int a,b;
cin>>a>>b; //输入a,b两变量的值
swap(a,b); //直接以变量a,b作为实参调用swap函数
cout<<a<<" "<<b;
}
由这个例子可以看出:
1)传递引用给函数与传递指针的效果是一样的。这时,被调函数的形参就成为原来主调函数中的实参变量或对象的一个别名来使用,所以在被调函数中对形参变量的操作就是对其相应的目标对象(在主调函数中)的操作。
2)使用引用传递函数的参数,在内存中并没有产生实参的副本,它是直接对实参操作;而使用一般变量传递函数的参数,当发生函数调用时,需要给形参分配存储单元,形参变量是实参变量的副本;如果传递的是对象,还将调用拷贝构造函数。因此,当参数传递的数据较大时,用引用比用一般变量传递参数的效率和所占空间都好。
3)使用指针作为函数的参数虽然也能达到与使用引用的效果,但是,在被调函数中同样要给形参分配单元,且需要重复使用“指针变量名”的形式进行运算,这很容易产生错误且程序可读性较差;另一方面,在主调函数的调用点处,必须用变量的地址作为实参。而引用更容易使用,更清晰。
如果既要利用引用提高程序的效率,又要保护传递给函数的数据不在函数中被改变,就应使用常引用。
2.常引用:
常引用声明方式:const 类型标识符 &引用名=目标变量名;
用这种方式声明的引用,不能通过引用对目标变量的值进行修改,从而使引用的目标成为const,达到了引用的安全性。
eg.
#include<iostream>
using namespace std;
int main()
{
int a;
const int &ra=a;
ra=1; //错误
a=1; //正确
cout<<ra;
}
这不光是让代码更健壮,也有些其他方面的需要。
3.引用作为返回值:
要以引用返回函数值,则函数定义时,要按以下格式:
类型标识符 &函数名(形参列表及类型说明){函数体}
说明:
1)以引用返回函数值,定义函数时需要在函数名前加&。
2)用引用返回一个函数值的最大好处是,在内存中不产生被返回值的副本。
eg.
#include <iostream>
using namespace std;
float temp; //定义全局变量temp
float fn1(float r); //声明函数fn1
float &fn2(float r); //声明函数fn2
float fn1(float r) //定义函数fn1,它以返回值的方法返回函数值
{
temp=(float)(r*r*3.14);
return temp;
}
float &fn2(float r) //定义函数fn2,它以引用方式返回函数值
{
temp=(float)(r*r*3.14);
return temp;
}
int main()
{
float a=fn1(10.0); //第一种情况,系统生成要返回值的副本(即临时变量)
//float &b=fn1(10.0); //可能会出错(不同C++系统有不同规定)
float c=fn2(10.0); //第三种情况,系统不生成返回值的副本
float &d=fn2(10.0); //第四种情况,系统不生成返回值的副本
cout<<a<<endl<<c<<endl<<d<<endl;
return 0;
}
引用作为返回值,必须遵守一下规则:
1)不能返回局部变量的引用。这条可以参照Effective C++[1]的Item 31。主要原因是局部变量会在函数返回后被销毁,因此被返回的引用就成为了“无所指”的引用,程序会进入未知状态。
2)不能返回函数内部new分配的内存的引用。这条可以参照Effective C++[1]的Item 31。虽然不存在局部变量的被动销毁问题,可对于这种情况(返回函数内部new分配内存的引用),又面临其它尴尬局面。例如,被函数返回的引用只是作为一个临时变量出现,而没有被赋予一个实际的变量,那么这个引用所指向的空间(由new分配)就无法释放,造成memory leak。
3)可以返回类成员的引用,但最好是const。这条原则可以参照Effective C++[1]的Item 30。主要原因是当对象的属性是与某种业务规则(business rule)相关联的时候,其赋值常常与某些其它属性或者对象的状态有关,因此有必要将赋值操作封装在一个业务规则中。如果其它对象可以获得该属性的非常 量引用(或指针),那么对该属性的单纯赋值就会破坏业务规则的完整性。
4)引用与一些操作符的重载:
流操作符<<和>>,这两个操作符常常希望被连续使用,例如cout<<"hello"<<endl;因此这两个操作符的返回值应该是一个仍然支持这两个操作符的流引用。可选的其他方案包括:返回一个流对象和返回一个流对象指针。但是由于返回一个流对象,程序必须重新(拷贝)构造一个新的流对象,也就是说,连续的两个<<操作符实际上是针对不同对象的!这无法让人接受。对于返回一个流指针则不能连续使用<<操作符。因此,返回一个流对象引用是唯一选择。这个唯一选择很关键,它说明了引用的重要性以及无可替代性,也许这就是C++语言中引入引用这个概念的原因把。赋值操作符=,这个操作符和流操作符一样,是可以连续使用的,例如:x=j=10;或者(x=10)=100;赋值操作符的返回值必须是一个左值,以便可以被继续赋值。因此引用成了这个操作符的唯一返回值选择。
#include <iostream>
using namespace std;
int &put(int n);
int vals[10];
int error=-1;
int main()
{
put(0)=10; //以put(0)函数值作为左值,等价于vals[0]=10
put(9)=20; //以put(9)函数值作为左值,等价于vals[9]=20
cout<<vals[0];
cout<<vals[9];
}
int &put(int n)
{
if(n>=0&&n<=9)
{
return vals[n];
}
else
{
cout<<"subscript error";
return error;
}
}
5)在另外的一些操作符中,却千万不能返回引用:+-*/四则运算符。它们不能返回应用,Effective C++[1]的Item23详细的讨论了这个问题。主要原因是这四个操作符没有side effect,因此,它们必须构造一个对象作为返回值,可选的方案包括:返回一个对象,返回一个局部变量的引用,返回一个new分配的对象的引用,返回一个静态对象引用。根据前面提到的引用作为返回值的三个规则,第二,三个方案都被否决了。静态对象的引用又因为((a+b)==(c+d))会永远为true而导致错误。所以可选的只剩下返回一个对象了。
4.引用和多态:
引用是除指针外另一个可以产生多态效果的手段,这意味着,一个基类的引用可以指向它的派生类实例。
class A;
class B:public A{.....};
B b;
A &Ref=b; //用派生类对象初始化基类对象的引用
Ref只能用来访问派生类对象中从基类继承下来的成员,是基类引用指向派生类。如果A类中定义有虚函数,并且在B类中重写了这个虚函数,就可以通过Ref产生多态效果。
三.引用总结
1)在引用的使用中,单纯给某个变量取个别名是毫无意义的,引用的主要目的主要用于在函数参数传递中,解决大块数据或对象的传递效率和空间不如意的问题。
2)用引用传递函数的参数,能保证参数传递中不产生副本,提高传递效率,且通过const的使用,保证了引用传递的安全性。
3)引用与指针的区别是,指针通过某个指针变量指向一个对象后,对它所指向的变量间接操作,程序中使用指针,程序的可读性差;而引用本身就是目标变量的别名,对引用的操作就是对目标变量的操作。
4)使用引用的时机,流操作符<<和>>,赋值运算符=的返回值,拷贝构造函数的参数,赋值操作符=的参数,其他情况都推荐使用引用。
附:
*(p++)与*(++p)
前者先取*p的值,然后p+1,后者先p+1,再取*p。 另外注意*p+1,是*p的值再+1。而*(p+1)的值是,*(p+1)的值。
C++文件:
C++通过以下几个类支持文件的输入输出:
ofstream:写操作(输出)的文件类(由ostream引申而来)
ifstream:读操作(输入)的文件类(由istream引申而来)
fstream:可同时读写操作的文件类(由iostraem引申而来)
打开文件:
对这些类的一个对象做的第一个操作通常就是将它和一个真正的文件联系起来,也就是说打开一个文件。被打开的文件在程序中由一个流对象(stream object)来表示(这些类的一个实例),而对这个流对象所做的任何输入输出操作实际就是对该文件所做的操作。
要通过一个流对象打开一个文件,我们使用它的成员函数
open():void open (const char *filename,openmode mode);
这里filename是一个字符串,代表要打开的文件名,mode是以下标志符的一个组合:
ios::in 为输入(读)而打开文件
ios::out 为输出(写)而打开文件
ios::ate 初始位置:文件尾
ios::app 所有输出附加在文件末尾
ios::trunc 如果文件已存在则先删除该文件
ios::binary 二进制方式
这些标识符可以被组合使用,中间以“或操作符(|)”间隔。例如:要用二进制方式打开文件"example.bin"来写入一些数据,我们可以通过以下方式调用成员函数open()来实现:
ofstream file;file.open("example.bin",ios::out|ios::app|ios::binary);
ofstream,ifstream和fstream所有这些类的成员函数open都包含了一个默认打开文件的方式,这三个类的默认方式各不相同:
类 参数的默认方式:
ofstream ios::out | ios::trunc
ifstream ios::in
fstream ios::in | ios::out
只有当函数被调用时没有声明方式参数的情况下,默认值才会被采用。如果函数被调用时声明了任何参数,默认值将被完全改写,而不会与调用参数组合。
由于对类ofstream,ifstream和fstream的对象所进行的第一个操作通常都是打开文件,这些类都有一个构造函数可以直接调用open函数,并拥有同样的参数。这样,我们就可以通过以下方式进行与上面同样的定义对象和打开文件的操作:
ofstraem file("example.bin",ios::out | ios::app | ios::binary);
两种打开文件的方式是等价的。
可以通过调用成员函数 is_open()来检查一个文件是否已经被顺利的打开了:bool is_open();
它返回一个布尔(boll)值,为真(true)代表文件已经被顺利打开,假(false)则相反。
关闭文件:
当文件读写操作完成之后,我们必须将文件关闭以使文件重新变为可访问的。关闭文件需要调用成员函数close(),它负责将缓存中的数据排放出来并关闭文件。它的格式很简单:void close();
这个函数一旦被调用,原先的流对象(stream object)就可以被用来打开其它的文件了,这个文件也可以重新被其它的进程(process)所有访问了。
为防止流对象被销毁时还联系着打开的文件,析构函数(destructor)将会自动调用关闭函数close。
文本文件:
类ofstream,ifstream和fstream是分别从ostream,istream和iostream中引申而来的,这就是为什么fstream的对象可以使用其父类的成员来访问数据。
一般来说,我们将使用这些类与同控制台(console)交互同样的成员函数(cin和cout)来进行输入输出。如下面的例题所示,我们使用重载的插入操作符<<:
#include <fstream>
using namespace std;
int main()
{
ofstream examplefile("example.txt");
if(examplefile.is_open())
{
examplefile<<"This is a line."<<endl;
examplefile<<"This is another line."<<endl;
examplefile.close();
}
return 0;
}
从文件中读入数据也可以用与cin的使用同样的方法:
#include <iostream>
#include <fstream>
#include <cstdlib>
using namespace std;
int main()
{
char buffer[256];
ifstream examplefile("example.txt");
if(!examplefile.is_open())
{
cout<<"Error opening file";
exit(1);
}
while(!examplefile.eof())
{
examplefile.getline(buffer,100);
cout<<buffer<<endl;
}
return 0;
}
状态标志符的验证:
bad():
如果在读写过程中出错,返回true。例如:当我们要对一个不是打开为写状态的文件进行写入时,或者我们要写入的设备没有剩余空间的时候。
fail():
除了与bad()同样的情况下会返回true以外,加上格式错误时也返回true,例如当想要读入一个整数,而获得了一个字母的时候。
eof():
如果读文件到达文件末尾,返回true。
good():
这是最通用的:如果调用以上任何一个函数返回true的话,此函数返回false。
要想重置以上成员函数所检查的状态标志,你可以使用成员函数clear(),没有参数。
获得和设置流指针(get and put stream pointers):
所有输入/输出流对象(i/o streams objects)都有至少一个流指针:
ifstream,类似istream,有一个被称为get pointer 的指针,指向下一个将被读取的元素。
ofstream,类似ostream,有一个指针 put pointer,指向写入下一个元素的位置。
fstream,类似iostream,同时继承了get和put。
我们可以通过使用以下成员函数来读出或配置这些指向流中读写位置的流指针:
tellg()和tellp():
这两个成员函数不用传入参数,返回pos_tyoe 类型的值(根据ANSI-C++标准),就是一个整数,代表当前get流指针的位置(用tellg)或put流指针的位置(用tellp)。
seekg()和seekp():
这对函数分别用来改变流指针get和put的位置,两个函数都被重载为不同的原型:
seekg(pos_type position);
seekp (pos_type position) ;
使用这个原型,流指针被改变为指向从文件开始计算的一个绝对位置。要求传入的参数类型与函数 tellg 和 tellp的返回值类型相同。
seekg(off_type offset,seekdir direction);
seekp (off_type offset,seekdir direction) ;
使用这个原型可以指定由参数direction决定的一个具体的指针开始计算的一个位移(offset)。它可以是:
ios::beg从流开始位置计算的位移
ios::cur从流指针当前位置开始计算的位置
ios::end从流末尾处开始计算的位移
流指针get和put的值对文本文件(text file)和二进制文件(binary file)的计算方法都是不同的,因为文本模式的文件中某些特殊字符可能被修改。由于这个原因,建议对以文本文件模式打开的文件总是使用seekg和seekp的第一种原型,而且不要对tellg或tellp的返回值进行修改。对二进制文件,你可以任意使用这些函数,应该不会有任何意外的行为产生。
一下例子使用这些函数来获得一个二进制文件的大小:
#include <iostream>
#include <fstream>
using namespace std;
int main()
{
const char *filename="example.txt";
long l,m;
ifstream file(filename,ios::in|ios::binary);
l=file.tellg();
file.seekg(0,ios::end);
m=file.tellg();
file.close();
cout<<"size of"<<filename;
cout<<"is"<<(m-l)<<"bytes.\n";
return 0;
}
二进制文件(Binary files):
在二进制文件中,使用<<和>>,以及函数(如getline)来操作符输入和输出数据,没有什么实际意义,虽然它们是符合语法的。
文件流包括两个为顺序读写数据特殊设计的成员函数:write和read。第一个函数(write)是ostream的一个成员函数,都是被ofstream所继承。而read是istream的一个成员函数,被ifstream所继承。类fstream的对象同时拥有这两个函数。它们的原型是:
write(char *buffer,streamsize size);
read(char *buffer,streamsize size);
这里buffer是一块内存的地址,用来存储或读出数据。参数size是一个整数值,表示要从缓冲(buffer)中读出或写入的字符数。
#include <iostream> //reading binary file
#include <fstream>
using namespace std;
int main()
{
const char *filename="example.txt";
char *buffer;
long size;
ifstream file(filename,ios::in|ios::binary|ios::ate);
size=file.tellg();
file.seekg(0,ios::beg);
buffer=new char[size];
file.read(buffer,size);
file.close();
cout<<"the complete file is in a buffer";
delete [] buffer;
return 0;
}
缓存和同步(Buffers and Synchronization):
当我们对文件流进行操作的时候,它们与一个streambuf类型的缓存(buffer)联系在一起。这个缓存(buffer)实际是一块内存空间,作为流(stream)和物理文件的媒介。例如,对于一个输出流,每次成员函数put(写一个单个字符)被调用,这个字符不是直接被写入该输出流所对应的物理文件中的,而是首先被插入到该流的缓存(buffer)中。
当缓存被排放出来(flush)时,它里面的所有数据被写入物理媒质中(如果是一个输出流的话),或者简单的被抹掉(如果是一个输入流的话)。这个过程称为同步(synchronization),它会在以下任一情况下发生:
1)当文件被关闭时:在文件被关闭之前,所有还没有被完全写出或读取的缓存都将被同步。
2)当缓存buffer满时:缓存Buffers有一定的空间限制。当缓存满时,它会被自动同步。
3)控制符明确指明:当遇到流中某些特定的控制符时,同步会发生。这些控制符包括:flush和endl。
4)明确调用函数sync():调用成员函数sync()(无参数)可以引发立即同步。这个函数返回一个int值,等于-1表示流没有联系的缓存或操作失败。
在C++中,有一个stream这个类,所有的I/O都以这个“流”类为基础的,包括我们要认识的文件I/O,stream这个类有两个重要的运算符:
1、插入器(<<):
向流输出数据,比如说系统有一个默认的标准输出流(cout),一般情况下就是指的显示器,所以,cout<<"Write Stdout"<<'n';就表示把字符串"Write Stdout"和换行字符('n')输出到标准输出流。
2、析取器(>>):
从流中输入数据。比如说系统有一个默认的标准输入流(cin),一般情况下指的是键盘,所以,cin>>x;就表示从标准输入流中读取一个指定类型(即变量x的类型)的数据。
在C++中,对文件的操作是通过stream的子类fstream(file stream)来实现的,所以,要用这种方式操作文件,就必须加入头文件fstream.h。下面就是具体的操作过程:
一、打开文件
在fstream类中,有一个成员函数open(),就是用来打开文件的,其原型是:
void open(const char *filename,int mode);
参数:
filename:要打开的文件名
mode:要打开文件的方式
打开文件的方式在类ios(是所有流式I/O类的基类)中定义,常用的值如下:
ios::app:以追加的方式打开文件
ios::ate:文件打开后定位到文件尾,ios::app就包含有此属性
ios::binary:以二进制方式打开文件,缺省的方式是文本方式。两种方式的区别见前文 (缺省:即系统默认状态)
ios::in:文件以输入方式打开
ios::out:文件以输出方式打开
ios::nocreate:不建立文件,所以文件不存在时打开失败
ios::noreplace:不覆盖文件,所以打开文件时如果文件存在则操作失败
ios::trunc:如果文件存在,把文件长度设置为0
可以用“ | ”把以上属性连接起来,如ios::out | ios::binary
打开文件的属性取值是: 此处不确定
0:普通文件,打开访问
1:只读文件
2:隐含文件
4:系统文件
可以用“ | ”或者“+”把以上属性连接起来,如3或1|2就是以只读和隐含属性打开文件。
例如:以二进制输入方式打开文件c:config.sys
fstream file1;
file1.open("c:config.sys",ios::binary|ios::in,0);
—————————————————————————————————
二、关闭文件:
打开的文件使用完成后一定要关闭,fstream提供了成员函数close()来完成此操作,如:file1.close();就把file1相连的文件关闭。
三、读写文件:
读写文件分为文本文件和二进制文件的读取,对于文本文件的读取比较简单,用插入器和析取器就可以了;而对于二进制的读取要复杂些,下面就详细的介绍这两种方式:
1、文本文件的读写
文本文件的读写很简单:用插入器(<<)向文件输出;用析取器(>>)从文件输入。假设file1是以输入方式打开,file2以输出打开。示例如下:
file2<<"I Love You"; //向文件写入一个字符串
int i;
file1>>i; //从文件输入一个整数值
这种方式还有一种简单的格式化能力,比如可以指定输出为16进制等等,具体的格式有以下这些:
操纵符 功能 输入/输出
dec 格式化为十进制数值数据 输入和输出
endl 输出一个换行符并刷新此流 输出
ends 输出一个空字符 输出
hex 格式化为十六进制数值数据 输入和输出
oct 格式化为八进制数值数据 输入和输出
setprecision(int p)设置浮点数的精度位数 输出
比如要把123当作十六进制输出:file1<<hex<<123;
要把3.1415926以五位精度输出:file1<<setprecision(5)<<3.1415926;
2、二进制文件的读写
1)put()
put()函数向流写入一个字符,其原型是ofstream &put(char ch),使用也比较简单,如file1.put('c');就是向流写一个字符'c'。
2)get()
get()函数比较灵活,有3种常用的重载形式:
一种就是和put()对应的形式:ifstream &get(char &ch);功能是从流中读取一个字符,结果保存在引用ch中,如果到文件尾,返回空字符。如file2.get(x);表示从文件中读取一个字符,并把读取的字符保存在x中。
另一种重载形式的原型是:int get();这种形式是从流中返回一个字符,如果到达文件尾,返回EOF,如x=file2.get();和上例功能是一样的。
还有一种形式的原型是:ifstream &get(char *buf,int num,char delim='n');这种形式把字符读入由buf指向的数组,直到读入了num个字符或遇到了由delim指定的字符,如果没使用delim这个参数,将使用缺省值换行符'n'。
例如:file2.get(str1,127,'A'); //从文件中读取字符到字符串str1,当遇到字符'A'或读取了127个字符时终止。
3)读写数据块:
要读写二进制数据块,使用成员函数read()和write()成员函数,它们原型如下:
read(unsigned char *buf,int num);
write(const unsigned char *buf,int num);
read()从文件中读取num个字符到buf指向的缓存中,如果在还未读入num个字符时就到了文件尾,可以用成员函数 int gcount();来取得实际读取的字符数;而write()从buf指向的缓存写num个字符到文件中,值得注意的是缓存的类型是 unsigned char*,有时可能需要类型转换。
eg.
#include <fstream>
#include <cstring>
#include <iostream>
using namespace std;
int main()
{
char str1[]="I Love You";
int n[5];
ifstream in("xxx.xxx");
ofstream out("yyy.yyy");
int q=strlen( str1);
out.write(str1,strlen(str1)); //把字符串str1全部写到yyy .yyy中
in.read((char *)n,sizeof(n)); //从xxx.xxx中读取指定个整数,注意类型转换
in.close();
out.close();
}
四、检测EOF
成员函数eof()用来检测是否到达文件尾,如果到达文件尾返回非0值,否则返回0.原型是int eof();
eg.
if(in.eof())
cout<<("已经到达文件尾!");
五、文件定位
和C的文件操作方式不同的是,C++I/O系统管理两个与一个文件相联系的指针。一个是读指针,它说明输入操作在文件中的位置;另一个是写指针,它说明下次写操作的位置。它每次执行输入或输出时,相应的指针自动变化。所以,C++的文件定位分为读位置和写位置的定位,对应的成员函数是seekg()和seekp(),seekg()是设置读位置,seekp()是设置写位置。它们最通用的形式如下:
istream &seekg(streamoff offset,seek_dir origin);
ostream &seekp(streamoff offset,seek_dir origin);
streamoff定义于iostream.h中,定义有偏移量offset所能取得的最大值,seek_dir表示移动的基准位置,是一个有以下值的枚举:
ios::beg:文件开头
ios::cur:文件当前位置
ios::end:文件结尾
这两个函数一般用于二进制文件,因为文本文件会因为系统对字符的解释而可能与预想的值不同。
eg.
file1.seekg(1234,ios::cur); //把文件的读指针从当前位置向后移1234个字节
file2.seekp(1234,ios::beg); //把文件的写指针从文件开头向后移1234个字节
友元:
采用类的机制后实现了数据的隐藏与封装,类的数据成员一般定义为私有成员,成员函数一般定义为公有的,依此提供类与外界间的通信接口。但是,有时需要定义一些函数,这些函数不是类的一部分,但又需要频繁地访问类的数据成员,这时可以将这些函数定义为该函数的友元函数。除了友元函数外,还有友元类,两者统称为友元。友元的作用是提高了程序的运行效率(即减少了类型检查和安全性检查等都需要的时间开销),但它破坏了类的封装性和隐藏性,使得非成员函数可以访问类的私有成员。
友元函数:
友元函数是可以直接访问类的私有成员的非成员函数。它是定义在类外的普通函数,它不属于任何类,但需要在类的定义中加以声明,声明时只需要在友元的名称前加上关键字friend,其格式如下:
friend 类型 函数名(形式参数);
友元函数的声明可以放在类的私有部分,也可以放在共有部分,它们是没有区别的,都说明是该类的一个友元函数。
一个函数可以是多个类的友元函数,只需要在各个类中分别声明。
友元函数的调用与一般函数的调用方式和原理一致。
友元类:
————————————————————————————————————————————————————
运算符重载:
C++中预定义的运算符的操作对象只能是基本数据类型。但实际上,对于许多用户自定义类型(例如类),也需要类似的运算操作。这是就必须在C++中重新定义这些运算符,赋予已有运算符新的功能,使它能够用于特定类型执行的操作。运算符重载的实质是函数重载,它提供了C++的可扩展性,也是C++最吸引人的特性之一。
运算符重载是通过创建运算符函数实现的,运算符函数定义了重载的运算符将要进行的操作。运算符函数的定义与其他函数的定义类似,唯一的区别是运算符函数的函数名是由关键字operator和其后要重载的运算符符号构成的。运算符函数定义的一般格式如下:
返回类型说明符 operator 运算符符号 (参数表)
{
函数体
}
运算符重载时要遵循以下规则:
1)除了类属关系运算符“.”成员指针运算符“.*”作用域运算符“::”sizeof运算符和三目运算符"?:"以外,C++中的所有运算符都可以重载。
2)重载运算符限制在C++语言中已有的运算符范围内的允许重载的运算符之中,不能创建新的运算符。
3)运算符重载实质上是函数重载,因此编译程序对运算符重载的选择,遵循函数重载的选择原则。
4)重载之后的运算符不能改变运算符的优先级和结合性,也不能改变运算符操作数的个数及语法结构。
5)运算符重载不能改变运算符用于内部类型对象的含义,它只能和用户自定义类型的对象一起使用,或者用于用户自定义类型的对象和内部类型的对象混合使用时。
6)运算符重载是针对新类型数据的实际需要对原有运算符进行的适当的改造,重载的功能应当与原有的功能相类似,避免没有目的使用重载运算符。
运算符函数重载一般有两种形式:重载为类的成员函数和重载为类的非成员函数。非成员函数通常为友元。(可以把一个运算符作为一个非成员,非友元函数重载。但是,这样的运算符函数访问类的私有和保护成员时,必须使用类的公有接口中提供的设置数据和读取数据的函数,调用这些函数时会降低性能。可以内联这些函数以提高性能。)
成员函数运算符:
运算符重载为类的成员函数的一般格式为:
函数类型 operator 运算符 (参数表)
{
函数体
}
当运算符重载为类的成员函数时,函数的参数个数比原来的操作数要少一个(后置单目运算符除外),这是因为成员函数使用this指针隐式的访问了类的一个对象,它充当了运算符函数最左边的操作数。因此:
1)双目运算符重载为类的成员函数时,函数只显式说明一个参数,该形参是运算符的右操作数。
2)前置单目运算符重载为类的成员函数时,不需要显式说明参数,即函数没有形参。
3)后置单目运算符重载为类的成员函数时,函数要带有一个整型形参。
调用成员函数运算符的格式如下:
对象名.operator运算符(参数)
它等价于
对象名运算符(参数)
例如:a+b等价于a.operator+(b)。
一般情况下,我们采用运算符的习惯表达式。
友元函数运算符:
运算符重载为类的友元函数的一般格式为:
friend 函数类型 operator 运算符 (参数表)
{
函数体
}
当运算符重载为类的友元函数时,由于没有隐含的this指针,因此操作数的个数没有变化,所有的操作数都必须通过函数的形参进行传递,函数的参数与操作数自左至右一一对应。
调用友元函数运算符的格式如下:
operator 运算符(参数1,参数2)
它等价于
参数1运算符参数2
例如:a+b等价于operator+(a,b)。
两种重载形式的比较:
在多数情况下,将运算符重载为类的成员函数和类的友元函数都是可以的。但成员函数运算符与友元函数运算符也具有各自的一些特点:
1)一般情况下,单目运算符最好重载为类的成员函数;双目运算符则最好重载为类的友元函数。
2)以下一些双目运算符不嫩重载为类的友元函数:=、()、[]、->。
3)类型转换函数只能定义为一个类的成员函数而不能定义为类的友元函数。
4)若一个运算符的操作需要修改对象的状态,选择重载为成员函数较好。
5)若运算符所需的操作数(尤其是第一个操作数)希望有隐式类型转换,则只能选择友元函数。
6)当运算符函数是一个成员函数时,最左边的操作数(或者只有最左边的操作数)必须是运算符类的一个类对象(或者是对该类对象的引用)。如果左边的操作数必须是一个不同类的对象,或者是一个内部类型的对象,该运算符函数必须作为一个友元函数来实现。
7)当需要重载运算符具有可交换性时,选择重载为友元函数。
静态数据成员:
1.静态数据成员的定义,静态数据成员实际上是类域中的全局变量。所以,静态数据成员的定义(初始化)不应该被放在头文件中,因为这样做会引起重复定义这样的错误。即使加上#ifndef #define等也不行。
其定义方式与全局变量相同,举例如下:
class base{
private:
static const int i; //声明,标准C++支持有序类型在类体中初始化,但vc6不支持。
};
const int base::i=10; //定义(初始化)时不受private和protected访问限制。
2.静态数据成员被类的所有对象所共享,包括该类派生类的对象。
3.静态数据成员可以成为成员函数的可选参数,而普通数据成员则不可以。举例如下:
#include <iostream>
using namespace std;
class base{
public:
static int _staticVar;
int _var;
void foo1(int i=_staticVar); //正确,_staticVar为静态数据成员
void foo2(int i=_var); //错误,_var为普通数据成员
};
int main()
{
return 0;
}
4.静态数据成员的类型可以是所属类的类型,而普通数据成员则不可以。普通数据成员只能声明为所属类类型的指针或引用。
举例如下:
#include <iostream>
using namespace std;
class base{
public:
static base _object1; //正确,静态数据成员
base _object2; //错误!
base *pobject; //正确,指针
base &mobject; //正确,引用
};
int main()
{
return 0;
}
5.静态数据成员的值在const成员函数中可以被合法的改变。
#include <iostream>
using namespace std;
class base{
public:
base()
{
_i=0;
_val=0;
}
mutable int _i;
static int _staticVal;
int _val;
void test() const{//const 成员函数
_i++; //正确,mutable数据成员
_staticVal++; //正确,static数据成员
_val++; //错误
}
};
int main()
{
return 0;
}
二.静态成员函数
1.静态成员函数的地址可用普通函数指针存储,而普通成员函数地址需要用类成员函数指针来存储。
举例如下:
#include <iostream>
using namespace std;
class base{
public:
static int func1();
int func2();
int (*pf1)()=&base::func1(); //普通函数指针
int (base::*pf2)()=&base::func2(); //成员函数指针
};
int main()
{
return 0;
}
2.静态成员函数不可以调用类的非静态成员。因为静态成员函数不含this指针。
3.静态成员函数不可以同时声明为virtual、const、volatile函数。
举例如下:
#include <iostream>
using namespace std;
class base{
public:
virtual static void func1(); //错误
static void func2() const; //错误
static void func3() volatile; //错误
};
int main()
{
return 0;
}