9.1 封装
9.1.1 封装的例子
class Student {
public:string name;int age;
public:void setName(string name_) {name = name_;}
};
int main() {Student s1;s1.setName("zhangsan");return 0;
}
类中的行为都叫做成员,例如成员属性,成员变量,成员方法
9.1.2 访问权限
名称 | 权限范围 |
---|---|
public | 类内、类外都可以访问 |
protected | 类内可以访问,类外不可以访问,子类可以访问 |
private | 类内可以访问,类外不可以访问,子类不可以访问 |
strcut 和class 区别
- 默认权限不同,struct 默认权限为public,class默认权限为private
9.2 对象的初始化和清理
9.2.1 构造函数与析构函数
构造函数语法: 类名(){}
- 没有返回值也不需要用void修饰
- 函数名与类名相同
- 可以有参数,可以重载
析构函数语法: ~类名(){}
- 没有返回值也不需要用void修饰
- 函数名与类名相同
- 不可以有参数,不可以重载
- 在对象销毁前会自动调用析构函数
9.2.2 构造函数的分类及调用
分类:
- 按照参数:有参构造函数和无参构造函数
- 按照类型:拷贝构造函数和普通构造函数
// 拷贝构造函数
Person(const Person &p){age=p.age;name=p.name;
}
调用:
- 括号法:
- 无参构造函数调用:Person p1;
- 有参构造函数调用:Person p2(10);
- 拷贝构造函数调用:Persiong p2(p1);
- 显示法:
- Person p1;
- Person p2 = Person(10);
- Person p3 = Person(p2);
- 隐式法
- Person p1=10;// 相当远Person p1=Person(10);
- Persion p5=p4;
c++ 默认提供无参构造函数,无参析构函数以及拷贝函数
9.2.3 初始化列表
class Student {
public:string name;int age;Person():name("zhangsan"),age(10){}Person(string a,int b):name(a),age(b){}
public:void setName(string name_) {name = name_;}
};
9.2.4 类对象作为类成员
- 当类对象作为类成员的时候,类成员的构造方法先执行,即内对象先构造,在构造外对象
- 析构方法与构造方法相反
9.2.5 静态成员
使用static修饰的成员变量和成员函数称之为静态成员变量、静态成员函数
-
静态成员变量:
- 所有对象共享同一份数据
- 在编译阶段分配内存
- 类内声明,类外初始化
-
静态成员函数:
- 所有对象共享同一个函数
- 静态成员函数只能访问静态成员变量
静态成员变量
- 初始化
class Student {public:string name;int age;static string school;
public:void setName(string name_) {name = name_;}
};
int deliverValue(Student s);
int deliverPlace(Student* s);
string Student::school = "黑龙江";// 静态成员变量初始化
int main() {Student s1;s1.setName("zhangsan");s1.school = "哈尔滨";// 或者直接通过某个对象初始化也可以return 0;
}
- 访问方式
- 通过对象访问 s1.school
- 通过类名访问 Student::school
静态成员函数
- 访问方式
- 通过对象访问 s1.func();
- 通过类名访问 Student::func();
9.3 C++对象模型和this指针
9.3.1 this指针
成员变量和成员函数式分开存储的,静态成员变量,静态成员函数,以及成员函数都是共享的,只有非静态成员变量是对属于对象的。
那么对于非静态成员函数是怎么区分是哪个对象调用自己的?C++通过提供特殊的对象指针this指针解决上述问题。this指针指向被调用的成员函数所属的对象。
- this 指针隐含在每个非静态成员函数内部,不需要定义,直接可以使用
this指针的作用
- 当形参与成员变量同名时,可以用this指针来区分
- 在类的非静态成员函数中返回对象本身,可以使用 return *this
class Student {public:string name;int age;static string school;
public:void setName(string name) {this->name = name;}
};
在VS2022中键盘输入this.age= 会自动修正为this->age=,对于VS2022 指针的"."相当于“->”的快捷键。
9.3.2 const修饰成员函数
- 常函数
- 成员函数添加const后我们称这个函数为常函数
- 常函数内不可以修改成员属性
- 成员属性声明时加关键字multable后,在常函数中依然可以修改
- 常对象
- 声明对象前加const称该对象为常对象
- 常对象只能调用常函数
常函数如下:
void show(string name) const {cout<<this->name;
}
const 本质上是修饰的this指针
9.3 友元
- 全局函数做友元
- 类做友元
- 成员函数做友元
9.3.1 全局函数做友元
在类中加入friend修饰的函数声明,就可以将该函数定义为当前类的友元
class Student {friend void visit(Student* s);
public:string name;int age;static string school;
private:string score;
public:void setName(string name) {this->name = name;}void show(string name) const {cout<<this->name;}
};
void visit(Student* s) {cout << s->score;
}
9.3.2 类做友元
在类中加入friend修饰的类声明,就可以将该类定义为当前类的友元
9.3.2 类做友元
在类中加入friend修饰的类声明,就可以将该类定义为当前类的友元
friend Person p;
9.3.2 成员函数做友元
在类中加入friend修饰的成员函数声明,就可以将成员函数定义为当前类的友元
friend void GoodGay::visit();
9.4 运算符重载
9.4.1 对于加号进行重载
- 成员函数重载
class Person(){Person operator+(Person &p){}
}
// 使用的时候
Person p3=p1.operator+(p2);
// 或者
Person p3=p1+p2;
- 全局函数重载
Person operator+(Person &p1, Person &p2){}
// 使用的时候
operator+(p1,p2)
// 或
Person p3=p1+p2;
9.4.2 对左移运算符进行重载
在输出的时候cout<< 无法对自定义类进行输出,所以可以对<<进行重载
通常情况下不会使用成员函数重载<<。因为无法简化为cout<<p。
- 使用全局函数重载<<
ostream& operator<<(ostream &cout,Person &p){cout<<p.name;rerturn cout;
}
9.4.3 递增运算符重载
- 前置递增重载(成员函数重载)
MyInteger& operator++(){m_num++;return *this;
}
这里必须要返回引用,当不返回引用的时候,两次连续的递增操作会出现问题
MyInteger a(0);// a.m_num=0;
cout<<++(++a);// 输出2 cout 重载过的函数,输出的是a.m_num的值
cout<<a;//输出1
这是因为如果不返回引用,那么会第一次++a会返回一个a的副本,即匿名对象,然后第二次递增是对于这个副本进行递增,因此递增的结果是正确的,但是真正的a.m_num只递增了一次
- 后置递增重载
MyInteger& operator++(int){
MyInteger tmp=*this;m_num++;return tmp;
}
这里需要注意,后置递增两次连加本身就是无法加2的,因为后置递增是先返回原值,在执行加法,即普通的int a=0;(a++)++也是等于1,在vs2022会直接报错,显示“表达式必须是可修改的左值”
9.4.4 赋值运算符重载
Person& operator=(Person &p){// 要先对原本对象存在堆区里的数据进行释放,否则就会存在一直不释放的数据if(m_Age!=NUll){delete m_Age;m_Age=NULL;}m_Age= new int(*p.m_Age);return *this;
}
9.4.5 关系运算符重载
Person& operator==(Person &p){if(m_Age==p.m_Age){return true;}return false;
}
9.4.6 函数调用运算符重载(仿函数)
class MyAdd{
public:void operator()(String s){cout<<s<<endl;}
}void test(){MyAdd myadd;int ret = myadd(100,100);cout<<ret<<endl;
}
9.5 继承
9.5.1 继承
class 子类:继承方式 父类
(子类也叫基类,父类也叫基类)
9.5.2 继承方式
- public:权限不变,即父类中的私有还是私有不可访问,受保护的还是受保护的,可以访问
- protected:父类中的公共权限和保护权限在子类中全部变为保护权限,私有不可访问
- private:公共权限和保护权限都变为私有权限,私有不可访问
9.5.3 继承中的对象
在父类中的任何非静态属性都会在子类中存在一份,只不过是父类的私有属性,无法访问
9.5.4 继承中的构造和析构顺序
base构造
son构造
son析构
base析构
9.5.5 继承中同名成员访问
class Base{public:Base(){m_A=100;}
}class Son :public Base{public:Son(){m_A=200;}
}int main(){Son s;cout<<"son"<<s.m_A<<endl;cout<<"Base"<<s.Base::m_A<<endl;
}
成员函数的调用方法和成员属性的调用方法类似
注意:当子类和父类中出现同名成员函数时,父类中的所有同名成员函数被隐藏,即子类中出现change()函数,那么父类中的change(),change(int a)等等都会被隐藏,一定要调用的时候加父类作用域。
9.5.6 多继承
class 子类:继承方式 父类1,继承方式 父类2
在实际开发中通常不建议使用多继承,父类属性出现重名情况是每次调用都需要明确作用域
9.5.6 菱形继承
- 两个父类存在相同的属性,可以通过加作用域区分 s.Sheep:m_Age; s.Tuo:m_Age;
- 存在重复属性,造成了数据的冗余,浪费内存。利用虚继承来解决:class Sheep:virtual public Animal;class Tuo:virtual public Animal; 这种情况下,s.Sheep:m_Age; s.Tuo:m_Age;是同一份数据
9.6 多态
9.6.1 基本概念
- 静态多态:函数重载、运算符重载以及复用函数名都属于静态重载
- 动态多态:派生类和虚函数实现运行时多态
静态多态和动态多态的根本区别:
- 静态多态的函数地址早绑定,编译阶段确定函数地址
- 动态多态的函数地址晚绑定,运行阶段确定函数地址
class Animal{
public:virtual void speack(){// 如果不使用虚函数,那么test函数执行的时候无论输入的是小猫,小狗都是“动物在说话”cout<<"动物在说话";}
}
class Cat:public Animal{
public:void speack(){cout<<"猫在说话";}
}
void test(Animal &animal){animal.speak();
}
动态多态满足条件:
- 有继承关系
- 子类重写父类的虚函数
重写:函数名与参数完全相同
9.6.2 纯虚函数和抽象类
- 语法:virtual 返回值类型 函数名(参数列表)= 0;
- 当类中存在纯虚函数那么这个类被称为抽象类
- 抽象类特点:
- 抽象类的子类必须重写纯虚函数。
- 抽象类无法实例化对象
9.6.3 虚析构和纯虚析构
问题:多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码,会造成内存溢出
解决办法:将父类中的析构函数改成虚析构或者纯虚析构
父类的纯虚析构代码要在类外实现
Animal::~Animal(){}