类和对象
C++是一门面向对象的语言,提到面向对象就不得不提到三大特点:封装、继承、多态。
首先来看第一大特点:封装
封装
在C++中类使用关键字class修饰 class [类名]{ }
举个例子:
以学生类为例
class Students{
//权限
public:
//类中的属性和行为都成为成员,成员变量、成员属性
//成员函数、成员方法。
//属性
string m_name;//姓名
int m_id;//学号
//行为
void showStuden(){
cout<< "学号:"<<m_id << "姓名" << m_name<<endl;
}
我们可以看到一个类里,包含了权限,属性,行为。
通俗的理解:
权限指的是类内部的成员的权限。这个成员可以是变量即成员变量或者是成员函数。属性指的是类的内部所需要的一些特性,或者说是字段,比如学生的姓名,学号,员工的工号,手机号等等诸如此类。行为指的是一些函数或者方法,来操纵这个类的成员。
假设我们通过类以及函数封装来实现一个求圆的周长和面积,应该怎么写:
const double PI = 3.14;
class Circle{
//访问权限
public:
//属性
double m_r;
//行为
double calculateZC(){
return 2*PI*m_r;
}
double calculateMJ(){
return PI*m_r*m_r;
}
};
其中,PI代表圆周率,是个常量,我们就用const来修饰,double类型。
然后不论是求面积还是周长,都需要圆的一个属性,那就是半径,所以我们需要给这个类的属性中一个半径r。
最后分别实现一个计算周长和面积的行为,也就是函数:calculateZC()和calculateMJ()。
面向对象中,类还是比较容易理解的,主要是通过抽象将一类事物统一的归为一个类,由一个类来管理操作。在实际使用时,我们需要实例化这个类,因为类是抽象的,是一个比较大的范围,但是用的时候,我闷需要创建一个对象,这个对象属于这个类,实际操纵的是这个对象,而非这个类。
访问权限
类在设计时,可以把属性和行为放在不同的权限里,加以控制,访问权限有三种:
- 访问权限类型:
- public:公共权限
- protected 保护权限
- private 私有权限
- 关于public:不仅类的内部成员之前可以访问,类的外部也可以访问。
- 关于protected :类内部可以访问,类外部不能访问。子类可以访问父类中的保护内容
- 关于private :类内部可以访问,类外部不可访问,子类也不可以访问父类中的私有内容
以学生类为例
class Students{
public:
//类中的属性和行为都成为成员,成员变量、成员属性
//成员函数、成员方法。
//属性
string m_name;//姓名
int m_id;//学号
//行为
void showStuden(){
cout<< "学号:"<<m_id << "姓名" << m_name<<endl;
}
void setName(string name){
m_name=name;
}
void setId(int id){
m_id = id;
}
};
这里给学生类中的成员均是public也就是公共权限,那就是说类内部可以访问,类外部也可以访问。
举个例子:
假设目前有个person类,包含公共权限的属性:姓名,包含保护权限的属性:房子、车子,包含私有属性的银行卡密码。
class Person{
public:
string m_Name;//姓名
protected:
string m_House;//房子
string m_Car;//车子
private:
int m_password;//银行卡密码
//方法
public:
void func(){
m_Name="张三";
m_Car="拖拉机";
m_House="别墅";
m_password=123456;
}
};
int main(){
Person p1;
p1.m_Name="李四";
p1.m_Car="法拉利";//protected在Persion类外就是无法访问的
p1.m_password=421331;//同样的private 在Persion类外也是无法访问的
}
那么这样的话 ,我们可以看到在main函数中,姓名字段是可以用的。但是车和密码字段是不能直接在main函数中使用的,因为脱离了person类的访问。
同样对于方法func(),其中包含了一些保护权限和私有权限的属性,但是这个方法是在Person类内部的,但它同时又是public权限的,所以main函数中可以访问func这个函数(即便其中包含了私有属性)。
struct和class区别
在C++中,struct和class的唯一区别就是默认的访问权限不同,struct默认权限是公共,class默认权限是私有。
class C1{
//一个权限都不给。就是默认权限
int m_A;
};
struct C2{
int m_A;
};
int main(){
//struct 默认权限是public
//class 默认权限是private
C1 c1;
c1.m_A=100;//无法访问
C2 c2;
c2.m_A=100;//不报错
}
我们给出一个类 C1,给出一个struct C2,都定义了一个int类型的m_A,在main函数中c1的m_A是无法访问的,c2的m_A是可以访问修改的。
成员属性设置为私有
成员属性设置为私有的目的是,可以自己控制读写权限。有些成员你可以自行设置成可读可写或者仅读,仅写。并且对于需要修改的也就是写,可以检测数据有效性。
自由设置权限
例如:
class Person{
public:
//可写
void setName(string name){
m_Name=name;
}
//可读
string getName(){
return m_Name;
}
int getAge(){
return m_Age;
}
void setIdol(string idol){
m_idol=idol;
}
private:
string m_Name;//可读写
int m_Age=20;//只读
string m_idol;//只写
};
上面这个例子。就对name设置了可读可写,对年龄设置了仅读,对idol设置了仅写权限。
通过成员方法,来间接控制成员属性的权限。默认所有的属性都是私有,这样一来比较容易控制其权限分配。
总结:只读就是get,只写就是set,可读写就是既有get又有set。这个在开发中是经常使用的。
检测数据有效性
还是上面的例子,假设我们要对年龄的设置加一个限制,要输入0~100岁之间,应如何去做。
class Person{
public:
//可写
void setName(string name){
m_Name=name;
}
//可读
string getName(){
return m_Name;
}
int getAge(){
return m_Age;
}
void setAge(int age){
if (age>=0&&age<=100)
m_Age=age;
else
cout<<"年龄设置不规范,请设置在0~100岁"<<endl;
}
void setIdol(string idol){
m_idol=idol;
}
private:
string m_Name;//可读写
int m_Age;//可读可写,但是写的必须是0~100之间
string m_idol;//只写
};
这里就需要写一个set函数,首先让年龄可以写,然后对要设置的年龄加一个限制。
封装的案例
案例1
设置一个立方体类,能够求其表面积和体积,并且写出全局函数和成员函数来比较两个立方体是否相同。
#include "iostream"
using namespace std;
class Cube{
public:
//成员函数
//全部可读写:
void setChang(double chang){
if (chang<=0){
cout<<"立方体的长设置有误,必须大于0"<<endl;
return;
}
else
m_Chang=chang;
}
double getChang(){
return m_Chang;
}
void setKuan(double kuan){
if (kuan<=0){
cout<<"立方体的宽设置有误,必须大于0"<<endl;
return;
}
else
m_Kuan=kuan;
}
double getKuan(){
return m_Kuan;
}
void setGao(double gao){
if (gao<=0){
cout<<"立方体的高设置有误,必须大于0"<<endl;
return;
}
else
m_Gao=gao;
}
double getGao(){
return m_Gao;
}
//求面积
double CalcMJ(){
if (m_Chang>0&&m_Kuan>0&&m_Gao>0)
return 2*(m_Chang*m_Kuan+m_Chang*m_Gao+m_Kuan*m_Gao);
else{
cout<<"长宽高均不能为0"<<endl;
}
}
//求体积
double CalcTJ(){
if (m_Chang>0&&m_Kuan>0&&m_Gao>0)
return m_Gao*m_Kuan*m_Chang;
else{
cout<<"长宽高均不能为0"<<endl;
}
}
//利用成员函数判断是否相等
bool isSameByClass(Cube c2){
if(m_Chang==c2.getChang()
&&m_Gao==c2.getGao()
&&m_Kuan==c2.getKuan())
return true;
else
return false;
}
private:
double m_Chang=1;
double m_Kuan=1;
double m_Gao=1;
};
//全局函数
bool isSame(Cube &c1,Cube &c2){
if(c1.getChang()==c2.getChang()
&&c1.getGao()==c2.getGao()
&&c1.getKuan()==c2.getKuan())
return true;
else
return false;
}
int main(){
Cube cube1;
cube1.setChang(5.5);
cube1.setKuan(6.2);
cube1.setGao(7.5);
cout<<"立方体的表面积为"<<cube1.CalcMJ()<<endl;
cout<<"立方体的体积为"<<cube1.CalcTJ()<<endl;
Cube cube2;
cube2.setChang(5.5);
cube2.setKuan(6.2);
cube2.setGao(7.5);
bool ret= isSame(cube1,cube2);
if (ret){
cout<<"两个立方体相等"<<endl;
}else{
cout<<"两个立方体不相等"<<endl;
}
bool retClass=cube1.isSameByClass(cube2);
if (retClass){
cout<<"两个立方体相等"<<endl;
}else{
cout<<"两个立方体不相等"<<endl;
}
return 0;
}
其中,对于长宽高的判断,其实只需要在单独设置里判断即可,只要单独的设置了判断,那么面积体积处就不需要设置判断了。
另外,可以看出如果是在类外的全局函数,来比较两个立方体是否相等,我闷需要传入,两个对象,这里使用了引用,不需要额外拷贝,如果使用了成员函数的话,我们可以直接在类内访问到私有权限的属性,这样只需要传入相比较的对象即可,稍微简化了一步骤。
案例2
点和圆的关系,设计一个圆类,和一个点类,计算圆和点之间的关系。
其中一种解法(我自己写的,如有问题,请留言):
//令坐标轴原点为圆心,作圆。
class Circle{
public:
double getR(){
return m_R;
}
void setR(double r){
m_R=r;
}
private:
double m_R=1;
};
class Point{
public:
double getX(){
return m_X;
}
void setX(double x){
m_X=x;
}
double getY(){
return m_Y;
}
void setY(double y){
m_Y=y;
}
void CircleAndPoint(Circle circle){
//求出点相对于坐标轴也就是圆心的距离
//与半径做比较,如果大于半径,那就在圆外
//如果等于半径,那就在圆上
//如果小于半径,那就在圆内
double XFang=abs(m_X)*abs(m_X);
double YFang=abs(m_Y)*abs(m_Y);
double distance= sqrt(XFang+YFang);
if (circle.getR()>distance){
cout<<"点在圆内"<<endl;
}else if(circle.getR()==distance){
cout<<"点在圆的边上"<<endl;
}else{
cout<<"点在圆外"<<endl;
}
}
private:
//默认坐标原点
double m_X=0;
double m_Y=0;
};
int main(){
Circle c1;
c1.setR(5);
Point p1;
p1.setX(3);
p1.setY(4);
p1.CircleAndPoint(c1);
return 0;
}
当然这种方式是默认圆心,是在原点,那当然还有可能圆心不在原点。应当如何做呢?
class Point{
public:
double getX() const {
return m_X;
}
void setX(double x) {
m_X = x;
}
double getY() const {
return m_Y;
}
void setY(double y) {
m_Y = y;
}
private:
// 默认坐标原点
double m_X = 0;
double m_Y = 0;
};
class Circle{
public:
double getR() const {
return m_R;
}
void setR(double r) {
m_R = r;
}
Point getCenter() const {
return m_Center;
}
void setCenter(Point center) {
m_Center = center;
}
private:
double m_R = 1;
Point m_Center;
};
void CircleAndPoint(const Circle& circle, const Point& point) {
Point center = circle.getCenter();
double XDistance = (point.getX() - center.getX()) * (point.getX() - center.getX());
double YDistance = (point.getY() - center.getY()) * (point.getY() - center.getY());
double distance = sqrt(XDistance + YDistance);
if (circle.getR() > distance) {
cout << "点在圆内" << endl;
} else if (circle.getR() == distance) {
cout << "点在圆的边上" << endl;
} else {
cout << "点在圆外" << endl;
}
}
int main() {
Point center;
center.setX(3);
center.setY(2);
Circle c1;
c1.setR(5);
c1.setCenter(center);
Point p1;
p1.setX(3);
p1.setY(4);
CircleAndPoint(c1, p1);
return 0;
}
对象的初始化和清理
C++的面向对象也是来源于生活,生活中我们需要什么东西都会获取也就是初始化,不需要后就会丢掉,也就是清理,c++中也同样如此,每个对象都有自己的初始化和清理工作。
构造函数和析构函数
对象的初始化和清理是比较重要的安全问题,一个对象或者变量没有初始状态,对其使用的后果是未知的,同样,使用完一个对象和变量,没有及时清理,也会造成一定的安全问题。
在C++中利用了构造函数和析构函数解决上述问题,这两个函数将会被编译器自动调用,完成对象的初始化和清理工作,这是编译器强制要求我们去做的事,因此如果我们不提供构造和析构,编译器会提供构造函数和析构函数是空实现。
构造函数:主要作用是在创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无需手动调用。
析构函数:主要作用在对象销毁前系统自动调用,执行清理工作。
构造函数语法:类名(){}
1.构造函数,没有返回值,也不需要写void
2.函数名与类名相同
3.构造函数可以有参数,因此可以发生重载
4.程序在调用对象时候会自动调用构造,无需手动调用,而且只会调用一次。
析构函数语法:~类名(){}
1.析构函数,没有返回值,也不需要写void
2.函数名称与类名相同,在名称前加上符号~
3.析构函数不可以有参数,因此不可能发生重载
4.程序在销毁前会自动调用析构,无需手动调用,而且只会调用一次。
举个例子
class Person{
public:
Person(){
cout<<"Person的构造函数调用"<<endl;
}
~Person(){
cout<<"Person的析构函数调用"<<endl;
}
};
//
int main(){
Person person;//栈上的数据,test01执行完毕后,会释放
return 0;
}
这里我们会直接得到输出是:
这里我们并没有去调用这个函数,但是其内容就是被调用过了。说明这个对象是会自动初始化和释放的。
如果我们自己不写构造函数和析构函数,编译器自动帮我们写了,只不过函数体是空的。
构造函数的分类以及调用
按照参数分类:
有参构造和无参构造
按类型分类:
普通构造和拷贝构造
三种调用方式:
括号法、显示法、隐式转换法
分类
首先是分类:举个例子来看
//构造函数分类
class Person{
public:
//无参构造
Person(){
cout<<"Person的无参构造函数调用"<<endl;
}
//有参构造
Person(int a){
age=a;
cout<<"Person的有参构造函数调用"<<endl;
}
//按照类型分上面的都是普通构造函数
//还有一种是拷贝构造函数
Person(const Person &p){//特点是传进来的不能修改,并且要以引用方式传递
age=p.age;//将传入的对象所有的属性都拷贝到本身
cout<<"Person的拷贝构造函数调用"<<endl;
}
//基本只要不是拷贝构造剩余的构造均属于普通构造
//析构函数不能有参数
~Person(){
cout<<"Person的析构函数调用"<<endl;
//
}
//private:
int age;
};
我们可以看到普通的构造类型,无论是否有参,都比较好理解,但是针对拷贝构造函数,它的格式需要稍微注意,因为是拷贝构造,所以传进来被拷贝的对象一定不能修改,要加const,另外需要使用引用传参数。
调用
调用同样根据上面的例子中我们给出的构造函数来分析。
//调用
void test(){
//括号法 (普通构造调用)
//注意::调用默认构造函数的时候,不要加(),因为编译器可能会认为是一个函数声明
//而不是在创建对象。
Person p;
Person p2(10);
//(拷贝构造函数)
Person p3(p2);
cout<<"p2的年龄为"<<p2.age<<endl;
cout<<"p3的年龄为"<<p3.age<<endl;
//显示法
Person p5;//无参
Person p6 =Person(10);//有参
Person p7=Person(p6);//拷贝构造
//Person(10)是匿名对象,=号前就是他的名,此行执行完 系统会立刻回收匿名对象
//注意:不要利用拷贝构造函数来初始化匿名对象。例如 Person(p6);
//Person(p6);会出现重定向错误,重定向到p6了
//隐士转换法
Person p8=10;//相当于写了Person p8 =Person(10); 有参构造调用
Person p9=p8;//拷贝构造调用相当于Person p9 = Person(p8 )
}
可以看出三种方法的调用方式,相对来说还是比较多的。
其中需要注意的是:
1.调用默认构造函数的时候,不要加(),因为编译器可能会认为是一个函数声明而不是在创建对象。
2.不要利用拷贝构造函数来初始化匿名对象。会出现重定向错误,编译器会认为这是你写了第二遍的上面某个构造函数。
拷贝构造函数的调用时机
C++中拷贝构造函数调用时机通常有三种情况
1.使用一个已经创建完毕的对象来初始化一个新对象(复制)
void test01(){
Person p1(20);
Person p2(p1);
cout<< "p2 Age"<< p2.m_Age<<endl;
}
2.值传递的方式给函数参数传值
void doWork(Person p){//值传递会拷贝一份新的,所以这里会是拷贝构造函数
};
void test02(){
Person p;
doWork(p);
}
3.以值方式返回局部对象
Person dowork2(){
Person p1;
cout<<(int *)&p1<<endl;
return p1;//值方式 返回同样会拷贝出来一份新的构造函数。
}
void test03(){
Person p=dowork2();
cout<<(int* )&p<<endl;
}
构造函数的调用规则
默认情况下,C++至少给一个类添加三个函数
1.默认构造函数(无参数,函数体为空)
2.默认析构函数(无参,函数体为空)
3.默认构造函数,对属性进行值拷贝
值得注意的规则是:
如果用户定义有参构造函数,c++不在提供默认无参构造,但会提供默认拷贝构造。
如果用户定义拷贝构造函数,c++不会在提供其他构造函数。
class Person{
public:
Person(){
cout<<"默认构造"<<endl;
}
Person(int age){
m_Age=age;
cout<<"有参数构造"<<endl;
}
Person(const Person &p){
cout<<"Person的拷贝构造函数调用"<<endl;
m_Age=p.m_Age;
}
~Person(){
cout<<"析构"<<endl;
}
int m_Age;
};
void test01(){
Person p1;
p1.m_Age=19;
Person p2(p1);
cout<< "P2的年龄为"<<p2.m_Age <<endl;
}
int main(){
test01();
return 0;
}
以上面的代码为例子,其输出结果是:
如果将自己写的拷贝构造函数注释掉的话会是什么结果呢?
class Person{
public:
Person(){
cout<<"默认构造"<<endl;
}
Person(int age){
m_Age=age;
cout<<"有参数构造"<<endl;
}
// Person(const Person &p){
// cout<<"Person的拷贝构造函数调用"<<endl;
// m_Age=p.m_Age;
// }
~Person(){
cout<<"析构"<<endl;
}
int m_Age;
};
再一次运行输出结果是:
可以看到少了一句我们自己写的输出,但是年龄仍然为19,说明其拷贝成功了。所以结论是:如果我们自己提供了拷贝构造,那么编译器就不会为我们提供了,如果我们没有写,编译器会默认提供空的拷贝函数,里面只有对所有的属性进行拷贝。
深拷贝和浅拷贝
浅拷贝:简单的赋值操作
深拷贝:在堆区重新申请空间,进行拷贝操作
通过下面案例分析一下:
class Person{
public:
Person(){
cout<<"默认构造"<<endl;
}
Person(int age,double height){
m_Age=age;
m_Height=new double (height);//堆区开辟的空间由程序员开辟,也有程序员回收
//
cout<<"有参数构造"<<endl;
}
// Person(const Person &p){
// cout<<"Person的拷贝构造函数调用"<<endl;
// m_Age=p.m_Age;
// }
~Person(){
//析构代码的作用通常来说会将堆区开放的数据作释放操作,
if(m_Height !=NULL){
delete m_Height;
m_Height=NULL;
}
cout<<"析构"<<endl;
}
int m_Age;
double * m_Height;//堆区
};
void test01(){
Person p1(10,1.8);
cout<<"p1年龄"<<p1.m_Age<<"身高为" <<*p1.m_Height<<endl;
Person p2(p1);
cout<<"p2年龄"<<p2.m_Age<<"身高为" <<*p2.m_Height<<endl;
// cout<<"p2年龄为"<<p2.m_Age<<endl;
}
这里我们调用test01这个函数后,是会报错的。
我们在Person类中提供了一个age 以及在堆区new了一个height,如果我们利用Person p2(p1),这是编译器提供的拷贝构造函数,因为是值拷贝,只会做浅拷贝操作。如果只是age那没问题,但是height是一个指针,值拷贝只会把指针地址原样拷贝过去。
构造函数执行完毕之后,会调用析构函数,释放,按照先进后出的原则,p2会先被释放,因此height的原本的地址就会被delete,就是释放过了,如果p1还要在执行一次释放,就会出问题了(非法操作)。
这时就需要我们去深拷贝来解决这个问题,深拷贝,重新在堆区开辟一段空间去保存身高,这样p1有一份身高,p2也有一份身高,只不过身高的值是一样的,但是指向这个值的地址是不同的,因此各自在释放的时候就不会出现非法问题了。
那么我们需要自己做一个拷贝构造函数,
Person(const Person &p){
cout<<"Person的拷贝构造函数调用"<<endl;
m_Age=p.m_Age;
// m_Height=p.m_Height;//如果是编译器默认实现就是这样。
//深拷贝一次
m_Height=new double (*p.m_Height);
}
来代替默认的拷贝构造函数,这样就解决了浅拷贝的问题。
实际上就是我们要重新开辟一段空间,来存放要拷贝堆区上的值。
初始化列表
C++中提供了初始化列表语法,用来初始化属性
语法:构造函数():属性1(值1),属性2(值2)....
class Person{
public:
//传统初始化操作
// Person(int a,int b,int c){
// m_A=a;
// m_B=b;
// m_C=c;
//
// }
//利用初始化列表来初始化
Person(int a,int b,int c):m_A(a),m_B(b),m_C(c){}
int m_A;
int m_B;
int m_C;
};
类对象作为类成员
C++类中的成员可以是另一个类的对象,我们称该成员为对象成员
class A{};
class B{
A a;
};
其中,B类中有对象A作为成员,A为对象成员。
但是这印出了一个问题,当创建B对象时,A与B的构造和析构顺序是如何的呢?
以如下例子
class Phone{
public:
Phone(string brand){
m_Brand=brand;
cout<<"Phone的构造函数调用"<<endl;
}
string m_Brand;
};
class Person{
public:
//Phone phone=pName 隐式转换法
Person(string name,string brand): m_Name(name), m_Phone(brand){
cout<<"Person的构造函数调用"<<endl;
}
string m_Name;
Phone m_Phone;
};
void test01(){
Person p("张三","XiaoMi");
cout<<p.m_Name<<"拿着"<< p.m_Phone.m_Brand<<"手机 "<<endl;
}
到底是先执行了Person的构造函数还是先执行了Phone的构造函数呢?
运行结果是:
那也就意味着对象成员会先执行构造函数。
总结:当其他类的对象作为本类的成员,先构造其他类,在构造本身。析构的执行顺序和构造正好是相反的,便于理解我们可以参考先进后出的原则。
静态成员
静态成员就是在成员变量和成员函数前面加上关键字static,称为静态成员,静态成员分为:
1.静态成员变量:
所有对象共享一份数据
在编译阶段分配内存
类内声明,类外初始化
举个例子:
class Person{
public:
//静态成员变量
//1.所有对象共享同一份数据
//2.编译阶段分配内存
//3. 类内声明,类外初始化
static int m_A;
};
int Person::m_A=100;//类外初始化,但是要明确作用域
void test01(){
//访问方式1
Person p;
cout<<p.m_A<<endl;
Person p2;
p2.m_A=200;
cout<<p.m_A<<endl;//结果是200 说明是共享同一份数据,我们是把100给改了。
cout<<Person::m_A<<endl;//访问方式2
}
声明静态成员变量,其不属于某个对象,所有对象共享同一份数据,一个改就全改了。因此其有两种访问方式:1.通过对象访问2.通过类名访问
静态成员同样是具有权限的,可以自己设定其权限。
2.静态成员函数:
所有对象共享同一个函数
静态成员函数只能访问静态成员变量
class Person{
//静态成员函数
//所有对象共享同一个函数
//静态成员函数只能访问静态成员变量
public:
static void pri(){
cout<< m_Age <<endl;
// cout<< m_phoneNumber <<endl;
}
static int m_Age;
int m_phoneNumber;
};
int Person::m_Age=85;
void test01(){
Person p;
p.pri();
Person::pri();//通过类名直接访问,说明他不属于某个对象。
}
上述例子给出了静态成员函数pri(),m_Age是静态成员变量,phoneNumber为普通成员变量。
静态成员函数只能输出m_Age的值,如果输出m_phoneNumber的值就会报错。
这个原因主要是因为静态成员函数只有一份,但是非静态成员变量,需要通过变量去访问。所以静态成员函数并不知道要访问的是那个对象的成员变量。
当然,静态成员函数同样是有权限的。