【杂七杂八】一些关于类的知识点

目录

以下面这个代码来说明:

1.虚析构函数

为什么需要虚析构函数?

何时需要定义虚析构函数?

虚析构函数的语法

总结

2.关于vector

为什么使用 vector?*>

规范的语法

vector 的表示方式

为什么 Drink 后面不需要加 ()?

3.new和delete

规范的语法

在这个代码中的作用

何时需要使用这种表示方式

选择哪种方式

4.push_back()

语法

使用场景

示例

5.dynamic_cast

Coffee* c = dynamic_cast(d); 的意义*>

dynamic_cast 的语法

使用 dynamic_cast 的场合

为什么使用 dynamic_cast?

示例


以下面这个代码来说明:

#include <iostream>

#include <string>

#include <vector>



using namespace std;



class Drink {

public:

    string DName;

    int DQuantity;

    double DPrice;

    virtual double total_price() = 0;

    virtual ~Drink() {}

};



class Coffee : public Drink {

public:

    string CTem;

    string CSug;

    double total_price() override {

        return DQuantity * DPrice;

    }

};



class Tea : public Drink {

public:

    string TTem;

    string TTas;

    double total_price() override {

        return DQuantity * DPrice * 0.8;

    }

};



int main() {

    int N;

    cin >> N;

    double total = 0;

    vector<Drink*> drinks;



    for (int i = 0; i < N; i++) {

        char type;

        cin >> type;



        if (type == 'C') {

            Coffee* c = new Coffee();

            cin >> c->DQuantity >> c->DPrice >> c->CTem >> c->CSug;

            drinks.push_back(c);

            total += c->total_price();

        } else if (type == 'T') {

            Tea* t = new Tea();

            cin >> t->DQuantity >> t->DPrice >> t->TTem >> t->TTas;

            drinks.push_back(t);

            total += t->total_price();

        }

    }



    int totalQuantity = 0;

    for (Drink* d : drinks) {

        totalQuantity += d->DQuantity;

    }



    if (totalQuantity > 6) {

        total *= 0.8;

    } else if (totalQuantity > 4) {

        total *= 0.9;

    }



    for (Drink* d : drinks) {

        if (Coffee* c = dynamic_cast<Coffee*>(d)) {

            cout << 'C' << " " << c->DQuantity << " " << c->DPrice << " " << c->CTem << " " << c->CSug << " " << c->total_price() << endl;

        } else if (Tea* t = dynamic_cast<Tea*>(d)) {

            cout << 'T' << " " << t->DQuantity << " " << t->DPrice << " " << t->TTem << " " << t->TTas << " " << t->total_price() << endl;

        }

    }



    cout << "总价:" << total << endl;



    for (Drink* d : drinks) {

        delete d;

    }



    return 0;

}

1.虚析构函数

为什么需要虚析构函数?

当你有一个基类指针指向一个派生类对象时,如果基类的析构函数不是虚的,那么在删除该指针时,只有基类的析构函数会被调用。这会导致一个问题:派生类的析构函数不会被调用,可能导致派生类中分配的资源(如动态内存、文件句柄等)未被适当释放,从而引发内存泄漏或其他问题。

何时需要定义虚析构函数?

当你的类被设计为基类,并且你期望通过基类指针来删除派生类对象时,你应该定义一个虚析构函数。如果你的类不打算被继承,或者你不会通过基类指针来删除对象,那么虚析构函数可能不是必需的。

虚析构函数的语法

虚析构函数的定义很简单。你只需要在基类的析构函数声明前加上virtual关键字。例如,在你的代码中,Drink类的虚析构函数可以这样定义:

class Drink {
public:
    // ... 其他成员 ...

    virtual ~Drink() {
        // 这里可以放置清理资源的代码
    }
};

在这个例子中,即使析构函数体为空,将析构函数声明为虚的也是重要的,这保证了当通过Drink*指针删除CoffeeTea对象时,相应的析构函数(Coffee::~Coffee()Tea::~Tea())将被调用。

总结

  • 使用虚析构函数是为了保证通过基类指针删除派生类对象时,能够正确地调用派生类的析构函数。
  • 当类被设计为基类,并且可能会通过基类指针来删除派生类对象时,应定义虚析构函数。
  • 虚析构函数的定义只需在基类的析构函数前加上virtual关键字。

2.关于vector

为什么使用 vector<Drink*>

在这个程序中,Drink 是一个抽象基类,不能直接实例化。但可以创建指向 Drink 的指针,指向其派生类(如 CoffeeTea)的实例。这允许利用多态:可以通过基类指针来调用派生类中的方法。所以,vector<Drink*> 存储的是指向 Drink 类(及其派生类)对象的指针。

规范的语法

规范的语法是 vector<Type> name;,其中 Type 是存储在向量中的元素类型。例如:

  • 存储整数的向量:vector<int> numbers;
  • 存储字符串的向量:vector<string> words;

vector 的表示方式

vector 可以用几种不同的方式声明和初始化,例如:

  1. 默认构造函数vector<Drink*> drinks; 创建一个空的向量。

  2. 带初始大小的构造函数vector<Drink*> drinks(10); 创建一个包含10个初始化为 nullptrDrink* 元素的向量。

  3. 带初始值的构造函数vector<Drink*> drinks(10, nullptr); 创建一个包含10个 nullptrDrink* 元素的向量。

  4. 初始化列表vector<int> numbers = {1, 2, 3, 4, 5}; 创建一个包含5个整数的向量。

为什么 Drink 后面不需要加 ()

在这个声明中,Drink* 是一个类型,表示指向 Drink 类型对象的指针。当你在 vector 的尖括号内指定一个类型时,你只是告诉 vector 它将存储什么类型的元素,而不是在创建这种类型的一个实例。因此,没有必要也不可能在类型名后面加上 ()。如果你添加了 (),如 vector<Drink*> drinks();,这将被解释为一个函数声明,而不是一个变量声明,这是一个常见的错误,称为“最令人困惑的解析器规则”(Most Vexing Parse)。

3.new和delete

在C++中,Coffee* c = new Coffee(); 这行代码执行了几个重要的操作:

  1. 动态内存分配new Coffee() 部分调用了 Coffee 类的构造函数来创建一个新的 Coffee 对象,并将其放置在堆(heap)上。这与在栈(stack)上创建对象不同,堆上的对象不会在离开创建它的作用域时自动销毁。

  2. 返回指针new 运算符返回指向新创建的对象的指针。

  3. 指针赋值:将返回的指针赋值给 Coffee* c,这里 c 是一个指针变量,用于存储指向 Coffee 类型对象的地址。

规范的语法

规范语法为 ClassName* pointerName = new ClassName();。在这个语法中:

  • ClassName 是你想要创建的对象的类型。
  • pointerName 是指向该对象的指针的名称。
  • new ClassName() 创建了 ClassName 类型的新对象。

例如,创建一个整数对象的指针:int* p = new int();

在这个代码中的作用

在这个程序中,这行代码用于根据用户的输入创建一个新的 Coffee 对象。由于 CoffeeDrink 类的派生类,这个对象可以被存储在 Drink* 类型的容器(如 vector<Drink*>)中,并利用多态的特性。

何时需要使用这种表示方式

通常会在以下情况下使用 new

  • 当你需要对象的生命周期超出其创建的作用域:在堆上创建的对象不会自动销毁,它们的生命周期直到你显式地使用 delete 删除它们为止。
  • 当你需要通过基类指针来实现多态:这在处理继承和多态时很常见,特别是当你有一个指向基类的指针或引用,需要指向不同的派生类对象时。
  • 当你需要大量数据:由于堆通常比栈有更多的内存空间,所以当你需要大量数据时(例如大型数组或对象),在堆上分配是更好的选择。

使用 new 的同时,你应该意识到需要在适当的时候使用 delete 来释放内存,避免内存泄漏。在现代C++中,更推荐使用智能指针(如 std::unique_ptrstd::shared_ptr)来自动管理内存,减少内存泄漏的风险。

在C++中,int* p = new int;int* p = new int(); 这两种方式都是在堆上分配一个整型数(int)并返回其地址的方式,但它们在初始化分配的整数时有细微的差别:

  1. int* p = new int;

    • 这个表达式分配了一个整数,但不对其进行初始化。这意味着分配的整数将包含一个不确定的值(通常是垃圾值)。
    • 在实践中,这意味着该整数的初始值是未定义的,依赖于特定的实现和当前内存中的值。
  2. int* p = new int();

    • 这个表达式不仅分配了一个整数,还使用了值初始化(())。这会将整数初始化为其类型的默认值,对于 int 来说,这个值是 0
    • 因此,通过这种方式分配的整数将被初始化为 0

选择哪种方式

选择使用哪种方式取决于你的需求:

  • 如果你需要一个具有确定初始值的整数(如 0),使用 new int();
  • 如果你打算在分配内存后立即赋予其一个值,或者对初始值没有特定要求,可以使用 new int;。但要记住,在赋值之前,这个整数的值是未定义的。

在现代C++编程中,更倾向于明确地初始化变量,因为这可以避免潜在的未定义行为和bug。因此,通常推荐使用 new int();,尤其是在不立即赋值的情况下。

4.push_back()

在C++中,drinks.push_back(c); 这行代码的作用是将新创建的对象(在这种情况下是指向 Coffee 类型的对象的指针 c)添加到 drinks 向量的末尾。这里,drinks 是一个存储指向 Drink 类型(及其派生类)对象的指针的 vector

语法

vector 类中的 push_back 方法的基本语法是:

vectorName.push_back(element);
  • vectorName 是向量的名称。
  • push_backvector 类的一个成员函数,用于在向量的末尾添加一个元素。
  • element 是要添加到向量的末尾的元素。这个元素应该与向量存储的元素类型相匹配。

在这个例子中,drinks 是一个 vector<Drink*> 类型的向量,所以 element 应该是一个 Drink* 类型的指针。

使用场景

在以下情况下使用 push_back 方法:

  1. 动态添加元素:当你需要向向量中添加元素,并且元素的数量在编译时不确定时,push_back 是一个很好的选择。它允许你在运行时动态地添加元素。

  2. 处理多态集合:在面向对象的编程中,特别是当你有一个基类指针的向量,并且你想存储指向不同派生类对象的指针时。例如,在这个程序中,drinks 向量可以存储指向 CoffeeTea 对象的指针,这些类都是 Drink 类的派生类。

  3. 避免数组的固定大小限制:与固定大小的数组相比,向量可以根据需要扩展和收缩,提供更大的灵活性和方便性。

示例

vector<int> numbers;   // 创建一个空的整数向量
numbers.push_back(5);  // 向向量中添加数字5
numbers.push_back(10); // 接着添加数字10

在这个例子中,numbers 最终会包含两个元素:5和10。

5.dynamic_cast

在C++中,dynamic_cast 用于在类的继承体系中进行安全的向下转型(downcasting),即将基类指针或引用转换为派生类指针或引用。这种转换在运行时检查其有效性,如果转换不合法(例如,基类指针实际上并不指向一个派生类对象),dynamic_cast 会失败。

Coffee* c = dynamic_cast<Coffee*>(d); 的意义

在这行代码中,d 是一个指向 Drink 类型(基类)对象的指针,而 CoffeeDrink 的一个派生类。dynamic_cast<Coffee*>(d) 尝试将 d 转换为一个指向 Coffee 类型的指针。

  • 如果 d 实际上指向一个 Coffee 对象,转换成功,c 将是一个有效的指向 Coffee 对象的指针。
  • 如果 d 不指向 Coffee 对象(可能指向其他派生类或为 nullptr),转换将失败,此时 c 将被设为 nullptr

dynamic_cast 的语法

dynamic_cast 的一般语法是:

dynamic_cast<NewType>(expression)
  • NewType 是你想要转换成的类型,可以是指针或引用类型。
  • expression 是要转换的表达式,通常是一个基类的指针或引用。

使用 dynamic_cast 的场合

你应该在以下情况使用 dynamic_cast

  1. 多态和继承:在一个类的继承体系中,当你需要将基类指针或引用转换为派生类指针或引用时。
  2. 类型安全:当你需要在运行时检查转换是否合法时。dynamic_cast 提供了类型安全,不合法的转换会导致返回 nullptr(对于指针)或抛出异常(对于引用)。
  3. 处理多态容器中的对象:例如,在处理存储了多种类型派生对象的基类指针的容器时,你可能需要根据实际的派生类型来进行特定的操作。

为什么使用 dynamic_cast

  • 类型安全:与其他类型转换(如 static_cast 或 C风格的转换)相比,dynamic_cast 更安全,因为它在运行时进行类型检查。
  • 多态支持:它是处理多态和继承关系中的向下转型的标准方式,尤其是当你不确定基类指针或引用实际上指向的派生类类型时。

示例

class Base { /* ... */ virtual void func() {} };
class Derived : public Base { /* ... */ };

Base* b = new Derived;
Derived* d = dynamic_cast<Derived*>(b); // 安全的向下转型
if (d) {
    // b 实际上指向一个 Derived 对象
} else {
    // b 不是一个 Derived 对象
}

在这个示例中,dynamic_cast 安全地将 Base 类型的指针 b 转换为 Derived 类型的指针 d

  • 29
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值