目录
(1)示例1:子类对象 可以赋值给 父类的对象 / 基类的指针 / 基类的引用。
(3)不同域中函数名相同就是隐藏,函数所在作用域相同才能构成函数重载
(1)子类构造函数原则:构造顺序-先父后子。析构顺序-先子后父。
(4)问题:能否把菱形继承中的公共A的成员写成static,这样BC中的公共A就都能访问了?
(5)static 静态成员函数/变量 可以被继承,但不被对象包含
面向对象三大特性:封装、继承、多态
一.封装(无固定阐述)
1、C+ + Stack类设计和C设计Stack对比。 封装更好、访问限定符+类 (狭义)
2、迭代器设计。没有迭代器,容器的访问只能暴露底层结构。-> 使用复杂、使用成本很高,对使用者要求极高。
封装了容器底层结构,不暴露底层结构情况,提供统-的访问容器的方式,降低使用成本, 简化使用。
3、stack/queue/priority. _queue的设计-- 适配器模式
二.继承
定义:类设计层次的复用
图书管理系统
角色类:学生、老师、保安、保洁、后勤:
有些数据和方法每个角色都有的——设计重复了
有些数据和方法每个角色独有的
继承:
1.继承定义(一般就用public继承就行)
(1)定义:类设计层次的复用
下面我们看到Person是父类,也称作基类。Student是子类,也称作派生类
继承后父类的Person的成员(成员函数+成员变量)都会变成子类的一部分。这里体现出了Student和Teacher复用了Person的成员。下面我们使用监视窗口查看Student和Teacher对象,可以看到变量的复用。调用Print可以看到成员函数的复用。
class Person
{
public:
void Print()
{
cout << "name:" << _name << endl;
cout << "age:" << _age << endl;
}
// protected/private成员对于基类 -- 一样的,类外面不能访问,类里面可以访问
// protected/private成员对于派生类 -- private成员不能用 protected成员类里面可以用
//protected:
private:
string _name = "peter"; // 姓名
int _age = 18; // 年龄
// ...
};
// 继承后父类的Person的成员(成员函数+成员变量)都会变成子类的一部分。这里体现出了Student和Teacher复用了Person的成员。下面我们使用监视窗口查看Student和Teacher对象,可以看到变量的复用。调用Print可以看到成员函数的复用。
class Student : public Person
{
public:
void func()
{
Print();
//_age = 0; // 不可见
}
protected:
int _stuid; // 学号
};
class Teacher : public Person
{
protected:
int _jobid; // 工号
};
int main()
{
Student s;
Teacher t;
s.Print();
t.Print();
return 0;
}
(2)继承的内存大小
子类继承父类后会把父类的内容拷贝一份放在子类中
class A
{
public:
void print()
{
cout << "1" << endl;
}
int a = 0;
};
class B:public A
{
public:
void print()
{
cout << "1" << endl;
}
int b = 1;
};
class C :public B
{
public:
void print()
{
cout << "1" << endl;
}
int b = 1;
};
int main()
{
A a;
B b;
C c;
cout << sizeof(a) << endl;
cout << sizeof(b) << endl;
cout << sizeof(c) << endl;
return 0;
}
继承的内存大小的题目考察
(1)下面哪项结果是正确的( )
class Base1{ public: int _b1; };
class Base2 { public: int _b2; };
class Derive : public Base1, public Base2
{
public: int _d;
};
int main()
{
Derive d;
Base1* p1 = &d;
Base2* p2 = &d;
Derive* p3 = &d;
return 0;
}
A.p1 == p2 == p3
B.p1 < p2 < p3
C.p1 == p3 != p2
D.p1 != p2 != p3
分析:p1和p2虽然都是其父类,但在子类内存模型中,其位置不同,所以p1和p2所指子类的位置也不相同,因此p1!=p2,
由于p1对象是第一个被继承的父类类型,所有其地址与子类对象的地址p3所指位置都为子类对象的起始位置,因此p1==p3,所以C正确
2.继承的性质
private成员不能用 protected成员类里面可以用
3.基类和派生类对象赋值转换(public继承条件下)
(1)示例1:子类对象 可以赋值给 父类的对象 / 基类的指针 / 基类的引用。
子类对象 赋值给 父类的对象时无类型转换,只是切片,把子类对象中继承父类的那一部分赋给父类对象(父类对象无法给子类对象)
公有条件下,rp和ptrp都是指向s父类的那一部分
class Person
{
protected :
string _name; // 姓名
string _sex; // 性别
int _age; // 年龄
};
class Student : public Person
{
public :
int _No ; // 学号
};
(2)示例2:基类对象不能赋值给派生类对象。
//s = (Student)p; 这样就是错的
4.继承中的作用域
(1)成员变量相同构成隐藏
子类成员将屏蔽父类对同名成员的直接访问(局部优先)
可以使用 基类::基类成员 显示访问父类的 _num
//Student的_num和Person的_num构成隐藏关系,可以看出这样代码虽然能跑,但是非常容易混淆
class Person
{
protected:
string _name = "小李子"; // 姓名
int _num = 111; // 身份证号
};
class Student : public Person
{
public:
void Print()
{
cout << " 姓名:" << _name << endl;
cout << " 学号:" << _num << endl;
cout << " 身份证号:" << Person::_num << endl;
}
protected:
int _num = 999; // 学号
};
int main()
{
Student s;
s.Print();
return 0;
}
(2)成员函数的函数名相同就构成隐藏
(自己设计继承,避开隐藏)
// 两个fun关系是隐藏
class A
{
public:
void fun()
{
cout << "A::func()" << endl;
}
};
class B : public A
{
public:
void fun()
{
cout << "B::func()"<< endl;
}
};
int main()
{
B b;
b.fun(); //优先调用B子类中的
b.A::fun(); //指定才调用A子类中的
return 0;
};
(3)不同域中函数名相同就是隐藏,函数所在作用域相同才能构成函数重载
(自己设计继承,避开隐藏)
因为系统的函数名区分中会加上作用域
这里 b.fun(); 不是调用A中的fun,而是构成隐藏,调用B中的fun,因为B中的fun需要传参数,所以编译报错
// A::fun 和 B::fun 的关系 -> 隐藏
// 函数重载要求在同一作用域
class A
{
public:
void fun()
{
cout << "A::func()" << endl;
}
};
class B : public A
{
public:
void fun(int i)
{
cout << "B::func()" << endl;
}
};
int main()
{
Student s1;
s1.Print();
B b;
b.fun(); // 构成隐藏,编译报错
b.A::fun();
b.fun(1);
return 0;
};
int main()
{
return 0;
}
打印结果:
A::func()
B::func()
(4)隐藏的题目:下面打印结果是什么?
class A
{
public: void test(float a) { cout << a; }
int a = 0;
};
class B :public A
{
public: void test(int b) { cout << b; cout << " sss"; }
int b = 1;
};
void main()
{
A* a = new A;
B* b = new B;
a = b;
a->test(1.1);
}
a=b是把b的父类那部分赋给a,相当于没有任何改变,a->test(1.1); 调用的是父类中的test,B中虽然隐藏了A的test函数,但调用的是父类对象a,隐藏是通过子类对象调用test才能勾成隐藏,所以打印结果是1.1
5.派生类的默认成员函数
假设父类都写全了
class Person
{
public:
Person(const char* name)
: _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; // 姓名
};
(1)子类构造函数原则:构造顺序-先父后子。析构顺序-先子后父。
a、先 调用父类构造函数初始化继承自父类成员(父类那一堆当成一个整体的自定义类型成员变量即可,下面的Person(name)就是显示传参调用父类构造函数)
b、后 子类的构造函数再初始化子类的成员 -- 规则参考普通类
总结:构造函数初始化顺序:先父后子。析构函数析构顺序:先子后父。
初始化列表顺序不能代表初始化顺序,初始化顺序看声明顺序,声明中父类在子类的所有成员变量之前,下面仍然是先调用父类Person的构造函数(假设父类所有函数都写齐全了)
(2)子类中的赋值运算符重载
Person::operator=(s); 调用父类的赋值运算符重载切片后赋给this的继承父类部分,s3传给s时切片出继承父类的部分,this传给父类的赋值运算符重载中的this时切片成继承父类的部分,共切片了2次
// s1 = s3
Student& operator=(const Student& s)
{
if (this != &s)
{
Person::operator=(s);
_num = s._num;
}
cout << "Student& operator=(const Student& s)" << endl;
return *this;
}
(3)析构函数:构造顺序-先父后子。析构顺序-先子后父。
父子类的析构函数构成隐藏关系
原因:多态的需要,析构函数名统一会被处理成destructor(),解释详见这篇文章大标题二 -> 7(124条消息) C++:多态 详解_beyond.myself的博客-CSDN博客
为了保证析构顺序,先子后父,
子类析构函数完成后会自动调用父类析构函数,所以不需要我们显示调用
// 父子类的析构函数构成隐藏关系
// 原因:下一节多态的需要,析构函数名统一会被处理成destructor()
// 为了保证析构顺序,先子后父,
// 子类析构函数完成后会自动调用父类析构函数,所以不需要我们显示调用
~Student()
{
//Person::~Person();
cout << "~Student()" << endl;
}// -》自动调用父类析构函数
(4)小问题:如何设计一个不能被继承的类?
使父类构造函数私有,CreateObj函数返回构造函数,并设置成静态,使CreateObj可以在main函数中不用对象就能调用
class A
{
//friend class B;
public:
static A CreateObj()
{
return A();
}
private:
A()
{}
};
// 父类A的构造函数私有化以后,B就无法构造对象
class B : public A
{
};
int main()
{
B b; 错误
A a = A::CreateObj();
return 0;
}
6.继承与友元
class Student;
class Person
{
public:
friend void Display(const Person& p, const Student& s);
protected:
string _name; // 姓名
};
class Student : public Person
{
protected:
int _stuNum; // 学号
};
void Display(const Person& p, const Student& s)
{
cout << p._name << endl; //是父类友元,可以访问父类保护
cout << s._stuNum << endl; //但不是子类友元,不可以访问子类保护
}
void main()
{
Person p;
Student s;
Display(p, s);
}
7.继承与静态成员
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()
{
Student s1;
Student s2;
Student s3;
Graduate s4;
Person s;
cout << " 人数 :" << Person::_count << endl;
cout << " 人数 :" << Student::_count << endl;
cout << " 人数 :" << s4._count << endl;
cout << " 人数 :" << &Person::_count << endl;
cout << " 人数 :" << &Student::_count << endl;
cout << " 人数 :" << &s4._count << endl;
return 0;
}
8.复杂的菱形继承及菱形虚拟继承
了解菱形继承的问题,解决起来也复杂,如何解决要了解一下。我们自己设计尽量不要用菱形继承
防止冗余二义性,用virtual虚继承
(1)示例1
在菱形继承的腰部class Student : virtual public Person 加上virtual虚继承,防止冗余二义性,使 Person不重复,virtual作用是:把_name放在公共区域,a.Student::_name 或 a.Teacher::_name都访问这个公共的_name
class Person
{
public:
string _name; // 姓名
int _a[10000];
};
class Student : virtual public Person //virtual虚继承,防止冗余,使_name不重复
{
protected:
int _num; //学号
};
class Teacher : virtual public Person
{
protected:
int _id; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected:
string _majorCourse; // 主修课程
};
int main()
{
// 这样会有二义性无法明确知道访问的是哪一个
Assistant a;
a._name = "peter";
// 需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决
a.Student::_name = "xxx";
a.Teacher::_name = "yyy";
cout << sizeof(a) << endl;
return 0;
}
(2)示例2:菱形继承中公共A的存储
// 对象模型
class A
{
public:
int _a;
};
//int A::_a = 0;
class B : public A
//class B : virtual public A
{
public:
int _b;
};
class C : public A
//class C : virtual public A
{
public:
int _c;
};
class D : public B, public C
{
public:
int _d;
};
int main()
{
D d;
//d._a = 0;
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
//D d1;
//D d2;
//cout << &d1._a << endl;
//cout << &d2._a << endl;
return 0;
}
菱形继承中,对象B和C中的A分别存在自己的对象中存着一份,这样就会造成冗余和二义性,冗余是多存储了一次A,多消耗了4字节;二义性是d._a不知道访问的是 B中的A 还是 C中的A。
(3)示例3:菱形虚拟继承中公共A的存储
// 对象模型
class A
{
public:
int _a;
};
//int A::_a = 0;
//class B : public A
class B : virtual public A
{
public:
int _b;
};
//class C : public A
class C : virtual public A
{
public:
int _c;
};
class D : public B, public C
{
public:
int _d;
};
int main()
{
D d;
//d._a = 0;
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
return 0;
}
虚拟菱形继承,对象B和C中的A放在了最底下一个公共的地方(只有一个A,d.B:: a 和 d.c:: a访问时就没有二义性了),并都存储了一个指针,这个指针指向的位置存储着 距离A存储位置的偏移量 ,这个指针叫虚基表指针,他指向的存着偏移量的表叫虚基表,A叫虚基类。
(4)问题:能否把菱形继承中的公共A的成员写成static,这样BC中的公共A就都能访问了?
答:不能,你这样定义d1和d2里面的_a地址是一样的,静态成员变量是属于所有对象的,谁都可以访问。
// 对象模型
class A
{
public:
int _a;
//static int _a;
};
//int A::_a = 0;
class B : public A
//class B : virtual public A
{
public:
int _b;
};
class C : public A
//class C : virtual public A
{
public:
int _c;
};
class D : public B, public C
{
public:
int _d;
};
int main()
{
D d1;
D d2;
cout << &d1._a << endl;
cout << &d2._a << endl;
return 0;
}
(5)static 静态成员函数/变量 可以被继承,但不被对象包含
静态成员一定是不被包含在对象中的,基类对象中不包含基类的静态成员变量,子类对象不包含基类的静态成员变量
静态成员属于整个类,不属于任何对象,所以在整体体系中只有一份
静态成员函数可以被继承,成员变量所有的都会被继承,无论公有私有
(6)示例
// 对象模型
class A
{
public:
int _a;
//static int _a;
};
//int A::_a = 0;
//class B : public A
class B : virtual public A
{
public:
int _b;
};
//class C : public A
class C : virtual public A
{
public:
int _c;
};
class D : public B, public C
{
public:
int _d;
};
int main()
{
D d;
d._a = 0;
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
//D d1;
//D d2;
//cout << &d1._a << endl;
//cout << &d2._a << endl;
B b;
b._a = 10;
b._b = 20;
B* ptr1 = &d;
B* ptr2 = &b;
cout << ptr1->_a << endl;
cout << ptr2->_a << endl;
cout << ptr1->_b << endl;
cout << ptr2->_b << endl;
return 0;
}
(7)关于以下菱形继承说法不正确的是( )
class B {public: int b;};
class C1: public B {public: int c1;};
class C2: public B {public: int c2;};
class D : public C1, public C2 {public: int d;};
A.D总共占了20个字节 (只看A选项)
B.B中的内容总共在D对象中存储了两份
C.D对象可以直接访问从基类继承的b成员
D.菱形继承存在二义性问题,尽量避免设计菱形继承
A.C1中b和c1共8个字节,C2中c2和b共8个字节,D自身成员d 4个字节,一共20字节
B.由于菱形继承,最终的父类B在D中有两份
C.子类对象不能直接访问最顶层基类B中继承下来的b成员,因为在D对象中,b有两份,一份是从
C1中继承的,一份是从C2中继承的,直接通过D的对象访问b会存在二义性问题,在访问时候,可
以加类名::b,来告诉编译器想要访问C1还是C2中继承下来的b。
D.菱形继承存在二义性问题,尽量避免设计菱形继承,如果真有需要,一般采用虚拟继承减少数据冗余,选C。
9.继承和组合
顺序容器(vector/list/deque)
stack queue priority_ queue 既可以继承,也可以组合,这里用的就是组合
模块间关系:高内聚,低耦合(UML)
适合is-a关系建议继承
适合has-a关系建议组合
都可以->建议组合