C++虚函数面试题及参考答案
什么是虚函数?它的作用是什么?
虚函数是在基类中使用关键字 virtual
声明的成员函数。当在派生类中重写(override)这个函数时,会根据对象的实际类型来调用相应的函数版本,而不是仅仅根据指针或引用的类型来决定调用哪个函数。
其主要作用是实现多态性。多态允许不同的对象对同一消息做出不同的响应,增强了程序的灵活性和可扩展性。例如,有一个基类 Shape
,它有一个虚函数 draw()
,然后派生出 Circle
和 Rectangle
等子类。每个子类都可以根据自身的特点重写 draw()
函数。当通过基类指针或引用调用 draw()
函数时,会根据实际指向的对象类型来调用相应子类的 draw()
函数,而不是统一调用基类的 draw()
函数,这样就可以实现不同形状以各自独特的方式进行绘制,使代码更加通用和易于维护。
如何在 C++ 中声明一个虚函数?请举个例子说明。
在 C++ 中,要声明一个虚函数,只需在基类的函数声明前加上关键字 virtual
。以下是一个简单的例子:
class Shape {
public:virtual void draw() {cout << "Drawing a shape." << endl;}
};class Circle : public Shape {
public:void draw() override {cout << "Drawing a circle." << endl;}
};class Rectangle : public Shape {
public:void draw() override {cout << "Drawing a rectangle." << endl;}
};
在上述代码中,Shape
类中的 draw()
函数被声明为虚函数。Circle
类和 Rectangle
类都继承自 Shape
类,并分别重写了 draw()
函数 。通过这种方式,当使用 Shape
类的指针或引用调用 draw()
函数时,会根据所指向或引用的实际对象类型来调用相应类的 draw()
函数。
虚函数如何实现多态?请给出简单示例。
当通过基类的指针或引用调用虚函数时,C++ 会根据指针或引用所指向的实际对象的类型来决定调用哪个类的虚函数,从而实现多态。
继续以上面的 Shape
、Circle
和 Rectangle
类为例:
int main() {Shape* shapePtr;Circle circle;Rectangle rectangle;shapePtr = &circle;shapePtr->draw(); shapePtr = &rectangle;shapePtr->draw(); return 0;
}
在 main
函数中,首先定义了一个 Shape
类的指针 shapePtr
。当 shapePtr
指向 Circle
类的对象 circle
时,调用 shapePtr->draw()
会调用 Circle
类的 draw()
函数,输出 "Drawing a circle." 。然后当 shapePtr
指向 Rectangle
类的对象 rectangle
时,再次调用 shapePtr->draw()
会调用 Rectangle
类的 draw()
函数,输出 "Drawing a rectangle." 。这就是通过虚函数实现多态的过程,根据对象的实际类型动态地选择调用相应的函数,使得同一段代码可以针对不同类型的对象表现出不同的行为。
解释什么是动态绑定(dynamic binding)与静态绑定(static binding)。
- 动态绑定
- 动态绑定也叫晚期绑定,是指在程序运行时才确定调用哪个函数的绑定方式。在 C++ 中,通过虚函数实现动态绑定。当使用基类的指针或引用调用虚函数时,编译器会在运行时根据对象的实际类型来决定调用哪个类的虚函数。例如在上述
Shape
类及其派生类的例子中,通过Shape*
指针调用draw()
虚函数时,实际调用的函数是在运行时根据指针所指向的具体对象类型(是Circle
还是Rectangle
等)来确定的,这就是动态绑定。 - 动态绑定的优点是增加了程序的灵活性和可扩展性,使得代码能够更好地适应变化和扩展。可以编写通用的代码来处理不同类型的对象,而无需在编译时就确定具体调用的函数。
- 动态绑定也叫晚期绑定,是指在程序运行时才确定调用哪个函数的绑定方式。在 C++ 中,通过虚函数实现动态绑定。当使用基类的指针或引用调用虚函数时,编译器会在运行时根据对象的实际类型来决定调用哪个类的虚函数。例如在上述
- 静态绑定
- 静态绑定也叫早期绑定,是指在程序编译阶段就确定了调用哪个函数的绑定方式。对于非虚函数,编译器会根据指针或引用的类型来确定调用哪个类的函数。例如,如果有一个普通的非虚函数
print()
在Shape
类和其派生类中都有定义,当通过Shape*
指针调用print()
函数时,编译器会直接根据指针的类型Shape*
调用Shape
类的print()
函数,而不会考虑指针实际指向的对象类型,这就是静态绑定。 - 静态绑定的优点是效率较高,因为在编译时就已经确定了函数调用的地址,不需要在运行时进行额外的查找和判断。但它的灵活性相对较差,无法像动态绑定那样根据对象的实际类型动态地选择函数调用。
- 静态绑定也叫早期绑定,是指在程序编译阶段就确定了调用哪个函数的绑定方式。对于非虚函数,编译器会根据指针或引用的类型来确定调用哪个类的函数。例如,如果有一个普通的非虚函数
如果基类的函数是虚函数,派生类的对应函数是否需要再次声明为虚函数?
在 C++ 中,如果基类的函数是虚函数,派生类中重写该函数时,不一定要再次声明为虚函数。因为一旦基类中的函数被声明为虚函数,在派生类中重写该函数时,无论是否显式地使用 virtual
关键字,该函数仍然是虚函数,它会自动继承基类函数的虚属性 。
例如,在前面提到的 Shape
类和其派生类的例子中,Circle
类和 Rectangle
类在重写 draw()
函数时,使用了 override
关键字来显式表明这是在重写基类的虚函数,但即使不使用 override
关键字和 virtual
关键字,它们重写的 draw()
函数仍然是虚函数。
不过,为了提高代码的可读性和可维护性,建议在派生类中重写虚函数时,仍然显式地使用 virtual
关键字或者 override
关键字。override
关键字可以帮助编译器检查是否正确地重写了基类的虚函数,如果函数签名不匹配,编译器会报错,从而避免一些潜在的错误。而使用 virtual
关键字则可以清晰地表明该函数是一个虚函数,方便其他开发人员阅读和理解代码。
解释虚函数表(vtable)以及如何工作
虚函数表是 C++ 实现多态的一个关键机制。当一个类中包含虚函数时,编译器会为该类创建一个虚函数表。这个表本质上是一个函数指针数组,数组中的每个元素指向一个虚函数的地址。
对于每个包含虚函数的类,编译器会在类的内存布局中添加一个指针,通常称为虚指针(vptr)。这个虚指针指向该类的虚函数表。当创建一个类的对象时,对象的内存空间中会包含这个虚指针,通过它可以找到对应的虚函数表。
当通过基类指针或引用调用虚函数时,程序会根据指针或引用所指向的对象的虚指针,找到对应的虚函数表,然后在虚函数表中查找要调用的虚函数的地址,进而调用正确的函数版本。例如,有基类 Base 和派生类 Derived,Base 中有虚函数 func (),Derived 重写了 func ()。当通过 Base * 指针指向 Derived 对象并调用 func () 时,会先通过对象的虚指针找到 Derived 的虚函数表,再从表中找到 Derived::func () 的地址并调用,从而实现多态性。 这种机制使得在运行时可以根据对象的实际类型来动态地决定调用哪个类的虚函数,而不是仅仅根据指针或引用的类型在编译时就确定。
如果基类中有一个虚函数,派生类中的同名函数会发生什么
如果基类中有一个虚函数,派生类中定义了同名函数,且函数签名(包括参数类型、个数和返回类型)相同,那么派生类中的这个函数会自动成为虚函数,并且会重写基类的虚函数。这意味着当通过基类指针或引用调用这个虚函数时,会根据指针或引用所指向的实际对象类型来决定调用基类的虚函数还是派生类的虚函数。
例如,基类 Animal 有虚函数 sound (),派生类 Dog 和 Cat 分别重写了 sound () 函数。当有 Animal* ptr,分别指向 Dog 和 Cat 的对象时,调用 ptr->sound () 会分别调用 Dog::sound () 和 Cat::sound ()。如果派生类中的同名函数的函数签名与基类的虚函数不完全相同,那么它就不是重写,而是隐藏了基类的虚函数。此时,通过派生类对象调用该函数时,会调用派生类自己的函数,而通过基类指针或引用调用时,会根据指针或引用的类型调用基类的虚函数,除非使用了作用域解析运算符明确指定调用派生类的函数。
在 C++ 中,虚函数的调用如何保证多态性
在 C++ 中,虚函数通过动态绑定来保证多态性。当程序中通过基类的指针或引用调用虚函数时,编译器不会在编译时就确定要调用的具体函数版本,而是在运行时根据指针或引用所指向的实际对象的类型来决定。
每个包含虚函数的类都有一个虚函数表,类的对象中有一个虚指针指向该虚函数表。当创建不同派生类的对象时,它们各自的虚函数表中存放着对应类的虚函数地址。当通过基类指针调用虚函数时,程序会顺着指针找到对象的虚指针,进而找到对应的虚函数表,然后从表中获取要调用的虚函数的地址,最终调用正确的函数版本。
例如,有图形类 Shape 作为基类,有 Circle 和 Rectangle 等派生类,都重写了 Shape 的虚函数 draw ()。当有 Shape* ptr,分别指向 Circle 和 Rectangle 的对象时,调用 ptr->draw () 会分别调用 Circle::draw () 和 Rectangle::draw (),即使 ptr 的类型是 Shape* ,也能根据对象的实际类型动态地调用不同的函数,实现了不同形状以各自独特的方式绘制的多态效果,使得代码更加灵活和通用,能够适应不同类型对象的需求。
如果类中没有任何虚函数,如何保证在继承关系中实现多态
如果类中没有虚函数,要在继承关系中实现多态,可以通过以下几种方式:
- 使用函数重载:在基类和派生类中定义名称相同但参数列表不同的非虚函数。通过传递不同类型或数量的参数来调用不同类中的相应函数,实现类似多态的效果,但这种方式的多态性比较有限,主要基于参数的类型和数量来区分。
- 使用模板:C++ 的模板机制可以实现一种编译时多态。通过定义函数模板或类模板,根据不同的模板参数生成不同的函数或类的具体版本。例如,定义一个函数模板来处理不同类型的数据,在编译时根据实际使用的数据类型生成相应的函数代码,从而实现对不同类型数据的通用处理。
- 使用接口类和纯虚函数:定义一个抽象基类作为接口,其中包含纯虚函数,派生类必须实现这些纯虚函数。通过基类指针或引用调用这些纯虚函数时,根据派生类的具体实现来实现多态性。虽然这种方式涉及到虚函数,但重点在于通过接口类的设计模式来强制派生类实现特定的行为,以达到多态的目的。
什么时候需要声明析构函数为虚函数?为什么?
当一个类有可能通过基类指针或引用被删除时,就需要将析构函数声明为虚函数。原因如下:
如果析构函数不是虚函数,当通过基类指针删除派生类对象时,只会调用基类的析构函数,而不会调用派生类的析构函数,这可能导致派生类中一些资源没有被正确释放,从而造成内存泄漏或其他资源相关的问题。
例如,有基类 Base 和派生类 Derived,Base 的析构函数不是虚函数。当有 Base* ptr = new Derived (); 然后执行 delete ptr; 时,只会调用 Base 的析构函数,而 Derived 类中特有的资源清理代码不会被执行。但如果将 Base 的析构函数声明为虚函数,那么在执行 delete ptr; 时,会先调用 Derived 的析构函数,然后再调用 Base 的析构函数,保证了整个对象包括派生类部分的资源都能被正确释放。 所以,在有继承关系且可能通过基类指针或引用操作派生类对象的情况下,将析构函数声明为虚函数是一种良好的编程实践,可以确保对象的正确销毁和资源的正确释放。
派生类中的虚函数可以重载吗?请举个例子说明。
派生类中的虚函数是可以重载的。当在派生类中重载基类的虚函数时,实际上是对虚函数的重写(override),也就是在派生类中提供了一个与基类虚函数具有相同函数签名(函数名、参数列表和返回类型都相同)的新函数实现,以实现多态性。
以下是一个例子:
class Shape {
public:virtual void draw() {cout << "Drawing a shape." << endl;}
};class Circle : public Shape {
public:void draw() override { // 这是对基类虚函数的重写cout << "Drawing a circle." << endl;}void draw(int radius) { // 这是对draw函数的重载cout << "Drawing a circle with radius " << radius << endl;}
};int main() {Circle circle;Shape* shapePtr = &circle;shapePtr->draw(); circle.draw(5); return 0;
}
在上述代码中,Shape
类有一个虚函数draw()
,Circle
类继承自Shape
类并对draw()
函数进行了重写。同时,Circle
类还重载了draw()
函数,增加了一个接受半径参数的版本。在main
函数中,通过基类指针shapePtr
调用draw()
函数时,会根据对象的实际类型调用Circle
类重写的draw()
函数,输出 "Drawing a circle." 。而直接通过Circle
对象circle
调用draw(5)
时,则会调用重载后的draw()
函数,输出 "Drawing a circle with radius 5" 。 这展示了派生类中虚函数既可以重写实现多态,也可以重载提供更多的功能变体。
如果基类的虚函数没有被派生类覆盖,那么派生类对象调用该函数时会发生什么?
如果基类的虚函数没有被派生类覆盖,那么当通过派生类对象调用该虚函数时,会调用基类的虚函数版本。
例如:
class Base {
public:virtual void print() {cout << "Base class print function." << endl;}
};class Derived : public Base {// 没有重写print函数
};int main() {Derived derived;derived.print(); return 0;
}
在这个例子中,Base
类有一个虚函数print()
,而Derived
类继承自Base
类但没有重写print()
函数。在main
函数中,创建了Derived
类的对象derived
并调用print()
函数,此时会调用Base
类的print()
函数,输出 "Base class print function." 。这是因为派生类继承了基类的虚函数,如果没有在派生类中提供自己的实现,那么就会使用基类的默认实现。这种机制保证了在存在继承关系且有虚函数的情况下,即使派生类没有对所有虚函数进行重写,程序仍然能够正确地调用到合适的函数版本,维持了一定的行为一致性和默认行为设定 。
解释 “纯虚函数” 以及如何定义它。
纯虚函数是在基类中声明的虚函数,它在基类中没有具体的实现,只有函数声明,并且通过在函数声明的结尾添加 = 0
来标识。
其语法形式如下:
class AbstractClass {
public:virtual void pureVirtualFunction() = 0;
};
纯虚函数的主要目的是为派生类提供一个统一的接口规范,强制派生类必须重写该函数以实现具体的功能。它定义了一种抽象的行为或操作,具体的实现留给派生类去完成。含有纯虚函数的类称为抽象类,抽象类不能被实例化,只能作为基类被继承,其存在的意义在于为一组相关的派生类提供一个通用的框架和接口约定。
例如,定义一个图形抽象类 Shape
:
class Shape {
public:virtual void draw() = 0; virtual double area() = 0;
};
这里的 draw()
和 area()
都是纯虚函数,它规定了所有图形类都应该有绘制自身和计算面积的功能,但具体如何绘制和计算面积则由具体的图形派生类如 Circle
、Rectangle
等来实现。通过纯虚函数,可以确保派生类遵循一定的接口规范,使得代码更加结构化和易于维护,同时也体现了面向对象编程中的抽象和多态的思想。
纯虚函数与普通虚函数的区别是什么?
-
有无具体实现
- 纯虚函数在基类中只有声明,没有具体的函数体,即没有实现代码,仅仅是定义了一个接口规范,要求派生类必须实现它。例如
virtual void pureVirtualFunction() = 0;
,这里只声明了函数,没有具体的代码逻辑。 - 普通虚函数在基类中有完整的函数实现,当派生类没有重写时,会使用基类的函数实现。如
virtual void normalVirtualFunction() { cout << "Base class implementation." << endl; }
,基类中有具体的输出语句来实现该函数的功能 。
- 纯虚函数在基类中只有声明,没有具体的函数体,即没有实现代码,仅仅是定义了一个接口规范,要求派生类必须实现它。例如
-
类的可实例化性
- 含有纯虚函数的类是抽象类,不能直接创建对象实例。它的存在主要是为了被继承,为派生类提供一个通用的框架和接口。比如上述的
Shape
类含有纯虚函数,就不能直接Shape s;
这样创建对象 。 - 包含普通虚函数的类是可以被实例化的,只要它满足其他类实例化的条件。例如
class MyClass { virtual void myVirtualFunction() { cout << "MyClass virtual function." << endl; } };
,可以正常地创建MyClass
的对象并使用。
- 含有纯虚函数的类是抽象类,不能直接创建对象实例。它的存在主要是为了被继承,为派生类提供一个通用的框架和接口。比如上述的
-
对派生类的要求
- 对于纯虚函数,派生类必须重写该函数以提供具体的实现,否则派生类也会成为抽象类,无法实例化。这是一种强制派生类遵循特定接口规范的机制。
- 普通虚函数的派生类可以选择重写也可以不重写,如果不重写,则继承基类的函数实现。这给予了派生类更大的灵活性,是否重写取决于具体的需求和设计。
如何声明和使用抽象类?
- 声明抽象类
- 抽象类的声明主要是通过在类中定义至少一个纯虚函数来实现。如前面提到的图形抽象类
Shape
:
- 抽象类的声明主要是通过在类中定义至少一个纯虚函数来实现。如前面提到的图形抽象类
class Shape {
public:virtual void draw() = 0; virtual double area() = 0;
};
这里通过在虚函数 draw()
和 area()
的声明结尾添加 = 0
,将它们定义为纯虚函数,从而使 Shape
类成为抽象类。
- 使用抽象类
- 抽象类不能直接被实例化,只能作为基类被其他类继承。派生类必须重写抽象类中的所有纯虚函数,否则派生类也会成为抽象类。例如,定义
Circle
类继承自Shape
类:
- 抽象类不能直接被实例化,只能作为基类被其他类继承。派生类必须重写抽象类中的所有纯虚函数,否则派生类也会成为抽象类。例如,定义
class Circle : public Shape {
private:double radius;
public:Circle(double r) : radius(r) {}void draw() override {cout << "Drawing a circle." << endl;}double area() override {return 3.14 * radius * radius;}
};
在 Circle
类中,重写了 Shape
类中的纯虚函数 draw()
和 area()
,这样 Circle
类就不再是抽象类,可以创建对象实例。然后可以通过基类指针或引用调用派生类对象的函数来实现多态性,如:
int main() {Shape* shapePtr = new Circle(5);shapePtr->draw(); cout << "Area: " << shapePtr->area() << endl;delete shapePtr;return 0;
}
在 main
函数中,通过基类指针 shapePtr
指向 Circle
类的对象,然后调用 draw()
和 area()
函数,会根据对象的实际类型调用 Circle
类的相应函数,实现了多态的效果。这种声明和使用抽象类的方式使得代码具有更好的可扩展性和可维护性,通过抽象类定义通用的接口和行为规范,由派生类具体实现,符合面向对象编程的开闭原则 。
如果一个类中包含纯虚函数,能否创建该类的对象?为什么?
一个类中如果包含纯虚函数,是不能创建该类的对象的。这是因为纯虚函数在类中只是一个声明,没有具体的函数实现,它的存在是为了定义一个接口规范,强制要求派生类去实现这个函数。从类的设计角度来看,包含纯虚函数的类是一种抽象的概念,它代表了一组具有某些共同特征和行为的对象的抽象类型,但本身不具备完整的实现细节,所以不应该被实例化。
例如,定义一个抽象的图形类 Shape
,其中有纯虚函数 draw()
和 area()
:
class Shape {
public:virtual void draw() = 0; virtual double area() = 0;
};
这里的 Shape
类就不能直接创建对象,如 Shape s;
这样的代码是错误的。因为如果允许创建 Shape
类的对象,那么调用 draw()
和 area()
函数时就没有具体的实现可执行,这违背了纯虚函数的设计初衷,即它只是为派生类提供一个统一的接口框架,具体的实现应该由派生类来完成。只有当派生类实现了所有的纯虚函数,该派生类才不再是抽象类,可以创建对象实例 。
基类中虚函数的定义与派生类中的实现有冲突时,会发生什么?
当基类中虚函数的定义与派生类中的实现存在冲突时,主要有以下几种情况及后果:
- 函数签名不一致但非故意重载
- 如果派生类中定义的函数与基类虚函数在函数名相同,但参数列表或返回类型不同,且不是有意进行函数重载,那么此时派生类中的函数不会重写基类的虚函数,而是隐藏了基类的虚函数。
- 这意味着通过基类指针或引用调用该虚函数时,会根据指针或引用的类型调用基类的虚函数,而不是根据对象的实际类型调用派生类中定义的那个看似相关但签名不同的函数。例如:
class Base {
public:virtual void func(int x) { cout << "Base::func(int)" << endl; }
};class Derived : public Base {
public:void func(double x) { cout << "Derived::func(double)" << endl; }
};int main() {Derived d;Base* ptr = &d;ptr->func(5); return 0;
}
在这个例子中,Derived
类中的 func()
函数并没有重写 Base
类的 func()
函数,尽管函数名相同,但参数类型不同。所以通过 Base*
指针调用 func(5)
时,会调用 Base
类的 func(int)
函数,输出 "Base::func (int)" 。
- 函数签名一致但违反重写规则
- 如果派生类在重写基类虚函数时,没有正确遵循重写的规则,例如没有使用
override
关键字(在 C++11 及以后)或者函数的const
修饰符等与基类不一致,可能会导致一些潜在的错误和不符合预期的行为。 - 在使用
override
关键字的情况下,如果函数签名不匹配,编译器会报错,提示没有正确重写基类的虚函数,这有助于在编译阶段发现错误。而如果没有使用override
关键字且函数签名有细微差别,可能会出现编译通过但运行时行为不符合预期的情况,例如函数的const
性质不同可能导致在某些情况下无法正确调用到期望的函数版本 。
- 如果派生类在重写基类虚函数时,没有正确遵循重写的规则,例如没有使用
派生类如何调用基类的虚函数?
派生类可以通过作用域解析运算符 ::
来调用基类的虚函数。即使派生类重写了基类的虚函数,仍然可以使用这种方式明确地指定调用基类的版本。
例如,有基类 Base
和派生类 Derived
:
class Base {
public:virtual void print() {cout << "Base::print()" << endl;}
};class Derived : public Base {
public:void print() override {cout << "Derived::print()" << endl;// 调用基类的print函数Base::print(); }
};int main() {Derived d;d.print();return 0;
}
在 Derived
类的 print()
函数中,通过 Base::print()
就可以调用基类 Base
的 print()
虚函数。当在 main
函数中创建 Derived
类的对象 d
并调用 print()
函数时,首先会输出 "Derived::print ()" ,然后通过 Base::print()
调用会输出 "Base::print ()" 。这样就实现了在派生类中调用基类的虚函数,在某些情况下,这对于在派生类的函数实现中复用基类的功能或者在重写的函数中添加一些额外的操作前后调用基类函数是非常有用的 。
如果派生类覆盖了基类的虚函数,是否可以调用基类版本的虚函数?请举例。
派生类覆盖了基类的虚函数后,是可以调用基类版本的虚函数的,正如前面所提到的,可以通过作用域解析运算符 ::
来实现。
以下是一个更详细的例子:
class Animal {
public:virtual void sound() {cout << "Animal makes a sound." << endl;}
};class Dog : public Animal {
public:void sound() override {cout << "Dog barks." << endl;// 调用基类的sound函数Animal::sound(); }
};int main() {Dog dog;dog.sound();return 0;
}
在这个例子中,Animal
类有一个虚函数 sound()
,Dog
类继承自 Animal
类并覆盖了 sound()
函数。在 Dog
类的 sound()
函数中,通过 Animal::sound()
调用了基类 Animal
的 sound()
函数。当在 main
函数中创建 Dog
类的对象 dog
并调用 sound()
函数时,首先会输出 "Dog barks." ,然后通过调用基类的函数会输出 "Animal makes a sound." 。
这种在派生类中调用基类虚函数的方式在很多场景下都很有用,比如在派生类的特定行为基础上,还需要保留基类的一些通用行为或者在派生类重写的函数中添加一些额外的逻辑前后调用基类函数,以实现更复杂的功能需求 。
如果基类的析构函数是虚函数,派生类的析构函数是否必须也是虚函数?
如果基类的析构函数是虚函数,派生类的析构函数不必须显式地声明为虚函数,但建议将其声明为虚函数。
从语法上来说,当基类的析构函数是虚函数时,派生类的析构函数无论是否显式声明为虚函数,它都会自动成为虚函数,这是因为析构函数的虚属性是会被继承的。
然而,从代码的可读性和可维护性角度考虑,建议在派生类中也显式地将析构函数声明为虚函数。这样做可以让代码更加清晰易懂,明确地表明该析构函数具有虚函数的性质,尤其是在大型项目或复杂的继承体系中,有助于其他开发人员快速理解代码的意图。
例如:
class Base {
public:virtual ~Base() {cout << "Base::~Base()" << endl;}
};class Derived : public Base {
public:~Derived() {cout << "Derived::~Derived()" << endl;}
};int main() {Base* ptr = new Derived();delete ptr;return 0;
}
在这个例子中,Base
类的析构函数是虚函数,Derived
类的析构函数虽然没有显式声明为虚函数,但它实际上也是虚函数。当通过基类指针 ptr
删除派生类对象时,会先调用 Derived
类的析构函数,然后再调用 Base
类的析构函数,正确地释放了整个对象的资源,包括派生类中可能额外分配的资源 。如果没有将基类的析构函数声明为虚函数,那么通过基类指针删除派生类对象时,只会调用基类的析构函数,可能导致派生类中的资源无法正确释放,从而引发内存泄漏等问题 。
为什么在面向对象编程中,基类的析构函数通常应该声明为虚函数?
在面向对象编程中,将基类的析构函数声明为虚函数主要是为了确保在通过基类指针或引用删除派生类对象时,能够正确地调用派生类的析构函数,从而避免内存泄漏和资源未正确释放等问题 。
当存在继承关系时,如果基类的析构函数不是虚函数,那么在通过基类指针删除派生类对象时,编译器只会根据指针的类型调用基类的析构函数,而不会调用派生类的析构函数。这就可能导致派生类中动态分配的内存或其他资源没有被释放,从而引发内存泄漏。
例如,有一个基类 Base
和一个派生类 Derived
,Derived
类中可能有一些自己动态分配的资源:
class Base {
public:~Base() {cout << "Base destructor" << endl;}
};class Derived : public Base {
private:int* arr;
public:Derived() {arr = new int[10];}~Derived() {cout << "Derived destructor" << endl;delete[] arr;}
};int main() {Base* ptr = new Derived();delete ptr; return 0;
}
在这个例子中,如果 Base
类的析构函数不是虚函数,那么执行 delete ptr;
时,只会调用 Base
类的析构函数,Derived
类中动态分配的数组 arr
就不会被释放,造成内存泄漏。
但如果将 Base
类的析构函数声明为虚函数,那么在执行 delete ptr;
时,会先调用 Derived
类的析构函数,释放 Derived
类中动态分配的资源,然后再调用 Base
类的析构函数,保证了整个对象包括派生类部分的资源都能被正确释放,从而实现了正确的对象销毁和资源清理,这在多态场景下是非常重要的,有助于维护程序的稳定性和可靠性 。
如果一个类的析构函数是虚函数,如何避免内存泄漏?
当一个类的析构函数是虚函数时,已经为正确释放资源和避免内存泄漏提供了基础保障,但还需要注意以下几点来进一步确保避免内存泄漏:
-
正确的内存分配与释放原则
- 在类的构造函数和其他成员函数中,如果有动态内存分配,一定要在析构函数中进行对应的释放。遵循谁分配谁释放的原则,确保每一块动态分配的内存都有对应的释放操作。例如,如果在类中使用
new
操作符分配了内存,那么在析构函数中一定要使用相应的delete
操作符进行释放。如果是分配了数组,要使用delete[]
。 - 除了内存,对于其他资源如文件句柄、网络连接等,也要在析构函数中进行正确的关闭和释放操作,以避免资源泄漏。
- 在类的构造函数和其他成员函数中,如果有动态内存分配,一定要在析构函数中进行对应的释放。遵循谁分配谁释放的原则,确保每一块动态分配的内存都有对应的释放操作。例如,如果在类中使用
-
注意派生类的资源管理
- 如果存在派生类,派生类的析构函数不仅要释放自身动态分配的资源,还要注意调用基类的析构函数。在 C++ 中,当派生类的析构函数被调用时,会自动调用基类的析构函数,但前提是基类的析构函数是虚函数或者在派生类的析构函数中显式地调用了基类的析构函数。
- 派生类在重写析构函数时,不能错误地隐藏基类的析构函数。如果在派生类中定义了一个与基类析构函数参数列表或其他方面不匹配的析构函数,可能会导致在删除对象时无法正确调用基类的析构函数,从而引发潜在的内存泄漏或资源未释放问题 。
-
避免悬空指针和野指针
- 在使用指针时,要确保指针始终指向有效的内存地址,避免出现悬空指针和野指针。当删除一个对象后,要将指向该对象的指针设置为
nullptr
,以防止后续误操作该指针导致程序错误和潜在的内存问题。 - 如果在类中有指针成员变量,要合理地初始化、赋值和释放指针,防止出现内存泄漏或悬空指针的情况。例如,在拷贝构造函数和赋值运算符重载函数中,要正确地处理指针的拷贝和释放,避免浅拷贝导致的内存问题。
- 在使用指针时,要确保指针始终指向有效的内存地址,避免出现悬空指针和野指针。当删除一个对象后,要将指向该对象的指针设置为
如果基类的析构函数不是虚函数,销毁派生类对象时会发生什么?
如果基类的析构函数不是虚函数,当销毁派生类对象时,会出现资源未完全释放的问题,具体表现如下:
-
只调用基类析构函数
- 当通过基类指针删除派生类对象时,编译器根据指针的类型来确定调用的析构函数,所以只会调用基类的析构函数,而不会调用派生类的析构函数。这意味着派生类中特有的资源,如动态分配的内存、打开的文件句柄、建立的网络连接等,都不会被释放,从而导致内存泄漏和资源浪费。
-
部分对象未正确销毁
- 由于派生类是基于基类扩展而来的,派生类对象中不仅包含派生类自身定义的成员变量和资源,还包含基类的成员变量和资源。只调用基类的析构函数,只能清理基类部分的资源,而派生类部分的资源仍然保留在内存中,这使得对象没有被完全销毁,可能导致程序出现各种错误和不稳定的情况。
-
内存泄漏的隐患
- 随着程序的运行,如果多次创建和删除这样的派生类对象,而每次都无法正确释放派生类的资源,那么内存泄漏会越来越严重,最终可能耗尽系统资源,影响程序的正常运行甚至导致程序崩溃。
例如,有基类 Animal
和派生类 Dog
,Dog
类中有动态分配的数组用于存储狗的名字:
class Animal {
public:~Animal() {cout << "Animal destructor" << endl;}
};class Dog : public Animal {
private:char* name;
public:Dog() {name = new char[20];strcpy(name, "Buddy");}~Dog() {cout << "Dog destructor" << endl;delete[] name;}
};int main() {Animal* ptr = new Dog();delete ptr; return 0;
}
在这个例子中,由于 Animal
类的析构函数不是虚函数,执行 delete ptr;
时,只会调用 Animal
类的析构函数,Dog
类中动态分配的数组 name
不会被释放,造成内存泄漏,且 Dog
类对象没有被完全销毁 。
请解释 “多态删除” 以及它和虚析构函数的关系。
-
多态删除的含义
- 多态删除是指在面向对象编程中,通过基类指针或引用删除派生类对象时,能够根据对象的实际类型来调用相应类的析构函数,从而实现正确的对象销毁和资源释放的机制。它是多态性在对象销毁过程中的体现,使得代码在处理不同类型的对象时更加灵活和通用。
-
与虚析构函数的关系
- 虚析构函数是实现多态删除的关键。当基类的析构函数被声明为虚函数时,在通过基类指针或引用删除对象时,编译器会在运行时根据对象的实际类型来决定调用哪个类的析构函数。如果对象是派生类类型,那么会先调用派生类的析构函数,然后再调用基类的析构函数,从而保证了整个对象包括派生类中特有的资源都能被正确释放,实现了多态删除。
- 没有虚析构函数,多态删除就无法正确实现。如果基类的析构函数不是虚函数,那么在通过基类指针删除派生类对象时,只会调用基类的析构函数,导致派生类的资源无法释放,引发内存泄漏等问题,也就无法达到多态删除所要求的根据对象实际类型正确销毁对象的目的。
例如,有一个基类 Shape
和派生类 Circle
、Rectangle
,它们都有各自的资源需要在析构函数中释放:
class Shape {
public:virtual ~Shape() {cout << "Shape destructor" << endl;}
};class Circle : public Shape {
private:double radius;
public:Circle(double r) : radius(r) {}~Circle() {cout << "Circle destructor" << endl;}
};class Rectangle : public Shape {
private:double length, width;
public:Rectangle(double l, double w) : length(l), width(w) {}~Rectangle() {cout << "Rectangle destructor" << endl;}
};int main() {Shape* shapePtr1 = new Circle(5);Shape* shapePtr2 = new Rectangle(3, 4);delete shapePtr1; delete shapePtr2; return 0;
}
在这个例子中,由于 Shape
类的析构函数是虚函数,所以在 main
函数中通过 Shape*
指针删除 Circle
和 Rectangle
类的对象时,能够实现多态删除,分别调用了 Circle
类和 Rectangle
类的析构函数,然后再调用 Shape
类的析构函数,确保了各个类的资源都能被正确释放 。
如果基类的析构函数没有定义,编译器会如何处理?
如果基类的析构函数没有定义,编译器会为基类生成一个默认的析构函数。这个默认析构函数的行为如下:
-
释放基类的成员变量资源
- 对于基类中普通的成员变量,如基本数据类型的变量、数组等,默认析构函数不会进行任何特殊的处理,因为这些变量在其生命周期结束时会自动按照相应的规则被销毁或释放。
- 如果基类中有类类型的成员变量,且该类的析构函数是公有的,那么默认析构函数会在合适的时候调用这些成员变量所属类的析构函数,以确保它们的资源也能得到正确释放。
-
不处理派生类的资源
- 由于编译器生成的默认析构函数并不知道派生类的存在以及派生类中可能有的资源,所以它不会对派生类中的资源进行释放。当通过基类指针或引用删除派生类对象时,如果基类的析构函数是默认的且没有被声明为虚函数,就会导致派生类中的资源无法被释放,从而引发内存泄漏等问题。
-
遵循一般的对象生命周期规则
- 编译器生成的默认析构函数会在对象的生命周期结束时被自动调用,例如当对象所在的作用域结束或者通过
delete
操作符显式删除对象时。它的调用顺序遵循 C++ 中对象销毁的一般规则,即先调用派生类的析构函数(如果有),然后再调用基类的析构函数,但前提是基类的析构函数是虚函数或者在派生类的析构函数中显式地调用了基类的析构函数。如果基类的析构函数不是虚函数,那么通过基类指针删除派生类对象时,就不会按照这个正确的顺序调用析构函数,导致资源未正确释放 。
- 编译器生成的默认析构函数会在对象的生命周期结束时被自动调用,例如当对象所在的作用域结束或者通过
例如,有基类 Base
和派生类 Derived
:
class Base {
public:// 没有定义析构函数,编译器会生成默认析构函数
};class Derived : public Base {
private:int* arr;
public:Derived() {arr = new int[10];}~Derived() {delete[] arr;}
};int main() {Base* ptr = new Derived();// 如果Base的析构函数不是虚函数,这里会导致内存泄漏delete ptr; return 0;
}
在这个例子中,如果 Base
类没有定义析构函数,编译器会生成默认析构函数,但这个默认析构函数不会释放 Derived
类中动态分配的数组 arr
,只有 Derived
类的析构函数能释放它。如果 Base
类的析构函数不是虚函数,那么执行 delete ptr;
时,就无法正确释放 Derived
类的资源,造成内存泄漏 。
相关文章:
C++虚函数面试题及参考答案
什么是虚函数?它的作用是什么? 虚函数是在基类中使用关键字 virtual 声明的成员函数。当在派生类中重写(override)这个函数时,会根据对象的实际类型来调用相应的函数版本,而不是仅仅根据指针或引用的类型来…...
如何搭建C++环境--1.下载安装并调试Microsoft Visual Studio Previerw(Windows)
1.首先,打开浏览器 首先,搜索“Microsoft Visual Studio Previerw” 安装 1.运行VisualStudioSetup (1).exe 无脑一直点继续 然后就到 选择需要的语言 我一般python用pycharm Java,HTML用vscode(Microsoft Visual Studio cod…...
大数据新视界 -- Hive 函数应用:复杂数据转换的实战案例(下)(12/ 30)
💖💖💖亲爱的朋友们,热烈欢迎你们来到 青云交的博客!能与你们在此邂逅,我满心欢喜,深感无比荣幸。在这个瞬息万变的时代,我们每个人都在苦苦追寻一处能让心灵安然栖息的港湾。而 我的…...
深入理解 TypeScript:联合类型与交叉类型的应用
在 TypeScript 的世界里,类型系统是核心特性之一,它提供了强大的工具来帮助开发者编写更安全、更可靠的代码。今天,我们将深入探讨 TypeScript 中的两个高级类型特性:联合类型(Union Types)和交叉类型&…...
fiddler抓包工具与requests库构建自动化报告
一. Fiddler 抓包工具 1.1 Fiddler 工具介绍和安装 Fiddler 是一款功能强大的 HTTP 调试代理工具,能够全面记录并深入检查您的计算机与互联网之间的 HTTP 和 HTTPS 通信数据。其主界面布局清晰,主要包含菜单栏、工具栏、树形标签栏和内容栏。 1.2 Fid…...
数据结构——排序算法第二幕(交换排序:冒泡排序、快速排序(三种版本) 归并排序:归并排序(分治))超详细!!!!
文章目录 前言一、交换排序1.1 冒泡排序1.2 快速排序1.2.1 hoare版本 快排1.2.2 挖坑法 快排1.2.3 lomuto前后指针 快排 二、归并排序总结 前言 继上篇学习了排序的前面两个部分:直接插入排序和选择排序 今天我们来学习排序中常用的交换排序以及非常稳定的归并排序 快排可是有多…...
Vue-常用指令
🌈个人主页:前端青山 🔥系列专栏:Vue篇 🔖人终将被年少不可得之物困其一生 依旧青山,本期给大家带来Vue篇专栏内容:Vue-常用指令 目录 1.1 v-cloak 1.2 双向数据绑定指令 v-model 1.3 v-once 1.4 绑定属性 v-bind…...
守护进程
目录 守护进程 前台进程 后台进程 session(进程会话) 前台任务和后台任务比较好 本质 绘画和终端都关掉了,那些任务仍然在 bash也退了,然后就托孤了 编辑 守护进程化---不想受到任何用户登陆和注销的影响编辑 如何…...
GPON原理
GPON网络架构 对于OLT来说,它就相当于一个指挥官,它指挥PON口下的ONU在指定的时间段内发送数据以及发起测距过程等 而ONU则是一个士兵,按照OLT的指挥做出相应 而ODN它主要就是提供一个传输通道,主要包括分光器和光纤组成 对于PO…...
华三(HCL)和华为(eNSP)模拟器共存安装手册
接上章叙述,解决同一台PC上同时部署华三(HCL)和华为(eNSP)模拟器。原因就是华三HCL 的老版本如v2及以下使用VirtualBox v5版本,可以直接和eNSP兼容Oracle VirtualBox,而其他版本均使用Oracle VirtualBox v6以上的版本,…...
类和对象--中--初始化列表(重要)、隐式类型转化(理解)、最后两个默认成员函数
1.初始化列表 1.1作用: 通过特定的值,来初始化对象。 1.2定义: 初始化列表,就相当于定义对象(开空间)。不管写不写初始化列表,每个成员变量都会走一遍初始化列表(开出对应的空间…...
clickhouse 使用global in 优化 in查询
文章目录 in例子使用global in in例子 SELECT uniq(UserID) FROM distributed_table WHERE CounterID 101500 AND UserID IN (SELECT UserID FROM distributed_table WHERE CounterID 34)对于in 查询来说,本来查询的就是分布式表,假设这个表有100 个…...
macos 14.0 Monoma 修改顶部菜单栏颜色
macos 14.0 设置暗色后顶部菜单栏还维持浅色,与整体不协调。 修改方式如下:...
鸿蒙动画开发07——粒子动画
1、概 述 粒子动画是在一定范围内随机生成的大量粒子产生运动而组成的动画。 动画元素是一个个粒子,这些粒子可以是圆点、图片。我们可以通过对粒子在颜色、透明度、大小、速度、加速度、自旋角度等维度变化做动画,来营造一种氛围感,比如下…...
Matlab 2016b安装教程附安装包下载
软件介绍 MATLAB(矩阵实验室)是MathWorks公司推出的用于算法开发、数据可视化、数据分析以及数值计算的高级技术计算语言和交互式环境的商业数学软件。MATLAB具有数值分析、数值和符号计算、工程与科学绘图、控制系统的设计与仿真、数字图像处理、数字信…...
Container image .... already present on machine 故障排除
故障现象: Normal Pulled 12s (x2 over 15s) kubelet Container image “registry.cn-hangzhou.aliyuncs.com/yinzhengjie-k8s/apps:v1” already present on machine kubectl get pods NAME READY STATUS RESTARTS AGE two-pod 1/2 Error …...
力扣 二叉树的层序遍历-102
二叉树的层序遍历-102 class Solution { public:vector<vector<int>> levelOrder(TreeNode* root) {vector<vector<int>> res; // 二维数组用来存储每层节点if (root nullptr)return res;queue<TreeNode*> q; // 队列用来进行层序遍历q.push(r…...
Java 平衡二叉树 判断 详解
判断平衡二叉树的详解(Java 实现) 平衡二叉树的定义: 平衡二叉树(Balanced Binary Tree)是指一棵二叉树中任意节点的左右子树高度差不超过 1。即: ∣ h e i g h t ( l e f t ) − h e i g h t ( r i g h …...
Java设计模式笔记(一)
Java设计模式笔记(一) (23种设计模式由于篇幅较大分为两篇展示) 一、设计模式介绍 1、设计模式的目的 让程序具有更好的: 代码重用性可读性可扩展性可靠性高内聚,低耦合 2、设计模式的七大原则 单一职…...
【人工智能学习之yolov8改进的网络怎么指定规模】
yolov8改进的网络怎么指定规模 在你更换主干网络或者做了其他修改之后,发现模型总是默认使用的n规模,而n规模有可能无法完成任务,怎么办呢,有什么办法指定规模大小呢? WARNING ⚠️ no model scale passed. Assuming …...
网络安全概述
网络安全 物理安全 网络的物理安全是整个网络系统安全的前提。在 校园网工程建设中,由于网络系统属于 弱电工程,耐压值很低。因此,在 网络工程的设计和施工中,必须优先考虑保护人和 网络设备不受电、火灾和雷击的侵害࿱…...
[MySQL#2] 库 | 表 | 详解CRUD命令 | 字符集 | 校验规则
目录 一. 库操作 1. 创建数据库 2. 字符集和校验规则 校验规则对数据库的影响 显示创建数据库时对应的命令 3. 修改数据库 4. 数据库删除 备份和恢复 还原 查看连接情况 二. 表操作 1. 创建表(定义实例化格式 2. 创建表案例 (实例化数据类型…...
【Unity基础】如何查看当前项目使用的渲染管线?
在 Unity 中,你可以通过以下几种方式查看当前项目使用的是哪个渲染管线: 1. 检查 Graphics Settings 打开 Unity 编辑器,进入顶部菜单:Edit → Project Settings → Graphics。在 Graphics Settings 窗口中,找到 Scr…...
什么是域名监控?
域名监控是持续跟踪全球域名系统(DNS)中变化以发现恶意活动迹象的过程。组织可以对其拥有的域名进行监控,以判断是否有威胁行为者试图入侵其网络。他们还可以对客户的域名使用这种技术以执行类似的检查。 你可以将域名监控比作跟踪与自己实物…...
apache中的Worker 和 Prefork 之间的区别是什么?
文章目录 内存使用稳定性兼容性适用场景 Apache中的Worker和Prefork两种工作模式在内存使用、稳定性以及兼容性等方面存在区别 内存使用 Worker:由于使用线程,内存占用较少。Prefork:每个进程独立运行,内存消耗较大。 稳定性 W…...
解决SSL VPN客户端一直提示无法连接服务器的问题
近期服务器更新VPN后,我的win10电脑一致无法连接到VPN服务器, SSL VPN客户端总是提示无法连接到服务端。网上百度尝试了各种方法后,终于通过以下设置方式解决了问题: 1、首先,在控制面板中打开“网络和共享中心”窗口&…...
网络基础概念
1.网络协议 网络协议是一组标准和规则,用于定义电子设备如何在网络上通信。这些规则涵盖了数据如何格式化,传输,路由以及接收。网络协议确保了不同制造商的设备能够相互理解和交换数据 协议分层 协议也是软件,在设计上为了更好…...
sunshine和moonlight串流网络丢失帧高的问题(局域网)
注:此贴结果仅供参考 场景环境:单身公寓 路由器:2016年的路由器 开始:电脑安装sunshine软件,手机安装moonlight软件开始串流发现网络丢失帧发现巨高 一开始怀疑就是路由器问题,因为是局域网,而…...
远程视频验证如何改变商业安全
如今,商业企业面临着无数的安全挑战。尽管企业的形态和规模各不相同——从餐厅、店面和办公楼到工业地产和购物中心——但诸如入室盗窃、盗窃、破坏和人身攻击等威胁让安全主管时刻保持警惕。 虽然传统的监控摄像头网络帮助组织扩大了其态势感知能力,但…...
CTO 实际上是做什么的?
https://vadimkravcenko.com/shorts/what-cto-does/ 有刪節 本文旨在为软件工程师解密CTO的角色,并为那些渴望担任这一职位的人提供路线图。 “他们是技术团队与公司其他部门之间的桥梁,确保技术支持并推动业务发展。” CTO的角色经常被误解。CTO有时是…...
【软考速通笔记】系统架构设计师④——系统工程基础知识
文章目录 一、前言二、系统工程方法2.1 霍尔的三维结构2.2 切克兰德法2.3 并行工程2.4 综合集成法 三、系统工程生命周期四、系统生命周期方法五、系统性能5.1 计算机的性能指标5.2 路由器的性能指标5.3 交换机的性能指标5.4 网络的性能资料5.5 操作系统的性能指标5.6 数据库的…...
2024赣ctf-web -wp
1.你到底多想要flag??? 首先来解决第一关: 先了解一下stripos(); 并且此函数处理数组返回false。而且pre_match同样遇见数组是返回false(解释一下正则 i:这是正则表达式的修饰符,代表“不区…...
Android Framework AudioFlinge 面试题及参考答案
目录 请解释什么是 AudioFlinger? AudioFlinger 在 Android 系统中的位置是什么? AudioFlinger 的主要职责有哪些? AudioFlinger 如何管理音频流? 在 AudioFlinger 中,什么是音频会话? 请简述 AudioFlinger 的工作流程。 AudioFlinger 是如何与硬件交互的? 在 A…...
英语知识在线平台:Spring Boot技术应用
2相关技术 2.1 MYSQL数据库 MySQL是一个真正的多用户、多线程SQL数据库服务器。 是基于SQL的客户/服务器模式的关系数据库管理系统,它的有点有有功能强大、使用简单、管理方便、安全可靠性高、运行速度快、多线程、跨平台性、完全网络化、稳定性等,非常…...
Qt5.14.2的安装与环境变量及一些依赖库的配置
目录 1.Qt5.14.2安装 2.Qt环境变量及一些依赖库的配置 1.Qt5.14.2安装 QT从入门到入土(一)——Qt5.14.2安装教程和VS2019环境配置 - 唯有自己强大 - 博客园 2.Qt环境变量及一些依赖库的配置 假设QT安装目录为: D:\Qt\Qt5.14.2 将目录: D:\Qt\Qt5.14.…...
2024年9月中国电子学会青少年软件编程(Python)等级考试试卷(六级)答案 + 解析
一、单选题 1、下面代码运行后出现的图像是?( ) import matplotlib.pyplot as plt import numpy as np x np.array([A, B, C, D]) y np.array([30, 25, 15, 35]) plt.bar(x, y) plt.show() A. B. C. D. 正确答案:A 答案…...
go编程中yaml的inline应用
下列代码,设计 Config 和 MyConfig 是为可扩展 Config,同时 Config 作为公共部分可保持变化。采用了匿名的内嵌结构体,但又不希望 yaml 结果多出一层。如果 MyConfig 中的 Config 没有使用“yaml:",inline"”修饰,则读取…...
Springboot自带注解@Scheduled实现定时任务
基于Scheduled注解实现简单定时任务 原理 Spring Boot 提供了Scheduled注解,通过在方法上添加此注解,可以方便地将方法配置为定时任务。在应用启动时,Spring 会自动扫描带有Scheduled注解的方法,并根据注解中的参数来确定任务的…...
VSCode【下载】【安装】【汉化】【配置C++环境(超快)】(Windows环境)
目录 一、VSCode 下载 & 安装 二、VSCode 汉化 三、VSCode C配置 配置环境变量 如何验证是否成功 接着在VSCode中配置编辑 一、VSCode 下载 & 安装 VSCode 下载 & 安装-CSDN博客https://blog.csdn.net/applelin2012/article/details/144009210Download Visual St…...
【八股文】小米
文章目录 一、vector 和 list 的区别?二、include 双引号和尖括号的区别?三、set 的底层数据结构?四、set 和 multiset 的区别?五、map 和 unordered_map 的区别?六、虚函数和纯虚函数的区别?七、extern C …...
ABAP OOALV模板
自用模板,可能存在问题 一、主程序 *&---------------------------------------------------------------------* *& Report ZVIA_OO_ALV *&---------------------------------------------------------------------* REPORT ZVIA_OO_ALV.INCLUDE ZVI…...
qt QDateTime详解
1. 概述 QDateTime 是 Qt 框架中用于处理日期和时间的类。它将 QDate 和 QTime 组合在一起,提供了日期时间的统一处理方案。QDateTime 可以精确到毫秒,并支持时区处理。 2. 重要方法 构造函数: QDateTime() 构造无效的日期时间 QDateTime(const QDa…...
鸿蒙安全控件之位置控件简介
位置控件使用直观且易懂的通用标识,让用户明确地知道这是一个获取位置信息的按钮。这满足了授权场景需要匹配用户真实意图的需求。只有当用户主观愿意,并且明确了解使用场景后点击位置控件,应用才会获得临时的授权,获取位置信息并…...
决策树分类算法【sklearn/决策树分裂指标/鸢尾花分类实战】
决策树分类算法 1. 什么是决策树?2. DecisionTreeClassifier的使用(sklearn)2.1 算例介绍2.2 构建决策树并实现可视化 3. 决策树分裂指标3.1 信息熵(ID3)3.2 信息增益3.3 基尼指数(CART) 4. 代码…...
【Android】RecyclerView回收复用机制
概述 RecyclerView 是 Android 中用于高效显示大量数据的视图组件,它是 ListView 的升级版本,支持更灵活的布局和功能。 我们创建一个RecyclerView的Adapter: public class MyRecyclerView extends RecyclerView.Adapter<MyRecyclerVie…...
自制Windows系统(十)
上图 (真的不是Windows破解版) 开源地址:仿Windows...
Linux——初识操作系统(Operator System)
前言:大佬写博客给别人看,菜鸟写博客给自己看,我是菜鸟。 一、冯偌伊曼体系 图一: 在初识操作系统之前,我们需要对计算机的硬件组成做一定的了解。本篇优先对数据信号做初步分析,暂时不考虑控制信号(操作系…...
RuoYi(若依)框架的介绍与基本使用(超详细分析)
**RuoYi(若依)**是一个基于Spring Boot和Spring Cloud的企业级快速开发平台。它集成了多种常用的技术栈和中间件,旨在帮助企业快速构建稳定、高效的应用系统。以下是关于RuoYi框架的详细介绍和基本使用教程,涵盖了从环境搭建到核心…...
js:基础
js是什么 JavaScript是一种运行在客户端的编程语言,实现人机交互的效果 js只要有个浏览器就能跑 js可以做网页特效、表单验证、数据交互、服务端编程 服务端编程是前端人拿他们特有的后端语言node.js来干后端干的事情 js怎么组成 JavaScriptECMAScript(语言基…...
easyui combobox 只能选择第一个问题解决
easyui combobox 只能选择第一个问题解决 问题现象 在拆分开票的时候,弹出框上面有一个下拉框用于选择需要新增的明细行,但是每次只能选择到第一个 选择第二条数据的时候默认选择到第一个了 代码如下 /*新增发票编辑窗口*/function addTicketDialog…...