面向对象的三大特征:封装+多态+继承
一、C++之封装上
1.c++类 封装
(1) 原始类赋值:
N.name="myy";
N.age=10;
正确赋值,将赋值封装进类定义的函数,这样就可以对赋值内容进行是否合法限制
class Sudent
{
public:
void setAge(int_age)
{
if(int_age>0&&int_age<100)
{
age=int_age;
}
else{...}
}
};
(2) 若希望内部内容只被读取而不被改变–只读属性
class Car
{
public:
int get_num(){return num;}
private:
int num;
};
2. 内联函数和类内定义
- 关键字:inline
inline void fun {cout<<“Hello”<<endl;} - Inline函数省去了编译时函数调用的步骤,使编译更高效,但仅限于结构简单的函数,复杂函数编译器拒绝执行inline。
- 类内定义
- 类内定义:将函数声明及定义写在类内,对于简单函数会相同。
3.构造函数初始化及定义
//.h中声明
class Car
{
public:
Car(int num=1, string name="BMW");
};
//.cpp中定义,num, na
me 不用再初始化
Car::Car(int num, string name):n_num(num), n_name(name){}
4. 拷贝构造函数
- 当我们对自定义类进行拷贝操作时,不会重新调用构造函数,而是调用编译器自动生成的拷贝构造函数(如果没有自定义拷贝构造函数的话)。
- 如果想自己构造拷贝构造函数,格式如下:
- 定义格式:类名(const类名&变量名)
- 如果没有自定义拷贝构造函数,系统会自动生成一个默认的。
- 构造函数分类:
- 无参构造函数
- 默认构造函数
- 有参构造函数
- 参数带默认值
- 参数无默认值
- 无参构造函数
- 代码示例:
//.h声明
class Teacher
{
public:
Teacher (string name="Jim", int age=1);
Teacher(const Teacher &tea)();//拷贝构造
int age;
string name;
};
//Teacher.cpp
Teacher::Teacher(const Teacher &tea)
{}
//main.cpp使用
//*拷贝构造在参数传递的使用中也能用到,代码示例test函数,内部的变量定义t生成时用的拷贝构造函数
int main()
{
Teacher t1;
Teacher t2=t1;
Teacher t3(t1);
return 0;
}
- 拷贝构造函数还会以参数传递的方式进行使用
void test(Teacher t)//
{
}
int main(void)
{
Teacher t1;
tes(t1);//此操作使用了拷贝构造函数。
}
二、C++封装下
2.1 对象数组
int main(void)
{
Teacher t[3];
t[3].name="lisa";//从栈中建立对象数组
Teacher *p=new Teacher[3];
p[0].name="Lisa";
p[1].name="Tom";
p[2].name="Jane";
delete []p;//从堆中建立对象数组
p=NULL;//p++即指向下一个对象元素,但是切记delete时,p的位置要回到第一个数组元素位置那。另外,P最后要指向NULL。
}
2.2 对象成员
2.3 深拷贝与浅拷贝
(1) 浅拷贝:对于非指针类型,直接拷贝值;对于指针类型,拷贝指针的值,而非指针指向的值,该种情况会造成程序崩溃。
(2) 深拷贝:对于指针类型的值,新建一个指针,并将待拷贝元素指针所指向的值拷贝过来。
- 代码示例
//浅拷贝,无指针参数
class Array
{
public:
Array();
Array(const Array &a)
{
m_ix=a.m_ix;
m_iy=a.m_iy;
}
private:
int m_ix;
int m_iy;
}
//深拷贝,带指针拷贝
class Array
{
public:
Array()
{
m_iCount=5;
m_iP=new int[m_iCount];
}
Array(const Array &a)
{
m_iCount=a.m_iCount;
m_iP=new int[m_iCount];
for(int i=0;i<m_iCount;i++)
{
m_iP[i]=a.m_iP[i];
}//将指针所指向的内容进行拷贝
}
private:
int* m_iP;
int m_iCount;
}
2.4 this指针–类变量的地址
(1) 传入的参数和类的数据成员不能重名,否则报错;
Array(int len){this->len=len;}//这样就可以实现
2.5 const 使用-带有const的,就不允许改变成员变量
(2) 常成员函数
- 函数不能对数据成员进行修改;
- 常量构造函数,赋值只能用初始化列表方式
- const Array::Array(int a):num(a){}
- 格式: void change() const;
- 重载: void change(); void change() const;互为重载函数,当变量定义为常量时,则调用常量函数,斗则调用普通函数。
- const 函数重载目的是为了防止用户自定义一个const类,然后再调用类函数时出错,所以做的一个备用完善。如果用户不需要类const,则不需要该代码
2.6 常指针与常引用
(1) 常指针
- 一般函数的this指针要求同时具有读和写权限,然而加const的变量只能有读权限,所以const变量调用函数时会有冲突报错,即使调用的函数不会改变成员变量的值,也因为有潜在风险,毕竟编译器不知道你在函数里定义了哪些操作,所以统统报错,只有函数后面加上const时,保证自己不会修改变量,此时编译器才不会报错。
(2) 易混淆情况:
- const Array *p=&n;
- Array * const p1=&n;
- 以上两个const指针含义不同,第一个是指指针指向的变量是常成员变量,该变量不能再进行修改值的操作; 而第二个定义的是常指针,即指针不能再指向其他变量。
(3) 常引用:
Array c1(2);
const Array &c2=c1;
此时c2是c1的别名,但是与c1不同的是,c2只有只读权限。
三、继承篇
3.1 继承篇
- 前提:要继承的类,要完全包含于父类。子类只需要再补充自己特有的函数即可。(基类->派生类)
- 内存关系:子类的内存等于父类的内存大小加上自己的特有的函数的内存大小。
- 语法:
class Worker : public Person
{
};
- 在一个类中调用别的类,析构时先析构该类,再析构调用的类,类似层层剥茧;
- 而析构一个继承了父类的子类时,要先析构子类,再析构父类,类似从尾到源式顺序。
3.2 三种继承
(1) 共有继承 class A:public B
(2) 保护继承 class A: protected B
(3) 私有继承 class A: private B
-
公有继承
- 父类的保护成员,可以被保护式的继承给子类,但是外部对象不可访问;
- 父类的私有成员,不可被继承,因为无法访问。
-
保护继承
- 父类的公有成员和保护成员,都会被子类继承为保护成员
- 私有成员同上
-
私有继承
- 父类的公有和保护成员,会被子类继承为私有成员;
- 私有成员不被继承\
(4) has a 关系,不同于is a; 私有继承也是一种has a 关系
3.3 隐藏
- 父子关系
- 成员同名
- 隐藏: 当子类有与父类同名的函数或者数据变量时,定义的子类对象调用时会调用子类里的函数。而如果想要调用父类的变量时,格式为
m.setName();//子类对象
m.FL::setName();
-
对于隐藏的两个对象,不会因为是否传入参数不同而形成重载,调用父类函数,只能用隐藏的语法。
小知识补充:
#include “”,用双引号则编译器会从文件目录去找头文件;如果用<>,则编译器会从安装包里包含的文件库找头文件。 -
派生类可以赋值给基类,但是基类不能赋值给派生类。
-
当子类给父类对象初始化时,父类只能接收两者共同部分的函数和数据值,对于子类独有的函数,父类不能被初始化
-
当父类的指针指向子类对象时,也只能访问到两者的公共部分,而不能访问到子类独有的部分
3.4 多继承和多重继承
(1)多重继承:多代继承
(2)多继承:一个子类有多个父类
代码示例
class Worker{};
class Farmer{};
class MirgrantWorker:public Worker, public Farmer{};
(3) 菱形继承
类A
| |
类B 类C
| |
类D
- 类D中由于继承可能会有两份类A,造成不必要的内存占用,如何避免内存开销?-虚继承
- 解决办法:
- class B:virtual public A;
- class C:virtual public A;
- class D:public B,public C
四、多态
4.1 虚函数–静态多态VS动态多态
多态形式是父类指针指向子类对象,所以如果子类中有指针数据成员,那个在子类构造时会申请一块内存。父类在析构时并不能析构子类的内容,会造成内存泄漏,所以父类需要虚析构函数
(继承特性:如果delete父类指针,则只会销毁父类的函数;而delete子类指针,则父类和子类定义都会被销毁)
(1) 静态多态(早绑定):同一对象收到相同的消息却产生不同的函数调用,一般通过***函数重载***来实现,在编译时就实现了绑定,属于早绑定。
(2) 动态多态(晚绑定):不同对象在收到相同消息时产生不同的动作,一般通过虚函数来实现,只有运行时才能实现绑定,属于动态绑定,一般通过函数覆盖来实现。
(3) 覆盖: 虚函数允许子类重新定义成员函数,而子类重新定义父类的做法称为覆盖。若不是虚函数,仅父类和子类有同样函数,是为隐藏。
4.2 虚析构函数
(1) 动态多态在父类指针释放子类对象时造成内存泄漏,为了防止而在父类中使用虚析构函数。
(2) 根据VS2019实践,即使子类对象中没有指针类型,只要使用了多态,析构函数就得用virtual;
(3) 虚函数可被继承,当子类中定义的函数与父类中虚函数声明相同时,该函数就是虚函数;
(4) virtual使用注意事项:
- 普通函数不能使用,即不在类中的函数;
- static 静态成员函数不能使用。
- 不能使用内联函数,会被编译器视为纯虚函数;
- 构造函数不能为虚函数\
(5) 虚函数与虚析构函数原理
- 父类存在一个虚函数指针,指向一个虚函数表,在表中存有各虚函数的地址。因此开始执行时,当执行到虚函数,编译器会从虚函数表中找虚函数地址;对于在子类中覆盖的虚函数,子类也存在一个虚函数表,只不过对应位置上的指针地址改为子类所覆盖的函数地址。虚析构函数同理。
寻址过程类似如下:
shape
|vftable|–>|…|
|m_iEdg| |calcArea_ptr|–>|calcArea|
|m_dRR|\
(6) 代码证明虚函数表的存在
概念说明
- 对象的大小:类里定义的函数不占内存大小,只有数据成员占内存。对于没有任何数据成员的对象,为表示其存在,编译器给其内存大小为1;对于存在一个int型的对象,其内存占用大小为int型大小4字节。
- 对象的地址:——
- 对象成员的地址:在对象地址的头部开始
- 虚函数表的指针: 在有虚函数情况下,实例化一个对象时,对象占用内存的第一块就是虚函数表的指针,占据内存大小4字节,然后依次是该对象的数据成员。
- 如果子类没有覆盖父类的虚函数,则两张虚函数表中,某一虚函数指针指向同一个函数。
4.2纯虚函数和抽象类
(1)定义:只有函数声明,没有定义,且声明后加等于0;
- 格式:
virtual double calcPerimeter()=o;//纯虚 virtual double calcArea(); //虚函数
(2)虚函数表中,表指针指向的纯虚函数值为0; 而虚函数则指向一个具有实际意义的函数。
(3)抽象类: 含有纯虚函数的类叫做抽象类
- 由于含有纯虚函数原因,抽象类无法实例化对象;
- 抽象类的子类,如果没有定义父类的纯虚函数,那么它也是抽象类,无法进行实例化;
(4)**接口类:**仅含有纯虚函数的类称为接口类 - 及没有数据成员,只有纯虚函数;
- 实际使用中,接口类更多用于表达一种能力或协议。
- 比如父类定义了一些函数特性,那么子类必须实现这些函数,才从概念上说明子类属于父类。
- Note that:
- 抽象函数中只有纯虚函数,没有数据成员,构造函数和析构函数。
- 接口类最常见用法是在定义全局函数时限制输入对象的类型,但是传入的所有数据都可以安全调用抽象类的纯虚函数,因为肯定已经被定义过了。
- 可以使用接口类指针指向子类对象来实例化一个对象,并调用子类中实现的接口类中的纯虚函数。
void flyMatch(Flyable *a, Flyable *b)
{
a->takeOff();
a->land();
b->takeOff();
b->land();
}
ctrl+k+u去掉注释;
ctrl+k+c加注释
4.3 RTTI(Run-Time Type Identification)-运行时类型识别
RTTI的作用就是需要对对象成员进行动态判断时,用关键字来确定对象的动态类型。如引入的是父类类型指针指向的子类对象,但是继承父类的子类有很多,需要实时的根据不同的子类执行不同的操作,此时就需要RTTI了
(1)关键字:dynamic_cast,以下是其注意事项:
- 只能应用于指针和引用的转换;
- 要转换的类型中必须包含虚函数;
- 转换成功返回子类地址,失败返回NULL;
(2)关键字: typeid,以下是其注意事项: - type_id返回一个type_info对象的引用;
- 如果想通过基类指针获得派生类的数据类型,基类必须带有虚函数
- 只能获取对象的实际类型。
(3)示例代码
void doSomething(Flyable *obj)//需要转换的只能是指针
{
if(typeid(*obj)==typeid(Bird))
{
Bird *b=dynamic_cast<Bird *>(obj);//要转换的类型中必须包含虚函数,子类继承父类的函数也是虚函数
}
if(typeid(*obj)==typeid(Plane))
{
Plane *p=dynamic_cast<Plane*>(obj);//如果想通过基类指针获得派生类数据类型,基类必须带有虚函数
}
}
4.4 C++异常处理
对可以预见的错误进行相应的处理,否则交由编译器处理的话会直接造成程序崩溃。
(1)关键字:try…catch… throw
- 说明:try.catch:将主逻辑放在try块里,异常处理模块放在catch里。
- 在main函数中,用try,catch块进行异常捕获
- 常见的异常有:数组溢出+除数为0+内存不足
(2)实践:可以通过定义父类作为接口,然后接受各种子类异常。
五、模板
5.1 友元函数和友元类
(1)友元全局函数:函数定义在全局,且在类中声明为友元,则在该函数中可以调用类的private成员;
(2)友元成员函数:定义在类A中,且在类A的定义前要加入“class B;”,以告知编译器不需要报错。该函数需要在另一个类B中声明为其友元函数,且在B中需要Include 类A的头文件。形式为
#include "A_class.h"
class B_class
{
public:
friend void A_class::printA(B_class &t);
...
};
(3)友元类:与友元函数类似,只需在类里面声明即可。形式为:
class Circle;
class Coordinate
{
friend Circle;
public:
...
};
将Circle定义为友元类后,我们就可以在circle类中定义coordinate的对象,通过该对象,调用其私有数据类型和私有函数。
(4)
- 友元关系不可传递;
- 友元关系单向;
- 友元声明的形式及数量不受限制
- 友元只是封装的补充,不推荐用,因为减弱了类的封装性。
5.2 类中static
- 应用场景:一个公司的系统,每新建一个用户,统计用户的num变量就++, num变量不依赖新用户或旧用户的生成或消失而生成或消失,但是却会因为新建一个用户而加一,注销一个用户而减1。外界可以随时调用静态成员查看num值。它依赖类存在而存在,不依赖对象存在而存在。
- 静态数据成员
- 注意事项:必须初始化;
- 可被非静态和静态函数均可调用。
- 静态成员函数
- 注意事项:不能调用非静态数据成员;
- 形式:
class A { public: A(){num++;} ~A(){num--;} static num; static getNum{return num;} }; int A::num=0;
- 调用静态函数和静态成员时形式:
//方式一 int main(void) { cout<<A::getNUm()<<endl; cout<<A::num<<endl; } //方式二 int main() { A a; cout<<a.num<<endl; cout<<a.getNm<<endl; }
5.3 运算符重载
(1)关键字:operator; 包括一元运算符重载、二元运算符重载等
(2)以一元运算符(只需与一个变量进行运算)重载的-(负号)、++两个符号为例;
- -(负号)的重载:友元函数重载+成员函数重载
-负号函数重载代码示例
//成员函数重载
class coordinate
{
public:
coordinate();
~coordinate();
coordinate & operator-()
{
m_x=-m_x;
m_y=-m_y;
}
private:
int m_x;
int m_y;
};
//友元函数重载
class coordinate
{
friend coordinate & operator-(coordinate &c);
{
m_x=-m_x;
m_y=-m_y;
}
public:
coordinate();
~coordinate();
private:
int m_x;
int m_y;
};
//friend function realization
//coordinate..cpp
coordinate & operator-(coordinate &c)
{
c.m_x=-c.m_x;
c.m_y=-c.m_y;
return c;
}
++符号重载示例
- 首先讲述一下,前置++,“++a”,表示本次操作结束后,a的值比原来大一;后置++,“a++”,表示的是本次执行结束后a的值不变,但是一旦执行下一条指令时a就会加一,值比原来大1。
- 编译器为了区分这两种++,在指令传入时有所不同,这点需仔细对比两者代码。
- 变量的返回也有不同,比如函数定义时,是否加引用&符号。
- 以下编程示例中会着重讲解,需要认真理解。
//前置++
class Coordinate
{
public:
Coordinate& operator++()
{
++m_x;
++m_y;
return *this;
}
};
//后置++
class Coordinate
{
public:
Coordinate operator++(int)
{
Coordinate old(*this)//复制传入的变量值
++(this->m_x);
++(this->m_y);
return old;//也就是说本次执行返回变量未加一之前的值;等下一个使用该变量的代码使用时,变量才变为原来的+1值。
}
};
(3) 二元运算符重载:成员函数重载和友元函数重载
- 第一个典型例子是+号的重载(友元和成员函数都可进行重载)
- 成员函数+号重载
class Coordinate
{
public:
Coordinate(int x,int y);
Coordinate operator+(const Coordinate &coor)//this指针表示+号左边值;coor为传入+右边值,但是返回一个新值给等号左边。传入的值为了保险起见,不能改变其值大小,所以加const.
{
Coorfinate m;
m.m_x=this->m_x+coor.m_x;
m.m_y=this->m_y+coor.m_y;
return m;
}
private:
int m_x;
int m_y;
};
- 友元函数+号重载
class Coordinate
{
friend Coordinate operator+(const Coordinate &coor1,const Coordinate &coor2)
{
Coordinate m;
m.m_x=coor1.m_x+coor2.m_x;
m.m_y=coor1.m_y+coor2.m_y;
return m;
}
public:
Coordinate(int x,int y);
private:
int m_x;
int m_y;
};
//main.cpp
int main()
{
Coordinate p1(2,3);
Coordinate p2(3,4);
Coordinate p3(0,0);
p3=p1+p2;
return 0;
}
- “<<”输出运算符重载(只能友元函数重载)
- 成员函数不可进行重载的原因:因为重载函数中首个参数必不包含对象的this指针,而是外部的cout的oostream类型引用,所以一定不能用成员函数进行重载。
- 友元函数重载
class Coordinate
{
friend ostream& operator<<(ostream &out, const Coordinate &coor)
{
out<<coor.m_x<<","<<coor.m_y;//该部分与cout形式一样
return out;//务必要返回
}
public:
Coordinate(int x,int y);
private:
int m_x;
int m_y;
};
//main.cpp
int main()
{
Coordinate coor(3,5);
cout<<coor;
}
- []索引运算符(只能成员函数进行重载)该重载的更多运用在数组上
- 因为索引的第一个参数一定是对象的this指针,而不能是别的,所以不能用友元函数进行重载
- 成员函数重载代码实现:
class Coordinate
{
public:
Coordinate(int x,int y);
int operator[](int index)
{
if(index==0)
{return m_x;}
if(index==1)
{return m_y;}
}
private:
int m_x;
int m_y;
};
//main.cpp
int main()
{
Coordinate c(3.5);
cout<<c[0];
cout<<c[1];
return 0;
}
5.4 模板函数与模板类
(1) 为什么要引入模板?
对于只有参数类型不同,其他均相同的函数,为了定义方便,将参数类型作为变量传入函数模板.
(2) 关键字:template typename class(typename 和 class可以混用)
(3)计算机中,如果仅仅写出函数模板,而没有使用它,那么计算机是不会产生任何代码数据。只有当去使用函数模板才会产生实际代码
(4) 函数模板及其语法
- 类型作为模板参数
template<class T>
T max(T a, T b)//注意term name:函数模板
{
return(a>b)?a:b
}
//使用
int main()
{
int m=max(100,90);//编译器可以自动根据检测到的数据类型进行计算;
//term name:模板函数
char n=max<char>('A','B');//也可以自己强制生成
}
- 常量值作为模板参数;需要注意的是不能使用浮点数,类,指针等作为模板参数,而且必须是确定的值,不能是变量。
template<int size>
void display()
{
cout<<size<<endl;
}
//使用
int main()
{
display<10>();
}
- 多参数函数模板
template<typename T,typename C>
void display(T a, C b)
{
cout<<a<<""<<b<<endl;
}
template<class A, int size>//这部分决定了函数在使用时要给出的类型和变量值的结构形式
void max(A a)//该部分决定了函数括号内部的结构
{
if(a>size)
{
cout<<a<<endl;
}
else
{
cout<<size<<endl;
}
}
//使用
int main()
{
int a=3;
char str='A';
display<int,char>(a,str);//将声明的都指定出来即可
max<int,5>(a);
}
- 函数模板与重载
- 函数名相同,但输入类型不同的函数模板不是函数重载,因为在未使用之前,它们不会在内存中生成任何代码,只有在使用的时候,生成了代码,才会形成重载
tempplate <typename T>
void display(T a);
tempplate <typename T,int size>
void display(T a);
tempplate <typename T>
void display(T a, T b);
(5) 类模板:适用于编写数据结构相关代码
- 必须在头文件中全部定义,无法做到.cpp/.h分别编译;
- 类模板外部定义的成员函数,每一个前面都需要加上模板<>声明。
- 语法
template<class T>
class MyArray
{
public:
void display(){...}//类内定义成员函数,与普通类无不同
void setName(T mm);
T Add(T a, T b);
private:
T *m_pArr;
};
template<class T>
void MyArray<T>::setName(T mm)//每一个函数都需要重申一次
{...}
template<class T>
T MyArray<T>::Add(T a, T b)
{...}
//使用时
int main()
{
MyArray<int> arr;
}
情况二
template<class T, int KSize>
class MyArray
{
public:
void display(){...}//类内定义成员函数,与普通类无不同
void setName(T mm);
T Add(T a, T b);
private:
T *m_pArr;
};
template<class T, int KSize>
void MyArray<T,KSize>::setName(T mm)//每一个函数都需要重申一次
{...}
template<class T, int KSize>
T MyArray<T,KSize>::Add(T a, T b)
{
a=b+KSize;
return a;
}
//使用时
int main()
{
int a=3;
int b=4;
MyArray<int,10> arr;
arr.Add(a,b);//Add函数不需要再加<>类型声明
}
5.5 标准模板类
(1)vector
- vector v3(n,i);//含义为包含n个值为i的元素
- vector v1(v3);//复制了一个v3
- 常用函数
empty()//返回是否为空
begin()//返回首个元素
end()//返回末元素的下一个元素
clear()//清空向量
front()//第一个数据
back()//最后一个数据
size()//返回大小
push_back()//将数据插入向量尾
pop_back()//删除向量尾部数据
- 迭代器 iterator
- 借助迭代器可以实现标准模板内部的遍历
- 迭代器定义的元素一使用时一定要加, 这样才表示前迭代器所指向的元素*
vector<string> vec;
vec.push_back('A');
vector<string>::iterator cite = vec.begin();
for(; cite!=vec.end();cite++)
{
cout<<*citer<<endl;//
}
(2)链表
- 链表的遍历只能通过iterator进行,不能使用普通的for循环
- 插入速度大于向量
(3)map 映射
- key-value
- 使用方法
map<int,string> m;
pair<int, string> p1(10,"shanghai");
pair<int, string> p2(20,"beijing");
m.insert(p1);
m.insert(p2);
cout<<m[10]<<endl;
cout<<m[20]<<endl;
- map的输出有两种方法,其中迭代器法需要注意。
例子2
map<string, string> m;
pair<string, string> p1("S","shanghai");
m.insert(p1);
cout<<m["S"]<<endl;//
//只能用迭代器遍历,且元素输出要注意
map<string, string>::iterator ite=m.begin();
for(; ite<m.end();ite++)
{
cout<<ite->first<<endl;//键值
cout<<ite->second<<endl;//value
}
总结:
- 类中可能定义的基本函数:
- 构造、析构函数;
- 拷贝构造函数(深/浅拷贝);//查询
- const等其他函数重载;
- 静态变量&静态函数;
- 虚函数/纯虚函数;(虚函数用于继承中的覆盖(有指针变量的话需要虚析构函数)纯虚函数用于做接口类型的类,是多个子类的父类,没有析构和构造函数);
- 运算符重载函数;