初识C++ · 继承(1)
对于面向对象这门语言的三大特性 -> 封装 继承 多态,我们已经学习了封装,这里简单理解一下封装,在面向过程的时候,数据和方法(函数)的分离开来的,所以C语言干什么事情都是要自己造轮子,比较麻烦,对于C++ 来说,有了类和对象这个概念,就可以把数据和方法放在一起,那么访问数据就更容易,不需要自己造轮子,这是一种封装,比如不同的数据结构,顺序表链表等,C++有专门的头文件,这也是一种封装,对于反向迭
目录
前言:
对于面向对象这门语言的三大特性 -> 封装 继承 多态,我们已经学习了封装,这里简单理解一下封装,在面向过程的时候,数据和方法(函数)的分离开来的,所以C语言干什么事情都是要自己造轮子,比较麻烦,对于C++ 来说,有了类和对象这个概念,就可以把数据和方法放在一起,那么访问数据就更容易,不需要自己造轮子,这是一种封装,比如不同的数据结构,顺序表链表等,C++有专门的头文件,这也是一种封装,对于反向迭代器来说,是对迭代器的一种封装。
那么今天,就进入到继承这一特点来。
1 继承的概念和定义
继承,顾名思义,从上一代传下来的,比如家中的传家宝可以继承下来给你使用,比如你父亲的财产也可以继承给你,再比如说,某种情况下,私房钱明面上不能继承给你,但是可以间接的继承给你。
在C++中的继承,比如有两个类,他们有着一样的成员变量,比如说人和学生,都有名字,年龄,性别,身高这些概念,我们定义两个类的时候,重复的元素太多,写起来就没那么舒服,那么使用继承,即学生有人的特点,也有属于自己的特点,比如学号等等,就不需要重复定义许多东西了。
继承的定义如下:
class Person
{
public:
private:
};
class student : public Person
{
public:
private:
};
定义的格式就是class 类名 :继承方式 类名.
其中继承方式一共有3种,public protected private。
在类和对象章节protected private是没有什么区别的,在继承这里,就有区别了,我们先看不同的继承方式对于访问权限的区别:
不难发现一个规律就是 两两权限继承之后,权限都是变成两两中权限小的那个,比如public 继承基类 ,也就是父类中的protected成员后,权限变成了protected,这里protected 和 private的区别就出来了:
权限大小 public > protected > private。
但是问题来了,protected private的成员都访问不了,继承下来有什么用处呢?实际上protected的成员变量我们可以间接的访问:
class Person
{
public:
void Func()
{
cout << _num << endl;
}
protected:
int _num = 18;
private:
string _name = "zhangsan";
};
class student : public Person
{
public:
private:
int _id = 232323;
};
int main()
{
student s1;
s1.Func();
return 0;
}
比如这里的打印就是一种间接的访问,通过基类继承下来的成员函数等,进行修改访问打印都是没有问题的。
C++的基础是C语言,那么C语言常用的是struct,C++里面可不可以使用struct来继承呢?
答案是可以,但是这里的继承默认是public,使用class默认的是private继承:
struct St
{
void Func()
{
cout << "Func()" << endl;
cout << _num << endl;
}
int _num = 0;
string _name = "xx";
int _age = 18;
string address = "earth";
};
struct Ss : St
{
void Func()
{
cout << _age << endl;
}
};
2 基类与子类的赋值转换
对于内置类型来说,直接赋值是没有问题的,对于自定义类型来说,直接赋值一般也是没有问题的,大不了就是涉及到拷贝临时对象然后赋值而已:
int a = 1;
int b = a;
string s1 = "xxxx";
那么基类和子类直接能否赋值呢?答案是可以,但是也不完全可以。
子类可以赋值给基类,但是基类不能赋值给子类,这里引入一个切片的概念,假如基类有5个成员变量,继承给子类后,子类加上自己的成员变量,就有7个成员变量了,那么子类赋值过去的时候,就会把自己的成员变量给切掉,赋值的部分是继承的部分,那么相反的,如果是基类赋值给子类,怎么赋值?子类有那么多成员变量,基类没办法赋值:
class Person
{
public:
protected:
int _num = 18;
private:
string _name = "zhangsan";
};
class student : public Person
{
public:
private:
int _id = 232323;
};
int main()
{
student s1("aaa");
Person p1;
p1 = s1;
return 0;
}
3 继承中的作用域
截止到现在,我们学习了局部域 全局域 类域 命名空间域,加上今天继承中的作用域,就有5个域了。
当局部域和全局域中都有一个整型a的时候,打印往往是打印的局部域中的a,那么同理,如果基类和子类都有一个相同名字的变量,打印的往往是最近的那个,比如:
class Person
{
public:
protected:
int _num = 18;
};
class student : public Person
{
public:
void Func()
{
cout << _num << endl;
}
private:
int _id = 232323;
int _num = 0;
};
int main()
{
student s1;
s1.Func();
}
这段代码的打印结果就是0,那么我们就是想要打印基类中的_num怎么办呢?这时候就需要域名访问限定符:
class Person
{
public:
protected:
int _num = 18;
};
class student : public Person
{
public:
void Func()
{
cout << Person::_num << endl;
}
private:
int _id = 232323;
int _num = 0;
};
这时候的打印结果就是18。
这种现象叫做隐藏,因为两个变量名是一样的,但是继承下来之后,基类的就被隐藏了,我们需要加点手段才能访问得到。
class A
{
public:
void fun()
{
cout << "func()" << endl;
}
};
class B : public A
{
public:
void fun(int i)
{
A::fun();
cout << "func(int i)->" <<i<<endl;
}
};
函数也是同理可得。
4 派生类的默认成员函数
默认成员函数有6个,取地址重载那两个不用管,我们需要注意构造函数,析构函数,拷贝构造函数以及赋值重载。
4.1 构造函数
class Person
{
public:
Person(const char* name)
:_name(name)
{}
private:
string _name = "zhangsan";
};
class student : public Person
{
public:
student()
:_id(1)
{}
private:
int _id = 232323;
};
类的构造函数对于自定义类型来说会调用它自己的构造函数,那么student继承了Person,Person相当于在student里面了,所以我们应该把student看成两部分,一部分是student,一部分是Person,那么构造函数调用的时候,就需要初始化两部分,一部分是student自己的成员变量,一部分是Person的成员变量。
按照上面的写法,是有问题的,student的对象创建好了后,_id的初始化没问题,但是Person的初始化就有问题了,因为没有默认构造函数,name是什么编译器也不知道,就会报错,那么如果加上:
class Person
{
public:
Person(const char* name = "hhh")
:_name(name)
{}
private:
string _name = "zhangsan";
};
就不会报错了。
那么问题来了,如果我们不想提供默认构造函数怎么办,就是想要传参,这时候需要用到student的初始化列表了:
class Person
{
public:
Person(const char* name)
:_name(name)
{}
private:
string _name = "zhangsan";
};
class student : public Person
{
public:
Person(const char* name)
:Person(name)
,_id(1)
{}
private:
int _id = 232323;
};
这里的语法看起来有点怪?
我们可以这样理解,构造的时候,调用的有两个构造函数,一个是子类自己的,一个是基类的,但是基类的如果没有默认的拷贝构造函数,我们就需要自己显式的去调用基类的构造函数。
4.2 拷贝构造
拷贝构造和构造函数一样,都要为基类考虑,这里有个问题就是,应该怎么调用它自己的拷贝构造?
class student : public Person
{
public:
student(const char* name)
:_id(1)
,Person(name)
{}
student(const student& s)
:_id(s._id)
,Person(s)
{}
private:
int _id = 232323;
};
就直接传s就可以了,语法稍稍有点怪,但不大。
4.3 赋值重载
赋值重载这里,我们要考虑的是如何调用基类函数的赋值重载:
student& operator=(const student& s)
{
if (this != &s)
{
operator=(s);
_id = s._id;
}
return *this;
}
如果这样调用,就会栈溢出,基类和子类的赋值重载的函数名一样,那么就构成了隐藏关系,所以需要我们显式的调用基类的赋值重载函数:
student& operator=(const student& s)
{
if (this != &s)
{
Person::operator=(s);
_id = s._id;
}
return *this;
}
4.4 析构函数
析构函数在这里就是很有说法的了,如果我们显式的去调用析构函数,就无法满足析构函数的先子后父原则。
这里引入两个原则,构造是先父后子,析构是先子后父,这也好理解,构造方面,如果基类都没有先构造好怎么继承给子类?
析构函数同理,如果我们先显式的调用了基类的析构函数,就无法满足先子后父了,所以如果加上打印观察的话,就会发生析构了两次的情况出现,如果碰上了动态开辟,就会析构两次从而导致程序挂掉。
那么这里,我们写析构函数只需要:
~student()
{
// 显示写无法先子后父
//Person::~Person();
cout << "~Student()" << endl;
// 注意,为了析构顺序是先子后父,子类析构函数结束后会自动调用父类析构
}
只调用子类的析构就好了,基类的析构会自己调用的。
实际上,子类的析构和基类的析构构成了隐藏关系,这里先了解,因为都是析构,编译器对函数名继续特殊处理,使析构函数的名字都变成destructor。
感谢阅读!
更多推荐
所有评论(0)