QT(5.8版本)标准库源码主要在安装目录下的(VC找inlcude目录)
扩充内容的源码在
OOP 和 GP
oop(面向对象编程):Object-Oriented Programming,数据和操作放在一起(同一个类中)
GP(泛型编程):Generic Programming,数据和操作分开
使用GP:
Container和Algorithms团队可各自设计,其间以Iterators沟通即可
Algorithms通过Iterators确定操作范围,并通过Iterators取用Container元素
举例之:sort
源文件:stl_algo.h
有两个函数,分别是自定义从小到大版本,和允许接收仿函数自定义排序规则版本,两者都调用底层的 __sort函数
__sort是底层实现,重点在于对introsort_loop的调用,关于它的写法不同版本可能有些许不同,但重点在于它必须是可以随机访问的迭代器,即RandomAccessIterator,否则无法实现 *2(或/2)的操作,这也是为什么list不能使用::sort()的原因
c++ forward转发
在学习源码之前,需要先了解c++中的forward转发的用法
非常好的一篇:https://www.cnblogs.com/kaleidopink/p/13720773.html
右值引用原理:https://blog.csdn.net/m0_54850825/article/details/124761539
背景
背景:在C++11出现之前,C++传值默认是copy,但copy开销很大
举例:(a = b + c + d)中,c+d是一个临时变量, b+(c+d)又是另一个临时变量
右值:这些临时变量在C++11中被定义为右值(rvalue,read value),右值没有相应的变量名存储它们
左值:与右值相对的是左值(lvalue,localtor value),左值有变量名
场景:
class A {...};
A a;
a.set("temp");//将"temp"复制给a的成员变量
可能的底层步骤:
临时变量"temp"在传参时先被复制一遍
被复制的内容放到a的成员变量中去
回收临时变量
可以改进的地方:临时变量既然是要回收的,那么可以直接使成员变量接管临时变量,就可以避免中间的复制过程
move和forward就是为此而产生的
左值引用和右值引用
左值引用
1. 原理
关于引用:&b=a实际上等价于int* const b=&a,而编译器会把&b编译为:&(*b)
它的实现是:将a的地址放到寄存器中,然后再将寄存器中的地址传给引用变量b
常量之所以可以左值引用,是因为它被放在静态存储区,是有地址的。而像是10这样的临时变量是放在寄存器中的,无法取址
2. 使用和注意事项
左值引用要求右值必须能够取地址,如果不能取地址,则必须为常引用。因为左值引用本质上是将地址赋给左值
int &b = a + 1;//错误,左值引用必须能够取地址
a+1不能被认为一个在内存中存在地址的变量,如果要这样用,需要加上const修饰。
同样的函数返回值不能使用左值引用,例如 int &a = func();这种情况要么修改为常引用或者不使用引用,但常引用会导致引用不可修改。
普通引用只能引用左值,不能引用右值,const引用既可引用左值,也可引用右值
当使用&b=a; 对b的操作相当于对(*b)的操作,即对a的操作
右值引用
1. 原理
int&& ref = 3;
临时变量(编译时未分配内存或使用寄存器存放的值)引用关联到右值时,右值被存储到特定位置
它的汇编指令和const &b = 3是一样的,只是后者不允许修改
用临时变量将3存起来,然后将地址送入寄存器;寄存器再将地址传给ref
变量和临时变量的值实际是按同一方式处理的,也就是说,临时变量根本上来说就是一个没有名字的变量而已,从汇编的角度来说,都是指针
C++引入右值引用,是为了延长临时变量的生命周期,避免昂贵的copy开销。相当于用指针直接接管这一块内存,而不是重新申请内存再复制
2. 使用和注意事项
右值引用可以处理临时变量的情况,即无法寻址的情况
类型 &&引用名 = 右值表达式;//定义格式
右值引用可以延长临时变量的生存周期,避免了无谓的内存复制操作。
C++11中右值引用:只能引用右值,一般情况不能直接引用左值
单纯的右值引用没有意义,它常与move和forward一起使用
引用叠加
int x = 1;
int&& r1 = x;//实际上这一步就会报错了
auto&& r2 = r1;
r2最终是一个左值引用
所有的右值引用叠加到右值引用上仍然是一个右值引用;
所有其他类型之间的叠加将会使得变成一个左值引用
move移动语义
参考:https://blog.csdn.net/u013015629/article/details/116525759
move是将对象的状态或者所有权从一个对象转移到另一个对象,只是转义,没有内存拷贝。
int d = 1;
int &&r = d;
上述操作是不被允许的,如果需要用右值引用接收左值,可以采用move函数(当不确定接收的是左值还是右值时可以这样使用)
int &&r = std::move(d);
而且move几乎没有代价,只是转移了资源的控制权。尽量可以用。
- 如果没有提供移动构造函数,只提供了拷贝构造函数,std::move()会失效但是不会发生错误,因为编译器找不到移动构造函数就去寻找拷贝构造函数,也这是拷贝构造函数的参数是const T&常量左值引用的原因
- c++11中的所有容器都实现了move语义,move只是转移了资源的控制权,本质上是将左值强制转化为右值使用,以用于移动拷贝或赋值,避免对含有资源的对象发生无谓的拷贝。move对于拥有如内存、文件句柄等资源的成员的对象有效,如果是一些基本类型,如int和char[10]数组等,如果使用move,仍会发生拷贝(因为没有对应的移动构造函数),所以说move对含有资源的对象说更有意义。
forward完美转发
move会将左值强制变成右值使用,而forward会保持原变量的属性
若对一个对象做move操作,就不要声明为 const. 因为对const对象的move请求会执行到copy操作上
void func(int&& i) {
cout << "rvalue: " << i << endl;
}
void func(int& i) {
cout << "lvalue: " << i << endl;
}
int main() {
int val = 2;
int &m1 = val;
//在不使用move或者forward时
func(m1);//lvalue
func(3);//rvalue
//使用move会将左值变成右值
func(std::move(m1));//rvalue
func(std::move(3));//rvalue
//使用forward会保持原有特性
func(std::forward<int&>(m1));//lvalue
func(std::forward<int>(3));//rvalue
return 0;
}
上述代码中可以看到move和forward的不同
如果仅是这样的用法,体现不出move或者forward的意义,它们常常是用在需要中间函数转发的情况,如下面代码所示。
如果不使用move和forward,经过中间函数rpass的转发,右值将变成左值
void func(int&& i) {
cout << "rvalue: " << i << endl;
}
void func(int& i) {
cout << "lvalue: " << i << endl;
}
void lpass(int& i){
func(i);//lvalue
func(std::move(i));//rvalue
func(std::forward<int &>(i));//lvalue
}
void rpass(int&& i){
func(i);//lvalue
func(std::move(i));//rvalue
func(std::forward<int &&>(i));//rvalue
}
int main() {
int val = 2;
lpass(val);
rpass(3);
return 0;
}
为了解决左值变右值的问题,C++用了move将左值右值都转为右值。
进一步的,为了使得左值始终为左值,右值始终为右值,C++推出了完美转发(Perfect Forwarding),即在函数模板中,完全按照模板的参数的类型(保持参数的左值、右值特征)进行参数的传递。
变参数模板
参考:https://zhuanlan.zhihu.com/p/394184676
C++11引入的新特性,参数个数和类型都可能发生变化的模板。变参数模板必须使用到模板形参包
模板形参包
模板形参包是可以接受0个或者n个模板实参的模板形参,至少有一个模板形参包的模板就可以称作变参数模板
模板形参包主要出现在函数模板和类模板中,主要有三种,即:非类型模板形参包、类型模板形参包、模板模板形参包。
非类型模板形参包
相较于class和typename而言的,实际上是固定类型形参包,如:
template<int ... data> xxxxxx;
这个类型只能为整型、指针和引用
- 示例:函数模板
//class type实际上用不到,但是因为template<>不能为空,所以要加上
template<class type>
void printAmt(int &iSumAge){
return;
}
template<class type, int age0, int ... age>
void printAmt(int &iSumAge){
iSumAge += age0;
//这里sizeof ... (age)是计算形参包里的形参个数,返回类型是std::size_t,后续同理
if ( (sizeof ... (age)) > 0 ){
//这里的age...其实就是语法中的一种包展开
printAmt<type, age...>(iSumAge);
}
}
int main(){
int sumAge = 0;
printAmt<int,1,2,3,4,5,7,6,8>(sumAge);
cout << "the sum of age is " << sumAge << endl;
return 0;
}
(1)开头的空模板必须要写,因为递归访问形参包(每次取形参包中的一个),最后一次必然是一个空参数调用
(2)sizeof … (age)返回类型是size_t,代表形参个数
(3)传递给非类型模板形参包的实参不是类型,而是实际的值
类型模板形参包
和前面的类似,只是这里的形参包的类型可以不再固定
typename|class ... Args
- 示例
void xprintf(){
cout << endl;
}
template<typename T, typename... Targs>
void xprintf(T value, Targs... Fargs){
//这里调用的时候没有显式指定模板,是因为函数模板可以根据函数参数自动推导
if ( (sizeof ...(Fargs)) > 0 ) xprintf(Fargs...);
else xprintf();
}
int main(){
xprintf("小明个人信息:", "小明", "男", 35, "程序员", 169.5);
return 0;
}
模板模板形参包
template < 形参列表 > class ... Args(可选)
- 示例1:模板作为参数
场景:要使用vector,但暂时不知道vector是什么类型。于是可以将vector和类型作为参数,新建一个模板,这个新的模板有一个模板形参(如传来的vector),还有一个类型形参,如下面的代码:
#include <typeinfo>
#include <cxxabi.h>
#include <iostream>
#include <vector>
//将gcc编译出来的类型翻译为真实的类型
const char* GetRealType(const char* p_szSingleType) {
const char* szRealType = abi::__cxa_demangle(p_szSingleType, nullptr, nullptr, nullptr);
return szRealType;
}
//这里的func是一个模板模板形参
template<template<typename, typename> class func, typename tp, typename alloc = std::allocator<tp> >
struct temp_traits {
using type = func<tp, alloc>;
type tt;//根据模板类型定义一个成员变量
};
int main() {
temp_traits<std::vector, int> _traits;
//获取结构体字段tt的类型
const std::type_info &info = typeid(_traits.tt);
std::cout << GetRealType(info.name()) << std::endl;
return 0;
}
//输出:std::vector<int, std::allocator<int> >
//还可以传入其他模板和类型
temp_traits<std::list, double> _traits;
//输出:std::__cxx11::list<double, std::allocator<double> >
- 示例二:模板作为参数,及可变模板参数
#include <typeinfo>
#include <cxxabi.h>
#include <iostream>
#include <vector>
#include <list>
#include <deque>
//将gcc编译出来的类型翻译为真实的类型
const char* GetRealType(const char* p_szSingleTyp {
const char* szRealType = abi::__cxa_demangle(p_szSingleType, nullptr, nullptr, nullptr);
return szRealType;
}
void xprintf() {
std::cout << "调用空函数" << std::endl;
}
template<typename tp, typename alloc, template<typename, typename> class T, template<typename, typename> class ... Targs >
void xprintf(T<tp, alloc> value, Targs<tp, alloc>... Fargs) {
std::cout << "容器类型:" << GetRealType(typeid(value).name()) << std::endl;
std::cout << "容器数据:" << std::endl;
auto it = value.begin();
for(; it != value.end(); ++it) std::cout << *it << ',';
std::cout << std::endl;
//这里调用的时候没有显式指定模板,是因为函数模板可以根据函数参数自动推导
if ( (sizeof ...(Fargs)) > 0 ) xprintf(Fargs...);
else xprintf();
}
int main(){
std::vector<int> vt;
std::deque<int> dq;
std::list<int> ls;
for(int i =0 ; i < 10 ; ++i) {
vt.push_back(i);
dq.push_back(i);
ls.push_back(i);
}
xprintf(vt, dq, ls);
return 0;
}
形参包的使用方法
(1)就像上面的例子那样,递归的方法一个个取出形参包里的形参,最后使用一个默认函数来处理形参包为空的情况
(2)直接展开形参包,然后将所有参数传递给另一个函数等
template<class type, int age0, int ... age>
void printAmt(int &iSumAge){
iSumAge += age0;
(void)std::initializer_list<int>{
(std::cout << age <<",", 0)...
};
}
int main(){
int sumAge = 0;
printAmt<int,1,2,3,4,5,7,6,8>(sumAge);
std::cout << "the sum of age is " << sumAge << std::endl;
return 0;
}