4.1封装
实例化:通过一个类,创建一个对象
类中的属性和行为统称为成员
属性:成员属性 成员变量
行为:成员函数 成员方法
封装的权限控制
公共权限 public: 成员 类内可以访问 类外也可以访问
保护权限 protected:成员 类内可以访问 类外不可以访问(儿子可以访问父亲的保护内容)
私有权限 private: 成员 类内可以访问 类外不可以访问(儿子不可以访问父亲的私有内容)
struct和class的区别:默认的访问权限不同
struct默认权限为公共,class默认权限为私有
Bool issame(Cube c1,Cube c2)//这里是值传递,会拷贝出一份数据,最好用引用传递,这样就会直接用原数据。 可以改成 bool issame(Cube &c1,Cube &c2)
bool的结果包括true 和false
4.2.1对象的初始化和清理(构造函数和析构函数)//没有返回值,不用写void
构造函数:主要作用在于创造对象时为对象的成员属性赋值,构造函数由编译器自动调用,无需手动调用。
析构函数:主要作用在于对象销毁前系统自动调用,执行一些清理工作。
语法:(构造函数) 类名(){}
(析构函数) ~类名(){} //不可以有参数,不会发生重载
4.2.2构造函数的分类和调用
- 分类:
1.按照参数分类 有参构造 和 无参构造(默认构造)
class Person{
public:
Person()
{
Cout<<“Person的无参构造函数调用”<<endl;
}
Person(int a)
{
age = a;
Cout<<"Person的有参构造函数调用“<<endl;
}
private:
Int age;
};
2.按照类型分类 普通构造 和 拷贝构造
Person(const Person &p)//加const是因为只需要拷贝,不能把原有的信息修改掉
{
age = p. age;//将传入的人身上的所有属性拷贝到该属性上。
}
- 调用
1.括号法
Person p1;//默认构造函数调用
Person p2(10);//有参构造函数调用
Person p3(p2);//拷贝构造函数调用
注意事项:
调用默认构造函数时不要加(),因为对下面这行代码,编译器会认为是一个函数的声明,不会认为在创建对象
Person p1(); 和 void func();
2.显示法
Person p1;//无参构造函数调用
Person p2 = Person(10);//有参构造函数调用
Person p3 = Person(p2);//拷贝构造函数调用
Person(10);//是个匿名对象,但放在等号右边时,等号左边就相当于对象名
匿名对象的特点:当前行执行结束以后系统会立即回收掉匿名对象(因为没有对象名,后续无法使用该对象)
注意事项:
不要利用拷贝构造函数来初始化匿名对象(下面的p3会调用拷贝构造函数)
例:Person (p3);//编译器会认为Person(p3)==Person p3,报错Person p3重定义。是个对象的声明。
3.隐式转换法
Person p4 = 10;//相当于Person p4 = Person(10);有参构造函数调用
Person p5 = p4;//拷贝构造函数调用
4.2.3拷贝构造函数调用时机
class Person{
public:
Person(){ cout<<"无参构造函数的调用"<<endl;}
Person(int age){
m_age = age;
cout<<"有参构造函数的调用"<<endl;
}
Person(const Person &p){
m_age = p.age;
cout<<"拷贝构造函数的调用"<<endl;
}
~Person();
private:
int m_age;
};
- 使用一个已经创建完毕的对象来初始化一个新的对象
void test01(){
Person p1(20);
Person p2(p1);
cout<<"p2的年龄为:"<<p2.m_age<<endl; //输出结果为“p2的年龄为:20”
}
- 以值传递的方式给函数参数传值
void dowork(Person p){ }
void test02(){
Person p;//会调用无参构造函数
dowork(p);//会调用拷贝构造函数,但是如果在dowork内修改成员信息,对对象p无影响,因为调用时生成拷贝构造函数,修改的是拷贝的对象
}
- 以值方式返回局部对象
Person dowork2(){
Person p1;
return p1;//返回的不是p1,而是一个p1的拷贝对象
}
void test03(){
Person p = dowork2();
}
4.2.4 构造函数的调用规则
- 如果用户定义有参构造函数,c++不再提供默认无参构造,但会提供默认拷贝构造
不能这样定义对象,例如Person p;
- 如果用户定义拷贝构造函数,c++不会再提供其他构造函数
创建一个类,c++编译器会给每个类都添加至少三个函数:默认构造(空实现),析构函数(空实现),拷贝构造(值拷贝)。
例:Person p;
p.age = 18;
Person p2(p);//前提:类中没有写拷贝构造函数
cout<<"P2的年龄为:"<<p2.age<<endl;//输出结果仍为18,编译器提供了拷贝构造函数进行值传递
4.2.5深拷贝与浅拷贝
浅拷贝:简单的赋值拷贝操作
深拷贝:在堆区重新申请空间,进行拷贝操作
class Person{
public:
Person(){
cout<<"Person的默认构造函数调用"<<endl;
}
Person(int age,int height){
m_age = age;
m_height = new int(height);//用new把height创建在堆区,返回的是int*类型
//堆区开辟的数据由程序员手动开辟手动释放,所以在对象销毁前应释放。在析构函数中释放
cout<<"Person的有参构造函数调用"<<endl;
}
~Person(){
//析构函数,将堆区开辟数据做释放操作
if(m_height !=NULL){
delete m_height;
m_height = NULL;//防止野指针出现
}
cout<<"Person的析构函数调用"<<endl;
}
private:
int m_age;
int *m_height;//int 类型的指针,指向身高
};
void test01(){
Person p1(18,160);
cout<<"p1的年龄为"<<p1.age<<"身高为"<<*p1.m_height<<endl;
//如果调用p1.m_height则输出的是地址,用*m_height则输出身高
Person p2(p1);
//利用编译器提供的拷贝构造函数会做浅拷贝操作,堆区的内存会被重复释放(p2已经调用析构函数释放掉堆区内存,p1会再次调用析构函数释放)
cout<<"p2的年龄为"<<p2.age<<"身高为"<<*p2.m_height<<endl;
}
解决方法:自己实现拷贝构造函数,重新在堆区创建一块内存,解决浅拷贝带来的问题
Person(const Person &p){
cout << "Person的拷贝构造函数调用" <<endl;
m_age = p.m_age ;
//m_height = p.m_height ; 编译器默认实现就是这行代码
//深拷贝操作
m_height = new int(*p.m_height);
}
4.2.6初始化列表
语法:构造函数():属性1(值1),属性2(值){}
Person(int a,int b,int c):m_a(a),m_b(b),m_c(c){}
4.2.7类对象作为类成员
class Phone{
public:
Phone(string pName){
m_PName = pName;
}
string m_PName;
};
class Person{
public:
Person(string name, string pName):m_Name(name),m_Phone(pName){ }
string m_Name;
Phone m_Phone;
};
当其他类对象作为本类成员,构造时候先构造其他类对象,再构造自身。
析构的顺序与构造时相反。
4.2.8静态成员函数
- 所有对象共享同一个函数
- 静态成员函数只能访问静态成员变量
class Person{
public:
static void func(){
m_A = 100;//静态成员函数可以访问静态成员变量
m_B = 200;//静态成员函数不可以访问非静态成员变量(m_B属于特定对象的成员,函数体无法区分到底是哪一个对象的,所以不能访问修改
cout<<" static void func 的调用"<<endl;
}
static int m_A;
int m_B;
private:
static void func2(){}//静态成员函数也有访问权限,类外访问不到func2函数。
};
int Person::m_A = 0;
void test01(){
1.通过对象访问
Person p;
p.func();
2.通过类名访问
Person::func();//所有对象共享该函数,所以不用创建对象也可以访问该函数
}
4.3.1成员变量和成员函数分开存储
(只有非静态成员变量才属于类的对象上)
空对象占用内存空间为1,c++编译器也会给每个空对象分配一个字节的空间,是为了区分空对象占内存的位置。
每个空对象也应该有一个独一无二的内存地址
class Person{
int m_A;//非静态成员变量 属于类的对象上
static int m_B;//静态成员变量 不属于类的对象上
void func(){}//非静态成员函数 不属于类的对象上
static void func(){}//静态成员函数 不属于类的对象上
};//该对象占用内存空间为4个字节,仅为int类型变量内存大小
4.3.2 this 指针概念
this 指针指向被调用的成员函数所属的对象,隐含在每一个非静态成员函数内
1.解决名称冲突
class Person{
public:
Person(int age){
this->age = age;//this指针指向被调用的成员函数所属的对象
}
int age;
};
2.返回对象本身用*this
class Person{
public:
Person(int age){
this-> age = age;
}
Person & PersonAddAge(Person &p)
//如果不是返回引用而是返回值(Person PersonAddAge(Person &p)),那最后输出结果为20
//如果是值,则会调用拷贝构造函数,创建新的对象
{
this->age += p.age;
return *this; //this指向p2的指针,而*this指向的就是p2这个对象本体
}
int age;
};
void test(){
Person p1(10);
Person p2(10);
//链式编程思想
p2.PersonAddAge(p1).PersonAddAge(p1).PersonAddAge(p1);
cout<<"p2的年龄为:"<<p2.age<<endl;//输出结果为40
4.3.4 const 修饰成员函数(const是为了 限定只读状态)
常函数:
- 成员函数后加const则称这个函数为常函数
- 常函数内不可以修改成员属性
- 成员属性声明时加关键字mutable后,在常函数中依然可以修改
class Person{
public:
//this指针的本质是指针常量,指针的指向是不可以修改的
//相当于Person * const this;
//在成员函数后面加const,修饰的是this指向,让指针指向的值也不可以修改
//相当于const Person * const this;
void showPerson()const
{
this->m_A = 100;//报错,指针指向的值不能修改
m_A = 100;//同上
this->m_B = 100;//不报错
}
int m_A;
mutable int m_B;//特殊变量,即使在常函数中也可以修改
};
常对象:
- 声明对象前加const称该对象为常对象
- 常对象只能调用常函数
const Person p;//在对象前加const,变为常对象
p.m_A = 100;//报错
p.m_B = 100;//不报错,特殊值,即使在常函数下也可以修改
4.4 友元
4.4.1全局函数作友元
假设有一全局函数 void myfriend(Building &building)//里面要访问到类中的私有成员属性
则在类中可以先声明 friend void myfriend(Building &building);
//该全局函数是类Building的好朋友,可以访问类中的私有成员
4.4.2 类作友元
如果有一类myfriend要访问类Building中的私有成员,则在Building中写friend class myfriend即可
//该类是Building类的好朋友,可以访问Building类中的私有内容
4.4.3成员函数作友元
如果myfriend类下的visit函数想要访问Building类中的私有成员,而visit2函数不可以访问,则在Building类中声明friend void myfriend::visit();即可
//告诉编译器,myfriend类下的visit成员函数作为本类的好朋友,可以访问类中的私有内容
4.5运算符重载
4.5.1 加号运算符重载
- 通过成员函数重载+号
Person operator+(Person &p){
Person temp;
temp.m_A = this-> m_A + p.m_A;
temp.m_B = this-> m_B + p.m_B;
return temp;
}
在主函数中就可以运行Person p3 = p1.operator+(p2);//标注出的部分为函数名
可以简化为Person p3 = p1 + p2;
- 通过全局函数重载+号
Person operator+(Person &p1,Person &p2){
Person temp;
temp.m_A = p1.m_A + p2.m_A;
temp.m_B = p1.m_B + p2.m_B;
return temp;
}
在主函数中就可以运行Person p3 = operator+(p1,p2);
可以简化为Person p3 = p1+p2;
运算符重载也可以发生函数重载
4.5.2 左移运算符重载
利用成员函数重载左移运算符 p.operator<<(cout) 简化为p<<cout
通常不会利用成员函数重载<<运算符,因为无法实现cout在左侧,所以只能利用全局函数重载左移运算符
void operator<<(ostream &cout ,Person &p){
cout<<"m_A = "<<p.m_A<<" m_B ="<<p.m_B;
}//ostream是输出流类
在主函数中就可以写cout<<p;
如果想调用cout<<p<<endl;利用链式思想把子函数改为
ostream &operator<<(ostream &cout,Person &p){
cout<<"m_A = "<<p.m_A<<" m_B ="<<p.m_B;
return cout;
}
再利用友元函数访问私有成员即可。
4.5.3 递增运算符重载 ++
- 重载前置++运算符
MyInteger& operator++(){
m_Num++;//先对自身属性做++运算
return *this;//再对自身做返回
}//返回引用是为了一直对一个数据进行递增操作,如果不是返回引用,则++(++myint)后再输出myint,值仍为++myint的值
- 重载后置++运算符
MyInteger operator++(int)//int代表占位参数,可以用于区分前置和后置递增
{
//先记录当时结果
MyInteger temp = *this;
//后递增
m_Num ++;
//最后将记录结果返回
return temp;
}//返回的是值,如果返回引用则返回的是局部对象,函数运行完以后就会被释放,后续操作则为非法操作
4.5.4 赋值运算符重载
class Person{
public:
Person(int age)
{
m_age = new int(age);
}
~Person(){
if(m_age!=NULL)
delete m_age;
m_age = NULL;
}
}
int *m_age;
};//如果用系统定义的赋值运算符完成p2=p1操作,则堆区内存会被重复释放,所以要重载赋值运算符
Person& operator=(Person &p){
//编译器提供的是浅拷贝,m_age = p.age;
//应该先判断是否有属性在堆区,如果有,先释放干净,再深拷贝
if(m_age!=NULL){
delete m_age;
m_age=NULL;
}
m_age = new int(*p.m_age);
//返回对象本身
return *this;
}
4.5.5关系运算符重载
bool operator==(Person &p){
if(this->m_Name == p.m_Name&& this->m_age == p.m_age)
return true;
else return false;
}
bool operator !=(Person &p){
if(this->m_Name == p.m_Name&& this->m_age == p.m_age)
return false;
else return true ;
}
int main(){
Person p1("Tom",18);
Person p2("Tom",18);
if(p1 == p2)
cout<<"p1和p2是相等的!"<<endl;
if(p1 != p2)
cout<<"p1和p2是不相等的!"<<endl;
4.5.6 函数调用运算符重载
class Print{
public:
void operator()(string test){
cout<<test<<endl;
}
};
int main(){
Print myprint;
myprint("Hello World!");//由于使用起来非常类似函数调用,因此称为仿函数
}
class MyAdd{
public:
int operator()(int num1,int num2){
return num1+num2;
}
};
int main(){
MyAdd myadd;
int ret = myadd(100,100);
cout<<ret<<endl;
//匿名函数对象调用
cout<<MyAdd()(100,100)<<endl;
}
4.6 继承
语法:
class 子类:继承方式 父类//子类也成为派生类,父类也成为基类
4.6.3继承中的对象模型
在父类中所有非静态成员属性都会被子类继承
父类中私有成员属性是被编译器隐藏了,访问不到,但是会被继承
利用开发人员命令提示工具查看对象模型
//跳转盘符 F:
//跳转文件路径 cd 具体路径下
//查看命名
//cl /d1 reportSingleClassLayout类名 文件名
4.6.4继承中的构造和析构顺序
先构造父类,再构造子类,析构顺序和构造顺序相反
4.6.5继承同名成员的处理方式(Son s;)
- 访问子类同名成员,直接访问即可 例 s.m_a s.func();
- 访问父类同名成员,需要加作用域 例 s.Base::m_a s.Base::func();
//如果子类中出现和父类同名的成员函数,子类的同名成员会隐藏掉父类中所有同名成员函数
//如果像访问到父类中被隐藏的同名成员函数,需要加作用域
4.6.6 继承中同名静态成员的处理假设子类和基类中均有成员static int m_a
通过对象访问,与上面一样
通过类名访问 Son::m_a(子类) Base::m_a或Son::Base::m_a(父类)
//第一个::代表通过类名方式访问,第二个::表示访问父类作用域下
4.6.7 多继承语法//实际开发中不建议用
class 子类:继承方式 基类1,继承方式 基类2……
//当父类中出现同名成员,需要加作用域区分
例如Base1和Base2中同时含有m_A
则输出时通过s.Base1::m_A 和 s.Base2::m_A 来区分
4.6.8 菱形继承
利用虚继承可以解决菱形继承的问题
Sheep 和 Tuo类在继承类型前加上virtual 变为虚继承
Animal类称为虚基类
从Sheep和Tuo继承下来的是vbptr指针(虚基类指针)指向虚基类的成员
4.7.1多态的基本概念
- 静态多态:函数重载和运算符重载属于静态多态,复用函数名(函数地址在编译阶段确定)
- 动态多态:派生类和虚函数实现运行时多态(运行阶段确定函数地址)
动态多态的满足条件:
1.有继承关系
2.子类要重写(函数返回值类型、函数名、参数列表完全相同)父类中的虚函数
动态多态使用
父类的指针或引用 指向子类对象
4.7.3纯虚函数和抽象类
virtual 返回值类型 函数名(参数列表)=0;
当类中只要有一个纯虚函数,这个类就是抽象类
抽象类的特点:
1.无法实例化对象 Base b或new Base 都无法运行
2.抽象类的子类必须要重写父类中的纯虚函数,否则也属于抽象类
4.7.5虚析构和纯虚析构
多态在使用时,如果子类中有属性开辟到堆去,那么父类指针在释放时无法调用到子类的析构代码,
解决方式:将父类的析构函数改为虚析构或者是纯虚析构
纯虚析构需要声明也需要实现,纯虚析构的实现在类外写。
有了纯虚析构以后这个类成为抽象类
如果子类中没有堆区数据,可以不写虚析构或者纯虚析构