🌈个人主页:秋风起,再归来~
🔥系列专栏:C++从入门到起飞
🔖克心守己,律己则安
目录
1、继承的概念
2、继承定义
2.1 定义格式
2.2 继承基类成员访问⽅式的变化
3、继承类模板
4、 基类和派⽣类间的转换
5、继承中的作⽤域
5.1 隐藏规则:
编辑 5.2 考察继承作⽤域相关选择题
6、完结散花
1、继承的概念
继承(inheritance)机制是⾯向对象程序设计使代码可以复⽤的最重要的⼿段,它允许我们在保持原有 类特性的基础上进⾏扩展,增加⽅法(成员函数)和属性(成员变量),这样产⽣新的类,称派⽣类。继承 呈现了⾯向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的函数层次的 复⽤,继承是类设计层次的复⽤。
下⾯我们看到没有继承之前我们设计了两个类Student和Teacher,Student和Teacher都有姓名/地址/ 电话/年龄等成员变量,都有identity⾝份认证的成员函数,设计到两个类⾥⾯就是冗余的。当然他们 也有⼀些不同的成员变量和函数,⽐如⽼师独有成员变量是职称,学⽣的独有成员变量是学号;学⽣ 的独有成员函数是学习,⽼师的独有成员函数是授课。
class Student
{
public:
// 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证
void identity()
{
// ...
}
// 学习
void study()
{
// ...
}
protected:
string _name = "peter"; // 姓名
string _address; // 地址
string _tel; // 电话
int _age = 18; // 年龄
int _stuid; // 学号
};
class Teacher
{
public:
// 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证
void identity()
{
// ...
}
// 授课
void teaching()
{
//...
}
protected:
string _name = "张三"; // 姓名
int _age = 18; // 年龄
string _address; // 地址
string _tel; // 电话
string _title; // 职称
};
int main()
{
return 0;
}
下⾯我们公共的成员都放到Person类中,Student和teacher都继承Person,就可以复⽤这些成员,就 不需要重复定义了,省去了很多⿇烦。
class Person
{
public:
// 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证
void identity()
{
cout << "void identity()" <<_name<< endl;
}
protected:
string _name = "张三"; // 姓名
string _address; // 地址
string _tel; // 电话
int _age = 18; // 年龄
};
class Student : public Person
{
public:
// 学习
void study()
{
// ...
}
protected:
int _stuid; // 学号
};
class Teacher : public Person
{
public:
// 授课
void teaching()
{
//...
}
protected:
string title; // 职称
};
int main()
{
Student s;
Teacher t;
s.identity();
t.identity();
return 0;
}
2、继承定义
2.1 定义格式
下⾯我们看到Person是基类,也称作⽗类。Student是派⽣类,也称作⼦类。(因为翻译的原因,所以 既叫基类/派⽣类,也叫⽗类/⼦类)
2.2 继承基类成员访问⽅式的变化
1. 基类private成员在派⽣类中⽆论以什么⽅式继承都是不可⻅的。这⾥的不可⻅是指基类的私有成员 还是被继承到了派⽣类对象中,但是语法上限制派⽣类对象不管在类⾥⾯还是类外⾯都不能去访问 它。
虽然我们不能直接使用父类的私有成员,但是我们可以通过父类的成员函数间接使用!
class Person
{
public:
void p_print()
{
cout << _name << endl;
cout << _age << endl;
}
protected:
string _name="张三"; // 姓名
private:
int _age=18; // 年龄
};
class Student : public Person
{
public:
//可以直接在子类里面访问父类的保护成员
void s_print()
{
cout << _name << endl;
}
protected:
int _stunum; // 学号
};
int main()
{
Student s1;
s1.p_print();
return 0;
}
父类的公有成员函数当然也被继承下来了,我们可以通过调用父类的成员函数来访问父类的私有成员!
2. 基类private成员在派⽣类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派⽣类 中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
// 实例演⽰三种继承关系下基类成员的各类型成员访问关系的变化
class Person
{
public:
void p_print()
{
cout << _name << endl;
}
protected:
string _name; // 姓名
private:
int _age; // 年龄
};
class Student : public Person
{
public:
//可以直接在子类里面访问父类的保护成员
void s_print()
{
cout << _name << endl;
}
protected:
int _stunum; // 学号
};
3. 实际上⾯的表格我们进⾏⼀下总结会发现,基类的私有成员在派⽣类都是不可⻅。基类的其他成员 在派⽣类的访问⽅式==Min(成员在基类的访问限定符,继承⽅式)public >protected> private。
4. 使⽤关键字class时默认的继承⽅式是private,使⽤struct时默认的继承⽅式是public,不过最好显 ⽰的写出继承⽅式。
//默认是private继承
class Student : Person
{
public:
//可以直接在子类里面访问父类的保护成员
void s_print()
{
cout << _name << endl;
}
protected:
int _stunum; // 学号
};
//默认是public继承
struct Student : Person
{
public:
//可以直接在子类里面访问父类的保护成员
void s_print()
{
cout << _name << endl;
}
protected:
int _stunum; // 学号
};
5. 在实际运⽤中⼀般使⽤都是public继承,⼏乎很少使⽤protetced/private继承,也不提倡使⽤ protetced/private继承,因为protetced/private继承下来的成员都只能在派⽣类的类⾥⾯使⽤,实 际中扩展维护性不强。
3、继承类模板
当我们继承的父类是类模版时,我们在子类里面需要使用父类的成员函数时一定要指定类域访问,不然编译器就会找不到这个函数。这是按需实例化的缘故,我们在实例化stack对象时,我们只使用了stack和父类的构造函数,所以编译器就只实例化了它们的构造函数。如果我们用stack对象去调用其他接口时,编译器才会按需实例化它们!如果我们不在子类指定类域,编译器就找不到父类的成员函数,然后报错。
namespace my_stack
{
//template<class T>
//class vector
//{};
// stack和vector的关系,既符合is-a,也符合has-a
template<class T>
class stack : public std::vector<T>
{
public:
void push(const T& x)
{
// 基类是类模板时,需要指定⼀下类域,
// 否则编译报错:error C3861: “push_back”: 找不到标识符
// 因为stack<int>实例化时,也实例化vector<int>了
// 但是模版是按需实例化,push_back等成员函数未实例化,所以找不到
vector<T>::push_back(x);
//push_back(x);
}
void pop()
{
vector<T>::pop_back();
}
const T& top()
{
return vector<T>::back();
}
bool empty()
{
return vector<T>::empty();
}
};
}
int main()
{
my_stack::stack<int> st;
st.push(1);
st.push(2);
st.push(3);
while (!st.empty())
{
cout << st.top() << " ";
st.pop();
}
return 0;
}
4、 基类和派⽣类间的转换
• public继承的派⽣类对象可以赋值给基类的指针/基类的引⽤。这⾥有个形象的说法叫切⽚或者切 割。寓意把派⽣类中基类那部分切出来,基类指针或引⽤指向的是派⽣类中切出来的基类那部分。
class Person
{
//protected:
virtual void func()
{}
public:
string _name; // 姓名
string _sex; // 性别
int _age; // 年龄
};
class Student : public Person
{
public:
int _No; // 学号
};
int main()
{
Student sobj;
// 1.子类对象可以赋值给父类对象/指针/引用
Person pobj = sobj;
Person* pp = &sobj;
Person& rp = sobj;
rp._name = "张三";
return 0;
}
需要注意的一点是,子类赋值给父类并不是走的隐式类型转换,因为如果是的话,子类可以赋值给父类的引用就说不通了!在这个过程中,编译器回先用子类对象拷贝构造一个临时对象,这个临时对象具有常性,给普通的引用,权限被放大的了!
事实上!派⽣类对象可以赋值给基类的对象是通过调⽤后⾯会讲解的基类的拷⻉构造完成的
• 基类对象不能赋值给派⽣类对象。(很好理解,子类的对象有的成员,父类对象不一定有!)
• 基类的指针或者引⽤可以通过强制类型转换赋值给派⽣类的指针或者引⽤。但是必须是基类的指针 是指向派⽣类对象时才是安全的。这⾥基类如果是多态类型,可以使用RTTI(RunTimeType Information)的dynamic_cast来进⾏识别后进⾏安全转换。(这个我们后⾯单独专⻔讲解,这⾥先提⼀下)
5、继承中的作⽤域
5.1 隐藏规则:
1. 在继承体系中基类和派⽣类都有独⽴的作⽤域。
2. 派⽣类和基类中有同名成员,派⽣类成员将屏蔽基类对同名成员的直接访问,这种情况叫隐藏。 (在派⽣类成员函数中,可以使⽤基类::基类成员显⽰访问)
3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
4. 注意在实际中在继承体系⾥⾯最好不要定义同名的成员。
// Student的_num和Person的_num构成隐藏关系,可以看出这样代码虽然能跑,但是⾮常容易混淆
class Person
{
protected:
string _name = "⼩李⼦"; // 姓名
int _num = 111; // ⾝份证号
};
class Student : public Person
{
public:
void Print()
{
cout << " 姓名:" << _name << endl;
cout << " ⾝份证号:" << Person::_num << endl;
cout << " 学号:" << _num << endl;
}
protected:
int _num = 999; // 学号
};
int main()
{
Student s1;
s1.Print();
return 0;
};
5.2 考察继承作⽤域相关选择题
class A
{
public:
void fun()
{
cout << "func()" << endl;
}
};
class B : public A
{
public:
void fun(int i)
{
cout << "func(int i)" << i << endl;
}
};
int main()
{
B b;
b.fun(10);
b.fun();
return 0;
};
1、A和B类中的两个func构成什么关系()
A.重载 B. 隐藏 C.没关系
解析:大多数人会被选项中的重载给迷惑,因为这两个同名函数参数不同,不就构成重载吗?但是,我们不要忘了构成函数重载的前提条件(在同一作用域下!)
而我们都知道在类里面定义的成员变量受访问限定符和类域的限制!A类和B类中的同名函数所在类域不同,因此并不构成重载,根据我们上面学习的隐藏规则,我们可以知道它们构成隐藏!
2、上⾯程序的编译运⾏结果是什么()
A.编译报错 B.运⾏报错 C.正常运⾏
解析:第一个fun调用我们都知道肯定是调用到子类的fun,那第二个呢?是不是调用了被继承下来的父类的fun呢?答案是否定的!因为这两个函数构成了隐藏,被继承下来的父类函数必须突破域(指定类域)才可以访问!
答案是编译报错!
突破类域访问!
6、完结散花
好了,这期的分享到这里就结束了~
如果这篇博客对你有帮助的话,可以用你们的小手指点一个免费的赞并收藏起来哟~
如果期待博主下期内容的话,可以点点关注,避免找不到我了呢~
我们下期不见不散~~