目录
Coffee* c = dynamic_cast(d); 的意义*>
以下面这个代码来说明:
#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*
指针删除Coffee
或Tea
对象时,相应的析构函数(Coffee::~Coffee()
或 Tea::~Tea()
)将被调用。
总结
- 使用虚析构函数是为了保证通过基类指针删除派生类对象时,能够正确地调用派生类的析构函数。
- 当类被设计为基类,并且可能会通过基类指针来删除派生类对象时,应定义虚析构函数。
- 虚析构函数的定义只需在基类的析构函数前加上
virtual
关键字。
2.关于vector
为什么使用 vector<Drink*>
?
在这个程序中,Drink
是一个抽象基类,不能直接实例化。但可以创建指向 Drink
的指针,指向其派生类(如 Coffee
和 Tea
)的实例。这允许利用多态:可以通过基类指针来调用派生类中的方法。所以,vector<Drink*>
存储的是指向 Drink
类(及其派生类)对象的指针。
规范的语法
规范的语法是 vector<Type> name;
,其中 Type
是存储在向量中的元素类型。例如:
- 存储整数的向量:
vector<int> numbers;
- 存储字符串的向量:
vector<string> words;
vector
的表示方式
vector
可以用几种不同的方式声明和初始化,例如:
-
默认构造函数:
vector<Drink*> drinks;
创建一个空的向量。 -
带初始大小的构造函数:
vector<Drink*> drinks(10);
创建一个包含10个初始化为nullptr
的Drink*
元素的向量。 -
带初始值的构造函数:
vector<Drink*> drinks(10, nullptr);
创建一个包含10个nullptr
的Drink*
元素的向量。 -
初始化列表:
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();
这行代码执行了几个重要的操作:
-
动态内存分配:
new Coffee()
部分调用了Coffee
类的构造函数来创建一个新的Coffee
对象,并将其放置在堆(heap)上。这与在栈(stack)上创建对象不同,堆上的对象不会在离开创建它的作用域时自动销毁。 -
返回指针:
new
运算符返回指向新创建的对象的指针。 -
指针赋值:将返回的指针赋值给
Coffee* c
,这里c
是一个指针变量,用于存储指向Coffee
类型对象的地址。
规范的语法
规范语法为 ClassName* pointerName = new ClassName();
。在这个语法中:
ClassName
是你想要创建的对象的类型。pointerName
是指向该对象的指针的名称。new ClassName()
创建了ClassName
类型的新对象。
例如,创建一个整数对象的指针:int* p = new int();
。
在这个代码中的作用
在这个程序中,这行代码用于根据用户的输入创建一个新的 Coffee
对象。由于 Coffee
是 Drink
类的派生类,这个对象可以被存储在 Drink*
类型的容器(如 vector<Drink*>
)中,并利用多态的特性。
何时需要使用这种表示方式
通常会在以下情况下使用 new
:
- 当你需要对象的生命周期超出其创建的作用域:在堆上创建的对象不会自动销毁,它们的生命周期直到你显式地使用
delete
删除它们为止。 - 当你需要通过基类指针来实现多态:这在处理继承和多态时很常见,特别是当你有一个指向基类的指针或引用,需要指向不同的派生类对象时。
- 当你需要大量数据:由于堆通常比栈有更多的内存空间,所以当你需要大量数据时(例如大型数组或对象),在堆上分配是更好的选择。
使用 new
的同时,你应该意识到需要在适当的时候使用 delete
来释放内存,避免内存泄漏。在现代C++中,更推荐使用智能指针(如 std::unique_ptr
或 std::shared_ptr
)来自动管理内存,减少内存泄漏的风险。
在C++中,int* p = new int;
和 int* p = new int();
这两种方式都是在堆上分配一个整型数(int
)并返回其地址的方式,但它们在初始化分配的整数时有细微的差别:
-
int* p = new int;
- 这个表达式分配了一个整数,但不对其进行初始化。这意味着分配的整数将包含一个不确定的值(通常是垃圾值)。
- 在实践中,这意味着该整数的初始值是未定义的,依赖于特定的实现和当前内存中的值。
-
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_back
是vector
类的一个成员函数,用于在向量的末尾添加一个元素。element
是要添加到向量的末尾的元素。这个元素应该与向量存储的元素类型相匹配。
在这个例子中,drinks
是一个 vector<Drink*>
类型的向量,所以 element
应该是一个 Drink*
类型的指针。
使用场景
在以下情况下使用 push_back
方法:
-
动态添加元素:当你需要向向量中添加元素,并且元素的数量在编译时不确定时,
push_back
是一个很好的选择。它允许你在运行时动态地添加元素。 -
处理多态集合:在面向对象的编程中,特别是当你有一个基类指针的向量,并且你想存储指向不同派生类对象的指针时。例如,在这个程序中,
drinks
向量可以存储指向Coffee
或Tea
对象的指针,这些类都是Drink
类的派生类。 -
避免数组的固定大小限制:与固定大小的数组相比,向量可以根据需要扩展和收缩,提供更大的灵活性和方便性。
示例
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
类型(基类)对象的指针,而 Coffee
是 Drink
的一个派生类。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
:
- 多态和继承:在一个类的继承体系中,当你需要将基类指针或引用转换为派生类指针或引用时。
- 类型安全:当你需要在运行时检查转换是否合法时。
dynamic_cast
提供了类型安全,不合法的转换会导致返回nullptr
(对于指针)或抛出异常(对于引用)。 - 处理多态容器中的对象:例如,在处理存储了多种类型派生对象的基类指针的容器时,你可能需要根据实际的派生类型来进行特定的操作。
为什么使用 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
。