基础
当VS中一个项目下有两个及以上的源文件时,编译会产生错误:main已经在test.obj中定义;找到一个或多个多重定义的符号。
**解决办法:**将不需要编译的源文件排除:右键“属性”,将“从生成中排除”选择“是”,保存后再运行需要运行的源文件就可以成功,且被排除的文件右下角有红标
常量
两种定义方式
-
define
#define DAY 7
-
const:在程序执行期间不能被修改改变。
const int A = 2; A = 3; //报错,A不可变
规范:把常量定义为大写字母形式
命名规则
- 不能是关键字
- 由字母、数字、下划线组成
- 第一个字符须为字母或下划线
- 区分大小写
数据类型
整形
数据类型 | 内存空间 | 取值范围 |
---|---|---|
short | 2B | -215~215-1 |
int | 4B | -231~231-1 |
long | Win4B、Linux8B(64位) | -231~231-1 |
long long | 16B | -263~263-1 |
sizeof
作用:计算数据类型或者变量所占的内存大小
int a = 2;
cout << sizeof(int/a) << endl; //output:4
浮点型
数据类型 | 内存空间 | 取值范围 |
---|---|---|
float | 4B | 7位有效数字 |
double | 8B | 15~16位有效数字 |
float f = 3.14; //3.14默认为double格式 随后进行float强转
float f = 3.14f; //定义时即为float格式
//科学计数法
float f = 3e2; // f = 3*10^2
float f = 3e-2; // f = 3*0.1^2
字符型
创建字符型变量使用单引号,且单引号内仅有一个字符而不是字符串
- 内存仅占用1B
- 内存中存储的是ASCII码值
字符串型
#include<string>
string str = "hello world";
typedef
typedef int zhengxing;
zhengxing a = 1; //a为一个int型变量
若变量未被显式初始化,全局变量或是静态变量初始值为0,局部变量是随机值。
全局变量隐式初始化
数据类型 | 初始化默认值 |
---|---|
int | 0 |
char | ‘\0’ |
float | 0 |
double | 0 |
pointer | NULL |
变量作用域
局部变量和全局变量名称可以相同,在函数内时,使用的是局部变量的值。
输入输出
- 输出cout
- 输入cin
- 行末换行endl
生成随机数
srand((unsigned int)time(NULL)); //设置随机数种子
int i = rand()%100 + 1; //(0~99)+1 = 1~100
若time出现报错,则需要增加头文件**#include**
数组
- 数组中的每个元素都是相同的数据类型
- 数组是由连续的内存位置存放的
// 定义数组的方式
int a[3];
int a[3] = {1, 2, 3};
int a[] = {1, 2, 3}; //[]内会自动判断个数
// 计算数组个数
sizeof(a) / sizeof(ar[0])
冒泡排序模板
void bubbleSort(int a[], int n){
bool flag = true;
for(int i = 0; i < n-1; i++){
for(int j = 0; j < n-1-i; j++){
if(a[j] > a[j+1])
swap(a[j], a[j+1]);
flag = false;
}
if(flag) break; // 提前结束
}
函数
函数声明
函数声明允许多次,函数定义如果发生多次则会报错。
int max(int a, int b); // 函数定义在main函数后,需要在前面做函数的声明
int main(){
pass;
}
int max(){
pass;
}
分文件函数
- 创建.h的头文件
- 创建.cpp的源文件
- 在头文件中写函数的声明,报错则需要引入头文件iostream
- 在源文件中写函数的定义
- 新文件中引入头文件后调用函数
指针
int a = 10;
int *p;
p = &a; //将p指向a的地址
*p = 100 //*对指针进行解引用(取值)
指针所占的内存大小:不论指针指向什么类型的数据单元,64位下都是占8B,32位都是占4B。
空指针
指针指向内存中编号为0的空间,用于初始化指针
空指针指向的内存是不可访问的
int * p = NULL;
野指针
指针变量指向了非法的内存空间
int * p = (int *)0x1100
指向的地址不属于当前进程地址空间,因此不允许访问。
const修饰指针
-
const修饰指针——常量指针
const int * p = &a; // const修饰指针:指向可变,值不可变
-
const修饰常量——指针常量
int const * p = &a; // const修饰常量:指向不可变,值可变
-
const修饰指针和常量
const int const * p = &a; // const修饰指针和常量:指向不可变,值y可变
结构体
1.1 创建结构体
struct 结构体名 {结构体成员列表}
1.2 创建结构体变量
struct student stu1; //先定义 后赋值
struct student stu1 = {xxx,xxx}; // 定义同时赋值
// 定义结构体时一起创建结构体变量
创建结构体变量时可以省略struct关键字
1.3 结构体指针
-
定义指针时,需要将指针也定义为结构体同类型。
-
访问结构体变量需要适用**->操作符**。
1.4 const在结构体中的使用
-
当形参进行传递时,使用地址传递将避免值传递中的复制开销,但同时由于值传递会影响到原始结构体的值,因此在形参中使用const
void PrintArray(const struct * p){ // p指针指向的结构体仅可读不可写 }
核心
内存分区模型
C++程序在执行时,将内存大致分为4个区域
-
代码区:存放函数体的二进制代码,操作系统管理
-
全局区:存放全局变量、静态变量、常量
-
栈区:由编译器自动分配释放,存放函数的参数值、局部变量等
程序关闭后,再次执行程序局部变量地址会发生改变,因此不要返回局部变量地址,如果强行用指针接收则相当于使用了一个野指针。
-
堆区:由程序员分配和释放,程序员不释放程序结束时由操作系统回收
new操作符
C++中利用new操作符在堆区开辟数据
释放需要使用delete操作符
采用new创建的数据,会返回该数据对应的类型指针。
// 对整形使用
int *p = new int(10);
delete p;
// 对数组使用
int *arr = new int[10];
delete[] arr;
引用
基本用法
给变量起别名,使得另外一个变量也指向同一块内存区域
int a = 10;
int& b = a;
b = 15;
cout << "a=" << a << endl;
cout << "b=" << b << endl;
// output: 15 15
注意事项
- 引用必须初始化(必须要指定内存空间)
- 引用在初始化后不能更改到另一块内存空间(指针常量)。
作为参数传递:使用引用作为形参传递,可以达到和地址传递相同效果
void swap(int &a, int &b){
int temp = a;
a = b;
b = temp;
}
引用做函数值:不要返回局部变量的引用(同指针);声明为引用返回类型的函数调用可以作为左值
int& test(){
static int a = 10;
return a;
}
int main(){
int& res = test();
cout<< res<<endl; // 10;
test() = 100;
cout<< res<<endl; // 100;
}
常量的引用
int& ref = 10; // 报错
const int& ref = 10; // 上述代码相当于
// int temp;
// const int& ref = temp;
使用场景:避免形参使用引用而误改实参值
void(const int& k){
// 此时k仅为可读,不允许更改
}
函数高级
设置默认参数
int fun(int a = 10,int b = 20){
}
有数据传入则使用传入数据,没有则使用默认参数
如果某个位置有默认参数,从这个位置往后都需要有默认参数
函数声明和定义仅有一个可以给出默认参数,不可以同时给
设置占位参数
void fun(int a, int){
// 该函数要求必须传入两个参数
}
该功能用作占位,同时该位参数在调用时也必须输入。
函数重载
函数重载需满足以下条件
- 同一个作用域下
- 函数名称相同
- 函数形参类型不同/个数不同/顺序不同
更改返回值类型不算函数重载
当函数重载与const结合
void func(int& a){
// func1
}
void func(const int& a){
// func2
}
int main(){
int a = 10;
func(a);//调用func1
func(10); //调用func2(类似常量的引用)
}
类和对象
封装
1 类的定义
class Student {
public:
string name;
int stuId;
void printInfo() {
cout << "姓名:" << name << " 学号:" << stuId << endl;
}
};
// 类的使用
int main() {
Student stu1;
stu1.name = "张三";
stu1.stuId = 202401;
stu1.printInfo();
return 0;
}
2 类的访问权限
- public:类内可访问,类外可访问
- protected:类内可访问(子类可访问),类外不可访问
- private:类内可访问(子类不可访问),类外不可访问
main函数也算是类外
class和struct的区别
- class的默认权限为private
- struct的默认权限为public
访问(读写)私有变量
可在类内编写public权限的set方法和get方法来进行赋值和获取私有成员变量的值
类的分文件编写
-
在头文件区创建类名.h文件,编写类内变量、方法【方法只写声明,不写具体实现】
#pragma once // 避免重复引入头文件 #include<iostream> using namespace std; class ...
-
在源文件区创建类名.cpp文件,只编写类内方法的具体实现,其中方法名前需要引用类名
#include"xxx.h" // 无需引入其他头文件 void xxx::setX(){ pass }
-
到主程序中,将全部类的.h文件添加到头文件里
对象的初始化和清理
构造函数和析构函数
-
构造函数:主要作用在创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无须手动调用
类名(){ }
- 构造函数没有返回值也不写void
- 函数名和类名相同
- 构造函数可以有参数,因此可以发生重载
- 程序在调用对象时会自动调用构造,无需手动调用,仅调用一次。
-
析构函数:主要作用在对象销毁前系统自动调用,执行清理工作
~类名(){ }
- 析构函数没有返回值也不写void
- 函数名和类名相同,前面加上~符号
- 析构函数无参数,因此不可以发生重载
- 程序在对象销毁前会自动调用析构,无需手动调用,仅调用一次。
构造函数和析构函数都是必须有的实现,如果没有显式给出,编译器将自动创建一个空实现的构造函数和析构函数。
构造函数的分类和调用
分类
-
按参数:有参构造和无参构造
-
按类型:普通构造和拷贝构造
Person(const Person& p1){ cout << "拷贝构造" << endl; }
调用方式
-
括号法(正常给对象传入参数)
调用无参构造函数不能加括号,如果加括号编译器会认为这是一个函数声明
-
显示法(如果有参数传入则放在等号右边)
Person p1; Person p2 = Person(10); Person p3 = Person(p2);
形如Person(10)只传入参数就是匿名对象,该行执行结束之后直接执行析构函数释放。
拷贝构造的调用时机
-
使用一个已经创建完毕的对象来初始化一个新对象
-
值传递的方式给函数参数传值
void func(Person p1){ // 如果是引用则不会触发拷贝 } void test(){ Person p; func(p); // 此时p将会以隐式拷贝构造的方式传入func }
-
以值方式返回局部对象
Person func() { Person p1; return Person(p1); // 或者只返回p1 看哪个可以运行 } void test(){ Person p = func(); }
构造函数的调用规则
默认情况下,编译器至少会给一个类添加3个函数
默认构造函数、默认析构函数、默认拷贝构造函数(相当于留一个接口给别人作复制)
构造函数调用规则如下:
- 如果定义了有参构造,那编译器不提供无参构造,但是有默认拷贝构造
- 如果定义了拷贝构造,那编译器不会提供其他的构造。
深拷贝与浅拷贝
深拷贝之前会产生什么问题:
如果存在指针变量(堆区),那么在进行拷贝构造时将会使得两个对象的指针都指向同一片内存区域。在函数执行的过程中,采用先进后出的方式,先出栈的会执行析构函数将内存空间释放,后者执行析构函数时将会重复释放引起内存访问问题。
浅拷贝:简单的赋值拷贝操作
深拷贝:在堆区重新申请空间,进行拷贝操作
堆区的拷贝是深拷贝,简单赋值的是浅拷贝
class Person {
public:
Person(int a, int h) {
cout << "有参构造" << endl;
age = a;
height = new int(h);
}
Person(const Person & p1){
age = p1.age;
cout << "拷贝构造" << endl;
height = new int(*p1.height); // 先获取待拷贝的值,另外申请一个空间来存放该值
}
~Person() {
cout << "析构函数" << endl;
if (height != NULL) {
delete height;
height = NULL; // delete后会随机指向一块内存,需要置空
}
}
int age;
int* height;
};
构造函数初始化列表
语法: 构造函数():属性1(值1),属性2(值2){}
class Person
{
public:
Person(int a,int b,int c):m_a(a),m_b(b),m_c(c){}
int m_a;
int m_b;
int m_c;
};
类对象作为类成员
class A{
};
class B{
A a; // 一个A类的对象a作为B的类成员
};
构造:当其他类的对象作为本类的成员时,构造时先构造其他类的对象,再构造自身。也就是先对成员(类A)进行构造函数调用。
析构:自身的析构函数先进行,之后其它类再进行。先对B类进行析构,随后再对成员进行析构
静态成员
静态成员就是在成员变量和成员函数前面加上关键字static
静态成员分为:
-
静态成员变量
- 所有对象共享同一数据
- 在编译阶段分配内存
- 类内声明,类外初始化
两种方法访问静态成员变量:通过对象来访问/通过
类名::变量名
来访问 -
静态成员函数
- 所有成员共享同一个函数
- 静态成员函数只能访问静态成员变量
两种方法访问静态成员函数:通过对象来访问/通过
类名::函数名
来访问
对象模型和this指针
在C++中,类内的成员变量和成员函数分开存储,**空对象的大小是1B,相当于是占位符。**如果有变量则显示变量所占的内存
class Person
{
int m_A; //非静态成员属于类对象上的。
static int m_B; //静态的成员变量不属于类的对象上。
void func() {} //非静态成员函数不属于类的对象上
static void func2(){} //静态成员函数不属于类的对象上
};
只有非静态成员变量(普通成员变量)才属于类的对象上
this指针
this指针指向被调用的成员函数所属的对象
this指针是指针常量,不可以修改指向
-
当形参和成员变量同名时,可用this指针来区分
this->age = age;
-
在类的非静态成员函数中返回对象本身,可使用return *this, 且返回类型需要为引用
Person& PersonAddAge(Person p) { this->age += p.age; return *this; } // main() p2.PersonAddAge(p1).PersonAddAge(p1); // 由于加了引用,因此返回的一直都是p2本身 // 如果不加引用仅返回Person类型,每一次都是返回一个新的对象 cout << p2.age << endl;
空指针调用成员函数
void ShowPersonAge(){
//提高健壮性,空的就直接返回,防止代码崩溃
if (this == NULL){
return;
}
//报错原因是因为传入的指针是NULL——无中生有,用一个空指针访问里面的属性
cout << (this->)m_Age << endl;
}
// func()
Person* p = NULL;
p->ShowPersonAge();
空指针可以直接调用成员函数。但是成员函数内如果涉及成员变量,则变量会以this->m_age的形式出现,this指针指向的是当前对象的变量,但是空指针代表this所指也为空,因此需要在成员函数内增加一个this空指针判断,增加健壮性
const修饰函数和对象
常函数:
- 成员函数后加const后我们称这个函数为常函数
- 常函数不可以修改成员属性
- 成员属性声明时加关键字mutable后,在常函数中依然可以修改
常对象:
- 声明对象前const称该对象为常对象。
- 常对象只能调用常函数,不能调用普通成员函数,因为普通成员函数可以修改属性。
mutable:
- 加了mutable修饰的特殊变量,即使在常函数、常对象中,也可以修改这个值
class Person {
public:
void showPerson() const {
a = 100;
}
void changePerson() {
b = 10;
}
mutable int a;
int b;
};
友元——friend
-
全局函数作友元
class Building { friend void goodgay(Building* building); // 友元的声明 public: Building() { m_SittingRoom = "客厅"; m_BedRoom = "卧室"; } public: string m_SittingRoom; private: string m_BedRoom; }; //全局函数 void goodgay(Building* building){ cout << "好基友全局函数正在访问你的" << building->m_BedRoom << endl; }
-
类作友元
class Building{ friend class GoodGay; public: Building(); // 构造函数类外实现 public: string m_SittingRoom; private: string m_BedRoom; }; class GoodGay{ public: GoodGay(); public: void visit();//参观函数 访问Building中的属性 Building* building; };
-
成员函数作友元
class Building{ friend void GoodGay::visit(); // 需要说明作用域 public: Building(); public: string m_SittingRoom; private: string m_BedRoom; };
运算符重载
加号重载
作用:实现两个自定义数据类型相加的运算,对于内置的数据类型表达式的运算符是不可改的。
语法:
成员函数:Person operator+(Person& p)
// class{}
Person operator+(Person& p){
Person temp;
temp.a = this->a + p.a;
return temp;
}
// call
Person p3 = p1.operator+(p2);
Person p3 = p1 + p2; //简写
全局函数:Person operator+(Person& p1, Person& p2)
// .cpp
Person operator+(Person& p1, Person& p2){
Person temp;
temp.a = p1.a + p2.a;
return temp;
}
// call
Person p3 = operator+(p1, p2);
Person p3 = p1 + p2; //简写
左移重载
作用:可以输出(cout)自定义的类型
class Person{
friend ostream& operator<<(ostream& cout, Person& p);
...
};
// 返回ostream&是为了方便链式输出
ostream& operator<<(ostream& cout, Person& p){
cout << p.a << endl;
return cout;
}
// main()
cout << p << endl;
//output:p.a
一般不利用成员函数来重载<<运算符,如果是成员函数则会变成
void operator<<(cout)
,调用是会变成p >> cout
不符合语法。因此只能利用全局函数来重载左移运算符
递增重载
作用:实现类内变量自增、自减
// 前置递增
MyInteger& operator++() {
++num;
return *this;
// this是指向自己对象的指针(地址),解引用后得到自身对象
}
// 后置递增
// 不使用引用因为不可返回局部变量的引用
MyInteger operator++(int) {
MyInteger temp = *this;
num++;
return temp;
}
总结:前置递增返回引用(可链式),后置递增返回值。
赋值重载
作用:如果类中有属性指向堆区,做赋值操作时也会出现深浅拷贝问题。
Person(int age){
m_Age = new int(age);
}
//重载赋值运算符
Person& operator=(Person &p){
//应该先判断是否有属性在堆区(原来已经赋了值),如果有先释放干净,然后再深拷贝。
if (m_Age != NULL){
delete m_Age;
m_Age = NULL;
}
//深拷贝操作
m_Age = new int(*p.m_Age);
return *this;
}
关系运算符重载
作用:可以让两个自定义类型对象进行对比操作
bool operator==(Person &p){
if(this->a==p.a){
return true;
}
else
return false;
}
函数调用重载
- 由于重载后使用的方式非常像函数的调用,因此称为仿函数
- 仿函数没有固定写法,非常灵活
class MyPrint{
public:
void operator()(string text){
cout << text << endl;
}
};
// main()
MyPrint myprint;
myprint("hello world");
// 匿名函数对象
MyPrint()("hello world")
匿名函数对象特点:当前行被执行完立即释放
继承
class 子类:继承方式 父类
子类也称派生类,父类也称基类
一个子类继承了所有的父类方法,但下列情况除外:
- 父类的构造函数、析构函数和拷贝构造函数。
- 父类的重载运算符。
- 父类的友元函数。
当一个类派生自基类,该基类可以被继承为 public、protected 或 private 几种类型。继承类型是通过访问修饰符 (继承方式) 来指定的
通常使用 public 继承。当使用不同类型的继承时,遵循以下几个规则:
- 公有继承(public):当一个类派生自公有基类时,基类的公有成员也是派生类的公有成员,基类的保护成员也是派生类的保护成员,基类的私有成员不能直接被派生类访问,但是可以通过调用基类的公有和保护成员来访问。
- 保护继承(protected): 当一个类派生自保护基类时,基类的公有和保护成员将成为派生类的保护成员。
- 私有继承(private):当一个类派生自私有基类时,基类的公有和保护成员将成为派生类的私有成员。
构造函数和析构函数顺序:
继承中先调用父类构造函数,再调用子类构造函数,析构顺序与构造相反
继承同名成员处理方式:
-
访问子类中与父类同名的成员,直接访问
cout << son.a << endl;
-
访问父类同名成员,需要加作用域
cout << son.Base::a << endl;
函数同名同上
如果是静态成员,不仅可以通过上述的利用对象来访问,还可以通过类名来访问
Son::func();
Son::Base::func();
多继承
class 子类:继承方式 父类1,继承方式 父类2
多继承可能会引发父类中有同名成员出现,需要加作用域区分
cout << "第一个父类的m_A:" << son1.Base1::m_A<<endl;
cout << "第二个父类的m_A:" << son1.Base2::m_A<<endl;
菱形继承
两个派生类B、C继承同一个基类A,又有某个类D同时继承这两个派生类B、C,这种继承称为菱形继承,或者钻石继承。
D类将B、C中同样的数据继承了两份,造成资源浪费且具有二义性
-
利用虚继承可以解决菱形继承问题——virtual
class Animal{ public: int m_Age; }; //在继承之前加上关键字virtual变为虚继承 // Animal类称为虚基类 class Sheep:virtual public Animal{}; class Tuo:virtual public Animal{};
多态
多态分为两种
- 静态多态:函数重载和运算符重载属于静态多态
- 动态多态:派生类和虚函数实现运行时多态
静态多态和动态多态的区别
- 静态多态的函数地址早绑定 - 编译阶段确定函数地址
- 动态多态的函数地址晚绑定 - 运行阶段确定函数地址
class Animal{
public:
//加上virtual变成虚函数,实现地址晚绑定
virtual void speak(){
cout << "动物在说话"<< endl;
}
};
class Cat :public Animal{
public:
void speak(){
cout << "小猫在说话" << endl;
}
};
void doSpeak(Animal &animal) //Animal &animal = cat;
// 早绑定看编译阶段(左animal)
// 晚绑定看运行阶段(右cat)
{
animal.speak();
}
// main()
Cat cat;
doSpeak(cat);
动态多条满足条件:
- 有继承关系
- 子类重写父类的虚函数
重写要求:
函数返回值类型、函数名、参数列表完全相同
动态多态的使用条件:
父类的指针或者引用指向子类的对象
Animal &animal = cat
纯虚函数和抽象类
virtual 返回值类型 函数名 (参数列表) = 0;
当类中有了纯虚函数,这个类也称为抽象类。
抽象类特点:
- 无法实例化对象
- 子类必须重写抽象类中的纯虚函数,否则也属于抽象类
一般而言纯虚函数的函数体是缺省的,但是也可以给出纯虚函数的函数体(此时纯虚函数变为虚函数)。
- 在定义纯虚函数时,不能定义虚函数的实现部分
- 在没有重新定义这种纯虚函数之前,是不能调用这种函数的
虚函数的析构函数
多态使用的时候,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码,会出现内存的泄漏情况。
解决方法:将父类中的析构函数改为虚析构或者纯虚析构
// 虚析构语法
// 虚析构可以直接写函数体
virtual ~类名(){
...
}
// 纯虚析构语法
virtual ~类名() = 0;//声明
// 类外实现(如基类也有堆区空间需要释放)
类名::~类名(){
...
}
总结:
- 虚析构或纯虚析构就是用来解决通过父类指针释放子类对象问题
- 如果子类中没有堆区数据,可以不写为虚析构或纯虚析构
- 拥有纯虚析构函数的类也属于抽象类
文件
文件类型分为两种:
- 文本文件-文件以文本的ASCII码形式存储在计算机中
- 二进制文件-文件以文本的二进制形式存储在计算机中,用户一般不能直接读懂他们
操作文件的三大类
- ofstream:写操作
- ifstream:读操作
- fstream:读写操作
o:output, i:input
写文件
- 包含头文件——#include< fstream>
- 创建流对象——ofstream ofs;
- 打开文件——ofs.open(“文件路径”,打开方式)
- 写数据——ofs<<“写入的数据”;
- 关闭文件——ofs.close();
读文件
-
包含头文件——#include< fstream>
#include< string>
-
创建流对象——ifstream ifs;
-
打开文件并判断文件是否打开成功——ifs.open(“文件路径”,打开方式);
-
读数据——四种方式读取
-
关闭文件——ifs.close();
// 判断是否打开成功
if (!ifs.is_open()){
cout << "文件打开失败了" << endl;
return;
}
string buf;
while (getline(ifs,buf)){
cout << buf << endl;
}
ifs.close();
项目问题
-
delete和delete[ ]
关于动态申请的内存,分两种情况:基本数据类型的分配和自定义数据类型的分配。
- 基本的数据类型:对象没有析构函数,并且new 在分配内存时会记录分配的空间大小,则delete时能正确释放内存,无需调用析构函数释放其余指针。因此两种方式均可。
- 自定义数据类型:通过new申请了一个对象数组,返回一个指针,对于此对象数组的内存释放,需要做两件事情:一是释放最初申请的那部分空间,二是调用析构函数完成清理工作。对于内存空间的清理,由于申请时记录了其大小,因此无论使用delete还是delete[ ]都能将这片空间完整释放,而问题就出在析构函数的调用上,当使用delete时,仅仅调用了对象数组中第一个对象的析构函数,而使用delete [ ]的话,将会逐个调用析构函数。
-
ifstream eof() 的使用
ifstream.eof() 读到文件结束符时返回true。文件结束符是文件最后一个字符的下一个字符0xFF,eof() 读到文件结束符0xFF时返回true。