前言
在我之前的学习当中,在编写程序时,通常采用以下步骤:
1. 将问题的解法分解成若干步骤
2. 使用函数分别实现这些步骤
3. 依次调用这些函数 这种编程风格的被称作面向过程。
除了面向过程之外,还有一种被称作面向对象的编程风格被广泛使 用。面向对象采用基于对象的概念建立模型,对现实世界进行模拟,从而完成对问题的解决。
一 面向对象
面向过程(Procedure Oriented 简称PO :如C语言):
从名字可以看出它是注重过程的。当解决一个问题的时候,面向过程会把事情拆分成: 一个个函数和数据(用于方法的参数) 。然后按照一定的顺序,执行完这些方法(每个方法看作一个过程),等方法执行完了,事情就搞定了。
面向对象(Object Oriented简称OO :如C++,JAVA等语言):
看名字它是注重对象的。当解决一个问题的时候,面向对象会把事物抽象成对象的概念,就是说这个问题里面有哪些对象,然后给对象赋一些属性和方法,然后让每个对象去执行自己的方法,问题得到解决。
笔者在网上看到了这样一则生动形象的蛋炒饭例子,复制搬运过来:
用面向过程的方法写出来的程序是一份蛋炒饭,而用面向对象写出来的程序是一份盖浇饭。
蛋炒饭制作的细节不说了,最后的一道工序肯定是把米饭和鸡蛋混在一起炒匀。盖浇饭呢,则是把米饭和盖菜分别做好,你如果要一份红烧肉盖饭呢,就给你浇一份红烧肉;如果要一份青椒土豆盖浇饭,就给浇一份青椒土豆丝。
蛋炒饭的好处就是入味均匀,吃起来香。如果恰巧你不爱吃鸡蛋,只爱吃青菜的话,那么唯一的办法就是全部倒掉,重新做一份青菜炒饭了。盖浇饭就没这么多麻烦,你只需要把上面的盖菜拨掉,更换一份盖菜就可以了。盖浇饭的缺点是入味不均,可能没有蛋炒饭那么香。
到底是蛋炒饭好还是盖浇饭好呢?其实这类问题都很难回答,非要比个上下高低的话,就必须设定一个场景,否则只能说是各有所长。如果大家都不是美食家,没那么多讲究,那么从饭馆角度来讲的话,做盖浇饭显然比蛋炒饭更有优势,他可以组合出来任意多的组合,而且不会浪费。
盖浇饭的好处就是‘’菜”“饭”分离,从而提高了制作盖浇饭的灵活性。饭不满意就换饭,菜不满意换菜。用软件工程的专业术语就是”可维护性“比较好,”饭” 和”菜”的耦合度比较低。蛋炒饭将”蛋”“饭”搅和在一起,想换”蛋”“饭”中任何一种都很困难,耦合度很高,以至于”可维护性”比较差。软件工程追求的目标之一就是可维护性,可维护性主要表现在3个方面:可理解性、可测试性和可修改性。面向对象的好处之一就是显著的改善了软件系统的可维护性。
总结来说:
面向过程
优点:性能比面向对象高,因为类调用时需要实例化,开销比较大,比较消耗资源;比如单片机、嵌入式开发、 Linux/Unix等一般采用面向过程开发,性能是最重要的因素。
缺点:没有面向对象易维护、易复用、易扩展
面向对象
优点:易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统 更加灵活、更加易于维护
缺点:性能比面向过程低
面向对象三大特性:
-
封装
-
继承
-
多态
二 类和对象
使用类封装属性,基于类创建出一个个的对象来使用
(1)基本语法
//创建类
class 类名称
{
//访问权限
//类属性
//类行为
};
//基于类创建对象
类名称 对象名;
//调用
对象名.成员变量
对象名.成员方法
- class:关键字,表示要定义类了
- 访问权限
- 类的属性:定义在类中的变量 ->事物的属性
- 类的行为:定义在类中的函数 ->事物的行为
(2)使用示例(设计一个圆类,求圆的周长)
//class表示设计一个类,后面跟着的是类名
class circle
{
public: //访问权限 公共权限
//属性
int r;//半径
//行为
//求圆周长的函数
double giveC()
{
return 2 * PI * r;
}
};
int main()
{
//通过圆类创建出具体的对象
circle c1;
c1.r = 10;
cout << "圆的周长是:" << c1.giveC() << endl;
system("pause");
return 0;
}
三 封装
将现实世界的事物的属性和行为在类中描述为成员变量和成员方法,完成程序对现实世界事物的描述
封装是面向对象三大特性之一
(1)封装的意义:
- 将属性和行为写在一起,表现事物
语法见上:class 类名{ 访问权限:属性/行为};
- 将属性和行为加以权限控制
类在设计时,可以把属性和行为放在不同的权限下,加以控制
访问的权限有三种:
1.public 公共权限 类内可以访问 类外可以访问
2.protected 保护权限 类内可以访问 类外不可以访问
3.private 私有权限 类内可以访问 类外不可以访问
- 示例
#include<string>
class Person
{
//姓名公共权限
public:
string Name;
//汽车保护权限
protected:
string Car;
//银行卡密码私有权限
private:
string Password;
private:
void func()
{
Name = "张三";//在类内访问成员变量
Car = "奔驰";
Password = "123456";
}
};
int main()
{
Person p1;
p1.Name = "李四";//只有public公共权限下的Name可以在类外进行访问
system("pause");
return 0;
}
(2)struct 和 class 的区别:
在c++中 struct 和 class 的区别就在于 默认的访问权限不同
区别:
- struct 默认权限为公共
- class 默认权限为私有
(3)成员属性设置为私有:
优点一:将所有成员属性设置为私有,可以自己控制读写权限
优点二:对于写权限,我们可以检测数据的有效性
四 对象的初始化和清理
C++中的面向对象来源于生活,每个对象也都会有初始设置以及 对象销毁前的清理数据的设置。
(1)构造函数与析构函数
对象的初始化和清理也是两个非常重要的安全问题
一个对象或者变最没有初始状态,对其使用后果是未知
同样的使用完一个对象或变量,没有及时清理,也会造成一定的安全问题C++利用了构造函数和析构函数解决上述问题,这两个函数将会被编译器自动调用,完成对象初始化和清理,工作对象的初始化和清理工作是编译器强制要我们做的事情,因此如果我们不提供构造和析构,编译器会提供编译器提供的构造函数和析构函数是空实现
- 构造函数:主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无须手动调用
- 析构函数:主要作用在于对象销毁前系统自动调用,执行一些清理工作。
构造函数语法: 类名(){}
1.构造函数,没有返回值也不写void
2.函数名称与类名相同
3.构造函数可以有参数,因此可以发生重载
4.程序在调用对象时候会自动调用构造,无须手动调用,而且只会调用一次
析构函数语法:~类名()0}
1.析构函数,没有返回值也不写void
2.函数名称与类名相同,在名称前加上符号~
3.析构函数不可以有参数,因此不可以发生重载
4.程席在对象销前会动调用析构,无须手动调用而月只会调用一次
class person
{
public:
//构造函数
person()
{
cout << "构造函数的使用" << endl;
}
//析构函数
~person()
{
cout << "析构函数的使用" << endl;
}
};
void text()
{
person p;//通过函数内局部变量的声明
}
int main()
{
text();
system("pause");
return 0;
}
(2)构造函数的分类及调用
1 两种分类方式
- 按参数分为:有参构造和无参构造
- 按类型分为:普通构造和拷贝构造
2 三种调用方式
- 括号法
- 显示法
- 隐式转换法
class person
{
public:
//这里同时也体现了函数重载
//无参构造(默认构造)
person()
{
cout << "无参构造的使用" << endl;
}
//有参构造
person(int a)
{
cout << "有参构造的使用" << endl;
}
//拷贝构造(顾名思义,拷贝)
person(const person& p)
{
cout << "拷贝构造的使用" << endl;
}
//析构函数
~person()
{
cout << "析构函数的使用" << endl;
}
};
//调用无参构造函数
void text1()
{
person p;//调用无参构造函数
}
//调用有参构造函数
void text2()
{
//1.括号法(最为常用)
person p1(10);
//注意:调用无参构造时不能加括号,这样会使得编译器认为那是一个函数声明
//person p();
//2.显示法
person p2 = person(10);
person p3 = person(p2);
//person(10) 单独写就是匿名对象,当前行结束后,立即析构
//3.隐式转换法
person p4 = 10;//相当于person p4 = person (10)或者 person p4(10)
person p5 = p4;//同上
//注意:不能使用拷贝构造函数初始化匿名对象,编译器会认为是对象声明
}
int main()
{
text1();
text2();
system("pause");
return 0;
}
(3)拷贝构造函数的调用时机
c++中拷贝构造函数的调用实际主要有三种情况:
- 使用一个已经创建壳毕的对象来初始化一个新对象
- 值传递的方式给函数参数传值
- 以值方式返回局部对象
(4)构造函数的调用规则
默认情况下,c++编译器至少给一个类添加3个函数
- 默认构造函数(无参,函数体为空)
- 默认析构函数(无参,函数体为空)
- 默认拷贝构造函数,对属性进行值拷贝
构造函数调用规则如下:
- 如果用户定义有参构造函数,c++不在提供默认无参构造,但是会提供默认拷贝构造
- 如果用户定义有参构造函数,c++不在提供默认无参构造,但是会提供默认拷贝构造
- 如果用户定义拷贝构造函数,c++不会再提供其他构造函数
(5) 浅拷贝与深拷贝
深浅拷贝是面试经典问题,也是常见的一个坑
浅拷贝: 简单的赋值拷贝操作
深拷贝: 在堆区重新申请空间,进行拷贝操作
class person
{
public:
person()
{
cout << "无参构造函数" << endl;
}
person(int age,int height)
{
m_age = age;
m_height = new int(height);
cout << "有参构造函数" << endl;
}
//自己来定义拷贝构造函数
person(const person& p)
{
m_age = p.m_age;
//如果不利用深拷贝在堆区创建新内存,会导致浅拷贝带来的重复释放内存的问题
m_height = new int(*p.m_height);
}
~person()
{
if (m_height != NULL)
{
delete m_height;
m_height = NULL;
}
cout << "析构函数" << endl;
}
int m_age;
int* m_height;
};
void text()
{
person p1;
}
int main()
{
text();
return 0;
}
总结: 如果属性有在堆区开辟的,一定要自己提供拷贝构造函数,防止浅拷贝带来的重复释放内存的问题
(6)初始化列表
作用:c++提供了初始化列表语法,用来初始化属性
语法:构造函数():属性1(值1),属性2(值2),属性3(值3)……{ }
class person
{
public:
//传统的初始化操作,创建对象时给其赋予初值
//person(int a,int b,int c)
//{
// A = a;
// B = b;
// C = c;
//}
//初始化列表初始化属性
person(int a,int b,int c) : A(a),B(b),C(c)
{
}
private:
int A;
int B;
int C;
};
int main()
{
//person p1(10);
person p2(10,10,10);
return 0;
}
(7) 静态成员
静态成员就是在成员变量和成员函数前加上关键字static,称为静态成员
静态成员分为:
- 静态成员变量
所有对象共享同一份数据
在编译阶段分配内存
类内声明,类外初始化
- 静态成员函数
所有对象共享同一个函数
静态成员函数只能访问静态成员变量
class person
{
public :
int C;
static int m_A;//静态成员变量
//静态成员变量特点
//1.所有对象共享同一份数据
//2.在编译阶段分配内存
//3.类内声明,类外初始化
static void func1()//静态成员函数
{
cout << "静态函数static void func1的调用" << endl;
m_A = 100;//只能访问静态成员变量
//m_C = 180;//会报错因其访问了非静态成员
}
//静态成员函数特点:
//1.所有对象共享同一个函数
//2.静态成员函数只能访问静态成员变量
private:
//静态成员变量也是有访问权限的
static int m_B;
//静态成员函数也是有访问权限的
static void func2()
{
cout << "静态成员函数static void func2的调用" << endl;
}
};
//类内声明,内外初始化
int person::m_A = 10;//表示在person作用域下
int person::m_B = 10;
void text1()
{
//静态成员变量有两种访问方式
//1.通过对象访问
person p1;
p1.m_A = 100;
cout << p1.m_A << endl;//输出100
//2.通过类名访问
cout << person::m_A << endl;
}
void text2()
{
//静态成员函数有两种访问方式
//1.通过对象访问
person p1;
p1.func1();
//2.通过类名访问
person::func1();
//person::func2();//错误,私有权限访问不到
}
int main()
{
text1();
text2();
return 0;
}
五 c++的对象模型和this指针
(1)成员变量和成员函数分开存储
在c++中,类内的成员变量和成员是分开存储的
只有非静态成员变量才属于类的对象上
(2)this指针
我们知道在C++中成员变量和成员函数是分开存储的每一个非静态成员函数只会诞生一份函数实例,也就是说多个同类型的对象会共用一块代码,那么问题是: 这一块代码是如何区分那个对象调用自己的呢?
c++通过提供特殊的对象指针,this指针,解决上述问题。
this指针指向被调用的成员函数所属的对象
(this指针是隐含每一个非静态成员函数内的一种指针
this指针不需要定义,直接使用即可)
this指针的用途:
- 当形参和成员变量同名时,可用this指针来区分
- 在类的非静态成员函数中返回对象本身,可使用return *this
非静态成员函数都有一个隐藏指针类型参数this,对象p调用函数时,把&p传递给this,这一切都由编译器完成并隐藏
class person
{
public:
person(int a)
{
//1.当形参和成员变量同名时,this可以用来区分
this->a = a;
}
person& func(person& p)//这里需要返回对象本身,需要将返回值类型设置为person&
{
this->a += p.a;
//返回对象本身
//引用指向本身内存,不用引用就是拷贝了,而拷贝指向另一个内存
//*this指向对象本体
return *this;
}
int a;
};
void text()
{
person p1(10);
person p2(10);
//链式编程思想
p2.func(p1).func(p1).func(p1);
cout << p2.a << endl;
}
int main()
{
text();
return 0;
}
(3)空指针访问成员函数
如果函数体中用到了this指针,会写下下面这一段代码,用来防止NULL带来的错误,以提高代码的健壮性
if(this == NULL)
{
return;
}
(4)const 修饰成员函数
- 常函数
- 成员函数后加const后我们称为这个函数为常函数
- 常函数内不可以修改成员属性
- 成员属性声明时加关键字mutable后,在常函数中依然可以修改
- 常对象
- 声明对象前加const称该对象为常对象
- 常对象只能调用常函数
六 友元
生活中你的家有客厅(Public),有你的卧室(Private)客厅所有来的客人都可以进去,但是你的卧室是私有的,也就是说只有你能进去但是呢,你也可以允许你的好闺霍好基友进去。
在程序里,有些私有属性 也想让类外特殊的一些函数或者类进行访问,就需要用到友元的技术 。
友元的目的就是让一个函数或者类 访问另一个类中私有成员。
友元的关键字为 friend
友元的三种实现
- 全局函数做友元
- 类做友元
- 成员函数做友元
(1) 全局函数做友元
class Building
{
friend void func();//直接在类里面用friend 加上函数声明就是全局函数的友元了
public:
Building()
{
this->livingroom = "客厅";
this->bedroom = "卧室";
}
string livingroom;
private:
string bedroom;
};
void func()
{
Building b;
b.bedroom = "书房";//此时的func函数就可以对私有变量进行访问了
}
int main()
{
func();
return 0;
}
(2)类做友元
语法同上
(3)成员函数做友元
在上面的基础上指出成员函数的作用域即可
七 继承
一个类继承另外一个类的成员变量和成员方法
继承是面向对象三大特性之一
有些类与类之间存在特殊的关系,例如:
我们发现,定义这些类时,下级别的成员除了拥有上一级的共性,还有自己的特性。这个时候我们就可以考虑利用继承的技术,减少重复代码
(1)继承的基本语法
语法:class 子类 :继承方式 父类
子类也称为派生类 父类也称为基类
这时的我们就要用到继承的方式
class Phone
{
public:
void photography()
{
cout << "摄影" << endl;
}
void call_by_4g()
{
cout << "开启4g通话!" << endl;
}
private:
string producer = "apple";
};
//继承的好处:减少重复代码
//语法:class 子类 :继承方式 父类
class Phone2022 :public Phone
{
public:
void call_by_5g()
{
cout << "开启5g通话!" << endl;
}
};
void text()
{
Phone2022 p1;
p1.call_by_4g();//可见子类成功的继承了父类的功能
p1.call_by_5g();//并且也实现了新功能
}
int main()
{
text();
return 0;
}
总结:
继承的好处:可以减少重复的代码
class A : public B;
A 称为子类 或 派生类
B 称为父类 或 基类
派生类中的成员,包含两大部分:一类是从基类继承过来的,一类是自己增加的成员。从基类继承过过来的表现其共性,而新增的成员体现了其个性
(2)继承方式
继承方式一共有三种:
- 公共继承
- 保护继承
- 私有继承
(3)继承中的对象模型
从父类继承过来的那些属于子类成员呢?
class base
{
public:
int A;
protected:
int B;
private:
int C;
};
class son :public base
{
public:
int D;
};
void text()
{
son s;
//父类的所有非静态属性都会被子类继承
//私有成员只是被隐藏了,但还是被子类继承
cout << sizeof(s) << endl;//输出16
}
int main()
{
text();
return 0;
}
(4)继承中的构造和析构顺序
class father
{
public:
father()
{
cout << "father构造函数" << endl;
}
~father()
{
cout << "father析构函数" << endl;
}
};
class son:public father
{
public:
son()
{
cout << "son构造函数" << endl;
}
~son()
{
cout << "son析构函数" << endl;
}
};
void text()
{
son s;
}
int main()
{
text();
return 0;
}
//程序输出为:
//father构造函数
//son构造函数
//son析构函数
//father析构函数
(白发人送黑发人)
(引自B站,可以很好的啊诠释这个问题:就是儿子想出生(构造),就必须得他蝶出生(构造)了他才有可能出生,然后儿子不想活了跳楼(析构),他爸知道了后也不想活了(析构),父亲总不能好端端的就先跳楼吧)
总结:
继承中先调用父类构造函数,再调用子类构造函数,析构顺序相反
(5)继承中同名函数的处理方式
问题: 当子类与父类出现同名的成员,如何通过子类对象,访问到子类或父类中同名的数据呢?
- 访问子类同名成员 直接访问即可
- 访问父类同名成员 需要加作用域
- 当子类和父类有同名的成员函数,子类会隐藏父类的成员函数,加作用域可以访问到父类中的成员函数
(6)c++中的多继承
C++允许一个类继承多个类
- 语法: class 子类 : 继承方式 父类1 , 继承方式 类2..
(多继承中如果父类中出现了同名情况,子类使用时候要加作用域)
多继承可能会引发父类中有同名成员出现,需要加作用域区分
C++实际开发中不建议用多继承
(7)菱形继承
装形继承概念:
两个派生类继承同一个基类
又有某个类同时继承者两个派生类
这种继承被称为姜形继承,或者钻石继承
- 菱形继承带来的主要问题是子类继承两份相同的数据,导致资源浪费且毫无意义
- 利用虚继承可以解决菱形继承问题
八 多态
多种状态,即完成某个行为时,使用不同的对象会得到不同的状态
多态是面向对象三大特性之一
- 多态分为两类
- 静态多态: 函数重载 和 运算符重属于静态多态,复用函数名。
- 动态多态: 派生类和围函数实现运行时多态
- 静态多态和动态多态区别:
- 静态多态的函数地址早绑定 - 编译阶段确定函数地址。
- 动态多态的函数地址晚绑定 · 运行阶段确定函数地址
(1)多态基础
class animal
{
public:
//speak函数就是虚函数
//函数名前加上virtual关键字,变成虚函数,那么编译器在编译的时候就不能确定函数使用了
virtual void speak()
{
cout << "动物在说话" << endl;
}
};
class dog :public animal
{
public:
//重写 函数返回值类型 函数名 参数列表 完全相同
//子类重写父类的虚函数
void speak()
{
cout << "小狗在说话" << endl;
}
};
class cat:public animal
{
public:
void speak()
{
cout << "小猫在说话" << endl;
}
};
void DoSpeak(animal &animal)//记笔记:父类引用接受子类对象
{
//为什么能传进去:这是c++的新特性,父类指针或引用可以指向子类
//具体原因可以深究一下c++继承的构造过程,和对象的内存布局,内存中子类对象的前半部分和就是一个父类对象的克隆
animal.speak();
}
//多态满足条件
//1、有继承关系
//2、子类重写父类的虚函数
//
//多态使用:
//父类指针或引用指向子类对象
void text()
{
dog d;
d.speak();
cat c;
c.speak();
}
int main()
{
text();
return 0;
}
总结:
多态满足条件
- 有继承关系
- 子类重写父类的虚函数
多态使用:
- 父类指针或引用指向子类对象
(2)纯虚函数与抽象类
在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容
因此可以将虚函数改为纯虚函数
纯虚函数语法: virtual 逅回值类型 函数名(参数列表) = 0 ;
当类中有了纯虚函数,这个类也称为抽象类
抽象类特点:
- 无法实例化对象
- 子类必须重写抽象类中的纯虚函数,否则也属于抽象类
class base
{
public:
//纯虚函数语法: virtual 逅回值类型 函数名(参数列表) = 0 ;
//当类中有了纯虚函数,这个类也称为抽象类
//
//抽象类特点 :
//
//无法实例化对象
//子类必须重写抽象类中的纯虚函数,否则也属于抽象类
virtual void func() = 0;
};
class son:public base
{
public:
void func()
{
cout << "func的调用" << endl;
}
};
void text()
{
son s;
s.func();
}
int main()
{
text();
return 0;
}
总结
c++中面向对象的相关知识基本就是以上的内容了