一、继承与构造
1、继承(inheritance)
所谓继承是在一个基类或父类基础上形成的子类或派生类。
如上图中在bike中延伸出Tandem Bike、racing bike和mountain bike这三种子类或派生类,即子继承父;而泛华则是父泛华子。
当要避免一个类被继承时,c++11引入final特殊标识符,可以使得类不能被继承。
class B{};//B可以被继承;
class C : public B {};//C继承类B;
class D final {};//类D不能被继承。
2、c++11中继承中的构造函数
并不是类中所有的成员函数都可以被继承,c++11中规定析构函数和友元函数是不能够被继承的。
当要继承基类中的构造函数时,可以使用using A::A,此时继承类A中所有的ctor,此时不能仅继承指定的某个基类ctor。
//调用继承的构造函数
class A{
public:
A(int i){};
A(double d, int i){};
};
class B : A{
using A::A;//继承基类A中的所有构造函数;
int d{0};
};
int main(){
B b(1);//调用A的ctor,A(int i);
}
当派生类成员也需要初始化,则可以在派生类ctor中调用基类构造函数;
class A{
public:
A(int i){}
A(double d, int i){}
};
class B : A{
using A::A;//此时不继承A(int i);
int d{0};
B(int i) : A{i}, d{i}{}
};
int main(){
B b(1);//调用B(int i);
return 1;
}
3、继承中的默认构造函数
若基类中的ctor未被显式调用,则基类的默认构造函数就会被调用。
//基类A
class A{
public:
A() = defalt;//默认构造函数;
A(int i){}
};
class B :A{
public:
B() {}//等价于B() : A{} {}
B(int i){}//等价于B(int i) : A{} {}
};
因此要考虑给基类提供默认构造函数。
4、构造链和析构链
构造链是构造函数链,构造类实例会沿着继承链调用所有的基类ctor,调用次序:base first,derive next(父先子后);
析构链是析构函数链,与构造链的顺序刚好相反,是子先父后。
//基类
class A{
public:
A(){
std::cout << "A()" << std::endl;
}
~A(){std::cout << "~A()" << std::endl;}
};
//派生类
class B : A{
public:
B(){std::cout << "B()" << std::endl;}
~B(){std::cout << "~B()" << std::endl;}
};
//测试用例
int main(){
{
B b;//创建对象b
}
return 1;
}
output:
A()
B()
~B()
~A()
二、名字隐藏与重定义
1、继承中的名字隐藏
class P{
public:
void f(){}
};
class C : public P{
public:
void f(int x){}
};
int main(){
C c;
c.f();
return 1;
}
上述代码在编译时会提示找不到f()函数,这是因为派生类视作内部作用域,基类视作外部作用域,而内部作用域的名字会隐藏外部作用域的同名名字。解决办法如下所示:
class P{
public:
void f(){}
};
class C : public P{
public:
using P ::f;//用using声明基类中的成员函数;
void f(int x){}
};
int main(){
C c;
c.f();
return 1;
}
2、重定义函数(Redefining functions)
//基类
class A
{
public:
string toString()
{
return "A";
}
};
//继承类
class B : public A
{
public:
string toString()
{
return "B";
}
void g()
{
std::cout << toString() << std::endl;//仅能调用类B中的toString函数;
}
};
int main()
{
B b;
std::cout << b.toString() << std::endl;//调用类B中的toString()函数;
b.A::toString();//调用A类中的toString()函数;
return 0;
}
在上述例子中,派生类B中声明了一个与基类A的toString()同名函数,在派生类中声明了一个与基类成员同名的新成员即为重定义。
如果仅在类A中声明toString()函数,类B中没有toString()函数时,当类B继承类A后,B对象可以访问基类A中的toString()函数,但此时的toString()函数只能输出基类的对象信息,无法输出继承类B的对象信息,但当在继承类B中也重定义toString()函数后,B对象b调用的toString()函数就可以输出类B的信息。
重定义和重载有区别,重载函数特点:a、函数名相同;b、至少一个特征不同(参数类型、参数数量或参数顺序不同)。
低于重定义函数,函数特征相同(同名、同参数和相同的返回类型),唯一区别在于定义的位置不同(在基类和继承类中分别定义)。
三、覆写与运行时多态
1、多态(polymorphism)的概念
广义的多态指不同类型的实体/对象对于同一消息有不同的响应。
目前多态性有两种表现的方式:重载多态和子类型多态。
//重载多态
class A
{
public:
int f(int x);
int f();
};
//子类型多态
class B
{
virtual int f()
{
return 1;
}
};
class C : public B
{
virtual int f()
{
return 0;
}
};
A a;
B b;
A* P = &b;
a.f();//调用A::f();
b.f();//调用B::f();
p->f();//调用B::f();
对于重载和重定义函数在程序运行时的不同在于联编不同,重载函数属于静态联编,是在程序编译时确定调用哪个函数;子类型多态属于动态联编,即在程序运行时才能确定调用哪个函数。动态联编实现的多态也成为运行时多态。
2、实现运行时多态
实现运行时多态有两个要素:a、虚函数(virtual function);b、覆写(override)(覆写即为在派生类中重定义一个虚函数)。
为何要使用运行时多态?
//利用重载函数(静态联编),不利用运行时多态;
class A
{
public:
string toString()
{
return "A";
}
};
class B : public A
{
public:
string toString()
{
return "B";
}
};
class C : public A
{
public:
string toString()
{
return "C";
}
};
void print(A* p)
{
std::cout << p->toString() << std::endl;
}
//当如果要打印类B和类C时,需重载print函数
void print(B* p)
{
std::cout << p->toString() << std::endl;
}
void print(C* p)
{
std::cout << p->toString() << std::endl;
}
int main()
{
A a{};
B b{};
C c{};
print(&a);//调用A::toString();
print(&b);//调用B::toString();
print(&c);//调用C::toString();
return 0;
}
当使用运行时多态时,上述例子如下所示:
//利用运行时多态;
class A
{
public:
virtual string toString()//将基类中同名函数声明为virtual
{
return "A";
}
};
class B : public A
{
public:
string toString()//此处virtual可以省略;
{
return "B";
}
};
class C : public A
{
public:
string toString()//此处virtual可以省略;
{
return "C";
}
};
void print(A* p)
{
std::cout << p->toString() << std::endl;
}
//当如果要打印类B和类C时,不需重载print函数
int main()
{
A a{};
B b{};
C c{};
print(&a);//调用A::toString();
print(&b);//调用B::toString();
print(&c);//调用C::toString();
return 0;
}
上述应用静态联编和动态联编的例子中,可以看到虚函数的传递性:当基类中定义了虚同名函数,那么派生类中的同名函数自动变为虚函数。
其实当使用动态联编时,类中保存了一个virtual函数表,此时程序运行时比非虚函数开销大,代码编译效率会有所降低。