5.类的高级特性
5.1 类的组合
5.2 静态成员
5.3 常对象与常成员函数
5.4 对象数组与对象指针
5.1 类的组合
类的属性不仅可以是基本数据类型,也可以是类对象,类的组合就是在一个类中内嵌其他类的对象作为成员。因为内嵌对象是该类对象的组成部分,
所以当创建该组合对象时,内嵌对象要先创建。此时要使用成员的初始化列表来完成。可以使用内嵌对象的构造函数或者拷贝构造函数完成创建。
如果内嵌对象的类有无参构造函数,则在组合类构造函数的初始化列表中可以不提供内嵌对象的初始化参数,编译器会自动调用内嵌对象的无参构造函数完成对象的创建。
如果内嵌对象的类有带参数的构造函数,则在组合类构造函数的初始化列表中必须提供该内嵌对象的初始化参数完成创建。
//案例:设计点类Point和圆类Circle,圆形的圆心使用点类对象来表示。
class Point
{
int x;
int y;
public:
Point(int x1, int y1)
{
x = x1;
y = y1;
cout << x << "," << y << "在构造" << endl;
}
Point(const Point& p)
{
x = p.x;
y = p.y;
cout << "Point在拷贝构造" << endl;
}
int getX() { return x; }
int getY() { return y; }
};
//组合类,圆形类
class Circle
{
const double PI;
double radius;//半径
Point center;//圆心,这是一个对象
public:
Circle(double r, const Point& p) :PI(3.14), center(p)//注意,Point有带参数的构造函数,必须在初始化列表位置给出构造函数或者拷贝构造函数,这里用的是拷贝构造
{
radius = r;
cout << "Cirle在构造" << endl;
}
//加一个拷贝构造
Circle(const Circle& c) :PI(3.14), center(c.center)
{
radius = c.radius;
cout << "Circle在拷贝构造" << endl;
}
};
void test01()
{
Point p(20, 30);
Circle c(10, p);
}
组合类的构造过程
如果组合类中有多个内嵌对象,则构造顺序如下:
1)按照内嵌对象的声明顺序依次调用内嵌的对象的构造函数或者拷贝构造函数完成对象的创建
2)执行组合类自身的构造函数
//三角形类,需要三个点的内嵌对象
class Triangle
{
//这里的声明顺序决定了内嵌对象的构造顺序
Point point1;
Point point3;
Point point2;
public:
//构造函数提供两个,一个是用Point对象作为参数,一个是用点的坐标来构造
Triangle(const Point& p1, const Point& p2, const Point& p3):point1(p1),point2(p2),point3(p3)//这里使用了拷贝构造
{
cout << "Triangle在构造" << endl;
}
Triangle(int a, int b, int c, int d, int e, int f) :point1(a, b), point2(c, d), point3(e, f)//这里使用了构造函数
{
cout << "Triangle在构造" << endl;
}
void show()
{
cout << "point1:" << point1.getX() << "," << point1.getY() << endl;
cout << "point2:" << point2.getX() << "," << point2.getY() << endl;
cout << "point3:" << point3.getX() << "," << point3.getY() << endl;
}
};
void test02()
{
Point p1(4, 6);
Point p2(8, 16);
Point p3(10, 26);
Triangle t1(p1, p2, p3);
Triangle t2(1, 3, 5, 9, 7, 10);
t1.show();
t2.show();
}
5.2 静态成员
静态成员指的是类中的静态数据成员以及静态成员函数。
静态数据成员的作用:
1)类的所有对象共享同一数据:静态数据成员由类的所有对象共同使用,而非每个对象单独拥有一份副本,有助于在不同对象之间共享信息。
2)存储类级别的信息:存储和类相关的信息,而非和特定对象相关的信息。
静态成员的特点:
静态成员的特点类似于静态变量,在程序运行期间只有一份拷贝,并且始终有效,不会被回收。它可以由所有的对象共同访问和维护,实现信息共享。
在定义类时,在某个数据成员前加static关键字即可。
格式:static 类型标识符 静态数据成员名;
static int count;
初始化:静态成员需要在全局范围即类外初始化,不能在类内初始化(除非它是常量),格式:类型标识符 类名::静态数据成员名=初始值;
访问:可以使用对象来访问,还可以使用类名来访问,格式:类名::静态数据成员名
class Child
{
static int count;//用于统计所有实例化对象的数量
int number;//学号
char name[20];//姓名
int age;//年龄
public:
//构造的时候需要增加统计数量
Child(const char* n, int a)//学号不在参数中传入,让它自动增长
{
count++;//每次构造一个新对象都对统计数字+1
number = 2025100 + count;
strcpy_s(name, strlen(n) + 1, n);
age = a;
}
int getNumber() { return number; }
char* getName() { return name; }
int getAge() { return age; }
int getCount() { return count; }
//静态成员要在类外初始化
int Child::count = 0;
void test03()
{
Child c1("张三", 8);
cout << c1.getNumber() << "," << c1.getName() << "," << c1.getAge() << "," << c1.getCount() << endl;
Child c2("李四", 7);
cout << c2.getNumber() << "," << c2.getName() << "," << c2.getAge() << "," << c2.getCount() << endl;
Child c3("王五", 9);
cout << c3.getNumber() << "," << c3.getName() << "," << c3.getAge() << "," << c3.getCount() << endl;
}
静态成员函数
定义类时,在某个成员函数前加static关键字,它就成为静态成员函数,它的主要作用:
1)访问静态数据成员:静态成员函数只能访问静态数据成员,不能访问非静态数据成员,也不能调用非静态成员函数。因为它不与特定对象绑定。
但是反过来,普通成员函数却可以访问静态成员(静态数据成员和静态成员函数),因为静态成员是类公有的,属于所有对象。
2)提供类级别的操作:静态成员函数可以提供与类相关的操作,而非和特定对象相关的操作。同时可以通过(类名::静态成员函数)这种方式,用类来访问静态成员函数。
所以,对于公有的静态成员函数,既可以通过对象来调用,也可以通过类名来调用。而普通的成员函数,只能通过对象来调用。
//定义一个静态成员函数,用来访问静态属性
static int getStaticCount()
{
return count;
//静态方法不能访问普通成员(属性和方法)
//cout << number << endl;
//getAge();
}
5.3 常对象与常成员函数
常对象:
指的是使用const修饰的对象,此时对象成为常量,一旦对象的数据成员被初始化之后就无法被修改。主要作用:
1)保证数据安全,如果希望某个对象在程序运行期间不会修改,那么就将它声明为常对象。
2)支持函数重载和泛型编程(下一个阶段再将),常对象可以调用常成员函数,利用这个特性可以实现函数的重载,可以为普通对象和常对象提供不同的处理逻辑。
格式:const 类名 对象名;
常成员函数:
指的是使用const修饰的成员函数,但是要注意const的位置是位于函数的参数列表的后面。主要作用:
1)用来访问常对象的属性。
2)可以保证不修改对象的属性。常成员函数承诺不会修改调用它的对象的属性,当普通对象调用常函数时,不用担心属性被修改。
访问规则:
1)因为常对象的属性不能修改,所以常对象禁止调用普通成员函数(普通函数可以修改对象的属性),它只能调用常函数。
2)同理,常函数也不能调用普通函数。
3)常对象可以调用静态成员函数(因为静态成员函数禁止调用普通成员函数)
4)普通对象是可以调用常函数的。
语法格式:
函数返回值 函数名(参数表) const{函数体代码;}
```c++
//定义一个常函数
void show() const
{
cout << "执行常函数show() const" << endl;
cout << number << "," << name << "," << count << endl;//常函数可以访问普通属性和静态属性
//getNumber();//常函数不能调用普通函数
getStaticCount();//常函数可以调用静态函数
}
void test05()
{
//创建一个常对象
const Child const_c("赵六", 6);
Child c1("张三", 8);
const_c.show();//常对象调用常函数
const_c.getStaticCount();//常对象也可以调用静态函数
//const_c.getName();//常对象不能调用普通函数
c1.show();//普通对象可以调用常函数
}
常函数与普通成员函数的重载
class Data
{
int value;
public:
Data(int v):value(v){}
//普通函数获取value值
int getValue() { cout << "调用普通函数" << endl; return value; }
int getValue() const { cout << "调用常函数" << endl; return value; }
void setValue(int v) { value = v; }
};
void test06()
{
Data d(2);
cout << d.getValue() << endl;
d.setValue(20);
cout << d.getValue() << endl;
const Data const_d(30);
cout << const_d.getValue() << endl;
}
5.4 对象数组与对象指针
对象数组:存放对象的数组
定义:类名 数组名[常量表达式];
访问方式:数组名[下标].对象的成员;
对象指针:指向对象的指针
每个对象创建之后会占用存储空间,所以可以使用指针来指向这个存储空间,来访问对象,对象指针存储的就是对象的地址。
定义:类名 *对象指针名;
//对象数组
Child c[3] = {Child("张三",7),Child("李四",9),Child("王五",10)};
c[0].show();
c[1].show();
c[2].show();
//用对象指针保存以上三个对象的地址
Child* p[3];
p[0] = &c[0];
p[1] = &c[1];
p[2] = &c[2];
p[0]->show();
6.输入输出与文件读写
6.1 流概念
6.2 cout和cin
6.3 格式化输入输出
6.4 文件读写
6.1 流概念
流是抽象的概念,比喻为流水。指的是计算机里的数据就像流水一样从一个对象流向另外一个对象。这些对象是抽象的概念,可以是计算机的一些输入输出设备:键盘、屏幕、内存、文件。
数据在这些设备之间流动是通过输入输出流的类库来实现的,比如说#include <iostream>这个头文件里包含了很多输入输出要用到的对象,如cout是输出流对象,cin是输入流对象。
注意:如何分辨输入和输出?要站在内存的角度来看,因为程序运行在内存中。当数据从外部流进内存就是输入,当数据从内存流出到其他设备,就是输出。
流类库的结构
1)ios_base类:表示流类的一般特征,最上面的基类
2)ios类:继承ios_base类,其中包含了一个指针成员可以操作流缓存
3)ostream类:继承ios类,提供输出方法
4)istream类:继承ios类,提供输入方法
5)iostream类:继承ostream和istream,既可以输入也可以输出
6)ifstream类:继承istream类,提供文件的输入方法(读取文件)
7)ofstream类:继承ostream类,提供文件的输出方法(写文件)
8)fstream类:继承iostream类,提供文件的输入和输出方法(读写文件)
6.2 cout和cin
cout是输出流对象,用于数据输出,使用方式有两种:
1)使用插入运算符<<来输出:
因为cout是iostream头文件中定义的对象,这个对象来自于ostream类,这个类中定义了重载运算符<<的函数,所以可以使用<<进行输出,它能识别所有的基本数据类型和字符串类型。
例如:cout<<"hello"; cout<<基本类型的变量;
它的输出过程是将<<后面的值输出到cout流对象中,然后再输出到cout关联的设备上,即显示器上。
2)使用cout的成员函数put和write来输出:
put:ostream类中的方法,可以将单个字符输出。
write:ostream类中的方法,可以将字符串输出。
cin是输入流对象,用于数据输入,使用方式有两种:
1)使用提取运算符>>来输入:
cin是istream类的对象,这个类中重载了>>运算符,使之可以识别基本类型和字符串。
例如:int i;cin>>i;表示从输入流对象cin中提取一个整形数据存储到变量i中。
2)使用cin的成员函数get和getline来输入:
get:istream类的方法,可以将单个字符输入。每次读取一个字符到内存中
getline:istream类的方法,可以一次读取一行字符,直到遇到换行符结束。
getline的第一个参数是要保存的内存位置,是char*类型,第二个参数是要读取的最大字符数+1,+1是存储结束符的。
重载的函数的第三个参数可以用于指定结束的标志字符。默认是换行符,一般不要去修改。
cin输入流用到的一些其他函数:
ignore(count,char)函数:
表示在遇到char字符之前,忽略count个字符。主要作用是用来消除上一次输入对下一次输入的影响。
输入的时候,有可能会读到不该读的字符,比如说上一行的换行符,比如说用户也可能多输入一些字符。
此时可以使用ignore()函数忽略上次输入的字符,ignore()是取了默认值ignore(1,'\n'),代表在遇到'\n'之前忽略1个字符。
good()函数:
检测输入的类型是否匹配,匹配返回1,否则返回0.
clear()函数:
清楚错误状态,输入错误的时候,比如用good函数检测到输入类型不匹配的时候,会产生输入错误状态,此时就不能再输入。
如果想继续输入,就要用clear函数清楚错误状态,就可以继续输入了。
sync()函数:
清空输入缓存,当发生了错误输入的时候,缓存区就保存了部分错误数据,如果想继续输入正确的数据,就要清空缓存,再次输入。
//cout
char c[] = "C++Programming";
//put单次输出单个字符
cout.put('A') << endl;
cout.put('B').put('C').put('\n');//连续输出
//write可以输出字符串,第一个参数是要输出的字符串,第二个参数可以指定输出的字符个数
cout.write(c, strlen(c)).put('\n');
cout.write("ABCDEFG", 4).put('\n');
//cin
cout << "下面是输入:" << endl;
//提取运算符
int i;
cin >> i;
cout << "i=" << i<<endl;
/*
注意,用户输入值后还会输入一个回车换行,此时如果下面还有输入命令的话,
会把这个换行符当作输入读进来,此时可以使用ignore函数忽略上一行多余的输入
*/
cin.ignore();//默认忽略一个字符,以换行符作为结束标志。
//get
char c0;
cin.get(c0);//将输入的字符读到了内存的c0变量中
cout << "c0=" << c0 << endl;
cin.ignore(100,'\n');//也可以是指忽略多个字符,比如100个
//getline
char str1[20];
cin.getline(str1, 5);
cout << "str1=" << str1 << endl;
练习:
//写一个允许用户一直输入的函数,如果输入错误,就让用户继续输入,直到正确为止:
int sales;//销售额,让用户输入
while (true)
{
cout << "请输入销售额:" << endl;
cin >> sales;
if (cin.good()==1)
{
break;
}
else
{
cout << "输入有误,请输入整数:" << endl;
cin.clear();//清楚错误状态
cin.sync();//清空缓存
cin.ignore(100,'\n');//忽略上次最多100个字符,直到遇到换行符为止
}
}
cout << "sales=" << sales << endl;
6.3 格式化输入输出
可以将输入输入进行特定的格式处理,比如设置输出宽度,填充字符,输出精度等等。需要包含头文件#include <iomanip>
有两种格式化的方法:
1)流控制符,配合运算符来使用:
可以直接再输出语句cout<<中直接使用:
setw:设置宽度,只对下次输出有效,右对齐,如果宽度大于实际输出的宽度,左边多余的为止填充空格,如果宽度小于实际输出的宽度,那么就输出全部的数据
setfill:设置填充字符,设置空白位置的填充字符,永久有效,直到再次被修改
setprecision:设置输出精度,设置浮点数的输出精度,永久有效,直到再次被修改。精度的位数取决于你用什么表示法,有定点表示法、科学计数法、默认浮点数计数法。
默认的是浮点数计数法,它包括所有的有效数字位数,包括整数部分和小数部分。如果只想统计小数部分,要用定点表示法。
进制:hex十六进制、oct八进制、dec十进制,永久有效,直到再次被修改。
2)成员函数:
width:设置宽度
fill:设置填充字符
precision:设置输出精度
特殊的地方:函数可以在设置格式的同时,返回旧格式,用于恢复旧格式。
//格式化输入输出
cout << "1234567890" << endl;
//使用流控制符
//设置宽度
cout << setw(6) << 4.5 << endl;
cout << setw(6) << 4.5 << setw(5) << 6.7 << endl;
cout << setw(3) << 1234 << endl;
//设置填充字符
double values[] = { 1.23,24.22,234.34,3955.32 };
cout << "1234567890" << endl;
cout << setfill('*');//一次设置永久有效
for (int i = 0; i < 4; i++)
{
cout << setw(10) << values[i] << endl;
}
cout << setfill(' ');//改回默认
//设置输出的浮点数精度
double pi = 3.141592653;
cout << pi << endl;//默认的精度就是double的精度,它的位数是6位,小数+整数
cout << std::fixed;//使用定点表示法,单独统计小数点后面的位数
int oldPre = cout.precision(4);
cout << pi << endl;
cout << std::defaultfloat;//恢复为默认的浮点数表示法
cout << pi << endl;
//使用流控制符控制精度
cout << setprecision(5) << pi << endl;
//恢复以前的精度
cout << setprecision(oldPre) << pi << endl;
//设置输出的数字进制
int n = 100;//默认十进制
cout << "十进制:" << n << endl;
cout << oct << "八进制:" << n << endl;
cout << hex << "十六进制:" << n << endl;
cout << dec << "十进制:" << n << endl;
//成员函数
//设置宽度
cout << "使用成员函数" << endl;
cout << "1234567890" << endl;
cout.width(6);
cout << "abc" << endl;
cout.width(4);
cout << "abcde" << endl;
//设置填充字符
//先保存之前的设置
char oldFill = cout.fill('#');
for (int i = 0; i < 4; i++)
{
cout << setw(10) << values[i] << endl;
}
//恢复以前的设置
cout.fill(oldFill);
for (int i = 0; i < 4; i++)
{
cout << setw(10) << values[i] << endl;
}
6.4 文件读写
文件可以持久化存储数据,程序运行在内存中,而内存断电即丢,所以如果想把程序允许过程中产生的数据持久化保存,就可以选择文件。
此时要用文件的输入输出流类库,如:ofstream、ifstream、fstream,需要包含对应的头文件#include <fstream>
文件输入输出的一般步骤是:
1)创建文件输入输出流对象
2)打开文件
3)读写文件
4)关闭文件
文件的打开方式决定了不同的操作模式,具体看图,如下所示:
打开文件的方式:
1)先使用构造函数构造文件流对象,然后再使用流对象的open方法打开文件,如:ofstream out_file; out_file.open(文件路径,打开方式);
2)合二为一,再构造函数的重载函数中,直接打开文件,如:ifstream int_file(文件路径,打开方式);
写入文件的方式:
首先要用ofstream对象打开某个文件,选择某种打开方式,然后才可以写入,写入方式有:
1)使用插入运算符<<写入文件,只支持基本类型和字符串。
//写入文件,属于输出
ofstream out_file("D:\\data.txt", ios::out);
//1)使用插入运算符 << 写入文件,只支持基本类型和字符串。
out_file << "hello" << ' ' << 234 << endl;
out_file.close();//使用close函数关闭文件,否则文件会被占用,无法继续操作
2)使用put函数可以将一个单个字符写入文件。
//使用put函数写文件
ofstream out_file("D:\\data.txt", ios::app);//改用追加的方式打开文件
out_file.put('A');
out_file.close();
3)使用write函数可以将内存中一块连续的内容写入文件。这样就可以写入非文本数据。比如二进制数据,比如写入一个对象。
第一个参数:用于指定输出数据的内存起始地址,该地址类型是char*,是字符指针
第二个参数:用于指定写入的字节数,是个整型。
//以二进制方式写入非文本格式的数据,如数组
ofstream out_file;
out_file.open("D:\\data.txt", ios::out | ios::binary);
int arr[] = { 39,53,25,32 };
out_file.write((char*)arr, sizeof(arr));//注意第一个参数必须是char*,如不是,需要强转
out_file.close()
将对象写入文件
//将对象写入文件
class MyClass
{
int m_nLen;
int m_nWid;
public:
MyClass(){}
MyClass(int len,int wid):m_nLen(len),m_nWid(wid){}
void setLen(int len) { m_nLen = len; }
void setWid(int wid) { m_nWid = wid; }
void show() { cout << m_nLen << "," << m_nWid << endl; }
};
void test07()
{
ofstream out_file("D:\\test.txt", ios::out | ios::binary);
MyClass c(20, 10);
out_file.write((char*)&c, sizeof(c));
out_file.close();
}
读取文件
读取属于输入操作,要使用ifstream类的对象
读取文件的方式:
1)使用提取运算符>>进行读取,支持基本类型和字符串,空白符(空格、tab、enter)作为数据之间的分隔符,不会作为数据读取出来。
//写文件,然后读取
ofstream out_file("D:\\read.txt");
out_file << "hello" << " " << 234 << endl;
out_file.close();
//读文件
ifstream in_file("D:\\read.txt", ios::in);
char s[20];
int i;
in_file >> s >> i;//空格是分隔符
cout << s << " " << i << endl;
in_file.close();
2)使用get函数,它有三种重载的用法:
第一种:int get(); 返回读到的字符的ASCII码
//第一种:int get(); 返回读到的字符的ASCII码
char c1;
ifstream in_file("D:\\read.txt");
c1 = in_file.get();
cout << "c1=" << c1 << endl;
第二种:istream& get(char c); 读取到的字符给了c,返回一个输入流对象
char c2;
in_file.get(c2);
cout << "c2=" << c2 << endl;//读到了h后面的e,因为读指针在往后走
第三种:istream& get(char* buf,int num,char c); 读到的字符存入buf开始的内存地址,直到读到num个字符或者遇到了c字符才结束。c默认是换行符。
char str[100];
in_file.get(str, 99);
cout << "str=" << str << endl;
in_file.close();
3)使用getline函数,一次读取一行内容
此处的getline跟cin.getline是两个函数:一个是用于从文件中读取内容的,一个用于从键盘输入读取内容的。
getline(char*,int,char);
第一个参数,用于存放读到的字符的开始地址
第二个参数,指定读出字符的最大字节数
第三个参数,指定分隔符,表示结束,默认是换行符
//3)使用getline函数,一次读取一行内容
char a[100];
ifstream in_file("D:\\read.txt");
//文件中有三行,需要读三次
for (int i = 0; i < 3; i++)
{
in_file.getline(a, 99);
cout << a << endl;
}
in_file.close();
4)使用read函数,可以读取一整块数据到内存中,包括那些非文本数据,比如对象,二进制数据等
read(char* buf,int n);
第一个参数:用于存放读到的字符的开始地址
第二个参数,指定读出字符的最大字节数
//4)使用read函数,可以读取一整块数据到内存中,包括那些非文本数据,比如对象,二进制数据等
//把刚刚写入test.txt文件中的MyClass对象读出来,放到对象里
ifstream in_file("D:\\test.txt", ios::in | ios::binary);
//准备一个对象存放读到的对象
MyClass m;
in_file.read((char*)&m, sizeof(m));
m.show();
in_file.close();
文件读写指针
文件的读写指针是用于操作文件读写位置的,文件的读写都是按照数据在文件中的顺序依次进行的,不会重复读写同一个位置。
读写指针是保存在流对象中的。我们可以人为改变读写指针的位置再去读写,但是此时一定要注意指针的位置,否则会发生读写错误。
以下四个函数可以操作读写指针:
1)写指针函数:
ofstream中的函数,可以指定下一次写数据的位置。
seekp函数:seek是求索探索,p代表put写,用来移动写指针到指定位置
tellp函数:告诉我写指针当前所在的位置。会返回指针当前位置。
2)读指针函数:
ifstream中的函数,可以指定下一次读数据的位置。
seekg函数:seek是求索探索,g代表get读,用来移动读指针到指定位置
函数用法:以读指针为例
seekg(int n):n>=0的整数,当n>0时,表示移动指针到文件的第n个字节后,n=0时,表示移动到文件的起始位置
seekg(int n,ios::beg):beg时begin的缩写,表示从起始位置开始移动,n>=0的整数,当n>0时,表示移动指针到文件的第n个字节后,n=0时,表示移动到文件的起始位置
seekg(int n,ios::end):表示从结尾位置向前移动,:n>=0的整数,n>0时,从结尾位置向前移动n个字节,n=0时,设置在结尾位置。
seekg(int n,ios::cur):cur是current,表示当前位置,有可能是文件的任意位置,表示从当前位置向前或者向后移动n个字节(正数向后移动,负数向前移动)
streampos n=ifstream对象.tellg(); //返回读指针当前的位置,这个位置用streampos来表示,pos是position的缩写,代表位置,streampos类型可以当int看待。
tellg函数:告诉我读指针当前所在的位置。会返回指针当前位置。
写指针的函数用法同上。
给文件中写入两个MyClass对象,然后通过移动读指针,只读第二个对象
void test12()
{
MyClass c1(100, 50);
MyClass c2(50, 20);
//写入文件
ofstream out_file("D:\\obj.txt", ios::out | ios::binary);
out_file.write((char*)&c1, sizeof(c1));
out_file.write((char*)&c2, sizeof(c2));
out_file.close();//写完要关闭文件,后面才能继续操作文件
//开始读文件
ifstream in_file("D:\\obj.txt", ios::in | ios::binary);
MyClass c;//用它保存读到的对象
//开始移动指针,移动到第二个对象开始处
in_file.seekg(sizeof(c1), ios::beg);//跳过了第一个对象
in_file.read((char*)&c, sizeof(c2));//读出来第二个对象c2,存入c中
c.show();//c展示的就是c2的属性
in_file.close();
}
错误处理函数:
错误处理函数的作用是检测流对象当前的状态,以便出现错误的时候进行正确处理。
bad():返回bool值,为true代表出现严重错误,此时就不能再操作文件了。
fail():返回bool值,为true代表某种操作失败,比如文件打开操作失败,或者不能读出数据,或者读到的数据类型不匹配等等。
good():返回bool值,如果以上错误未发生,就表示一切正常,此时good函数会返回true
clear():清除所有错误状态,以便继续操作。
eof():用于判断是否到达文件结尾。如果到达结尾返回非零值,可以认为是true,否则返回0,可以认为是false
注意:文件结尾并不是文件的最后一个字符。是通过一个特定字符来表示的,这个字符是0xFF,即-1,它要放到文件最后一个字符的下一个位置。
当程序中的文件流对象不能再读入数据时,才发现已经达到文件结尾,此时才给文件设置结尾标志,才会返回真,这个案例中abcd四个字符,
读完c之后,继续读发现没有了,get读取失败,所以此时char c中保存的仍然是d,所以d会输出两次。如下代码所示:
{
//eof函数的使用
//读取D盘的eof.txt文件,每次读出一个字符,将其输出,直到文件结尾
char c;
ifstream in_file("D:\\eof.txt");
if (in_file.good())
{
while (!in_file.eof())
{
in_file.get(c);
cout << c;
}
}
in_file.close();
}
改进方案
:在输出之前先做一次检测,如果发现读取失败了就不输出了。
void test14()
{
//eof函数的使用
//读取D盘的eof.txt文件,每次读出一个字符,将其输出,直到文件结尾
char c;
ifstream in_file("D:\\eof.txt");
if (in_file.good())
{
while (!in_file.eof())
{
in_file.get(c);
if (in_file.fail())
{
break;
}
cout << c;
}
}
in_file.close();
}
//如果不是单个字符读取,而是整行读取,就不会有这个问题。
void test15()
{
char a[100];
ifstream in_file("D:\\eof.txt");
while (!in_file.eof())
{
in_file.getline(a, 99);
cout << a << endl;
}
in_file.close();
}
输入输出文件流fstream
fstream既能输入又能输出,可读可写。
fstream iofile(文件位置,ios::in|ios::out);
fstream用起来比较复杂,需要注意读写两个指针的实时位置,容易出错,建议还是用ifstream和ofstream来分别读写。
void test16()
{
//使用fstream流对象来对D盘的d.txt文件进行读写操作
fstream iofile("D:\\d.txt", ios::in | ios::out);
//将读到的内容,存储到堆内存,可以根据文件的大小,来申请对应的堆内存大小
//那如何统计文件的大小呢?
//可以将读指针移动到文件结尾,然后获取当前的指针位置,返回值即文件大小的字节数
iofile.seekg(0, ios::end);
streampos len_of_file = iofile.tellg();
int len = len_of_file;
char* data = new char[len + 1];//多申请一个保存结束符
//初始化这个空间
memset(data, 0, len + 1);//表示将data开始的len+1个字符空间,初始化为0,0代表的就是结束符'\0'
//开始将读到的内容存储到堆空间data中
//注意!此时读指针的位置已经在末尾,所以还需要移动到开头才能读
iofile.seekg(0, ios::beg);
iofile.read(data, len);
cout << "从文件中读到的内容:";
cout << data << endl;
//再进行写操作,把读到的内容再写回去
iofile.seekp(0, ios::end);//再将写指针移动到末尾,不覆盖原来的内容
iofile.write(data, len);
delete[]data;
data = NULL;
iofile.close();
}
7.调试与异常
7.1 C++开发常见问题
7.2 VS调试技巧
7.3 异常概念
7.4 异常处理语句
7.1 C++开发常见问题
主要归纳三个问题:粗心问题,内存问题,语法问题
1)数字和字母的混淆,0和o,1和l等等容易混淆,中英文混淆,主要是符号。
2)数组下标从0开始,数组不能越界访问。内存越界比如字符串没有加结束符,会导致越界读到乱码。
3)括号匹配,尤其是层次多的时候。写代码的良好习惯就是先把括号结构写出来,再写代码。
4)分号的问题,类结束要有分号,函数结束不能有分号(除非函数写在一行)
5)变量的作用域问题,要时刻关注你的变量作用域,尤其是一个变量被多次使用的情况下,还有多个作用域变量同名情况下。
6)文件读写时,忘记关闭文件,会导致文件被占用,后面的程序就无法再次对文件进行读写了。
7)运算符优先级问题,C++的运算符很多,优先级不同,共同使用时受到了优先级的影响,如果记不住,要用()来保证优先级。
8)语法问题,C++的语法多变而复杂,多去重复以达到熟练,还有一些是新特性,后面课程再说。
9)内观管理问题,使用堆内存的时候容易出问题。总结了几点:
定义指针的时候,如果此刻没有确定的指向,先指向空NULL
使用指针的时候,先判断这个指针是不是空
指针操作常量字符串的时候,注意不能修改字符串的值。
避免内存泄露,申请的堆内存,不用的时候记得释放掉,释放掉之后,再把指针置空,防止出现悬空指针。
7.2 VS调试技巧
调试是什么
调试是软件开发中很重要的部分,是每个程序必将经历的过程,调试技巧是每个开发人员必备的技能。调试英文是Debug,也叫做代码的除错,是发现代码错误,定位错误,修复错误的一个过程。
VS的调试方法:
1)打断点,进行调试
断点即程序的中断点,执行到这里会暂停,以便我们观察程序此时的状态,变量的值。
将光标定位某一行代码处,使用鼠标左键或者快捷键F9打断点或者取消断点。
2)观察断点处,程序变量的值,左下方会出现一个局部变量窗口,可以观察局部变量的值。
3)观察表达式的值:切换到监视窗口,可以添加表达式
4)条件断点:一般用在循环中,如果需要在某个特定条件下中断程序执行,需要使用条件断点。关键是设置一个返回bool值表达式,当表达式为true时,程序中断执行。
5)F5:启动调试
Ctrl+F5:启动但不调试
F9:切换断点
F10:逐过程调试,一个函数会当作一个过程,不会进入函数内部。 F11:逐语句调试,会进入函数内部,逐条语句执行。
7.3 异常概念
异常就是在程序运行过程中发生难以预料的、不正常的事件而导致程序偏离了正常的流程的现象。
注意:异常跟bug的概念不一样,即使程序没有bug,能编译通过并且正常运行,也可能会碰到一些意外情况导致异常。
所以,如果出现了异常,会导致程序中断。如果不想让程序中断,可以通过对异常的处理,即提前准备好相关的预案,程序就不会被中断。
举几个异常的情况:
访问数组的下标越界,在越界时又写入了数据;
申请堆内存的时候,使用new申请失败,返回了空指针;
算术运算时溢出;
整数除法时除数为0;
通过一个悬空指针访问内存并写入数据;
7.4 异常处理语句
异常发生时如果有异常处理,程序就不会被中断,而是按照异常处理的逻辑对应处理。
异常处理的过程:对异常的检测、捕获、处理和提示、传递。1C++提供了异常的抛出语句throw和异常捕获处理语句try-catch,由它们构成一个异常的流程控制。
异常处理错误的方式:当一个函数发现自己无法处理的错误时,用throw语句抛出异常,让函数的直接调用者或者间接调用者去处理逐个错误。直到抛给main函数,如果main函数也处理不了,程序只能停止了。
语法:throw 异常对象
throw抛出的是一个对象,是一个实例。一旦发生了抛出异常,C++的异常处理机制开始寻找异常处理模块try_catch,寻找其中跟抛出的异常对象类型相匹配的catch语句,
如果找到了就按照提前设置好的异常处理逻辑去处理。
语法:try的作用域里面应该包含可能会出现异常的代码,后面跟上不同的catch语句,来处理不同类型的异常。
一个异常只能被处理一次,如果代码没有异常处理,遇到异常会导致程序中断。如果加了异常处理,会按照处理方式去处理,然后继续执行后面的代码。
语法:catch(异常类型 异常变量)
用于捕捉对应类型的异常,如果前面写的catch没有捕捉到对应的类型,可以加一个catch(...)来捕捉任意类型的异常。相当于条件分支中的else
异常类型:
基本类型:int、char、float等等基本类型
聚合类型:指针、数组、字符串、结构体、类
标准库异常类:
标准库给我们提供了一个异常基类exception,旗下包括了很多异常的派生类,用于处理不同种类的异常。
一般我们会把派生类的异常捕捉语句catch放在前面去捕获一些具体的异常,基类的异常捕捉放到最后,用于捕捉所有类型的异常。
exception异常类提供了一个what()方法可以返回当前异常的类型信息。
C++语言本身以及标准库中的函数抛出了异常,都是exception类或者其派生类的异常。
我们自己的代码,可以定义自己的异常类型,或者通过继承标准库中的异常基类来使用它的异常类型。
访问动态数组越界实例
//下面函数的作用是访问动态数组中下标为i的值,i有可能越界
void printVector(vector<int>& v, int i)
{
cout << v.at(i) << endl;
}
void test03()
{
vector<int> v = { 1,2,3,4,5,6 };
try
{
//将有可能产生异常的代码写道try中
printVector(v, 6);//这里的下标6越界了,会除法异常
}
catch (const out_of_range& e)//捕捉具体的异常子类
{
cout << "动态数组访问越界" << endl;
cout << e.what() << endl;//打印异常相关信息
}
catch (const exception& e)//所有标准库异常的基类,如果上面没有捕捉到对应的类型,那就通过基类捕捉
{
cout << e.what() << endl;
}
}
异常层次:
同一个try语句可以对应多个catch语句,catch语句可以定义具体的异常类型来处理,不同类型的异常由多个catch来处理。
try语句用来检测有可能出异常的代码,如果前面的catch没有捕获到对应的异常类型,可以在最后添加catch(...)来捕获所有异常。
throw抛出的异常必须被catch语句处理,如果当前函数能够处理,处理完就会继续往后执行。如果不能不能处理,异常会顺着函数的调用栈向上传递,直到被处理为止,否则程序会停止。
演示异常处理,发生了除零错误
int div()
{
int a;
int b;
cout << "输入两个整数,空格分开:" << endl;
cin >> a >> b;
if (b==0)//加检测,如果除数为零,就抛出异常
{
throw "发生除零错误";
//如果改为抛出int的异常,就会除法int的catch语句
//throw 0;
//throw 'a';//这个类型会被catch(...)来处理
}
return a / b;
}
void test04()
{
//如果没有异常处理语句,调用div的时候发生了异常,就会导致程序中断
int try_res = div();
cout << "结果是:" << try_res << endl;
}
void test05()
{
//加上异常处理语句,程序遇到异常就按照对应的类型去处理,不会导致中断
try
{
int try_res = div();
cout << "结果是:" << try_res << endl;
}
//添加对应的异常类型处理
catch (const char* a)//用来捕获字符串类型的异常
{
cout << a << endl;
}
catch (int i)//用来捕获int类型的异常
{
cout << i << endl;
cout << "发生除零错误" << endl;
}
catch (...)//捕获所有其他类型的异常
{
cout << "发生了未知错误" << endl;
}
}
上嘉路