面向对象编程
实际上就是把具有相同属性的数据和函数封装在一个对象单元中,然后对对象进行逻辑操作,这样能够提高代码安全性可靠性、可维护性,降低编程的复杂性;面对对象编程有几个关键的概念 :类和对象,继承、封装、抽象、多态;
类&对象
定义:类用于指定对象的形式,是一种用户自定义的数据类型,它是一种封装了数据和函数的组合。类中的数据称为成员变量,函数称为成员函数。
定义一个类需要使用关键字 class,然后指定类的名称,并类的主体是包含在一对花括号中,主体包含类的成员变量和成员函数。
例如:
#include <iostream>
using namespace std;
class Box
{
public:
//成员变量
double length; // 长度
double breadth; // 宽度
double height; // 高度
// 成员函数定义
double get(void){
return length * breadth * height;
}
void set( double len, double bre, double hei ){
length = len;
breadth = bre;
height = hei;
}
};
成员函数的定义还有一种方法:
也可以在类的外部使用范围解析运算符 :: 定义该函数
#include <iostream>
using namespace std;
class Box
{
public:
double length; // 长度
double breadth; // 宽度
double height; // 高度
// 成员函数声明
double get(void);
void set( double len, double bre, double hei );
};
// 成员函数定义
double Box::get(void)
{
return length * breadth * height;
}
void Box::set( double len, double bre, double hei)
{
length = len;
breadth = bre;
height = hei;
}
int main(){
Box box1;//这就是对象
}
访问修饰符:共有三种,public,private,protected
public公有成员变量在类的外部进行访问,可以不使用成员函数来访问和设置变量的值;
private变量只能通过成员函数和友元函数进行访问;
protected变量可以通过成员函数、友元函数以及派生类的成员函数进行访问;
需要注意的是派生类的成员函数只能访问派生类生成对象的成员变量,但是无法修改基类中生成对象的成员变量;
友元函数一般可能为全局函数,想要访问成员变量,所以在类中进行声明为友元函数
class Box{
private:
//私有变量
int high;
protected:
//受保护变量
int width;
public :
//公有变量
int length;
void set(int h,int w,int l){
width=w;
high=h;
length=l;
}
friend void set2(Box& b,int x,int y,int z)
};
void set2(Box& b,int x,int y,int z){
b.high=x;
b.width=y;
b.length=z;
}
//Box的派生类
class Pass:public Box{
public:
void set1(int w,int z){
width=w;
length=z;
}
};
int main(){
Box box1;
//成员函数访问公有变量、私有变量和受保护变量
box1.set(1,2,3);
//友元函数访问公有变量、私有变量和受保护变量
set2(box1,1,2,3);
Pass pass;
//派生类的成员函数访问公有变量和受保护变量
pass.set1(1,2);
}
类的构造函数
类的构造函数是一种特殊的成员函数,用于创建对象并且在创建对象的同时初始化对象;
构造函数的特点:
- 类名称与构造函数名称相同
- 没有返回类型
- 自动调用:创建对象时自动调用
- 可以重载:有一个或者多个构造函数,
class Box{
private:
int length;
int width;
int high;
public :
Box(int len) :length(len) {
cout<<"object is being created"<<endl;
} //构造函数,利用初始化列表,来初始化参数
Box(int len,int wid,int hig) :length(len),width(wid),high(hig) {}
};
int main(){
Box box1;
}
程序编译运行后:object is being created
类的析构函数
析构函数也是一种特殊的成员函数,析构函数的名称仍然适合类的名称是相同的,只不过,需要在前面加上(~)作为前缀;
不带有任何返回值,也没有任何参数,并且在程序关闭前自动执行;例如:
class Box{
private:
int length;
int width;
int high;
public :
void print(){
cout<<length<<" "<<width<<" "<<high<<endl;
}
Box(int len,int wid,int hi) :length(len),width(wid),high(hi) {
cout<<"object is being created!"<<endl;
}
~Box() {
cout<<"object is being destroied!"<<endl;
}
};
int main(){
Box box1(1,2,3);
box1.print();
return 0;
}
结果:
object is being created!
1 2 3
object is being destroied!
拷贝构造函数:
拷贝构造函数是一类特殊的构造函数,通过已经创建的对象来初始化新创建的对象;如果在类中没有拷贝构造函数,则编译器默认自行定义一个拷贝构造函数,如果类中带有指针变量,并且有动态内存分配,那么这个类中必须要有一个拷贝构造函数,这是因为拷贝构造函数可以分为深拷贝和浅拷贝两种方式,要明确这两种拷贝的区别:
- 首先是浅拷贝,浅拷贝只是复制原对象中所有成员的值,对于指针成员,浅拷贝只是复制原对象中指针的地址,并不是复制指针指向的值,因此在浅拷贝后生成的对象与原对象中的指针指向相同一块区域;
- 然后是深拷贝,深拷贝不仅复制原对象中成员的值,还为新对象中的指针申请一个新的地址,将这个指针指向的值与源对象中指针指向的值相同;
下面用两个实例来讨论深拷贝与浅拷贝的区别:
class Box{
private:
int* next; //next为一个int型的指针
public :
//构造函数
Box(int n){
next=new int;//申请动态内存
*next=n; //赋值
cout<<"object is being created!"<<endl;
}
//浅拷贝构造函数
Box(const Box& obj){
next=obj.next;
cout<<"another object is being copied!"<<endl;
}
//析构函数
~Box(){
delete next;
cout<<"an object is being destroied!"<<endl;
}
void set(int n){
*next=n;
}
void print(){
cout<<*next<<" "<<endl;
}
};
int main(){
Box box1(1);
cout<<"box1 中指针指向的值为";
box1.print();
Box box2=box1;
cout<<"box2 中指针指向的值为";
box2.print();
box2.set(222);
cout<<"修改后box2 中指针指向的值为";
box2.print();
cout<<"此时box1 中指针指向的值为:";
box1.print();
return 0;
}
结果:
object is being created!
box1 中指针指向的值为1
another object is being copied!
box2 中指针指向的值为1
修改后box2 中指针指向的值为222
此时box1 中指针指向的值为:222
an object is being destroied!
结果分析:
从结果来看,在进行浅拷贝构造之后,修改box2中指针成员指向的值,同时会对box1中指针成员指向的值进行修改,这表明box1和box2中指针成员为同一内存地址,因此在利用析构函数将内存释放时,会进行两次释放同一内存区域,从而出现错误。所以从结果中也可以看出,本应该调用两次析构函数,但是只调用了一次,第二次调用失败;这就要求我们在当类中具有指针成员时,尽量使用深拷贝。
class Box{
private:
int* next; //next为一个int型的指针
public :
//构造函数
Box(int n){
next=new int;//申请动态内存
*next=n; //赋值
cout<<"object is being created!"<<endl;
}
//深拷贝构造函数
Box(const Box& obj){
next=new int ;
*(next)=*(obj.next);
cout<<"another object is being copied!"<<endl;
}
//析构函数
~Box(){
delete next;
cout<<"an object is being destroied!"<<endl;
}
void set(int n){
*next=n;
}
void print(){
cout<<*next<<" "<<endl;
}
};
int main(){
Box box1(1);
cout<<"box1 中指针指向的值为";
box1.print();
Box box2=box1;
cout<<"box2 中指针指向的值为";
box2.print();
box2.set(222);
cout<<"修改后box2 中指针指向的值为";
box2.print();
cout<<"此时box1 中指针指向的值为:";
box1.print();
return 0;
}
运行结果:
object is being created!
box1 中指针指向的值为1
another object is being copied!
box2 中指针指向的值为1
修改后box2 中指针指向的值为222
此时box1 中指针指向的值为:1
an object is being destroied!
an object is being destroied!
从深拷贝结果来看,创建box2后,修改box2的值,不会对box1的值造成影响,说明这两个对象中指针指向不同区域,结果显示调用两次析构函数,所以对象的销毁也是正常的;
友元函数:
前面在类的访问修饰符中提到了友元函数可以访问私有变量和受保护变量,虽然友元函数需要在类中进行声明,但是需要注意的是友元函数并不是成员函数;
this指针:在每一个成员函数中都有一个隐式的指针,这个指针指向调用该成员函数的对象;这个指针可以解决函数参数和成员变量名称相同的问题,例如:
class Box{
public :
int x;
int y;
void set(int x,int y){
this->x=x;
this->y=y;
}
void print(){
cout<<x<<" "<<y<<endl;
cout<<"利用this指针来进行访问:";
cout<<this->x<<" "<<this->y<<endl;
}
};
int main(){
Box box1;
box1.set(1,2);
box1.print();
}
结果:
1 2
利用this指针来进行访问:1 2
继承
- 面向对象程序设计方法中最重要的一个概念就是继承,继承允许使用已有的类来创建另一个类,这两个类分别叫做基类和派生类;
- 一个类既可以派生多个类,也可以继承于多个类; 基类中的非私有变量和成员函数可以被派生类访问,也就是公有和受保护变量和函数可以被派生类访问;
- 一个派生类继承于基类时,可以访问所有的公有和受保护变量和函数,但除了构造函数、拷贝构造函数、析构函数,基类的重载运算符还有友元函数;
- 当一个类继承于另一个类时,继承方式有三种:public、protected、private形式,一般情况下都是用public,不会使用其他两种类型;
派生类的定义方式如下所示:
class Box{
public :
int x;
int y;
Box(){
cout<<"Box类对象已经创建!"<<endl;
}
~Box(){
cout<<"Box类的对象已经被销毁!"<<endl;
}
void set(int x,int y){
this->x=x;
this->y=y;
}
void print(){
cout<<this->x<<" "<<this->y<<endl;
}
};
class Pass: public Box{
public :
void setNum(int m,int n){
x=m;
y=n;
}
Pass(){
cout<<"Pass类对象已经被创建!"<<endl;
}
~Pass(){
cout<<"Pass类对象已经被销毁!"<<endl;
}
};
int main(){
Pass pass;
pass.set(1,2);
pass.print();
}
运行结果:
class Box{
public :
int x;
int y;
Box(){
cout<<"Box类对象已经创建!"<<endl;
}
~Box(){
cout<<"Box类的对象已经被销毁!"<<endl;
}
void set(int x,int y){
this->x=x;
this->y=y;
}
void print(){
cout<<this->x<<" "<<this->y<<endl;
}
};
class Pass: public Box{
public :
void setNum(int m,int n){
x=m;
y=n;
}
Pass(){
cout<<"Pass类对象已经被创建!"<<endl;
}
~Pass(){
cout<<"Pass类对象已经被销毁!"<<endl;
}
};
int main(){
Pass pass;
pass.set(1,2);
pass.print();
}
class Box{
public :
int x;
int y;
Box(){
cout<<"Box类对象已经创建!"<<endl;
}
~Box(){
cout<<"Box类的对象已经被销毁!"<<endl;
}
void set(int x,int y){
this->x=x;
this->y=y;
}
void print(){
cout<<this->x<<" "<<this->y<<endl;
}
};
class Pass: public Box{
public :
void setNum(int m,int n){
x=m;
y=n;
}
Pass(){
cout<<"Pass类对象已经被创建!"<<endl;
}
~Pass(){
cout<<"Pass类对象已经被销毁!"<<endl;
}
};
int main(){
Pass pass;
pass.set(1,2);
pass.print();
}
运行结果:
Box类对象已经创建!
Pass类对象已经被创建!
1 2
Pass类对象已经被销毁!
Box类的对象已经被销毁!
从运行结果来看,在派生类创建对象的时候,首先会调用基类的构造函数,初始化基类的部分,然后再调用派生类的构造函数来初始化派生类的部分;
重载:
重载只要包括两种类型,一种是重载函数另一种是重载运算符;
重载函数:函数重载是指在同一作用域中的多个函数具有相同的名称,但是参数列表和定义不同,编译器会根据参数的数量和顺序等进行匹配,这个过程叫做重载决策;
重载运算符:重载运算符可以使得类&对象之间使用内置运算符进行运算;并且运算符重载的操作数必须包含类或者结构体类型;
多态
多态是面向对象变成的核心概念,多态的核心在于通过基类指针或引用来调用虚函数,使得程序可以在运行时根据对象的实际类型动态选择调用适当的函数。这种机制使得代码更加灵活、可重用和易于扩展。
例如,我们讨论在不同情况下,调用函数的情况:
class Box{
public :
int length;
void print(){
cout<<"Base class show function called!"<<endl;
}
};
class Pass: public Box{
public :
void print(){
cout<<"Pass class show function called!"<<endl;
}
};
int main(){
Box* ptr;
Box box1;
Pass pass1;
ptr=&box1;
ptr->print();
ptr=&pass1;
ptr->print();
return 0;
}
运行结果:
Base class show function called!
Base class show function called!
从运行结果来看,在不设置虚函数的情况下,调用函数被设置为基类中的版本,这种情况叫做静态链接,也就是说在程序执行前(编译时),函数调用就已经确定好了;
class Box{
public :
int length;
virtual void print(){
cout<<"Base class show function called!"<<endl;
}
};
class Pass: public Box{
public :
void print(){
cout<<"Pass class show function called!"<<endl;
}
};
int main(){
Box* ptr;
Box box1;
Pass pass1;
ptr=&box1;
ptr->print();
ptr=&pass1;
ptr->print();
return 0;
}
运行结果:
Base class show function called!
Pass class show function called!
运行结果来看,可以通过设置虚函数,使用基类指针来调用基类或者派生类中的某一个同名函数;此外,还可以通过引用的方式来调用基类或者派生类中的同名函数;
class Shape{
public :
virtual void print(){
cout<<"Shape is not Fine!"<<endl;
}
};
class Circle: public Shape{
public :
void print(){
cout<<"Circle showed!"<<endl;
}
};
class Rectangle: public Shape{
public :
void print(){
cout<<"Rectangle showed!"<<endl;
}
};
class Trigle:public Shape{
public :
void print(){
cout<<"Trigle showed!"<<endl;
}
};
void printShow(Shape& shape){
shape.print();
}
int main(){
Shape shape1;
Circle circle1;
Rectangle rec1;
Trigle tri1;
printShow(shape1);
printShow(circle1);
printShow(rec1);
printShow(tri1);
return 0;
}
运行结果:
Shape is not Fine!
Circle showed!
Rectangle showed!
Trigle showed!
运行时根据实际对象类型来确定调用哪一个类的同名函数;
抽象
抽象也是面向对象编程的一个重要概念,抽象是给用户提供特定的外部接口,而隐藏内部的实现细节;
在c++中,抽象的实现主要借助于抽象类,抽象类是一种特殊的基类,在这个基类中声明了一个或者多个纯虚函数,而不进行定义,具体的定义在派生类中进行,这样可以在程序运行时,不需要过多的关注对象的实际类型,只需要将对象输入到统一的接口中,由程序自行去调用对应派生类中的函数;例如上面通过引用来调用派生类中的函数的代码中,printShow函数就相当于一个外部的接口,不需要去辨别对象的类型;但是上面的代码中的基类并非抽象类,因此需要做简单的修改:
需要注意的是抽象类无法直接创建对象,原因有两点,一是抽象类中包含纯虚函数,编译器无法实例化,另一方面是抽象类的目的只是提供一个外部接口;
class Shape{
public :
virtual void print()=0;
};
class Circle: public Shape{
public :
void print(){
cout<<"Circle showed!"<<endl;
}
};
class Rectangle: public Shape{
public :
void print(){
cout<<"Rectangle showed!"<<endl;
}
};
class Trigle:public Shape{
public :
void print(){
cout<<"Trigle showed!"<<endl;
}
};
void printShow(Shape& shape){
shape.print();
}
int main(){
Circle circle1;
Rectangle rec1;
Trigle tri1;
printShow(circle1);
printShow(rec1);
printShow(tri1);
return 0;
}
运行结果:
Circle showed!
Rectangle showed!
Trigle showed!
从代码中也可以看出printShow函数不关注具体哪一个派生类,根据对象的实际类型选择对应的派生类函数;这就是抽象;
封装
数据的封装是面向对象编程的基本概念,将数据和操作函数封装在一个类中,确保数据的私有性和完整性;