4 类和对象
类型和变量
类型=类型数据+类型操作,比如整型
数据结构=结构定义+结构操作
C++内部类型其实都是数据结构,类只是C++让我们自己来创造数据结构
数据结构的核心思想永远都是封装,将数据和行为封装到一起,也就是结构定义和结构操作封装到一起。反应到C++中其实就是类和对象这部分知识。
类其实就是数据类型,对象其实就是变量,在面向对象程序设计思想中,对象其实代表的是一个个体。
命名空间其实就是包,把不同的类,方法封装到一个包里去,防止冲突
namespace haizei { int a,b; } //命名空间的使用,把ab打包到haizei的命名空间中 int main { haizei::a = 1;//域作用符来访问 cout<<haizei::a<<endl; }
C++面向对象三大特性:封装、继承、多态
C++认为万物皆为对象,对象上有属性和行为
访问权限
struct和class都可以表现一个类,区别在于:struct的默认权限为公共,class默认权限为私有
将成员属性私有化的优点:
自己控制读写权限:提供公共接口来读写,可以控制可读可写或者只读不可写等权限
对于写权限,我们可以检测数据的有效性
一个类中,可以让另一个类作为本类的成员
分文件编写
#pragma once 防止头文件重复包含
.h 函数声明和成员变量声明即可
.c 当中实现函数,但是要加作用域::
声明和实现分文件编写,很清晰
构造和析构都是必须有的,你不写编译器给你写,但是是空的;不管是不是你写的,编译器都是自动调用的。
构造函数可以有参数,能发生函数重载;但是析构函数不能有参数,不可以发生函数重载。
个人喜欢括号法。
#include<iostream> #include<string> using namespace std; // 1.构造函数的分类和调用 // 分类 // 按照参数分类: 无参构造 有参构造 // 按照类型分类: 普通构造 拷贝构造 class Person { public: // 普通构造函数 Person() { cout << "Person的无参构造函数" << endl; } Person(int a) { age = a; cout << "Person的有参构造函数" << endl; } // 拷贝构造函数: Person(const Person &p) // 这种写法是固定的:把一个p这个Person完完全全的复制给当前的这个Person,因为你是复制的,所以你不能修改人家那个Person,所以要用const,还要用引用的形式传参,相当于拷贝了一模一样的一份数据出来 { // 将传入的人身上所有的属性拷贝到“我”身上 age = p.age; cout << "Person的拷贝构造函数调用" << endl; } ~Person() { cout << "Person的析构函数调用" << endl; } int age; }; // 调用 int main() { // 1.括号法 //Person p; // 默认构造函数的调用 //Person p2(10); // 调用有参构造函数 // 拷贝构造函数 //Person p3(p2); // 把p2身上所有的属性都拷贝到p3; // 注意事项 // 调用默认构造函数时候不要加小括号 // 否则编译器会认为是一个函数声明 //2.显示法 Person p1; Person p2 = Person(10); Person p3 = Person(p2); // 注意事项: //Person(10); // 匿名对象 特点:当前行执行结束后系统会立即回收掉匿名对象 //不要利用拷贝构造函数来初始化匿名对象 //Person(p3); // 不能这么写!编译器会认为这个等价于Person p3; 创建一个对象,还是无参构造,跟之前的重复了 //3、隐式转换法 Person p4 = 10; // 相当于Person p4 = Person(10); Person p5 = p4; // 拷贝构造 }
第二个:值传递,相当于创建了一个副本,也会调用构造函数,因为实参是类对象,所以调用的是拷贝构造函数
第三个:返回局部对象。局部对象在作用域执行完后就会释放,如果返回一个类对象,返回的不是当前的这个类对象,而是这个对象的值,调用方有一个接收对象,相当于调用拷贝构造函数进行对象创建。
值拷贝其实是对所有的值做了拷贝。
所以要不就只使用默认构造函数,不用自己写,编译器自己生成;
要不就所有的都自己写出来。
浅拷贝:编译器提供的等号的赋值操作
深拷贝: 在堆区再创建一块空间进行拷贝
什么时候用析构:如果堆区有内存,需要在析构函数里释放干净。
面试的坑!
#include<iostream> using namespace std; // 深拷贝与浅拷贝问题 class Person { public: Person() { } Person(int age, int height) { m_Age = age; // 堆区开辟的数据,需要程序员手动开辟,也需要程序员手动释放,在对象销毁前释放 m_Height = new int(height); // new方法会在堆区分配一个int空间,并且返回一个int*指针,要有指针来接收; } ~Person() { // 将我们堆区开辟的数据进行释放操作 if (m_Height != nullptr) { delete m_Height; m_Height = nullptr; // 为了防止野指针 } } int m_Age; int* m_Height; }; void test01() { Person p1(18, 160); cout << "P1的年龄为:" << p1.m_Age << endl; Person p2(p1); // 拷贝构造函数 cout << "P2的年龄为:" << p2.m_Age << endl; // 编译器自己会给类写拷贝构造函数,进行浅拷贝也就是简单的值拷贝 cout<< "P2的身高为:" << *p2.m_Height << endl; } int main() { test01(); system("pause"); return 0; }
带来的问题:堆区内存的重复释放!
浅拷贝的问题要利用深拷贝来解决:
#include<iostream> using namespace std; class Person { public: Person() { cout << "默认构造函数" << endl; } Person(int age, int height) { this->age = age; this->height = new int(height); } Person(const Person& p) { cout << "有参构造函数" << endl; age = p.age; //height = p.height 默认的拷贝构造函数 height = new int(*p.height); } ~Person() { cout << "析构函数调用" << endl; if (height != nullptr) { delete height; height = nullptr; } } int age; int* height; }; int main() { Person p1(18,120); Person p2(p1); }
总结:如果属性有在堆区开辟的,一定要提供一个拷贝构造函数,防止浅拷贝带来的问题。
用途:给类中的属性做初始化
一种方法:构造函数
另一种方法:初始化列表
#include<iostream> using namespace std; // 初始化列表 class Person { public: // 传统的默认构造函数赋值 Person(int a, int b, int c) { ma = a; mb = b; mc = c; } // 利用初始化列表赋值 Person() :ma(10), mb(20), mc(30) { } // 更加灵活 Person(int a, int b, int c) :ma(a), mb(b), mc(c) { } int ma; int mb; int mc; }; int main() { system("pause"); return 0; }
先有B还是先有A?
先有A!当其他类的对象作为本类的对象成员时候,先构造其他类的对象。先有胳膊腿,才能构造人。
析构的顺序:
先释放B,再释放A!
#include<iostream> #include<string> using namespace std; class Phone { public: Phone(string name) { p_Name = name; } string p_Name; }; class Person { public: Person(string name, string pName) :m_Name(name), m_Phone(pName) { } string m_Name; Phone m_Phone; // 隐式构造函数 }; void test01() { Person p("张三", "苹果MAX"); cout << p.m_Name << "拿着:" << p.m_Phone.p_Name << "手机" << endl; } int main() { test01(); system("pause"); return 0; }
静态成员变量!
所有对象共享同一份数据;
在编译阶段分配内存;
类内声明,类外初始化。
静态成员函数:
所有对象共享同一个函数;
只能访问静态成员变量。
静态成员函数也是根据public和private等访问权限访问的
#include<iostream> #include<string> using namespace std; // 静态成员函数 //所有对象共享同一个函数 //只能访问静态成员变量 class Person { public: static void func() { m_A = 100; // 这个是共享的,只有一份 //m_B = 100; // 不能访问,因为这个函数只有一份,不同的对象调用这个方法的时候,根本不知道这个变量是谁的,无法区分到底是哪个对象的m_B cout << "static void func()函数的调用" << endl; } static int m_A; // 静态成员变量,类内声明,类外初始化 int m_B: // 非静态成员变量 }; int Person::m_A = 0; void test01() { // 通过对象来访问 Person p; p.func(); // 通过类名来访问 Person::func(); } int main() { test01(); system("pause"); return 0; }
虽然封装的概念是把所有的成员和函数放到一起,但并不是真正的存储在一起的
#include<iostream> #include<string> using namespace std; // 成员变量和成员函数是分开存储的 class Person { int m_A; // 非静态成员变量 属于类对象 static int m_B; //静态成员变量 不属于类对象上 void func() {} // 非静态成员函数 不属于类对象上 static void func2() {} //静态成员函数 不属于类对象上 }; int Person::m_B = 2; // 类外初始化 void test01() { Person p; // 空对象占用的内存空间:1 // C++编译器会给每个空对象也分配一个字节空间,为了区分空对象占内存的位置,因为空对象很多,每个应该不一样 // 每个空对象也应该有一个独一无二的内存地址 cout << "size of p = " << sizeof(p) << endl; } void test02() { Person p; // 占用4个字节,如果没有成员,就是1,有的话就按有的这个分配就行了 cout << "size of p = " << sizeof(p) << endl; } int main() { test01(); system("pause"); return 0; }
this指针指向被调用的成员函数所属的对象
用途:
当形参和成员变量同名时,用this指针来区分。
在类的非静态成员函数中返回对象本身,可以使用return *this
#include<iostream> using namespace std; // this指针 class Person { public: Person(int age) { // this指针指向 被调用的成员函数所属的对象,也就是p this->age = age; } Person& PersonAddAge(Person &p) // 返回本题要用引用的方式做返回,这样返回的是本体;如果返回值,返回的不是本体了,而是创建了一个新的对象(参见拷贝构造函数用值的形式返回) { age += p.age; return *this; // 相当于返回p2,因为this是指向P2的指针 } int age; }; //1.解决名称冲突 void test01() { Person p(18); cout << "p的年龄为:" << p.age << endl; } //2.返回对象本身用*this void test02() { Person p1(10); Person p2(10); p2.PersonAddAge(p1).PersonAddAge(p1).PersonAddAge(p1).PersonAddAge(p1); cout << "p2的年龄为:" << p2.age << endl; } int main() { test01(); test02(); system("pause"); return 0; }
#include<iostream> using namespace std; // 空指针调用成员函数 class Person { public: void showClassName() { cout << "this is Person class" << endl; } void showPersonAge() { if (this == nullptr) // 这是防止以下错误的发生,增强代码的健壮性! return; cout << "age = " << m_Age << endl; // this->m_Age 传入的指针为空 this=nullptr,所以会报错 } int m_Age; }; void test01() { Person* p = nullptr; // 空指针 p->showClassName(); // 空指针 p->showPersonAge(); // 不能访问成员函数中带有成员变量的,因为成员变量默认前面加上了this指针,而此时this指针指向的是空的,没有实体对象 } int main() { test01(); system("pause"); return 0; }
常函数!
常函数内成员属性是只读状态的,如果非要修改,就要加mutable关键字
常对象
常对象只能调用常函数
#include<iostream> using namespace std; // const修饰成员函数 class Person { public: // this指针的本质:指针常量,指针的指向是不可以修改的 // const Person * const this // 以下的const相当于第一个const,将this变成了一个指向常量的指针常量。 // 在成员函数后面加const,修饰的其实是this指针! void showPerson() const // 常函数 { this->m_B = 100; // 这个就可以修改了 //this->m_A = 10; // 如果const,指针指向的值是可以修改的 // 如果指针指向的值都不修改,就要加const,所以就是加const位置的问题 就加到后面了 //this = nullptr; // this指针不可以被修改 } int m_A; mutable int m_B; // 特殊变量,即使在常函数中,也可以修改,就要加关键字 }; // 常对象 void test01() { const Person p; // 在对象前加const,变成常对象 p.m_A = 100; // 不可以修改 p.m_B = 100; // 加了关键字的特殊值,常对象下也可以修改 // 常对象也只能调用常函数 p.showPerson(); // 只能调用常函数 } int main() { system("pause"); return 0; }
4.4.1 全局函数作友元
#include<iostream> using namespace std; // 全部函数做友元 class Building { // goodGay全局函数是Building的好朋友,可以访问building中私有成员,只要写到这里就行了,也不用写public什么的 friend void goodGay(Building* building); public: Building() { m_SittingRoom = "客厅"; m_BedRoom = "卧室"; } public: string m_SittingRoom; // 客厅 private: string m_BedRoom; // 卧室 }; // 全局函数 void goodGay(Building* building) { cout << "好基友正在访问:" << building->m_SittingRoom << endl; cout << "好基友正在访问:" << building->m_BedRoom << endl; } void test01() { Building building; goodGay(&building); // 打印“好基友正在访问客厅” } int main() { test01(); system("pause"); return 0; }
4.4.2类作友元
一个类可以访问另一个类中的私有成员
#include<iostream> using namespace std; // 类做友元 class Building { // GoodGay这个类是Building这个类的好朋友,可以访问它的私有成员 friend class GoodGay; public: Building() { m_SittingRoom = "客厅"; m_BedRoom = "卧室"; } public: string m_SittingRoom; // 客厅 private: string m_BedRoom; // 卧室 }; // 好基友的类 class GoodGay { public: GoodGay() { // 创建一个建筑物对象 buliding = new Building; // 在堆区创建一个Building对象 } public: void visit()// 餐参观函数,访问Building中的属性 { cout << "好基友类正在访问:" << buliding->m_SittingRoom << endl; cout << "好基友类正在访问:" << buliding->m_BedRoom << endl; } Building* buliding; }; void test01() { GoodGay gg; gg.visit(); } int main() { test01(); system("pause"); return 0; }
4.4.3 成员函数做友元
#include<iostream> #include<string> using namespace std; // 成员函数做友元 class Building { // visit这个GoodGay下的成员函数是Building这个类的好朋友,可以访问它的私有成员 friend void GoodGay::visit(); public: Building() { m_SittingRoom = "客厅"; m_BedRoom = "卧室"; } public: string m_SittingRoom; // 客厅 private: string m_BedRoom; // 卧室 }; // 好基友的类 class GoodGay { public: GoodGay() { // 创建一个建筑物对象 buliding = new Building; // 在堆区创建一个Building对象 } public: void visit()// 参观函数,访问Building中的私有成员 { cout << "visit正在访问:" << buliding->m_SittingRoom << endl; cout << "visit正在访问:" << buliding->m_BedRoom << endl; } void visit2() // 让它不可以访问Building的私有成员 { cout << "visit2正在访问:" << buliding->m_SittingRoom << endl; //cout << "visit2正在访问:" << buliding->m_BedRoom << endl; } Building* buliding; }; void test01() { GoodGay gg; gg.visit(); gg.visit2(); } int main() { test01(); system("pause"); return 0; }
对于内置的数据类型,编译器知道如何运算
既可以通过成员函数重载也可以通过全局函数重载
#include<iostream> #include<string> using namespace std; // 加号运算符重载 class Person { public: //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); int m_A; int m_B; }; // 2.全局函数重载+ 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; // 返回的是值,会先创建一个对象 } void test01() { Person p1; p1.m_A = 10; p1.m_B = 10; Person p2; p2.m_A = 10; p2.m_B = 10; Person p3 = p1 + p2; // 运算符重载,也可以发生函数重载 cout << "p3.m_A = " << p3.m_A << endl; cout << "p3.m_B = " << p3.m_B << endl; } int main() { test01(); system("pause"); return 0; }
1.利用成员函数重载
2.利用全局函数重载
#include<iostream> #include<string> using namespace std; // 加号运算符重载 class Person { friend ostream& operator<<(ostream& cout, Person p); public: Person(int a, int b) { m_A = a; m_B = b; } private: int m_A; int m_B; }; // 1.<<重载不能使用成员函数来写,因为无法实现cout在左侧 // 2.只能利用全局函数重载<< ostream& operator<<(ostream &cout, Person p) // 本质 operator<< (cout, p) 简化 cout << p { cout << "m_A = " << p.m_A << endl; cout << "m_B = " << p.m_B << endl; return cout;// 实现链式表达式的方式 } void test01() { Person p(10, 10); cout << p << "hello world" << endl; } int main() { test01(); system("pause"); return 0; }
总结:重载左移运算符配合友元可以实现输出自定义数据类型
递增运算符重载++
#include<iostream> using namespace std; // 重载递增运算符 // 自定义整型 class MyInteger { friend ostream& operator<<(ostream& cout, MyInteger myint); public: MyInteger() { m_Num = 0; } // 重载前置++运算符 MyInteger& operator++() // 以引用方式返回,返回的是本身,不会再创建一个,一直对一个数据进行递增操作,可以实现链式表达式 { // 先进行++运算,再将自身做一个返回 m_Num++; // 这里的++还是内置 return *this; // 返回自身 } // 重载后置++运算符 MyInteger operator++(int) // 加int是为了区分函数重载,是一个占位参数,区分前置和后置递增,而且这里必须写int { // 先 记录当时的结果 MyInteger temp = *this; // 后 递增 m_Num++; // 最后 将记录的结果返回 return temp; // 返回值 这是一个局部对象必须返回值 } private: int m_Num; }; // 重载左移运算符 ostream& operator<<(ostream &out, MyInteger myint) { cout << myint.m_Num; return out; } void test01() { MyInteger myint; cout << ++(++myint) << endl; } int main() { test01(); system("pause"); return 0; }
总结:前置递增反回引用,后置递增返回值。
编译器还提供赋值运算符函数
值拷贝会带来浅拷贝的问题
#include<iostream> using namespace std; // 重载赋值运算符 class Person { public: Person(int age) { m_Age = new int(age); // 堆区的数据 } int* m_Age; ~Person() { if (m_Age != nullptr) { delete m_Age; m_Age = nullptr; } } // 重载 赋值运算符 Person& operator=(Person &p) { // 编译器浅拷贝 //m_Age = p.m_Age; // 应该先判断是否有属性在堆区,如果有先释放干净,再赋值拷贝 if (m_Age != nullptr) // 判断原来有没有堆区内存,如果有,先清干净 { delete m_Age; m_Age = nullptr; } // 深拷贝操作 m_Age = new int(*p.m_Age); // 返回对象本身 return *this; } }; void test01() { Person p1(18); Person p2(20); Person p3(30); p2 = p1; // 赋值操作,浅拷贝,特别注意堆区数据的赋值,赋值的其实是地址,在析构函数释放的时候,会二次释放,崩掉 // 解决方案:深拷贝! p3 = p2 = p1; // 链式表达式 cout << "p1的年龄为:" << *p1.m_Age << endl; cout << "p2的年龄为:" << *p2.m_Age << endl; cout << "p3的年龄为:" << *p3.m_Age << endl; } int main() { test01(); system("pause"); return 0; }
< > ==
#include<iostream> using namespace std; // 重载关系运算符 class Person { public: Person(string name, int age) { m_Name = name; m_Age = age; } bool operator==(Person& p) { if (this->m_Name == p.m_Name && this->m_Age == p.m_Age) return true; else return false; } string m_Name; int m_Age; }; void test01() { Person p1("Tom", 18); Person p2("Tom", 18); if (p1 == p2) { cout << "p1和p2是相等的" << endl; } } int main() { test01(); system("pause"); return 0; }
()居然也可以重载!
明明为仿函数!
#include<iostream> #include<string> using namespace std; // 重载函数调用运算符重载 class MyPrint { public: void operator()(string test) { cout << test << endl; } }; void test01() { MyPrint myPrint; myPrint("hello world"); // 由于使用起来非常像是函数调用,因此称为仿函数 } // 仿函数非常灵活,没有固定的写法 // 匿名函数对象:当前行执行完,立刻被释放 int main() { test01(); system("pause"); return 0; }
好处:减少重复代码!
语法:class 子类 ; 继承方式 父类
子类——派生类
父类——基类
父类中私有的内容,子类中不管是哪种继承方式都不能访问:父亲也是有隐私的。
公共继承:
父类中公有的和保护的属性,子类中一样的。
保护继承:
父类中公有的和私有的都变为保护权限。
私有权限:
父类中的公有和保护内容在子类中都是私有。
父类中所有的非静态成员属性都会被子类继承下去,也就是说非静态成员属性是属于子类的,sizeof要算上;但是要注意,子类继承了父类所有的成员,这点跟前面说的不是一个意思哈
父类中私有的成员属性是被编译器隐藏了,访问不到,但确实是被继承下去了
#include<iostream> using namespace std; // 继承中的对象模型 class Base { public: int m_A; protected: int m_B; private: int m_C; // 私有成员只是被隐藏了,但是还是会继承下去 }; // 公共继承 class Son :public Base { public: int m_D; }; void test01() { // 16 父类中所有的非静态成员属性都会被子类继承下去 // 父类中私有的成员属性是被编译器隐藏了,访问不到,但确实是被继承下去了 cout << "size of Son = " << sizeof(Son) << endl; //输出了:16 } int main() { test01(); system("pause"); return 0; }
也就是父类和子类的构造顺序谁先谁后:先构造父类、再构造儿子
父类和子类的析构顺序谁先谁后:先析构儿子、再析构父亲
#include<iostream> using namespace std; // 继承中同名成员的处理方式 class Base { public: Base() { m_A = 100; } void func() { cout << "Base下的func成员函数调用" << endl; } void func(int a) { cout << "Son下的func(int a)成员函数调用" << endl; } int m_A; }; class Son :public Base { public: Son() { m_A = 200; } void func() { cout << "Son下的func成员函数调用" << endl; } int m_A; }; // 同名成员属性 void test01() { Son s; s.func(); // 直接调用调用的是子类中的同名函数 s.Base::func(); // 访问父类的同名成员函数,加上作用域 // 如果子类中出现和父类同名的成员函数,子类的同名成员函数会隐藏掉父类中所有的成员函数 //s.func(100); // 如果想访问到,就要加作用域 s.Base::func(100); cout << s.m_A << endl; // 访问的是儿子的 cout << s.Base::m_A << endl; // 访问父类的,加上父类作用域 } // 同名成员函数 void test02() { } int main() { test01(); system("pause"); return 0; }
#include<iostream> using namespace std; // 继承中同名静态成员的处理方式 class Base { public: static int m_A; // 类内声明 static void func() { cout << "父类static void func()调用" << endl; } }; int Base::m_A = 100; // 类外初始化 class Son :public Base { public: static int m_A; // 类内声明 static void func() { cout << "子类static void func()调用" << endl; } }; int Son::m_A = 200; // 类外初始化 // 同名静态成员属性 void test01() { // 通过对象访问数据 Son s; cout << s.m_A << endl; // 直接访问访问子类 cout << s.Base::m_A << endl; // 加作用域访问父类静态成员 // 通过类名访问数据 cout << Son::m_A << endl; // 访问父类的 // 第一个::代表通过类名方式访问 第二个::代表访问父类作用域下的数据 cout << Son::Base::m_A << endl; // 通过类名的方式访问父类下的数据 } // 同名静态成员函数 void test02() { // 通过对象访问 Son s; s.func(); // 调用子类的 s.Base::func(); // 调用父类的 // 通过类名方式访问 Son::func(); Son::Base::func(); // 通过子类访问父类的static函数 } int main() { test01(); system("pause"); return 0; }
当父类中出现了同名的内容,访问的时候需要加作用域,否则会出现二义性,指定不明确
通常不建议多继承,可能是多人开发,有可能会同名,增加麻烦
vbptr:虚基类指针 指向一个叫vbtable,虚基类表
所以虚继承解决多份数据的问题是通过虚基类指针解决的,指针指向的是同一个数据
例子当中,羊驼从羊和驼父类中各自继承了一个虚基类指针,这两个指针通过偏移量指向唯一的一份数据,所以解决了这个问题
#include<iostream> using namespace std; // 动物类 class Animal { public: int m_Age; }; // 利用虚继承可以解决菱形继承的问题 // 在继承之前加上关键字virtual // Animal 类称为 虚基类 // 虚继承之后,羊驼从动物类继承来的数据就只有一份 // 羊类 class Sheep :virtual public Animal {}; // 虚继承 // 驼类 class Tuo :virtual public Animal {}; // 羊驼类 class SheepTuo :public Sheep, public Tuo {}; void test01() { SheepTuo st; //st.m_Age = 18; // 不明确 // 当出现菱形继承的时候,两个父类拥有相同的数据,需要加以作用域区分 st.Sheep::m_Age = 18; // 加作用域去区分 st.Tuo::m_Age = 28; // 这份数据我们知道只要有一份就可以了,菱形数据导致数据有两份,浪费资源 } int main() { system("pause"); return 0; }
C++经常说的多态是动态的多态
静态多态和动态多态的区别
#include<iostream> using namespace std; class Animal { public: // 虚函数 virtual void speak() { cout << "动物在说话" << endl; } }; class Cat :public Animal { public: void speak() // 子类的virtual可写可不写 { cout << "小猫在说话" << endl; } }; // 执行说话的函数 // 地址早绑定 在编译阶段就确定了函数的地址 // 如果想执行让猫说话,这个函数的地址就不能提前绑定,需要在运行阶段进行绑定,也就是地址晚绑定 // 动态多态的满足条件 // 1.得有继承关系 // 2.子类要重写父类的虚函数 重写:所有内容都相同 返回值类型 函数名称 参数 // 动态多态的使用 // 父类的指针或者引用 指向子类对象 void doSpeak(Animal &animal) // Animal & animal = cat; { animal.speak(); } void test01() { Cat cat; // 本意是传谁让谁说话 doSpeak(cat); } int main() { test01(); return 0; }
原理:
Animal类中有虚函数,有虚函数指针,指向虚函数表,虚函数表中记录的是Animal::speak函数的入口地址,当Cat类继承Animal,但是并没有重写speak方法的时候,Cat类相当于继承了Animal类的所有,包括虚函数指针和虚函数表,这个时候Cat中的虚函数指针指向的虚函数表中记录的还是Animal::speak方法;但是如果Cat重写了speak虚函数,那么Cat的虚函数指针指向的虚函数表中记录的就是Cat::speak的入口地址了。当Animal &animal = cat时候,animal是引用cat的引用,调用speak方法时候,自然就调用的是cat的speak方法。
#include<iostream> using namespace std; // sizeof(Animal) = 1;这是正常情况下 // 但是如果有虚函数 sizeof(Animal) = 4; // 指针vfptr: // v:virtual // f:function // ptr:pointer // 虚函数(表)指针 // 指针指向虚函数表vftable,里头存放的是虚函数表,表内记录虚函数的地址 class Animal { public: // 虚函数 virtual void speak() { cout << "动物在说话" << endl; } }; class Cat :public Animal { public: void speak() // 子类的virtual可写可不写 { cout << "小猫在说话" << endl; } }; // 执行说话的函数 // 地址早绑定 在编译阶段就确定了函数的地址 // 如果想执行让猫说话,这个函数的地址就不能提前绑定,需要在运行阶段进行绑定,也就是地址晚绑定 // 动态多态的满足条件 // 1.得有继承关系 // 2.子类要重写父类的虚函数 重写:所有内容都相同 返回值类型 函数名称 参数 // 动态多态的使用 // 父类的指针或者引用 指向子类对象 void doSpeak(Animal &animal) // Animal & animal = cat; { animal.speak(); } void test01() { Cat cat; // 本意是传谁让谁说话 doSpeak(cat); } int main() { test01(); return 0; }
#include<iostream> #include<string> using namespace std; //分别利用普通写法和多态技术实现计算器 //普通写法 class Calculator { public: int getResult(string oper) { if (oper == "+") { return m_Num1 + m_Num2; } else if (oper == "-") { return m_Num1 - m_Num2; } else if (oper == "*") { return m_Num1 * m_Num2; } // 如果想扩展新的操作,需要修改源码 // 在真实的开发中,提倡开闭原则:对扩展进行开放,对修改进行关闭 } int m_Num1; int m_Num2; }; void test01() { // 创建计算器对象 Calculator c; c.m_Num1 = 10; c.m_Num2 = 10; cout << c.m_Num1 << "+" << c.m_Num2 << "=" << c.getResult("+") << endl; cout << c.m_Num1 << "-" << c.m_Num2 << "=" << c.getResult("-") << endl; cout << c.m_Num1 << "*" << c.m_Num2 << "=" << c.getResult("*") << endl; } // 利用多态来实现计算器 // 多态带来的好处 // 1.组织结构清晰 // 2.可读性强 // 3.对于前期和后期的扩展和维护性高 // 实现一个计算器的抽象类 class AbstractCalculator { public: virtual int getResult() { return 0; } int m_Num1; int m_Num2; }; // 加法计算器类 class AddCalculator :public AbstractCalculator { public: virtual int getResult() { return m_Num1 + m_Num1; } }; // 减法计算器类 class SubCalculator :public AbstractCalculator { public: virtual int getResult() { return m_Num1- m_Num1; } }; // 乘法计算器类 class MulCalculator :public AbstractCalculator { public: virtual int getResult() { return m_Num1 * m_Num1; } }; void test02() { // 多态使用条件:父类的指针或者引用来指向子类对象 // 加法运算 // 用父类指针去指向新创建的加法计算器类 AbstractCalculator* abc = new AddCalculator; abc->m_Num1 = 100; abc->m_Num2 = 100; cout << abc->m_Num1 << "+" << abc->m_Num2 << "=" << abc->getResult() << endl; // 用完记得销毁 delete abc; // 只是释放了堆区的内存,类型没有变 // 减法运算 abc = new SubCalculator; abc->m_Num1 = 100; abc->m_Num2 = 100; cout << abc->m_Num1 << "-" << abc->m_Num2 << "=" << abc->getResult() << endl; delete abc; // 乘法运算 abc = new MulCalculator; abc->m_Num1 = 100; abc->m_Num2 = 100; cout << abc->m_Num1 << "*" << abc->m_Num2 << "=" << abc->getResult() << endl; delete abc; } int main() { test02(); return 0; }
C++中非常提倡利用多态设计程序架构,因为多态优点很多
虚函数里面的代码实现是毫无意义的,就可以变为纯虚函数
只要写了一个纯虚函数,这个类就变成了抽象类
抽象类有两个特点
然后就可以利用正常的多态的技术了
#include<iostream> #include<string> using namespace std; //纯虚函数和抽象类 class Base // 抽象类 { public: // 只要有一个纯虚函数,这个类就是抽象类 // 抽象类特点:1.无法实例化对象 // 2.抽象类的子类必须要重写父类中的纯虚函数,否则也属于抽象类 // 写抽象类的目的:就是想写多态,让子类重写父类的纯虚函数,否则没有意义 virtual void func() = 0; // 纯虚函数 }; class Son :public Base { public: virtual void func() { cout << "func()函数的调用" << endl; } }; void test01() { //Base b; // 栈区不行 //new Base; // 堆区也不行 //Son son; // 子类重写了父类的纯虚函数 Base* base = new Son; base->func() } int main() { test01(); return 0; }
#include<iostream> using namespace std; // 多态案例二:制作饮品 class AbstructDrinking { public: // 煮水 virtual void Boil() = 0; // 冲泡 virtual void Brew() = 0; //倒入杯中 virtual void PourInCup() = 0; // 加入辅料 virtual void PutSomething() = 0; // 制作饮品 void makeDrinking() { Boil(); Brew(); PourInCup(); PutSomething(); } }; // 制作咖啡 class Coffe :public AbstructDrinking { public: // 煮水 virtual void Boil() { cout << "煮冰水" << endl; } // 冲泡 virtual void Brew() { cout << "冲泡咖啡豆" << endl; } //倒入杯中 virtual void PourInCup() { cout << "倒入杯中" << endl; } // 加入辅料 virtual void PutSomething() { cout << "加入牛奶" << endl; } }; // 制作茶叶 class Tea :public AbstructDrinking { public: // 煮水 virtual void Boil() { cout << "煮茶水" << endl; } // 冲泡 virtual void Brew() { cout << "冲泡茶叶" << endl; } //倒入杯中 virtual void PourInCup() { cout << "倒入杯中" << endl; } // 加入辅料 virtual void PutSomething() { cout << "加入糖" << endl; } }; // 制作咖啡 void doWork(AbstructDrinking * abs) // AbstructDrinking * abs = new Coffe { abs->makeDrinking(); // 一个接口不同行为,只是传入的对象不一样 delete abs; // 防止内存泄露 } void test01() { // 制作咖啡 doWork(new Coffe); // 制作茶叶 doWork(new Tea); } int main() { test01(); return 0; }
什么时候析构函数要用虚函数:当父类指针指向一个子类对象的时候,如果这个子类对象当中有从堆区创建的数据,你在删除父类指针的时候,子类的析构函数不会执行,会导致堆区的内存泄露,所以要把父类的析构函数写成虚析构,这样就能走到子类的析构函数了。
不管是虚析构还是纯虚析构,都是为了解决子类中的析构函数调不到的问题
#include<iostream> #include<string> using namespace std; // 虚析构和纯虚析构 class Animal { public: Animal() { cout << "Animal构造函数调用" << endl; } // 利用虚析构可以解决 父类指针释放子类对象时不干净的问题 /*virtual ~Animal() { cout << "Animal析构函数调用" << endl; }*/ // 纯虚析构 需要声明也需要实现 跟纯虚函数不一样 因为有可能有些数据也是开辟到堆区的 // 有了纯虚析构之后,这类也属于抽象类,无法实例化对象 virtual ~Animal() = 0; //纯虚函数 virtual void speak() = 0; }; // 纯虚虚构函数的实现 Animal::~Animal() { cout << "Animal纯虚析构函数调用" << endl; } class Cat :public Animal { public: Cat(string name) { cout << "Cat构造函数调用" << endl; m_Name = new string(name); // 创建在堆区,需要释放 } virtual ~Cat() { if (m_Name != nullptr) { cout << "Cat析构函数调用" << endl; delete m_Name; // 先释放 m_Name = nullptr; // 再指向空指针,防止野指针出现 } } virtual void speak() { cout <<*m_Name<< "Cat在说话" << endl; } string *m_Name; // 让小猫的名字创建在堆区,用这个指针去维护它 }; void test01() { Animal* animal = new Cat("Tom"); // Cat是继承来的,先构造父类,再构造子类 animal->speak(); // 父类的指针在析构的时候不会调用子类的析构函数,导致如果子类有堆区数据造成内存泄漏 delete animal; } int main() { test01(); return 0; }
#include<iostream> #include<string> using namespace std; // 抽象CPU类 class CPU { public: // 抽象计算函数 virtual void calculate() = 0; }; // 抽象显卡类 class VideoCart { public: // 抽象显示函数 virtual void display() = 0; }; /// 抽象内存条类 class Memory { public: // 抽象存储函数 virtual void storage() = 0; }; // 计算机抽象类 class Computer { public: Computer(CPU* cpu, VideoCart* vc, Memory* mem) { m_cpu = cpu; m_vc = vc; m_mem = mem; } // 提供工作的函数 void work() { m_cpu->calculate(); m_vc->display(); m_mem->storage(); } // 提供一个析构函数,释放三个电脑零件 ~Computer() { if (m_cpu != nullptr) { delete m_cpu; m_cpu = nullptr; } if (m_vc != nullptr) { delete m_vc; m_vc = nullptr; } if (m_mem != nullptr) { delete m_mem; m_mem = nullptr; } } private: CPU* m_cpu; VideoCart* m_vc; Memory* m_mem; }; // 具体的厂商 class IntelCPU :public CPU { public: void calculate() { cout << "Intel的CPU开始计算了" << endl; } }; class IntelVideoCart :public VideoCart { public: void display() { cout << "Intel的显卡开始显示了" << endl; } }; class IntelMemory :public Memory { public: void storage() { cout << "Intel的内存条开始存储了" << endl; } }; void test01() { // 第一台电脑零件 CPU* intelCPU = new IntelCPU; VideoCart* intelCard = new IntelVideoCart; Memory* intelMem = new IntelMemory; // 创建第一台电脑 Computer* computer1 = new Computer(intelCPU, intelCard, intelMem); computer1->work(); delete computer1; //delete的时候会调用Computer的析构函数,零件也会被释放干净 } int main() { test01(); return 0; }