目录
在生活中,子女继承父母的财产这是一个很正常的社会现象,在C++的语法中也有继承者一概念,今天我们就来学习C++中第二个特性------继承。
继承的概念
有这样一个场景,有学生和老师两个实体,学生有姓名和年龄,老师也有姓名和年龄,如果我们创建了两个类来表示学生和老师两个实体,那么在这两个类中,年龄和姓名就会被定义两次,这样就会导致冗余。我们可以先定一个person实体类,这个类中定义年龄和姓名两个成员变量,让学生类和老师类去继承person类,这样就相当于我们只创建了一次年龄和姓名成员变量,不会造成代码的冗余。所以我们就可以给出继承的概念。
继承是C++中一个代码服用的机制,允许程序员在原有类的基础上对类进行拓展,增加相应的功能,基于此产生的类我们称之为派生类(子类),原有类我们称之为基类(父类)。
继承的定义
继承的方式和访问限定符
继承的方式有三种:public protected private
访问限定符有三种:public protected private
所以如果两两组合,总共有9中组合,图示如下。
总结:
1.无论是对于访问限定符还是继承方式,权限大小从大到小依次为:public>protected>private。所以无论派生类以哪种方式继承,基类中的成员在派生类中都会向着权限最小的靠近。但是基类中的私有成员不管派生类以哪种方式继承,在派生类中都是不可见的。
2.struct创建的类中,如果没有指定访问限定符,默认为public,而class定义的类默认为private。
3.派生类不管以何种方式继承,子类中的所有的成员变量和成员函数都会被继承下来,总是基类的private修饰的成员变量和成员函数不可见,但是仍然是被继承了下来的。所以基类的private成员变量无论是哪种方式继承到了派生类,因为其不可见,所以是无法被其孙子类继承的,但是protected成员被继承到了派生类中变成了派生类的protected成员,这些成员是可以被其孙子类继承的。
基类派生类赋值转换
我们把子类对象传递给父类对象或者父类的引用,子类对象的地址传递给父类的指针,我们把这一现象称为切割或切片,即把子类从父类继承过来的成员传递给父类。
代码如下。
class A
{
public:
int A;
};
class B : public A
{
public:
int B;
};
int main()
{
A a;
B b;
a = b;
A* pb = &b;
A& a = b;
return 0;
}
能不能反过来呢?父类能不能传给子类呢?
答案是不行的,因为会导致越界的问题,图示如下。
但是可以将父类对象的地址强转成子类的指针类型传递给子类的指针。这其实也很好理解,因为最终访问时也访问的是父类对象,并不是子类对象,所以并不会导致越界的问题。
继承的作用域
还是上述A B两个类,我们在B中添加了与基类A中相等的成员变量A。
class A
{
public:
int _A=10;
};
class B : public A
{
public:
int _A=20;
int _B;
};
int main()
{
B b;
cout << b._A << " " << endl;
return 0;
}
我们此时如果直接访问B类中的A变量,那么此时访问的究竟是B变量中本身的成员变量还是从A中继承下来的成员变量A呢?运行代码进行试验。
我们发现打印的结果是20。也就是说直接访问的是派生类中的A变量(也就是就近原则)。 那么如和访问从基类中继承下来的_A变量呢?
我们可以直接加上类作用域限定符。代码如下。
为什么无法直接访问从基类中继承下来的_A变量,这是因为派生类中的定义的_A变量对从基类继承下的的_A变量构成了隐藏,我们也称之为重定义。 除此之外,函数名相同也构成隐藏。但是最好不要在类中定义同名的成员变量和成员函数。
派生类的默认成员函数
我们知道当我们不写某些成员函数时编译器是会默认生成默认成员函数。比如类似的构造函数,析构函数,拷贝构造函数,赋值运算符重载函数。那么当我们不去定义派生类的成员函数,而是由编译器自动生成这些默认成员函数,那么派生类的这些由编译器生成的默认成员函数会实现什么功能呢?代码如下。
class Person
{
public:
Person()
{
cout << "Person()" << endl;
}
Person(const Person& p)
{
_name = p._name;
cout << "Person(const Person& p)" << endl;
}
Person& operator=(const Person& p)
{
_name = p._name;
cout << "operator= " << endl;
return *this;
}
~Person()
{
cout << "~Person() " << endl;
}
public:
string _name;
};
class Student:public Person
{
public:
int _id;
};
int main()
{
Student s1;
Student s2;
Student s3(s2);
s1 = s2;
return 0;
}
运行结果如下。
总结:编译器生成派生类的默认构造函数: 对于派生类的成员,对内置类型不做处理,对自定义类型调用其默认的的构造函数,对于从基类继承下来的成员调用基类的默认构造函数进行处理。
编译器生成派生类的拷贝构造函数:对于派生类的成员,对内置类型进行字节序的值拷贝,对自定义类型调用其拷贝构造函数,对于从基类继承下来的成员调用基类拷贝构造函数进行处理。
编译器生成派生类的赋值运算符重载函数:对于派生类的成员,对于内置类型进行字节序的值拷贝,对于自定义类型调用其赋值运算符重载函数,对于从基类继承下来的成员调用基类的赋值运算符重载函数进行处理。
编译器生成派生类的析构函数:对于派生类的内置类型不做处理,自定义类型调用其析构函数进行处理,对于从基类继承下来的成员调用基类的析构函数进行处理。
自己实现默认成员函数,代码如下。
class Person
{
public:
Person()
{
cout << "Person()" << endl;
}
Person(const char* name)
{
_name = name;
}
Person(const Person& p)
{
_name = p._name;
cout << "Person(const Person& p)" << endl;
}
Person& operator=(const Person& p)
{
_name = p._name;
cout << "operator= " << endl;
return *this;
}
~Person()
{
cout << "~Person() " << endl;
}
public:
string _name;
};
class Student:public Person
{
public:
//构造函数,对于从父类继承下来的成员调用父类的构造函数,这里就相当于创建了一个匿名对象,从而调用了父类的构造函数
Student(const char* name,int id)
:Person(name)
{
_id = id;
}
//拷贝构造函数
Student(const Student& s)
:Person(s)
{
_id = s._id;
}
//赋值运算符重载
Student& operator=(const Student& s)
{
//不能自己给自己赋值
if (this != &s)
{
//赋值运算符重载函数构成了隐藏
Person::operator =(s);
_id = s._id;
}
return *this;
}
//析构函数
~Student()
{
//析构函数不用显示调用父类的析构函数,因为编译器会自动调用
cout << "~Student()" << endl;
}
public:
int _id;
};
int main()
{
Student s1("张三", 110);
Student s2(s1);
Student s3("李四", 119);
s2 = s3;
return 0;
}
运行结果如下。
运行结果符合预期,需要注意的是除过基类的析构函数编译器可以自动调用之外,其它的基类的默认成员函数需要我们手动调用。
继承与友元
直接给出结论:友元关系不可以被继承,即基类的友元不能访问子类的私有成员。定义func函数为基类Person的友元函数。
访问时,会编译报错,无法访问派生类的私有成员。
继承与static成员
代码如下。
class A
{
public:
A()
{
_num++;
}
public:
int _A;
static int _num;
};
int A::_num = 0;
class B : public A
{
public:
int _B ;
};
class C :public B
{
public :
int _C;
};
int main()
{
A a;
B b;
C c;
cout << &(a._num) << endl;
cout << &(b._num) << endl;
cout << &(c._num) << endl;
return 0;
}
运行结果如下。
由运行结果可知,三个对象中的静态成员变量_num的地址是相同的。
本期内容到此结束^_^