目录
一、不可被继承的类
实现不可被继承的类有两种方法:一种是c++98的设置私有的父类构造函数;另一种是c++11引入的final关键字。
1、私有父类构造函数:
//父类
class base final
{
public:
base()
{
cout << "父类的共有构造" << endl;
}
private:
};
//子类
class derive : public base
{
public:
private:
};
下面来分析
- 阻止被继承可行的原因:子类在初始化时必须先调用父类的构造函数,如果父类的构造函数是私有,那子类就无权调用父类构造,导致无法继承。
- 已经被淘汰的原因:1、这种方法不够明确,毕竟父类的构造函数是否私有在涉及子类时不够透明,2、如果没有实例化子类对象,即便强行继承父类,编译器甚至不会报错!!!
2、关键字final:
对于不期望被继承的类,只用在类名后面加上关键字 final就好。
//父类
class base final
{
public:
base()
{
cout << "父类的共有构造" << endl;
}
private:
};
//子类
class derive : public base
{
public:
private:
};
二、特殊的继承内容:
1、不可被继承的友元:
父类的友元函数无法被子类继承。如果期望子类也使用同样的友元函数,就必须在子类里进行声明。
//类的前置声明
class Student;
//函数的前置声明
void printInfo(Student st);
//父类Person
class Person
{
public:
friend void printInfo(Student st);
Person(string name = "张三")
:_name(name)
{
}
protected:
string _name;
};
//子类Student
class Student : public Person
{
public:
//friend void printInfo(Student st); //暂时注释掉
Student(Person pe = Person(), string id = "112233")
:Person(pe)
,_id(id)
{
}
private:
string _id;
};
//一个函数,接受Student类型的参数
void printInfo(Student st)
{
cout << st._name << " " << st._id << endl;
}
-------------------------------------------------
//源文件里的调用语句
Person pe("普通人");
printInfo(pe);
Student st(Person("林某人"), "147896325");
printInfo(st);
以上的代码主要有Person和Student两个类,以及一个prinfInfo函数。
- Student共有继承自Person
- PrintInfo函数可以接受Student类的参数,也可以通过复制兼容转换来接受父类Person的参数
- Person类里存在函数printInfo的友元声明,Student类里则没有(上面代码里给注释掉了)
执行调用语句,会发现报错:
当然,只要吧Student类里的关于友元声明的注释去掉,让PrintInfo成为Student类的友元函数,就万事大吉了。
2、静态成员的继承:
静态成员可以被继承,但是只会存在一份。
//父类
class base
{
public:
base()
{
}
static int static_val; //静态成员变量
private:
};
int base::static_val = 0;
//子类
class derive : public base
{
public:
private:
};
----------------------------------------------------------------------------
//调用语句
base obj1;
derive obj2;
cout <<"pobj1" << & base::static_val << endl;
cout << "pobj2" << &derive::static_val << endl;
很容易看出:
- 子类并没有声明这个静态变量,但仍然可以正常打印,说明静态成员得到的继承。
- 父子类对象分别调用这个静态变量,得到的地址值相同,说明的确只存储了一份。
三、杂糅的继承关系:
在此之前,我们所探讨的都是单继承,即一个子类继承一个父类。其实还存在多继承的概念,而多继承在进一步满足现实事物之间关系的同时,又连带着引出了菱形继承这一问题。为了解决这个问题,又引入了virtual关键字。
1、单继承:
单继承不用做过多探讨,就是此前我们一直用到的方式。
2、多继承:
多继承指的是多个类继承同一个类,比如老师和学生就可以继承自人这一个类。
//Person为父类
class Person
{
public:
Person(string name)
:_name(name)
{
}
protected:
string _name;
};
//Student继承自Person类
class Student : public Person
{
public:
Student(string name = "")
:Person(name)
{ }
void StuInfo()
{
cout << _name << " " << st_id << endl;
}
private:
int st_id = 111;
};
//Teacher同样继承自Person类
class Teacher : public Person
{
public:
Teacher(string name = "")
:Person(name)
{
}
void TeaInfo()
{
cout << _name << " " << te_id << endl;
}
private:
int te_id = 666;
};
3、多继承引出的问题:菱形继承
除了两个类继承自同一个类叫做多继承,一个类继承自两个类也是多继承。比如向上面的情况:老师和学生继承自人,即老师和学生都是人
但是,助教可以同时是老师和学生,因此助教类可以同时又继承老师和学生。很合理,但引出了菱形继承的麻烦。
//Assistant类继承自Student和Teacher
class Assistant : public Teacher, Student
{
public:
private:
int as_id = 999;
};
害处一:数据冗余,浪费空间
由于最终的子类Assistant继承了两份父类的成员,因此浪费了内存空间
害处二: 产生二义性,调用不明确
最终的子类D,继承了两份同名的父类的成员内容,一份来自B类,另一份来自C类。在调用时就会产生歧义
class A
{
public:
int test_val; //会被一路继承下去的基类成员变量
void test_func() //会被一路继承下去的基类成员函数
{
}
};
class B : public A
{
};
class C : public A
{
};
class D : public B, C
{
};
---------------------------------------------------------------
//调用
D d;
d.test_val;
d.test_func();
D d;
//限定类域后就可以正常访问了,虽然看起来真的很挫,要知道无论是B还是C域里公共的那部分值是相同的。
d.B::test_func();
d.B::test_val;
d.C::test_func();
d.C::test_val;
这个问题倒还好,起码可以通过限定类域来解决,但是数据冗余依然是有待解决的问题。
4、解决菱形继承:
应对方案 1 : 不写菱形继承
这个就不用多说了,既然菱形继承不好,那就避开设计出菱形继承结构的情况。
方案2:使用关键字virtual来虚继承
有一个关键字叫做 virtual ,用于修饰菱形继承结构中包含相同父类成员的类,避免让最终的子类继承多份相同的内容。
class A
{
public:
int test_val;
void test_func()
{
}
};
class B : virtual public A //virtual修饰
{
public:
};
class C : virtual public A //virtaul修饰
{
public:
};
class D : public B,public C
{
public:
};
5、指针偏移问题(选择题高难)
下面是一个和继承有关的选择题:
下面是分析:
- 从语法角度来看,继承实现了代码层面的复用
- 而在内存层面,继承也在一定程度上节省了内存空间,毕竟,子类里有一部分包含了父类,没必要总是另外单独占据空间。
- 在题目中:子类首先继承了父类Base1,然后才是Base2,这是关键所在。
- 所以子类就可以在已有父类的基础上扩充一点点,也就是和Base!公用一个起点!!!
四、组合和继承:
继承是咱们本篇探讨的重点,那组合是个啥?其实 就是在使用适配器模式实现Stack时用到的小手段。
1、组合(has a):
回顾通过适配器模式来实现Stack的场景,代码如下:
template<class T , class container = deque<T>>
class myStack
{
public:
void push(const T& val)
{
self.push_back(val);
}
void pop()
{
self.pos_back();
}
T& top()
{
return self.back();
}
private:
container self;
};
- 其中。myStack类里可以定义和使用自己的成员函数和变量,同时也可以调用deque开放的函数接口。
- 我们不能说这个myStack是一个deque,因为他俩本质上还是比较独立的,这就是组合
2、继承(is a):
对于继承,我们会发现每一种关系都可以用“……是……”来表述。比如:
- 学生是一个人
- 老师是一个人
- 助教即使老师,也是学生,也是人
五、软件工程概念:耦合度
耦合度指的是程序中两个模块之间依赖程度的高低,这里用刚刚了解的组合与继承来理解:
1、组合的耦合度较低:
- 上面提到过的适配器模式里,deque和myStack之间就比较独立。
- myStack只能访问到底层deque暴露出来的函数接口(也就是他的共有成员)
- 如果deque提供的函数接口足够有限,两者相互依赖的程度会更低。无论底层模块做了什么修改,只要让上面的调用层修改少量的接口即可(很多时候为了方便维护甚至只暴露一个调用结构)
2、继承的耦合度较高:
- 子类的结构严格依赖父类:是否还记得c++11之前如何实现不能被封装的类???把父类的构造函数设为私有,就会导致子类根本用不了。
- 父类结构对于子类来讲相对透明:通常来讲,一个类如果需要被继承,他的成员访问限定符就会设置成public和protect,然后以共有继承方式来继承。这样会导致子类可以比较随意的窥探和访问父类底层的各种函数结构和成员变量,容易误操作。
3、不要非黑即白:
的确,组合的耦合度低,可维护性好,继承则与之相反,看起来一无是处。但是:
- 实际设计场景中,不见得所有的结构都可以描述为“has a”。
- 如果要支持c++的多态机制,继承是一个必备条件。