C++:封装,继承

目录

一.封装(无固定阐述)

二.继承

1.继承定义(一般就用public继承就行)

(1)定义:类设计层次的复用

(2)继承的内存大小

继承的内存大小的题目考察

(1)下面哪项结果是正确的( )

2.继承的性质

3.基类和派生类对象赋值转换(public继承条件下)

(1)示例1:子类对象 可以赋值给 父类的对象 / 基类的指针 / 基类的引用。

(2)示例2:基类对象不能赋值给派生类对象。

4.继承中的作用域

(1)成员变量相同构成隐藏

(2)成员函数的函数名相同就构成隐藏

(3)不同域中函数名相同就是隐藏,函数所在作用域相同才能构成函数重载

(4)隐藏的题目:下面打印结果是什么?

5.派生类的默认成员函数

(1)子类构造函数原则:构造顺序-先父后子。析构顺序-先子后父。

(2)子类中的赋值运算符重载

(3)析构函数:构造顺序-先父后子。析构顺序-先子后父。

(4)小问题:如何设计一个不能被继承的类?

6.继承与友元

7.继承与静态成员

8.复杂的菱形继承及菱形虚拟继承

防止冗余二义性,用virtual虚继承

(1)示例1

(2)示例2:菱形继承中公共A的存储

(3)示例3:菱形虚拟继承中公共A的存储

(4)问题:能否把菱形继承中的公共A的成员写成static,这样BC中的公共A就都能访问了?

(5)static 静态成员函数/变量 可以被继承,但不被对象包含

(6)示例

(7)关于以下菱形继承说法不正确的是( )

9.继承和组合

面向对象三大特性:封装、继承、多态

一.封装(无固定阐述)

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成员类里面可以用

总结:
1. 基类 private 成员在派生类中无论以什么方式继承都是不可见的。这里的 不可见是指基类的私
有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面
都不能去访问它
2. 基类 private 成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在
派生类中能访问,就定义为 protected 可以看出保护成员限定符是因继承才出现的
3. 实际上面的表格我们进行一下总结会发现,基类的私有成员在子类都是不可见。基类的其他
成员在子类的访问方式 == Min( 成员在基类的访问限定符,继承方式 ) public > protected
> private
4. 使用关键字 class 时默认的继承方式是 private ,使用 struct 时默认的继承方式是 public 不过
最好显示的写出继承方式
5. 在实际运用中一般使用都是 public 继承,几乎很少使用 protetced/private 继承 ,也不提倡
使用 protetced/private 继承,因为 protetced/private 继承下来的成员都只能在派生类的类里
面使用,实际中扩展维护性不强

3.基类和派生类对象赋值转换(public继承条件下)

①派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用 。这里有个形象的说法叫切片
或者切割。寓意把派生类中父类那部分切来赋值过去。
②基类对象不能赋值给派生类对象。
③基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类
的指针是指向派生类对象时才是安全的。这里基类如果是多态类型,可以使用 RTTI(Run
Time Type Information) dynamic_cast 来进行识别后进行安全转换。( ps :这个我们后
面再讲解,这里先了解一下

(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. 在继承体系中 基类 派生类 都有 独立的作用域
2. 子类和父类中有同名成员 子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,
也叫重定义。 (在子类成员函数中,可以 使用 基类::基类成员 显示访问
3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
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.派生类的默认成员函数

6 个默认成员函数, 默认 的意思就是指我们不写,编译器会变我们自动生成一个,那么在派生类
中,这几个成员函数是如何生成的呢?
1. 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认
的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
2. 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
3. 派生类的 operator= 必须要调用基类的 operator= 完成基类的复制。
4. 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能
保证派生类对象先清理派生类成员再清理基类成员的顺序。
5. 派生类对象初始化先调用基类构造再调派生类构造。
6. 派生类对象析构清理先调用派生类析构再调基类的析构。
7. 因为后续一些场景析构函数需要构成重写,重写的条件之一是函数名相同 ( 这个我们后面会讲
) 。那么编译器会对析构函数名进行特殊处理,处理成 destrutor() ,所以父类析构函数不加
virtual 的情况下,子类析构函数和父类析构函数构成隐藏关系。
假设父类都写全了
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.继承与静态成员

基类定义了 static 静态成员,则整个继承体系里面只有一个这样的成员 。无论派生出多少个子
类,都只有一个 static 成员实例
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.复杂的菱形继承及菱形虚拟继承

了解菱形继承的问题,解决起来也复杂,如何解决要了解一下。我们自己设计尽量不要用菱形继承

单继承:一个子类只有一个直接父类时称这个继承关系为单继承
多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承

菱形继承:菱形继承是多继承的一种特殊情况。

菱形继承的问题:从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题。
Assistant 的对象中 Person成员会有两份。

防止冗余二义性,用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.继承和组合

1. 很多人说 C++ 语法复杂,其实多继承就是一个体现。有了多继承 ,就存在菱形继承,有了菱
形继承就有菱形虚拟继承,底层实现就很复杂。所以一般不建议设计出多继承,一定不要设
计出菱形继承。否则在复杂度及性能上都有问题。
2. 多继承可以认为是 C++ 的缺陷之一,很多后来的 OO 语言都没有多继承,如 Java
3. 继承和组合
public 继承是一种 is-a 的关系。也就是说每个派生类对象都是一个基类对象。
组合是一种 has-a 的关系。假设 B 组合了 A ,每个 B 对象中都有一个 A 对象。
优先使用对象组合,而不是类继承
继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称
白箱复用 (white-box reuse) 。术语 白箱 是相对可视性而言:在继承方式中,基类的
内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很
大的影响。派生类和基类间的依赖关系很强,耦合度高
对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象
来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复
(black-box reuse) ,因为 对象的内部细节是不可见的 。对象只以 黑箱 的形式出现。
组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被
封装。
实际尽量多去用组合。组合的耦合度低,代码维护性好 。不过继承也有用武之地的,有
些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用
继承,可以用组合,就用组合。

顺序容器(vector/list/deque)
stack queue priority_ queue 既可以继承,也可以组合,这里用的就是组合
模块间关系:高内聚,低耦合(UML)

适合is-a关系建议继承
适合has-a关系建议组合
都可以->建议组合

猜你喜欢

转载自blog.csdn.net/zhang_si_hang/article/details/126131704