C++继承概念梳理

继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能。


类的成员具有三种访问限定符:public                protect(保护)          private    

(父)类和派生(子)类之间继承关系也是三种:    public      protect       private

说明:   关键字protect修饰的成员为保护成员,保护成员可以在本类的成员函数中访问,也可以被本类的派生类的成员函数访问,而类外的任何访问都是非法的。即它是半隐藏的。(类的对象也是不能访问)

声明一个派生类的格式一般为:

class 派生类名 : 继承方式  基类名{

        派生类新增的数据成员和成员函数

};


说明:

1.基类private成员在派生类中是不能被访问的,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。

2.public继承是一个接口继承,保持is-a原则,每个父类可用的成员对子类也可用,因为每个子类对象也都是一个父类对象

3.protected/private继承是一个实现继承,基类的部分成员并非完全成为子类接口的一部分,是 has-a 的关系原则,所以非特殊情况下不会使用这两种继承关系,在绝大多数的场景下使用的都是公有继承。私有继承意味着is-implemented-in-terms-of(是根据……实现的)。通常比组合(composition)更低级,但当一个派生类需要访问基类保护成员或需要重定义基类的虚函数时它就是合理的。

4.不管是哪种继承方式,在派生类内部都可以访问基类的公有成员和保护成员,基类的私有成员存在但是在子类中不可见(不能访问)

5.使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式

6.在实际运用中一般使用都是public继承,极少场景下才会使用protetced/private继承

三种继承场景的测试时用例:

class Person
{
public:
	//Person(const string name)
	//	:_name(name)
	//{
	//}
	//Person(const string& name)
	//	:_name(name)
	//{}

	void Display(){
		cout << _name << endl;
	}

protected:
	string _name;
private:
	int _age;
};

//class Student : public Person
//class Student : protected Person
class Student : private Person
{
public :
	void Show()
	{
		Display();
		cout << _name << endl;
		cout << _num << endl;
	}
protected:
	int _num;
};

int main()
{
	Person p;
	Student s;
	s.Show();
	s.Display();
	system("pause");
	return 0;
}
在public继承权限下,子类和派生类对象之间有:
1.子类对象可以赋值给父类对象(切割/切片)
2.父类对象不能赋值给子类对象
3.父类的指针/引用可以指向子类对象

4.子类的指针/引用不能指向父类对象(可以通过强制类型转换完成)

父子类之间的切片行为:


int main()                // ---父子类同上
{
	Person p;
	Student s;
	// 1.子类对象可以赋值给父类对象(切割/切片)
	p = s;
	// 2.父类对象不能赋值给子类对象
	//s = p;                //出错
	// 3.父类的指针/引用可以指向子类对象
	Person* p1 = &s;
	Person&r1 = s;
	// 4.子类的指针/引用不能指向父类对象(可以通过强制类型转换完成)
	Student* p2 = (Student*)&p;
	Student&r2 = (Student&)p;

	system("pause");
	return 0;
}

对切片/切割的说明:

在进行切片时,并没有发生类型转换。没有产生临时变量。


继承体系中的作用域

1. 在继承体系中基类和派生类都有独立的作用域。

2. 子类和父类中有同名成员,子类成员将屏蔽父类对成员的直接访问。(在子类成员函数中,可以使用基类::基类成员访问)--隐藏--重定义

3. 注意在实际中在继承体系里面最好不要定义同名的成员

隐藏属性的探究:

class Person
{
public:
	void f1()
	{
		cout<<"Person::f1()"<<endl;
	}

	int _stunum;
};

class Student : public Person
{
public:
	void f1()
	{
		printf("%p\n", this);
		cout<<"Student::f()"<<endl;
	}

	cout<<"Student::f4()"<<endl;
	}

	int _stunum;
};

int main()
{
	Student s;
	cout<<sizeof(s)<<endl;
	// 隐藏 重定义
	s._stunum = 200;              //这里的_stunum为派生类的stunum,基类中的_stunum被隐藏
	//s.Person::_stunum = 20;     //这样可以访问基类中的_stunum

	s.Person::f1();
	s.f1();

	return 0;
}

在下面的代码中,Person类中的函数 f1(in i) 和 Student类中的 f1()之间是什么关系?   ------- 重载?  ------隐藏?

class Person
{
public:
	int f1(int i)
	{
		cout<<"Person::f1()"<<endl;
	}

	int _stunum;
};

class Student : public Person
{
public:
	void f1()
	{
		printf("%p\n", this);
		cout<<"Student::f1()"<<endl;
	}

	int _stunum;
};
这两个函数其实构成了隐藏!这里要打到隐藏的条件只需要一条:只要函数名相同即可!

总结

不论对于变量还是函数来说,只要名称相同,就能构成隐藏。

来看一个有意思的题:

class Person
{
public:
	void f()
	{
		cout<<"Person::f()"<<endl;
	}

	void f1()
	{
		cout<<"Person::f1()"<<endl;
	}

	int _stunum;
};

class Student : public Person
{
public:
	void f1()
	{
		printf("%p\n", this);
		cout<<"Student::f()"<<endl;
	}

	void f2()
	{
		cout<<this->_stunum<<endl;
		cout<<"Student::f1()"<<endl;
	}

	void f3()
	{
		this->_stunum = 3;
		cout<<"Student::f1()"<<endl;
	}

	void f4()
	{
		this->f();
		cout<<"Student::f4()"<<endl;
	}

	int _stunum;
};

int main()
{


	Student* p = NULL;
	//p->f1();
	//p->f2();
	//p->f3();
	//p->f4();

	// A.代码编不过
	// B.可以编译通过,程序会崩溃
	// C.可以编译通过,正常输出Student::f()
	// D.以上选项都不对

	return 0;
}

答案:     C B B C

解析:(首先需要明白,类的成员变量存储在类中,即和类是紧密关联的,而类的成员函数是存放在公共代码段中的)p虽然是指针但是它并没有指向具体的对象,所以调用p并不会发生崩溃。当调用f2、f3时在函数中都对_stunum做了操作,而访问_stunum都是通过this指针进行访问的,而这个this指针所指向的位置是NULL,那么对空地址访问当然会出错了。如果还是不明白的的话请看图:


派生类的构造函数,拷贝构造函数,赋值运算符的重载探究:

继承体系下,派生类中如果没有显示定义这六个成员函数,编译器则会合成这六个默认的成员函数

派生类对象的构造与析构:

1、构造函数的调用按照先调用基类的构造函数,后调用派生类的构造函数的顺序执行,析构函数的调用刚好相反

2、基类没有缺省构造函数,派生类必须要在初始化列表中显式给出基类名和参数列表

3、基类没有定义构造函数,或者构造函数没有参数,则派生类也可以不用定义,全部使用缺省构造函数

4、基类定义了带有形参表构造函数,派生类就一定定义构造函数

5、如果派生类的基类也是一个派生类,每个派生类只需负责其直接基类的数据成员的初始化。

class Person
{
public :
	Person(const char* name = "xxx")
		: _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()
		:Person("ssss")
		,_stunum(0)
	{
		cout << "Student()" << endl;
	}

	Student(const Student& s)
		:Person(s)
		,_stunum(s._stunum)
	{}

	Student& operator=(const Student& s)
	{
		if (this != &s)
		{
			Person::operator=(s);
			_stunum = s._stunum;
		}

		return *this;
	}

	~Student()
	{
		//Person::~Person();  //这里会调用两次基类的析构
	}

protected:
	int _stunum;
};


class BB : public AA
{

};

int main()
{
	Student s1;
	/*Student s2(s1);

	Student s3;
	s1 = s3;*/
	//Person p;

	//s1.~Student();
	//p.~Person();


	return 0;
}

说明:

1.子类的构造函数,拷贝构造函数,赋值运算符的重载,都是合成版本的,即子类先调父类的构造,拷贝构造,赋值等函数,对子类对象中父类的部分构造,在调子类的完成对子类的构造。

2.析构函数在子类中会被编译器处理成和父类的析构同名。

3.析构函数先调用子类的,然后编译器自动的调用父类的析构函数。(这样做保证了子类先析构,符合栈的后进先出原则)

单继承:   一个子类只有一个直接父类时称这个继承关系为单继承

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

菱形(钻石)继承:  有两个子类共同继承了一个父类,而又有一个孙子类继承了这两个子类。

缺点:1、数据冗余           2、数据的二义性

class A
{
public:
	int _a;
};

class B : public A
{
public:
	int _b;
};

class C : public A
{
public:
	int _c;
};

class D : public B, public C
{
public:
	int _d;
};

int main()
{
	cout<<sizeof(D)<<endl;
	D d;
	d.B::_a = 1;
	d.C::_a = 2;
	d._b = 3;
	d._c = 4;
	d._d = 5;

	return 0;
}


解决菱形继承的数据冗余问题:    指定访问的具体成员。

解决菱形继承的数据冗余和二义性问题: 虚继承

虚继承:C++使用虚拟继承(Virtual Inheritance),解决从不同途径继承来的同名的数据成员在内存中有不同的拷贝造成数据不一致问题,将共同基类设置为虚基类。这时从不同的路径继承过来的同名数据成员在内存中就只有一个拷贝,同一个函数名也只有一个映射。

1. 虚继承解决了在菱形继承体系里面子类对象包含多份父类对象的数据冗余&浪费空间的问题。
2. 虚继承体系看起来好复杂,在实际应用我们通常不会定义如此复杂的继承体系。一般不到万不得已都不要定义菱形结构的虚继承体系结构,因为使用虚继承解决数据冗余问题也带来了性能上的损耗。

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()
{
	cout<<sizeof(D)<<endl;
	D d;
	d.B::_a = 1;
	d.C::_a = 2;
	d._b = 3;
	d._c = 4;
	d._d = 5;

	return 0;
}


为什么这里的D的大小是24?    这里添加了两个指针分别保存偏移量的地址

继承和友元:友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员

class Person
{
     friend void Display( Person &p , Student&s);
protected :
     string _name ; // 姓名
};
class Student : public Person
{
protected :
     int _stuNum ; // 学号
};
void Display( Person&p , Student &s)
{
     cout <<p . _name<< endl ;
     cout <<s. _name<< endl ;
     cout <<s. _stuNum<< endl ;
}
void TestPerson1 ()
{
     Person p;
     Students;
     Display(p,s);
}
继承与静态成员: 基类定义了static成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例。
class Person
{
public:
     Person ()
     {
            ++ _count ;
     }
protected :
     string _name ; // 姓名
public:
     staticint _count; // 统计人的个数。
};
int Person :: _count = 0;
class Student : public Person
{
protected :
      int _stuNum ; // 学号
};
class Graduate : public Student
{
protected :
     string _seminarCourse ; // 研究科目
};
void TestPerson1 ()
{
     Students1 ;
     Students2 ;
     Students3 ;
     Graduate s4 ;
     cout <<" 人数:"<< Person ::_count << endl;
     Student ::_count = 0;
     cout <<" 人数:"<< Person ::_count << endl;
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值