仅供个人复习,

C语言IO占位符表

%d十进制整数(int)
%ldlong
%lldlong long
%uunsigned int
%o八进制整型
%x十六进制整数/字符串地址
%c单个字符
%s字符串
%ffloat,默认保留6位
%lfdouble
%e科学计数法
%g根据大小自动选取f或e格式,去掉无效0

转义符表

转义符可以取消关键字符的特殊性,下面是常见的转义符使搭配

	printf("\b");//退格符
	printf("\n");//换行
	printf("\a");//响铃,电脑真的响一下,不可思议
	printf("\t");//水平制表符
	printf("\v");//垂直制表符
	printf("\130");//输出char对应130的字符
	printf("%% %d",12);//%的转义使用%,而不是\

随机数生成

srand((unsigned int)time(NULL));

int ret1 = rand() % 10 + 1;//生成1~10的随机数
int ret2 = rand() % 100 + 1;//生成1~100的随机数
int ret3 = rand() % 34 + 66;//生成66~99的随机数
int ret4 = rand() % (n - m + 1) + m;//生成m~n的随机数

//规律是第一个数+第二个-1,是上界
//第二个数是下界

C/C++ 语言数据类型大小

ANSI/ISO规范

sizeof(short int)<=sizeof(int)
sizeof(int)<=sizeof(long int) short
int至少应为16位(2字节)
long int至少应为32位。

16位编译器

数据类型字节数
char1
short2
int2
long4
float4
double8
bool1
指针2

32位编译器

数据类型字节数
char1
short2
int4
long4
long long8
float4
double8
bool1
long double12
指针4

64位编译器

数据类型字节数
char1
short2
int4
long8
long long8
float4
double8
bool1
long double16
指针8

struc和class的区别

1.struct和class都能被继承
2.struct和成员和继承默认都是public的,而class能够自己选择
3.在C++中struct可以定义函数,而C不能

内存分布

1.堆区(Heap):由开发者自己管理,主要通过new, delete, malloc, free进行分配和释放,空间往往比较大,但有可能会存在内存泄漏和内存碎片的情况
2.栈区(Stack):由编译器管理分配和释放,用于存放函数参数,局部变量,等等
3.只读数据段(.rodata - ReadOnly Data Segment):用于储存常量值,例如整数,字符串等字面量,一般不允许修改
4.代码段(Text Segment)): 存储程序执行所用的二进制指令
5.全局/静态区:未初始化部分称为BSS Segment,不会占用可执行文件的空间,但是在程序初始化后会为这里的变量分配内存。初始化部分(数据段 -.data - Data Segment)会直接把初始值写入可执行文件,在程序初始化后分配内存并赋值。

new和malloc的区别

4、new和malloc的区别
(1)new和delete是C++关键字,而malloc和free是C语言库函数。都用于申请动态内存和释放内存。new的实现也是基于malloc的
(2)new在调用时先分配内存,再调用构造函数,delete在调用时先使用析构函数,再释放内存,而malloc只分配内存,free只释放内存。
(3)new申请内存不需要计算申请内存的大小,且不需要强制类型转换,其返回的类型就是对应申请类型的指针,而malloc申请时,需要手动计算申请内存的大小,并且还需要强制类型转换为需要的类型,因为malloc返回的类型为 void*。
(5)new申请内存失败,返回NULL,而malloc申请内存失败,则返回异常。

为什么有了malloc,free还需要new和delete:因为malloc和free是C的库函数,不在编译器的控制范围内,所以C++把new和delete变成关键字。而且malloc和free不能满足OOP对构造和析构安全性的需要。

函数参数传递的方式

1.值传递:形参是实参的拷贝,形参的改变并不影响实参的值
2.引用传递:形参接收实参的地址,使得对形参所指对象进行操作等价于对实参的操作
3.指针传递:形参接收实参的指针,使得函数能够通过形参映射到实参进行修改和读取

指针和引用的区别

(1)指针是一个实体,而引用只是一个别名。
(2)指针指向的是一块内存地址,所指向的内容是内存的地址,但指针所指向的值是可以改变的,允许拷贝和赋值,可以为空,sizeof指针得到的也是指针类型的大小。

(3)引用是一块内存的别名,引用必须在定义时绑定到一块内存上,即引用必须初始化,后续不可更改,也不能为空,且没有const和非const的区别。sizeof引用得到的是所初始化对象的大小。

(4)指针必须在解引用后才能对对象进行操作,而引用可以直接对对象进行操作。

指针和数组的区别

1.指针指向的是一个内存地址,代表了该类型所对应的固定大小的一块内存空间,而数组指向的虽然是首元素地址,但是数组代表的是一个内存连续的数据集合,也就是n个指针代表的内存空间。
2.数组指向首元素地址,不能直接通过赋值来进行拷贝,而是需要逐个拷贝

函数指针的含义

编译后,每个函数都会有一个入口地址,函数指针指向函数的入口地址

空指针,野指针,悬挂指针

空指针:指向NULL或者nullptr或0的指针
野指针:指向未知或无效位置的指针,例如没有初始化,越界,释放后继续使用。
悬挂指针:指向了之前有效但是现在无效的地址,没有被置空,原地址仍然悬挂。

对这三种指针的访问均会产生各种异常和问题。

如何避免:
1.保持良好的习惯:例如在指针使用完毕后即时的置空,先对指针进行检查后再使用。
2.使用智能指针或其他内存管理策略:例如unique_ptr,shared_ptr等,这些指针能够通过引用计数自动确定释放时机,或者在C++中实现GC进行内存管理(例如UE中的GC和C# GC)

指针常量和常量指针

const int* p = &a;   //常量指针,指向可以改,但指向的值不可以改。
int* const p = &a;   //指针常量,指向不可以改,但指向的值可以改
const int* const p = &a; //常量指针常量 ,指向和指向的值都不能改

怎么记,先出现const就是常量,再出现int*就是指针

const在前是const int*,代表指针指向了一个常量。
const在后是分别修饰p,代表这个指针变量本身是常量。

宏函数和内联函数的区别

宏函数(Macro Functions):

  1. 替换规则:在编译预处理阶段的简单文本替换

  2. 参数展开:没有参数类型检查

  3. 适用场景:简单的、短小的代码片段,例如进行简单的数学计算、位操作等。

内联函数(Inline Functions):

  1. 替换规则:编译阶段处理。编译器会尝试将函数调用处直接替换为函数体。

  2. 参数类型检查:参数和返回值检查与正常函数无异

  3. 适用场景:内联函数适用于较短小的函数,包含一些简单的代码逻辑,且频繁调用的情况。

  4. 推荐用法:在类里定义的函数都是默认加上inline关键字的(是不是由编译器决定),或者在头文件中避免函数重定义问题。inline关键字只能建议编译器进行内联,是否采纳取决于编译器

内联函数可以优化函数进入和离开的开销,但内联可能会导致编译后的代码体积增大,宏函数的使用更需要小心,因为它在文本替换阶段可能会引发一些意想不到的问题。

差异案例

#include <iostream>

#define SQUARE_MACRO(x) x * x

inline int square_inline(int x) {
    return x * x;
}

int main() {
    int num = 5;
    // result = (++num) * (++num)   直接把参数替换进函数
    int result_macro = SQUARE_MACRO(++num); // 使用宏函数
    num = 5;
    //先自增再传入
    int result_inline = square_inline(++num); // 使用内联函数
	
    std::cout << "Number: " << num << std::endl;
    std::cout << "Result (Macro): " << result_macro << std::endl;
    std::cout << "Result (Inline): " << result_inline << std::endl;

    return 0;
}

在这里插入图片描述

宏和const的区别

(1)类型和安全检查不同:宏定义只是字符的替换,没有数据类型的区别和类型安全检查,而const是常量的声明,有数据类型的区别,同时在编译阶段也会有类型的检查。

(2)编译器处理方式的不同:编译器在预处理阶段就把宏给展开,不能对宏进行调试;而编译器在编译阶段处理const变量,类似于一个只读数据。

(3)存储方式不同:宏定义是直接替换,没有内存分配,而const常量有内存分配

(4)作用域不同:宏可以在所有包含的文件里使用,而const允许局部定义,只能在作用域内使用

C++的编译过程

1.预处理:编译器展开宏和内联,展开条件编译
2.编译:编译器将预处理后的代码,编译为汇编代码,通常为.o和.obj格式
3.汇编:编译器将编译后的汇编代码转换为二进制机器码,输出到.o和.obj中
4.链接:编译器将以上生成的编译单元与库函数,必要信息结合生成可执行文件.

static相关的

**1.修饰类的成员函数:**该函数属于这个类,而不属于类的任意实例,只能访问静态成员。无需实例即可调用。
**2.修饰类的成员变量:**这个变量属于类,不属于实例,在所有实例之间共享,无需实例即可使用。
**3.修饰局部变量:**将局部变量的存储从栈区转移到静态区,例如在函数体内定义静态变量,在下次访问这个函数时,如果没有在别的地方进行修改,该变量的值与上次离开相同。
4.修饰全局变量:
首先明确概念,全局变量int x = 10默认就是extern int x = 10
哪怕其他文件不include,也能通过在本文件extern一个相同的变量来访问它。
但是声明了static全局变量后,必须进行include才能访问,禁止了extern的使用
在头文件中声明一个static变量后,在包含了该头文件的cpp文件修改了该变量值,对于其他也包含了该头文件的cpp文件并不影响.
**5.修饰函数:**会使得该函数只可以在其他文件实现,但是不能在其他文件调用

C++中的重载和重写、重定义

(1)重载:同一个作用域内,声明多个参数列表不同的同名函数(不包含返回值类型),C++会把函数的参数类型加到函数签名中,从而使得在编写时函数名一样,但在编译后函数名不一样,而C没有函数重载。

(2)重写:派生类中重新定义父类中函数签名相同的虚函数,重写的一定是虚函数,在子类中重写函数,其访问权限可以重新被定义。(动态多态)

(3)重定义:在派生类中,重新定义和父类函数名相同的非虚函数,其参数列表和返回值都可以不同。则父类中的同名函数被子类所隐藏,如果想要调用父类中的同名函数,则需要加上父类的作用域。

编译器为类生成的构造和析构函数

1.如果没有定义任何构造和析构,编译器生成公开的空构造和析构,以及拷贝构造函数
2.如果定义了拷贝构造函数,则仅生成析构
3.如果仅定义了普通构造函数,则生成拷贝构造和析构
4.如果仅定义了析构,则生成拷贝构造和默认构造

include时<>和""的区别

1.使用<>,编译器会从系统文件目录下,系统配置的库环境中去寻找对应的头文件,一般用于引用标准库的头文件。
2.使用"",编译器会先在当前目录下去寻找对应的文件,找不到的时候再去库环境中去寻找。一般用于引用个人编写的头文件

另外可以在IDE中配置inlcude内容,也是一个备选的查找项

面向对象的三(四)大特性

1.封装
2.继承
3.多态 : 对不同对象发送相同消息产生不同效果
静态多态的实现:运算符重载和函数重载
动态多态的实现:子类重写父类函数
(4.抽象)

STL标准库

通用的算法API

条件排序

#include <algorithm>

// 使用 lambda 表达式按照姓名升序排序
    std::sort(v.begin(), v.end(), 
    [](const Person& a, const Person& b) 
    {
        return a.name < b.name;
    });
    
//运算符重载
struct Person {
    int age;
    Person(int a) : age(a) {}

    // 重载 < 运算符,按照年龄升序排序
    bool operator<(const Person& other) const {
        return age < other.age;
    }
};

sort(v.begin(),v.end());

移除符合条件的元素

#include <algorithm>
//返回的是移除后的逻辑结尾的迭代器,将符合条件的置于尾端
auto newEnd = std::remove_if(v.begin(), v.end(), [](T item) 
{
        return ...
});
//真正的移除
numbers.erase(newEnd, numbers.end()); // 删除废弃的元素

vector

优点:

  • 内存是连续分配的,访问元素的速度较快。
  • 在末尾插入和删除元素的时间复杂度为常数。

缺点:

  • 在中间插入或删除元素的时间复杂度较高,需要移动后续元素。
  • 在内存不足时,可能会导致重新分配内存和复制元素。
#include <vector>

std::vector<int> v;

v.push_back(10); // 在末尾插入元素
v.pop_back();    // 删除末尾的元素
v.size();        // 返回容器中的元素数量
v.empty();       // 检查容器是否为空
v.clear();       // 清空容器中的所有元素
v.at(index);     // 访问指定索引的元素
v.begin();       // 返回指向容器第一个元素的迭代器
v.end();         // 返回指向容器最后一个元素之后的迭代器

list

优点:

  • 支持在任意位置快速插入和删除元素。
  • 在中间插入和删除元素的时间复杂度为常数。

缺点:

  • 元素在内存中不连续存储,访问元素的速度慢。
  • 占用更多内存,每个节点需要存储额外的指针。
#include <list>

std::list<int> lst;

lst.push_back(10); // 在末尾插入元素
lst.push_front(20); // 在开头插入元素
lst.pop_back();     // 删除末尾的元素
lst.pop_front();    // 删除开头的元素
lst.size();         // 返回容器中的元素数量
lst.empty();        // 检查容器是否为空
lst.clear();        // 清空容器中的所有元素
lst.front();        // 访问首元素
lst.back();         // 访问末尾元素
lst.begin();        // 返回指向容器第一个元素的迭代器
lst.end();          // 返回指向容器最后一个元素之后的迭代器

forawrd_list

与list类似,仅支持单向访问,效率更佳一些

#include <forward_list>
std::forward_list<T> fl;

fl.push_front(const value_type& value); // 在头部插入元素
fl.pop_front(); // 从头部删除元素
fl.insert_after(pos, const value_type& value); // 在指定位置后插入元素
fl.erase_after(pos); // 在指定位置后删除元素
fl.front(); // 访问第一个元素
fl.begin(); // 返回指向第一个元素的迭代器
fl.end(); // 返回指向最后一个元素之后的迭代器


deque

优点:

  • 支持在两端快速插入和删除元素。
  • 内存是分块分配的,访问元素的速度较快。

缺点:

  • 难以在中间插入或删除元素
  • 存储多个分块,占用较多内存
#include <deque>

std::deque<int> dq;

dq.push_back(10); // 在末尾插入元素
dq.push_front(20); // 在开头插入元素
dq.pop_back();     // 删除末尾的元素
dq.pop_front();    // 删除开头的元素
dq.size();         // 返回容器中的元素数量
dq.empty();        // 检查容器是否为空
dq.clear();        // 清空容器中的所有元素
dq.front();        // 访问首元素
dq.back();         // 访问末尾元素
dq.begin();        // 返回指向容器第一个元素的迭代器
dq.end();          // 返回指向容器最后一个元素之后的迭代器

map

优点:

  • 存储键值对,支持按键进行高效的查找和插入。
  • 根据键的顺序遍历元素。

缺点:

  • 内存使用较多,每个键值对都需要额外的内存存储键。
  • 没有连续存储,访问元素的速度相对较慢。
#include <map>

std::map<std::string, int> m;

m["one"] = 1; // 插入键值对
m["two"] = 2;
m.find(const key_type& k); // 查找键的位置
m.count(const key_type& k); // 计算具有特定键的元素数量
m.size(); // 返回容器中的键值对数量
m.empty(); // 检查容器是否为空
m.clear(); // 清空容器中的所有键值对
m.begin(); // 返回指向容器第一个键值对的迭代器
m.end(); // 返回指向容器最后一个键值对之后的迭代器

set

优点:

  • 存储唯一的元素,支持按值进行高效的查找和插入。

缺点:

  • 内存使用较多,每个元素都需要额外的内存存储。
  • 不连续存储,访问元素的速度相对较慢。
#include <set>

std::set<int> s;

s.insert(const value_type& val); // 插入元素
s.find(const key_type& k); // 查找元素
s.size(); // 返回容器中的元素数量
s.empty(); // 检查容器是否为空
s.clear(); // 清空容器中的所有元素
s.begin(); // 返回指向容器第一个元素的迭代器
s.end(); // 返回指向容器最后一个元素之后的迭代器

unordered_map (C++11)

优点:

  • 使用哈希表实现,支持快速的查找和插入操作,平均时间复杂度为常数。
  • 对于大数据集,查找效率高于std::map。

缺点:

  • 内存占用较高,需要存储哈希表和键值对。
  • 不保证元素的顺序。
#include <unordered_map>

std::unordered_map<std::string, int> um;

um["one"] = 1; // 插入键值对
um["two"] = 2;
um.find(const key_type& k); // 查找键的位置
um.count(const key_type& k); // 计算具有特定键的元素数量
um.size(); // 返回容器中的键值对数量
um.empty(); // 检查容器是否为空
um.clear(); // 清空容器中的所有键值对
um.begin(); // 返回指向容器第一个键值对的迭代器
um.end(); // 返回指向容器最后一个键值对之后的迭代器

unordered_set (C++11)

优点:

  • 使用哈希表进行实现,支持快速的查找和插入操作,平均时间复杂度为常数。
  • 对于大数据集,查找效率高于std::set。

缺点:

  • 内存占用较高,因为需要存储哈希表和元素。
  • 不保证元素的顺序。
#include <unordered_set>

std::unordered_set<int> us;

us.insert(const value_type& val); // 插入元素
us.find(const key_type& k); // 查找元素
us.size(); // 返回容器中的元素数量
us.empty(); // 检查容器是否为空
us.clear(); // 清空容器中的所有元素
us.begin(); // 返回指向容器第一个元素的迭代器
us.end(); // 返回指向容器最后一个元素之后的迭代器

stack

#include <stack>

std::stack<T> s;

s.push(const value_type& value); // 将元素压入堆栈顶部
s.pop(); // 弹出堆栈顶部的元素
s.top(); // 访问堆栈顶部的元素
s.empty(); // 检查堆栈是否为空
s.size(); // 返回堆栈中元素的数量

queue

#include <queue>

std::queue<T> q;

q.push(const value_type& value); // 将元素推入队列尾部
q.pop(); // 从队列头部弹出元素
q.front(); // 访问队列头部元素
q.back(); // 访问队列尾部元素
q.empty(); // 检查队列是否为空
q.size(); // 返回队列中元素的数量

priority_queue

#include <queue>

std::priority_queue<T> pq;

pq.push(const value_type& value); // 将元素推入优先队列
pq.pop(); // 从优先队列中弹出元素
pq.top(); // 访问优先队列中优先级最高的元素
pq.empty(); // 检查优先队列是否为空
pq.size(); // 返回优先队列中元素的数量

#include <iostream>
#include <queue>
#include <vector>

struct MyStruct {
    int value;
    // 比较操作符,根据 value 来比较 ,越大优先级越高
    bool operator<(const MyStruct& other) const {
        return value < other.value;
    }
};

int main() {
    std::priority_queue<MyStruct> pq;

    pq.push({5});
    pq.push({2});
    pq.push({8});
    pq.push({1});

    // 遍历优先队列按优先级输出
    while (!pq.empty()) {
        std::cout << pq.top().value << " ";
        pq.pop();
    }

    return 0;
}
// 8 5 2 1

智能指针

  • std::shared_ptr:允许多个智能指针共享同一个对象,通过引用计数来管理对象的生命周期。当最后一个引用被释放时,对象会被销毁。
auto sp = std::make_shared<int>(); // 分配堆空间,创建智能指针
auto sp2 = sp; // 创建另一个智能指针 
  • std::unique_ptr:用于独占地拥有一个对象,不能被多个智能指针共享。它提供了更轻量级的智能指针,适用于不需要共享所有权的情况。

  • std::weak_ptr:用于解决std::shared_ptr的循环引用问题。它可以与std::shared_ptr一起使用,但不会增加对象的引用计数。

构造函数执行顺序

  • 先成员的构造,再当前类型的构造
  • 父类构造优先于子类构造
  • 成员初始化按书写顺序,低于构造顺序
  • 虚基类只构造一次,非虚构造两次

例题:问输出结果是多少

#include <iostream>

class A {
public:
    A() {
        std::cout << "A Constructor" << std::endl;
    }
};

class B : public A {
public:
    B() {
        std::cout << "B Constructor" << std::endl;
    }
};

class C : public A {
public:
    C() {
        std::cout << "C Constructor" << std::endl;
    }
};

class D : public B, public C {
public:
    D() {
        std::cout << "D Constructor" << std::endl;
    }
};

int main() {
    D d;
    return 0;
}

在这里插入图片描述

析构函数: 子类>子类成员>父类>父类成员
在这里插入图片描述
一个冷知识:一个类的构造函数和析构函数都是public才能被外界实例化
如果是protected只能在派生类实例化和析构

也就是说得在当前位置能对该类析构和构造才能被实例化(掂量一下访问权限在这里是否是同时有)

继承的理解

C++中
如果A派生于B,那么在创建A的实例时,会形成如下的结构,也就是说A持有一个B的实例
在这里插入图片描述

class B
{
	继承方式(public,protected,private) A parent;
	
};

可能这样看起来很奇怪,但是实际上却是编译器为我们建立了一个特殊的关系,让我们能直接让B能拥有A的一切,而不是B.A.XXX,相当于一种缩略的调用方式。基于此我们能产生更加深入的理解,B间接持有了A的内存。

这样,我们创建一个B的实例,可以得知,构造和析构,初始化顺序,访问权限等一系列规则。

基类成员public 继承protected 继承private 继承
public 成员public 成员protected 成员不可访问
protected 成员protected 成员protected 成员不可访问
private 成员不可访问不可访问不可访问

实际上就是等同于通过B的实例,我们能否访问A中的成员。
B通过private继承,显然无法访问成员变量A,更不谈A内的访问权限了
B通过protected继承,显然是只有继承了B才能访问A成员,接着才能谈A内的权限问题
B通过public继承,能够完全访问A,此时访问权限就和A定义的一样,

只需要记住:何种继承方式决定能否访问基类,基类的何种修饰符决定能否访问基类的成员
public为公开,不设限
protected为继承关系可访问
private为当前类(友元也包含在内)可以访问

构造顺序则与上面提到的无异。

(virtual) 虚基类和虚函数原理

普通函数的继承覆盖

class B {
public:
    void Test()
    {
        cout << "B"<<endl;
    }
};

class C : public B {
public:
    void Test() 
    {
        cout << "C"<<endl;
    }
};



int main() {

    C* c = new C();
    c->Test();
    ((B*)c)->Test();
    
    return 0;
}

这样的执行结果是
在这里插入图片描述
证明普通函数在执行时与当前的数据类型相关(如指针类型),当前指针类型为什么,则执行哪个类型的函数。

虚函数

在函数前增加virtual关键字即可定义虚函数,虚函数在整个作用过程发生以下行为

  1. 编译器生成虚函数表(为每个函数定义了指向真正函数的指针称为虚表指针)
  2. 不论当前的指针是何种类型,通过查询虚函数表找到真正的函数
  3. 一个类只有一张虚函数表,所以类的对象共享一张虚函数表。在编译期间确定虚函数表。
  4. 如果没有重写父类中的虚函数,那么子类的虚函数指针则指向父类的虚函数表,子类不会生成虚函数表;
  5. 如果子类有增加虚函数,那么新增的虚函数地址将追加到虚函数表尾端。
  6. 多继承时,子类中有多个虚函数表和多个虚函数指针

原理非常简单,就是为同名函数建立了一个表格,一个函数指向了一个真正的实现,根据继承关系不断产生覆盖。像查字典一样

#include <iostream>
using namespace std;
class A {
private:
    virtual void Test()
    {
        cout << "A";
    }
};

class B : public A {
public:
    void Test()
    {
        cout << "B";
    }
};

class C : public B {
public:
    void Test() 
    {
        cout << "C";
    }
};



int main() {

    C* c = new C();
    ((B*)c)->Test();
    return 0;
}

在这里插入图片描述
(可以去掉virtual得到CB输出)
且虚函数有如下特性

  1. 与访问权限关键字无关(无论何种权限都会建立虚函数表),与继承关系有关(在继承树中具备高深度优先,和唯一性)
  2. 查询虚函数表降低了函数效率

虚基类

虚基类旨在解决菱形继承问题,如果按照下面这张图的继承关系
在这里插入图片描述
如果我们不采用虚基类,则按照上面我们提到的继承的内存原理,实例应该是这样的
这样不仅导致了内存浪费,而且导致从B和C修改的数据不同步。
在这里插入图片描述

class A {
public:
    int x;
};

class B : virtual public Base {
public:
    int y;
};

class C : virtual public Base {
public:
    int z;
};

class D : public B, public C {
public:
    int w;
};

在这里千万注意构造函数的问题,如果是虚,则创建D的构造顺序是A->B->C->D
如果不是,则A->B->A->C->D

虚析构函数

如果A继承B,此时创建A类型的对象,转换为B类型的指针

  • B的析构函数是虚的
    那么在此对象被delete时,会先调用A的析构,然后调用B的析构
  • B的析构非虚
    在此对象被delete时,调用了B的析构,而没有调用A的析构

为什么?
因为此时编译器只知道这个对象的类型是B的指针,所有认为这是B的对象,只需要调用B的析构,所以导致生成的汇编,二进制都只调用了B的析构。

自定义类型转换函数

在这里插入图片描述

explict构造

显式构造函数

class MyClass {
public:
    explicit MyClass(int x) {
        this->x = x;
    }

    void print() {
        std::cout << x << std::endl;
    }

private:
    int x;
};

int main() {
    MyClass obj1 = 42; // 错误,不能隐式转换
    MyClass obj2(42); // 正确,需要显式调用构造函数
    obj2.print(); // 输出 42
    return 0;
}

特别的,对于 non-explict-one-argument ctor

  1. 非explict
  2. 单参数(默认参数也计数)
    这时我们可以
    在这里插入图片描述
    也要小心这种情况:
    编译器把Fraction转为double与4进行运算,但是结果右值double没有向Fraction的转换
    在这里插入图片描述
    相反的我们把4放到前面则编译通过
    在这里插入图片描述

模板特化

全特化

template <typename T>
class MyTemplate {
public:
    void foo() {
        // 通用实现
    }
};

template <>
class MyTemplate<int> {
public:
    void foo() {
        // 专门为int类型的特化实现
    }
};

偏特化

特定的偏
在这里插入图片描述
范围的偏
在这里插入图片描述

在这里插入图片描述

今天到这里,改日再更