封装
封装的设计哲学:
封装是指将内部的实现细节尽量隐藏起来不对外暴露,只提供少量接口让使用方调用。这样做有两个好处:
1、当自身的逻辑发生变化时,不会破坏使用方的逻辑,或是强制使用方修改自身的逻辑,而是只需要修改自身的代码就可以了。
2、可以更好的保护对象的内部属性,防止被外界随意破坏。
封装性的实现
将类的属性用private修饰,然后对外提供getter、setter方法
java的访问权限分类:
c++的访问权限只是比java少了一个default而已,其他三个修饰符的访问权限是一样的。
继承
继承的设计哲学:
多个类中存在相同属性和行为时,将这些内容抽取到单独一个类中,那么多个类无需再定义这些属性和行为,只要继承那个类即可
有以下几个好处:
① 减少代码的冗余,提高代码的复用性
② 子类可以扩展父类的功能,满足对扩展开放,对修改关闭的开闭原则
③ 是实现多态的前提
java只支持单继承、c++支持多继承
java默认是public继承,c++中class默认是private继承,struct默认是public继承
初始化的加载顺序为:
父类静态成员变量 父类静态代码块
子类静态成员变量 子类静态代码块
父类非静态成员变量,父类非静态代码块,父类构造函数
子类非静态成员变量,子类非静态代码块,子类构造函数
不继承构造函数&析构函数
我们说基类的成员函数可以被继承,可以通过派生类的对象访问,但这仅仅指的是普通的成员函数,基类的构造函数和析构函数都不会被继承,因为即使继承了,它的名字和派生类的名字也不一样,不能成为派生类的构造/析构函数
派生类的构造函数中必须调用基类的构造函数,告诉编译器想要调用父类的那个构造函数,如果不指明,就调用基类的默认构造函数(不带参数的构造函数);如果没有默认构造函数,那么编译失败
但是析构函数不需要,因为每个类只有一个析构函数,编译器知道如何选择,无需程序员干涉。
下面的例子展示了如何在派生类的构造函数中调用基类的构造函数:
#include<iostream>
using namespace std;
//基类People
class People{
protected:
char *m_name;
int m_age;
public:
People(char*, int);
};
People::People(char *name, int age): m_name(name), m_age(age){}
//派生类Student
class Student: public People{
private:
float m_score;
public:
Student(char *name, int age, float score);
void display();
};
//关键代码:People(name, age)就是调用基类的构造函数
Student::Student(char *name, int age, float score): People(name, age), m_score(score){ }
void Student::display(){
cout<<m_name<<"的年龄是"<<m_age<<",成绩是"<<m_score<<"。"<<endl;
}
int main(){
Student stu("小明", 16, 90.5);
stu.display();
return 0;
}
//运行结果:小明的年龄是16,成绩是90.5。
关键代码:
Student::Student(char *name, int age, float score): People(name, age), m_score(score){ }
People(name, age)就是调用基类的构造函数,并将 name 和 age 作为实参传递给它,m_score(score)是派生类的参数初始化表,它们之间以逗号,隔开。
也可以将基类构造函数的调用放在参数初始化表后面,无论前面还是后面,派生类构造函数总是先调用基类构造函数再执行其他代码(包括参数初始化表以及函数体中的代码),总体上看和下面的形式类似:
Student::Student(char *name, int age, float score){
People(name, age);
m_score = score;
}
当然这段代码只是为了方便大家理解,实际上这样写是错误的,因为基类构造函数不会被继承,不能当做普通的成员函数来调用。
另外,函数头部是对基类构造函数的调用,而不是声明,所以括号里的参数是实参,它们不但可以是派生类构造函数参数列表中的参数,还可以是局部变量、常量等
继承时的对象内存模型
看下面的例子
//基类A
class A{
int m_a;
int m_b;
};
//派生类B
class B: public A{
int m_c;
};
//派生类C
class C: public B{
int m_d;
};
内存模型如下
成员变量按照派生的层级依次排列,新增成员变量始终在最后。
如果有成员变量遮蔽呢,内存分布怎样的?
//声明并定义派生类C
class C: public B{
int m_b; //遮蔽A类的成员变量
int m_c; //遮蔽B类的成员变量
int m_d; //新增成员变量
};
当基类 A、B 的成员变量被遮蔽时,仍然会留在派生类对象 obj_c 的内存中,C 类新增的成员变量始终排在基类 A、B 的后面。
总结:在派生类的对象模型中,会包含所有基类的成员变量。这种设计方案的优点是访问效率高,能够在派生类对象中直接访问基类变量,无需经过好几层间接计算。
多继承时的对象内存模型
c同时继承a和b
//基类A
class A{
int m_a;
int m_b;
};
//基类B
class B{
int m_b;
int m_c;
};
//派生类C
class C: public A, public B{
int m_a;
int m_c;
int m_d;
};
A、B 是基类,C 是派生类,内存模型如下:
菱形继承、虚继承
菱形继承
多继承时很容易产生命名冲突,即使我们很小心地将所有类中的成员变量和成员函数都命名为不同的名字,命名冲突依然有可能发生,比如典型的是菱形继承,如下图所示:
类 A 派生出类 B 和类 C,类 D 继承自类 B 和类 C,这个时候类 A 中的成员变量和成员函数继承到类 D 中变成了两份,
一份来自 A–>B–>D 这条路径,
另一份来自 A–>C–>D 这条路径。
虚继承
为了解决多继承时的命名冲突和冗余数据问题,C++ 提出了虚继承,使得在派生类中只保留一份间接基类的成员。
在继承方式前面加上 virtual 关键字
就是虚继承
//间接基类A
class A{
int m_a;
};
//直接基类B
class B: virtual public A{ //虚继承
};
//直接基类C
class C: virtual public A{ //虚继承
};
//派生类D
class D: public B, public C{
public:
void seta(int a){ m_a = a; } //正确
};
这段代码使用虚继承重新实现了上面的菱形继承,这样在派生类 D 中就只保留了一份成员变量 m_a,直接访问就不会再有歧义了。
虚继承的目的是让当前的派生类承诺愿意共享自己继承的基类。这个被共享的基类就称为虚基类,本例中的 A 就是一个虚基类。
在这种机制下,不论虚基类在继承体系中出现了多少次,在派生类中都只包含一份虚基类的成员。
最后,不提倡在程序中使用多继承,只有在比较简单和不易出现二义性的情况或实在必要时才使用多继承,能用单一继承解决的问题就不要使用多继承。
派生类指针强制转换为基类指针(向上转型),指针内容会发生变化吗?
派生类在强制转换为基类时,派生类独有的部分将会被丢弃,只保存基类的内容,这种现象叫做对象切片
,不是语法错误,是一种与指针类型有关的问题
这种转换关系是不可逆的,只能用派生类对象给基类对象赋值,而不能用基类对象给派生类对象赋值。
理由很简单,基类不包含派生类的成员变量,无法对派生类的成员变量赋值。同理,同一基类的不同派生类对象之间也不能赋值。