(这里要用到的符号、变量名、标识符、名称等都是同一个意思,这里还是统一用名称)
-
定义和声明 One Defination Rule,定义只有一次。(内联的情况特殊点但是也只选一个定义用) 声明有多个,并且具有外部链接的声明将把名称的作用域扩展到不含定义的文件。
-
内部链接、外部链接、无链接 链接,其实是编译单元层次上的可见性。无链接的名称、具有内部链接的名称,意味着它们是编译单元封装的部分;外部链接名称是对其他编译单元暴露的部分,可以认为是接口或者用于交互的名称。 从作用域的角度看,有链接的名称包括:(后文称作全局名称)
-
通常所说的全局变量。
-
函数的名称(刨掉了通过函数指针、
std::function等方式实现的奇怪玩意)。 -
自定义类型名称(自定义类型的名称和自定义的类型名称)。
-
类作用域下的,满足上面三条的名称。
(这意味着实在是很多……类模板、函数模板当作类、函数处理,因为它们可以看作是类/函数家族)
-
具有内部链接的名称: (特点是通常写法会将声明定义写成一句)
//xx.h static int i = 1; static int func(){...}; namespace name{ static int i = 1; static int func(){...}; } //具有内部链接 namespace { int i = 1; int func(){...}; typedef int Int; using Int = int; class MyClass{...}; } //匿名名字空间起到代替static的作用。 //而且在其中的其他全局名称也将会是内部链接。 -
具有外部链接的名称: (可以看出,由于类通常要对外暴露,类的全局名称都默认有外部链接)
int i = 1;//extern int i = 1;是建议的写法 typedef ... using ... class MyClass{ static int i; static int func(){...}; int func2(){...}; }; namespace name{ extern int i = 1; class MyClass{...}; } -
类外定义与inline 类外定义的问题导致了编程的一些特例,并且类外定义不用再static,因为在声明中已经干了一次。 在编写类时:
-
由于非静态成员变量是无链接的,所以不能类外定义。
-
成员函数习惯性类外定义,但都可以用inline显式免除类外定义或直接写出函数体隐式免除。
-
静态成员变量要求类外定义,格式和成员函数类似,但是它免除类外定义的方式进化了几个c++标准的版本。
//.h class MyClass{ static int i; inline static int si2 = 2; int func(){...} inline int func2(); static int sfunc3(){...} inline static int sfunc4() }; //.c int MyClass::i = 1; int MyClass::func2(){...}; int MyClass::sfunc4(){...}; //类外定义,但是后两个由于inline免除了类外定义。事实上通常学到最早的inline作用是编译时插入代码减少函数调用带来的开销,而这里的免除类外定义事实上伴随了内联。(你不能免除类外定义但不内联)
-
-
const和constexpr 常量其实处理方式和普通类型接近一样,在实际编程时避免那些会造成不一样的语法就好了。 constexpr是编译时常量,这意味着它可以作非类型模板实参。 成员常量的主要问题是必须要初始化,这要么在初始化列表中,要么在定义中;constexpr和静态成员常量只能在定义中,并且类内constexpr对象不用static一定是有病的。
-
-
符号表 这是c++管理名称的方式,也是管理名称的数据结构。(符号也是名称) 一个符号表仅针对一个编译单元,而一个编译单元是由.c和它包含的.h编译成的,所以不涉及内外链接,因为预编译器会展开包含的文件。 编译时,解析到一个声明,该声明就会加入符号表。而在非声明语句中扫描到标识符时,就会在符号表中查找。 标记的结果可以是类型、非类型、模板id等,随上下文的符号不同而结果不同。 (符号标记+解析是一般编译器的编译步骤) (c++是上下文相关,所以同一符号会因上下文不同而有不同解析,即得到不同的符号标记)
-
作用域: 全局作用域(这个名词和前面的全局名称撞车,代表的含义完全不同了)、命名空间作用域、类作用域、函数作用域、块作用域。根据大括号分隔分别对应物理空间的文件下、命名空间体、类体、函数体、代码块。 (函数作用域和块作用域合称局部作用域,这个局部和前文还是能对应的)
-
作用域规则: (这部分不讨论函数重载) (限定查找和受限查找一样)
-
非限定查找时使用作用域规则,按物理空间的大括号顺序进行。自由名称会从块->函数->类(如果有)->全局中查找,成员名称则限定在类作用域及以下查找。 (成员名称相当于使用了类空间限定符+this指针,其实是限定查找) 这使得在更近的位置定义同名变量会隐藏远的变量(overwrite/hide)。
-
限定查找不会使用作用域规则。 作用域限定符:
-
全局空间限定::
-
类空间限定MyClass::
-
名字空间限定name::
(像其他语言一样类和名字空间不用同一个符号将增强可读性但提高编写难度)
-
-
-
-
名称 (主要目的是看懂依赖名称)
-
标识符(Identifier)相关 标识符(id,变量名、函数名、自定义类型名)、运算符id、类型转换函数id、模板id合称非受限id,含义事实上是广义标识符。 用上面作用域限定符限定一个非受限id得到受限id。 被受限查找的名称称为受限名称,要求必须是显式使用了作用域限定符或成员访问符的名称。
S::X//受限id S::X,this->f,p->A::m//受限名称被非受限查找的名称称为非受限名称。 (当我们使用id说法时是不考虑成员访问符的)
-
非依赖名称和依赖名称 依赖名称的确认需要满足四者之一:
- 名称显式依赖模板参数,对于变量名,其类型是依赖名称时,它是依赖名称
- 访问运算符左侧表达式的类型是依赖名称,则整个受限名称是依赖名称
this->b在类模板中是依赖名称- 函数的形参类型是依赖名称,则函数名是依赖名称,查找范围扩展到形参的名字空间作用域和类作用域
(ADL这个概念很重要但是也很不容易在上面犯错所以略过)
Two-phase lookup,两阶段查找;Argument-dependent lookup,参数依赖查找。 符号表查找分为两个阶段: 第一阶段在模板定义时,非依赖名称将被解析,此时查找名称的范围在类模板作用域。 第二阶段在模板实例化时,依赖名称将被解析,此时查找名称的范围在模板类作用域。 (AI回答的依托还是靠书解决的)
-
-
解析模板问题 主要是由于编译时容易发生歧义而出现的一些规定。
-
依赖型类型名称(依赖名称作类型名称)
//Temp<T>::x可能是变量名或类型名,这在第一阶段不能确定,但是第一阶段要完成符号标记,所以要显式声明 Temp<T>::x//是一个变量名称 typename Temp<T>::X//是一个类型名称这种功能的typename要求使用在:
-
模板中
-
受限id
-
不可用于成员初始化列表和基类继承列表
-
名称依赖于模板参数
-
-
依赖型模板名称(依赖名称作模板名称) 限定符号的概念,定义为
::、->、.三个。//Shell<T>::In<T>中,可能会将Shell<T>::In作为一个变量,再把<>作为比较运算符。 Shell<T>::template In<T>//显式限定是成员模板。 p->template In<T>//当p是依赖类型的指针时 p.template In<T>//当p是依赖类型变量或引用时 -
using(依赖名称被using引入) using可以创建一个有链接的名称,链接到另一作用域的一个名称,起到扩展作用域的作用,称为using-declaration。 (通称引入) using引入名称在高版本更加神奇了。可以选择设置或不设置别名,但是都需要在应该的地方使用typename和template。 还可以达成修改访问范围符的作用。 别名模板的特性也很重要,是类型萃取(模板元编程)的重要工具。
using T::template Magic;//引入成员模板本身 Magic<int> ...//扩展作用域 using Alias = typename T::template Type<int>;//别名 template<typename T> using Type = std::pair<T,int>;//别名模板 Type<double> ...//使用别名模板可以用一种符合自然语言的方式说明typename和template的作用:提示编译器它后面的东西是类型名或模板名,因为依赖名称用在限定符号左侧时,右侧的名称不能确定其情况,默认解析为变量名称。
-
调用依赖基类的成员 要求用指向成员的依赖名称来调用成员,因为第一阶段非依赖查找中,查找不到依赖基类的成员。 从语义上理解可以知道,依赖基类的成员在实例化前是不能确定的。 前面三个问题中也会发生查找不到成员的情况,但也是在实例化后才能判断依赖名称是否存在该成员。
baseFunc();//可能会查找到外围作用域的作用域,包含(外部类作用域、)命名空间作用域、全局作用域等 this->baseFunc(); Base<T>::baseFunc(); using Base<T>::baseFunc; baseFunc();非依赖名称尝试调用依赖名称的问题似乎只有这一个原因。
-
-
SFINAE Substitution is not an error。(今天最抽象的东西) 这其实是另一类解析模板问题,不过这类问题的核心在于“鸭子类型”相关,与3.4同类。(所以书里面把3.4拆进前面讲也不好,这点ai又赢了) 如果你尝试在类模板成员函数中调用依赖类型成员变量,这并不会产生错误,因为这个变量名称本身能在第一阶段被标记为变量。 但是调用这个依赖类型变量的成员不一样,它的实例化可以产生任何成员,因此只要有实现了所有受限名称的类型模板实参,这个成员函数就能正常编译;否则就会发生“不存在”编译错误。 依赖基类其实就是一种依赖类型成员变量,所以调用依赖基类成员从原理上是SFINAE内容,但是表现形式又是一种特殊的语法或者非依赖名称查找失败,所以介于之间。
template<typename T> class MyVec{ public: std::vector<T> v; void func(){ v.empty(); } }//当T使std::vector<T>.empty()不存在时出错模板代码只要存在一种合法的实例化可能,就不应该引发错误。错误应当发生于实例化。
-
有涉及模板元编程的信息
<type_traits>类型萃取、类型特征标头,<concepts>概念、概念约束标头,static_assert()静态断言,特化但不给出代码体的方式。虚函数不能是模板,所以依靠多态实现类型擦除和还原是不行的。
(逻辑结构方面应该差不多了,后面是物理结构方面的)
comment 评论区
star_outline 咱快来抢个沙发吧!