类和对象
其不和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 Fraction { public: Fraction(int num, int den = 1):num(num), denom(den){} operator double() const { return double(num) / denom; } private: int num; //分子 int denom; //分母 }; int main() { Fraction f(3, 5); double d=4+f; return 0; }在这段代码中,double d=4+f这一句代码,编译器一共有两种办法编译这句代码。
一个是operator+的运算符重载,因为它会在类中寻找operator+且输入为浮点数或者整数的函数,很明显没有。
二是由于输出的d为double类型,因此将f当作一个double类型处理,然后就在类中寻找转换函数,发现了double的转换函数,因此f=0.6,从而得出d=4.6。
因此这样可以看出这样的隐式转化有着很容易出错的二义性,目前来说,除非非常必要,一般不使用使用转换。
显示转换--explicit
一般的转换函数都是显示转换,因此一般可以在转换函数前假如一个关键字explicit,直接告诉编译器这个转换函数是显示转换。
class Fraction { public: Fraction(int num, int den = 1):num(num), denom(den){} explicit operator double() const { return double(num) / denom; } private: int num; //分子 int denom; //分母 }; int main() { Fraction f(3, 5); //double d=4+f; double d=4+double(f); return 0; }如果main函数还是和上一个一样,那么系统会直接报错,说找不到对应的重载符,因此使用显示转换更加安全,同时如果只使用显示转换也一定要在前面加入explicit,避免系统的二义性。
禁止隐式转换--explicit
这个关键字在上文已经使用过了,有了一个初步的了解,现在来具体讲述一下,其核心作用是强制要求开发者必须显式调用构造函数或转换函数。常用于单参数转换构造函数或类型转换函数。
class Fraction { public: Fraction(double num, double den = 1):num(num), denom(den){} Fraction operator+(const Fraction& f){ return (this->num / this->denom) + (f.num / f.denom); } void printFraction() { cout << (this->num / this->denom) << endl; } private: double num; //分子 double denom; //分母 }; int main() { Fraction f(3, 5); Fraction d = f + 4; d.printFraction(); return 0; }如上述代码,在Fraction d = f + 4中,编译器会去寻找Fraction类中是否有满足+的返回,随后便发现了重载了+运算符,但是由于f后面加上的是一个整数4,但是这个整数又满足Fraction构造函数,因此这个4便隐式调用了Fraction的构造函数,将num赋值为4,den为默认值,从而调用运算符重载。
但是如果在类中加入一个转换函数重载
operator double() const{ return double(num) / denum; }通过上面的讲解,我们可以知道这个也符合编译器的调用规则,因此这里便出现了二义性,从而导致报错。
为了避免这个错误,我们通常在单参数转换构造函数或类型转换函数前面加explicit以强制要求开发者必须显式调用构造函数或转换函数。
继承
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。
多态

满足条件

关键字--virtual
virtual关键字除了解决上面出现的"二义性"的问题,含有一个便是将函数变为虚函数,从而实现动态多态
静态
#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和vftable,vfptr直接指向vftable,同时这个vfptr和vftable都是新的,独立的。但vftable中储存的地址仍然是父类的地址(由于继承)。因此当我们调用这个子类的这个函数时(没有写同名函数),便是通过这个指针访问子类的vftable从而再运行的。
但是当在子类中创建同名函数时,那么程序便会重新创建一个新的代码段地址并覆盖子类之间继承的地址,从而到达一个多态的目的。
最后的(*(p->vptr)[n])(p)是通过c语言的形式解释虚函数的内部运行,通过p函数找到虚表的首地址,由于虚表就是一个函数的指针数组,通过[n]找到第n个虚函数地址,然后解引用得到虚函数本身,最后传入参数p。
实例
#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;
当有了这个纯虚函数,该类也被称为抽象类
特点

原因
从上面的案例也可以看出来,其实父类的虚函数根本用不到,主要都是调用子类重写的内容,因此给父类加入纯虚函数,可以使子类必须写该同名函数,否则也无法实例化对象
this指针和虚函数的关系--模板方法设计模式

这是虚函数多态的核心原理,是模板方法设计模式,由于CDocument类中的Serialize函数没有定义,Serialize函数变为纯虚函数,必须要求子类对其进行改写,同时CDocument类也作为抽象类无法初始化。
普通函数和虚函数之间的区别
普通函数和虚函数之间最大的区别在于内存的绑定的不同,普通函数的绑定是静态绑定,而虚函数则是动态绑定。
简单来说就是普通函数是在那个类就只能调用这个类的同名函数,不能跨类调用,除非该类作为子类且没有重写这个同名普通函数,才能调用父类的函数。
但是虚函数则不同,如果父类和子类同写了一个同名的函数,那么不管是调用子类还是父类的该函数,其都优先使用子类重定义的函数。
虚析构和纯虚析构
多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码,从而导致内存泄漏。
只要定义了虚函数,那么最好的再定义一个虚析构函数,避免内存的泄露
语法
virtual ~类名(){}--虚析构语法
virtual ~类名() = 0;
类名::类名(){}--纯虚析构函数
纯虚析构函数和纯虚函数相同,都会将类归属于抽象类而无法实例化
同时在写纯虚析构函数时,由于程序一定会调用这个这个函数,所有一定要书写这个函数的内容,只不过需要在类外定义。
模板--泛型编程
函数模板
作用
建立一个通用函数,其函数返回值类型和形参类型可以不具体制定,用一个虚拟的类型来代表。
语法
template<typename T>
函数声明或定义
template---声明创建模板
typename---表面其后面的符号是一种数据类型,可以用class代替
T---通用数据类型,名称可以替代,通常为大写字母
模板后面必须直接跟它要修饰的函数 / 类 / 结构体,中间不能插入任何代码(注释和空行除外),同时如果函数的声明和定义分开写,那么也需要重新写template头
使用
#include <iostream> #include <string> using namespace std; template<typename 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); //显示指定类型 Swap<int>(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) { } }; template<typename 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; }如上述代码所示,在重载函数模板时,必须先有通用的主模板,不能直接写特化版本
类模板
作用
建立一个通用类,类中的成员数据类型可以不具体制定,用一个虚拟的类型来代表。
语法
template<typename T>
类
template---声明创建模板
typename---表面其后面的符号是一种数据类型,可以用class代替
T---通用数据类型,名称可以替代,通常为大写字母
类模板和函数模板的区别
类模板中的成员函数创建时机

类模板对象做函数参数

#include <iostream> #include <string> using namespace std; template<typename 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(Person<string, int>& p) { p.showPerson(); } //参数模板化 template<typename T1, typename T2> void printPerson02(Person<T1, T2>& p) { p.showPerson(); cout << "T1的数据类型" << typeid(T1).name() << endl; cout << "T2的数据类型" << typeid(T2).name() << endl; } //整个类模板化 template<typename T> void printPerson03(T& p) { p.showPerson(); cout << "T的数据类型" << typeid(T).name() << endl; } int main() { Person<string, int>p("tom", 100); printPerson01(p); //指定传入类型 printPerson02(p); //参数模板化 printPerson03(p); //整个类模板化 return 0; }
类模板和继承

#include <iostream> #include <string> using namespace std; template<typename 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 Person<string, int> { public: Son1(string name, int age):Person<string,int> (name, age){} void showPerson() { cout << "姓名:" << this->name << "\t年龄" << this->age << endl; } }; //子类也作为类模板 template<typename T1, typename T2> class Son2 :public Person<T1, T2> { public: Son2(T1 name, T2 age) :Person<T1, T2> (name, age) {} void showPerson() { cout << "姓名:" << this->name << "\t年龄" << this->age << endl; } }; int main() { Son1 S1("afsa", 12); Son2<string, int>S2("ADFGFA", 134); S1.showPerson(); S2.showPerson(); return 0; }
类外实现类模板的成员函数
template<typename T1, typename T2> Person<T1, T2>::Person(T1 name,T2 age){ ..... } template<typename T1, typename T2> void Person<T1, T2>::showPerson(){ ..... }
类模板分文件编写
由于类模板成员函数的创建时机是在调用阶段,从而导致分文件编写时链接不到,针对于此问题一共有两个解决方案
直接包含.cpp源文件
//main.cpp #include <iostream> #include <string> #include "person.cpp" using namespace std; int main() { Person<string, int> P("Tom", 18); P.showPerson(); system("pause"); return 0; } //person.cpp #include"person.h" template<typename T1, typename T2> Person<T1, T2>::Person(T1 name, T2 age) { this->name = name; this->age = age; } template<typename T1, typename T2> void Person<T1, T2>::showPerson() { cout << "姓名:" << this->name << "\t年龄" << this->age << endl; } //person.h #pragma once #include<iostream> #include<string> using namespace std; template<typename 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() { Person<string, int> P("Tom", 18); P.showPerson(); system("pause"); return 0; } //person.hpp #pragma once #include<iostream> #include<string> using namespace std; template<typename T1, typename T2> class Person { public: T1 name; T2 age; Person(T1 name, T2 age); void showPerson(); }; template<typename T1, typename T2> Person<T1, T2>::Person(T1 name, T2 age) { this->name = name; this->age = age; } template<typename T1, typename T2> void Person<T1, T2>::showPerson() { cout << "姓名:" << this->name << "\t年龄" << this->age << endl; }
成员模板
成员模板最为典型的例子便是pair对组
template<typename T1, typename T2> struct pair { typedef T1 first_type; typedef T2 second_type; T1 first; T2 second; pair():first(T1()), second(T2()){} pair(const T1& a, const T2& b):first(a), second(b){} template<typename U1, typename U2> pair(const pair<U1, U2>& p):first(p.first), second(p.second){} }; class Base1{}; class Derived1:public Base1{}; class Base2{}; class Derived2:public Base2{}; int main(){ //pair<Base1, Base2> p2(pair<Derived1, Derived2> p); pair<Derived1, Derived2> p; pair<Base1, Base2> p2(p); return 0; }这个函数相当于是将Derived1赋值给p2这个对组的第一个元素,该元素的数据类型为Base1类,将Derived2赋值给p2这个对组的第二个元素,该元素的数据类型为Base2类
模板模板参数
模板模板参数与普通模板参数的区别在于其传入的是模板本身(vector,list,deque等)。
template<typename T, template <typename U> typename Container> class MyContainer { private: Container<T> data; // 用传入的模板Container,结合T,实例化出完整类型 public: void push(const T& val) { data.push_back(val); } }; // 第二个参数传入的是vector模板本身,不是vector<int> // 但是其在补全MyContainer类中的成员函数后,可以当作vector<int>来用 MyContainer<int, std::vector> obj;其实这样也有一个问题,那就是对于补全函数差异性,例如vector和list两个容器,由于list是链表,其可以在任意地方插入,但是vector只能从底部开始插入,这就导致了利用模板模板参数来传入不同的容器时,需要考虑它的兼容性问题。
一般的解决方法有四个:
1、明确接口约束文档,即写一个说明文档
2、使用Concepts关键字,这是c++20才引入的关键字,专用于解决这个问题--在编译期强制约束模板参数必须满足的接口要求,如果不满足,编译器会直接报清晰、易懂的错误,而不是一堆晦涩的模板错误。
3、模板的特化或偏特化,类似于继承,写出一个通用的模板,正对于某些特定的模板写出其偏特化模板
template<typename T, template<typename U> class Container> class MyContainer { private: Container<T> data; public: void push(const T& val) { data.push_back(val); } }; // 偏特化 template<typename T> class MyContainer<T, std::list> { // 只有当传入模板容器为list才调用 private: std::list<T> data; public: void push(const T& val) { data.push_front(val); } };4、定义一套自己的统一接口,然后在内部转发到底层容器的对应方法,完全屏蔽底层容器的接口差异。这又是STL库的做法。
变参关键字--...
变参函数
用以表示函数接受任意数量、任意类型的参数
int printf(const char* format,...);但是其需要使用
<cstdarg>库的宏操作参数。同时其数据类型也不安全,一般不适用。
变参模板
表示模板或函数接受任意数量、任意类型的参数。是c++为了替代上面的变参函数所推出的语法。
#include <iostream> // 递归终止函数:当参数包为空时调用 void func() { } // 递归主函数:每次处理1个参数,剩余参数继续递归 template<typename T, typename... Args> void func(const T& first,const Args&... rest) { std::cout << "当前参数: " << first << std::endl; // 处理第一个参数 func(rest...); // 递归处理剩余参数包 } int main() { func(1, 3.14, "hello", 'a'); return 0; }通过上面的代码可以看出其实定义了两个func函数,其具体的运行规律如下:
当传入1,3.14,"hello",'a'时,其为有参传入,因此调用void func(const T& first,const Args&... rest)函数,将1传给first,剩下的传给rest,称为包,然后打印输出first,同时将包传入func。
由于包中仍有元素,仍是有参传入,再次调用void func(const T& first,const Args&... rest)函数。
直到包还剩一个'a'元素,将'a'传入first,空传入rest作为包,打印输出'a'后,由于包为空,因此其不符合void func(const T& first,const Args&... rest)函数的传入,但是符合void func()的传入,因此调用void func(),同时由于包为空,无法继续递归调用,因此结束循环。
可以使用sizeof...(rest)来显示包中元素的个数
折叠表达式
#include <iostream> template<typename... Args> auto sum(Args... args) { // 一元右折叠:((arg1 + arg2) + arg3) +... return (... + args); } int main() { std::cout << sum(1, 2, 3, 4) << std::endl; // 输出: 10 std::cout << sum(1.5, 2.5, 3.0) << std::endl; // 输出: 7.0 return 0; }













