继承
子类拥有父类所有的共性并且拥有自己的特性。父类又叫基类,子类又叫派生类。
小例子(车)
比如汽车类作为父类,那么轿车和卡车就可以作为子类。轿车和卡车就具有父类所有的共性(比如:四个轮胎,四个车灯,四个车座),也具有自己的特性(小轿车的体积小,卡车的体积大),在用函数实现的时候,需要在轿车类中将三个共性和一个特性分别都实现一遍,卡车也是同样的也需要将共性的内容和特性的内容进行实现。这样就会十分麻烦,可以将汽车类作为父类进行实现,然后子类就不需要再一个一个的实现共性内容了。
继承的优点:减少重复的代码,提高代码的复用性。
有关car例子:
class car
{
public:
void tire()
{
cout << "有四个轮胎" << endl;
}
void lights()
{
cout << "有四个车灯" << endl;
}
void seat()
{
cout << "有四个车座" << endl;
}
protected:
void price()
{
cout << "平价车" << endl;
}
};
class truck: public car
{
public:
void volume()
{
cout << "体积小" << endl;
}
};
class saloon: public car
{
public:
void volume()
{
cout << "体积大" << endl;
}
};
int main()
{
truck truck;
truck.lights();//四个车灯
saloon saloon;
saloon.lights();//四个车灯
return 0;
}
继承代码的格式:
class 子类 : 继承方式 父类
注意在类后面写冒号:是继承;在构造函数后面写冒号是为了初始化列表。
比如:
class Person
{
public:
Person(int num) :tel(num) {};
int tel;
};
int main()
{
Person p1(110);
cout << p1.tel << endl;//110
}
//初始化列表就是在创建类中的有参构造的时候将传进去的参数赋给属性的语句直接写在有参构造的后面而不是函数体中,函数体{}为空。
注意下区分就好,一个是为了创建类,一个是为了创建对象。
三种继承方式
共又三种继承方式,public,private,protected。
父类中的private权限的属性在子类中都变成不可访问。父类中的public和protected权限的属性是随着继承方式的不同而不同的,如果是继承方式是public,那么保持原样,如果继承方式是protected,那么子类中的属性就会变成protected(若权限变小,就按小的来设定),以此类推。
注意,在子类中的时候是可以对父类的public属性和protected属性进行修改赋值的,但是不能对private的属性进行赋值初始化。若采用的是public继承方式,子类创建的对象是可以对public属性进行修改,但是无法对protected和private属性进行修改赋值的。采用的如果是private或者protected继承方式,那么子类创建的对象都是无法进行访问赋值的
。
private和protected的区别就是:父类中的private属性不允许子类访问修改,而protected属性允许子类访问修改。
对于父类中的private属性和protected属性,子类创建的对象都是不允许访问的。(因为子类继承过来的属性变成了这两种权限,子类创建的对象是不允许访问类中的private和protected属性的)
继承中的对象模型的查看(借助工具)
父类中的私有属性子类访问不到,但是子类依然会继承(编译器会隐藏)。对子类进行sizeof的时候需要把所有的属性都要考虑到。
可以通过工具查看类内部的对象的,如何查看对象模型呢?在Visual Studio 2022文件中找Tools文件夹中的Developer Command Prompt for VS 2022
,找到你的项目保存的盘的,然后cd 输入你项目的路径(路径可以从编译器的打开文件上面的位置获取)。dir命令可以查看目录。
class Base1
{
public:
int m_A;
protected:
int m_B;
private:
int m_C;
};
class son1 : public Base1
{
public:
int m_D;
};
上方代码是我项目中的test.cpp文件中的代码。
在工具中输入dir
显示:022/02/14 20:32 366 test.cpp
显示有这个文件
然后输入:cl /d1 reportSingleClassLayoutson1 test.cpp
cl /d1 报告-单个-类-的布局-类的名字 隶属的文件的名字
注意report的首字母小写,后面单词的首字符大写,类的名字按照自己规定的来。(字母l和数字1要分清楚)
class son1 size(16):
±–
0 | ±-- (base class Base1)
0 | | m_A
4 | | m_B
8 | | m_C
| ±–
12 | m_D
±–
这样就会显示这个类的大小是16个字节。而且可以显示A,B,C都是从父类Base1中继承下来的。
继承中的构造和析构
class Base1
{
public :
Base1()
{
cout << "Base1的构造函数调用"<<endl;
}
~Base1()
{
cout << "Base1的析构函数就调用" << endl;
}
};
class Son1 :public Base1
{
public:
Son1()
{
cout << "Son1的构造函数调用" << endl;
}
~Son1()
{
cout << "Son1的析构函数就调用" << endl;
}
};
int main()
{
Base1 base;
Son1 son;
return 0;
}
如果创建一个子类,那么会先调用父类的构造,然后再调用自己的构造,接着调用自己的析构,最后调用父类的析构。
如果子类中有一个其他类的对象,我们知道会先调用其他类的构造和析构,但是如果和父类相比,是先调用其他类的构造还是先调用父类的构造?应该是先有父类构造,然后再调用其他成员的构造,最后调用自己类的构造。
class Base1
{
public :
Base1()
{
cout << "Base1的构造函数调用"<<endl;
}
~Base1()
{
cout << "Base1的析构函数就调用" << endl;
}
};
class Other
{
public:
Other()
{
cout << "Other的构造函数调用" << endl;
}
~Other()
{
cout << "Other的析构函数就调用" << endl;
}
};
class Son1 :public Base1
{
public:
Son1()
{
cout << "Son1的构造函数调用" << endl;
}
~Son1()
{
cout << "Son1的析构函数就调用" << endl;
}
Other other;
};
int main()
{
//Base1 base;
Son1 son;
return 0;
}
打印的结果:
Base1的构造函数调用
Other的构造函数调用
Son1的构造函数调用
Son1的析构函数就调用
Other的析构函数就调用
Base1的析构函数就调用
如果父类中的构造函数不是默认构造(无参构造),而是有参构造,那么子类就无法调用父类的默认构造(会报错)。可以为父类提供一个默认构造,还有一种方法就是
class Base1
{
public :
Base1(int a)
{
cout << "Base1的构造函数调用"<<endl;
}
};
class Son1 :public Base1
{
public:
Son1() :Base1(1)
{
cout << "Son1的构造函数调用" << endl;
}
//也可以Son1(int a):Base1(a){};以后创建对象就是:Son1 son(100);传一个参数到父类,如果父类中正好是要用a来给自己的属性赋值,那么就可以用子类提供的这个。也可以写成:Son1(int a = 100):Base1(a){}//直接赋值给父类做初始化
//构造函数后面+冒号:是初始化列表。只不过不是给自己的属性赋值,而是给父类的有参构造赋值。
};
父类中的构造,析构,拷贝构造,operator=是不会被子类继承下去的。
因为父类创建自己的对象肯定是自己来为属性赋值,子类没有权限访问到一些属性。
继承中的同名成员处理
成员属性
class Base
{
public:
Base()
{
this->m_A = 10;
}
int m_A;
};
class son :public Base
{
public:
//m_A = 100;
};
int main()
{
son son1;
cout << son1.m_A << endl;//10
return 0;
}
输出的结果是10,因为子类创建对象会调用父类的默认构造,然后将m_A赋值10,然后子类同时也将父类中的属性m_A继承下来了,值就是10,那么使用子类创建的对象来访问m_A,值就是10.
如果子类中也有个属性m_A的值是100,那么就会输出100.
但是怎么访问父类中的同名的成员?可以利用作用域来访问父类中的同名成员。
cout << son1.Base::m_A << endl;
在.点后面加类范围。
成员函数
class Base
{
public:
void func()
{
cout << "Base中的func调用" << endl;
}
};
class son :public Base
{
public:
void func()
{
cout << "son中的func调用" << endl;
}
};
int main()
{
son son1;
son1.func();//son中的func调用
son1.Base::func();//Base中的func调用
return 0;
}
跟成员属性是同样的道理。
注意:如果父类中有个void func(int a)
函数,在main函数中也不能通过子类对象传参数调用,因为当子类重新定义了父类中的同名函数以后,子类的成员函数会隐藏父类的所有重载版本的同名函数,但是可以利用作用域显示指定调用。
静态成员
静态成员的特点:
1,所有的对象都共享同一个数据。
2,类内声明,类外需要初始化。(注意类外初始化的时候要注意将类的范围写上)
静态成员变量
class Base
{
public:
static int m_A;
};
int Base::m_A = 10;
class Son:public Base
{
public:
static int m_A;
};
int Son::m_A = 20;
int main()
{
Son son;
cout << son.Base::m_A << endl;//10
return 0;
}
访问父类中的静态成员变量和自己类的静态成员变量的方法和上面的是一样的。(通过对象访问)
访问静态变量有两种方法,一种就是通过对象访问,另一种是通过类访问。后者不需要创建对象。
cout << Son::m_A << endl; cout << Base::m_A << endl;
``cout << Son::Base::m_A << endl;`第一个::代表通过类名的方法来访问,第二个代表范围(父类作用域下)。
静态成员函数
静态函数特点:
1,函数中没有指针
2,函数不能访问非静态的成员变量。
class Base
{
public:
static int m_A;
static void func()
{
cout << "调用Base中的func函数" << endl;
}
};
int Base::m_A = 10;
class Son:public Base
{
public:
static void func()
{
cout << "调用Son中的func函数" << endl;
}
static int m_A;
};
int Son::m_A = 20;
int main()
{
Son son;
son.func();
Son::func();
son.Base::func();
Base::func();
return 0;
}
静态成员函数和静态成员变量是相同的。(也是有两种方法访问静态成员函数)
也是可以用:Son::Base::func();
来调用父类中的函数
如果父类中有重载的静态成员函数,那么通过子类创建的对象也是无法访问的,因为如果子类中有相同名字的静态成员函数,那么就会将父类中的隐藏,如果想调用父类的函数,那么就需要写范围。
多继承
一个子类可以拥有多个父类
class A:public B,public B2
;
class Base1
{
public:
Base1()
{
this->m_A = 10;
}
int m_A;
};
class Base2
{
public:
Base2()
{
this->m_B = 20;
}
int m_B;
};
class Son:public Base1,public Base2
{
public:
int m_C;
int m_D;
};
int main()
{
cout << sizeof(Son) << endl;//大小是16。
Son s;
cout << s.m_A << endl;//10
return 0;
}
如果Base1中有和Base2一样的成员属性,m_A,那么访问的时候就会报错,不明确。解决方法就是加作用域。
这个也可以用上面的借助工具的方法来看看Son类中的结构。
菱形继承问题
动物作为父类,羊,驼作为子类,继承了动物的成员属性:年龄。然后羊驼作为羊和驼的共同子类,会继承它们两个的成员。这时候就发现羊驼就继承了两个年龄成员变量,而它只需一个,出现了浪费的问题,这就是菱形继承问题。
class Animal
{
public:
int m_age;
};
class Sheep:public Animal
{
};
class Tuo:public Animal
{
};
class Yangtuo:public Sheep,public Tuo
{
};
int main()
{
Yangtuo son;
cout << son.m_age << endl;//这里会出错
return 0;
}
因为不明确,但是可以解决,直接加对应的作用域即可。但是只需要一个啊,有两个age那么羊驼究竟是几岁呢?
可以利用虚继承的操作来解决这样的菱形继承问题。在继承的冒号后面加关键字virtual
class Sheep: virtual public Animal
{
};
class Tuo: virtual public Animal
{
};
这样Animal这个父类就叫做虚基类。
现在再进行
int main()
{
Yangtuo son;
son.Sheep::m_age = 10;
son.Tuo::m_age = 20;
cout << son.m_age << endl;
cout << son.Sheep::m_age << endl;
cout << son.Tuo::m_age << endl;
return 0;
}
这样的操作的结果是返回3个20.加了虚继承以后age就只有一个份了。
可以通过工具看看类中的结构:
由原先的8个字节变成了12个字节。
vbptr是什么:v代表virtual(虚拟),b代表base(基类),ptr代表pointer(指针),连起来就是虚基类指针。这个指针会指向vbtable虚基类表,在继承的Sheep中有个虚基类指针,它会指向它自己的虚基类表,指针是0,然后加上表中的8这个偏移量就可以找到唯一age的数据。Tuo中也有个虚基类指针,它指向Tuo的虚基类表,它的偏移量是4,4+4后也可以找到唯一age的数据。
(总结:现在的age只有一份了,我们继承的这两个东西不是继承的age,而是继承的两个指针,指针指向的是一个表,表中有偏移量,通过指针和偏移量就可以找到这个唯一的age)(这个age就是从animal中拿过来的,继承了爷爷的age,继承了父类的指针)
内部的工作原理:
创建一个羊驼对象,给这个羊驼对象赋值。然后通过sheep的虚基类指针来找到羊类的偏移量。
首先对对象取地址,找到首地址(是sheep 的指针)。然后强制转换为int*类型(目的就是规定步长),规定步长为4个字节,然后再加 *解引用,这样就找到了sheep的虚基表了。
int main()
{
Yangtuo son;
son.m_age = 10;
*(int*)&son;//解引用到了虚基表
return 0;
}
这个虚继表其实是一个数组,步长也是4个字节,还需要进行强转为int *然后+1,代表从0跳到了1。然后再解引用就出来8了(偏移量)
*((int*)*(int*)&son + 1)//直接输出即可
通过Tuo虚基类指针来找偏移量
cout <<*((int *) * ((int*)&son + 1)+1) << endl;//4
也可通过偏移量来访问m_age
cout << ((Animal*)((char*)&son + *((int*)*(int*)&son + 1)))->m_age << endl;
解释:已经选出偏移量是8了,然后将对象的地址强转成char*类型,步长就变成了1,然后加上偏移量,这个时候就找到了Aniaml的类,(见上图),然后对它进行强转,就变成了Animal的指针,就可以访问里面的属性了。
也可以不转成Animal类型,因为已经找到了Animal 的首地址,而首地址正好是age,所以直接强转成int*,然后再解引用即可。
cout << *((int*)((char*)&son + *((int*)*(int*)&son + 1)))<< endl;
两种方法都可以。