一、总结封装
1、从类的角度:把数据方法放到一起,想让你访问的定义为公有,不想访问的定义为私有。
2、把一个类放到另一个类中,通过typedef 成员函数等方式封装出一个新的类。
二、继承相关概念
1、继承理解
继承与模板相似,都是复用。模板解决类型的复用,继承是类成员设计的复用。
2、语法
cpp
class Student : public Person
{...}
Student:要继承的类,叫子类或派生类。
public:继承方式。
Person:被继承的类,叫父类或基类。
3、类成员与继承方式
|--------------|--------------|--------------|------------|
| 类成员 / 继承方式 | public继承 | protected继承 | private继承 |
| 父类的public | 派生类public | 派生类protected | 派生类private |
| 父类的protected | 派生类protected | 派生类protected | 派生类private |
| 父类的private | 派生类不可见 | 派生类不可见 | 派生类不可见 |
不可见:不是没有继承,而是派生类用不了父类的私有成员。
细节
(1)父类 private 在派生类中不可见,但私有是相对的,可以在父类的 get set 函数中获得。
(2)父类其他成员在派生类中的访问方式 == min(访问限定符,继承方式)
(3)protected 成员在派生类中可以使用,类外不能使用,可以看出 protected 因继承出现。
(4)一般用 public 继承
(5)struct 默认共有继承,class 默认私有继承。
4、切片
假设我现在有一个父类 Person(成员变量是name) 和一个子类 Student(成员变量是id)
cpp
int main()
{
Student s;
Person p1 = s;
Proson& p2 = s;
Person* p3 = &s;
return 0;
}
p1就是典型的切片,在public继承中,如果要把子类赋值给父类,那么就是把子类从父类继承的成员变量赋值给父类。这种是赋值兼容,不产生临时对象!
p2, p3就是直接指向子类中父类的成员变量。
不能把父类赋值给子类,因为在子类中有父类没有的成员变量。
5、继承的作用域
|-------|----------------------|
| 域名 | 备注 |
| 全局域 | 编译器默认在里面找数据,影响变量生命周期 |
| 局部域 | 编译器默认在里面找数据,影响变量生命周期 |
| 类域 | 分为父类域和子类域,是两个不同的域 |
| 命名空间域 | 用命名空间包的域,独立 |
结论
(1)父类与子类的作用域是两个不同的域。
(2)父类与子类可以定义同名成员,若直接访问,默认访问子类成员,这就是隐藏或重定义,可以用父类::父类成员的方式访问。
(3)成员函数名相同就构成隐藏。
注意区分隐藏和函数重载:函数重载是在同一个域中两个函数名相同,参数不同的函数,但是隐藏是父类和子类这两个不同域中的概念。
三、派生类中的默认成员函数
1、举例
cpp
class Person
{
public:
//构造函数
Person(const char* name)
: _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(int num, const char* str, const char* name)
:Person(name)
,_num(num)
,_str(str)
{
cout << "Student()" << endl;
}
//拷贝构造,切片
Student(const Student& s)
:Person(s)
,_num(s._num)
,_str(s._str)
{}
//赋值构造
Student& operator=(const Student& s)
{
if (this != &s)
{
Person::operator=(s);
_num = s._num;
_str = s._str;
}
return *this;
}
// 子类的析构也会隐藏父类
// 因为后续多态的需要,析构函数名字会被统一处理成destructor
~Student()
{
//如果显示调用父类析构会造成析构两次
//Person::~Person();
cout << _name << endl;
cout << "~Student()" << endl;
// 注意,为了析构顺序是先子后父,子类析构函数结束后会自动调用父类析构
}
protected:
int _num;
string _str;
};
2、解析
(1)子类构造函数
首先子类被编译器看成父类成员 + 子类成员(内置类型 + 自定义类型)
构造子类成员和普通类一样:内置类型不做处理,自定义类型调用它的默认构造。
构造父类成员:如果父类有默认构造就可以不显示调用,如果父类没有默认构造就要像上面代码一样显示调用父类构造。
(2)子类拷贝构造函数
像上面代码一样调用父类的拷贝构造函数,传参进行切片。
(3)子类赋值构造函数
调用父类赋值构造函数必须加上类域,不然会默认调用子类赋值构造无限递归栈溢出。
(4)子类析构函数
不要显示调用父类析构函数,为了先析构子类,在析构父类,编译器会自动调用父类析构,如果再手动调用会导致一块空间被释放两次报错。
构造函数:先父后子
析构函数:先子后父