1.基类通常都应该定义一个虚析构函数,即使该函数不执行任何实际操作也是如此。
2.任何构造函数之外的非静态函数都可以是虚函数,关键字virtual只能出现在类内部的声明语句之前而不能用于类外部的函数定义。如果基类把一个函数声明成虚函数,则该函数在派生类中隐式地也是虚函数。成员函数如果没有被声明成虚函数,则其解析过程发生在编译时而非运行时。
3.继承的访问说明符的作用是控制派生类从基类继承而来的成员是否对派生类用户可见。
4.在一个对象中,继承自基类的部分和派生类自定义的部分不一定是连续存储的。C++标准并没有明确规定派生类的对象在内存中如何分布。
5.在派生类对象中含有与其基类对应的组成部分,这一事实是继承的关键所在。所以我们能把派生类的对象当成基类对象来使用,和其他类型一样,编译器会隐式的执行派生类到基类的转换。
6.每个类控制它自己的成员初始化过程。尽管在派生类对象中含有从基类继承而来的成员,但是派生类并不能直接初始化这些成员,而必须使用基类的构造函数来初始化它的基类部分。
7.派生类的初始化顺序是首先初始化基类的部分,然后按照声明的顺序依次初始化派生类的成员。
8.如果基类定义了一个静态成员,则在整个继承体系中只存在该成员的唯一定义,而且静态成员遵循通用的访问控制规则。
9.派生类的声明中不能包含它的派生列表。
class testex; // 正确 class testex : public test; // 错误
10.如果我们想将某个类用作基类,则该类必须已经定义而非仅仅声明。因为派生类中包含并且可以使用它从基类继承而来的成员。派生类必须要知道它们是什么。此规定还有一层隐含的意思,即一个类不能派生它本身。
11.直接基类出现在派生列表中,而间接基类由派生类通过其直接基类继承而来。最终的派生类将包含它的直接基类的子对象以及每个间接基类的子对象。
12.C++11新标准提供了一种防止继承发生的方法,即在类名后面跟一个关键字 final 。
13.和内置指针一样,智能指针类也支持派生类向基类的类型转换。
14.表达式的静态类型在编译时总是已知的,它是变量声明时的类型或表达式生成的类型,动态类型则是变量或表达式表示的内存中的对象的类型,直到运行时才可知。如果表达式既不是引用也不是指针,则它的动态类型永远与静态类型一致。
15.当我们用一个派生类对象为一个基类对象初始化或赋值时,只有该派生类对象中的基类部分会被拷贝,移动或赋值,它的派生类部分将被忽略掉。
16.通常情况下如果我们不使用某个函数,则无须为该函数提供定义,但是我们必须为每个虚函数提供定义,因为连编译器也无法确定到底会使用哪个虚函数。
17.OOP的核心思想是多态性,我们把具有继承关系的多个类型称为多态类型,因为我们能使用这些类型的“多种形式”而无须在意它们的差异。
18.当派生类覆盖了某个虚函数时,该该函数在基类中的形参必须与派生类的严格匹配,返回类型也必须匹配,但是有个例外,当类的虚函数返回类型是类本身的指针或引用时,返回类型可以不完全一样。
19.C++11新标准中可以使用override关键字来说明派生类中的虚函数,如果我们使用了这个关键字,但是该函数并没有覆盖已存在的虚函数,此时编译器会报错。我们还可以把某个函数指定为final,如果我们已经把函数定义成了final了,则之后任何尝试覆盖该函数的操作都将引发错误。
class test { public: virtual int add(int a, int b) { return a + b; } int sub(int a, int b) { return a - b; }; virtual int div(int a, int b) final { return a / b; } }; class testex : public test { public: virtual int add(double a, int b) { return a + b; } // 正确 virtual int add(int a, double b) override { return a + b; } // 错误 int sub(int a, int b) override { return a - b; }; // 错误,sub不是虚函数不可以覆盖 int div(int a, int b) { return (a + 1) / b; } // 错误,final函数不可以覆盖 };
20.基类和派生类的虚函数的默认实参可以不相同,该实参由本次调用的静态类型决定,但是最好定义成一致的。
class test { public: virtual int add(int a, int b = 1) { return a + b; } }; class testex : public test { public: virtual int add(int a, int b = 2) override { return a + b; } };
21.如果一个派生类的虚函数需要调用它的基类版本,但是没有使用作用域运算符,则会导致无限递归。
class test { public: virtual int add(int a, int b) { return a + b; } }; class testex : public test { public: virtual int add(int a, int b) override { int tmp = add(a, b); // 错误,会无线递归调用testex::add int tmp = test::add(a, b); // 正确 return tmp + 1; } };
22. =0可以将一个虚函数说明为纯虚函数,但是只能出现在类内部的声明语句处。和普通的虚函数不一样,纯虚函数无须定义。但是我们可以在类的外部定义纯虚函数(即使被定义了,如果派生类不希望是纯虚函数,则仍然需要覆盖这个函数)。含有(或未经覆盖直接继承)纯虚函数的类是抽象基类,我们不能(直接)创建一个抽象基类的对象(实际上它的派生类在构造时还是会构造一个抽象基类的对象)。
class test { public: test(int i ):m_id(i) {} virtual int add(int a, int b) = 0; int m_id; }; int test::add(int a, int b) { return a + b; } class testex : public test { public: testex(int i) :test(i) {} // 构造testex时也构造了test }; testex t(1); // 错误,testex是纯虚函数
23.派生类的成员或友元只能通过派生类对象来访问基类的受保护成员,派生类对于一个基类对象中的受保护成员没有任何访问特权。
class test { public: test() :m_count(0) {} protected: int m_count; }; class testex : public test { public: testex() :test() {} int add(test t) { //++t.m_count; // 错误 ++test::m_count; // 正确 return ++m_count; // 正确 } };
24. 派生访问说明符对于派生类的成员(及友元)能否访问其直接基类的成员没有什么影响,目的是控制派生类用户对于基类成员的访问权限。对基类成员的访问权限只与基类中的访问说明符有关。 派生访问说明符还可以控制继承自派生类的新类的访问权限。
25.派生类向基类的转换是否可访问由使用该转换的代码决定,同时派生类的派生访问说明符也会有影响,假设B继承自A:
- 只有当B公有地继承A时,用户代码才能使用派生类向基类的转换。
- 不论B以什么方式继承A,B的成员函数和友元都能使用派生类向基类转换。
- 如果B继承A的方式是共有的或者受保护的,则D的派生类的成员和友元可以使用B向A的类型转换。
26.就像友元关系不能传递一样,友元关系同样也不能继承。
class test { friend class test_friend; private: int m_count; }; class testex : public test { private: int m_id; }; class test_friend { public: int count(test t) { return t.m_count; } // 正确 int id(testex t) { return t.m_id; } // 错误,友元不能继承 int testex_count(testex t) { return t.m_count; } // 正确 };
27.有时候我们需要改变派生类继承的某个成员的访问级别,通过使用using声明,派生类只能为那些它可以访问的名字提供using声明。
class test { public: int m_public_count; protected: int m_protected_count; }; class testex : private test { public: using test::m_public_count; protected: using test::m_protected_count; int get_count() { return m_protected_count; } // 正确 }; testex t; t.m_public_count; // 正确
28.默认情况下class关键字定义的派生类是私有继承,而struct关键字定义的派生类是公有继承的。除了这个和默认成员访问说明符,struct和class再无其它任何不同之处。
29.如果一个名字在派生类的作用域内无法正确解析,则编译器将继续在外层的基类作用域中寻找该名字的定义。
30.派生类的成员将隐藏同名的基类成员。我们可以用作用域运算符来使用被隐藏的基类成员。
31.定义在派生类中的函数不会重载其基类中的成员,如果成员同名,则派生类将在其作用域内隐藏该基类成员,即使这个成员的形参列表不一致。
class test { public: int add(int a, int b) { return a + b; } }; class testex : public test { public: int add(int a) { return a + 1; } }; testex tex; tex.add(1, 2); // 错误,test::add(int,int)被隐藏了 tex.test::add(1, 2); // 正确
32.和其他函数一样,成员函数无论是否是虚函数都能被重载,如果派生类希望所有的重载版本对于它来说都是可见的,那么它就需要覆盖所有的版本,或者一个也不覆盖。为了避免这个限制,我们可以使用using声明。
class test { public: int add(int a) { return a + 1; } int add(int a, int b) { return a + b; } }; class testex : public test { public: using test::add; int add(int a) { return a + 2; } }; testex tex; tex.add(1, 2); // 如果没有using语句就会报错 tex.add(2); // 正确
33.如果基类的析构函数不是虚函数,则delete一个指向派生类对象的基类指针将产生未定义的行为。
34.如果一个类定义了析构函数,即使它通过=default的形式使用了合成的版本,编译器也不会为这个类合成移动操作。
35.对于派生类的析构函数来说,它除了销毁派生类自己的成员外,还负责销毁派生类的直接基类,该直接基类又销毁它自己的直接基类,以此类推直。
36.某些定义基类的方式可能导致有的派生类成为被删除的函数:
- 如果基类中的默认构造函数,拷贝构造函数,拷贝赋值运算符或析构函数是被删除的或不可访问的,则派生类中对应的成员将是被删除的。
- 如果在基类中有一个不可访问或删除掉的析构函数,则派生类中合成的默认和拷贝构造函数将是被删除的,因为编译器无法销毁派生类对象的基类部分。
- 如果基类中的移动操作是删除的或不可访问的,那么派生类中该函数将是被删除的。同样,如果基类的析构函数是删除的或不可访问的,则派生类的移动构造函数也将是被删除的。
37.当派生类定义了拷贝或移动操作时,该操作负责拷贝或移动包括基类部分成员在内的整个对象。默认情况下,基类的默认构造函数初始化派生类对象的基类部分,如果我们想拷贝或移动基类部分必须显示地使用基类的拷贝(或移动)构造函数。
class test { public: int m_count; }; class testex : public test { public: testex() {} testex(const testex& tex) :test(tex){} }; testex tex; tex.m_count = 1; testex tex1(tex); // 此时tex1的m_count=1,如果没有显示调用则m_count是未初始化的
38.派生类的析构函数首先执行,然后是基类的析构函数,派生类的析构函数只负责销毁由派生类自己分配的资源。
39.在构造函数和析构函数中调用了某个虚函数,则调用的虚函数是这个构造函数或者析构函数所属类型对应的虚函数版本。
class test { public: test() { m_count = get_default_count(); } virtual int get_default_count() { return 1; } int m_count; }; class testex : public test { public: testex():test() {} virtual int get_default_count() { return m_default_count; } private: int m_default_count; }; test *t = new testex(); // 理论上构造test的时候应该调用testex的虚函数, // 但是如果这么做,会用到testex的m_default_count,而此时m_default_count还没有构造
40.一个类只能继承其直接基类的构造函数,类不能继承默认,拷贝和移动构造函数。派生类继承基类构造函数的方式是提供一条注明了(直接)基类名的using声明语句。通常情况下,using声明语句只是令某个名字在当前作用域可见,而当作用于构造函数时,using语句将令编译器产生代码,对于基类的每个构造函数,编译器都生成一个与之对应的派生类构造函数。换句话说,对于基类的每个构造函数,编译器都在派生类中生成一个形参列表完全相同的构造函数。
class test { public: test(int count) { m_count = count; } int m_count; }; class testex : public test { public: using test::test; // 等价于下面的构造函数 // testex(int count) :test(count) {} private: int m_id; }; testex tex(1); // m_id默认初始化
41.和普通成员的using不一样,一个构造函数的using声明不会改变该构造函数的访问级别。如果基类构造函数是explicit或者constexpr,则继承的构造函数也拥有相同的属性。
42.当一个基类构造函数含有默认实参时,这些实参并不会被继承,相反,派生类将获得多个继承的构造函数。
class test { public: test(int id, int count = 0) { m_id = id; m_count = count; } int m_id; int m_count; }; class testex : public test { public: using test::test; // 等价于下面的两个构造函数 // testex(int id) :test(id) {} // testex(int id, int count) :test(id, count) {} }; testex tex(1); // m_id=1 m_count=0 testex tex1(2, 1); // m_id=2 m_count=1
43.如果基类含有几个构造函数,则除了两个例外请况,大多数的时候派生类会继承所有这些构造函数。第一个例外是派生类定义了具有相同参数列表的自己的版本,第二个例外是默认,拷贝和移动构造函数不会被继承。继承的构造函数不会被作为用户定义的构造函数来使用,因此如果一个类只含有继承的构造函数,则它也将拥有一个合成的默认构造函数。
44.当派生类对象被赋值给基类对象时,其中的派生类部分将被“切掉”,因此容器中存在继承关系的类型无法兼容。解决这个问题的方法就是在容器中存放基类指针(最好是智能指针)。