C++ 面向对象程序设计 14万字总结笔记(六)

简介: C++ 面向对象程序设计 14万字总结笔记(六)

多态的作用

在面向对象的程序设计中使用多态,能够增强程序的可扩充性,即程序需要修改或增加功能的时候,需要改动和增加的代码较少。

使用多态的游戏程序实例

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QfNyOCXd-1688033782520)(2023-06-20-20-13-01.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pRRIqO9H-1688033782521)(2023-06-20-20-13-30.png)]

基本思路:

为每个怪物类编写 Attack、FightBack和 Hurted成员函数。

Attact函数表现攻击动作,攻击某个怪物,并调用被攻击怪物的

Hurted函数,以减少被攻击怪物的生命值,同时也调用被攻击怪物的 FightBack成员函数,遭受被攻击怪物反击。

Hurted函数减少自身生命值,并表现受伤动作。

FightBack成员函数表现反击动作,并调用被反击对象的Hurted成

员函数,使被反击对象受伤。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4vHr7mnE-1688033782521)(2023-06-20-20-14-16.png)]

非多态的实现方法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zSV6IHlV-1688033782521)(2023-06-20-20-14-39.png)]

代码示例

下面是一个使用多态的游戏程序实例,该程序模拟了一个简单的 RPG 游戏,其中包含了不同类型的角色,每种角色都有自己的攻击力和防御力。具体实现如下:

#include <iostream>
#include <string>
using namespace std;
// 角色基类
class Character {
protected:
    string name;
    int attack;
    int defense;
public:
    Character(string name, int attack, int defense) {
        this->name = name;
        this->attack = attack;
        this->defense = defense;
    }
    virtual int getAttack() { return attack; } // 获取攻击力
    virtual int getDefense() { return defense; } // 获取防御力
    virtual void attackTarget(Character* target) {} // 攻击目标
};
// 具体角色类:战士
class Warrior : public Character {
public:
    Warrior(string name, int attack, int defense) : Character(name, attack, defense) {}
    int getAttack() { return attack * 2; } // 攻击力加倍
    void attackTarget(Character* target) {
        int damage = getAttack() - target->getDefense();
        damage = max(damage, 0);
        cout << name << " 对 " << target->name << " 造成了 " << damage << " 点伤害!" << endl;
    }
};
// 具体角色类:法师
class Mage : public Character {
public:
    Mage(string name, int attack, int defense) : Character(name, attack, defense) {}
    int getDefense() { return defense / 2; } // 防御力减半
    void attackTarget(Character* target) {
        int damage = getAttack() - target->getDefense();
        damage = max(damage, 0);
        cout << name << " 对 " << target->name << " 造成了 " << damage << " 点伤害!" << endl;
    }
};
// 游戏主程序
int main() {
    Warrior warrior("战士", 50, 30);
    Mage mage("法师", 40, 40);
    Character* player = &warrior;
    Character* enemy = &mage;
    cout << player->name << " 攻击 " << enemy->name << ":" << endl;
    player->attackTarget(enemy);
    cout << enemy->name << " 攻击 " << player->name << ":" << endl;
    enemy->attackTarget(player);
    return 0;
}

在上述代码中,角色基类 Character 中定义了角色的基本属性和行为,其中 getAttack()getDefense() 函数是虚函数,分别获取角色的攻击力和防御力。具体的角色类 WarriorMage 继承自角色基类 Character,并分别重载了虚函数 getAttack()getDefense(),以及定义了攻击目标的具体行为。

在游戏主程序中,先创建了一个战士和一个法师角色,然后通过基类指针来访问角色的属性和行为。当战士攻击法师时,调用的是战士类中的 attackTarget() 函数,而当法师攻击战士时,调用的是法师类中的 attackTarget() 函数。由于这两个函数都是虚函数,并且被重新定义了,所以实际上调用的是派生类中的函数,实现了运行时多态性。

!!!更多多态程序实例!!!

几何形体处理程序

几何形体处理程序: 输入若干个几何形体的参数,

要求按面积排序输出。输出时要指明形状。

Input:

第一行是几何形体数目n(不超过100).下面有n行,每行以一个字母c开头.

若 c 是 ‘R’,则代表一个矩形,本行后面跟着两个整数,分别是矩形的宽和高;

若 c 是 ‘C’,则代表一个圆,本行后面跟着一个整数代表其半径

若 c 是 ‘T’,则代表一个三角形,本行后面跟着三个整数,代表三条边的长度

Output:

按面积从小到大依次输出每个几何形体的种类及面积。每行一个几何形体,输出格式为:

形体名称:面积

课本代码示例

#include <iostream>
#include <stdlib.h>
#include <math.h>
using namespace std;
class CShape
{
public:
virtual double Area() = 0; //纯虚函数
virtual void PrintInfo() = 0;
}; 
class CRectangle:public CShape
{
public:
int w,h; 
virtual double Area();
virtual void PrintInfo();
};
class CCircle:public CShape {
public:
int r; 
virtual double Area();
virtual void PrintInfo();
};
class CTriangle:public CShape {
public:
int a,b,c; 
virtual double Area();
virtual void PrintInfo();
}; 
double CRectangle::Area() { 
return w * h; 
}
void CRectangle::PrintInfo() {
cout << "Rectangle:" << Area() << endl;
}
double CCircle::Area() {
return 3.14 * r * r ;
}
void CCircle::PrintInfo() {
cout << "Circle:" << Area() << endl;
}
double CTriangle::Area() {
double p = ( a + b + c) / 2.0;
return sqrt(p * ( p - a)*(p- b)*(p - c));
}
void CTriangle::PrintInfo() {
cout << "Triangle:" << Area() << endl; 
}
CShape * pShapes[100];
int MyCompare(const void * s1, const void * s2);
int main()
{ 
int i; int n;
CRectangle * pr; CCircle * pc; CTriangle * pt;
cin >> n;
for( i = 0;i < n;i ++ ) {
char c;
cin >> c;
switch(c) {
case 'R':
pr = new CRectangle();
cin >> pr->w >> pr->h;
pShapes[i] = pr; 
break; 
case 'C':
pc = new CCircle();
cin >> pc->r;
pShapes[i] = pc;
break;
case 'T':
pt = new CTriangle();
cin >> pt->a >> pt->b >> pt->c;
pShapes[i] = pt; 
break;
} 
}
qsort(pShapes,n,sizeof( CShape*),MyCompare);
for( i = 0;i <n;i ++)
pShapes[i]->PrintInfo(); 
return 0;
}
int MyCompare(const void * s1, const void * s2)
{
double a1,a2;
CShape * * p1 ; // s1,s2 是 void * ,不可写 “* s1”来取得s1指向的内容
CShape * * p2;
p1 = ( CShape * * ) s1; //s1,s2指向pShapes数组中的元素,数组元素的类型是CShape *
p2 = ( CShape * * ) s2; // 故 p1,p2都是指向指针的指针,类型为 CShape ** 
a1 = (*p1)->Area(); // * p1 的类型是 Cshape * ,是基类指针,故此句为多态
a2 = (*p2)->Area();
if( a1 < a2 ) 
return -1;
else if ( a2 < a1 )
return 1;
else
return 0;
} 
case 'C':
pc = new CCircle();
cin >> pc->r;
pShapes[i] = pc;
break;
case 'T':
pt = new CTriangle();
cin >> pt->a >> pt->b >> pt->c;
pShapes[i] = pt; 
break;
} 
}
qsort(pShapes,n,sizeof( CShape*),MyCompare);
for( i = 0;i <n;i ++)
pShapes[i]->PrintInfo(); 
return 0;
}

如果添加新的几何形体,比如五边形,则只需要从CShape派生出CPentagon,以及在main中switch语句中增加一个case,其余部分不变有木有!

用基类指针数组存放指向各种派生类对象的指针,然后遍历该数组,就能对各个派生类对象做各种操作,是很常用的做法

下面是一个利用 C++ 实现的几何形体处理程序,可以输入若干个几何形体的参数,并按照面积排序输出各个几何形体的种类及面积:
```c++
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
const double pi = 3.14159265358979323846;
// 几何形体基类
class Shape {
public:
    virtual double getArea() = 0; // 获取面积
    virtual string getName() = 0; // 获取名称
};
// 具体几何形体类:矩形
class Rectangle : public Shape {
private:
    double width;
    double height;
public:
    Rectangle(double w, double h) {
        width = w;
        height = h;
    }
    double getArea() { return width * height; }
    string getName() { return "矩形"; }
};
// 具体几何形体类:圆形
class Circle : public Shape {
private:
    double radius;
public:
    Circle(double r) { radius = r; }
    double getArea() { return pi * radius * radius; }
    string getName() { return "圆形"; }
};
// 具体几何形体类:三角形
class Triangle : public Shape {
private:
    double a, b, c;
public:
    Triangle(double aa, double bb, double cc) {
        a = aa;
        b = bb;
        c = cc;
    }
    double getArea() {
        double p = (a + b + c) / 2;
        return sqrt(p * (p - a) * (p - b) * (p - c));
    }
    string getName() { return "三角形"; }
};
// 比较函数,用于排序
bool cmp(Shape* s1, Shape* s2) { return s1->getArea() < s2->getArea(); }
// 主程序
int main() {
    int n;
    cin >> n;
    vector<Shape*> shapes;
    for (int i = 0; i < n; i++) {
        char c;
        cin >> c;
        if (c == 'R') {
            double w, h;
            cin >> w >> h;
            shapes.push_back(new Rectangle(w, h));
        }
        else if (c == 'C') {
            double r;
            cin >> r;
            shapes.push_back(new Circle(r));
        }
        else if (c == 'T') {
            double a, b, c;
            cin >> a >> b >> c;
            shapes.push_back(new Triangle(a, b, c));
        }
    }
    sort(shapes.begin(), shapes.end(), cmp);
    for (int i = 0; i < n; i++) {
        cout << shapes[i]->getName() << ":" << shapes[i]->getArea() << endl;
        delete shapes[i];
    }
    return 0;
}
```
在上述代码中,几何形体基类 `Shape` 定义了接口函数 `getArea()` 和 `getName()`,分别用于获取几何形体的面积和名称。具体的几何形体类 `Rectangle`、`Circle` 和 `Triangle` 继承自 `Shape`,并实现了这两个接口函数。
在主程序中,首先输入几何形体的数目 `n`,然后根据每个几何形体的名称和参数创建相应的对象,并添加到 `shapes` 向量中。最后利用 `sort()` 函数和比较函数 `cmp()` 对 `shapes` 向量进行排序,按照面积从小到大排序。最后遍历 `shapes` 向量,输出每个几何形体的名称和面积,并释放相应的内存。

构造函数和析构函数中调用虚函数

在构造函数和析构函数中调用虚函数,不是多态。编译时即可确定,调用的函数是自己的类或基类中定义的函数,不会等到运行时才决定调用自己的还是派生类的函数

在构造函数和析构函数中调用虚函数是一种危险的做法,因为虚函数的调用是运行时决定的,而在构造函数和析构函数中,对象的状态可能还没有完全初始化或已经被销毁,此时调用虚函数可能会产生未定义的行为。

具体来说,在构造函数中调用虚函数可能会导致以下问题:

  1. 对象可能还没有完全初始化,此时调用虚函数可能会访问到未初始化的成员变量或无效的指针,导致程序崩溃或产生未定义的行为。
  2. 对象的动态类型可能还没有被确定,此时调用虚函数会默认使用基类的实现,而不是派生类的实现,导致程序产生错误的行为。
    在析构函数中调用虚函数可能会导致以下问题:
  3. 对象的动态类型可能已经被销毁,此时调用虚函数会导致程序崩溃或产生未定义的行为。
  4. 调用虚函数可能会触发虚函数表的查找和调用,而在对象被销毁的过程中,虚函数表和虚函数指针可能已经被销毁,导致程序出现未定义的行为。

因此,在构造函数和析构函数中应该尽量避免调用虚函数,或者采用其他解决方案,如在构造函数中使用初始化列表初始化成员变量,或者在析构函数中只进行简单的资源释放操作。如果确实需要在构造函数或析构函数中调用虚函数,应该尽可能保证对象的状态已经完全初始化或还没有被销毁,并且应该注意虚函数的实现是否能够正确地处理这种情况。

多态实现的关键 — 虚函数表

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-24r4yzri-1688033782521)(2023-06-20-20-22-42.png)]

多态是面向对象编程的重要特性之一,其中的关键就是通过运行时绑定来实现函数调用的动态分派。在 C++ 中,实现多态的一种常见方式是通过虚函数来实现,其中的关键就是需要使用虚函数表(vtable)来存储类的虚函数指针,从而实现动态绑定。

虚函数表是一个由编译器自动生成的静态变量,用于存储类的虚函数指针。对于每个含有虚函数的类,编译器都会在编译时为其生成一个虚函数表,并将其作为类的元数据存储在程序的数据段中。虚函数表中的每个条目都是一个指向虚函数的指针,其中的顺序和声明顺序相同。因此,当类的对象被创建时,虚函数表的地址会被存储在对象的头部,成为对象的虚函数指针(vptr)。

当使用基类类型的指针或引用调用虚函数时,会根据对象的实际类型来查找虚函数表,并通过虚函数指针调用相应的虚函数。具体来说,编译器会在对象的虚函数指针(vptr)所指向的虚函数表中查找相应的虚函数,然后通过函数指针调用相应的函数。因此,由于虚函数的调用是在运行时动态决定的,因此可以实现多态的目的。

需要注意的是,在使用虚函数时,需要满足以下条件:

  1. 虚函数必须是类的成员函数。
  2. 虚函数必须在基类中被声明为虚函数,并在派生类中被覆盖(override)或实现(implement)。
  3. 虚函数的参数和返回类型必须与基类中的虚函数完全相同。
  4. 虚函数表的大小和顺序必须与类的继承关系相对应。
  5. 虚函数的调用必须通过基类类型的指针或引用进行,才能触发动态绑定。

通过虚函数表的实现,C++ 实现了多态的功能,使得程序具有更加灵活和可扩展的特性。但需要注意的是,虚函数表的实现也增加了程序的开销和复杂性,因此在设计和实现程序时需要谨慎考虑。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jivpN9CP-1688033782522)(2023-06-20-20-24-16.png)]

虚析构函数

通过基类的指针删除派生类对象时,通常情况下只调用基类的析构函数

但是,删除一个派生类的对象时,应该先调用派生类的析构函数,然后调用基类的析构函数。

解决办法:把基类的析构函数声明为virtual 派生类的析构函数可以virtual不进行声明

通过基类的指针删除派生类对象时,首先调用派生类的析构函数,然后调用基类的析构函数

一般来说,一个类如果定义了虚函数,则应该将析构函数也定义成虚函数。或者,一个类打算作为基类使用,也应该将析构函数定义成虚函数。

注意:不允许以虚函数作为构造函数

虚析构函数是指在析构函数前加上关键字 “virtual”,用于实现多态的析构函数。

当使用基类的指针或引用来删除一个派生类的对象时,如果基类的析构函数不是虚函数,那么只会调用基类的析构函数,而不会调用派生类的析构函数。这样就会导致派生类中分配的内存资源没有被正确释放,从而导致内存泄漏的问题。

而通过将基类的析构函数声明为虚函数,可以实现在析构对象时动态绑定,调用派生类的析构函数。这样就能够正确地释放派生类中分配的内存资源,避免内存泄漏的问题。

例如:

class Base {
public:
    virtual ~Base() { // 声明为虚函数
        std::cout << "Base::~Base()" << std::endl;
    }
};
class Derived : public Base {
public:
    ~Derived() override { // 实现派生类的析构函数
        std::cout << "Derived::~Derived()" << std::endl;
    }
};
int main() {
    Base *p = new Derived();
    delete p; // 动态绑定,调用派生类的析构函数
    return 0;
}

在上述代码中,基类的析构函数被声明为虚函数,派生类的析构函数被实现并重写了基类的虚析构函数。在删除派生类对象时,通过基类的指针进行删除,并利用动态绑定机制实现了对派生类析构函数的调用。

虚析构函数是为了解决基类指针删除派生类对象时可能出现的内存泄漏问题,是实现多态的关键。在设计和实现对象继承体系时,应该尽可能地将析构函数声明为虚函数。

需要注意的是,如果派生类没有显式地提供析构函数,那么编译器会自动生成一个默认的析构函数。这个默认的析构函数也会被声明为虚函数,因为它继承了基类的虚析构函数。

另外,需要注意的是,虚析构函数只适用于基类指针或引用删除派生类对象时的情况。如果使用对象的直接名字进行删除,那么只会调用对象自身的析构函数,而不会触发动态绑定,也不会调用派生类的析构函数。

例如:

class Base {
public:
    virtual ~Base() { // 声明为虚函数
        std::cout << "Base::~Base()" << std::endl;
    }
};
class Derived : public Base {
public:
    ~Derived() override { // 实现派生类的析构函数
        std::cout << "Derived::~Derived()" << std::endl;
    }
};
int main() {
    Derived d;
    Base *p = &d;
    p->~Base(); // 不会触发动态绑定,只会调用 Base 的析构函数
    return 0;
}

在上述代码中,对象 d 的析构函数是派生类 Derived 的析构函数,在删除对象 d 时会被自动调用。但是,如果使用基类指针直接调用对象的析构函数,不会触发动态绑定,只会调用基类的析构函数,从而导致派生类的析构函数没有被正确调用。

因此,需要在使用虚析构函数时,注意使用基类指针或引用来删除派生类对象,以确保正确调用派生类的析构函数,避免内存泄漏的问题。

此外,需要注意的是,如果派生类的析构函数需要进行特殊的资源释放操作,例如释放动态分配的内存或关闭文件等,那么必须在派生类的析构函数中显式地调用基类的虚析构函数。这样才能保证在析构派生类对象时,先调用派生类的析构函数,再调用基类的析构函数,从而避免资源泄漏的问题。

例如:

class Base {
public:
    virtual ~Base() {
        std::cout << "Base::~Base()" << std::endl;
    }
};
class Derived : public Base {
public:
    Derived() {
        data = new int[10];
    }
    ~Derived() override {
        delete[] data; // 释放动态分配的内存
        std::cout << "Derived::~Derived()" << std::endl;
        // 显式调用基类的虚析构函数
        // ensure the base class's destructor is executed
        Base::~Base();
    }
private:
    int *data;
};
int main() {
    Base *p = new Derived();
    delete p;
    return 0;
}

在上述代码中,派生类 Derived 在构造函数中分配了一个 int 数组的内存空间,然后在析构函数中释放了这个内存空间。同时,派生类的析构函数中显式地调用了基类的虚析构函数,以确保在析构对象时,先调用派生类的析构函数,再调用基类的析构函数,从而避免资源泄漏的问题。

总之,虚析构函数是实现多态和避免内存泄漏的关键,需要注意使用基类指针或引用来删除派生类对象,以及在派生类的析构函数中显式地调用基类的虚析构函数,以确保正确地释放资源。

纯虚函数和抽象类

包含纯虚函数的类叫抽象类

 抽象类只能作为基类来派生新类使用,不能创建独立的抽象类的对象

 抽象类的指针和引用可以指向由抽象类派生出来的类的对象

A a ; // 错,A 是抽象类,不能创建对象

A * pa ; // ok,可以定义抽象类的指针和引用

pa = new A ; //错误, A 是抽象类,不能创建对象

 在抽象类的成员函数内可以调用纯虚函数,但是在构造函数或析构函数内部不能调用纯虚函数。

 如果一个类从抽象类派生而来,那么当且仅当它实现了基类中的所有纯虚函数,它才能成为非抽象类。

纯虚函数和抽象类是 C++ 中用于实现接口和多态的重要特性。

纯虚函数是指在函数声明的结尾处使用 “= 0” 指明的函数,它是一种没有实现的虚函数。纯虚函数的作用是为了实现接口,即定义一个接口,而不需要提供具体的实现。由于纯虚函数没有具体实现,因此不能直接创建该类的对象,必须在派生类中实现该函数才能使用。例如:

class Shape {
public:
    virtual double area() = 0; // 纯虚函数
};
class Circle : public Shape {
public:
    double area() override { // 实现纯虚函数
        return 3.14 * radius * radius;
    }
private:
    double radius;
};

抽象类是指含有纯虚函数的类,它不能被直接实例化,只能被用作派生其他类的基类。抽象类的作用在于,定义了一组接口,而不需要提供具体的实现。由于抽象类含有纯虚函数,因此必须在派生类中实现所有的纯虚函数才能使用。例如:

class Shape {
public:
    virtual double area() = 0; // 纯虚函数,使得 Shape 变成了抽象类
    virtual void draw() = 0;   // 纯虚函数
};
class Circle : public Shape {
public:
    double area() override {
        return 3.14 * radius * radius;
    }
    void draw() override {
        // 绘制圆形
    }
private:
    double radius;
};

需要注意的是,抽象类可以包含非纯虚函数,但是含有纯虚函数的类一定是抽象类。因为含有纯虚函数的类不能被直接实例化,必须被用作派生其他类的基类,提供接口的定义。

纯虚函数和抽象类的作用是为了实现接口和多态,使得程序具有更加灵活和可扩展的特性。在实际的程序设计中,可以使用抽象类或纯虚函数来定义接口,从而使得程序的设计更加模块化和可维护。

输入和输出

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vG4xqoVi-1688033782522)(2023-06-20-20-53-48.png)]

与输入输出流操作相关的类有很多,以下是一些常见的:

  1. std::ifstream / std::ofstream:用于读取和写入文件。
  2. std::stringstream / std::ostringstream:用于将字符串作为流来处理。
  3. std::cin / std::cout:用于输入和输出标准输入输出流。
  4. std::wifstream / std::wofstream:用于读取和写入宽字符文件。
  5. std::istringstream / std::ostringstream:用于将字符串流化为输入输出流。
  6. std::wstringstream / std::wostringstream:用于将 wchar_t 类型字符串转化为输入输出流。

以上这些类都是基于标准输入输出流 std::stream 的派生类,使用这些类的时候需要包含 头文件。在使用这些类时,通常需要注意打开和关闭文件、读写字符或字符串的方式等操作。同时,需要注意数据的编码方式,例如 ASCII 码和 Unicode 码等,以确保数据的正确读写和传输。

标准流对象

标准流对象是指在 C++ 标准库中预定义好的输入输出流对象,包括三个标准流对象:cin、cout、cerr 和 clog。

  1. cin:用于标准输入(通常是终端或命令行窗口),可以使用流提取运算符(>>)读取数据。
  2. cout:用于标准输出(通常是终端或命令行窗口),可以使用流插入运算符(<<)输出数据。
  3. cerr 和 clog:用于输出错误信息和日志信息,与 cout 不同的是,cerr 和 clog 通常不会被重定向。cerr 是标准错误流,通常用于输出错误信息;clog 是标准日志流,通常用于输出日志信息。
    这些标准流对象都是全局对象,可以在任何地方访问它们。在使用标准流对象时,需要包含 头文件。
    例如,可以使用 cin 和 cout 来实现从标准输入读取数据并输出到标准输出:
#include <iostream>
int main() {
    int num;
    std::cout << "Please enter a number: ";
    std::cin >> num;
    std::cout << "The number you entered is: " << num << std::endl;
    return 0;

在上述代码中,使用 std::cout 来输出提示信息,然后使用 std::cin 来读取一个整数,最后再使用 std::cout 输出读取到的整数。

需要注意的是,标准流对象是缓冲流,缓冲区的大小可以通过调用 std::ios_base::sync_with_stdio 函数来设置。同时,可以通过调用 std::cin.clear() 和 std::cin.ignore() 函数来清空 cin 输入缓冲区和忽略输入缓冲区中的指定字符。

cin对应于标准输入流,用于从键盘读取数据,也可以被重定向

为从文件中读取数据。

 cout对应于标准输出流,用于向屏幕输出数据,也可以被重定

向为向文件写入数据。

 cerr对应于标准错误输出流,用于向屏幕输出出错信息,

 clog对应于标准错误输出流,用于向屏幕输出出错信息,

 cerr和clog的区别在于cerr不使用缓冲区,直接向显示器输出信息;而输出到clog中的信息先会被存放在

缓冲区,缓冲区满或者刷新时才输出到屏幕。

判断输入流结束

可以用如下方法判输入流结束:

int x;
while(cin>>x){
…..
}
return 0;
 如果是从文件输入,比如前面有
freopen(“some.txt”,”r”,stdin);

那么,读到文件尾部,输入流就算结束

 如果从键盘输入,则在单独一行输入Ctrl+Z代表输入流结束

istream 是 C++ 中用于输入流的基类,它提供了一些用于输入流操作的成员函数,以下是一些常用的成员函数:

  1. getline():从输入流中读取一行数据。可以指定分隔符,例如:
std::string line;
std::getline(std::cin, line); // 从标准输入中读取一行数据
  1. ignore():忽略输入流中的指定字符或一定数量的字符。可以指定忽略的字符数或忽略到某个分隔符为止,例如:
std::cin.ignore(100, '\n'); // 忽略输入流中的 100 个字符或遇到换行符为止
  1. peek():查看输入流中下一个字符但不移动输入流的指针位置,例如:
char ch = std::cin.peek(); // 查看输入流中下一个字符
  1. putback():将一个字符放回输入流中,例如:
std::cin.putback('x'); // 将字符 'x' 放回输入流中
  1. read():从输入流中读取指定数量的字节到指定的内存位置,例如:
char buf[100];
std::cin.read(buf, 100); // 从输入流中读取 100 个字节到 buf 中
  1. seekg() / tellg():用于移动输入流的指针位置和获取当前指针位置的函数。seekg() 可以将指针移动到指定位置,tellg() 可以获取当前指针的位置,例如:
std::cin.seekg(10); // 将输入流的指针位置移动到第 10 个字符处
std::streampos pos = std::cin.tellg(); // 获取当前输入流的指针位置

需要注意的是,以上成员函数是从 istream 类继承来的,也可以在派生类中使用。同时,需要注意输入流的状态,例如输入流是否已经到达文件结尾或输入流是否存在错误等。可以使用 eof()、fail()、bad() 和 good() 成员函数来获取输入流的状态。

istream类的成员函数

istream 是 C++ 中用于输入流的基类,它提供了一些用于输入流操作的成员函数,以下是一些常用的成员函数:

  1. getline():从输入流中读取一行数据。可以指定分隔符,例如:
std::string line;
std::getline(std::cin, line); // 从标准输入中读取一行数据
  1. ignore():忽略输入流中的指定字符或一定数量的字符。可以指定忽略的字符数或忽略到某个分隔符为止,例如:
std::cin.ignore(100, '\n'); // 忽略输入流中的 100 个字符或遇到换行符为止
  1. peek():查看输入流中下一个字符但不移动输入流的指针位置,例如:
char ch = std::cin.peek(); // 查看输入流中下一个字符
  1. putback():将一个字符放回输入流中,例如:
std::cin.putback('x'); // 将字符 'x' 放回输入流中
  1. read():从输入流中读取指定数量的字节到指定的内存位置,例如:
char buf[100];
std::cin.read(buf, 100); // 从输入流中读取 100 个字节到 buf 中
  1. seekg() / tellg():用于移动输入流的指针位置和获取当前指针位置的函数。seekg() 可以将指针移动到指定位置,tellg() 可以获取当前指针的位置,例如:
std::cin.seekg(10); // 将输入流的指针位置移动到第 10 个字符处
std::streampos pos = std::cin.tellg(); // 获取当前输入流的指针位置

需要注意的是,以上成员函数是从 istream 类继承来的,也可以在派生类中使用。同时,需要注意输入流的状态,例如输入流是否已经到达文件结尾或输入流是否存在错误等。可以使用 eof()、fail()、bad() 和 good() 成员函数来获取输入流的状态。

C++文件读写

C++ 文件读写通常使用标准库中的 ifstream 和 ofstream 类来实现,这两个类都继承自 istream 和 ostream 类,提供了丰富的文件读写方法。

文件读取可以使用 ifstream 类,例如:

#include <iostream>
#include <fstream>
#include <string>
int main() {
    std::ifstream infile("filename.txt");
    std::string line;
    while (std::getline(infile, line)) {
        std::cout << line << std::endl;
    }
    infile.close();
    return 0;
}

在上述代码中,使用 ifstream 类打开名为 filename.txt 的文件,然后使用 getline() 函数读取文件中的每一行数据,并输出到标准输出流中。最后调用 close() 函数关闭文件。

文件写入可以使用 ofstream 类,例如:

#include <fstream>
#include <string>
int main() {
    std::ofstream outfile("filename.txt");
    outfile << "Hello, World!" << std::endl;
    outfile.close();
    return 0;
}

在上述代码中,使用 ofstream 类打开名为 filename.txt 的文件,然后使用流插入运算符(<<)将字符串 “Hello, World!” 写入文件中。最后调用 close() 函数关闭文件。

需要注意的是,文件读写操作可能会抛出异常,因此需要使用 try-catch 块来捕获异常并进行处理。同时,需要确保使用文件流之前先打开文件,使用之后再关闭文件,以保证操作的正确性和文件C++ 文件读写通常使用流操作,主要包括以下几个步骤:

  1. 打开文件:使用 std::ofstream 或 std::ifstream 类型的对象打开文件,可以指定打开方式和文件名等信息。
std::ofstream outfile("example.txt"); // 以写入方式打开文件
std::ifstream infile("example.txt"); // 以读取方式打开文件
  1. 写文件:通过流插入运算符(<<)写入数据到文件中。
outfile << "This is a line of text" << std::endl;
  1. 读文件:通过流提取运算符(>>)读取文件中的数据。
std::string line;
while (std::getline(infile, line)) {
    std::cout << line << std::endl;
}
  1. 关闭文件:使用 std::ofstream 或 std::ifstream 类型的对象关闭文件。
outfile.close();
infile.close();

需要注意的是,文件读写操作可能会出现错误,例如文件不存在、文件无法打开、文件权限不足等。可以使用流状态函数(eof()、fail()、bad() 和 good())或异常机制来处理这些错误。

完整的文件读写示例代码如下:

#include <iostream>
#include <fstream>
#include <string>
int main() {
    std::ofstream outfile("example.txt"); // 以写入方式打开文件
    if (!outfile.is_open()) {
        std::cerr << "Failed to open file" << std::endl;
        return -1;
    }
    outfile << "This is a line of text" << std::endl;
    outfile.close(); // 关闭文件
    std::ifstream infile("example.txt"); // 以读取方式打开文件
    if (!infile.is_open()) {
        std::cerr << "Failed to open file" << std::endl;
        return -1;
    }
    std::string line;
    while (std::getline(infile, line)) { // 逐行读取文件中的内容
        std::cout << line << std::endl;
    }
    infile.close(); // 关闭文件
    return 0;
}

以上代码先使用 std::ofstream 打开文件并写入一行文本,然后关闭文件,接着使用 std::ifstream 打开同一个文件并逐行读取其内容,并在标准输出中输出每行文本。需要注意的是,可以通过检查文件是否成功打开来判断文件是否存在或是否有读写权限。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-V2csTOYp-1688033782522)(2023-06-20-21-02-28.png)]

除了使用 ofstream 和 ifstream 类,还可以使用 fstream 类来实现文件读写,它同时继承了 istream 和 ostream 类,可以用于读写文件。以下是一个使用 fstream 类读写文件的示例:

#include <iostream>
#include <fstream>
#include <string>
int main() {
    std::fstream file("example.txt", std::ios::in | std::ios::out | std::ios::trunc);
    if (!file.is_open()) {
        std::cerr << "Failed to open file" << std::endl;
        return -1;
    }
    file << "This is a line of text" << std::endl;
    std::string line;
    while (std::getline(file, line)) {
        std::cout << line << std::endl;
    }
    file.close();
    return 0;
}

在上述代码中,使用 fstream 类打开名为 example.txt 的文件,同时指定了打开方式为输入输出和截断文件。然后将一行文本写入文件中,接着使用 getline() 函数读取文件中的每一行数据,并输出到标准输出流中。最后调用 close() 函数关闭文件。

需要注意的是,在使用 fstream 类进行文件读写时,需要指定文件的打开方式。常用的文件打开方式包括:

  1. ios::in:以读取方式打开文件。
  2. ios::out:以写入方式打开文件。
  3. ios::app:在文件末尾追加内容。
  4. ios::trunc:打开文件并截断文件,即删除文件中的所有内容。
    同时,还可以使用按位或操作符将多个打开方式组合起来。例如,可以使用以下方式打开文件:
std::fstream file("example.txt", std::ios::in | std::ios::out | std::ios::app);

在上述代码中,打开文件的方式包括输入输出和在文件末尾追加内容。

需要注意的是,当打开文件时指定了 ios::trunc 标志时,文件的所有内容将被删除。因此,在使用此方式打开文件时,需要谨慎操作。

文件的读写指针

 对于输入文件,有一个读指针;

 对于输出文件,有一个写指针;

 对于输入输出文件,有一个读写指针;

 标识文件操作的当前位置, 该指针在哪里,读写操作就在哪里进行。

=文件的读写指针是非常重要的概念,它标识了文件操作的当前位置。

对于输入文件,读指针指示了当前读取位置在文件中的位置。当我们打开一个文件进行读取操作时,文件读指针默认位于文件的开头,每次读取操作后,文件读指针会自动向后移动,指向下一个要读取的位置。

对于输出文件,写指针指示了当前写入位置在文件中的位置。当我们打开一个文件进行写入操作时,文件写指针默认位于文件的开头,每次写入操作后,文件写指针会自动向后移动,指向下一个要写入的位置。

对于输入输出文件,读写指针可以同时进行读取和写入操作。在打开文件时,我们可以指定文件的读写指针初始位置,也可以使用 fseek() 函数移动文件的读写指针。

在进行文件读写操作时,我们需要非常注意文件读写指针的位置,以保证文件读写操作的正确性和安全性。如果读写指针的位置不正确,可能会导致数据的丢失或者文件内容的不一致。因此,在进行文件读写操作时,需要严格遵守文件读写指针的规则和约定。

目录
相关文章
|
2月前
|
Java 编译器 C++
C++入门指南:类和对象总结笔记(下)
C++入门指南:类和对象总结笔记(下)
30 0
|
2月前
|
存储 编译器 C语言
C++入门: 类和对象笔记总结(上)
C++入门: 类和对象笔记总结(上)
34 0
|
2月前
|
存储 编译器 C++
C++:多态究竟是什么?为何能成为面向对象的重要手段之一?
C++:多态究竟是什么?为何能成为面向对象的重要手段之一?
51 0
|
21天前
|
算法 Java 程序员
【C++专栏】C++入门 | 类和对象 | 面向过程与面向对象的初步认识
【C++专栏】C++入门 | 类和对象 | 面向过程与面向对象的初步认识
21 0
|
3天前
|
安全 Java 程序员
【C++笔记】从零开始认识继承
在编程中,继承是C++的核心特性,它允许类复用和扩展已有功能。继承自一个基类的派生类可以拥有基类的属性和方法,同时添加自己的特性。继承的起源是为了解决代码重复,提高模块化和可维护性。继承关系中的类形成层次结构,基类定义共性,派生类则根据需求添加特有功能。在继承时,需要注意成员函数的隐藏、作用域以及默认成员函数(的处理。此外,继承不支持友元关系的继承,静态成员在整个继承体系中是唯一的。虽然多继承和菱形继承可以提供复杂的设计,但它们可能导致二义性、数据冗余和性能问题,因此在实际编程中应谨慎使用。
5 1
【C++笔记】从零开始认识继承
|
7天前
|
C++
C++从入门到精通:2.1.2函数和类——深入学习面向对象的编程基础
C++从入门到精通:2.1.2函数和类——深入学习面向对象的编程基础
|
13天前
|
C++
面向对象的C++题目以及解法2
面向对象的C++题目以及解法2
25 1
|
13天前
|
C++
面向对象的C++题目以及解法
面向对象的C++题目以及解法
19 0
|
15天前
|
编译器 C语言 C++
【C++成长记】C++入门 | 类和对象(上) |面向过程和面向对象初步认识、类的引入、类的定义、类的访问限定符及封装
【C++成长记】C++入门 | 类和对象(上) |面向过程和面向对象初步认识、类的引入、类的定义、类的访问限定符及封装
|
22天前
|
存储 编译器 程序员
【C++】类和对象①(什么是面向对象 | 类的定义 | 类的访问限定符及封装 | 类的作用域和实例化 | 类对象的存储方式 | this指针)
【C++】类和对象①(什么是面向对象 | 类的定义 | 类的访问限定符及封装 | 类的作用域和实例化 | 类对象的存储方式 | this指针)