类和对象
C++面向对象的三大特性为:封装、继承、多态
C++认为万事万物都皆为对象,对象上有其属性和行为
例如:
人可以作为对象,属性有姓名、年龄、身高、体重…,行为有走、跑、跳、吃饭、唱歌…
车也可以作为对象,属性有轮胎、方向盘、车灯…,行为有载人、放音乐、放空调…
具有相同性质的对象,我们可以抽象称为类,人属于人类,车属于车类
类中的属性和行为统一称为 成员
属性: 成员属性 成员变量
行为: 成员函数 成员方法
封装
封装
封装是C++面向对象三大特性之一
语法: class 类名{ 访问权限: 属性 / 行为 };
封装的意义:
1.在设计类的时候,属性和行为写在一起,表现事物
示例1:设计一个圆类,求圆的周长和面积
#include <iostream>
using namespace std;
const double pi = 3.14;
class circle
{
//访问权限
public:
//属性
int r; //圆的半径
//行为
//周长:
void c()
{
double c = 2 * pi *r;
cout << "周长:" << c << endl;
}
//面积
void s()
{
double s = pi * r*r;
cout << "面积:" << s << endl;
}
};
int main()
{
//实例化一个圆
circle c1;
cout << "请输入半径:" << endl;
cin >> c1.r;
//周长
c1.c();
//面积
c1.s();
system("pause");
return 0;
}
示例2:设计一个学生类,能够输入信息,显示信息
#include <iostream>
using namespace std;
#include <string>
//定义一个学生类
class student
{
//访问权限
public:
//属性
string name;//姓名
string num;//学号
//行为
void set()
{
cout << "输入学生姓名:" << endl;
cin >> name;
cout << "输入学生学号:" << endl;
cin >> num;
}
void show()
{
cout << "姓名:" << name << " 学号:" << num << endl;
}
};
int main()
{
//实例化一个学生
student s1;
//输入学生信息
s1.set();
//显示学生信息
s1.show();
system("pause");
return 0;
}
2.类在设计时,可以把属性和行为放在不同的权限下,加以控制
访问权限有三种:
公共权限 public 类内可以访问 类外可以访问
保护权限 protected 类内可以访问 类外不可以访问
私有权限 private 类内可以访问 类外不可以访问
struct和class区别
在C++中 struct和class唯一的区别就在于默认的访问权限不同
struct 默认权限为公共
class 默认权限为私有
成员属性设置为私有
优点1:将所有成员属性设置为私有,可以自己控制读写权限
优点2:对于写权限,我们可以检测数据的有效性s
#include <iostream>
using namespace std;
#include <string>
class lei
{
public:
//写姓名
void writename(string name)
{
lei_name = name;
}
//读姓名
string redname()
{
return lei_name;
}
//写年龄
void writeage(int age)
{
if (age < 0 || age>100)
{
cout << "年龄输入有误" << endl;
}
lei_age = age;
}
//读年龄
int redage()
{
return lei_age;
}
//访问权限
private:
//姓名
string lei_name;
//年龄
int lei_age;
};
int main()
{
//实例化一个人
lei people;
people.writename ("赵宏磊");// 写姓名
cout << "姓名:" << people.redname() << endl;//读姓名
people.writeage(21);
cout << "年龄:" << people.redage() << endl;
system("pause");
return 0;
}
案例一:
设计立方体类(Cube)
求出立方体的面积和体积
分别用全局函数和成员函数判断两个立方体是否相等。
#include <iostream>
using namespace std;
//定义立方体类
class cube
{
private:
//长
int m_l;
//宽
int m_w;
//高
int m_h;
public:
//写长读长
void writel(int w_l)
{
m_l = w_l;
}
int readl()
{
return m_l;
}
//写宽读宽
void writew(int w_w)
{
m_w = w_w;
}
int readw()
{
return m_w;
}
//写高读高
void writeh(int w_h)
{
m_h = w_h;
}
int readh()
{
return m_h;
}
//获取面积
int getarea()
{
return ((m_l * m_w) + (m_l * m_h) + (m_w * m_h)) * 2;
}
//获取体积
int getvolt()
{
return m_l * m_w * m_h;
}
//成员函数定义比较函数体
void bijiao(cube cu2)
{
if (m_l == cu2.readl() && m_w == cu2.readw() && m_h == cu2.readh())
{
cout << "立方体1等于立方体2" << endl;
}
else
{
cout << "立方体1不等于立方体2" << endl;
}
}
};
//全局函数定义比较函数体
void bijiao(cube &cu1, cube &cu2)
{
if (cu1.readl() == cu2.readl() && cu1.readw() == cu2.readw() && cu1.readh() == cu2.readh())
{
cout << "立方体1等于立方体2" << endl;
}
else
{
cout << "立方体1不等于立方体2" << endl;
}
}
int main()
{
int w_l, w_w, w_h;
//实例化立方体1
cube cu1;
cout << "请输入立方体1长:";
cin >> w_l;
cu1.writel(w_l);
cout << "请输入立方体1宽:";
cin >> w_w;
cu1.writew(w_w);
cout << "请输入立方体1高:";
cin >> w_h;
cu1.writeh(w_h);
cout << "立方体1 的面积为:" << cu1.getarea() << endl;
cout << "立方体1 的体积为:" << cu1.getvolt() << endl;
//实例化立方体2
cube cu2;
cout << "请输入立方体2长:";
cin >> w_l;
cu2.writel(w_l);
cout << "请输入立方体2宽:";
cin >> w_w;
cu2.writew(w_w);
cout << "请输入立方体2高:";
cin >> w_h;
cu2.writeh(w_h);
cout << "立方体2 的面积为:" << cu2.getarea() << endl;
cout << "立方体2 的体积为:" << cu2.getvolt() << endl;
cout << endl;
//比较立方体大小
bijiao(cu1, cu2);
system("pause");
return 0;
}
案例二:
设计一个圆形类(Circle)
和一个点类(Point)
计算点和圆的关系。
#include <iostream>
using namespace std;
const double PI = 3.14;
//#include "point.h"
//#include "circle.h"
//定义一个点类
class Point
{
private:
int m_X;//X坐标
int m_Y;//Y坐标
public:
//写坐标
void writeX(int w_X)
{
m_X = w_X;
}
void writeY(int w_Y)
{
m_Y = w_Y;
}
//读坐标
int readX()
{
return m_X;
}
int readY()
{
return m_Y;
}
};
//定义一个圆类
class Circle
{
private:
int m_R;//半径
Point m_center;//圆心
public:
//写圆心
void writeCenter(Point center)
{
m_center = center;
}
//读取圆心
Point readCenter()
{
return m_center;
}
//写半径
void writeR(int w_R)
{
m_R = w_R;
}
//读取半径
int readR()
{
return m_R;
}
};
//定义点到直线距离全局函数
//定义判断点和圆关系
void isInCircle(Circle &c, Point &p)
{
//计算两点之间距离
int pdistance = (c.readCenter().readX() - p.readX()) * (c.readCenter().readX() - p.readX()) + (c.readCenter().readY() - p.readY()) * (c.readCenter().readY() - p.readY());
int rdistance = c.readR() * c.readR();
if (pdistance == rdistance)
{
cout << "点在圆上" << endl;
}
else if (pdistance > rdistance)
{
cout << "点在圆外" << endl;
}
else
{
cout << "点在圆内" << endl;
}
}
int main()
{
//实例化一个圆
Circle c;
//设置半径
c.writeR(10);
//设置圆心
Point center;
center.writeX(10);
center.writeY(0);
c.writeCenter(center);
//实例化一个点
Point p;
p.writeX(10);
p.writeY(10);
isInCircle(c, p);
}
构造函数和析构函数
对象的初始化和清理也是两个非常重要的安全问题一个对象或者变量没有初始状态,对其使用后果是未知的,同样的使用完一个对象或变量,没有及时清理,也会造成一定的安全问题
c++利用了构造函数和析构函数解决上述问题
这两个函数将会被编译器自动调用,完成对象初始化和清理工作。
对象的初始化和清理工作是编译器强制要我们做的事情,因此如果我们不提供构造和析构,编译器会提供
编译器提供的构造函数和析构函数是空实现。
- 构造函数:主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无须手动调用。
- 析构函数:主要作用在于对象销毁前系统自动调用,执行一些清理工作。
构造函数语法:类名(){};
- 构造函数,没有返回值也不写void
- 函数名称与类名相同
- 构造函数可以有参数,因此可以发生重载
- 程序在调用对象时候会自动调用构造,无须手动调用,而且只会调用一次
class Person
{
public:
//构造函数
Person()
{
cout << "Person的构造函数调用" << endl;
}
};
析构函数语法: ~类名(){};
5. 析构函数,没有返回值也不写void
6. 函数名称与类名相同,在名称前加上符号 ~
7. 析构函数不可以有参数,因此不可以发生重载
8. 程序在对象销毁前会自动调用析构,无须手动调用,而且只会调用一次
class Person
{
public:
//析构函数
~Person()
{
cout << "Person的析构函数调用" << endl;
}
};
构造函数的分类及调用
两种分类方式:
按参数分为: 有参构造和无参构造
Person()
{
cout << "Person 无参构造函数的调用" << endl;
}
Person(int a)
{
age = 10;
cout << "Person 有参构造函数的调用" << endl;
}
按类型分为: 普通构造和拷贝构造
//普通构造函数
Person()
{
cout << "Person 构造函数的调用" << endl;
}
//拷贝构造函数
Person(const Person &p)
{
cout << "Person 拷贝构造函数的调用" << endl;
age = p.age;
}
三种调用方式:
- 括号法
注意: 调用无参构造函数不能加括号 加了编译器会认为这是一个函数声明 Person p1();
Person p1;//默认构造函数的引用
Person p2(10);//有参构造函数
Person p3(p2);//拷贝构造函数
cout << "p2年龄:" << p2.age << endl;
cout << "p3年龄:" << p3.age << endl;
- 显示法
注意:
Person(10);匿名对象 特点:当前行执行结束后,系统会立即回收掉匿名对象
Person(p3);不要利用拷贝构造函数初始化匿名对象 编译器会认为Person(p3) == Person p3; 对象重定义.
Person p1;
Person p2 = Person(10);//有参构造函数
Person p3 = Person(p2);//调用拷贝函数
cout << "p2年龄:" << p2.age << endl;
cout << "p3年龄:" << p3.age << endl;
- 隐式转换法
Person p4 = 10; //相当于写了 Person p4 = Person (10); 有参构造
Person p5 = p4; // 拷贝构造
拷贝构造函数调用时机
C++中拷贝构造函数调用时机通常有三种情况
1.使用一个已经创建完毕的对象来初始化一个新对象
2.值传递的方式给函数参数传值
3.以值方式返回局部对象
构造函数调用规则
默认情况下,c++编译器至少给一个类添加3个函数
1.默认构造函数(无参,函数体为空)
2.默认析构函数(无参,函数体为空)
3.默认拷贝构造函数,对属性进行值拷贝
构造函数调用规则如下:
如果用户定义有参构造函数,c++不在提供默认无参构造,但是会提供默认拷贝构造
如果用户定义拷贝构造函数,c++不会再提供其他构造函数
深拷贝与浅拷贝
深浅拷贝是面试经典问题,也是常见的一个坑
- 浅拷贝: 简单的赋值拷贝操作
- 深拷贝: 在堆区重新申请空间,进行拷贝操作
- 注意: 如果属性有在堆区开辟的,一定要自己提供拷贝构造函数,防止浅拷贝带来的问题
#include <iostream>
using namespace std;
class Person
{
public:
//属性
int m_age;
int *m_height;
//构造函数
Person()
{
cout << "默认构造函数" << endl;
}
//有参构造函数
Person(int age, int height)
{
cout << "有参构造函数" << endl;
m_age = age;
m_height = new int(height);
}
//拷贝构造函数
Person(const Person &p)
{
cout << "拷贝构造函数" << endl;
m_age = p.m_age;//编译器简单赋值
//编译器浅拷贝操作
//m_hight = p.m_height;
//如果不利用深拷贝在堆区创建新内存,会导致浅拷贝带来的重复释放堆区问题
m_height = new int(*p.m_height);
}
//析构函数
~Person()
{
cout << "析构函数" << endl;
if (m_height != NULL)
{
delete m_height;
}
}
};
void test01()
{
Person p(21, 175);
cout << "p的年龄:" << p.m_age << " p的身高:" << *p.m_height << endl;
Person copy(p);
cout << "copy的年龄:" << copy.m_age << " copy的身高:" << *copy.m_height << endl;
}
int main()
{
test01();
system("pause");
return 0;
}
初始化列表
作用:
C++提供了初始化列表语法,用来初始化属性
语法:构造函数():属性1(值1),属性2(值2)... {}
Person(int a, int b, int c) :m_A(a), m_B(b), m_C(c)
{
}
//调用
Person p(1, 2, 3);
类对象作为类成员
C++类中的成员可以是另一个类的对象,我们称该成员为对象成员
其他类对象作为本类成员,构造函数先构造类对象,再构造自身。析构函数与构造函数相反
class Phone
{
public:
//手机名字
string m_pname;
Phone(string pname)
{
m_pname = pname;
}
~Phone()
{
cout << "Phone析构函数的调用" << endl;
}
};
class Person
{
public:
//属性
//姓名
string m_name;
//手机
Phone m_Phone;
Person(string name, string pname) :m_name(name), m_Phone(pname)
{
//m_name = name;
//m_Phone = pname;
}
~Person()
{
cout << "Person析构函数的调用" << endl;
}
};
静态成员
静态成员就是在成员变量和成员函数前加上关键字static,称为静态成员
静态成员分为:
静态成员变量
所有对象共享同一份数据
在编译阶段分配内存
类内声明,类外初始化
class Person
{
public:
static int m_A;//静态成员变量类内声明
};
int Person::m_A = 10; //静态成员变量类外初始化
静态成员函数
所有对象共享同一个函数
静态成员函数只能访问静态成员变量
lass Person
{
public:
//静态成员函数只能访问静态成员变量
static void func() //静态成员函数
{
m_A = 100;
//m_B = 100; 不可以访问非静态成员变量
cout << "func的调用" << endl;
}
static int m_A;//静态成员变量
int m_B;
};
int Person::m_A = 10;
静态成员函数访问方式
1.通过对象
2.通过类名
void test()
{
//1.通过对象
Person p1;
p1.func();
//2.通过类名
Person::func();
cout << p1.m_A << endl;
cout << Person::m_A << endl;
}
C++对象模型和this指针
成员变量和成员函数分开存储
在C++中,类内的成员变量和成员函数分开存储,只有非静态成员变量才属于类的对象上,其他成员都不在类的对象上。
非静态成员变量占对象空间
静态成员变量不占对象空间
函数也不占对象空间,所有函数共享一个函数实例
静态成员函数也不占对象空间
- 空对象占用内存空间: 1
this指针概念
this指针指向被调用的成员函数所属的对象
this指针是隐含每一个非静态成员函数内的一种指针
this指针不需要定义,直接使用即可
this指针的用途
当形参和成员变量同名时,可用this指针来区分
class Person
{
public:
int age;
Person(int age)
{
this->age = age; //当形参和成员变量同名时,可用this指针来区分
}
};
//调用类
void test01()
{
Person p1(10);
cout << p1.age << endl;
}
在类的非静态成员函数中返回对象本身,可使用return *this,谁调用函数返回谁。
class Person
{
public:
int age;
Person(int age)
{
this->age = age;
}
Person& addpersonage(Person p)
{
this->age += p.age;
return *this; //p2调用函数 返回的就是调用它的运算后的p2本身
}
};
//调用类
void test02()
{
Person p1(10);
Person p2(10);
//链式编程思想
p2.addpersonage(p1).addpersonage(p1).addpersonage(p1).addpersonage(p1);
cout << p2.age << endl;
}
空指针访问成员函数
C++中空指针也是可以调用成员函数的,但是也要注意有没有用到this指针
如果用到this指针,需要加以判断保证代码的健壮性
void ShowPerson() {
if (this == NULL)
{
return;
}
cout << mAge << endl; //mAge 默认 this->mAge ,用到了this指针
}
const修饰成员函数
常函数:
- 成员函数后加const后我们称为这个函数为常函数
- 常函数内不可以修改成员属性
- 成员属性声明时加关键字mutable后,在常函数中依然可以修改
class Person
{
public:
Person()
{
m_A = 0;
m_B = 0;
}
public:
int m_A;
mutable int m_B; //可修改 可变的
//this指针的本质是一个指针常量,指针的指向不可修改 指针指向的值可以修改
//如果想让指针指向的值也不可以修改,需要声明常函数
void ShowPerson() const //const Type* const pointer; 既不能改指向 也不能改值
{
//this = NULL; //不能修改指针的指向 Person* const this;
//this->m_A = 100; //但是this指针指向的对象的数据是可以修改的
//const修饰成员函数,表示指针指向的内存空间的数据不能修改,除了mutable修饰的变量
this->m_B = 100;
}
void MyFunc()
{
//m_A = 100 ;
}
};
常对象:
- 声明对象前加const称该对象为常对象
- 常对象只能调用常函数
//const修饰对象 常对象
void test01() {
const Person person; //常量对象
cout << person.m_A << endl;
//person.mA = 100; //常对象不能修改成员变量的值,但是可以访问
person.m_B = 100; //但是常对象可以修改mutable修饰的成员变量
//常对象访问成员函数
person.ShowPerson();//常对象只能调用常函数
//person.MyFunc(); //常对象不能调用普通函数
}
继承
继承是面向对象三大特性之一
有些类与类之间存在特殊的关系
我们发现,定义这些类时,下级别的成员除了拥有上一级的共性,还有自己的特性。
这个时候我们就可以考虑利用继承的技术,减少重复代码
继承的基本语法
语法:class 派生类名 : 继承方式 基类名 {}
class son: public father
{
}
class son : public father;
son 类称为子类 或 派生类
father 类称为父类 或 基类
派生类中的成员,包含两大部分:
一类是从基类继承过来的,一类是自己增加的成员。
从基类继承过过来的表现其共性,而新增的成员体现了其个性。
继承方式
继承方式一共有三种:
- 公共继承
- 保护继承
- 私有继承
继承中构造和析构顺序
子类继承父类后,当创建子类对象,也会调用父类的构造函数
继承中 先调用父类构造函数,再调用子类构造函数,析构顺序与构造相反
继承同名成员处理方式 - 访问子类同名成员 直接访问即可
- 访问父类同名成员 需要加作用域
- 子类对象可以直接访问到子类中同名成员
- 子类对象加作用域可以访问到父类同名成员
- 当子类与父类拥有同名的成员函数,子类会隐藏父类中同名成员函数,加作用域可以访问到父类中同名函数
继承同名静态成员处理方式
静态成员和非静态成员出现同名,处理方式一致
- 访问子类同名成员 直接访问即可
- 访问父类同名成员 需要加作用域
同名静态成员处理方式和非静态处理方式一样,只不过有两种访问的方式(通过对象 和 通过类名)
多继承语法
一个类继承多个类
语法:class 子类 :继承方式 父类1 , 继承方式 父类2...
多继承可能会引发父类中有同名成员出现,需要加作用域区分
多继承中如果父类中出现了同名情况,子类使用时候要加作用域
菱形继承
菱形继承概念:
两个派生类继承同一个基类
又有某个类同时继承者两个派生类
这种继承被称为菱形继承,或者钻石继承
菱形继承带来的主要问题是子类继承两份相同的数据,导致资源浪费以及毫无意义
利用虚继承可以解决菱形继承问题
多态
多态是C++面向对象三大特性之一
多态分为两类
- 静态多态: 函数重载 和 运算符重载属于静态多态,复用函数名
- 动态多态: 派生类和虚函数实现运行时多态
静态多态和动态多态区别:
- 静态多态的函数地址早绑定 - 编译阶段确定函数地址
- 动态多态的函数地址晚绑定 - 运行阶段确定函数地址
我们希望传入什么对象,那么就调用什么对象的函数
如果函数地址在编译阶段就能确定,那么静态联编
如果函数地址在运行阶段才能确定,就是动态联编
多态满足条件
-
有继承关系
-
子类重写父类中的虚函数
多态使用条件 -
父类指针或引用指向子类对象
重写:函数返回值类型 函数名 参数列表 完全一致称为重写
纯虚函数和抽象类
在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容
因此可以将虚函数改为纯虚函数
纯虚函数语法:virtual 返回值类型 函数名 (参数列表)= 0 ;
当类中有了纯虚函数,这个类也称为抽象类
抽象类特点: -
无法实例化对象
-
子类必须重写抽象类中的纯虚函数,否则也属于抽象类
虚析构和纯虚析构
多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码
解决方式:将父类中的析构函数改为虚析构或者纯虚析构
虚析构和纯虚析构共性: -
可以解决父类指针释放子类对象
-
都需要有具体的函数实现
虚析构和纯虚析构区别:
- 如果是纯虚析构,该类属于抽象类,无法实例化对象
虚析构语法:virtual ~类名(){}
纯虚析构语法:virtual ~类名() = 0;
类名::~类名(){}
1. 虚析构或纯虚析构就是用来解决通过父类指针释放子类对象
2. 如果子类中没有堆区数据,可以不写为虚析构或纯虚析构
3. 拥有纯虚析构函数的类也属于抽象类