继承是面向对象的程序设计能够实现类的代码复用的重要手段,它允许程序员在保留原有类的特性的基础上进行扩展。
我们将共有的数据和方法提取到一个类中,这个类叫做父类/基类;
然后在此基础上用需要扩展的数据和方法产生新的类,并且继承父类,这些类叫做子类/派生类。
代码如下:
class Person
{public:
void print()
{cout<< "name: "<< _name<< endl;
cout<< "age: "<< _age<< endl;
}
protected:
string _name;
int _age;
};
class Student : public Person
{protected:
int _stuid;
};
class Teacher : public Person
{protected:
int _Jobid;
};
int main()
{Student s1;
Teacher t1;
s1.print();
t1.print();
return 0;
}
从以上代码我们可以看出,父类是Person,Student和Teacher两个类都继承了Person,在创建对象后,都能够通过对象访问Person中的公有成员函数。
2.定义在上面的代码Student类中,Student是子类,:后面的public是继承方式,Person是父类。
继承方式与访问限定符一样,都有三种:public、protected和private,继承方式与访问限定符之间的关系如下:
1.基类的private成员无论什么继承方式都是不可见的;
不可见的意思是:隐身,在类作用域里面和类外面都不能访问,例如:
class Person
{public:
void print()
{cout<< "name: "<< _name<< endl;
cout<< "age: "<< _age<< endl;
}
//protected:
private:
string _name;
int _age;
};
class Student : public Person
{public:
void print_stu()
{cout<< _name<< _age<< _stuid<< endl;
}
protected:
int _stuid;
};
Student以public继承了Person类,但是Person类中的成员变量都是private的,即使继承到Student类中,也不能够访问,而且在类的外面也不能访问。
相对而言,protect和private的意思是,在类作用域里面可以访问,在类外面不能访问:
class Person
{public:
void print()
{cout<< "name: "<< _name<< endl;
cout<< "age: "<< _age<< endl;
}
protected:
//private:
string _name;
int _age;
};
class Student : public Person
{public:
void print_stu()
{cout<< _name<< _age<< _stuid<< endl;
}
protected:
int _stuid;
};
将Person中的成员变量限定改为protected,公有继承到Student类中,就能在子类里访问_name和_age了,但是在类外面依旧不能访问。
2.实际上最常用的是以下的两种继承关系:
3.除了父类的私有成员在子类中都是不可见的,其他基类成员在子类中的访问方式 == Min(成员在父类的访问限定符, 继承方式),public >protected >private;
4.使用关键字struct的默认继承方式是public,使用class的默认继承方式是private,最好显式写出继承方式;
class Student1 : Person//默认继承方式是private
{protected:
int _stuid;
};
struct Student2 : Person//默认继承方式是public
{int _stuid;
};
注:struct的默认访问限定符是public,class的默认访问限定符是private;
5.private成员的意义:不想被子类继承的成员,可以设计成私有;
如果父类中想给子类复用,但又不想直接暴露在外面的成员,就设计成protected;
1.在继承中,父类和子类都有独立的作用域;
2.若父类和子类中有同名的成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫做隐藏,也叫重定义。
若想访问父类中的同名成员,可以在前面加上父类的作用域,如 Person::print();
class Person
{public:
void print()
{cout<< "name: "<< _name<< endl;
cout<< "age: "<< _age<< endl;
}
protected:
string _name;
int _age;
int _num = 111;
};
class Student : public Person
{public:
void print_stu()
{cout<< _num<< endl;
cout }
protected:
int _stuid;
int _num = 999;
};
int main()
{Student s1;
s1.print_stu();
return 0;
}
上述代码的运行结果是:
第一行的结果999就是由于子类和父类有同名成员,父类中的就被隐藏了,默认访问的是子类的_num;
第二行的结果111就是在_num前加了Person的作用域限定,访问的就是父类中的成员了。
3.如果是同名的成员函数,只需要函数名相同就构成隐藏,参数不相同也是隐藏,这里需要和函数的重载进行区分;
函数的隐藏是有继承关系的父类和子类中有同名的成员函数,这两个函数有不同的作用域,且只需函数名相同;
函数的重载是在同一作用域下,两个同名且参数不一样的函数,才构成重载;
1.派生类对象可以赋值给基类的对象、基类的指针、基类的引用,是将派生类中基类的那部分切片后,再赋值过去;
2.基类对象不可赋值给派生类对象;
3.基类的指针或引用可以通过强制类型转换赋值给派生类的指针或引用;
int main()
{Student s1;
Teacher t1;
Person p1 = s1;
Person* pp = &s1;
Person& rp = s1;
Student* ps = (Student*)pp;//父类指针可以通过强制类型转换赋值给子类指针
return 0;
}
注:父类的指针指向的是子类中切片的父类的那部分;
例题:以下代码中p1,p2,p3的关系是:
创建了一个子类对象d,父类Base1的指针p1指向d,父类Base2的指针p2指向d,p3是子类Derive的指针;
d对象中包含继承的父类对象Base1和Base2还有_d,p1指向的是d中切片的Base1的那部分,p2指向的是d中切片的Base2的那部分,所以p1 != p2;
p3指向的是子类对象d,指向的是最开始的部分,因此p3 == p1;
正确答案:C
注:
先继承的类放在子类对象的前面部分;
由于栈是从高地址向低地址增长的,而对象是按照一个整体去创建的,因此子类对象的位置如上图所示,类对象在栈里面是倒着放的,因此p2 >p1;
子类默认生成的构造函数会完成如下操作:
1.对于自己特有的成员,内置类型不做处理,自定义类型调用其构造函数;
2.对于继承父类的成员,必须调用父类的构造函数初始化。
如果要显式写构造函数,代码如下:
class Person
{public:
Person(const char* name)
: _name(name)
{cout<< "Person(const char* name)"<< endl;
}
protected:
string _name;
};
class Student : public Person
{public:
Student(const char* name = "Peter", int id = 1000000)
: _name(name)
, _stuid(id)
{cout<< "Student(const char* name, int id)"<< endl;
}
protected:
int _stuid;
};
int main()
{Student s1;
return 0;
}
当子类用初始化列表初始化父类的成员时,上面的写法会报错:
这是因为子类是将父类看作一个整体去初始化的,父类成员必须调用父类的构造函数,不能再子类中单独初始化;
正确方式入下:
class Student : public Person
{public:
Student(const char* name, int id)
: Person(name) //注意这种特殊写法!
, _stuid(id)
{cout<< "Student(const char* name, int id)"<< endl;
}
这里类似于一个匿名对象,但不是,注意这种特殊写法!
2.默认析构函数子类默认生成的析构函数与默认构造函数类似:
1.对于自己特有的成员,内置类型不做处理,自定义类型调用其析构函数;
2.对于继承父类的成员,必须调用父类的析构函数。
如果要显式写析构函数,代码如下:
class Person
{public:
~Person()
{cout<< "~Person()"<< endl;
}
protected:
string _name;
};
class Student : public Person
{public:
~Student()
{~Person(); //在子类的析构中调用父类的析构
}
protected:
int _stuid;
};
上面这种写法时会报错的,因为由于多态的需要,析构函数的名字会被统一处理成destructor(),子类的析构和父类的析构构成了隐藏,只要加上作用域就行了:
~Student()
{Person::~Person();
}
如果子类的析构中显式调用了父类的析构(如上面代码所示),那么会出现以下情况:
父类的析构被调用了两次,重复析构了,造成这种现象的原因是:
每个子类的析构函数后面,都会默认调用父类的析构函数,这样才能保证先析构子类,再析构父类,所以子类的析构函数中不去显式调用父类的析构函数才是正确的。
~Student()
{cout<< "~Student()"<< endl;
}
注:子类构造时,先构造父类,再构造子类的成员;
子类析构时,先析构子类成员,再析构父类。
子类默认拷贝构造会进行如下操作:
1.对于自己特有的成员,内置类型会进行浅拷贝,自定义类型会调用其拷贝构造;
2.对于继承的父类成员,必须调用父类的拷贝构造。
如果子类中显式写拷贝构造,代码如下:
class Person
{public:
Person(const Person& p)
: _name(p._name)
{cout<< "Person(const Person& p)"<< endl;
}
protected:
string _name;
};
class Student : public Person
{public:
Student(const Student& s)
: Person(s) //运用了切片的原理
, _stuid(s._stuid)
{cout<< "Student(const Student& s)"<< endl;
}
protected:
int _stuid;
};
在子类的拷贝构造中,没法将子类中父类的成员直接拿出来进行赋值,这时就将子类直接赋值给父类,构成赋值切片。
4.默认赋值重载函数编译器默认生成的子类赋值重载函数进行的操作如下:
1.对于自己特有的成员,内置类型会进行浅拷贝,自定义类型会调用其赋值重载;
2.对于继承的父类成员,必须调用父类的赋值重载。
如果子类显式写赋值重载,代码如下:
class Person
{public:
Person& operator=(const Person& p)
{if (this != &p)
{ _name = p._name;
}
return *this;
}
protected:
string _name;
};
class Student : public Person
{public:
Student& operator=(const Student& s)
{if (this != &s)
{ operator=(s); //父类的成员调用父类的赋值重载
_stuid = s._stuid;
}
return *this;
}
protected:
int _stuid;
};
上面的写法是会报错的,因为子类的operator=()和继承父类的operator=()构成了隐藏,这时,只要指定了父类的作用域就可以了:
Student& operator=(const Student& s)
{if (this != &s)
{ Person::operator=(s); //父类的成员调用父类的赋值重载
_stuid = s._stuid;
}
return *this;
}
五、继承与友元友元关系不能继承!!!
也就是说,父类的友元函数不能访问子类的私有和保护成员。
父类定义了一个static静态成员,那么整个继承体系中就只有这一个成员,无论派生出多少子类,都只有一个static成员的实例。
七、多继承、菱形继承与虚拟继承 1.单继承:一个子类只有一个父类2.多继承:一个子类有两个或以上直接父类3.菱形继承:
代码如下:
class Person
{protected:
string _name;
};
class Student : public Person
{protected:
int _stuid;
};
class Teacher : public Person
{protected:
int _Jobid;
};
class Assistant :public Student, public Teacher
{protected:
string _majorCourse;
};
菱形继承是由多继承引发的一个问题:
从上图可以看出,菱形继承有数据冗余和二义性的问题,在Assistant对象中Person成员会有两份,而且在Assistant对象访问Person的成员时们也会出现二义性问题,无法明确知道访问的是哪一个成员。
int main()
{Assistant a1;
a1._name = "Jack"; //编译器无法知道访问的是那个父类的_name
a1.Student::_name = "Jack"; //可以通过指定作用域来解决二义性问题,但是数据冗余问题无法解决
a1.Teacher::_name = "Bob";
return 0;
}
4.虚拟继承虚拟继承可以解决菱形继承的数据冗余问题和二义性问题,在Student和Teacher继承Person时,使用虚拟继承,代码如下:
class Person
{protected:
string _name;
};
class Student : virtual public Person //虚拟继承
{protected:
int _stuid;
};
class Teacher : virtual public Person
{protected:
int _Jobid;
};
class Assistant :public Student, public Teacher
{protected:
string _majorCourse;
};
这样在实例化Assistant对象时,对象中只会有一组Person的成员,不会出现数据冗余和二义性问题。
5.虚拟继承解决数据冗余和二义性问题的原理给出一个简化的菱形继承,代码如下:
class A
{public:
int _a;
};
class B : public A
//class B : virtual public A
{public:
int _b;
};
class C : public A
//class C : virtual public A
{public:
int _c;
};
class D : public B, public C
{public:
int _d;
};
int main()
{D d;
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
return 0;
}
下图是菱形继承的对象成员模型,可以看到有数据冗余,A的成员有两份:
如果将B和C对A的继承改成虚拟继承,其对象成员模型如下:
不存在数据冗余了,A的成员只有一份,B和C中原本存放A成员的地方,现在存放了一个地址,叫做虚基表指针,指向的就是虚基表,虚基表中存放的是当前对象与虚继承的父类对象之间的偏移量,通过偏移量来找到A。
在普通继承中不要使用虚继承,因为会开辟虚基表,造成空间浪费。
八、如何定义一个不能被继承的类在类名后加一个关键词:final,表示当前类为最终类,无法被继承。代码如下:
class A final
{public:
int _a;
};
你是否还在寻找稳定的海外服务器提供商?创新互联www.cdcxhl.cn海外机房具备T级流量清洗系统配攻击溯源,准确流量调度确保服务器高可用性,企业级服务器适合批量采购,新人活动首月15元起,快前往官网查看详情吧