C++中继承,隐藏(重定义),切片行为

继承

继承是面向对象 复用的重要手段,继承是类型之间的关系建模,部分实现共享,并且实现各自本质不同的东西。


.继承方式和访问限定符的关系

三种继承关系:

public://共有
private://私有      
protected://保护

三种访问限定符:

public://共有继承
private://私有继承   
protected://保护继承
两者之间的关系:

总结下来就是:
基类中的私有成员,在派生类中是不可见的   
基类中的公有成员和保护成员,则是根据继承方式来决定的,若是公有继承则成员访问限定不变,私有继承则成员访问限定为私有,保护继承则成员访问限定为保护。


而一般情况下,我们用到 最多的是公有继承,私有和保护都很少用到    
并且基类中的成员函数多为 公有
成员变量若是想让其派生类使用就设为保护的
成员变量若是不想让其派生类使用就设为私有的
2.一个简单的继承关系:
class Person//基类(父类)
{
public:
	Person(const string& name = "") :_name(name)
	{;}
	void Display()
    {cout <<"name:"<< _name << endl;}
protected:
	string _name;
};
class Student :public Person//这里的Student为派生类,继承关系为public,如果没有写则是默认为公有继承
{
public:
	Student(const string & num, const string & name)
		: Person(name)
		, _stunum(num)
	{;}
	void Display()
    {
		cout << "name:" << _name << endl;
		cout <<"stunum:"<<_stunum << endl;
    }
private:
	string _stunum;
};

为了很好的理解公有继承和其他两者继承的区别,我们认为:
公有继承是一种接口继承,其实是一个is_a的关系,意思是说,每个子类对象也都是一个父类对象,可以将子类对象直接赋值给父类对象。(发生一种切片行为)
私有继承和保护继承是一种实现继承,一个has_a的关系,意思是说,基类中的部分成员并非完全是派生类中的一部分(成员限定方式改变),这里的派生类就不可以直接赋值给基类对象了。
下面的例子来说明这种情况:

并且我们知道,在公有继承下,将子类赋值给父类对象中间是发生了一种切片行为,并没有说是中间转化为一个临时变量,我们可以用用一个例子来说明这个问题
总结一下:继承与转换的关系(公有继承)

1.父类对象不可以赋值给子类对象,但是当用指针或者引用加上类型强制转换虽然可以赋值过去,但是该对象并不可以用,会出现越界访问(子类的成员一般多余父类(例如成员变量stunum),当父类进行解引用访问时,它自己并没有那块空间)

2.公有继承下,子类对象可以赋值给父类对象,发生切片行为


2.  继承体系中的作用域:

基类和派生类都有自己独立的作用域
子类和父类中有同名的成员,子类成员将屏蔽对父类成员的直接访问----隐藏 ,重定义
这里的同名对于成员变量来说,仅仅是指函数名,与函数的参数和返回值都无关

但我们不建议在基类和派生类中使用同名的变量


先看一个例子:
class Person
{
public:
	void Display(){
		cout <<"class Person"<< endl;}
protected:
	string _name;
};
class Student :public Person
{
public:
	void Display(){
		cout << "class Student" << endl;
	}
protected:
	string _stunum;
};
int main()
{
	Student S1;
	S1.Display();
	cout << "***\n";
	S1.Person::Display();
	system("pause");
	return 0;
}
上面的程序执行结果为:
观察下面代码再给出下面选择题的答案:(不定项选择)
class Person
{
public:
	void Display(){
		cout <<"class Person"<< endl;}
	void func1()
	{
		cout << "func1" << endl;
	}
protected:
	string _name;
};
class Student :public Person
{
public:
	void Display(){
		cout << "class Student" << endl;
	}
	void func1(int a)
	{
		cout << "func1" << endl;
	}
protected:
	string _stunum;
};
int main()
{
	Student S1;
	S1.func1();
	system("pause");
	return 0;
}

//A.两个func1()构成隐藏
//B.两个func1()构成重载 
//C.代码不能编译通过 
//D.以上说法都错误
经编译后发现,这段代码没有编译通过, 
因为我们上面提到,对于成员函数,只是要求函数名相同就可以构成隐藏,
并且知道函数重载是在相同的作用域中的同名不同参的函数

所以上面的答案为AC

再看一个选择题,并回答下面的选择题,
class Student 
{
public:
	void f()
	{
		cout << "f()" << endl;
	}
	void func1()
	{
		cout << "func1()" << endl;
	}
	void func2()
	{
		cout << _stunum << endl;
		cout << "func2()" << endl;
	}
	void func3()
	{
		_stunum = 3;
		cout << "func3()" << endl;
	}
	void func4()
	{
		f();
	}
protected:
	string _stunum;
};
int main()
{
	Student *p = NULL;
	p->func1();
	p->func2();
	p->func3();
	p->func4();
	return 0;
}

//A .代码不能编译通过
//B.代码可以编译通过,但是 程序会崩溃
//C.代码可以编译通过,可以正常输出
//D.以上选项都不对

 p->func1();//答案:C 
 p->func2();//答案:B 
 p->func3();//答案:B
 p->func4();//答案:C
解释:
先了解1.因为一个类中的成员函数是放在代码段中,并不是这个对象所占空间的一部分
          2.对一个指针进行解引用是想访问其所指类型大小的空间中的内容
这样的话,当我们用这个指针来访问成员函数时,并不是对这个指针进行解引用,而是将这个指针当作参数来传递给成员函数来调用这个函数的; 我们需要用来访问成员变量时,就会对该指针进行解引用,而对一个空指针进行解引用,程序当然会崩溃

三、派生类的6个默认成员函数

当我们定义一个类时,当我们没有写构造函数时,会调用默认的构造函数
那么当派生类中没有写构造函数时呢,它的构造函数会是怎么样的呢在派生类中如果没有显示定义这六个成员函数, 
编译系统则会默认合成这六个默认的成员函数 
意在,派生类的构造函数中会先调用基类的构造函数,再进行定义其自己的成员变量

有下面代码:

class Person
{
public:
	Person()
	{
		cout << "Person()" << endl;
	}
	~Person()
	{
		cout << "~Person()" << endl;
	}
protected:
	string _name;
};
class Student :public Person
{
public:
	Student()
	{
		cout << "Student()" << endl;
	}
	~Student()
	{
		cout << "~Student()" << endl;
	}
protected:
	string _stunum;
};
int main()
{
	Student S1;
	S1.~Student();
	system("pause");
	return 0;
}

上面的代码,我们定义了一个派生类对象,并且调用了它的析构函数(析构函数是可以显示调用的,构造函数不能显示调用)

我们注意这里构造函数和析构函数的顺序 ,是符合我们对象创建时的压栈入栈顺序的当定义一个子类对象时,会先调用父类的构造函数,子类中继承父类那部分先定义出来,再来定义自己独有的那部分 
析构函数是,先清理子类的那部分,再去析构子类继承父类的那部分。 
我们再看下面的代码:
class Person
{
public:
	Person(const string& name = "") :_name(name)
	{cout<<"Person(const string& name = \"\")\n";}
	Person(const Person &p) :_name(p._name)
	{
		;
	}
	Person &operator=(const Person& p)
	{
		if (this != &p)
		{
			_name = p._name;
		}
		return *this;
	}
	~Person()
	{
		cout<<"~Person()\n";
	}
	void Display()
    {cout <<"name:"<< _name << endl;}
protected:
	string _name;
};
class Student :public Person
{
public:	Student(const string & num ="", const string & name="")
		: Person(name)//这里有显示的调用父类构造函数
		,_stunum(num)
	{cout<<"Student(const string & num, const string & name)\n";}
		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();//显示的去调用父类的析构函数
			cout<<"~Student()\n";
		}
	void Display(){
		cout << "name:" << _name << endl;
		cout <<"stunum:"<<_stunum << endl;}
private:string _stunum;
};
int main()
{
	
	Student S1("2015", "小明");
	S1.~Student();
	system("pause");
	return 0;
}
结果却是构造仍是按照我们预期的那样,析构函数却有三个,一个是我们显示调用父类的析构函数,一个是子类的析构函数,编译器会在调用析构函数之后自己调用一次父类的析构函数


并且注意到,但我们显示的调用父类的构造函数时,可以直接调用,但是调用父类的析构时却要指定类域,这一点我们后面讲。


那么编译器为什么会自己调用父类的析构函数呢,因为上面我们说到, 对象的创建与销毁是符合压栈顺序的,当我们自己在子类的析构函数中调用父类的析构函数时,我们不能保证每次都将父类的析构在最后面,所以编译器为我们做了优化,在每次调完子类的析构函数时会自行的调父类的析构,也避免了程序员疏忽忘记调用父类的析构而造成内存泄漏的问题

我们用底层汇编来看是如何调用析构函数的

子类调用构造函数都是调用的自己类同名的构造函数,而析构函数并不是调用自己的析构,是调用了一个destructor的函数,至于为什么编译器会将这里的析构函数处理为同名函数,与后面讲到的多态有很大的关系,当我们想显示调用父类的析构时,必须指明类域,因为这两个析构函数会构成隐藏,而且既然是同一个函数,我们为此也就不用在子类中显式的调用父类的构造函数。

比较常见的面试题:

实现一个不能被继承的类
实现一个只能在栈上生成对象的类
实现一个只能在堆上生成对象的类
感兴趣的可以看一下: 点击打开链接

 
 

猜你喜欢

转载自blog.csdn.net/misszhoudandan/article/details/80039192