概念
先说下c++中的类的特性:封装、继承、多态
封装:将数据和操作数据的函数绑定在一起,同时能设置访问权限,比如类中的所有成员变量都是私有的,这就是封装的意义。
继承:继承允许我们依据另一个类来定义一个类,这使得创建和维护一个应用程序变得更容易。这样做,也达到了重用代码功能和提高执行时间的效果。
多态:多态的方式有两种
多态
多态分为静态多态(函数重载)和动态多态(函数重写(覆盖、虚函数))
静态多态
静态多态最简单的例子 静态多态就是函数重载,函数重载在编译期决定用哪个
int Add(int a ,int b);
int Add(double a,int b);
当调用add函数是就会根据参数的类型来判断用哪个函数;这个实现是在编译的时候编译器根据实参的类型来选择对应的函数。
动态多态
动态多态,就是在程序运行的时候根据基类的指针(引用)的对象来决定到底用哪个类里面的虚函数。
(这么理解,我需要提前定义一个出门对象,根据时间来判断到底乘坐什么工具;如果没有多态我就需要把每一个交通工具都定义过去,最后在根据时间判断调用哪个交通工具函数;多态的效果就是我只需要定义一个出门对象,我根据时间来把创建一个交通工具对象赋给出门对象,最后就只要调用出门函数即可)
#include<iostream>
using namespace std;
class Goout{
public :
virtual void takevehicles(int x)=0;
};
class Bus : public Goout{
public:
virtual void takevehicles(int x){
cout<<"take bus"<<endl;
}
};
class Subway : public Goout{
public:
virtual void takevehicles(int x ){
cout<<"take subway"<<endl;
}
};
int main(){
//定义基类
Goout* go=NULL;
int i=rand();
if(i%2==1){
go=new Bus;
}
else {
go=new Subway;
}
cout<<i<<endl;
go->takevehicles(1);
delete go;
return 0;
}
动态多态的使用条件
如果用了基类的指针指向了派生类的对象,用基类的指针删除派生类的对象的时候就需要用到虚析构函数,要不然会产生内存泄漏
不能处理友元函数 全局函数 静态函数 构造
● 基类中必须包含虚函数,并且派生类中一定要对基类中的虚函数进行重写。
● 通过基类对象的指针或者引用调用虚函数。
虚函数相关内容
- 虚函数指针是在什么时候初始化的?
在构造函数进行虚表的创建和虚表指针的初始化,和构造子类对象时。
- 首先调用父类的构造函数,此时编译器只看到了父类,因此先初始化了父类的虚表指针,使其指向父类的虚函数表。
- 执行子类的构造函数的时候,子类的虚表指针被初始化,只要父类中有虚函数表那么就会在父类的虚表中添加虚函数指针。如果是重写的话,那就回修改虚函数表中原有父类的虚函数指针。
3.重写了之后,那么如果用父类的指针指向子类的对象,当访问重写的虚函数时,就根据指针来判断是访问的父类还是子类的函数。 这就是动态绑定
虚函数时在编译阶段生成的,他一般存放再代码段,也就是常量区。
- 派生类虚表
1.先将基类的虚表中的内容拷贝一份
2.如果派生类对基类中的虚函数进行重写,使用派生类的虚函数替换相同偏移量位置的基类虚函数
3.如果派生类中新增加自己的虚函数,按照其在派生类中的声明次序,放在上述虚函数之后
- 使用多态的缺陷
效率,多态的实现需要搜索虚函数表;空间的浪费。
- 虚函数表和编译器的关系理解?
当类中声明了虚函数是,编译器会在类中生成一个虚函数表VS中存放在代码段,虚函数表实际上就是一个存放虚函数指针的指针数组,是由编译器自动生成并维护的。虚表是属于类的,不属于某个具体的对象,一个类中只需要有一个虚表即可。同一个类中的所有对象使用同一个虚表,为了让每个包含虚表的类的对象都拥有一个虚表指针,编译器在每个对象的头添加了一个指针,用来指向虚表,并且这个指针的值会自动被设置成指向类的虚表,每一个virtaul函数的函数指针存放在虚表中,如果是单继承,先将父类的虚表添加到子类的虚表中,然后子类再添加自己新增的虚函数指针,但是在VS编译器中我们通常看不到新添加的虚函数指针,是编译器故意将他们隐藏起来,如果是多继承,在子类中新添加的虚函数指针会存放在第一个继承父类的虚函数表中。
成员变量、构造函数初始化顺序
- 对于成员变量初始化的时候,和构造函数中初始化成员列表的顺序无关,只和定义成员变量的顺序有关。所以一般构造函数的初始化列表和定义顺序相同。如 A(int a,int b,int c):_a(a),_b(b),_c©{};
- const成员变量和引用成员必须在初始化列表初始化,初始化列表的执行比构造函数体的执行先,因此如果类中存在const成员和引用成员就不能用缺省的构造函数,而且必须初始化列表来初始化。const成员不能直接初始化
class A{
public:
int &a;
int b;
const int c;
A():b(9),a(b),c(1){}//引用和常量成员变量必须要种这种形式来初始化
};
- static成员变量和函数,在类内声明,然后再类外初始化;(不能在类内初始化,因为如果在类内初始化那么就意味着每个类都有对应的静态对象,但是静态成员是属于整个类的,明显矛盾了)为了保证静态成员只会被初始化一次,所以才把初始化放在了类外来做。 但是 静态常量是可以在类内初始化的 static const int a=1; 这样是没有问题的。
- 整体顺序:基类的静态变量和全局变量->派生类的静态变量和全局变量->基类成员变量->派生类成员变量
默认构造函数
- 空类的大小是多少?
1个字节,一个空类不包含任何信息,但是必须在内存空间中占有一点的位置,否则就没法使用这些实例。
- 空类中包含哪些函数?
class empty{
public:
empty();
~empty();
empty(const empty& rhs);//复制构造函数
empty& operator=(const empty& rhs);//赋值运算符
empty* operator&();//取地址运算符
const empty* operator&() const;//const的取地址运算符
};
- 默认拷贝构造函数会出现什么问题?
默认拷贝构造函数是浅拷贝,因此指针会指向同一个内存空间,释放指针的时候会出现问题。
比如类中含有指针指向了一个地址,在默认复制构造函数中,会使得复制后的对象指向同一个地址空间,
如果删除一个对象同时释放空间之后,另一个指针指向的位置就是无效的,那么再次析构的时候就删除了两次对象。
为了避免这种情况出现有两种方式1.重写默认复制构造函数 2.禁用默认复制构造函数,放入private中。
class Rect{
private:
int width;
int height;
int *p;
public:
Rect(){
p=new int(100);
}
~Rect(){
delete p;
}
};
- 虚函数占得空间多大?
在32位机上,占4个字节;但是如果需要计算类的大小的时候就要考虑对齐的问题。
说到对齐问题就可以多说一点,比如一个类
class A{
char a;
double b;
int c;
};//这个类的大小就是24个字节,因为需要进行对齐,a虽然只占了一个字节但是用了8个字节的空间
class A{
char a;
int c;
double b;
};//这个类的大小就是16个字节,为什么比上面少了8个字节呢? 因为可以把a和c都放在一个8字节里。
class A{
char a;
int c;
double b;
virtual void f(){};
};//这个类的大小就是24个字节,因为后面的虚函数要用一个虚指针来指向虚函数表,占了4个字节,因为需要对齐所以耗费了8个字节。
- 为什么构造函数不能有返回值?
如果有返回值,那么每次初始化对象的时候就会返回得到一个返回值,那么赋值的时候就会把返回值传递给等号左边的对象。
class A{
private:
int x_;
public:
A():x_(0){}
A(int x):x_(x){}
};
//假设这时候A初始化有返回值, A():x_(0){return 1;}
A a=A();//此时A()返回了1, A a=1;就变成用1又初始化了对象,这样就混乱了,这样语言就产生了歧义。
如果去理解的话,可以说是A()尝试了一个A的隐藏返回值,因此就不需要其他返回值了。