1.继承
1.1.继承语法格式
要实现继承,遵循如下语法格式:
class SubClass : [public/protected/private] SuperClass
{
//...
};
其中SubClass
称为派生类或子类,SuperClass
称为基类或父类。
冒号之后的访问修饰符的作用如下:
- 1.
public
表示公有继承,基类的公有成员将成为派生类的共有成员,基类的私有部分也将成为派生类的一部分,但只能通过基类的公有方法和保护方法访问; - 2.
private
表示私有继承,基类的共有成员和保护成员将成为派生类的私有成员,这将意味着基类方法将不会成为派生类对象公有接口的一部分,但可以在派生类的成员函数中使用他们。 - 3.
protected
表示保护继承,它是私有继承的一个变体,基类的共有成员和保护成员将成为派生类的保护成员。
不管何种继承方式,基类的私有部分只能通过基类接口访问。
如果省略访问限定符,默认为private
继承。
如:
//student.h:
#include <string>
class Person
{
private:
int age;
std::string name;
public:
Person(int age = 0,std::string name="None");
~Person();
void show() const;
};
class Student : public Person
{
private:
double grade;
std::string schoolname;
public:
Student(int age = 0, std::string name = "None", double grade = 0.0, std::string schoolname = "None");
Student(const Person & person, double grade = 0.0, std::string schoolname = "None");
~Student();
void show() const;
void showSchoolName() const;//派生类添加方法
};
1.2.基类的构造函数能继承吗?
派生类可以继承到基类所有的公有部分和保护部分,但是构造函数除外。派生类不能继承基类的构造函数,同时,在创建派生类对象时,程序首先调用基类构造函数创建基类对象,再调用派生类构造函数。
正是由于这个原因,派生类的构造方法时,需要以成员初始化列表的方式将基类信息传递给基类构造函数,如我们定义Student
的构造函数时:
Student::Student(int age, std::string name, double grade, std::string schoolname) : Person(age,name)
{
std::cout << "Student::Student(int age, std::string name, double grade, std::string schoolname) call====" << std::endl;
this->schoolname = schoolname;
this->grade = grade;
}
如果省略成员初始化列表,那么当创建基类对象时,将使用默认基类构造函数,在这个构造函数中,可以省略成员初始列表,但在下一个构造函数中则不能省略了,如下:
Student::Student(const Person & person, double grade, std::string schoolname):Person(person)
{
std::cout << "Student::Student(const Person & person, double grade, std::string schoolname) call====" << std::endl;
this->schoolname = schoolname;
this->grade = grade;
}
在这个构造函数中,使用Person(person)
,因此将调用基类的拷贝构造函数,由于没有提供拷贝构造函数,因此编译器将提供默认的拷贝构造函数来完成赋值。
在类设计时,派生类构造函数主要用于初始化新增的数据成员,继承的数据成员由基类构造函数初始化。
1.3.类型兼容性原则
派生类和基类有如下5种特殊关系:
- 1.派生类对象可以使用基类的非私有方法;
- 2.基类指针可以直接指向派生类对象;
- 3.基类引用可以直接引用派生类对象;
- 4.基于3得,基类对象可以使用派生类对象进行初始化;
- 5.基于3得,派生类对象可以赋值给基类对象。
由于基类引用可以直接引用派生类对象,因此,当使用派生类对象初始化基类对象时:
Person p = s;
由于存在拷贝构造函数Person(const Person & p)
,从而可以得到第4点。
第5点也类似,由于存在隐式赋值运算符operator=(const Person &p )
,故可以使p引用派生类对象。
以上5点称为类型兼容性原则。
我们对上例中student.h
中声明的类进行实现,并通过这个例子来看看类型兼容性原则:
//student.cpp
#include <iostream>
#include "student.h"
Person::Person(int age, std::string name)
{
this->age = age;
this->name = name;
std::cout << "Person::Person(int age, std::string name) call====" << std::endl;
}
Person::~Person()
{
std::cout << "Person::~Person() call====" << std::endl;
}
void Person::show() const
{
std::cout << "Person Detail:" << std::endl;
std::cout << " name: " << name << std::endl;
std::cout << " age: " << age << std::endl;
}
Student::Student(int age, std::string name, double grade, std::string schoolname) : Person(age,name)
{
std::cout << "Student::Student(int age, std::string name, double grade, std::string schoolname) call====" << std::endl;
this->schoolname = schoolname;
this->grade = grade;
}
Student::Student(const Person & person, double grade, std::string schoolname):Person(person)
{
std::cout << "Student::Student(const Person & person, double grade, std::string schoolname) call====" << std::endl;
this->schoolname = schoolname;
this->grade = grade;
}
Student::~Student()
{
std::cout << "Student::~Student() call====" << std::endl;
}
void Student::show() const {
Person::show();
std::cout << " grade: " << grade << std::endl;
showSchoolName();
}
//派生类Student新添加的方法
void Student::showSchoolName() const {
std::cout << " School: " << schoolname << std::endl;
}
stu.cpp中程序入口:
#include <iostream>
#include "student.h"
void printDetail(const Person & p);
int main()
{
using namespace std;
cout << endl << "======1.Use Person &p call show():===========" << endl;
Person person(56,"zhangsan");
printDetail(person);
cout << endl << "======2.Use Student obj call show():===========" << endl;
Student stu(12,"小明",66,"牛蓝山小学");
stu.show();
Person *pperson = &stu;//基类指针指向子类对象
cout << endl << "======3.when Person &p = stu, call show():===========" << endl;
printDetail(stu);//形参为基类引用,基类引用引用子类对象
cout << endl << "======4.when Person person2 = stu2,call show():===========" << endl;
Student stu2(20,"小强", 77,"蓝翔");
Person person2 = stu2;//子类对象对基类u对象进行初始化
person2.show();
cout << endl << "Done." << endl;
return 0;
}
//形参为基类const引用
void printDetail(const Person & p) {
p.show();
}
编译并运行后,结果如下:
jiayongqiang@ubuntu:~$ g++ student.h student.cpp stu.cpp -o stu
@ubuntu:~$ ./stu
======1.Use Person &p call show():===========
Person::Person(int age, std::string name) call
Person Detail:
name: zhangsan
age: 56
======2.Use Student obj call show():===========
Person::Person(int age, std::string name) call
Student::Student(int age, std::string name, double grade, std::string schoolname) call
Person Detail:
name: 小明
age: 12
grade: 66
School: 牛蓝山小学
======3.when Person &p = stu, call show():===========
Person Detail:
name: 小明
age: 12
======4.when Person person2 = stu2,call show():===========
Person::Person(int age, std::string name) call
Student::Student(int age, std::string name, double grade, std::string schoolname) call
Person Detail:
name: 小强
age: 20
Done.
Person::~Person() call
Student::~Student() call
Person::~Person() call
Student::~Student() call
Person::~Person() call
Person::~Person() call
@ubuntu:
以上示例中,基类和派生类中都定义了show()
函数,并且在派生类中还新添加了一个showSchoolName()
函数。同时发现,当Student对象stu调用show()
时,将执行Student::show()
,但是将引用stu对象的基类引用调用该方法时,将执行基类的Person::show()
。这个原因将在多态中来解释。
1.4.其他细节
1.4.1.继承中构造函数和析构函数的调用规则
- 先调用父类的构造函数,再调用子类的构造函数;
- 先调用子类的析构函数,在调用父类的析构函数,和调用构造函数的步骤恰好相反;
1.4.2.继承和组合混搭情况下的调用规则
- 先构造父类,再构造组合的成员,最后构造自己;
- 先析构自己,再析构组合的成员,最后析构父类;
1.4.3.继承中同名成员变量处理方法
- 1.当子类中成员变量与父类成员变量重名时,子类依然继承同名成员,默认情况下使用的是子类的成员变量,如果要使用父类中的成员变量,使用域作用符
::
; - 2.重名成员存储在内存中不同的位置;
2.虚函数
面向对象的一大特性就是多态,是指由继承而产生的相关的不同的类,其对象对同一行为(方法)会做出不同的响应。具体地说,就是方法的行为具有多种形态,这取决于调用方法的对象。
实际上,由于私有继承和保护继承而言,在类外部无法访问基类的成员函数,因此,就不具有多态的特性。
C++中要实现多态,必须满足以下三个条件:
- 1.在派生类中重新实现基类的方法;
- 2.将基类中的方法定义为虚方法;
- 3.有基类指针或引用指向派生类对象。
在基类成员方法前,使用关键字virtual
修饰,则该方法将成为虚方法。
2.1.虚函数的作用
如果方法是通过引用或指针调用的,若没有使用关键字virtual
,则程序将根据引用类型或者指针类型选择方法;
如果使用了关键字virtual
,则程序将根据引用或者指针指向的对象的类型选择方法。
因此,在上例中当printDetail(stu)
时,由于未使用关键字virtual
,最终执行了父类的方法而不是子类的方法,这里对show()
方法进行改进:
class Person
{
//...
virtual ~Person();
virtual void show() const;//虚函数
};
class Student : public Person
{
public:
//...
~Student();
virtual void show() const;//虚函数
};
因此,如果基类中的方法需要在派生类中重写,则应将该方法在基类中声明为virtual
,一经声明,派生类中将自动成为虚方法,不过也最好在派生类中显式指出。
2.2.虚析构函数
应将基类析构函数声明为virtual
,这样可以确保在使用delete
关键字释放内存时,可以确保按照正确的顺序调用析构函数。
如果非虚析构函数,则将只调用对应指针或引用类型的析构函数,如果该指针指向的是派生类对象,则将不会调用派生类的析构函数。
2.3.虚函数的工作原理
在了解虚函数工作原理之前,需要知道什么是静态联编和动态联编。
2.3.1.静态联编
在C++中,由于出现了函数重载,因此编译器必须通过函数名和参数来确定使用的函数,这是在编译过程中进行的,所以这种方式称为静态联编。
2.3.2.动态联编
随着虚函数的出现,编译器将无法通过静态联编来确定使用的函数(是基类的呢?还是子类的),因此,编译器必须生成可以在程序运行时能够选择正确的虚函数的代码。这种方式称为动态联编。
因此,编译器对非虚函数,使用静态联编,而对于虚函数,则使用动态联编。
那么,对虚函数进行动态联编时,到底生成了什么标记代码呢?
编译器处理虚函数时,会在类中生成一个虚函数表(vtbl),vtbl
中存储了类声明的虚函数的地址;然后将给每个对象添加一个隐式成员——虚函数指针(vptr),vptr
指向虚函数表。
调用虚函数时,程序将通过vptr找到虚函数表,再从虚函数表中找到相应的函数地址。
2.4.虚函数带来的影响
使用虚函数将会带来如下一些影响:
- 1.存在虚函数时,对象将生成虚函数表,因此每个对象都将增大;
- 2.虚函数使用了动态联编,是在运行时根据vptr指针查找函数地址,因此效率上要低。
所以,在基类中,只能对派生类需要重新定义的函数声明为虚函数。
2.5.final
和override
关键字
在C++11中,可以将final
关键字和override
关键字用于类成员函数中。
override
用来说明派生类中的虚函数,如果在派生类中某个函数被override
标记,但该函数并非重写已存在的虚函数,那么编译将报错,如:
class A
{
virtual void show();
};
class B : public A
{
virtual void show() override;
void fw() override;//ERROR,基类中不存在fw()
};
final
用来说明该函数不能被派生类重写,如果派生类对一个final
标记的函数进行重写,编译器也将报错,如:
class A
{
virtual void show();
void f() final;//不允许派生类对其进行重写
};
class B : public A
{
virtual void show() override;
void f();//Error,f()被final标记
};
final
和override
出现在形参列表之后
3.抽象基类和纯虚函数
纯虚函数是只有声明没有定义的虚函数,由于没有定义,故函数结尾处为=0,如:
class FactoryABC
{
public:
virtual int getNum() = 0;
};
当类声明中包含纯虚函数时,该类将成为一个抽象类,因此不能创建该类的对象,只能用作基类。如下示例中使用了纯虚函数和抽象基类:
C++中,纯虚函数也可以在抽象类中进行定义,这是允许的
animal.h中类声明:
#include <string>
class Animal
{
private:
std::string type;
int color;
public:
Animal(std::string type,int color);
virtual ~Animal() {}
virtual void shout() const = 0;//由于纯虚函数的存在,因此Aniaml将变成抽象类
};
class Cat : public Animal
{
private:
int weight;
public:
Cat(int weight,std::string type,int color);
Cat(const Animal & ani,int weight);
virtual void shout() const;
~Cat(){}
};
class Dog : public Animal
{
public:
Dog(std::string type,int color);
~Dog() {}
virtual void shout() const;
};
animal.cpp中类的定义:
#include <iostream>
#include "animal.h"
Animal::Animal(std::string type,int color) {
this->type = type;
this->color = color;
}
Cat::Cat(int weight,std::string type,int color):Animal(type,color) {
this->weight = weight;
}
Cat::Cat(const Animal & ani,int weight):Animal(ani) {
this->weight = weight;
}
void Cat::shout() const {
std::cout << "喵喵..." << std::endl;
}
Dog::Dog(std::string type,int color) : Animal(type,color) {}
void Dog::shout() const {
std::cout << "汪汪..." << std::endl;
}
main()中:
#include <iostream>
#include "animal.h"
const int SIZE = 2;
int main()
{
enum {WHITE,BLACK,YELLOW};
//Animal animal("动物", WHITE);//ERROR,cannot declare variable ‘animal’ to be of abstract type ‘Animal’
Animal * ani1 = new Cat(12,"猫王",WHITE);
Animal * ani2 = new Dog("哈士奇",YELLOW);
Animal * animal[SIZE] = {ani1,ani2};
for(int i = 0; i < SIZE; i++) {
animal[i]->shout();
}
return 0;
}
抽象基类也叫做ABC(Abstract Base Class) 类,设计它的目的,是为了给其他类提供一个可以继承的适当的基类。抽象类不能被用于实例化对象,它只能作为接口使用。
4.多继承和虚基类
C++中允许多继承,如:
class A
{
public:
int a;
}
class B : public A
{
private:
int b;
};
class C : public A
{
private:
int c;
};
//D继承于B和C
class D : public B,public C
{
private:
int d;
};
然而,多继承可能会带来二义性,比如在D类中,使用来自于基类的变量a时,就会出现二义性。
因此,需要知道,多重继承会有二义性。
通常,如果一个派生类从多个基类继承,则这个公共基类会在派生类的对象中产生多个基类子对象,例如D中就存在2个基类子对象,分别位于B和C中。为了保证从多个相同基类的基类继承而来的派生类中只存在一个最终基类,引入了虚基类的概念。
4.1.虚基类
虚基类使得从多个类(它们的基类相同)派生出的对象只继承一个基类对象。在类声明时通过关键字virtual
将使得类变为虚基类,如:
class A
{
public:
int a;
}
class B : virtual public A
{
private:
int b;
};
class C : virtual public A
{
private:
int c;
};
//D继承于B和C
class D : public B,public C
{
private:
int d;
};
此时,派生类D中将只有一个基类A的子对象。
总结
- 1.如果一个派生类从多个基类继承,而这些基类又有共同的基类,则在对该基类中声明的变量进行访问时,可能产生二义性;
- 2.如果一个派生类从多个基类继承,则这个公共基类会在派生类的对象中产生多个基类子对象;
- 3.要使这个公共基类在派生类中只产生一个子对象,必须对这个基类声明为虚继承,使这个基类成为虚基类,使用
virtual
关键字。