文章目录
- 拷贝控制
- 一、拷贝构造函数
- 二、析构函数
- 三、对象移动(参考[知乎大佬](https://zhuanlan.zhihu.com/p/54050093?utm_source=wechatMessage_article_bottom))
- 四、杂例
- 运算符重载
- 一、基本概念
- 二、递增和递减运算符
- 三、函数调用运算符
- 四、类型转换运算符
- 面向程序设计
- 一、虚函数
- 二、类型转换与继承
- 三、[虚继承](https://blog.csdn.net/jackystudio/article/details/17877219?utm_medium=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.add_param_isCf&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.add_param_isCf)
- 模板与泛式编程
- 标准库特殊设施
- 一、tuple
- 特殊工具与技术
- 一、RTTI(运行时类型识别)
- 二、枚举类型和共用体
- 三、不可移植的特性
- 总结
拷贝控制
一、拷贝构造函数
1.定义
- 一个构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值,此构造函数是拷贝构造函数
- 合成拷贝构造函数是缺省的拷贝构造函数(自己未定义,编译器自动生成的)可以逐元素地拷贝一个数组类型的成员
//逐元素拷贝,但却是浅拷贝,下面是浅拷贝的例子
#include <iostream>
#include<string>
using namespace std;
class B
{
public:
int y;
B(int y) : y(y)
{
}
};
class A
{
public:
B *x[20];
A()
{
x[0] = new B(2);
}
};
int main()
{
A a;
A b = a;
cout << b.x[0]->y << endl;
a.x[0]->y = 6;
cout << b.x[0]->y << endl;
return 0;
}
2.触发条件
- 对象作为实参传递给非引用类型的形参(这也是拷贝构造函数形参为&的原因,如果不是引用,则会调用拷贝构造函数,这种行为是为定义的)
- 从返回类型为非引用类型的函数返回一个对象
- {}初始化列表——数组,聚合类(定义如下)
- 所有成员都是public
- 没有定义构造函数
- 没有类内初始值
- 没有基类,也没有虚函数
- 容器调用insert和push,进行拷贝初始化
3.注意
- 编译器为了优化,也会绕过拷贝构造函数,但拷贝构造必须存在且可访问
二、析构函数
1.特点
- 没有返回值,不接受参数,因此不能被重载,只有唯一一个析构函数
- 成员按初始化顺序的逆序销毁
- 内置类型没有析构函数
2.触发条件
- 变量离开作用域
- 对象被销毁,成员被销毁
- 容器被销毁,其元素也被销毁
- delete销毁
- 对于临时变量,当创建它的完整表达式结束时被销毁
3.注意
析构函数自身并不直接销毁成员,成员是在析构函数体之后隐含的析构阶段中被销毁的
三、对象移动(参考知乎大佬)
1.适用对象
IO类和unique_ptr类可以移动但不能拷贝
2.右值引用&&
- 只能绑定到临时对象,不能绑定到一个变量上
- 所引用的对象将要被销毁
- 该对象没有其他用户
- 具体到函数:
emplcae_back 接收一个右值引用,调用其移动构造函数,将对象移动到容器中,而之前的push_back 是调用一次对象的拷贝构造函数, 容器中存储的是拷贝后的副本 - 右值引用一定不能被左值所初始化,只能用右值初始化:
int x = 20; // 左值
int&& rrx1 = x; // 非法:右值引用无法被左值初始化
const int&& rrx2 = x; // 非法:右值引用无法被左值初始化
- const左值引用可以接收右值
3.移动语义(例子可以看知乎那篇文章)
将内存的所有权从一个对象转移到另外一个对象,高效的移动用来替换效率低下的复制
通过内存所有权的接管来免去临时对象的复制操作
- 编译器不会创建默认的移动构造函数和移动赋值运算符,所以4个函数(拷贝构造,赋值构造,移动构造,移动赋值)实现了一个,最好把剩余3个都实现了
- move:左值转化为右值
vector<int> v1{1, 2, 3, 4};
vector<int> v2 = v1; // 此时调用复制构造函数,v2是v1的副本
vector<int> v3 = std::move(v1); // 此时调用移动构造函数,v3与v1交换:v1为空,v3为{1, 2, 3, 4}
典型应用swap
//C++11以前,拷贝构造和赋值
template <typename T>
void swap(T& a, T& b)
{
T tmp{a}; // 调用复制构造函数
a = b; // 复制赋值运算符
b = tmp; // 复制赋值运算符
}
//C++11,移动构造和移动赋值
template <typename T>
void swap(T& a, T& b)
{
T temp{std::move(a)}; // 调用移动构造函数
a = std::move(b); // 调用移动赋值运算符
b = std::move(tmp); // 调用移动赋值运算符
}
简化版move,详解见知乎文章
template <typename T>
typename remove_reference<T>::type&& move(T&& param)
{
using ReturnType = typename remove_reference<T>::type&&;
return static_cast<ReturnType>(param);
}
- 注意:出现3个&符号的解释,我们知道C++不允许引用的引用,那么其实编译器这里进行是引用折叠(reference collapsing,大致就是后面的引用消掉)
4.完美转发(还不是很懂,留坑!)
定义一个函数模板,该函数模板可以接收任意类型参数,然后将参数转发给其它目标函数,且保证目标函数接受的参数其类型与传递给模板函数的类型相同
四、杂例
1.三五法则
- 需要析构函数的类也需要拷贝和赋值操作
- 需要拷贝操作的类也需要赋值操作,反之亦然
2.阻止拷贝(采用默认合成的拷贝)
- =delete
- 析构函数不能是删除的成员
运算符重载
一、基本概念
- , & || &&不应该被重载
:: .* . ?:不能被重载 - 对于运算符函数来说,它或者是类的成员,或者至少含有一个类类型的参数
- 成员函数还是非成员函数
- = [] () ->必须是成员
- 复合运算符(+=)一般也是成员,并非必须
- 改变状态的运算符或者与给定类型密切相关的运算符,如++ – &,通常是成员
- 具有对称性的运算符通常是普通的非成员函数
二、递增和递减运算符
- 区分前置后置运算符:后置版本接受一个额外的int类型形参(后来者排队,所以总共有两个参数,前置只有一个)
- 后置运算符应该返回对象原值返回的形式是一个值而非引用,与内置版本保持一致
三、函数调用运算符
- lambda是函数对象:编译器将lambda表达式翻译成一个未命名类的未命名对象,在lambda表达式产生的类中含有一个重载的函数调用运算符(),lambda产生的类当中的函数调用运算符是一个const成员函数
- 可调用对象与function:定义函数表,使用function存储,function是一个模板,function<int(int,int)>,表示接受两个int,返回一个int的可调用对象
- 可调用对象的具体作用:使不同类型具有相同的调用形式
四、类型转换运算符
- 没有显式返回类型,也没有形参,必须被定义成类的成员函数,且为const成员
- 显式的类型转换将被隐式执行,即表达式被用作条件,则编译器会将显式的类型转换自动应用于它
- if,while,do及语句的条件部分
- for语句头的条件表达式
- ! || && 的运算对象
- ? : 的条件表达式
面向程序设计
一、虚函数
1.细节
基类中是虚函数,派生类中也是默认为虚函数
2.函数表剖析
#include <iostream>
using namespace std;
class Base1
{
public:
virtual void f()
{ cout << "Base1::f" << endl; }
virtual void g()
{ cout << "Base1::g" << endl; }
virtual void h()
{ cout << "Base1::h" << endl; }
};
class Base2
{
public:
virtual void f()
{ cout << "Base2::f" << endl; }
virtual void g()
{ cout << "Base2::g" << endl; }
virtual void h()
{ cout << "Base2::h" << endl; }
};
class Base3
{
public:
virtual void f()
{ cout << "Base3::f" << endl; }
virtual void g()
{ cout << "Base3::g" << endl; }
virtual void h()
{ cout << "Base3::h" << endl; }
};
class Derive:
public Base1, public Base2, public Base3
{
public:
virtual void f()
{ cout << "Derive::f" << endl; }
virtual void g1()
{ cout << "Derive::g1" << endl; }
};
typedef void(*Fun)(void);
int main()
{
Fun pFun = nullptr;
Derive d;
long **pVtab = (long **) &d;
//Base1's vtable
//pFun = (Fun)*((int*)*(int*)((int*)&d+0)+0);
pFun = (Fun) pVtab[0][0];
pFun();
//pFun = (Fun)*((int*)*(int*)((int*)&d+0)+1);
pFun = (Fun) pVtab[0][1];
pFun();
//pFun = (Fun)*((int*)*(int*)((int*)&d+0)+2);
pFun = (Fun) pVtab[0][2];
pFun();
//Derive's vtable
//pFun = (Fun)*((int*)*(int*)((int*)&d+0)+3);
pFun = (Fun) pVtab[0][3];
pFun();
//The tail of the vtable
pFun = (Fun) pVtab[0][4];
cout << pFun << endl;
//Base2's vtable
//pFun = (Fun)*((int*)*(int*)((int*)&d+1)+0);
pFun = (Fun) pVtab[1][0];
pFun();
//pFun = (Fun)*((int*)*(int*)((int*)&d+1)+1);
pFun = (Fun) pVtab[1][1];
pFun();
pFun = (Fun) pVtab[1][2];
pFun();
//The tail of the vtable
pFun = (Fun) pVtab[1][3];
cout << pFun << endl;
//Base3's vtable
//pFun = (Fun)*((int*)*(int*)((int*)&d+1)+0);
pFun = (Fun) pVtab[2][0];
pFun();
//pFun = (Fun)*((int*)*(int*)((int*)&d+1)+1);
pFun = (Fun) pVtab[2][1];
pFun();
pFun = (Fun) pVtab[2][2];
pFun();
//The tail of the vtable
pFun = (Fun) pVtab[2][3];
cout << pFun << endl;
return 0;
}
二、类型转换与继承
- 不存在基类向派生类的隐式类型转换,但有显式的(后续会讲)
- 继承方式决定了在派生类中基类成员的访问权限(公有继承,保护继承,私有继承)
三、虚继承
- 类通过虚继承指出它希望共享虚基类的状态
- 主要解决菱形继承的资源浪费问题,D继承了两份相同的A,把A设置成虚基类可以解决这个问题,D中就只会有一份A的成员变量和方法
模板与泛式编程
(这个有打算专门写一片博客,主要是太菜了,没怎么学明白,后续打算补一下这块的知识…)
标准库特殊设施
一、tuple
- 任意数量成员,每个成员类型可以不同
- tuple<T1,T2,T3,…,Tn> t (v1,v2,…,vn)
特殊工具与技术
一、RTTI(运行时类型识别)
- typeid——abi::__cxa_demangle(typeid(a).name(), 0, 0, 0)使typeid获取类的完整声明
#include <iostream>
#include <cxxabi.h>
using namespace std;
class AB
{
};
int main()
{
AB a;
cout << typeid(a).name() << endl;//2AB,前面的2代表类的字符串长度为2
cout << abi::__cxa_demangle(typeid(a).name(), 0, 0, 0) << endl;//AB,这一串可以只显示类名
return 0;
}
- dynamic_cast(将基类指针转化成派生类指针)
- 提供了运行时确定对象类型的方法(区分于auto和decltype编译时确定对象类型)
二、枚举类型和共用体
1.枚举类型
enum
2.共用体
union变量所占用的内存长度等于最长的成员的内存长度(可用这个性质判断大小端)转自这位大佬
#include <stdio.h>
typedef union SmallBig
{
char buf[4];
long num;
} smallbig;
int main()
{
smallbig sb;
sb.num = 0x12345678;
int i = 0;
for (i = 0; i < 4; i++)
printf("sb.buf[%d]=%x \n", i, sb.buf[i]);
return 0;
}
/*
小端结果
sb.buf[0]=78
sb.buf[1]=56
sb.buf[2]=34
sb.buf[3]=12
*/
三、不可移植的特性
1.位域
规定变量占几个二进制位;int x:27——x占27位
充分利用内存空间
2.extern “C”
总结
完结撒花!接下来是暑假的一个专题总结,关于23种设计模式的总结,当然了也是像这样的简陋版本,争取在开学之前完成吧!最后附赠一张我C++总结的思维导图,内容和这三篇博客相差无几。