一:什么是继承
定义:
继承机制是面向对象程序设计中使用代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能。这样产生的新类,称为派生类(或子类),被继承的类称为基类(或父类)。
继承呈现了面向对象程序设计的层析结构,体现了由简单到复杂的认知过程。之前接触的复用都是函数复用,继承是类设计层次的复用。
class human
//定义了一个父类
{
public:
string name = "小明";
int age = 18;
};
class student :public human
//定义了一个以public方式继承父类的子类student
{
public:
int school = 666;
void print()
{
cout << name << endl << age << endl << school << endl;
}
};
int main()
{
student st;
st.print();
return 0;
}
二:继承的格式
class 新类的名字:继承方式 继承类的名字{ };
如:class student:public human{ };
我们这里对于student和human就有两种叫法。
一种是教科书里面的基类(human)和派生类(student)。
还有一种是父类和子类的称呼
三:继承后的子类成员访问权限
继承的总结:
1.基类private成员无论以什么方式继承到派生类中都是不可兼得。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是在类外面都不能去访问它。
2.基类private成员在派生类中不能被访问,如果基类成员不想在派生类外直接被访问,但需要在派生类中访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
3.基类的私有成员在子类都是不可见;基类的其他成员在子类的访问方式就是访问限定符和继承方式中权限更小的那个(权限排序:public>protected>private)。
4.使用关键字class时默认的继承方式时private,使用struct时默认的继承方式时public,但最好显示地写出继承方式。
二:子类和父类(基类和派生类)
1.子类和父类的互相赋值:
class human
//定义了一个父类
{
public:
string name = "小明";
int age = 18;
};
class student :public human
//定义了一个以public方式继承父类的子类student
{
public:
int school = 666;
};
int main()
{
student st;
human hm;
hm = st;//将子类赋值给父类
st = hm;//将父类赋值给子类
return 0;
}
就会出现这样的结果:
在这里我们引入一个叫做切片原则的东西
因为父类中没有school,所以父类接收子类传过来的name和age之后,多余的school就被切割丢掉了。但是如果父类传给子类,少传一个,就会报错。
同时我们给出三种赋值方式
一:=符号
student st;//子类
human hm;//父类
hm=st;
二、引用
student st;//子类
human& hm=st;父类
三、指针
student st;//子类
human* hm=&st;//父类
2.同名的成员变量
在有些时候,父类和子类出现了同一个成员变量,如下name
class human
{
public:
string name = "小明";
};
class student :public human
{
public:
string name = "小红";
void print()
{
cout << name << endl;
}
};
int main()
{
student st;
st.print();
return 0;
}
这个时候编译器是以子类优先
结构如下
但是如果我们就是想要访问父类的该成员变量,就需要加上修饰
void print()
{
cout << human::name << endl;
}
其实也很好理解,默认子类,父类就修饰限定
3.同名成员函数
如下,同样一个函数print在父类和子类中都存在
class human
{
public:
string name = "小明";
void print()
{
cout << name << endl;
}
};
class student :public human
{
public:
string name = "小红";
void print()
{
cout << name << endl;
}
};
int main()
{
student st;
st.print();
return 0;
}
这就构成了隐藏。(函数重载是在同一个作用域,这里父类和子类是两个作用域)
函数的隐藏,编译器会默认调用子类中匹配的函数,如果没有编译器就会报错
虽然成员函数的隐藏,只需要函数名就构成隐藏,对参数列表没有要求。
但是我们修改一下子类的函数
void print(int x)//我们对子类的print函数加入一个参数
{
cout<<name<<endl;
}
这是因为编译器默认调用子类中的print函数,但是子类中唯一的print函数有一个默认的参数,所以
编译器无法找到匹配的print函数,所以就会报错。
三:子类中默认的成员函数
1.构造函数
编译器会先默认调用父类的构造函数,再调用子类的构造函数,如下
class human
{
public:
human(string name = "小明")//先调用:父类默认构造调用一个print打印name
:_name(name)
{
cout << name << endl;
}
protected:
string _name;
};
class student :public human//后调用:子类默认构造调用一个print打印name和age
{
public:
student(string name, int age)
:_age(age)
{
cout << name << endl << age << endl;
}
protected:
int _age;
};
int main()
{
student st("小红",18);
return 0;
}
结果如下
可以看到,编译器先调用了父类的,打印出了小明,然后再次调用类子类的打印出了小红和age。
所以说请务必保证父类构造有效,假如父类失效
human(string name)//你这里不传值,那么就不能完成初始化,相当于父类失效
:_name(name)
{
cout << name << endl;
}
那么就必须在子类中给父类构造赋值
student(string name,int age)
:_age(age)
, human(name)//新增,子类以自己的name给父类的析构中的name赋值,age和name的顺序随意变动
结构如下
2.析构函数
析构函数和构造函数相反,编译器默认先调用子类的析构函数,再调用父类的析构函数。
验证如下:
我们在原有的代码上,加入两个析构函数
class human
{
public:
human(string name = "小明")//先调用:父类默认构造调用一个print打印name
:_name(name)
{
cout << name << endl;
}
~human()
{
cout << "我是父类" << endl;
}
protected:
string _name;
};
class student :public human//后调用:子类默认构造调用一个print打印name和age
{
public:
student(string name, int age)
:_age(age)
{
cout << name << endl << age << endl;
}
~student()
{
cout << "我是子类" << endl;
}
protected:
int _age;
};
int main()
{
student st("小红",18);
return 0;
}
结构如下:
所以说千万不要在子类中调用父类的析构
如果是指针类型,那么同一块区域被析构两次就会造成野指针的问题。
3.拷贝构造
子类中调用父类的拷贝构造时,直接传入子类对象即可,父类的拷贝构造会通过“切片”拿到父类的那一部分。
class human
{
public:
human(string name = "小明")
:_name(name)
{
cout << name << endl;
}
protected:
string _name;
};
class student :public human
{
public:
student(string name, int age)
:_age(age)
{
cout << name << endl << age << endl;
}
student(student& s)
:human(s)//直接将st传过来通过切片拿到父类中的值
, _age(s._age)//拿除了父类之外的值
{
cout << s._age << endl << s._name << endl;
}
protected:
int _age;
};
int main()
{
student st("小红", 18);
student st2(st);
return 0;
}
结果如下:
4.赋值运算符重载
子类的operator=必须要显式调用父类的operator=完成父类的赋值。
因为子类和父类的运算符,编译器默认给与了同一个名字,所以构成了隐藏,所以每次调用=这个赋值运算符都会一直调用子类,就会造成循环,所以这里的赋值要直接修饰限定父类
class human
{
public:
human(string name = "小明")
:_name(name)
{}
human& operator=(const human& p)
{
if (this != &p)
{
cout << "调用父类" << endl;
_name = p._name;
}
return *this;
}
protected:
string _name;
};
class student :public human
{
public:
student(string name,int age)
:_age(age)
{}
student(student& s)
:human(s)
,_age(s._age)
{}
student& operator=(const student& s)
{
if (this != &s)
{
cout << "调用子类" << endl;
human::operator=(s);
_age = s._age;
_name = s._name;
}
return *this;
}
protected:
int _age;
};
int main()
{
student st("小红", 18);
student st2(st);
student st3("小刚", 16);
st = st3;
return 0;
}
结果如下:
这里补充说明一下:为什么三个对象都是小明,因为继承构造函数的调用先调用父类,再调用子类,只有父类可以修改_name。
四:单继承和多继承
单继承:
一个子类只有一个直接父类的继承关系。
多继承:
一个子类有两个或以上直接父类的继承关系。
以上两种组合就会出现菱形继承
菱形继承:
这段代码有问题
class A
{
public:
string name;
};
class B :public A
{
public:
int age;
};
class C :public A
{
public:
string sex;
};
class D :public B, public C
{
public:
int id;
};
int main()
{
D st;
st.name = "小明";
st.age = 18;
st.sex = "男";
st.id = 666;
return 0;
}
因为这里的name,同时存在B和C中,所以D不知道继承B的name还是C中的name
这也就是引出了代码冗余和二义性问题。
所以我们有两种解决方法
解决办法一:
加修饰限定
student.B::name=“小明”
这里我们继承B中的name,就不会冲突了
解决办法二:
虚继承:继承方式前加上virtual。
class B :virtual public A
{
public:
int age;
};
class C :virtual public A
{
public:
string sex;
};
单继承和多继承的总结:
一般不要用菱形继承。