C++进阶--类和模板 类和对象其不和python一样不加入括号python就只是创建给类取了别名但是该类没有任何函数作用也没有初始化。而c其都会默认调用构造函数和析构函数除了调用无参构造函数时使用(Person p())这会导致编译器将其识别为定义一个函数类型为Person名字为p没有参数三大特性封装、继承、多态和python中的类相似语法class 类名{访问权限: 属性/行为}#include iostream #include string using namespace std; const double PI 3.14; class Circle { // 访问权限 // 公共权限 public: // 属性 int radius; // 半径 // 行为 double getArea() { return PI * radius * radius; }// 求圆的面积 }; int main() { Circle c1; // 创建一个圆对象 c1.radius 5; // 设置圆的半径 cout 圆的半径: c1.radius endl; // 输出圆的半径 cout 圆的面积: c1.getArea() endl; // 输出圆的面积 system(pause); return 0; }类中的属性和行为统称为成员属性称为成员属性/成员变量行为称为成员函数/成员方法创建一个对象称为实例化属性和行为都有权限访问权限公共权限--public保护权限--protected私有权限--private下面的保护权限和私有权限主要在继承父类是有所不同构造函数自动调用初始化对象或者变量定义类名 (){}分类按参数分有参构造/无参构造按类型分普通构造/拷贝构造// 无参构造 Person() { cout Person的构造函数被调用了 endl; } // 有参构造 Person(string name, int age) { cout Person的构造函数被调用了 endl; } // 拷贝构造 Person(const Person p) { cout Person的构造函数被调用了 endl; }所有的无参构造都是普通构造有参构造中除了使用参数为const修饰的引用外也都是普通构造。调用// 括号法 Person p1; Person p2(111, 10); Person p3(p2); // 显示法 Person p1; Person p2 Person(Alice, 30); Person p3 Person(p2); // 隐式转换法 Person p1; Person p2 { 张三, 20}; Person p3 p1;调用默认构造时即无参构造不要加()因为编译器会认为这是一个函数的声明而非是创建对象。使用匿名对象时即Person(111, 10)调用有参构造时在当前行执行结束后系统会立即回收匿名对象。不要利用拷贝函数初始化匿名对象(Person(p2))因为这个代码等价于Person p2即使用无参构造实例化一个对象p2再由于拷贝构造需要一个具体的实例化对象因为这就会导致p2的重定义。拷贝函数的调用时机析构函数自动调用清理对象或者变量的缓存一般用于释放使用new创建的堆区数据~类名 (){}#include iostream #include string using namespace std; class Person { public: Person() { cout Person的构造函数被调用了 endl; } ~Person() { cout Person的析构函数被调用了 endl; } }; void test01() { Person p; // 创建一个对象 } int main() { test01(); // 调用函数创建对象 system(pause); return 0; }深浅拷贝当你使用无参/有参构造函数来创建一个对象p1后使用系统默认的拷贝构造函数创建一个新的对象p2这个p2的拷贝属于浅拷贝即是将变量的地址、值等所有都拷贝过来。如果是使用new创建的变量其会在堆区储存值而堆区中的值需要使用delete来自行清理因此一般都需要在析构函数中提供delete代码进行删除。由于创建了两个对象所以就会删除两次同一个堆区的数据根据“先进后出”的原则先将p2中堆区的值删除然后删除p1的值这样系统就会报错。这是因为p2的值率先一步被删除由于浅拷贝p1的值和p2的值保存在同一个地方删除p1时编译器会发现p1这个地区是空的从而导致报错。因此简单来说浅拷贝带来的问题就是堆区的内存重复释放。为了解决这个问题就需要使用深拷贝。#include iostream #include string using namespace std; class Person { public: int Age; int* Height; // 有参构造 Person(int age, int height) { Age age; Height new int(height); // 动态分配内存存储身高值 cout Person有参构造函数的调用 endl; } // 拷贝构造 Person(const Person p) { cout Person拷贝构造函数的调用 endl; Age p.Age; Height p.Height; // 浅拷贝系统默认的拷贝构造函数就是这样实现的直接复制指针地址 // height new int(*p.height); // 深拷贝 } ~Person() { if (Height ! nullptr) { delete Height; // 释放动态分配的内存 Height nullptr; } cout Person的析构函数被调用了 endl; } }; void test01() { Person p1(18, 170); Person p2 p1; // 调用拷贝构造函数 } int main() { test01(); // 调用函数创建对象 system(pause); return 0; }浅拷贝深拷贝初始化列表构造函数():属性1(值1),属性2(值2)...{}Person(int age, int height):Age(age), Height(new int(height)){ // 默认构造函数初始化年龄和身高 cout Person默认构造函数的调用 endl; }嵌套类#include iostream #include string using namespace std; class Phone { public: string Brand; int Size; Phone(string brand, int size):Brand(brand), Size(size){ cout Phone默认构造函数的调用 endl; } ~Phone() { cout Phone的析构函数被调用了 endl; } }; class Person { public: int Age; int* Height; Phone MyPhone; Person(int age, int height, string brand, int size):Age(age), Height(new int(height)), MyPhone{brand, size}{ // 默认构造函数初始化年龄和身高 cout Person默认构造函数的调用 endl; } ~Person() { if (Height ! nullptr) { delete Height; // 释放动态分配的内存 Height nullptr; } cout Person的析构函数被调用了 endl; } }; void test01() { Person p1(18, 170, 小米, 134); cout Age p1.Age endl; // 输出年龄 cout Height *(p1.Height) endl; // 输出身高 cout Brand p1.MyPhone.Brand endl; // 输出手机品牌 cout Size p1.MyPhone.Size endl; // 输出手机尺寸 } int main() { test01(); // 调用函数创建对象 cout ----------------------------- endl; system(pause); return 0; }构造函数先调用类内部的类的构造函数再调用外部的类的构造函数但是析构函数恰好相反。静态成员静态成员变量class Person { public: static int Age; // 类内声明 }; int Person::Age 0; // 类外初始化 void test01() { Person p1; cout Age p1.Age endl; // 通过对象访问静态成员变量 Person p2; p2.Age 10; cout Age p1.Age endl; //在另一个对象中修改静态变量其本身的值也会被修改 cout Age Person::Age endl; // 通过类名访问静态成员变量 } //Age 0 //Age 10 //Age 10静态变量也有访问权限设置为保护或者私有无法像上述表达中一样在外部访问静态变量。静态成员函数class Person { public: static int m_A; // 静态成员变量 int m_B 80; // 非静态成员变量 static void func() { cout 静态成员变量 m_A m_A endl; //cout 静态成员变量 m_B m_B endl; 报错 } }; int Person::m_A 100; // 静态成员变量初始化 void test01() { Person p1; p1.func(); // 通过对象访问静态成员函数 Person::func(); // 通过类名访问静态成员函数 }静态函数也有访问权限设置为保护或者私有无法像上述表达中一样在外部访问静态变量。内存类的内存只有成员变量的内存即当内部只有一个int成员变量时其大小为4字节。如类为空则内存为1字节用于保存类在内存的空间同时该内存也有内存对齐静态成员变量、非静态成员函数、静态成员函数都不属于类对象上因此在内部书写不会占用内存class Person { public: static int m_A; // 静态成员变量 int m_B 80; // 非静态成员变量 char m_C; double m_D; static void func() { cout 静态成员变量 m_A m_A endl; //cout 静态成员变量 m_B m_B endl; 报错 } }; # 16字节静态成员变量和静态成员函数不占类的内存int占4字节、char占1字节、double占8字节但是由于内存对齐char会多出3个字节进行填充和int一起共占用8个字节故而一共占用16个字节。this指针--指针常量指向无法修改定义this指针是一个指向被调用成员函数所属对象的一种指针。用途class Person { public: int age; Person(int age){ this-age age; } // 构造函数初始化年龄 Person AddPerson(Person p) { this-age p.age; // 将当前对象的年龄与传入对象的年龄相加 return *this; // 解引用返回当前对象的引用 } }; void test01() { Person p1(20); cout p1.age endl; // 输出对象的大小 Person p2(10); p2.AddPerson(p1).AddPerson(p1); // 链式调用连续添加年龄 cout p2.age endl; // 输出对象的大小 }当你不使用初始化列表初始化构造函数时由于上面介绍的作用域我们可以知道如果不加入this系统会出现乱码因此加入this即是将左边的age定义为被调用成员函数所属对象的age从而避免两者的名称冲突。下面的链式调用同理在第一次p2.AddPerson(p1)返回一个p2然后再调用AddPerson(p1)从而实现两次对于AddPerson(p1)的调用这个返回值便可以使用this指针通过解引用的方法进行返回。Person AddPerson(Person p) { this-age p.age; // 将当前对象的年龄与传入对象的年龄相加 return *this; // 返回当前对象的引用 } Person AddPerson(Person p) { this-age p.age; // 将当前对象的年龄与传入对象的年龄相加 return *this; // 返回当前对象的引用 }上面的是返回的是一个新的Person变量 其是被调用对象的值拷贝如果使用这个成员函数那么输出的值为30下面的则是返回被调用对象的引用其是该函数作用后的值如果使用这个成员函数那么输出的值为50常函数/常对象常函数的主要用途是服务于常对象的某一些对象由于其特殊性导致其内部的值不能进行修改因此会使用const改变其权限为可读但是这个对象需要调用一些局部的变量因此就需要使用常函数调用。可以看出上图中有两处报错一次是在常函数中修改age的值第二是通过常对象调用普通函数。同时也可以得知加入一个mutable关键字声明后变量在常函数/常对象中可以正常修改。类中每次变量的正确引用其实相当于前面都加入了this如上为this-age a而在常函数后面加入const就相当于在指针常量前面再加一个const指针常量本就无法修改它的指向再加一个导致其的值也无法修改。友元--friend实现方式全局函数做友元类做友元成员函数做友元#include iostream #include string using namespace std; class Person { friend void printAge(const Person p) { cout Age: p.age endl; } private: int age 15; }; void printAge(const Person p); int main() { Person p; printAge(p); system(pause); return 0; }运算符重载概念对已有运算符重新进行定义赋予其另一种功能以适应不同的数据类型。注意事项1、运算符重载也可以发生函数重载即通过不同的参数个数/类型/顺序通过同一个函数名引用不同的函数2、该函数不可以使用引用因为返回的temp是一个新创建的个体而引用却是给被调用对象起一个别名。3、对于内置的数据类型的运算符不可能改变即两个整型/两个浮点型/一个整型和一个浮点型等4、不要滥用运算重载即operator后面是什么符号函数内实现的就应该是什么功能实现其他功能虽然不会报错也能正常运行但是容易让人产生歧义。加号/减号/乘号/除号/取余运算符重载实现俩个自定义数据类型的相加/相减/相乘/相除/取余#include iostream #include string using namespace std; class Person { public: int m_A; int m_B; //// 内部定义成员函数重载号 //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 operator(Person p); }; Person 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 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 20; Person p2; p2.m_A 10; p2.m_B 20; 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; }左移运算符重载输出自定义数据类型#include iostream #include string using namespace std; class Person { public: int m_A; int m_B; }; ostream operator(ostream cout, Person p) { cout m_A p.m_A endl; cout m_B p.m_B endl; return cout; } void test01() { Person p1; p1.m_A 10; p1.m_B 20; cout p1 endl; } int main() { test01(); system(pause); return 0; }注意事项1、cout为ostream类型输出流2、重载左移运算符时避免在类中定义局部重载函数这是因为当你定义这个局部重载函数时输出应为p cout这不符合c常规的编程习惯3、如果你想实现上述代码中cout p后续再输出值则需要在重载函数中返回cout这是因为这种输出本质就是链式输出前一个的右值作为后一个的左值。递增运算符重载通过重载递增运算符实现自己的整型数据//前置 MyIntger operator() { num; return *this; } //后置 MyIntger operator(int) { MyIntger temp *this; num; return temp; }赋值运算符重载//赋值运算符重载 Person operator(Person p) { if (age ! nullptr) { delete age;; age nullptr; } age new int(*p.age); return *this; }赋值运算符主要是为了避免重复释放堆区的内存从而导致系统的崩溃具体原因和上面深浅拷贝中下相同。关系运算符重载//关系运算符重载 bool operator(Person p) { if (this-m_B p.m_B this-num p.num) { return true; } return false; }函数调用运算符重载//关系运算符重载 void operator()(string str) { cout 调用了函数调用运算符重载 endl; cout str str endl; } int operator()(int a, int b) { cout 调用了函数调用运算符重载 endl; cout a a b b endl; return a b; }继承class 子类: 继承方式 父类class son: public father{}son包含father中所有的成员信息同时也可以定义自己的成员信息相当于python的继承super.__init__其内部的构造函数和析构函数现运行父类的构造函数再运行子类的构造函数后续先运行子类的析构函数再运行父类的析构函数继承方式公共继承public、保护继承protected、私有继承private继承同名成员处理方式访问同名变量Son s; cout Son下的a s.a endl; //访问子类同名成员 cout Base下的a s.Base::a endl; //访问父类同名成员当子类需要使用一个与父类同名的变量时需要区分该同名变量属于父类还是子类直接继承父类和子类同时使用一个同名变量class Base { public: int a; Base() { a 100; } }; class Son :public Base { public: Son() { a 200; } }; void test01() { Son s; cout Son下的a s.a endl; cout Base下的a s.Base::a endl; }父类和子类需要不同的同名变量class Base { public: int a; Base() { a 100; } }; class Son :public Base { public: int a;//多出一个子类的自定义同名变量 Son() { a 200; } }; void test01() { Son s; cout Son下的a s.a endl; cout Base下的a s.Base::a endl; }同名函数Son s; s.func(); s.Base::func();需要注意的是如果子类中出现和父类同名的成员函数子类的同名成员会隐藏掉父类中的所有同名成员函数class Base { public: void func() { cout Base下的func函数被调用了 endl; } void func(string str) { cout Son下的func函数被调用了参数是 str endl; } };可以看出之前使用继承直接调用的写法出错需要添加作用域。静态变量由于静态变量的特殊性其不属于某个对象而是属于类本身因此继承后子类和父类共同分享一个静态成员不会创建额外的副本多继承语法--不常用c允许一个类继承多个类语法class 子类:继承方式 父类1, 继承方式 父类2当父类中同时包含相同的同名变量时子类需要访问父类该变量时需要在访问前面加入作用域避免“二义性”菱形继承加入一个基类有一个变量为age下面两个派生类同时继承了这个变量age但是这两个派生类下的子类同时继承了这两个派生类这就出现了一个问题最后的这个子类也需要一个age但是这个age无法确定是来自哪一个派生类的。此时便可以利用虚继承来解决这个问题即在继承之前加入关键字virtual。多态满足条件关键字--virtualvirtual关键字除了解决上面出现的“二义性”的问题含有一个便是将函数变为虚函数从而实现动态多态静态#include iostream #include string using namespace std; class Animal { public: void speak() { cout Animal speak endl; } }; class Cat : public Animal { public: void speak() { cout Cat speak endl; } }; void doSpeak(Animal animal) { animal.speak(); } int main() { Animal animal; Cat cat; doSpeak(animal); doSpeak(cat); system(pause); return 0; }很明显可以看出doSpeak中传入的是Animal这个类的引用也就是这个类的别名因此后续的调用也一定是调用这个类中的函数但是如果我们想调用子类的同名函数这就需要将该函数设置为虚函数。动态#include iostream #include string using namespace std; class Animal { public: virtual void speak() { cout Animal speak endl; } }; class Cat : public Animal { public: void speak() { cout Cat speak endl; } }; void doSpeak(Animal animal) { animal.speak(); } int main() { Animal animal; Cat cat; doSpeak(animal); doSpeak(cat); system(pause); return 0; }virtual的中文意思本就是“虚拟的”因此在函数或者变量前面加virtual也就是将这个函数或者变量变为虚函数或者虚变量。虚函数/虚变量就像它的名字一样其本身就是虚拟的随时可能会被覆盖点因此只有子类中存在一个同名的函数/变量那么就会像父类的虚函数/虚变量给覆盖掉。virtual的本质就是创建一个vfptr(虚函数指针)也就是说一个指针变量故而创建它便需要直接占用四个字节的类空间内存例如你的空类为1字节加入一个成员函数仍然是1字节但是假如一个虚成员函数便会使这个类占用4个字节。当程序在编译时发现了一个virtual函数便会马上创建一个vfptr并立马初始化直接指向vftable(虚函数表)而这个vftable中便储存着函数的相关代码的地址代码一般都储存在程序的代码段一个类中的vfptr和vftable是不允许更改的。当该类创建了一个子类时这个子类便会自己创建一个vfptr和vftablevfptr直接指向vftable同时这个vfptr和vftable都是新的独立的。但vftable中储存的地址仍然是父类的地址由于继承。因此当我们调用这个子类的这个函数时没有写同名函数便是通过这个指针访问子类的vftable从而再运行的。但是当在子类中创建同名函数时那么程序便会重新创建一个新的代码段地址并覆盖子类之间继承的地址从而到达一个多态的目的。实例#include iostream #include string using namespace std; class Calculator { public: int num1; int num2; Calculator(int num1, int num2) :num1(num1), num2(num2) { } virtual int cal() { return 0; } }; class Add :public Calculator { public: Add(int num1, int num2) :Calculator(num1, num2) { } int cal() { return num1 num2; } }; class Sub :public Calculator { public: Sub(int num1, int num2) :Calculator(num1, num2) { } int cal() { return num1 - num2; } }; int main() { int num1; int num2; cin num1; cin num2; //引用调用 //Add add(num1, num2); //Sub sub(num1, num2); //Calculator cal_add add; //Calculator cal_sub sub; //cout num1 num2 cal_add.cal() endl; //cout num1 - num2 cal_sub.cal() endl; //指针调用 Calculator* cal_add new Add(num1, num2); Calculator* cal_sub new Sub(num1, num2); cout num1 num2 cal_add-cal() endl; cout num1 - num2 cal_sub-cal() endl; system(pause); return 0; }纯虚函数语法virtual 返回值类型 函数名 (参数列表) 0;当有了这个纯虚函数该类也被称为抽象类特点原因从上面的案例也可以看出来其实父类的虚函数根本用不到主要都是调用子类重写的内容因此给父类加入纯虚函数可以使子类必须写该同名函数否则也无法实例化对象虚析构和纯虚析构多态使用时如果子类中有属性开辟到堆区那么父类指针在释放时无法调用到子类的析构代码从而导致内存泄漏。只要定义了虚函数那么最好的再定义一个虚析构函数避免内存的泄露语法virtual ~类名(){}--虚析构语法virtual ~类名() 0;类名::类名(){}--纯虚析构函数纯虚析构函数和纯虚函数相同都会将类归属于抽象类而无法实例化同时在写纯虚析构函数时由于程序一定会调用这个这个函数所有一定要书写这个函数的内容只不过需要在类外定义。模板--泛型编程函数模板作用建立一个通用函数其函数返回值类型和形参类型可以不具体制定用一个虚拟的类型来代表。语法templatetypename T函数声明或定义template---声明创建模板typename---表面其后面的符号是一种数据类型可以用class代替T---通用数据类型名称可以替代通常为大写字母模板后面必须直接跟它要修饰的函数 / 类 / 结构体中间不能插入任何代码注释和空行除外同时如果函数的声明和定义分开写那么也需要重新写template头使用#include iostream #include string using namespace std; templatetypename T void Swap(T a, T b) { T temp a; a b; b temp; cout a a endl; cout b b endl; } int main() { int a 10; int b 11; //自动类型推导 Swap(a, b); //显示指定类型 Swapint(a, b); system(pause); return 0; }注意事项普通函数和函数模板区别调用运算符重载模板#include iostream #include string using namespace std; class Person { public: string name; int age; Person(string name, int age) :name(name), age(age) { } }; templatetypename T bool Compare(T a, T b) { return (a b); } template bool Compare(Person p1, Person p2) { return (p1.name p2.name) (p1.age p2.age); } int main() { Person p1 { tom, 13 }; Person p2 { tom, 13 }; cout Compare(p1, p2) endl; return 0; }如上述代码所示在重载函数模板时必须先有通用的主模板不能直接写特化版本类模板作用建立一个通用类类中的成员数据类型可以不具体制定用一个虚拟的类型来代表。语法templatetypename T类template---声明创建模板typename---表面其后面的符号是一种数据类型可以用class代替T---通用数据类型名称可以替代通常为大写字母类模板和函数模板的区别类模板中的成员函数创建时机类模板对象做函数参数#include iostream #include string using namespace std; templatetypename T1, typename T2 class Person { public: T1 name; T2 age; Person(T1 name, T2 age) :name(name), age(age) { } void showPerson() { cout 姓名 this-name \t年龄 this-age endl; } }; //指定传入类型 void printPerson01(Personstring, int p) { p.showPerson(); } //参数模板化 templatetypename T1, typename T2 void printPerson02(PersonT1, T2 p) { p.showPerson(); cout T1的数据类型 typeid(T1).name() endl; cout T2的数据类型 typeid(T2).name() endl; } //整个类模板化 templatetypename T void printPerson03(T p) { p.showPerson(); cout T的数据类型 typeid(T).name() endl; } int main() { Personstring, intp(tom, 100); printPerson01(p); //指定传入类型 printPerson02(p); //参数模板化 printPerson03(p); //整个类模板化 return 0; }类模板和继承#include iostream #include string using namespace std; templatetypename T1, typename T2 class Person { public: T1 name; T2 age; Person(T1 name, T2 age) :name(name), age(age) { } virtual void showPerson() { cout 姓名 this-name \t年龄 this-age endl; } }; //声明子类的数据类型 class Son1 :public Personstring, int { public: Son1(string name, int age):Personstring,int (name, age){} void showPerson() { cout 姓名 this-name \t年龄 this-age endl; } }; //子类也作为类模板 templatetypename T1, typename T2 class Son2 :public PersonT1, T2 { public: Son2(T1 name, T2 age) :PersonT1, T2 (name, age) {} void showPerson() { cout 姓名 this-name \t年龄 this-age endl; } }; int main() { Son1 S1(afsa, 12); Son2string, intS2(ADFGFA, 134); S1.showPerson(); S2.showPerson(); return 0; }类外实现类模板的成员函数templatetypename T1, typename T2 PersonT1, T2::Person(T1 name,T2 age){ ..... } templatetypename T1, typename T2 void PersonT1, T2::showPerson(){ ..... }类模板分文件编写由于类模板成员函数的创建时机是在调用阶段从而导致分文件编写时链接不到针对于此问题一共有两个解决方案直接包含.cpp源文件//main.cpp #include iostream #include string #include person.cpp using namespace std; int main() { Personstring, int P(Tom, 18); P.showPerson(); system(pause); return 0; } //person.cpp #includeperson.h templatetypename T1, typename T2 PersonT1, T2::Person(T1 name, T2 age) { this-name name; this-age age; } templatetypename T1, typename T2 void PersonT1, T2::showPerson() { cout 姓名 this-name \t年龄 this-age endl; } //person.h #pragma once #includeiostream #includestring using namespace std; templatetypename T1, typename T2 class Person { public: T1 name; T2 age; Person(T1 name, T2 age); void showPerson(); };虽然这样写没有问题但是cpp为编译文件直接在主函数中引用会破坏代码的语义使得代码阅读难度增加也不太美观。将声明和实现写在同一文件中同时将其后缀名更改为.hpp.hpp文件是一个约定俗成的叫法其实.hpp文件就是.h文件之所以这样叫是为了和.h文件区分。.h文件为头文件其主要作用是包含代码的声明但是不进行实现。而.hpp文件即是同时包含代码的声明和实现。//main.h #include iostream #include string #include person.hpp using namespace std; int main() { Personstring, int P(Tom, 18); P.showPerson(); system(pause); return 0; } //person.hpp #pragma once #includeiostream #includestring using namespace std; templatetypename T1, typename T2 class Person { public: T1 name; T2 age; Person(T1 name, T2 age); void showPerson(); }; templatetypename T1, typename T2 PersonT1, T2::Person(T1 name, T2 age) { this-name name; this-age age; } templatetypename T1, typename T2 void PersonT1, T2::showPerson() { cout 姓名 this-name \t年龄 this-age endl; }