C/C++
1.有虚函数的类有个virtual table(虚函数表),里面包含了类的所有虚函数,存放的是每个虚函数的函数入口地址,类中有个virtual table pointers,通常称为vptr,指向这个virtual table。虚函数表不占用类对象的存储空间,但是虚函数表指针占用类对象的存储空间。
虚函数存放在代码段,虚函数表在Linux/Unix中存放在可执行文件的只读数据段中(rodata),微软的编译器将虚函数表存放在常量段。
如果类的对象的内存是动态申请的,则该对象的所有成员均在堆区,相应的,该对象对应的vptr也在堆区。
查找具体的虚函数的地址:
在虚函数表中,虚函数按照声明的顺序被依次存放。虚函数表中存放的是函数指针。
找到虚函数表之后,根据虚函数的声明顺序便可找到对应的函数地址。
2.<<Effective C++>> 条款34
纯虚函数也可以有定义,可以提供给派生类的非纯虚函数(派生类继承的虚函数仍然是虚函数)一个缺省的实现方式。(link)
B* b = new D1;
b->B::f(); //不是b- >f();
派生类可以不重写基类的纯虚函数,但是此时派生类不能实例化,不能创建对象。
3.虚函数可以为私有函数。
若基类的虚函数属性为public,派生类重写的虚函数属性为private,则通过基类指针可以调用派生类的虚函数,但是派生类指针不能调用派生类的虚函数。
#include<iostream>
class Derived;
class Base {
public:
virtual void fun() {
std::cout << "Base Fun"; }
};
class Derived : public Base {
private:
void fun() {
std::cout << "Derived Fun"; }
};
int main()
{
Base* ptr = new Derived;
ptr->fun();
return 0;
}
若基类的虚函数属性为private,派生类重写的虚函数属性为public,则通过基类指针不可以调用派生类的虚函数,但是若将main()函数声明为友元函数,则可以进行上述调用。
友元函数不受访问权限的控制,可以访问所有成员。若要限制友元函数权限,可以在类中定义一个子类,将函数声明为子类的友元函数。
#include<iostream>
class Derived;
class Base {
private:
virtual void fun() {
std::cout << "Base Fun"; }
friend int main();
};
class Derived : public Base {
public:
void fun() {
std::cout << "Derived Fun"; }
};
int main()
{
Base* ptr = new Derived;
ptr->fun();
return 0;
}
4.静态函数不可以是虚函数(虚函数的调用过程)(link)。
静态成员函数不能声明为const函数。
#include<iostream>
class Test
{
public:
// 编译错误: static成员函数不能为const
static void fun() const {
}
// 如果声明为下面这样,是可以的。
const static void fun() {
}
或类似于
const static int fun() {
return 0; }
};
5.虚函数不一定会表现出多态性,但虚函数表现多态性的时候不能被内联。例如实际对象调用的虚函数可以被内联展开,而基类指针或引用调用的虚函数会表现出多态性,因此不能被内联。
6.建立在整个类中都恒定的常量,对于每个对象来说,该常量是相同的。可以使用枚举常量或const static常量。
枚举常量不会占用对象的存储空间,在编译期间,枚举变量被全部求值。
7.虚继承:为了解决菱形继承中的命名冲突问题。
C++ 规定必须由最终的派生类 D 来初始化虚基类 A,直接派生类 B 和 C 对 A 的构造函数的调用是无效的。
虚继承时构造函数的执行顺序与普通继承时不同:在最终派生类的构造函数调用列表中,不管各个构造函数出现的顺序如何,编译器总是先调用虚基类的构造函数,再按照出现的顺序调用其他的构造函数;而对于普通继承,就是按照构造函数出现的顺序依次调用的。
D::D(int a, int b, int c, int d): B(90, b), C(100, c), A(a), m_d(d){
}
虽然将 A() 放在了最后,但是编译器仍然会先调用 A(),然后再调用 B()、C(),因为 A() 是虚基类的构造函数,比其他构造函数优先级高。如果没有使用虚继承的话,那么编译器将按照出现的顺序依次调用 B()、C()、A() (即依次调用各个构造函数)。
8.析构函数不能、也不应该抛出异常。构造函数可以抛出异常。
1)如果析构函数抛出异常,则异常点之后的程序不会执行,如果析构函数在异常点之后执行了某些必要的动作比如释放某些资源,则这些动作不会执行,会造成诸如资源泄漏等问题。
2)通常异常发生时,c++的机制会调用已经构造对象的析构函数来释放资源,此时若析构函数本身也抛出异常,则前一个异常尚未处理,又有新的异常,可能会造成程序崩溃。
那么当无法保证在析构函数中不发生异常时, 该怎么办?
把异常完全封装在析构函数内部,决不让异常抛出函数之外。这是一种非常简单,也非常有效的方法。
~ClassName()
{
try
{
do_something();
}
catch()
{
//这里可以什么都不做,只是保证catch块的程序抛出的异常不会被扔出析构函数之外。
}
}
当构造函数、析构函数为公共成员函数时,可以被显式调用。
构造函数与析构函数没有返回值,没有返回类型。
A a = A();
a.~A();
9.const对象只能调用const成员函数、不能调用非const成员函数;
非const对象可以调用const成员函数。(link)
10.模板成员函数不可以是虚函数。(link)
模板类可以有虚函数。
11.C++类内可以定义引用数据成员.。
但是必须在构造函数的参数初始化列表中进行初始化,不能在构造函数的函数体内进行初始化。
因为初始化列表是初始化,而函数体内是赋值操作,对于普通的数据类型两种操作只有资源消耗的区别。但引用和const常量都是不能被赋值的,它们在类内只能在构造函数的参数初始化列表中被初始化。
12.初始化和赋值
初始化对象,就是初始化对象的数据成员,以及虚函数表指针。
赋值时,仅是对数据成员进行赋值。
赋值是在两个已经存在的对象间进行的;
初始化是要创建一个新的对象。
编译器会区别这两种情况,赋值的时候调用重载的赋值运算符,初始化的时候调用构造函数。如果类中没有构造函数,则编译器会提供一个默认的。默认的拷贝构造函数只是简单地复制类中的每个成员。
对于基本数据类型,二者差异不大。但是对于自定义类型,二者差异较大。
1)编译器首先调用A的默认构造函数构造a,而后调用A的赋值运算符函数将 t 的数据成员的值赋值给 a.
2)编译器直接调用拷贝构造函数,使用 t 的值来初始化 a.
class A
{
public:
A() {
cout << "Construct A" << endl ;} //默认构造函数
A(int p):a(p){
}//非默认构造函数
A(const A& t)