C++入门03,类与对象
Cukor丘克
文章目录
面向对象程序设计的基本特点
一、抽象
-
抽象就是一种只可意会不可言传的感觉。面对现实的事物,我们都可以知道这个事物的一些基本的特性,抽象就是对这种事物的一些概括。抽象就是抽出一类对象的公共性质并加以描述的过程。
举个简单的例子:
学校里都有学生。学生就是一种抽象。现在就只知道是学生这个名词,并不知道学生的具体内部是干什么的。后来发现学生有姓名,学生有年龄这些是学生的属性,然后学生可以学习,学生可以看书,学生还可以考试这些是学生的行为。那这个属性和行为呢就是对学生这个类的概括,这个就是抽象。
二、封装
- 封装就是将抽象得到的属性和行为相结合,形成一个有机的整体,也就是将数据和操作数据的函数代码及逆行有机地结合,形成”类“,其中的数据和函数都是类的成员。(这段话是清华大学出版社《C++程序设计》(第5版)的原话)
- 我简单理解就是从字面意思出发,封装就是拿了一堆东西,然后用一个箱子或者是麻袋之类的把它们放在一起,这就是封装了。而在C++中类封装的内容就是属性和行为。
- 属性就是这个抽象体有什么。在C++中一般是基本数据类型,或者是自定义类型。
- 行为就是这个抽象体能干嘛。在C++中一般是函数。
//现在来简单的封装一个类
class Student
{
//class 是关键字,Student是类名
//一般在C++中行为使用public限定,属性使用protected限定
public:
//行为
void printStu();
protected:
//属性
string name;
int age;
};
三、继承
-
子承父业这个词应该是听说过的,意思很简单,就是儿子继承了他老爸的所有东西。
-
举个简单的例子:
老爸有名字,有性别,有年龄的属性,有行走,有赚钱,有吃饭的行为。然后老爸有了儿子之后呢,儿子也继承了老爸有名字,有性别,有年龄的属性,有行走,有赚钱,有吃饭的行为。
-
-
在C++中给类提供了继承的机制,就是子类可以全部继承父类里面的所有东西,但是能不能全访问得到是另外一回事。
-
举个白话例子:
老爸有有老婆的属性,儿子继承了老爸的有老婆的属性,但是老爸的这个老婆是老爸私有的属性或者是被保护的属性,那么儿子是访问不到的,虽然儿子继承了老爸的有老婆的属性。
-
举个代码的例子:
-
#include<iostream>
using namespace std;
class Father
{
public:
//行为
void run();
protected:
//属性
string name;
};
//采用公有继承
class Son:public Father
{
public:
//什么都是不写,就只是简单的继承
//如果在子类中写了一些新的东西,那么这个就叫派生
protected:
};
int main()
{
Son son;
son.name="xxx"; //这个是错误的,因为在父类Father中name是被protected保护限定的,所以子类访问不到
son.run(); //这个是正确的,因为在父类Father中run()是被public公有限定的,所以子类可以访问到
//删掉son.name后运行结果是this is run
return 0;
}
void Father::run()
{
cout<<"this is run "<<endl;
}
四、多态
-
多态性就是程序能处理多种类型对象的能力。
-
简单点说,多态就是多种状态。
日常生活中,我们都会说“吃饭”,而”吃“,就是一种抽象的信息,”吃“的后面跟这不同的名词就表示干着不一样的事,比如说,”吃饭“,那就是干着吃饭这个事情,”吃冰淇淋“,就是干着吃冰淇淋这个事情,”吃鸡“,就是干着吃鸡这个事情。同样都是”吃“但是,具体的吃法是不一样的,”吃饭“,你可能是拿着筷子细嚼慢咽的吃;”吃冰淇淋“,你可能是用舌头舔着吃;”吃鸡“,你可能是用手抓着啃着吃。那么这就是多态,同种类型却干着不一样的事。
多态必须的三个条件:
- 公有继承(public 继承)
- 存在不正当的指针引用赋值关系
- 父类中存在虚函数virtual修饰的函数
#include<iostream>
using namespace std;
//虚函数就是,父类的东西不会继承到子类中去的函数
class Father
{
public:
//行为
//虚函数
virtual void print()
{
cout << "Father::print" << endl;
}
protected:
//属性
string name;
string sex;
int age;
};
//采用公有继承
class Son :public Father
{
public:
void print()
{
cout << "Son::print" << endl;
}
protected:
};
int main()
{
//不正常的指针赋值关系
Father* fa = new Son;
fa->print(); //打印: Son::print
fa = new Father;
fa->print(); //打印: Father::print
/*
正如上面看到,fa调用的都是print函数
但是出来的结果却是不同的,这个就是多态
*/
return 0;
}
类和对象
在面向对象程序设计中,程序模块是由类构成的。类是对逻辑上的相关函数与数据的封装。类就是抽象的描述,对象就是类的具体实例。
C++中的类是由class关键字表示,那就是从class我学过的英文单词(班级)出发理解,班级就是一个类,别人只是知道班级这个概念,但是却不知道班级里的人是怎样的,也不知道班级里的事物是干什么的,就只是简单的知道class是一个班级。那么学生就是这个班级的实例,也就是传说中的对象,老师也是属于这个class中的一个对象也是一个班级类的实例。当然不仅是人可以是对象,物体也是可以作为对象的,就比如说班级里的黑板,也是班级里的一个对象,这些对象能干嘛就关键在封装这个类的时候放了什么属性和行为。
类的定义
不想多说直接看代码
#include<iostream>
using namespace std;
//定义了一个MM类
class MM
{
public:
void print()
{
cout<<"this is MM::print"<<endl;
}
protected:
string name;
int age;
int money;
};
int main()
{
MM mm; //这里就是创建一个对象的过程
mm.print();//用这个对象调用一下print成员函数
return 0;
}
C++中,被封装起来的东西没有顺序可言的,不像是在C语言中,一定要是自顶向下。
类的成员访问控制(权限限定)
C++中有三个权限限定
- public //公有限定,类外对象可以访问到
- protected //保护限定,类外对象访问不到
- private //私有限定,类外对象访问不到
通常,行为都是使用public限定,属性都是使用protected或private限定。
这些限定词没有先后顺序要求,也没有个数要求,在一个类中可以有多个public限定可以有多个protected限定也可以有多个private限定。
没有限定词的地方,在类中默认为private限定,结构体中默认为public限定。
C++中通常是把public限定的行为放在最前面。因为它们是外部访问时需要了解的。
C++类继承的权限表
父类限定: | public | protected | private |
---|---|---|---|
子类public继承结果: | public | protected | private |
子类protected继承结果: | protected | 无 | 无 |
子类private继承结果: | private | private | private |
类的成员函数
类的行为就是通过类的成员函数去实现的。
class MM
{
public:
void print(); //这个就是类中的成员函数,就是具体实现类的行为
}
程序实例
封装一个美女类,然后创建3个美女,键盘输入这3个美女的信息然后按表格的样式打印出来。然后简单调用美女中的一些行为。
要求:美女有姓名、年龄、钱的属性,有唱歌、跳舞、花钱的行为。
假设现在还没学过构造函数
#include<iostream>
using namespace std;
class MM
{
public:
//封装一个给MM初始化的函数
void initMM(string name1, int age1, int money1);
void sing();
void dance();
void pay(int iMoney);
void printMM();
protected:
string name;
int age;
int money;
};
int main()
{
MM mm[3];
//没有构造函数,那就只能是通过中间缓冲区来实现初始化MM信息,但严格意义上已经不初始化了,应该叫赋值
string buffer_name;
int buffer_age;
int buffer_money;
for (int i = 0; i < 3; i++)
{
cout << "请输入第" << i + 1 << "个美女的信息" << endl;
cin >> buffer_name >> buffer_age >> buffer_money;
mm[i].initMM(buffer_name, buffer_age, buffer_money);
}
cout << "美女们的信息如下:" << endl;
cout << "姓名" << "\t" << "年龄" << "\t" << "钱" << endl;
for (int i = 0; i < 3; i++)
{
mm[i].printMM();
}
mm[2].pay(9);
mm[2].printMM();
return 0;
}
void MM::initMM(string name1, int age1, int money1)
{
name = name1;
age = age1;
money = money1;
}
void MM::sing()
{
cout << "美女唱了一首《可爱女人》" << endl;
}
void MM::dance()
{
cout << "美女跳了一段舞" << endl;
}
void MM::pay(int iMoney)
{
cout << "美女花了"<<iMoney<<"块钱" <<"美女还剩"<<money-iMoney<<"块钱"<< endl;
money = money - iMoney;
}
void MM::printMM()
{
cout << name << '\t' << age << '\t' << money << endl;
}
案例结果:
- 请输入第1个美女的信息
于文文 19 1001
请输入第2个美女的信息
张靓颖 20 2020
请输入第3个美女的信息
徐若瑄 21 1230
美女们的信息如下:
姓名 年龄 钱
于文文 19 1001
张靓颖 20 2020
徐若瑄 21 1230
美女花了9块钱美女还剩1221块钱
徐若瑄 21 1221
构造函数和析构函数
类和对象的关系就像基本数据类型和它的变量的关系一样。在使用变量的时候我们有时候会在定义的时候就初始化了,像上面的代码实例中是没有初始化的,是借助了一个中间变量当作缓冲区存放数据,然后再把缓冲区里的内容放到对象的对应属性中去。其实在一开始的时候,mm被创建出来的时候是调用了类的默认的构造函数,一开始放的是垃圾值,然后buffer里面的东西赋值到对象中,这样才能得到我们想要的结果。那么下面就开始介绍类中的构造函数和析构函数。
构造函数
一般用来做类的对象的初始化工作。不写构造函数会存在一个默认的构造函数,当自己写了构造函数后,默认的构造函数就不存在了。
构造函数的特征:
- 没有返回值
- 函数名和类名相同
- 可以被重载
- 不写构造函数的时候,会存在一个默认的构造函数,写了构造函数,默认的就不存在了
- 构造函数不需要自己调用,在创建对象的时候会根据对象的参数来调用对应的构造函数
- 构造函数的长相和创建对象的时候传进来的参数对应
- 构造函数的作用一般是用来初始化对象
class MM
{
public:
MM(){} //默认的构造函数原型
//有参的构造函数,这里采用初始化参数列表的方式初始化
MM(string name,int faceScore):name(name),faceScore(faceScore){}
//有构造函数,这里采用一般的初始化方式
MM(int faceScore1,string name1)
{
name=name1;
faceScore=faceScore1;
}
//两种方式都可以正确的初始化类中的数据成员,初始化参数列表的方式速度会快一下
protected:
string name;
int faceScore;
}
默认构造函数
直接看代码
#include<iostream>
using namespace std;
class Dog
{
public:
//这个就是一个默认的构造函数的原型
Dog()
{
cout<<"调用无参构造函数"<<endl;
}
protected:
string name;
string sex;
};
class Cat
{
public:
//不写构造函数的时候会存在一个默认构造函数
protected:
string sex;
};
class Student
{
public:
//自己写了构造函数,默认的就不存在了
Student(string name1,int age1)
{
name=name1;
age=age1;
cout<<"调用有参构造函数"<<endl;
}
protected:
string name;
int age;
}
int main()
{
Dog dog; //正确定义对象
Cat cat; //正确定义对象
Student Jack; //错误,因为不存在默认的构造函数
Student Tony("Tony",19); //正确,因为有相匹配的构造函数
Student JayChou("周杰伦"); //错误,因为没有与之匹配的构造函数
return 0;
}
通过上面的代码可以看到,构造函数在没写的时候是存在一个默认的构造函数,但是自己写了构造函数之后默认的构造函数就不存在了,然后自己写的构造函数对应的参数个数决定在创建对象时要传进来对应的参数个数。
构造函数的作用就是用来初始化类的对象的。有了构造函数后写法就方便了许多,就不需要通过一个接口来初始化类中的属性了。
通常有一个方法可以不用多写默认的构造函数,就是说我们自己写了一个构造函数后,默认的不存在了,然后我们也能不用多写默认的构造函数,程序也不会报错。那就是采用形参缺省的写法来完成。
#include<iostream>
using namespace std;
class Boy
{
public:
Boy(string name1="",int age1=0,int money1=0)
{
name=name1;
age=age1;
money=money1;
}
protected:
string name;
int age;
int money;
};
int main()
{
Boy boy; //这样是可以的,程序不会报错
return 0;
}
委托构造函数
从字面意思就很好理解了,就这种构造函数,委托其他构造函数来初始化,一种偷懒的写法。
#include<iostream>
using namespace std;
class Boy
{
public:
Boy(string name1, int age1, int money1)
{
name = name1;
age = age1;
money = money1;
cout << "有参构造函数" << endl;
}
//委托写法
Boy() :Boy("委托", 3, 10)
{
cout << "委托构造函数" << endl;
}
void printBoy()
{
cout << name << '\t' << age << '\t' << money << endl;
}
protected:
string name;
int age;
int money;
};
int main()
{
Boy boy;
boy.printBoy();
/*
有参构造函数
委托构造函数
委托 3 10
*/
return 0;
}
拷贝构造函数
拷贝构造函数是一种特殊的构造函数,具有一般构造函数的所有特性,期形参是本类型的对象的引用。其作用就是使用一个已经存在的对象去初始化同类的一个对象。
类中存在一个默认的拷贝构造函数使得以下代码成立
//懒得造轮子了,上面有Boy类的代码
Boy boy;
Boy boy2=boy; //这里是成立的,因为类中会存在一个默认的拷贝构造函数
类中默认的拷贝构造函数通常被称为浅拷贝,而自己写的拷贝构造函数并且类的数据成员申请了动态内存这样的函数通常被称为深拷贝。
当自己写了拷贝构造函数后,默认的拷贝构造函数就消失了。
-
为什么会有深拷贝和浅拷贝之分?为什么深拷贝的形参一定要是这个类的对象的引用?
这是因为调用函数的时候是通过赋值的操作传的参数,当形参得到的实参是一个指针的时候,那么改变这个指针里面的内容的时候,形参和实参都是一样的,都会被修改(因为多个指针同时指向的是同一个内存,通过一个指针修改后内存里面的值是会被修改的而用另一个指针访问的时候得到的值就是这个被修改之后的值),这样就不符合拷贝的意义了,所以在某种意义上讲,浅拷贝是不安全的。而传引用的话就不会产生修改内容后全部值都会改变的情况。如果不理解直接看代码就很容易理解了。
自己手动写拷贝构造函数的必要条件:
- 首先是构造函数(函数名和类名相同)
- 然后是需要一个已经存在的对象的引用(函数的参数是这个类的对象的引用)
- 深拷贝构造函数中的要new一个新的空间
#include<iostream>
using namespace std;
class MM
{
public:
//无参构造函数
MM(){}
//有参构造函数
MM(int* p)
{
pi = p;
}
/* 默认的浅拷贝长这个样子,但是这里要注释掉,要不然深拷贝就没法写了
MM(MM& mm)
{
pi=mm.pi;
}
*/
//手写深拷贝
MM(MM& mm)
{
pi = new int(*mm.pi);
}
//接口,在修改的时候用到
int& getData()
{
return *pi;
}
void print()
{
cout << "pi=" << pi << '\t' << "*pi=" << *pi << endl;
}
protected:
int* pi = nullptr;
};
/* 测试结果
pi=010FF8AC *pi=99
pi=013A58B0 *pi=99
pi=013A58E0 *pi=99
pi=010FF8AC *pi=99
pi=013A58B0 *pi=9
pi=013A58E0 *pi=99
pi=010FF8AC *pi=99
pi=013A58B0 *pi=9
pi=013A58E0 *pi=666
*/
int main()
{
int iNum = 99;
MM mm(&iNum);
MM mm2(mm); //调用深拷贝
MM mm3 = mm; //这里是因为有默认的重载运算符存在,所以就算是自己写了深拷贝,这里依然是对的,但是这里也是深拷贝
mm.print();
mm2.print();
mm3.print();
cout << endl;
mm2.getData() = 9;
mm.print();
mm2.print();
mm3.print();
cout << endl;
mm3.getData() = 666;
mm.print();
mm2.print();
mm3.print();
return 0;
}
析构函数
析构函数就是杀死对象的过程,就是在对象死亡的时候会自动调用析构函数,不需要程序员手动调用,在不写析构函数的时候会存在一个默认的析构函数,在类中的数据类型使用了new申请堆中的内存的时候就要自己写析构函数来释放内存,不写的话程序运行完了释放问题也是交给操作系统,由操作系统来释放。
析构函数长什么样?
class MM
{
public:
//析构函数就长这样
~MM()
{
//函数体,随便写
}
protected:
int iNum;
};
析构函数的特征:
-
没有返回值
-
函数名是 “~” +类名
-
没有参数
-
一个类中只有一个析构函数,不写析构函数,会存在一个默认的析构函数
-
类中的 数据成员做了动态内存申请的时候就需要手动写析构函数
#include<iostream>
using namespace std;
class MM
{
public:
~MM()
{
cout<<"自动调用析构函数"<<endl;
}
};
int main()
{
{
MM mm;
}
//输出:自动调用析构函数
return 0;
}
从上面的代码中可以看到,我们在类的对象死亡之前调用析构函数,也就是说明,我们可以在对象死亡之前做一些事情,就比如上面的
cout<<"自动调用析构函数"<<endl;
当类中的数据成员做了动态内存申请,就要手动写析构函数
#include<iostream>
#include<cstring>
using namespace std;
class MM
{
public:
MM(const char* str)
{
pi=new char[strlen(str)+1];
strcopy(pi,str);
}
void print()
{
cout<<pi<<endl;
}
~MM()
{
delete[] pi; //释放内存
pi=nullptr;
cout<<"调用析构函数"<<endl;
}
protected:
char* pi;
};
int main()
{
{
MM mm("ILoveyou");
mm.print(); //打印ILoveyou
}
//打印:调用析构函数
return 0;
}
对构造函数和析构函数的简单小结
-
构造函数就是用来初始化对象的,析构函数就是用来杀死对象的,两者都不需要手动调用。可以理解成构造函数和析构函数的功能相反。
-
构造函数和析构函数,在不手动写的情况下会存在默认的。手动写了之后默认的就不存在了。
-
构造函数可以重载,析构函数不能重载
default、delete函数
default在计算机科学里面一般翻译成默认
delete在计算机科学里面一般翻译成删除
那从字面意思就可以理解就是当一个东西使用到default的时候,表示这个东西是默认的,当使用到delete的时候就表示这个东西被删除。
直接看代码
#include<iostream>
using namespace std;
class MM
{
public:
string str;
MM() = default; //默认合成无参构造函数
MM(MM& str) = default; //默认合成拷贝构造函数
void print();
~MM() = default; //默认合成析构函数
};
void MM::print()
{
cout << str << endl;
}
int main()
{
MM mm;
mm.str = "IMissyou";
MM mm2(mm);
mm2.print(); //打印: IMissyou
return 0;
}
default只能合成简单的构造函数,拷贝构造函数,析构函数,但合成不了复杂的函数,因为在这些所谓的复杂中,是形参列表的复杂,然后函数内部的具体实现编译器也不知道,所以,default是合成不了复杂的函数的。所以复杂的函数只能是程序员自己是实现。当程序员不需要一些函数的时候就可以采用“=delete”的方式把函数删掉。
#include<iostream>
using namespace std;
class MM
{
public:
string str;
MM() = default; //默认合成无参构造函数
MM(MM& str) = delete; //删除拷贝构造函数
void print();
~MM() = default; //默认合成析构函数
};
void MM::print()
{
cout << str << endl;
}
int main()
{
MM mm;
mm.str = "IMissyou";
MM mm2(mm); //这里就直接报错了,因为已经删除了
//禁止显示状态"MM::MM(MM &str)" (已声明 所在行数 : 9) --它是已删除的函数
mm2.print();
return 0;
}
类的组合
类的组合就是类中嵌套着其他类的对象作为自己的成员的情况,类和类之间是一种包含于被包含的关系。当存在类的组合时,初始化阶段要先初始化被嵌套的类的对象然后才初始化当前类的对象。
组合
//这里的情况是B类包含A类
class A
{
public:
A(int a):a(a){}
protected:
int a;
};
//B类的数据成员含有A类的对象
class B
{
public:
//初始化B类的对象之前先初始化A的对象
B(int b,A a):b(b),a(a){}
protected:
int b;
A a;
};
//这里的情况是A和B相互包含
class A
{
public:
A(){} //默认的构造函数是一定要写的,要不然会报错
A(int a,B b):a(a),b(b){}
protected:
int a;
B b;
};
class B
{
public:
B(){} //默认的构造函数是一定要写的,要不然会报错
B(int b,A a):b(b),a(a){}
protected:
int b;
A a;
};
前向引用声明
C++的类应当先定义然后使用。但是在处理相对复杂的问题、考虑到列的组合的时候,出现两个类相互引用的情况,这种情况就是就是循环依赖。
//这里的情况是A和B相互包含
class A
{
public:
A(){} //默认的构造函数是一定要写的,要不然会报错
A(int a,B b):a(a),b(b){}
void print(B b); //这里就会报错,未知符号“B”
protected:
int a;
B b;
};
class B
{
public:
B(){} //默认的构造函数是一定要写的,要不然会报错
B(int b,A a):b(b),a(a){}
protected:
int b;
A a;
};
上面的错误是因为在编译的时候,编译器不知道B是什么,所以报错。要解决这个问题就必须在A类定义之前先声明B类,也就是在A类前写上
class B;
这样之后编译器就知道B是一个类。这个就是前行引用声明。就是当我不知道字符的意思的时候我先在定义之前声明这个字符是什么东西然后在定义。