一、运算符重载
1、重载对象的赋值运算符
编译器在默认情况下为每个类生成一个默认的赋值操作,用于同类的两个对象之间赋值。默认的含义是逐个为成员赋值,即将一个对象成员的值赋给另一个对象相应的成员,这种赋值方式对于有些类可能是不正确的。假设类Str的数据成员char *st,则下面的语句经赋值后是有问题的:
Str s1("hello"),s2("world");
s2 = s1;
经赋值后,s2.st和s1.st是同一块存储地址,当它们的生存周期结束时,存储“hello”的变量被删除了两次,这是个严重的错误。另外,对于s1 = s1的情况,也应不执行赋值操作。因此,程序必须为Str类定义自己的赋值操作符“=”,可使用如下的方法实现:
Str& Str::operator=(Str &s){
if(this == &s){//防止s=s这样的赋值
return *this;
}
delete st;
st = new char[strlen(s.st)+1];//申请内存
strcpy(st,s.st);//将对象s的数据复制一份到申请的内存区
return *this;//返回this指针指向的对象
}
上面这个方法必须使用引用参数,关键字operator和=一起表示一个运算符函数,这里可以将operator=整体上视为一个函数名,这样的话就可以把它声明为:
Str& operator= (Str &);
即函数operator=(Str &)返回Str类对象的引用,它在定义时可以写成如下形式:
Str& Str::operator=(Str &s){
//函数体
}
调用函数时写成如下形式:
Str s1,s2;
s2.operator=(s1);
但由于要实现的是=的作用,所以这里可以简写为如下形式:
Str s1,s2;
s2=s1
下面是一个完整的示例:
class Str{
private:
char *st;
public:
Str(char *);//使用字符指针的构造函数
Str(Str &);//使用对象引用的构造函数
Str& operator=(char *);//重载使用字符指针的赋值运算符
Str& operator=(Str &);//重载使用对象引用的赋值运算符
void print(){
cout << st << endl;
}
~Str(){
delete st;
}
};
Str::Str(char *c){
st = new char[strlen(c)+1];//字符数组以\0结尾,需要多申请一个字符长度的内存空间
strcpy(st,c);//将字符串复制到内存区st
}
Str::Str(Str &s){
st = new char[strlen(s.st)+1];//申请内存
strcpy(st,s.st);//将对象s的字符串复制到申请的内存区
}
Str& Str::operator=(char *c){
delete st;//先释放内存空间
st = new char[strlen(c)+1];//重新申请内存
strcpy(st,c);//将字符串c复制到内存区
return *this;
}
Str& Str::operator=(Str &s){
if(this == &s){//防止s=s这样的赋值
return *this;
}
delete st;//先释放内存
st = new char[strlen(s.st)+1];//重新申请内存
strcpy(st,s.st);//将对象s的字符串复制到新申请的内存区
return *this;//返回this指针指向的对象
}
#include <string>
#include <string.h>
#include "Example1.h"
void example1();
int main() {
example1();
return 0;
}
void example1() {
Str s1("hello"), s2("world"), s3(s1);
s1.print();
s2.print();
s3.print();
// hello
// world
// hello
s2 = s1 = s3;
s3 = "other";
s1.print();
s2.print();
s3.print();
// hello
// hello
// other
}
2、运算符重载的实质
在上面的示例中,当编译器处理s1=s2时,就会去看有没有operator=()为型的函数,如果系统定义了一个这样的函数,那么就调用这个函数,所以运算符重载其实就是函数重载,要重载某个运算符,只需要重载相应的函数就可以了。与其他重载不同的是,它需要使用一个新的关键字operator和一个运算符连用,构成一个运算符函数名。
一般情况下,为用户定义的类型重载运算符都要求能够访问这个类型的私有成员,所以只能两条路可走:一是将运算符重载为这个类型的成员函数;二是将运算符重载为这个类型的友元。
C++中的运算符大部分都可以重载,不能重载的只有. :: *和? : ,前面三个是因为在C++中有特定的含义,不准重载以避免不必要的麻烦;后面两个则是因为不值得重载。另外sizeof和#不是运算符,也不能重载,而且= () [] -> 这4个运算符只能用类运算符来重载。
3、<<、>>和++运算符重载
<<和>>在重载时,操作符左边是流对象的别名而不是操作对象,运算符跟在流对象的后面,它们要直接访问类的私有数据,而且流是标准类库,用户只能继承不能修改,更不能是流库的成员,所以它们必须作为类的友元来进行重载。
插入符函数的一般形式如下:
ostream& operator<<(ostream& output,类名& 对象名){
//函数体
return output;//output是类ostream对象的引用,它是cout的别名,即ostream& output = cout
}
提取符函数的一般形式如下:
istream& operator >>(istream& input,类名& 类对象){
//函数体
return input;//input是类istream对象的引用,它是cin的别名,即istream& input = cin
}
使用友元函数重载运算符<<
class Test{
private:
char c;
int i;
public:
Test(char c1,int i1):c(c1),i(i1){
}
friend ostream& operator<<(ostream& output,Test& t);
};
ostream& operator<<(ostream& output,Test& t){
output << t.c;
output << t.i;
output << endl;
return output;
}
#include "Example2.h"
void example2();
int main() {
example2();
return 0;
}
void example2(){
Test t1('W',10);
//使用函数形式调用
operator<<(cout,t1);
//使用简写形式调用
cout << t1;
}
使用类运算符重载++运算符,示例如下:
class Test1{
private:
int num;
public:
Test1(int a):num(a){}
int operator++();//前缀++
int operator++(int);//后缀++
void print(){
cout << num << endl;
}
};
int Test1::operator ++(){
num++;
return num;
}
int Test1::operator ++(int){//不用给出形参名
int i = num;
num++;
return i;
}
#include "Example3.h"
void example3();
int main() {
example3();
return 0;
}
void example3(){
Test1 t2(10);
t2.print(); //10
//使用函数调用形式,前缀++
int i = t2.operator ++();
cout << i << endl; //11
//不使用函数调用形式,前缀++
int j = ++t2;
cout << j << endl; //12
Test1 t3(10);
t3.print(); //10
//使用函数调用形式,后缀++
int k = t3.operator ++(0);
cout << k << endl; //10
//不使用函数调用形式,后缀++
int t = t3++;
cout << t << endl; //11
}
有些C++编译器不区分前缀或后缀运算符,这时只能通过对运算符函数进行重载来反映其为前缀或后缀运算符。需要注意的是,不能自己定义新的运算符,只能把C++原有的运算符用到自己设计的类上面去。同时,经过重载,运算符并不改变原有的优先级,也不改变它所需的操作数数目。当不涉及到定义的类对象时,它仍然执行系统预定义的运算,只有用到自己定义的对象上,才执行新定义的操作。
4、类运算符和友元运算符的区别
如果运算符所需的操作数(尤其是第一个操作数)希望进行隐式的类型转换,则运算符应通过友元来重载;如果一个运算符的操作需要修改类对象的状态,则应当使用类运算符,这样更符合数据封装的要求。但参数是使用引用还是对象,则要根据运算符在使用中的情况来定。
如果对象作为重载运算符函数的参数,则可以使用构造函数将常量转化为该类型的对象;如果使用引用作为参数,因为这些常量不能作为对象名使用,所以编译系统就会报错。再使用时,一定分清楚场合及使用方法。
成员运算符比友元运算符少一个参数,这是因为成员运算符具有this指针。
5、下标运算符“[ ]”的重载
class TestArray{
private:
int _size;
int* data;
public:
TestArray(int);
~TestArray(){
delete []data;//释放数组所占的内存空间
}
const int size(){
return _size;
}
int& operator[](int);//使用类运算符来进行重载
};
TestArray::TestArray(int a){
if(a<1){
cout << "数组初始化长度不能小于1" << endl;
exit(1);//退出程序
}
this->_size = a;
this->data = new int[a];
}
int& TestArray::operator[](int b){
if(b<0||b>_size-1){//检查数组是否越界
cout << "数组越界" << endl;
delete []data;
exit(1);
}
return data[b];
}
#include "Example4.h"
void example4();
int main() {
example4();
return 0;
}
void example4(){
TestArray t(10);
cout << "数组的大小是:" << t.size() << endl;
//给数组赋值
for(int i=0;i<t.size();i++){
t[i] = 10*(i+1);
}
//循环输出数组内容
for(int j=0;j<t.size();j++){
cout << t[j] << " ";
}
// 数组的大小是:10
// 10 20 30 40 50 60 70 80 90 100
}
二、流类库
C++的流类库由几个进行I/O操作的基础类和几个支持特定种类的源和目标的I/O操作的类组成。
1、流类库的基础类
在C++中,输入输出是通过流来完成的,C++的输出操作将一个对象的状态转换成一个字符序列,输出到某个地方。输入操作也从某个地方接收到一个字符序列,然后将其转换成一个对象的状态所要求的格式。这看起来像数据在流动,于是把接收输出数据的地方叫做目标,把输入数据的地方叫做源,而输入输出操作可以看作字符序列在源、目标以及对象之间的流动。C++把与输入和输出有关的操作定义为一个类体系,把执行输入输出操作的类体系叫做流类,而提供这个流类实现的系统库就叫做流类库。
C++的流类库预定义了4个流,它们是cin、cout、cerr和clog,可以将cin视作类istream的一个对象,而将cout视为ostream的一个对象。
流是一个抽象概念,当实际进行I/O操作时,必须将流和一种具体的物理设备联接起来。C++的流类库预定义的4个流所联接的具体设备为:
cin | 与标准输入设备相联接 |
cout | 与标准输出设备相联接 |
cerr | 与标准错误输出设备相联接(非缓冲方式) |
clog | 与标准错误输出设备相联接(缓冲方式) |
操作系统在默认情况下,指定标准输出设备是显示终端,标准输入设备是键盘。
2、默认输入输出格式控制
关于数值数据,默认方式能够自动识别浮点数并用最短的格式输出,例如将5.0作为5输出,输入3.4e+2就会输出340等。还可以将定点数分成整数和小数部分,示例如下:
void example5(){
double d = 5.0;
cout << d << endl; //5
double d1 = 3.4e+2;
cout << d1 << endl; //340
int a;double b;
cin >> a >> b; //将定点数分为整数和小数部分,比如输入:20.55
cout << a << " " << b; //输出20 0.55
}
特别要注意字符的读入规则,对单字符来讲,它将舍去空格,直到读到字符为止。示例如下:
void example6(){
//读取单字符
char z;
cin >> z; //输入字符: 1
cout << z << endl; //输出:1(舍去空格,直到读取到字符)
//读取连续的字符
char a,b,c;
cin >> a >> b >> c; //输入:123
cout << a << " " << b << " " << c; //输出1 2 3
}
对字符串来说,它从读到第一个字符开始,到空格符结束。对于字符数组,使用数组名来整体读入。示例如下:
void example7(){
//读取字符数组
char c[8];
cin >> c; //输入:helloworld (遇到空格就结束)
c[7] = '\0'; //为了避免赋值的数组越界,应强制在字符数组末尾加上结束符,因为字符数组只有在初始化赋值时才会加上结束符,其他时候由系统决定
cout << c << endl; //输出hellowo (结束符不会输出)
//读取字符串
string s;
cin >> s; //输入:hello world
cout << s << endl; //输出:hello (遇到空格就结束)
}
关于字符数组与数组越界问题可以参考: https://blog.csdn.net/u010355144/article/details/44976495
对于字符指针,尽管为它动态的分配了地址,但也只能采取逐个赋值的方法,它不仅不以空格结束,反而舍弃空格(读到字符才计数)。因为字符串没有结束位,所以将字符串作为整体输出时,有效字符串的后面将会出现乱码,这时要手工字符串结束符来消除乱码。示例如下:
void example8(){
const int size = 5;
char *p = new char[size]; //声明char类型的数组指针
for(int i=0;i<size-1;i++){ //输入:he llo
cin >> *(p+i);
}
p[size-1] = '\0';
cout << p; //输出:hell (忽略了空格,一直读取字符,直到遇到结束符)
}
对于布尔型的值,如果输入为0则是false,否则均为true。输出的时候则只有0和1两个值。示例如下:
void example9(){
bool b = 0;
string s;
s = b?"yes":"no";
cout << s << endl; //no
cout << b << endl; //0
bool b1 = true;
string s1;
s1 = b1?"yes":"no";
cout << s1 << endl; //yes
cout << b1 << endl; //1
}
三、文件流
在C++里,文件是通过流来完成的,C++有输入文件流(ifstream)、输出文件流(ofstream)和输入输出文件流(fstream)三种,并已经将它们进行标准化。
1、使用文件流
示例如下:
#include <fstream>
void example10();
int main() {
example10();
return 0;
}
void example10(){
const int size = 10;
char ch[size];
char *p = "hello";
ofstream out;//建立文件输出流
out.open("test.txt");//打开一个测试的文件
out << p;
out << "world";
out.close();//关闭输出流
ifstream in("test.txt");//建立文件流
for(int i=0;i<size+1;i++){//循环输入字符
in >> ch[i];
}
ch[size] = '\0';//将最后一个字符置为结束符
in.close();//关闭输入流
cout << ch;//helloworld
}
2、几个典型流成员的函数
⑴ 输出流的open函数
它有3个参数,第1个是要打开的文件名,第2个是文件的打开方式,第3个是文件的保护方式,一般都使用默认值。第2个参数可以取如下的值:
ios_base::in | 打开文件进行读操作,可避免删除现存文件的内容 |
ios_base::out | 打开文件进行写操作,这是默认模式 |
ios_base::ate | 打开一个已有的输入或输出文件并查找到文件尾 |
ios_base::app | 打开文件以便在文件的尾部添加数据 |
ios_base::binary | 指定文件以二进制方式进行打开,默认为文本方式 |
ios_base::trunc | 如文件存在,将其长度截断为0并清除原有内容 |
以上的几个参数,除app外,在其他几种方式下,文件刚打开时,指示当前读写位置的文件指针都定位于文件的开始位置;而app使用文件当前的写指针位于文件末尾。这几种方式也可通过“或”运算符“|”同时使用。
⑵ 输入流的open函数
可以使用默认构造函数建立对象,然后调用open成员函数打开,示例如下:
ifstream in;
in.open("filename",iosmode);
或者使用参数构造函数指定文件名和打开模式,示例如下:
ifstream in("filename",iosmode);
还可以使用指针,示例如下:
ifstream *in;
in->open("filename",isomode);
其中,输入流的打开模式如下:
ios_base::in | 打开文件用于输入,默认模式 |
ios_base::binary | 指定文件以二进制方式打开,默认为文本方式 |
⑶ close成员函数
close成员函数用来关闭与一个文件流相关联的磁盘文件,如果没有关闭该文件,因为文件流是对象,所以在文件流对象的生存期结束时,将自动关闭该文件。不过,应养成及时关闭文件的习惯。
⑷ 错误处理函数
在对一个流对象进行I/O操作时,可能会产生错误,当错误发生时,可以使用文件流的错误处理成员函数进行错误类型判别。成员函数及其功能如下:
bad() | 如果进行非法操作,返回true,否则返回false |
clear() | 设置内部错误状态,如果用缺省参量调用则清除所有错误位 |
eof() | 如果提取操作已经到达文件尾,则返回true,否则返回false |
good() | 如果没有错误条件和没有设置文件结束标志,返回true,否则返回false |
fail() | 与good相反,操作失败返回false,否则返回true |
is_open() | 判定流对象是否成功的与文件关联,如果是返回true,否则返回false |