C++ virtual member function FAQ

【1】  虚成员函数和非虚成员函数调用方式有什么不同?
    非虚成员函数是静态确定的。也就是说,该成员函数(在编译时)被静态地选择,该选择基于指向对象的指针(或引用)的类型。 相比而言,虚成员函数是动态确定的(在运行时)。也就是说,成员函数(在运行时)被动态地选择,该选择基于对象的类型,而不是指向该对象的指针/引用的类型。这被称作“动态绑定/动态联编”。大多数的编译器使用以下的一些的技术,也就是所谓的“VTABLE”机制:
     编译器发现一个类中有被声明为virtual的函数,就会为其搞一个虚函数表,也就是VTABLE。VTABLE实际上是一个函数指针的数组,每个虚函数占用这个数组的一个slot。一个类只有一个VTABLE,不管它有多少个实例。派生类有自己的VTABLE,但是派生类的VTABLE与基类的VTABLE有相同的函数排列顺序,同名的虚函数被放在两个数组的相同位置上。在创建类实例的时候,编译器还会在每个实例的内存布局中增加一个vptr字段,该字段指向本类的VTABLE。通过这些手段,编译器在看到一个虚函数调用的时候,就会将这个调用改写,在分发一个虚函数时,运行时系统跟随对象的 vptr找到类的 vtbl,然后跟随vtbl中适当的项找到方法的代码。
    以上技术的空间开销是存在的:每个对象一个额外的指针(仅仅对于需要动态绑定的对象),加上每个方法一个额外的指针(仅仅对于虚方法)。时间开销也是有的:和普通函数调用比较,虚函数调用需要两个额外的步骤(得到vptr的值,得到方法的地址)。由于编译器在编译时就通过指针类型解决了非虚函数的调用,所以这些开销不会发生在非虚函数上。
    下面代码演示了如何通过获取虚函数指针来调用虚函数的例子:
class Base
{
 int a;
public:
 virtual void fun1() {cout<<"Base::fun1()"<<endl;}
 virtual void fun2() {cout<<"Base::fun2()"<<endl;}
 virtual void fun3() {cout<<"Base::fun3()"<<endl;}
};

class A : public Base
{
 int a;
public:
 virtual void fun1() {cout<<"A::fun1()"<<endl;}
 virtual void fun2() {cout<<"A::fun2()"<<endl;}
};

void *getp (void* p)
{
 return (void*) *(unsigned long*)p;  // 获取对象内存中的虚函数表地址,即vptr指向的内容
}

fun getfun (Base* obj, unsigned long off)
{
 void *vptr = getp(obj);
 
 unsigned char *p = (unsigned char *)vptr;
 p += sizeof(void*)*off;  // 按字节数加上指针偏移,找到存储虚函数地址的内存位置
 
 return (fun)getp(p);     // 去虚函数表中当前位置的内容,即虚函数在内存中的地址
}

int main()
{
 Demo::A a;
 Demo::Base *p = &a;
 
 fun f = getfun(p, 0);
 (*f)();
 f = getfun(p, 1);
 (*f)();
 f = getfun(p, 2);
 (*f)(); 

 return 0;
}
注意:上面示例在vs2003下编译通过,其通过偏移获取虚函数表地址,进而获取虚函数地址,是基于vs下对象的内存布局是先虚函数表指针,然后成员变量的,也就是说指向虚函数表的指针被放置在对象内存的最前面。gcc下的情况有所不同,指向虚函数的指针是放在成员变量后面的。

【2】 析构函数也可以是虚的,甚至是纯虚的,但是构造函数不能是虚的
     纯虚的析构函数并没有什么作用,是虚的就够了。通常只有在希望将一个类变成抽象类(不能实例化的类),而这个类又没有合适的函数可以被纯虚化的时候,可以使用纯虚的析构函数来达到目的。构造函数不能是虚的(为什么?因为在一个构造函数调用期间,虚机制并不工作),但是你可以可能通过虚函数 virtual clone()(对于拷贝构造函数)或虚函数 virtual create()(对于默认构造函数),得到虚构造函数产生的效果。如下:
class Shape {
 public:
   virtual ~Shape() { }                 
// 虚析构函数
   virtual void draw() = 0;             
// 纯虚函数
   virtual void move() = 0;
   
// ...
   virtual Shape* clone()  const = 0;   
// 使用拷贝构造函数
   virtual Shape* create() const = 0;   
// 使用默认构造函数
 };
 
 class Circle : public Shape {
 public:
   Circle* clone()  const { return new Circle(*this); }
   Circle* create() const { return new Circle();      }
   
// ...
 };
    在 clone() 成员函数中,代码 new Circle(*this) 调用 Circle 的拷贝构造函数来复制this的状态到新创建的Circle对象。在 create()成员函数中,代码 new Circle() 调用Circle的默认构造函数。
用户将它们看作“虚构造函数”来使用它们:
 void userCode(Shape& s)
 {
   Shape* s2 = s.clone();
   Shape* s3 = s.create();
   
// ...
   delete s2;    
// 在此处,你可能需要虚析构函数
   delete s3;
 }
    这个函数将正确工作,而不管 Shape 是一个CircleSquare,或是其他种类的 Shape,甚至它们还并不存在。

【3】 构造函数和析构函数中的虚函数调用
    一个类的虚函数在它自己的构造函数和析构函数中被调用的时候,它们就变成普通函数了,不“虚”了。也就是说不能在构造函数和析构函数中让自己“多态”。例如:
class A
{
public:
    A() { foo();}        // 在这里,无论如何都是A::foo()被调用!
    ~A() { foo();}       // 同上
    virtual void foo();
};

class B: public A
{
public:
    virtual void foo();
};

void bar()
{
    A * a = new B;
    delete a;
}
    如果你希望delete a的时候,会导致B::foo()被调用,那么你就错了。同样,在new B的时候,A的构造函数被调用,但是在A的构造函数中,被调用的是A::foo()而不是B::foo()。为什么会有这样的规定呢,原因如下:
    当基类被构造时,对象还不是一个派生类的对象,所以如果 Base::Base()调用了虚函数 virt(),则 Base::virt() 将被调用,即使 Derived::virt()(派生类重写该虚函数)存在。
    同样,当基类被析构时,对象已经不再是一个派生类对象了,所以如果 Base::~Base()调用了virt(),则 Base::virt()得到控制权,而不是重写的 Derived::virt()
    当你可以想象到如果 Derived::virt() 涉及到派生类的某个成员对象将造成的灾难的时候,你很快就能看到这种方法的明智。详细来说,如果 Base::Base()调用了虚函数 virt(),这个规则使得 Base::virt()被调用。如果不按照这个规则,Derived::virt()将在派生对象的派生部分被构造之前被调用,此时属于派生对象的派生部分的某个成员对象还没有被构造,而 Derived::virt()却能够访问它。这将是灾难。

【4】私有private的虚函数是否具有多态性
    考虑下面的例子:
class A
{
public:
    void foo() { bar();}
private:
    virtual void bar() { ...}
};

class B: public A
{
private:
    virtual void bar() { ...}
};
    在这个例子中,虽然bar()在A类中是private的,但是仍然可以出现在派生类中,并仍然可以与public或者protected的虚函数一样产生多态的效果。并不会因为它是private的,就发生A::foo()不能访问B::bar()的情况,也不会发生B::bar()对A::bar()的override不起作用的情况。
    这种写法的语意是:A告诉B,你最好override我的bar()函数,但是你不要管它如何使用,也不要自己调用这个函数。

Copyright@戴维 2006.4  于北京

import random general_questions = [ "What is the capital of France?", "How do I install Python on Windows?", "What is machine learning?", "What does HTTP stand for?", "How can I create a virtual environment in Python?" ] cpp_questions = [ "Explain inheritance in C++.", "What is the difference between stack and heap memory?", "How do you declare a constant variable in C++?", "What is a virtual function in C++?", "What is the purpose of the 'new' operator in C++?" ] answers = { general_questions[0]: "The capital of France is Paris.", general_questions[1]: "To install Python on Windows, download the installer from python.org and run it. Make sure to check the box that says 'Add Python to PATH' during installation.", general_questions[2]: "Machine learning is a subset of artificial intelligence that involves training algorithms to make predictions or decisions based on data.", general_questions[3]: "HTTP stands for HyperText Transfer Protocol, which is used for transmitting web pages over the internet.", general_questions[4]: "You can create a virtual environment using the command 'python -m venv env_name' followed by activating it with 'source env_name/bin/activate' on Unix or 'env_name\\Scripts\\activate' on Windows.", cpp_questions[0]: "Inheritance in C++ allows a class (called a derived or child class) to inherit properties and methods from another class (known as the base or parent class)[^2].", cpp_questions[1]: "Stack memory is automatically managed by the system, while heap memory requires manual allocation and deallocation using functions like malloc() or new. Stack is faster but limited in size, whereas heap is larger but slower to access.", cpp_questions[2]: "You can use the const keyword before the data type when declaring a variable, for example: const int value = 10;", cpp_questions[3]: "A virtual function is a member function in a base class that is expected to be redefined in derived classes. It enables dynamic dispatch through pointers or references to the base class.", cpp_questions[4]: "The 'new' operator is used to dynamically allocate memory on the heap for an object or array and returns a pointer to the allocated memory." } with open("cplus_faq_5000_pairs.txt", "w") as f: for i in range(5000): q_type = random.choice(["general", "cpp"]) if q_type == "general": question = random.choice(general_questions) else: question = random.choice(cpp_questions) answer = answers[question] f.write(f"{question}|{answer}\n")转话为c++
最新发布
07-12
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值