C++ 学习笔记
Posted 腾讯技术工程
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++ 学习笔记相关的知识,希望对你有一定的参考价值。
作者:readywang(王玉龙)
。template 是 c++ 相当重要的组成部分,堪称 c++语言的一大利器。在大大小小的 c++ 程序中,模板无处不在。c++ templates 作为模板学习的经典书籍,历来被无数 c++学习者所推崇。第二版书籍覆盖了 c++ 11 14 和 17 标准,值得程序猿们精读学习,特此整理学习笔记,将每一部分自认为较为重要的部分逐条陈列,并对少数错误代码进行修改
可以将函数模板定义为友元,此时若模板参数可推导,在友元声明时可以省略。
template<typename T>
class Tree
friend class Factory; // OK even if first declaration of Factory
friend class MyNode<T>; // error MyNode未找到声明
;
template<typename T>
class Stack
public:
// assign stack of elements of type T2
template<typename T2>
Stack<T>& operator= (Stack<T2> const&);
// to get access to private members of Stack<T2> for any type T2: template<typename> friend class Stack;
;
template<typename T>
class Test
friend T; // 可以将类型模板参数定义为友元
;
template<typename T1, typename T2>
void combine(T1, T2);
class Mixer
friend void combine<>(int&, int&); //OK:T1 = int&,T2 = int&
friend void combine<int, int>(int, int);//OK:T1 = int,T2 = int
friend void combine<char>(char, int);//OK:T1 = charT2 = int
friend void combine<char>(char&, int);// ERROR: doesn’t match combine() template
friend void combine<>(long, long) // ERROR: definition not allowed!
;
十三、模板中的名称13.1 名称分类
1.名称分为受限名称和非受限名称,受限名称前面有显式的出现 ->, ::, . 这三个限定符
13.2 名称查找
c++名称的普通查找规则为从名称所在的 scope 从内向外依次查找。
ADL( Argument-Dependent Lookup)查找为依赖于参数的查找,是用于函数调用表达式中查找非限定函数名称的规则。当在使用函数的上下文中找不到函数定义,我们可以在其参数的关联类和关联名字空间中查找该函数的定义。
ADL 生效条件:a.使用此规则的函数必须要有参数 b. 使用此规则的函数名称必须为非受限名称
4.ADL 查找范围:
(1)对于基本类型(int, char 等), 该集合为空集
(2)对于指针和数组类型,该集合是所引用类型的关联类和关联名字空间
(3)对于枚举类型,名字空间是名字空间是枚举声明所在的名字空间,对于类成员,关联类是枚举所在的类
(4)对于 class(包含联合类型),关联类包括该类本身,他的外围类,直接基类,间接基类。关联名字空间包括每个关联类所在的名字空间。
(5)对于函数类型, 该集合包含所有参数类型和返回类型的关联类和关联名字空间
(6)对于类 X 的成员指针类型,除了包括成员相关的关联名字空间,关联类,该集合还包括与 X 相关的关联名字空间和关联类
#include <iostream>
namespace X
template<typename T> void f(T);
namespace N
using namespace X;
enum E e1 ;
void f(E)
std::cout << "N::f(N::E) called\\n";
void f(int)
std::cout << "::f(int) called\\n";
int main()
::f(N::e1); // qualified function name: no ADL
f(N::e1); // ordinary lookup finds ::f() and ADL finds N::f(),the latter is preferred
c++ 类中声明的友元函数在类外是不可见的,若未在类外提供定义,要想查找到该函数,只能通过 ADL.
template<typename T>
class C
friend void f();
friend void f(C<T> const&);
;
void g (C<int>* p)
f(); // error 类内声明的友元类外不可见,普通查找无法找到
f(*p); // ok 虽然不可见,但是可以通过ADL找到
类名注入:在类作用域中,当前类的名字被当做它如同是一个公开成员名一样;这被称为注入类名(injected-class-name)。
int X;
struct X
void f()
X* p; // OK:X 指代注入类名
::X* q; // 错误:名称查找找到变量名,它隐藏 struct 名
;
对于类模板,除了注入类名,还可以注入实例化的类名。
template<typename T> class C
using Type = T;
struct J
C* c; // C refers to a current instantiation
C<Type>* c2; // C<Type> refers to a current instantiation
I* i; // I refers to an unknown specialization,
// because I does not enclose J
J* j; // J refers to a current instantiation
;
struct I
C* c; // C refers to a current instantiation
C<Type>* c2; // C<Type> refers to a current instantiation
I* i; // I refers to a current instantiation
;
;
13.3 模板解析
大多数编程语言的编译器的两个基本活动是标记化和解析。
X<1>(0) ;被解析时,若 X 是一个类型,<1>则被认为是模板实参;若 X 不是类型,则被认为是 x<1 比较后的结果再和>0 比较。在处理模板时,一定要避免大于符号>被当做模板参数终止符。
template<bool B>
class Invert
public:
static bool const result = !B;
;
void g()
bool test = Invert<1>0>::result; // error 模板参数被提前结束
bool test = Invert<(1>0)>::result; // ok
类型名称具有以下性质时,就需要在其前面添加 typename
a. 名称出现在一个模板中
b. 名称是受限的
c. 名称不是用于基类的派生列表或构造函数的初始化列表中
d. 名称依赖于模板参数
ADL 用于模板函数时,可能会产生错误。
namespace N
class X
;
template<int I> void select(X*);
void g (N::X* xp)
select<3>(xp); // ERROR 编译无法知道<3>是模板参数,进而无法判断select是函数调用
13.4 派生和类模板
大多数情况中类模板派生和普通类派生无太大区别。
2.非依赖型基类:无需知道模板名称就可以完全确定类型的基类。
3.非依赖型基类的派生类中查找一个非受限名称时,会先从非依赖型基类中查找,然后才是模板参数列表。
template<typename X>
class Base
public:
int basefield;
using T = int;
;
class D1: public Base<Base<void>> // 实际上不是模板
public:
void f() basefield = 3; // 继承成员普通访问
;
template<typename T>
class D2 : public Base<double> // 继承自非依赖型基类
public:
void f() basefield = 7; // 继承成员普通访问
T strange; // T 是 Base<double>::T 类型,也就是int, 而不是模板参数T!
;
十四、实例化14.1 On-Demand 实例化
1.模板被实例化时,编译器需要知道实例化部分的完整定义。
14.2 延迟实例化
1.模板实例化存在延迟现象,编译器只会实例化需要的部分。如类模板会只实例化用到的部分成员函数,函数模板如果提供了默认参数,也只会在这个参数会用到的时候实例化它。
14.3 c++实例化模型
1.两阶段查找:编译器在模板解析阶段会检测不依赖于模板参数的非依懒型名称,在模板实例化阶段再检查依懒型名称。
2.Points of Instantiation: 编译器会在需要实例化模板的地方插入实例化点(POI)
14.4 几种实现方案
14.5 显式实例化
十五、模板实参推导15.1 推导的过程
1.函数模板实例化过程中,编译器会根据实参的类型和模板参数 T 定义的形式,推导出函数的各个参数的类型,如果最后推导的结论矛盾,则推导失败。
template<typename T>
T max (T a, T b)
return b < a ? a : b;
auto g = max(1, 1.0); // error 根据1推导T为int 根据1.0推导T为double
template<typename T> void f(T);
template<typename T> void g(T&);
double arr[20];
int const seven = 7;
f(arr); // T 被decay为double*
g(arr); //T 推断为 double[20]
f(seven); // T 被decay为int
g(seven); // T 推断为 int const
15.2 推导的上下文
15.3 特殊的推导情况
1.函数模板被取地址赋予函数指针时,也会产生实参推导。
2.类中定义了类型转换的模板函数时,在类型转换时可以产生实参推导。
template<typename T>
void f(T, T);
void (*pf)(char, char) = &f; // T 被推断为char
class S
public:
template<typename T> operator T&();
;
void f(int (&)[20]);
void g(S s)
f(s); // 类型转换时T被推导为int[20]
15.4 初始化列表
1.模板实参如果是初始化列表时,无法直接完成模板参数类型 T 的推导。若函数参数通过 std::initializer_list 定义,则实参类型需要一致。
template<typename T> void g(T p);
template<typename T> void f(std::initializer_list<T>);
int main()
g(’a’, ’e’, ’i’, ’o’, ’u’, 42); // ERROR: T deduced to both char and int
f(1, 2, 3); // ERROR: cannot deduce T from a braced list
f(2, 3, 5, 7, 9); // OK: T is deduced to int
15.5 参数包
15.6 右值引用
1.引用折叠:只有两个右值引用会被折叠为右值引用,其它情形都是左值引用
using RCI = int const&;
RCI volatile&& r = 42; // OK: r has type int const&
using RRI = int&&;
RRI const&& rr = 42; // OK: rr has type int&&
15.7 SFINAE
1.根据 SFINAE 原理,编译器在用实参推导模板参数失败时,会将该模板忽略。
template<typename T, unsigned N>
T* begin(T (&array)[N])
return array;
template<typename Container>
typename Container::iterator begin(Container& c)
return c.begin();
int main()
std::vector<int> v;
int a[10];
::begin(v); // OK: only container begin() matches, because the first deduction fails
::begin(a); // OK: only array begin() matches, because the second substitution fails
十六、特化和重载16.1 当泛型代码不再适用的时候
16.2 重载函数模板
1.函数模板和普通函数一样,是可以被重载的。
#include<iostream>
template<typename T>
int f(T)
return 1;
template<typename T>
int f(T*)
return 2;
int main()
std::cout << f<int*>((int*)0) << std::endl; // 更匹配第一个函数calls f<T>(T)
std::cout << f<int>((int*)0) << std::endl; // 只匹配第二个函数 calls f<T>(T*)
std::cout << f((int*)0) << std::endl; // 都匹配,但第二个更特殊,优先选用 calls f<T>(T*)
如上所示,main 中实例化后的前两个函数完全相同,但是可以同时存在,原因是它们具有不同的签名。
3.函数签名由以下部分构成:
a. 非受限函数名称
b. 名称所属的类作用域
c. 函数的 const volatile 限定符
d. 函数参数的类型
e. 如果是函数模板,还包括返回类型、模板参数和模板实参。
// 以下模板的实例化函数可以同时存在
template<typename T1, typename T2>
void f1(T1, T2);
template<typename T1, typename T2>
void f1(T2, T1);
template<typename T>
long f2(T);
template<typename T>
char f2(T);
c++最开始例子中最后一个函数实例化时和两个模板均匹配,此时将选用更特殊的函数模板。
普通函数和模板函数也可以同时重载,此时在匹配程度相同时,优先调用普通函数。
template<typename T>
std::string f(T)
return "Template";
std::string f(int&)
return "Nontemplate";
int main()
int x = 7;
std::cout << f(x) << std::endl; // 调用非模板函数
16.3 显式特化
重载只适用于函数模板,对于类模板,可以使用特化的方式使得编译器进行更优的选择。
类模板若提供了默认参数,特化后的模板可以不加。
template<typename T>
class Types
public:
using I = int;
;
template<typename T, typename U = typename Types<T>::I>
class S; // 模板1声明
template<>
class S<void> // 特化后的模板2定义,可以不用默认参数
public:
void f();
;
template<> class S<char, char>; // 特化后的模板3声明
template<> class S<char, 0>; // error 特化失败,0不能当类型
int main()
S<int>* pi; // OK: 使用模板1且不需定义
S<int> e1; // ERROR: 使用模板1且需要定义
S<void>* pv; // OK: 使用模板2且不需定义
S<void,int> sv; // OK: 使用模板2且不需定义
S<void,char> e2; // ERROR: 使用模板1且需要定义
S<char,char> e3; // ERROR: 使用模板3且需要定义
template<>
class S<char, char> // 特化后的模板3定义,此处定义对main中的实例化调用是不可见的
;
模板全特化之后的类和由相同的特化参数实例化后的类是相同的,不能同时存在。
template<typename T,typename U>
class Invalid
;
Invalid<double, int> x1; // 实例化一个类模板
template<typename T>
class Invalid<T,double>; // ok, 部分偏特化可以和实体化实体同时存在
template<>
class Invalid<double,int>; // ERROR: 全特化后的模板不能和实例化实体同时存在
类模板全特化后,若在类外定义函数,则不能在前面加 template<>
template<typename T>
class S;
template<> class S<char**>
public:
void print() const;
;
// 不能出现template<>
void S<char**>::print() const
std::cout << "pointer to pointer to char\\n";
函数模板全特化时,不能含有默认参数值,但可以直接使用。
template<typename T>
int f(T) // #1
return 1;
template<typename T>
int f(T*) // #2
return 2;
template<> int f(int) // OK: specialization of #1
return 3;
template<> int f(int*) // OK: specialization of #2
return 4;
template<typename T>
int f(T, T x = 42)
return x;
template<> int f(int, int = 35) // 特化时不能指定默认参数
return 0;
template<typename T>
int g(T, T x = 42)
return x;
template<> int g(int, int y)
return y/2; // 返回21
变量模板也可以进行全特化。
template<typename T> constexpr std::size_t SZ = sizeof(T);
template<> constexpr std::size_t SZ<void> = 0;
除了对类模板进行全特化以外,也可以单独为类模板中的某个成员进行全特化。
template<typename T>
class Outer
public:
static int code;
void print() const
std::cout << "generic";
void test();
;
template<typename T>
int Outer<T>::code = 6; // 类外定义
// 对于Outer<void>类型,code和print将采用特化版本,其余采用普通版本
template<>
int Outer<void>::code = 12; // 特化静态成员
template<>
void Outer<void>::print() const // 特化类模板
std::cout << "Outer<void>";
16.4 类模板偏特化
对于类模板,可以进行偏特化,偏特化也不能提供默认参数,但可以直接使用。
template<typename T, int I = 3>
class S; // primary template
template<typename T>
class S<int, T>; // ERROR: 参数类型不匹配
template<typename T = int>
class S<T, 10>; // ERROR: 不能有默认参数
template<int I>
class S<int, I*2>; // ERROR:不能有非类型表达式
template<typename U, int K>
class S<U, K>; // ERROR:特化模板和基本模板无区别
16.5 变量模板偏特化
对于变量模板,也可以偏特化。
template<typename T> constexpr std::size_t SZ = sizeof(T);
template<typename T> constexpr std::size_t SZ<T&> = sizeof(void*);
十七、未来的方向十八、模板的多态性18.1 动态多态
动态多态:通过继承和虚函数实现,在运行期根据指针或引用的具体类型决定具体调用那一个虚函数。
18.2 静态多态
静态多态:通过模板实现,在编译期基于类型调用不同模板。
18.3 动态多态 vs 静态多态
18.4 使用 concepts
使用静态多态时可以采用 6.5 中介绍的 concept 对可传入模板的类型做以限制。
18.5 新形式的设计模式
对于桥接模式,若具体实现的类型在编译期间可以确定,则可以使用模板代替传统的桥接模式实现。
18.6 泛型编程
十九、萃取实现19.1 序列求和示例
简单的求和模板函数的定义及使用如下:
template<typename T>
T accum (T const* beg, T const* end)
T total; // 初始化为零值
while (beg != end)
total += *beg;
++beg;
return total;
int main()
int num[] = 1, 2, 3, 4, 5 ;
std::cout << "the average value of the integer values is "
<< accum(num, num+5) / 5; // 输出3
char name[] = "templates";
int length = sizeof(name)-1;
std::cout << "the average value of the characters in \\""
<< name << "\\" is "
<< accum(name, name+length) / length;
// 输出-5 因为字符转为对应的八位ASCII编码数字进行累加,累计超过了char表示的最大数字
为了避免求和溢出,可以通过下面简单萃取的方式重新定义求和函数。
template<typename T>
struct AccumulationTraits;
template<>
struct AccumulationTraits<char>
using AccT = int;
;
template<>
struct AccumulationTraits<short>
using AccT = int;
;
template<>
struct AccumulationTraits<int>
using AccT = long;
;
template<>
struct AccumulationTraits<unsigned int>
using AccT = unsigned long;
;
template<>
struct AccumulationTraits<float>
using AccT = double;
;
template<typename T>
auto accum (T const* beg, T const* end)
using AccT = typename AccumulationTraits<T>::AccT;
AccT total; // assume this actually creates a zero value
while (beg != end)
total += *beg;
++beg;
return total;
上面例子还可能出现问题,就是初始化 total 时,有些类型可能会并不支持默认初始化。为了兼容这种情形,可采用值萃取的形式提供默认初始值。
template<>
struct AccumulationTraits<char>
using AccT = int;
static AccT const zero = 0;
;
template<>
struct AccumulationTraits<short>
using AccT = int;
static AccT const zero = 0;
;
template<>
struct AccumulationTraits<int>
using AccT = long;
static AccT const zero = 0;
;
template<typename T>
auto accum (T const* beg, T const* end)
using AccT = typename AccumulationTraits<T>::AccT;
AccT total = AccumulationTraits<T>::zero;
while (beg != end)
total += *beg;
++beg;
return total;
上面类内初始化静态数据成员的方式只对整型有效,对于 float 和字面值常量,可以通过 constexpr 定义进行类类初始化,对于非字面值的类型,则可以通过 inline 成员函数提供类内定义。
template<>
struct AccumulationTraits<char>
using AccT = int;
static constexpr AccT zero() // 通过内联函数定义初始值
return 0;
;
也可以将上述的萃取形式参数化,便于特殊情形下指定不同的萃取形式。
template<typename T, typename AT = AccumulationTraits<T>>
auto accum (T const* beg, T const* end)
typename AT::AccT total = AT::zero();
while (beg != end)
total += *beg;
++beg;
return total;
19.2 萃取 vs 策略或策略类
对于 19.1 中的 accum 函数,可以指定不同策略,不止用来求和,可以求乘。
// 求和策略
class SumPolicy
public:
template<typename T1, typename T2>
static void accumulate (T1& total, T2 const& value)
total += value;
;
// 求乘积策略
class MultPolicy
public:
template<typename T1, typename T2>
static void accumulate (T1& total, T2 const& value)
total *= value;
;
template<typename T,
typename Policy = SumPolicy,
typename Traits = AccumulationTraits<T>>
auto accum (T const* beg, T const* end)
using AccT = typename Traits::AccT;
AccT total = Traits::zero();
while (beg != end)
Policy::accumulate(total, *beg);
++beg;
return total;
萃取偏向于模板参数本质的特性,策略偏向于泛型可配置的行为。
19.3 类型函数
类型函数:接收一些类型作为参数,返回类型或常量值为结果。
#include<vector>
#include<list>
#include<iostream>
#include<typeinfo>
template<typename T>
struct ElementT; // primary template
template<typename T>
struct ElementT<std::vector<T>> // partial specialization for std::vector
using Type = T;
;
template<typename T>
struct ElementT<std::list<T>>
using Type = T;
;
// partial specialization for std::list
template<typename T, std::size_t N>
struct ElementT<T[N]>
using Type = T;
;
template<typename T>
struct ElementT<T[]>
using Type = T;
;
// 类型函数,打印容器类型T中元素的类型
template<typename T>
void printElementType (T const& c)
std::cout << "Container of "
<< typeid(typename ElementT<T>::Type).name()
<< " elements.\\n";
int main()
std::vector<bool> s;
printElementType(s); // print b
int arr[42];
printElementType(arr); // print i
萃取还可以用来为类型添加或移除引用,const,volatile 等限定符。
// 移除引用
template<typename T>
struct RemoveReferenceT
using Type = T;
;
template<typename T>
struct RemoveReferenceT<T&>
using Type = T;
;
template<typename T>
struct RemoveReferenceT<T&&>
using Type = T;
;
// 添加引用
template<typename T>
struct AddLValueReferenceT
using Type = T&;
;
template<typename T>
struct AddRValueReferenceT
using Type = T&&;
;
除了对单个类型进行萃取,也可以通过萃取对多个类型进行预测。
// 判断两个类型是否相同
template<typename T1, typename T2>
struct IsSameT
static constexpr bool value = false;
;
template<typename T>
struct IsSameT<T, T>
static constexpr bool value = true;
;
对 3 中的 same 判断可以进一步优化,以实现编译期多态。
#include <iostream>
// 定义bool模板类型
template<bool val>
struct BoolConstant
using Type = BoolConstant<val>;
static constexpr bool value = val;
;
// 定义两个实例化类型表示true 和 false
using TrueType = BoolConstant<true>;
using FalseType = BoolConstant<false>;
// 普通IsSameT模板继承FalseType
template<typename T1, typename T2>
struct IsSameT : FalseType
;
// 特化的相同类型IsSameT模板继承TrueType
template<typename T>
struct IsSameT<T, T> : TrueType
;
// 根据fool的结果调用不同的foolImpl
template<typename T>
void foolImpl(T, TrueType)
std::cout<<"true"<<std::endl;
template<typename T>
void foolImpl(T, FalseType)
std::cout<<"false"<<std::endl;
template<typename T>
void fool(T t)
foolImpl(t, IsSameT<T,int>); // IsSameT<T,int> 类型可能为TrueType或FalseType
int main()
fool(5); // print true
fool(5.0); // print false
19.4 基于 SFINAE 的萃取
可以基于 SFINAE 原理排除某些重载的函数模板。
// 实现一个能判断T是否具有默认构造函数的模板
template<typename T>
struct IsDefaultConstructibleT
private:
// 定义两个重载的函数模板,返回类型不同,用于传入IsSameT进行判断
template<typename U, typename = decltype(U())> // decltype(U()) 中U若不可以默认初始化,则替换失败,这个函数模板被淘汰,注意此处不能为T,否则会编译失败
static char test(void*);
template<typename>
static long test(...); // ...参数可以匹配任意实参
public:
// 将T传入test,如可以默认构造,则匹配第一个test,test返回类型为char,IsSameT 结果为true,否则为false
static constexpr bool value = IsSameT<decltype(test<T>(nullptr)), char>::value;
;
IsDefaultConstructibleT<int>::value //结果为 true
struct S
S() = delete;
;
IsDefaultConstructibleT<S>::value // 结果为false
可以基于 SFINAE 原理排除某些重载的类模板。
// helper to ignore any number of template parameters:
template<typename...> using VoidT = void;
// primary template:
template<typename, typename = VoidT<>>
struct IsDefaultConstructibleT : std::false_type
;
嵌入式学习笔记以及C++学习笔记更新计划
后面打算每周写一篇嵌入式学习笔记以及C++学习笔记,只有不断输出,倒逼自己成长。
嵌入式平台选用IMX6ULL开发板,先学完驱动部分,后学习应用部分,每周都要有进展,时刻最好做好笔记,这些东西太容易忘了。从头学习太浪费时间了。
C++的学习就前期比较容易一些,选黑客松的,B站资源相当不错。如果时间充裕,可以好好做做C++项目,做个贪吃蛇啥的。
对于数量,每周各更新两篇就好,本质上还是做笔记和记录学习心得,督促自己努力学习,扎实基础。
后续的一些计划:
嵌入式学习笔记(每周一篇)
C++学习笔记(每周一篇)
项目分享(每月一个)
JAVA学习笔记(C++学完后再说每周一篇)
实物展示(两个月出一个,淘宝上可以出现)
力扣刷题
一些算法介绍以及实操情况
要求就是技术点讲清楚,知识点说的明了简单,实物制作流程最好做到面面俱到。不求多而范,只求细而精。只是一些计划,要一步一步慢慢去实现,我错过了太多的时间,时不我待。
自信人生二百年
,会当水击
三千里
。
加油
,骚年
!
最后送一句我最喜欢的话给自己:
不要怂,就是干!
以上是关于C++ 学习笔记的主要内容,如果未能解决你的问题,请参考以下文章
C++ Primer 5th笔记(chap 16 模板和泛型编程)函数指针和实参推断