C++从入门到放弃
继承
1. 继承基本概念
-
对象的自恰性
所有和基类相关成员的操作都应有基类提供相应的方法功能 -
继承的概念
通过一种机制描述类型之间共性和特征的方式,利用已有的数据类型定义新的数据类型,这种机制就是继承
基类--派生-->子类
子类--继承-->基类
-
继承语法
class/struct 类名:继承方式 基类,继承方式 基类,...{//继承表
访问控制限定符:
类名 (形参表):成员变量(初值),...{//初始化列表
函数体;//构造函数
}
~类名(void){ //析构函数
函数体;
}
返回值类型 函数名(形参表) 常属性 异常说明{ //成员函数
函数体;
}
访问控制限定符:
数据类型 变量名;//成员变量
}
#include <iostream>
using namespace std;
class Human {
public:
Human(const string &name, unsigned int age) : m_name(name), m_age(age) {
}
void eat(const string &food) {
cout << "I eat some " << food << ". ";
}
void sleep(const unsigned int time) {
cout << "I sleep " << time << "hours" << ". ";
}
private:
protected:
string m_name;
unsigned int m_age;
};
class Student : public Human {
public:
Student(const string &name, unsigned int age, unsigned int no) : Human(name, age), m_no(no) {
}
void talk() {
cout << "I'm " << m_name << ", " << m_age << "years old. My no is " << m_no <<". " <<endl;
}
void learn(const string& course){
cout << "I learn "<<course<<". ";
}
private:
unsigned int m_no;
};
class Teacher : public Human {
public:
Teacher(const string &name, unsigned int age,unsigned int sal):Human(name,age),m_sal(sal){
}
void talk(){
cout << "I'm " << m_name << ", " << m_age << "years old. My sal is "<< m_sal <<". " <<endl;
}
void teach(const string &course){
cout<<"I teach "<<course<<". ";
}
protected:
unsigned int m_sal;
};
int main() {
Student lisi("lisi",20,10010);
lisi.talk();
Teacher wangwu("wangwu",25,3000);
wangwu.talk();
return 0;
}
2. 继承方式
- 公有继承
public
基类的特性将通过子类向外扩散
公有继承时,对基类的公有成员和保护成员的访问属性不变,派生类的新增成员可以访问基类的公有成员和保护成员,但是访问不了基类的私有成员,派生类的对象只能访问派生类的公有成员包括继承的公有成员,访问不了保护成员和私有成员
#include <iostream>
using namespace std;
class Base {
public:
Base() : m_public(10), m_private(20), m_protected(30) {}
const int& getPrivate(){
return m_private;
}
int m_public;
private:
int m_private;
protected:
int m_protected;
};
class Derived: public Base{
public:
void func(){
cout<<m_public<<endl;
cout<<m_protected<<endl;
//cout <<m_private<<endl;//子类无法直接访问基类的私有成员
cout <<getPrivate()<<endl;//子类通过基类的公有成员函数进行访问
}
private:
protected:
};
int main() {
Derived derived1;
derived1.func();
return 0;
}
-
保护继承
protected
基类的特性仅在继承链的范围内扩散
保护继承中,基类的公有成员和保护成员被派生类继承后变成保护成员,派生类的新增成员可以访问基类的公有成员和保护成员,但是访问不了基类的私有成员,派生类的对象只能访问派生类继承基类的公有成员,访问不了保护成员和私有成员,通过保护继承的子类再进行派生时,孙子类将无法访问基类中(爷爷类)中的保护成员和私有成员 -
私有继承
private
基类的特性仅为子类所有,不向任何方向扩散
私有继承时,基类的公有成员和保护成员在派生类中的属性为私有成员,派生类新增成员可以访问基类的公有成员和保护成员,但是访问不了基类的私有成员,派生类的对象不能访问派生类继承基类的公有成员,保护成员和私有成员 -
保护继承和私有继承的比较
比较一下私有继承和保护继承(也就是比较在私有派生类中和在保护派生类中的访问属性), 可以发现,在直接派生类中,以上两种继承方式的作用实际上是相同的:在类外不能访问任何成员,而在派生类中可以通过成员函数访问基类中的公用成员和保护成员。但是如果继续派生,在新的派生类中,两种继承方式的作用就不同了。
例如,如果以公用继承方式派生出一个新派生类,原来私有基类中的成员在新派生类中都成为不可访问的成员,无论在派生类内或外都不能访问,而原来保护基类中的公用成员和保护成员在新派生类中为保护成员,可以被新派生类的成员函数访问。
大家需要记住:基类的私有成员被派生类继承(不管是私有继承、公有继承还是保护继承)后变为不可访问的成员,派生类中的一切成员均无法访问它们。如果需要在派生类中引用基类的某些成员,应当将基类的这些成员声明为protected
,而不要声明为private
3. 公有继承语法特性
子类对象会继承基类的属性和行为,通过子类对象可以访问基类的成员,就如同基类访问它们一样
子类对象中包含基类部分称为基类子对象
1> 向上造型(upcast) 子类->基类
概念:
子类类型的指针或引用总可以被隐式转换为基类类型的指针或引用,
这种操作性缩小的转化在编译器看来是安全的,可以直接隐式转换
作用
在函数参数设计时使用基类类型,可以具有通用性
2> 向下造型(downcast)基类->子类
概念:
基类类型的指针或者引用不能被隐式转换为其子类类型的指针或引用,
这种将操作性放大的类型转换在编译器看来是危险的,不能隐式转换,但是可以显式转换,可以使用静态类型转换static_cast<目标类型>(原类型)
class A{...};
class B:public A{...};
class C:publicA{...};
...
void func1(A* pa){
...
}
void func2(A& ra){
...
}
void func3(B* pb){
...
}
void fun4(B& rb){
...
}
int main(void){
A a;
func3(static_cast<B*>(&a)); //向下造型
func4(static_cast<B>(a));//向下造型
B b;
func1(&b);//向上造型
func2(b);//向上造型
C c;
func1(&c);//向上造型
func2(c);//向上造型
}
3> 子类继承基类的成员
- 在子类中或通过子类对象可以直接访问使用基类中的公有和保护成员,就如同他们自己的成员一样(基类中的公有成员和保护成员,就如同他们在子类中的声明一样)
- 基类的私有成员子类也可以继承,但会受到访问控制属性的影响,在子类无法直接对基类的私有成员访问,如果让子类可以访问基类中私有成员,可以让基类提供公有或保护成员函数来间接访问
4> 子类隐藏基类的成员
局部有限原则
- 如果子类和基类中定义的成员名字一样,因为作用域不同,不会构成重载关系,而是一种隐藏无法直接访问隐藏关系,通过子类对象将优先访问子类自己自己的成员,基类中的同名成员将被隐藏无法直接访问
- 这时如果还希望访问基类中被隐藏的成员,可以通过
类名::成员
的方式来显式指明现在要访问的成员属于哪个类 - 如果隐藏成员的成员是成员函数并满足不同参数的重载要求,也可以使用using声明,将基类中的成员函数引入到子类的作用域中,基类中被隐藏的成员函数与子类中的成员函数形成重载,通过函数重载匹配来解决
5> 访问控制属性和继承方式
1)访问控制属性:影响访问该类成员的位置
访问控制 访问控制 内部 子类 外部 友元
限定符 属性 访问 访问 访问 访问
public 公有成员 ok ok ok ok
protected 保护成员 ok ok no ok
private 私有成员 ok no no ok
2)继承方式:影响通过子类访问基类中的成员的可访问性
基类中的 在公有继承 在保护继承 在私有继承
的子类中 的子类中 的子类中
公有成员 公有成员 保护成员 私有成员
保护成员 保护成员 保护成员 私有成员
私有成员 私有成员 私有成员 私有成员
注:向上造型的语法特性在保护继承和私有继承中不再适用
6> 子类的构造函数
- 如果子类的构造函数没有显式指明基类子对象的初始化方式,那么编译器将会自动调用基类的无参构造函数来初始化基类子对象
- 如果希望基类子对象以有参的方式初始化,那么必须使用初始化列表,显式指明基类子对象需要的构造实参
- 子类对象的创建过程
1> 分配内存
2>构造基类子对象(继承表顺序)
2> 构造成员子对象(声明顺序)
3> 执行子类构造函数代码
7> 子类对象的析构函数
- 子类的析构函数,无论是自定义的子类析构函数,还是编译器缺省提供的子类析构函数都会自动调用基类的析构函数,完成基类子对象的销毁
- 子类对象的销毁过程
1> 执行子类的析构函数代码
2> 析构成员子对象(按声明的逆序)
3> 析构基类子对象(按继承表顺序)
4> 释放内存 - 基类的析构函数不能调用子类的析构函数,因此
delete
一个指向子类对象的基类指针,实际被执行的将是基类的析构函数,而子类的析构函数不会被执行,有内存泄漏的风险
class A{
...
};
class B:public A{
...
};
A* pa = new B;//pa 指针指向子类对象
delete pa;//该语句调用A类的析构函数,而pa指向的却是子类,不会调用子类的析构函数,因此有内存泄漏的风险
/*
解决方法: 虚析构函数
*/
8> 子类拷贝构造和拷贝赋值
- 拷贝构造
- 子类没有自定义拷贝构造函数,编译器会为子类提供缺省的拷贝构造函数,该函数会自动调用基类的拷贝构造函数,完成基类子对象的拷贝初始化
- 如果子类自己定义拷贝构造函数需要使用初始化列表来显式指明基类子对象也要以拷贝初始化的方式进行初始化
class Base{};
class Derived:public Base{
//Base(that):显式指明基类子对象以拷贝方式进行初始化
Derived(const Derived& that):Base(that),...{...}
};
- 拷贝赋值
- 子类没有自定义拷贝赋值函数,那么编译器会为子类提供拷贝赋值函数,该函数会自动调用基类的拷贝赋值函数,完成基类子对象的赋值操作
- 如果子类自己定义了拷贝赋值函数,那么需要显式调用基类的拷贝赋值函数,以完成基类子对象的拷贝赋值操作
class Base{
...
};
class Derived:public Base{
Derived& operator=(const Derived& that){
if(&that != this){
//显式调用基类拷贝赋值函数
Base::operator=(that);
...
}
return *this;
}
};
9> 关于子类的操作符重载
在重写子类的操作符重载时,可以复用基类的输出操作符重载函数,完成对基类子对象成员的输出操作