继承其实是类层次的一个复用,争对某部分类具有共同的特征。
继承方式
class默认的继承方式是private,struct默认的继承方式是public。
保护protect和private在类里面都可以访问在类外面都不可以访问,在当前类二者没有区别。区别是在继承的时候派生类才会体现出来,基类private在子类是不可见,但是基类的protect在子类中是可见的。private和protect都防外人,但是protect还防儿子。
基类和派生类对象赋值转换
class Person
{
public:
void Print()
{
cout << "name:" << _name << endl;
cout << "age:" << _age << endl;
}
private:
string _name = "peter";
int _age = 18;
};
class Student : public Person//student是派生类,public是继承方式,person是基类
{
public:
void func()
{
//cout <<_name << endl;对于基类的私有成员,无法通过这种方式去直接访问,因为基类的私有成员对于派生类是不可见的。但是有间接的方式。
cout << "void func()" << endl;
}
protected:
int _stuid; // 学号
};
int main()
{
Student s;
s.Print();//但是我们可以通过基类的公有成员函数来访问基类的私有成员。因为父类掉print函数和派生类掉的print的函数是同一个函数,二者并没有区别。这就像自己无法去动父亲的私房钱(访问父类的private成员变量),但是可以通过父亲的同意来使用父亲的私房钱(也就是通过父类的公有成员函数访问父类的private成员变量)。
double d = 1.1;
int i = d; // 隐式类型转换
const int& ri = d;//要加const的原因是因为d不是直接传给ri的,而是先传给一个临时变量,而临时变量是具有常性的。所以ri引用的不是d,而是引用的临时变量。
//下面都只争对于public的继承方式,切割/切片并不是将剩余的部分直接丢掉。
Student s;
Person p = s;//子类可以给父类赋值,这种方式是天然支持的,不存在类型转换,也就是不会产生临时变量,其底层其实是调用了父类的拷贝构造。它认为子类对象就是一个特殊的父类对象,子类s不是将所有的成员都传给父类p,而是先进行切割,将子类中父类的那部分切来赋值过去。
Person& rp = s;//这里也验证了s并不会产生临时变量再赋值的,而是直接赋值的。此时rp并不是子类对象s的别名,而是子类对象s当中父类那一部分的别名。好像是将子类s当中属于父类的那一部分切割出来,但这并不是说将特属于子类的部分丢掉。
Person* ptrp = &s;//指针也是同上面一行一样,这里不存在强转,而是天然发生,但是只能指向子类s对象中父类的那一部分。
//父类是不可以给子类的,但是存在特殊的情况。
return 0;
}
继承中的作用域
class Person
{
protected:
string _name = "wjj"; // 姓名
int _num = 2023;// 身份证号
};
class Student : public Person
{
public:
void Print()
{
cout << Person::_num << endl;//但是通过访问限定符可以访问到基类的
cout << _num << endl;//默认访问的是子类的
}
protected:
// 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。隐藏只会出现在子类对象调用与父类对象同名成员的过程中出现。
int _num = 2024; // 学号
};
class A
{
public:
void fun()
{
cout << "func()" << endl;
}
};
class B : public A
{
public:
void fun(int i)
{
cout << "func(int i)->" << i << endl;
}
};
void Test()
{
B b;
b.fun(10);//对于成员函数的隐藏,只需要函数名相同就可以构成隐藏了,并不需要考虑参数的类型个数、返回值等一系列问题。此时A和B中的fun( )构成的是隐藏,而不是重载,因为这两个函数的作用域不同。
b.A::fun();//通过指定作用域来访问基类的函数
};
编译器生成的默认构造函数是对于内置类型不处理,对于自定义类型调用其默认的构造函数。而拷贝构造和赋值运算符重载不写的话其默认生成的对于内置类型(生拷贝)和自定义类型(调用其拷贝或赋值运算符重载)都会进行处理。
派生类的默认成员函数的继承规则
//父类 class Person { public: Person(const char* name = "wjj") : _name(name) { cout << "Person()" << endl; } Person(const Person& p) : _name(p._name) { cout << "Person(const Person& p)" << endl; } Person& operator=(const Person& p) { cout << "Person operator=(const Person& p)" << endl; if (this != &p) _name = p._name; return *this; } ~Person() { cout << "~Person()" << endl; } protected: string _name; // 姓名 }; //子类 class Student : public Person { public: Student(const char* name, int num) :Person(name)//写成_name(name)是会报错的,因为_name是父类的成员,只能用父类的构造函数进行初始话。该行代码不写的话就会去调用Person的默认构造函数。继承就好像将父类当成子类的一个自定义成员一样。 ,_num(num)//_num是子类的成员,所以是可以采用子类的构造函数进行初始化的 { cout << "Student()" << endl; } Student(const Student& s) :Person(s)//初始化父类的成员可以传子类对象,这里就是切片的应用 , _num(s._num) { cout << "Student(const Student& s)" << endl; } Student& operator=(const Student& s) { if (this != &s) { Person::operator=(s);//父类干父类的活,子类干子类的活。必须要指定作用域,否则调用的就是子类的赋值运算符重载 _num = s._num; } cout << "Student& operator=(const Student& s)" << endl; return *this; } // 析构函数会被处理成destructor ~Student() { //Person::~Person();//当我们想要构造一个子类对象的时候,构造函数运行的顺序是先构造父类再构造子类,析构的顺序应该就是先析构子类,再析构父类。这是栈帧的规则,后进先出。如果我们自己显式去写无法保证先子后父。 //所以对于子类对象的析构编译器并不会要求我们去显式调用父类的析构函数,而是子类析构函数完成时,会自动调用父类析构函数,保证先析构子再析构父 cout << "~Student()" << endl; } protected: int _num; //学号 } int main() { Student s1("w", 12); Student s2(s1); Person p1 = s1; s1 = s2; return 0; }
友元关系的继承规则
//关于友元关系的继承
class Student;
class Person
{
public:
friend void Display(const Person& p, const Student& s);
protected:
string _name;
};
class Student : public Person
{
friend void Display(const Person& p, const Student& s);
protected:
int _stuNum;
};
void Display(const Person& p, const Student& s)
{
cout << p._name << endl;
cout << s._stuNum << endl;//如果没有上面第7行代码,该行代码是行不通的,因为Display并不是Student的友元,无法访问Student的_strNum对象。
}
静态成员变量的继承规则
//静态成员变量的继承
class Person
{
public:
Person() { ++_count; }//我们可以通过再父类的构造函数当中对静态成员变量进行++,来计算父类及其派生类的成员个数。因为派生类对象必须要调用父类对象构造函数去进行初始化
//protected:
string _name;
public:
static int _count; // 统计人的个数。
};
int Person::_count = 0;
class Student : public Person
{
protected:
int _stuNum;
};
class Graduate : public Student
{
protected:
string _seminarCourse;
};
int main()
{
Person p;
Student s;
cout << &(p._name) << endl;//父类对象里面有_name的成员
cout << &(s._name) << endl;//子类对象里面也有_name的成员,但是和父类的_name并不是同一个,所以地址不是同一个。
cout << &(p._count) << endl;
cout << &(s._count) << endl;//但是对于父类的静态成员变量从子类继承下来之后,给变量还是同一个,所以地址是一样的。
Graduate g1;
Graduate g2;
cout << Person::_count << endl;
cout << Graduate::_count << endl;//静态成员变量对于父类和其派生类来说都是共享的
return 0;
}
实现一个不可以继承的类
//如何实现一个不能被继承的类
class A
{
public:
static A CreateObj()//2、但是接下来的问题时自己也没有办法去调用构造函数,此时采用一个共有函数来创建类
//3、接下来的问题是没有对象去调用该公有函数,因为调用函数需要对象,而创建对象有需要调用该共有函数才可以创建,于是将该函数设置为静态成员函数
{
return A();
}
private:
A()//1、将类的构造函数设为私有,析构函数也可以
{}
};
class B : public A
{};
int main()
{
A::CreateObj();
return 0;
}
多继承
一个子类有两个或以上直接父类时称这个继承关系为多继承
关于菱形继承的问题
//多继承导致的菱形继承带来的问题
class Person
{
public:
string _name; // 姓名
//int _age;
//int _tel;
//int _address;
};
class Student : public Person
{
protected:
int _num; //学号
};
class Teacher : public Person
{
protected:
int _id; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected:
string _majorCourse; // 主修课程
};
void test()
{
// 这样会有二义性无法明确知道访问的是哪一个,但我们可以通过访问限定符来解决这个问题。
Assistant a;
//a._name = "张三";//改代码会报错,因为不知道访问哪个父类的_name变量,这是二义性的问题,并不是隐藏,隐藏式父子之间存在相同名称的成员,而这里是上面的父类存在相同的成员。
//下面的访问限定符虽然可以解决二义性的问题,但是这种菱形继承关系带来的关键问题是数据冗余,也就是空间浪费。
a.Student::_name = "小张";
a.Teacher::_name = "老张";
}
菱形继承的解决方法(虚继承)
class A { public: int _a; }; class B : virtual public A//虚继承 { public: int _b; }; class C : virtual public A//虚继承 { public: int _c; }; class D : public B, public C { public: int _d; }; int main() { D d; d.B::_a = 1; d.C::_a = 2; d._b = 3; d._c = 4; d._d = 5; B b; b._a = 10; b._b = 20; B* ptrb = &d; ptrb->_a++; ptrb = &b; ptrb->_a++; D d1, d2; return 0; }
由上可见,通过存储父类对象a的偏移量可以大大的节省空间,虽然效率变低了,因为多了一个步骤,但是解决了数据冗余和二义性的问题。而且优厚D类实例化的对象,不同对象存储的地址都是这两个,因为不同对象由于是同一个类实例化出来的,所以偏移量是一样的。
菱形继承很复杂,而且效率会下降,因为部分成员对象不可以直接找而要通过偏移量来找。
谁先被继承,谁就是属于先声明。
继承与组合
//public继承的关系式B is A,B可以直接用A的3个成员,A改动保护很可能会影响B。耦合度高。
class A
{
public:
void func(){}
protected:
int _a1;
int _a2;
};
class B : public A
{};
//组合的关系式D has C,D可以直接用C的一个成员func()函数,可以通过func()函数简介用C的另外两个成员。C改动保护和私有成员基本不会影响D,D无法用C的私有和保护。耦合度低。
class C
{
public:
void func(){}
protected:
int _c1;
int _c2;
};
class D
{
private:
C _cc;
};