C++三大特性:封装、继承和多态
类的基本思想是数据抽象(data abstraction)和封装(encapsulation)
类(数据抽象):
1)数据抽象
- 接口:用户能执行的操作
- 实现:① 类的数据成员;② 负责接口实现的函数体;③ 定义类所需的各种私有函数。
2)封装
- 实现了类的接口和实现的分离。封装之后的类隐藏了它的实现细节,也就是说,类的用户只能使用接口而无法访问实现部分。
1、成员
C++成员通常包括三个部分:
- 成员变量:一般为数据,对外界封装起来的类的每个对象都具有
- 成员函数:申明必须写在类的内部,它的定义既可以在类的内部,也可以在类的外部。就像类的其他变量一样,成员函数是类的一个成员,它可以操作类的任意对象,可以访问对象中的所有成员。
成员函数定义在类的外部话,格式为:
int 类名::meber_function() { //function body }
- 静态(static)成员:可以是数据or函数,也可以是类对外的接口。独立于对象存在,该类的所有对象共享一个静态存储区。
-
非成员函数:类的实现常常需要一些辅助函数,尽管这些函数从概念上属于类接口的组成部分,但他们实际上并不属于类本身。如果函数在概念上属于类但是不定义在类中,则它一般应与类声明(而非定义)在头一个头文件内。在这种方式下,用户使用接口的任何部分都只需引入一个头文件。
引入this:
c++中this是一个额外的隐式函数,用来访问当前的对象,而它的本质是一个常量指针,不能把它绑定到一个常量对象上。
当我们调用一个成员函数时,用请求该函数的对象地址初始化this。
例如,调用total.isbn() <=> Sales data::isbn(&total)
常量成员函数:
2、类的访问限制
类的默认权限为private。
C++类的访问修饰符:private、protected、public
- 类的一个特征就是封装,public和private作用就是实现这一目的。所以:用户代码(类外)可以访问public成员而不能访问private成员;private成员只能由类成员(类内)和友元访问。
- 类的另一个特征就是继承,protected的作用就是实现这一目的。所以:protected成员可以被派生类对象访问,不能被用户代码(类外)访问。
一般来说,作为接口的一部分,构造函数和一部分成员函数应定义在public说明符之后,而数据成员和作为实现部分的函数这应该是在private说明符之后
private | 外界不可见,不能直接访问 | |
protected | 外界不可见,不能直接访问;子类可以访问 | |
public | 外界可以直接访问 |
- private:可以通过类的成员函数、类的实例变量进行访问
- public:无法通过类的实例变量进行访问。但是可以通过类的友元函数、友元类进行访问。
- protected:无法通过类的实例变量进行访问。但是可以通过类的友元函数、友元类进行访问。
保护成员的可访问范围比私有成员大,比公有成员小。能访问私有成员的地方都能访问保护成员。保护成员扩大的访问范围表现在:基类的保护成员可以在派生类的成员函数中被访问。
引入保护成员的理由是:基类的成员本来就是派生类的成员,因此对于那些出于隐藏的目的不宜设为公有,但又确实需要在派生类的成员函数中经常访问的基类成员,将它们设置为保护成员,既能起到隐藏的目的,又避免了派生类成员函数要访问它们时只能间接访问所带来的麻烦。
C++就近原则:对于成员函数setName函数中使用的参数age,并不会与成员变量age产生误会,因为会就近使用形参的age。
#include <iostream>
using namespace std;
class student {
public:
int age;
void setAge(int age)
{
this->age = age;
return;
}
};
int main(int argc, char** argv)
{
student a;
a.setAge(10);
cout << "a.age = " << a.age << endl;
return 0;
}
输出:
a.age = 10
3、构造函数、拷贝、赋值和析构函数
1)类的构造函数(constructor):每个类都分别定义了它的对象被初始化的方式,类通过一个或几个特殊的成员函数来控制其对象的初始化过程,这些函数称为构造函数(constructor),比如setAge(int age)。
- 构造函数的名字和类名相同,和其他函数不一样的是:构造函数没有返回类型
- 构造函数也有一个(可能为空)参数列表和一个(可能为空)的函数体
- 构造函数不能被声明为const的。在创建一个const对象时,直到构造函数完成初始化过程,对象才能真正取得其“常量”属性。因此,构造函数在const对象的构造过程中可以向其写值。
- 是一种特殊的成员函数,它会在每次创建类的新对象时执行。
默认构造函数:
如果类没有显示的定义构造函数,那么编译器就会 隐式的定义一个默认构造函数(default constructor)。其具有以下特性:
- 不需要任何实参
- 如果类内存在初始值,用它来初始化成员;否则,执行默认初始化。
#include <iostream>
using namespace std;
class Date {
public:
Date(int year,int month,int day):year(year), month(month), day(day){
}
void printData(void){
cout << "year = " << year << endl;
cout << "month = " << month << endl;
cout << "day = " << day << endl;
}
private:
int year;
int month;
int day;
};
int main()
{
Date date1(1,2,3);
date1.printData();
return 0;
}
最好令构造函数初始值的顺序与成员声明的顺序保持一致。而且如果可能的话,尽量避免使用某些成员初始化其他成员。
成员的初始化顺序与他们在类定义中出现顺序一致
构造顺序:先父后儿
- 先调用基类的构造函数;对于基类,先虚拟基类,后一般的基类(虚拟基类构造函数总是最先调用的)
- 自身,先是对象成员,后自己
对于虚拟基类,即使多次被继承,构造函数也只执行一次
2)拷贝构造函数:
是一种特殊的构造函数。它在创建对象时,是使用同一类中之前创建的对象来初始化新创建的对象。拷贝构造函数通常用于:
- 通过使用另一个同类型的对象来初始化新创建的对象
- 复制对象把它作为参数传递的函数
- 复制对象,并从函数返回这个对象
3)析构函数:
类的析构函数是类的一种特殊成员函数,它会在每次删除所创建爱你的对象时执行。析构函数有助于在跳出程序(比如关闭文件、释放内存等)钱释放资源。
#include <iostream>
using namespace std;
class Date {
public:
Date(int year,int month,int day):year(year), month(month), day(day){
cout << "constructor occur" << endl;
}
~Date(){
cout << "~ occur" << endl;
}/* 析构函数 */
......
};
int main()
{
Date *date1 = new Date(1,2,3);
date1->printData();
//手动删除
delete date1;
return 0;
}
4、类的友元
友元(friend):允许其他类或者函数访问它的非公有成员(非public成员)。只要增加一条以friend声明的即可。
#include <iostream>
using namespace std;
class Date {
public:
Date(){}
Date(int year,int month,int day):year(year), month(month), day(day){
cout << "constructor occur" << endl;
}
~Date(){
cout << "~ occur" << endl;
}
void printData(void){
cout << "message is :" << endl;
cout << "year = " << year << endl;
cout << "month = " << month << endl;
cout << "day = " << day << endl;
}
/* 友元函数的声明 */
friend void printAddYearOne(Date &dat0,Date &dat1,Date &dat2);
private:
int year;
int month;
int day;
};
/* 友元函数的定义 */
void printAddYearOne(Date &dat0,Date &dat1,Date &dat2){
dat0.year = dat1.year + dat2.year;
}
int main()
{
Date date1(1,2,3);
date1.printData();
Date date2(1,2,3);
date2.printData();
Date DateTemp(1,2,3);
printAddYearOne(DateTemp,date1,date2);
DateTemp.printData();
//手动删除
return 0;
}
运行结果:
5、封装,继承、多重继承
1)封装有两个重要的优点:
- 确保用户代码不会无意间破坏封装对象的状态
- 被封装的类的具体实现细节可以随时改变,而无需调整用户级别的代码
2)继承
- 派生类不能访问基类的私有成员
- 派生类可以访问protected成员,其他代码不可以
- 派生类可以通过protected/public的成员函数
调整访问控制:派生类继承父类的非private成员函数可以变成private或者protected
/* 将printfInfo私有化
* 将proteced公有化
*/
class Father{
proteced:
int proteced = 0;
public:
void printfInfo();
}
class Son : public Father{
private:
using Father::printfInfo;
public:
using Father::proteced;
}
继承方式:
- 无论哪种继承方式,在派生类内部使用父类时并无差别
- 不同继承方式,会影响这两方面:外部代码对派生类的使用、派生类的子类
3)多重继承:一个派生类可以有多个父类(基类)
#include <iostream>
using namespace std;
/* 多重继承
*/
class Sofa
{
public:
Sofa() {}
void watchTv(){
cout << "Watch TV" << endl;
}
};
class Bed
{
public:
Bed() {}
void sleep(){
cout << "sleep" << endl;
}
};
class Sofabed : public Sofa,public Bed//如果不写public,默认是私有继承
{
public:
Sofabed(){}
};
int main(int argc,char **argv)
{
Sofabed testDemo;
testDemo.sleep();
testDemo.watchTv();
cout << "Hello World!" << endl;
return 0;
}
多重继承存在的问题:二义性:多个基类有相同的成员函数
解决方法:
① 调用时加上相对应的父类加以限定,例如:testDemo.Sofa::setweight(10);
② 虚拟继承:虚基类使得多个派生出的对象只继承一个基类对象
#include <iostream>
using namespace std;
/* 多重继承存在二义性
* 虚拟继承消除二义性
*/
class Furniture
{
private:
int weight;
public:
Furniture() {}
void setweight(int weight){
this->weight = weight;
}
int getWeight(void) const{
return weight;
}//const成员函数
};
class Sofa : virtual public Furniture{
private:
int a;
public:
Sofa() {}
void watchTv(){
cout << "Watch TV" << endl;
}
};
class Bed : virtual public Furniture
{
private:
int b;
public:
Bed() {}
void sleep(){
cout << "sleep" << endl;
}
};
class Sofabed : public Sofa,public Bed//如果不写public,默认是私有继承
{
private:
int c;
public:
Sofabed(){}
};
int main(int argc,char **argv)
{
Sofabed testDemo;
testDemo.sleep();
testDemo.watchTv();
testDemo.setweight(10);
// testDemo.Sofa::setweight(10);
cout << testDemo.getWeight() << endl;
cout << "Hello World!" << endl;
return 0;
}
Sofa、Bed内存情况:
Sofa | Bed | |
共用 | weight | |
各自成员变量 | a | b |
testDemo:
weigt | Furniture |
a | Sofa |
b | Bed |
c | 自己 |
应该尽量避免使用多重继承,它使得程序变得更加复杂、更容易出错。
6、多态、类型转换
1)多态:使用相同的调用的调用方法,对于不同的对象,它会调用不同的类里面实现的函数
#include <iostream>
using namespace std;
/* 多态 */
class Furniture{
private:
int a;
public:
Furniture(){
cout << "Furniture()" << endl;
}
~Furniture(){
cout << "~Furniture()" << endl;
}
virtual void eatting(void){
cout << "eatting something" << endl;
}
};
class Sofa:public Furniture{
private:
int b;
public:
Sofa(){
cout << "Sofa()" << endl;
}
~Sofa(){
cout << "~Sofa()" << endl;
}
void eatting(void){
cout << "eatting something on the sofa" << endl;
}
};
class Bed:public Furniture{
private:
int c;
public:
Bed(){
cout << "Bed()" << endl;
}
~Bed(){
cout << "~Bed()" << endl;
}
void eatting(void){
cout << "eatting something on the bed" << endl;
}
};
void test_eatting(Furniture& F){
F.eatting();
}
int main()
{
Furniture F;
Sofa S;
Bed B;
test_eatting(F);
test_eatting(S);
test_eatting(B);
cout << "Hello World!" << endl;
return 0;
}
机制:
静态联编:非虚函数,在编译时确定好调用哪一个函数
动态联编:在运行时候才确定调用哪个函数。①对象里有一个指针,该指针指向一个表(虚函数表);② 调用函数时,通过该指针找到表,调用虚函数。
- 多态只有使用指针或者引用来使用对象时,才有多态test_func(Human *h)、test_func(Human &h);传值时,无多态test_func(Human h),只能使用静态联编
- 只有类的成员函数才能声明为虚函数
- 静态成员函数不能是虚函数
- 内联函数不能是虚函数
- 构造函数不能是虚函数
- 析构函数一般都声明为虚函数
- 重载:函数参数不同,不可设为虚函数(不存在相同的调用方法);覆盖:函数参数、返回值相同,可设置为虚函数
- 返回值例外:函数参数相同,但是返回值是当前对象的指针或引用时,也可以设为虚函数
2)类型转换
隐式类型转换:由编译器来实现,编译器不一定能够猜测出我们的真实意图,所以使用隐式类型转换的时候经常有各种警告,为此,引入显示类型转换。
double d = 100.1;
int i = d; //double转换成int,i = 100
char *str = "hello";
int *p = str; //char*转为int*
显示类型转换:
(1)对于C语言:格式:(type-id)
double i = 100.1;
int i = (int)d;
char *str = "hello";
int *p = (int*)str;
(2)对于C++
① 动态转换(dynamic_cast)
格式:dynamic_cast<type-id>(expression)
该运算符把expression转换成type-id类型的对象。Type-id必须是类的指针、类的引用或者void *;如果type-id是类指针类型,那么expression也必须是一个指针;如果type-id是一个引用,那么expression也必须是一个引用。
- 用于多态场合,即:必须有虚函数
- 主要用于类层次间的上行转换和下行转换,还可以用于类之间的交叉转换
- 在类层次见进行上行转换时,dynamic_cast和static_cast的效果是一样的;
- 在进行类型转换时,dynamic具有类型检查的功能,比static_cast更安全
#include <iostream>
using namespace std;
/* 多态 */
class Furniture{
private:
int a;
public:
Furniture(){
cout << "Furniture()" << endl;
}
virtual ~Furniture(){
cout << "~Furniture()" << endl;
}
virtual void eatting(void){
cout << "eatting something" << endl;
}
};
class Sofa:public Furniture{
private:
int b;
public:
Sofa(){
cout << "Sofa()" << endl;
}
virtual ~Sofa(){
cout << "~Sofa()" << endl;
}
void eatting(void){
cout << "eatting something on the sofa" << endl;
}
};
class Bed:public Furniture{
private:
int c;
public:
Bed(){
cout << "Bed()" << endl;
}
virtual ~Bed(){
cout << "~Bed()" << endl;
}
void eatting(void){
cout << "eatting something on the bed" << endl;
}
};
void test_eatting(Furniture *F){
Sofa *S = new Sofa;
Bed *B = new Bed;
F->eatting();
/* 想分辨是Sofa还是Bed */
/* 使用动态类型转换 */
if(S == dynamic_cast<Sofa *>(F)){
cout << "This is Sofa" << endl;
}
if(B == dynamic_cast<Bed *>(F)){
cout << "This is Bed" << endl;
}
delete S;
delete B;
}
int main()
{
Furniture F;
Sofa S;
Bed B;
test_eatting(&F);
test_eatting(&S);
test_eatting(&B);
cout << "Hello World!" << endl;
return 0;
}
② 静态转换(static_cast):该运算符把expression转换为type-id类型,但运行时没有类型检查来保证转换的安全性。
格式:static_cast<type-id>(expression)
- 用于类层次结构中基类和子类之间指针或引用的转换
- 进行上行转换(把子类的指针或引用转换成基类表示)是安全的
- 进行下行转换(把基类指针或引用转换成基类表示)是安全的
- 用于基本数据类型之间的转换,如把int转换成char,把int转换成enum:这种转换的安全性也要开发人员来保证
- 把woid指针类型转换成目标类型的指针(不安全!!!)
- 把任何类型的表达式转换成void类型
注意:static_cat不能转换掉expression的const、volatile、或者__unaliged属性
③ 重新解析转换(reinterpret_cast):先当于C风格的用小括号( )实现的强制类型转换(1)
格式:reinterpret_cast<type_id>(expression)、圆括号不能少
- type-id必须是一个指针、引用、算术类型、函数指针或者成员指针
- 他可以吧一个指针转换成一个整数,也可以把一个整数转换成一个指针
- 跟C风格的强制转换类似,没有安全性检查
#include <iostream>
using namespace std;
//类型转换
int main()
{
double d = 100.1;
int i = reinterpret_cast<int>(d); //错误
char *str = "hello";
int *p = reinterpret_cast<int*>(str); //正确:char*转为int*
cout << "Hello World!" << endl;
return 0;
}
④ const_cast转换:该运算符用来去除原来类型的const或volatile属性。
格式:const_cast<type_id> (expression)
#include <iostream>
using namespace std;
//类型转换
int main()
{
double d = 100.1;
int i = (int)(d); //double转换成int,i = 100
const char *str = "hello";
char *str2 = const_cast<char *>(str);
int *p = reinterpret_cast<int*>(str2); //char*转为int*
cout << i << "\n" << *p << endl;
cout << "Hello World!" << endl;
return 0;
}
No Warnning,No Error。