声明:其中所有内容是自己在学习过程中所作的笔记,部分内容是来源于网络资源和c++ primer,也是自己辛苦一字一字打出来的,其它内容在之后进行补充。(使用的是obsidian编辑器,是一款开源的markdown编辑器,感兴趣可以了解下载使用)。
c++类基础相关
拷贝构造函数
- 如果一个类的第一个参数是自身的引用且额外参数都有其默认值,则为拷贝构造函数
- 拷贝构造的第一个参数必须为引用类型
- 拷贝构造函数在几种情况下都会被隐式地使用。因此,拷贝构造函数通常不应该是explicit的
- 与默认的构造函数不同,即使定义了其它构造函数,编译器依然会合成一个拷贝构造函数,一般情况下合成的拷贝构造函数会将其参数的成员逐个拷贝到正在创建的对象中(非static)
拷贝初始化
- 直接初始化:要求编译器使用普通的函数匹配来选择与我们提供的参数最匹配的构造函数
- 拷贝初始化:要求编译器将右侧的运算对象拷贝到正在创建的对象中,如果需要的话还会进行类型转换。拷贝初始化通常使用拷贝构造函数完成,如果一个类有一个移动构造函数,则拷贝初始化有时会使用移动构造函数而非拷贝构造函数来完成。
- 拷贝初始化发生时机
- 将一个对象作为实参传递给一个非引用类型的形参
- 从一个返回类型为非引用类型的函数返回一个对象
- 用花括号列表初始化一个数组中的元素或一个聚合类中的成员
operator
加法运算符重载 #运算符重载
- 运算符重载也可以函数重载
// 本质调用
Base p3 = operator+(p1, p2);
// 全局函数
Base operator+(const Base &p1, const Base &p2)
{
Base temp;
temp.m_A = p1.m_A + p2.m_A;
temp.m_B = p1.m_B + p2.m_B;
return temp;
}
Base operator+(const Base &p1,int value)
{
Base temp;
temp.m_A = p1.m_A + value;
temp.m_B = p1.m_B + value;
return temp;
}
- 内置数据类型不可以发生重载
运算符重载
左移运算符重载
- 一般不会使用成员函数实现 operator<< 无法实现 cout 在左侧
// Global function
ostream& operator<<(ostream &os, const Person &p);
class Person
{
public:
Person() : m_A(10), m_B(10) { }
public:
int m_A;
int m_B;
};
ostream& operator<<(ostream &os, const Person &p)
{
os << p.m_A << " " << p.m_B;
return os;
}
++运算符重载
- 返回引用,如果不返回引用会重新拷贝一份临时数据
// 返回引用 于上下仿此
MyInterger& operator++();
// 后置递增
// int 占位符, 可以用来区分前置和后置重载运算符 不可以用double fload long 等;
MyInterger operator++(int); // 不可以返回局部变量的引用
MyInterger operator++(int)
{
MyInterger temp = *this;
m_Num++;
return temp;
}
// 递减 仿此
重载赋值运算符
如果一个类第一个参数是自身类型的引用且任何额外的参数都有默认值,则为拷贝构造函数
// 重载赋值运算符,先判断当前对象是否有数据在堆区,不为空 delete;
// 返回自身以链式调用
Person& operator=(const Person &p);
class Person
{
public:
Person(int age) : m_Age(new int(age)) { }
~Person()
{
if (m_Age != nullptr)
delete m_Age;
}
Person& operator=(const Person &p)
{
if (m_Age != nullptr)
delete m_Age;
m_Age = new int(*p.m_Age);
return *this;
}
int *m_Age;
};
关系运算符重载
// 同上
bool operator==(const Person &p);
bool operator!=(const Person &p);
class Person
{
public:
Person(string name, int age) :
m_Name(name), m_Age(age)
{
}
bool operator==(const Person &p)
{
if (m_Name == p.m_Name && m_Age == p.m_Age)
return true;
return false;
}
bool operator!=(const Person &p)
{
if (m_Name != p.m_Name || m_Age != p.m_Age)
return true;
return false;
}
string m_Name;
int m_Age;
};
仿函数 #仿函数
void operator()();
// 可通过匿名对象调用 匿名函数对象
cout << MyAdd()(100, 100) << endl;
继承
- 减少代码重复量 #继承
class B : public A
/* public private protected
/ 不管哪一种继承方式都不可以访问父类的 private
/ public 继承 保留父类的访问权限
/ private protected 除父类private不变外,均变
/ protected 类内可以访问类外不可,private 类内类外都不可
*/
- 子类依然会继承父类的 private 成员
- 编译器隐藏了父类的 private 成员,不可以访问
- 先构造子类再构造父类,析构反之
- 同名成员处理:加上作用域
Son.Base::func();
Son.Base::m_A;
// 如果子类出现父类的同名成员函数,则会隐藏掉父类的所有同名成员函数(包括重载),可以加作用域解决此问题
// 静态成员变量和静态成员函数 仿此
// static
Son::Base::func();
Son::Base::m_A;
- 静态成员 类内声明类外初始化,编译阶段分配内存,对象共享同一份数据
- 静态成员函数,只能访问静态成员变量,对象共享同一份函数
- 多继承情况下,出现同名的父类成员,加作用域以解决
- 菱形继承问题:可以加 virtual 关键字解决,使用后,多继承只会存在一份数据(使用后为虚继承)
- vbptr 虚基类指针,指向 vbtable 虚基类表
多态
- 多态分为两类:静态多态和动态多态 #多态
- 静态多态:函数重载和运算符重载数据静态多态,复用函数名
- 动态多态:派生类和虚函数实现运行时多态
- 区别:
- 静态多态的函数地址早绑定:编译阶段确定函数地址 #静态多态
- 动态多态的函数地址晚绑定:运行阶段确定函数地址 #动态多态
- 动态多态满足条件:1. 有继承条件 2. 子类重写父类的虚函数
- 动态多态的使用:父类的指针或引用指向子类对象
- vfptr:虚函数指针; vftable:虚函数表 #vfptr #vftable
- 虚函数表内记录的是虚函数的地址(虚函数表在常量区)
- 当子类重写父类的虚函数时,子类中的虚函数表会替换成子类的虚函数地址
- 当父类的指针或者引用指向子类对象时,发生多态
- GDB 中可以使用 info vtbl object 查看虚函数表
- 多态的优点:代码组织结构清晰,可读性强,利于前期和后期的维护和扩展
纯虚函数和抽象类
- 纯虚函数:virtual 返回值类型 函数名(参数列表) = 0; #纯虚函数和抽象类
- 当有纯虚函数存在时,这个类为抽象类
- 特点:
- 无法实例化对象
- 子类必须重写抽象类中的纯虚函数,否则也属于抽象类
- 特点:
虚析构和纯虚析构
- 多态使用时,如果子类有数据开辟到堆区,那么父类的指针在释放时无法调用到子类的析构代码(如果子类堆区有数据会导致内存泄漏的情况)
- 解决办法:将父类中的析构函数改为虚析构或纯虚析构
- 如果是纯虚析构 则该类属于抽象类,无法实例化对象
- 不管是纯虚析构还是虚析构都需要代码实现,因为父类如果也有数据开辟在堆区也是需要释放的。在父类中纯虚析构为声明,需要类外实现,否则编译器会报无法解析的外部命令错误
virtual ~Animal() = 0;
};
Animal::~Animal() {
}
- 可以通过在一个类中维护一个或多个父类成员属性,在类中传入子类对象,以实现多态。
文件操作
- 头文件 fstream #文件操作
- 文件类型分两种:文本文件和二进制文件
- ofstream :写操作
- ifstream : 读操作
- fstream: 读写操作
模版
- 分为函数模版和类模版 #函数模版和类模版
template<typename T> void swap_T(T &val1, T &val2);
swap_T(val1, val2); // 自动类型推导
swap_T<int> (val1, val2); // 显示指定类型
- 自动类型推导,必须推导出一致的数据类型T, 才可以使用
- 模版必须要确定出 T 的数据类型,才可以使用
普通函数与函数模版的区别
- 普通函数调用时可以发生自动类型转换(隐式类型转换)
- 函数模板调用时,如果利用自动类型推导,则不会发生类型转换
- 如果利用显示指定类型的方式,可以发生类型转换
普通函数与函数模版的调用规则
- 如果函数模版和普通函数都可以调用,优先调用普通函数
- 可以通过空模板列表来强制调用模板函数
- 函数模版也可以发生重载
- 如果函数模版可以产生更好的匹配,优先调用函数模版
自定义数据类型模版推导
// 加上template关键字和空的模版列表
template<> bool compare(Person &p1, Person &p2);
类模版和函数模版的区别
- 类模版不能使用自动类型推导
- 类模版的模板参数列表中可以有默认参数
template <class NameT, class AgeT>
class Person;
template <class NameT = string, class AgeT = int> // 在使用默认参数时可以直接调用不用 <>
class Person;
Person p("wuyan", 10000); // 在使用默认参数时可以直接调用不用 <>
Person<> p("wuyan", 10000); // 也可以
Person<string> p("wuyan", 10000); // 也可以
类模版中成员函数创建时机
- 普通类中的成员函数一开始就创建
- 类模版中的成员函数调用时才创建
类模版对象做函数参数
- 指定传入的类型:直接显示对象的数据模型
- 模版参数化: 将对象中的参数变为模版进行传递
- 整个类模板化: 将整个对象模型模板化进行传递
//指定传入的类型
void show_person1(Person<string, int> &p);
//模板参数化
template <class T1, class T2>
void show_person2(Person<T1, T2> &p);
//整个类模版化
template <class T>
void show_person3(T &p);
类模版与继承
- 当子类继承的父类是一个模版时,子类在声明的时候,要指定出父类中T的类型
- 如果不指定,编译器无法给子类分配内存
- 如果需要灵活指出父类中T的类型,子类也需要变为类模版
template<class T>
class Base;
// 子类声明父类中T的类型
class Son1 :public Base<int>
// 子类模板化
template <class T1, class T2>
class Son2 :public Base<T1>
{
T2 m;
};
类模版成员函数类外实现
template <class T1, class T2>
class Person
{
public:
Person(T1, T2);
void show_Person() cosnt;
T1 m_Name;
T2 m_Age;
};
// 构造函数类外实现
template <class T1, class T2>
Person<T1, T2>::Person(T1 name, T2 age);
// 成员函数类外实现
template <class T1, class T2>
void Person<T1, T2>::show_Person() const;
类模版分文件编写
- 可以直接包含 .cpp 源文件
- 将声明和实现写在用一个文件中,后缀可以为 hpp
类模版成员函数的创建时机是在调用阶段,可以用上两种方法解决,无法解析的外部命令问题
类模版与友元
- 全局函数类内实现:直接在类内友元即可;
- 全局函数类外实现:需要让编译器知道全局函数的存在
// 声明类
template <class T1, class T2>
class Person;
// 类外实现
template <class T1, class T2>
void show_Person2(const Person<T1, T2> &p)
{
cout << "out of class name: " << p.m_Name << "\n" << "age: " << p.m_Age << endl;
}
template <class T1, class T2>
class Person
{
// 全局函数类内实现
friend void show_Person1(const Person<T1, T2> &p)
{
cout << "name: " << p.m_Name << "\n" << "age: " << p.m_Age << endl;
}
// 全局函数类外实现 函数名后需要加空模版列表,并且需要在类上方实现,让编译器提前看到 实现
friend void show_Person2<>(const Person<T1, T2> &p);
public:
Person(string name, int age) :
m_Name(name), m_Age(age)
{}
T1 m_Name;
T2 m_Age;
};