目录
一、封装
封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。用类将对象的属性和方法结合在一块,让对象更加完善,通过访问权限(private,public),选择性的将其接口提供给外部的用户使用。
该隐藏的数据私有化,该公开的数据设计为公有的接口。这样做的好处是为了更好地分工合作,有助于数据的安全性和使用的方便性,也防止不必要的扩展。
二、继承(inherite)
1.继承定义
继承(inheritance)机制是面向对象程序设计使代码可以复用的重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称为派生类。
继承实现了代码的复用,复用的实现是在已有代码的基础上进行扩展。继承发生在类与类之间。只有符合A is a B的情形,A与B就可以存在继承关系。
语法实现
class A{...};
class B:继承方式 A{/*新增的内容*/}; //B类中会包含A类中的所有内容
在继承当中,父类也称为基类,子类是由基类派生而来的,所以子类又称为派生类。
继承方式包括:
公有继承 ----- class B:public A{...};
保护继承 ----- class B:protected A{...};
私有继承 ----- class B:private A{...};
注意:如果不写继承方式,默认是私有继承。
继承方式影响的是父类成员在子类中的访问权限。
①公有继承
父类的公有成员在子类中仍然是公有的
父类中的保护成员在子类中仍然是保护的
父类的私有成员在子类中是隐藏的(不可访问的)
②保护继承
父类的公有成员在子类中变为保护的
父类中的保护成员在子类中仍然是保护的
父类的私有成员在子类中是隐藏的(不可访问)
③私有继承
父类的公有成员在子类中变为私有的
父类中的保护成员在子类中变为私有的
父类的私有成员在子类中是隐藏的(不可访问的)
在基类当中的访问方式为public或protected的成员,在派生类当中的访问方式变为:Min(成员在基类的访问方式,继承方式)。注意:父类成员在子类中的访问权限只会收缩不会扩大,在子类中的访问全不会超过继承方式。
在基类当中的访问方式为private的成员,在派生类当中都是隐藏的(不可访问的)。基类的私有成员虽然被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。
因此,基类的private成员在派生类中是不能被访问的,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就需要定义为protected,由此可以看出,protected限定符是因继承才出现的。
所谓继承方式就是父类成员能够提供给子类的最大访问权限,实际权限小于等于继承方式,私有数据在子类中总是隐藏的(隐藏不代表不存在)。
注意: 在实际运用中一般使用的都是public继承,几乎很少使用protected和private继承,也不提倡使用protected和private继承,因为使用protected和private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。
2.继承中的构造函数和析构函数
构造子类时,会自动调用父类的构造函数;析构子类时,自动调用父类的析构函数。调用构造函数和析构函数的顺序是相反的,先构造父类再构造子类,先析构子类再析构父类。
继承父类的数据由父类构造和析构,而子类新增的数据由子类构造析构。
子类默认调用时父类的无参构造函数,如果需要给父类构造函数传参,可以通过子类构造函数的初始化参数列表传参。在子类构造函数形参列表之后,函数体之前使用":父类(参数1,参数2)"向父类传递参数。
class A{
public:
A(){cout<<"A()"<<endl;}
A(int a,int b){cout<<"A(int,int)"<<endl;}
~A(){cout<<"~A()"<<endl;}
private:
int x;
int y;
};
class B:public A{
public:
B(){cout<<"B()"<<endl;}
B(int a,int b,int c):A(a,b)/*给父类的构造函数传参*/,z(c){cout<<"B(int,int,int)"<<endl;};
~B(){cout<<"~B()"<<endl;}
private:
int z;
};
3.继承中的拷贝构造
子类使用默认的拷贝构造,会自动调用父类的拷贝构造。但是重写子类的拷贝构造,默认不调用父类的拷贝构造,需要在子类的拷贝构造函数中使用初始化参数列表去调用父类的拷贝构造函数,使用子类的引用去代替父类的引用作为参数。
class A{
public:
A()
{
cout<<"A()"<<endl;
this->pdata = new char[10];
memset(this->pdata, 0, 10);
}
//拷贝构造
A(const A &a)
{
cout<<"A(const A &a)"<<endl;
this->pdata = new char[10];
memcpy(this->pdata,a.pdata,10);
}
~A()
{
cout<<"~A()"<<endl;
delete[] this->pdata;
}
private:
char *pdata;
};
class B:public A{
public:
B()
{
cout<<"B()"<<endl;
this->abc = new char[10];
memset(this->abc, 0, 10);
}
//子类拷贝构造 --- 通过初始化参数列表调用父类的拷贝构造
B(const B &b):A(b)
{
cout<<"B(const B &b)"<<endl;
this->abc = new char[10];
memcpy(this->abc,b.abc,10);
}
~B()
{
cout<<"~B()"<<endl;
delete[] this->abc;
}
private:
char *abc;
};
4.名字隐藏
如果子类中出现了和父类重名的成员,那么在子类中父类的同名成员将被隐藏。如果想访问父类中被隐藏的成员,可以使用父类名+作用域符来访问
父类名::隐藏成员;
5.多重继承
继承方式有单继承和多继承。
单继承:一个子类只有一个直接父类时称这个继承关系为单继承。
多继承:一个子类有两个或两个以上直接父类时称这个继承关系为多继承。
C++允许一个子类继承多个父类,当一个类中包含多个类的属性和功能时,可以使用多重继承。
语法实现
class 子类名:继承方式1 父类1,继承方式2 父类2,...{
//新增的成员
};
如果继承的数据不冲突(无重名),可以直接访问,如果发生了冲突可以使用父类名+作用域加以区分。
//电话类
class Phone{
public:
Phone(double p=1000):price(p)
{
cout<<"Phone()"<<endl;
}
~Phone()
{
cout<<"~Phone()"<<endl;
}
//读私有的成员的接口
double get_price()
{
return this->price;
}
void call()
{
cout<<"打电话"<<endl;
}
private:
double price;
};
//MP3类
class Mp3{
public:
Mp3(double p=300):price(p)
{
cout<<"Mp3()"<<endl;
}
~Mp3()
{
cout<<"~Mp3()"<<endl;
}
//读私有的成员的接口
double get_price()
{
return this->price;
}
void play(string song)
{
cout<<"播放"<<song<<endl;
}
private:
double price;
};
//相机类
class Camera{
public:
Camera(double p=500):price(p)
{
cout<<"Camera()"<<endl;
}
~Camera()
{
cout<<"~Camera()"<<endl;
}
//读私有的成员的接口
double get_price()
{
return this->price;
}
void capture()
{
cout<<"拍照"<<endl;
}
private:
double price;
};
//多重继承
class SmartPhone:public Phone,public Mp3,public Camera{
public:
double get_price()
{
return Phone::get_price()+Mp3::get_price()+Camera::get_price()+3000;
}
};
6.菱形虚拟继承(多继承的优化)
多继承的子类中容易出现同名成员,造成代码冗余,可以使用以下方法进行优化。如上面的代码price和get_price()这两个成员函数每个基类中都有,代码冗余。
优化步骤:
①将父类中的同名成员提取出来
double price;
double get_price();
②将这些同名成员放入一个更高层的父类
class Product{
double price;
double get_price();
};
③父类使用虚继承(virtual)继承最高层的父类(Product)
class 子类名:virtual 继承方式 父类名{
//...
};
对于单层继承来说,虚继承和普通继承完全一样,区别在于多层继承时,子类在拷贝数据时,如果该数据处于更高层的父类,子类不再从直接父类拷贝数据,而是从最高层的父类去拷贝。
price和get_price成员变量直接从更高层的父类继承。
//产品类
class Product{
public:
Product(int p=0):price(p)
{
}
double get_price()
{
return this->price;
}
private:
double price;
};
//电话类 --- 虚继承
class Phone:virtual public Product{
public:
Phone(double p=1000):Product(p)
{
cout<<"Phone()"<<endl;
}
~Phone()
{
cout<<"~Phone()"<<endl;
}
void call()
{
cout<<"打电话"<<endl;
}
};
//MP3类
class Mp3:virtual public Product{
public:
Mp3(double p=300):Product(p)
{
cout<<"Mp3()"<<endl;
}
~Mp3()
{
cout<<"~Mp3()"<<endl;
}
void play(string song)
{
cout<<"播放"<<song<<endl;
}
};
//相机类
class Camera:virtual public Product{
public:
Camera(double p=500):Product(p)
{
cout<<"Camera()"<<endl;
}
~Camera()
{
cout<<"~Camera()"<<endl;
}
void capture()
{
cout<<"拍照"<<endl;
}
};
//多重继承
class SmartPhone:public Phone,public Mp3,public Camera{
public:
SmartPhone(double a=0,double b=0,double c=0):Product(a+b+c+3000)
{
}
};
①什么是菱形继承?菱形继承的问题是什么?
菱形继承是多继承的一种特殊情况,两个子类继承同一个父类,而又有子类同时继承这两个子类,称这种继承为菱形继承。菱形继承因为子类对象当中会有两份父类的成员,因此会导致数据冗余和二义性的问题。
②什么是菱形虚拟继承?如何解决数据冗余和二义性?
菱形虚拟继承是指在菱形继承的腰部使用虚拟继承(virtual)的继承方式,菱形虚拟继承对于D类对象当中重复的A类成员只存储一份,然后采用虚基表指针和虚基表使得D类对象当中继承的B类和C类可以找到自己继承的A类成员,从而解决了数据冗余和二义性的问题。
三、多态
多态就是函数调用的多种形态,使用多态能够使得不同的对象去完成同一件事时,产生不同的动作和结果。
多态是指不同继承关系的类对象,去调用同一函数,产生了不同的行为。在继承中要想构成多态需要满足两个条件:
①必须通过基类的指针或者引用调用虚函数。
②被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写。
1.虚函数
虚函数就是在类的成员函数声明前加virtual修饰符,该成员函数就变成了虚函数。一旦一个类中有虚函数,编译器就会为该类生成虚函数表。
class Animal{
public:
//成员函数
virtual void show()//虚函数
{
cout<<"Animal show"<<endl;
}
};
虚函数表中一个元素记录一个虚函数的地址,使用该类构造对象时,对象前4(8)个字节记录虚函数表的首地址。在C++中一个类中若没有成员变量,可以有成员函数(成员函数存储在代码段),该类的大小为1个字节。空类的大小也是1个字节。
①只有类的非静态成员函数前可以加virtual,普通函数前不能加virtual。
②虚函数的virtual和虚继承中的virtual是同一个关键字,但是它们之间没有任何关系。虚函数的virtual是为了实现多态,而虚继承的virtual是为了解决菱形继承的数据冗余和二义性。
2.虚函数的重写
虚函数的重写也叫做虚函数的覆盖,若派生类中有一个和基类完全相同的虚函数(返回值类型相同、函数名相同以及参数列表完全相同),此时称该派生类的虚函数重写了基类的虚函数。
如果父类中有虚函数,子类中对虚函数进行重写(overwrite),子类会继承父类的虚函数表,但是重写的虚函数会覆盖父类中对应虚函数在虚函数表中的位置。
如果父类的虚函数在子类中被重写,则可以使用父类类型记录子类对象,此时调用虚函数会去调用子类中虚函数的实现,而不调用父类中的原虚函数。
使用虚函数可以实现用父类类型记录子类对象,可以通过父类型的 指针/引用 访问子类中对应的接口,大大提高编程的灵活性(动态绑定),这种语法就叫多态。
静态绑定: 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也成为静态多态,比如:函数重载。
动态绑定: 动态绑定又称为后期绑定(晚绑定),在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。
class Animal{
public:
//成员函数
virtual void show()//虚函数
{
cout<<"Animal show"<<endl;
}
void run()
{
cout<<"Animal run"<<endl;
}
virtual void eat()
{
cout<<"Animal eat"<<endl;
}
};
class Dog:public Animal{
public:
//重写虚函数
virtual void show()//虚函数
{
cout<<"Dog show"<<endl;
}
void run()//名字隐藏
{
cout<<"Dog run"<<endl;
}
virtual void eat()
{
cout<<"Dog eat bones"<<endl;
}
};
class Cat:public Animal{
public:
//重写虚函数
virtual void show()//虚函数
{
cout<<"Cat show"<<endl;
}
virtual void eat()
{
cout<<"Cat eat fish"<<endl;
}
};
//传入一个Animal对象 ----- 多态
void animal_gogo(Animal *p)
{
p->show();
p->eat();
}
int main()
{
Animal *pa = new Animal;
//pa->show();
//pa->run();
//pa->eat();
animal_gogo(pa);
delete pa;
//有虚函数,父类类型记录子类对象
pa = new Dog;
animal_gogo(pa);
delete pa;
pa = new Cat;
animal_gogo(pa);
delete pa;
return 0;
}
通过父类 指针/引用 记录子类对象,调用虚函数时体现的是子类中虚函数的实现。这里实现了一个函数animal_gogo(Animal *p),该函数会根据传入不同的类型而指向对象相应的虚函数。从而实现多态。
多态的条件:
1.继承是多态的基础
2.虚函数是实现多态的关键
3.虚函数重写是实现多态的必要条件
3.虚析构
如果父类类型指向子类对象,当释放对象时,默认调用父类的析构函数而不是子类的析构函数。
如果在多态中希望根据对象的具体类型调用其析构函数,需要将析构函数写成虚析构(在析构函数前加virtual)。类中本身有析构函数,当类中同时也有虚函数时必须将析构函数写成虚析构。
class A{
public:
virtual ~A(); //虚析构函数
virtual void show();//虚函数
}
4.纯虚函数和抽象类
纯虚函数:用virtual修饰,没有语句体,只有声明和(=0)的成员函数。
class A{
public:
virtual void show()=0;//纯虚函数
}
如果类中有纯虚函数,该类不能实例化对象,这种类叫抽象类。
抽象类的作用不是用来构造对象,而是用作父类(基类)继承产生子类。子类在继承抽象父类时,如果没有实现父类中所有的纯虚函数,那么子类仍然是一个抽象类。
如果一个类中所有的成员函数都是纯虚函数,那么该类就叫纯抽象类。一个项目中的抽象类的设计属于框架设计的一部分。
5.几种重名机制的处理
重载、覆盖(重写)、隐藏(重定义)的对比
函数重载:在同一作用域,函数名相同,参数列表不同的函数构成重载关系
虚函数重写:子类中重写父类的虚函数,父类的虚函数在子类的虚函数表中将被覆盖
重定义(名字隐藏):子类中出现与父类中同名的成员,子类中父类的同名成员会被隐藏
四、C++中类和实现的分离
C++中使用类来组织代码,类的声明写在头文件,类的声明包括成员变量的声明,成员函数的声明,成员变量的声明无需修改,类中的函数只保留声明语句。
类的函数实现写在配对的源文件中,实现类中的函数时应该指定该函数属于哪个类。
头文件(xxx.h xxx.hpp)
class 类名{
成员变量的声明;
成员函数的声明;
构造函数,析构函数,拷贝构造函数的声明;
};
源文件(xxx.cpp)
成员函数的实现;
构造函数,析构函数,拷贝构造函数的实现;
函数参数的默认值要写到声明中(头文件),初始化参数列表写到实现中(源文件)。
//头文件
//类的声明
class mytime{
public:
//函数只留下声明语句
mytime(int h=15,int m=27,int s=30);//参数默认值写到声明中
~mytime();
void print_time();
void run();
private:
int hour;
int min;
int sec;
};
//源文件
/函数的实现 ----- 函数名前加类名和作用域符
mytime::mytime(int h,int m,int s):hour(h),min(m),sec(s)//初始化参数列表写在实现中
{
cout<<"mytime()"<<endl;
}