390 lines
16 KiB
C++
390 lines
16 KiB
C++
#include "grammar.h"
|
|
|
|
#include <iostream>
|
|
#include <vector>
|
|
|
|
// key: static 修饰全局变量
|
|
// 本来全局变量可以在同工程其他源文件访问(添加 extern),
|
|
// 添加之后仅本文件可见。
|
|
|
|
static double ConstAndStatic_sDouble = 5.77;
|
|
|
|
class CConstAndStatic_DemoA
|
|
{
|
|
public:
|
|
CConstAndStatic_DemoA() = default;
|
|
~CConstAndStatic_DemoA() = default;
|
|
|
|
public:
|
|
// Run属于类而不属于某个特定的对象
|
|
static void Run() {}
|
|
|
|
private:
|
|
// 初始化只能在类外初始化,这里仅是声明。
|
|
static int m_Code;
|
|
};
|
|
|
|
// 该变量先于对象存在
|
|
int CConstAndStatic_DemoA::m_Code = 99;
|
|
|
|
// key:修饰函数和修饰全局变量类似
|
|
static void ConstAndStatic_Function()
|
|
{
|
|
// 静态变量什么时候初始化?
|
|
// C:任何代码执行之前。
|
|
// CPP:全局或静态对象首次使用才会构造。
|
|
}
|
|
|
|
// const和static关键字
|
|
void ConstAndStatic()
|
|
{
|
|
// key: static 修饰局部变量
|
|
// static 修饰的数据会放在:静态数据区,生命周期到程序结束
|
|
// static 静态函数只能在本源文件中使用。
|
|
// 但是作用域并没有改变
|
|
static int s_Int = 56;
|
|
|
|
// K: static函数和变量受 [模块] 范围管制。
|
|
|
|
// ===const 基本类型,类型符左右都行
|
|
const int nData = 32;
|
|
//
|
|
int nCn = 55;
|
|
const int* pData = &nCn; // 值不能改变
|
|
int* const pData1 = &nCn; // 指针不能改变
|
|
|
|
// ===const 在类中的用法:
|
|
// 在对象内是常量,对类而言是可变的,因为要创建对象,所以不能在类声明中初始化。
|
|
// const 和 static 在类中是不能同时使用的,因为 const 需要具体到某个对象。
|
|
}
|
|
|
|
void basic()
|
|
{
|
|
// x.sturct 和 class 的区别
|
|
// 1.权限(包括成员权限、继承权限)
|
|
// 2.数据结构封装(struct),对象的封装(class)
|
|
// 3.class 可以用于定义模板参数,struct 不可以。
|
|
|
|
// class
|
|
// 若没有用到解引用的地方:A* a = nullptr, a->print(); 是正确的。
|
|
|
|
// new 和 malloc 区别
|
|
// x.new 是操作符,malloc 是函数。
|
|
// x.构造
|
|
// x.new 可以重载。
|
|
// x.返回值。
|
|
|
|
// 内联函数
|
|
// x.避免了函数调用开销。
|
|
// x.不允许循环语句开关语句,有就无意义。
|
|
|
|
// 传递方式:值、引用、指针。
|
|
|
|
// 重写、重载
|
|
// 重载:函数名相同,参数列表不同。使用 name mangling
|
|
// (倾轧),改变函数名,编译阶段。
|
|
|
|
// 四类构造函数:默认、初始化、拷贝、移动。
|
|
|
|
//
|
|
}
|
|
|
|
void to_binary_progress()
|
|
{
|
|
// 1.预编译、编译、汇编、链接。
|
|
// 预编译:去除define,展开宏定义,处理条件预编译,注释行号等。
|
|
// 编译:词法分析、语法分析、语义分析,代码优化和目标代码生成。
|
|
// 汇编:将汇编代码编译成机器指令。
|
|
// 链接:将目标文件链接形成可执行程序。
|
|
}
|
|
|
|
// C和C++的区别
|
|
void DiffBetweenCppAndC()
|
|
{
|
|
// 1.基本语法没有什么大的区别
|
|
// 2.C++有新增的语法和关键字,如命名空间、内存管理方式不同、auto、nullptr、引用等
|
|
// x.封装、继承、多态。
|
|
// x.四类 cast 转换。
|
|
// 3.C++有重载和虚函数概念
|
|
// 4.C++的struct可以成员函数,且有访问权限的概念。
|
|
// 5.C++有模板用于重用代码
|
|
}
|
|
|
|
void memory_use()
|
|
{
|
|
// 分配方式:
|
|
// 1.栈
|
|
// 2.堆,new 分配的。
|
|
// 3.自由存储区,malloc 分配。
|
|
// 4.全局/静态存储区,同一块内存。
|
|
// 5.常量存储区,不允许修改。
|
|
}
|
|
|
|
// C++的四个类型转换
|
|
void FourTypeConvert()
|
|
{
|
|
// 1.static_cast:
|
|
// 没有动态类型检查,上行转换(派生类->基类)安全,下⾏转换(基类->派生类)
|
|
// 不安全,所以主要执行多态的转换操作
|
|
// 2.dynamic_cast:专门于派生类之间的转换,type-id 必须是类指针,类引用或
|
|
// void*,对于下行转换是安全的,当类型不一致时,转换过来的是空指针
|
|
// 3.const_cast: 专门于 const 属性的转换,去除 const 性质,或增加 const
|
|
// 性质, 是四个转换符中唯一个可以操作常量的转换符。
|
|
// 4.reinterpret_cast:没有任何类型检查和格式转换,仅仅是简单的二进制数据拷贝。
|
|
}
|
|
|
|
// 指针和引用的区别
|
|
void PointerAndReference()
|
|
{
|
|
// 1. 都是一种内存地址的概念,指针是一个实体,引用只是一个别名
|
|
// 2.
|
|
// 引用必须且只能在定义时被绑定到一块内存上,后续不能更改,也不能为空,也没有
|
|
// const 和非 const 区别
|
|
// 3. 指针包含的内容是可以改变的,允许拷贝和赋值,有 const 和非 const
|
|
// 区别,甚⾄可以为空,sizeof 指针得到的是指针类型的大小。
|
|
}
|
|
|
|
// volatile 和 extern 关键字
|
|
void VolaAndExtern()
|
|
{
|
|
// ===<1> volatile
|
|
// 1.易变性:下一条语句不会直接使用上一条语句对应的 volatile
|
|
// 变量的寄存器内容,而是重新从内存中读取
|
|
// 2.不可优化性:不要对我这个变量进行各种激进的优化
|
|
// 3.能够保证 volatile 变量之间的顺序性,编译器不会进行乱序优化。
|
|
|
|
// ===<2> extern ⽤来说明 “此变量/函数是在别处定义的,要在此处引用
|
|
// 1.extern 会加速程序的编译过程,这样能节省时间
|
|
// 2.哪里声明才能在哪里使用
|
|
// 3.在 C++ 中 extern 还有另外一种作用,用于指示 C 或者
|
|
// C++函数的调用规范。
|
|
}
|
|
|
|
// define 和 const
|
|
void DefineAndConst()
|
|
{
|
|
// 1.define 以立即数的方式保留了多份数据的拷贝
|
|
// 2.const 是在编译期间进行处理的,const
|
|
// 有类型,也有类型检查,程序运行时系统会为 const 常量
|
|
// 分配内存,而且从汇编的角度讲,const
|
|
// 常量在出现的地方保留的是真正数据的内存地址,只保留了一份数据的
|
|
// 拷贝,省去了不必要的内存空间。而且,有时编译器不会为普通的 const
|
|
// 常量分配内存,而是直接将 const 常量添
|
|
// 加到符号表中,省去了读取和写入内存的操作,效率更高。
|
|
}
|
|
|
|
class CalcClassSize_A
|
|
{
|
|
};
|
|
|
|
class CalcClassSize_B
|
|
{
|
|
public:
|
|
virtual void fun() {}
|
|
};
|
|
|
|
class CalcClassSize_C
|
|
{
|
|
public:
|
|
static int m_nA;
|
|
};
|
|
|
|
class CalcClassSize_D
|
|
{
|
|
public:
|
|
int m_nA;
|
|
};
|
|
|
|
// 计算类的大小
|
|
void CalcClassSize()
|
|
{
|
|
// 空类在实例化时得到一个独一无二的地址,所以为 1
|
|
std::cout << "CalcClassSize_A Size: " << sizeof(CalcClassSize_A) << "\n";
|
|
// 当 C++ 类中有虚函数的时候,会有一个指向虚函数表的指针(vptr)
|
|
std::cout << "CalcClassSize_B Size: " << sizeof(CalcClassSize_B) << "\n";
|
|
//
|
|
std::cout << "CalcClassSize_C Size: " << sizeof(CalcClassSize_C) << "\n";
|
|
//
|
|
std::cout << "CalcClassSize_D Size: " << sizeof(CalcClassSize_D) << "\n";
|
|
}
|
|
|
|
void class_relate()
|
|
{
|
|
// 构造函数为什么不能是虚函数
|
|
// 1.虚函数只需要知道函数接口而不需要知道对象的具体类型,但是如果使用构造创建对象的话,则必须知道对象
|
|
// 的完整信息,特别是具体类型,如果说构造函数是虚的话,那么虚函数表指针则不存在,也是违反了先实例化
|
|
// 后调用的规则(虚表在实例化后才存在)。
|
|
|
|
// 虚函数表是一个存储虚函数地址的数组,以NULL结尾,在编译阶段生成,位于程序数据段,内存开辟后,写入对象的vfptr,再调用构造函数。
|
|
}
|
|
|
|
// 面向对象的三大特性、封装,继承、多态
|
|
void EncapInheriAndPolyMorphism()
|
|
{
|
|
// 封装:一个类就是一个封装了数据以及操作这些数据的代码的逻辑实体,将客观事物封装成抽象的类
|
|
// 继承:可以让某个类型的对象获得另一个类型的对象的属性的方法。
|
|
// 即一个接口,可以实现多种方法。
|
|
// 多态:同一个行为具有多个不同表现形式或形态的能力,通常通过继承和接口实现。
|
|
|
|
// Note: 多态的访问权限以基类为主。
|
|
}
|
|
|
|
// lambda 表达式
|
|
void Lambda()
|
|
{
|
|
// 1.相当于匿名函数
|
|
// 2.lamdba 表达式产生的是函数对象。
|
|
// 3.在类中,可以重载函数调用运算符(),此时类的对象可以将具有
|
|
// 类似函数的行为,我们称这些对象为函数对象(Function
|
|
// Object)或者仿函数(Functor)。
|
|
|
|
// 适配器:将某些已经存在的东西进行限制或者组合变成一个新的东西,这个新的东西体现一些新的特性,但底层都是由一些已经存在的东西实现的。
|
|
|
|
auto add = []() -> int { return 33; };
|
|
|
|
// 最前边的 [] 是 lambda 表达式的一个很重要的功能,就是 闭包。
|
|
// 大致原理:每当你定义一个 lambda
|
|
// 表达式后,编译器会自动生成一个匿名类(这个类当然重载了()运算符)
|
|
// 我们称为闭包类型(closure type)。那么在运行时,这个 lambda
|
|
// 表达式就会返回一个匿名的闭包实例,其实是一个右值。
|
|
|
|
// 闭包的一个强大之处是其可以通过传值或者引用的方式捕捉其封装作用域内的变量,前面的方括号就是用来定义捕
|
|
// 捉模式以及变量,我们⼜将其称为 lambda 捕捉块。
|
|
// lambda
|
|
// 表达式一个更加要的应用是其可以用于函数的参数,通过这种方式可以实现回调函数。
|
|
}
|
|
|
|
// auto 和 decltype
|
|
void AutoAndDecltype()
|
|
{
|
|
// C++ 提供了 auto 和 decltype 来静态推导类型
|
|
// Decltype: 用于获取一个表达式的类型,而不对表达式进行求值(类似于 sizeof
|
|
// )。 decltyp(e) 规则如下 若 e
|
|
// 为一个无括号的变量、函数参数、类成员,则返回类型为该变量 / 参数 /
|
|
// 类成员在源程序中的声明类型;
|
|
// 否则的话,根据表达式的值分类(value categories),设设 T 为 e 的类型:
|
|
// 的类型:
|
|
// 若 e 是一个左值(lvalue,即“可寻址值”),返回 T& ;
|
|
// 若 e 是一个临终值(xvalue),则返回值为 T&& ;
|
|
// 若 e 是一个纯右值(prvalue),则返回值为 T 。
|
|
|
|
const std::vector<int> v(1);
|
|
const int&& foo(); // 返回临终值:生命周期已结束但内存还未拿走
|
|
auto a = v[0]; // a 为 int
|
|
decltype(v[0]) b =
|
|
0; // b 为 const int&
|
|
// 即 vector<int>::operator[](size_type) const 的返回值类型
|
|
auto c = 0; // c, d 均为 int
|
|
auto d = c;
|
|
decltype(c) e; // e 为 int,即 c 的类型
|
|
decltype((c)) f = e; // f 为 int&,因为 c 是左值
|
|
decltype(0) g{}; // g 为 int,因为 0 是右值
|
|
}
|
|
|
|
// 静态 assert
|
|
void StaticAssert()
|
|
{
|
|
// C++ 提供了两种方式来 assert :一种是 assert 宏,另一种是预处理指令
|
|
// #error 。 前者在运行期起作用,而后
|
|
// 者是预处理期起作用。它们对模板都不好使,因为模板是编译期的概念。
|
|
// static_assert 关键字的使用方式如下:
|
|
}
|
|
|
|
template <class T> struct Check {
|
|
static_assert(sizeof(int) <= sizeof(T), "T is not big enough!");
|
|
};
|
|
|
|
// 类型推断和转发
|
|
void TypeInferenceAndForwarding() {}
|
|
|
|
// 一、模板实参推断
|
|
|
|
// 一个完美转发可以理解为:1.一般推断 + 2.引用折叠
|
|
|
|
// 1.函数调用进行类型推断
|
|
template <typename T> void functionA(T& Val) {} // 实参必须是一个左值
|
|
template <typename T> void functionB(const T& Val) {} // 实参可以接受一个右值
|
|
template <typename T> void functionC(T&& Val) {}
|
|
void DemoA()
|
|
{
|
|
// Note: functionA 中的调用使用实参所引起的类型作为模板参数的类型
|
|
int i = 0;
|
|
const int ci = 1;
|
|
functionA(i); // OK, i: int, T: int
|
|
functionA(ci); // OK, i: const int, T: const int
|
|
// functionA(5); // ERR, 必须是一个左值
|
|
}
|
|
static void DemoB()
|
|
{
|
|
// Note: functionB 的参数是 const &,与实参中的 const 无关。
|
|
// Note: functionB 函数参数都会被推断为 const int&
|
|
int i = 0;
|
|
const int ci = 1;
|
|
functionB(i); // OK, i: int, T: int
|
|
functionB(ci); // OK, i: const int,
|
|
// T: int
|
|
// (当函数参数本身是const时,T的类型推断的结果不会是一个const类型,const已经是函数参数类型的一部分)
|
|
functionB(5); // OK, 一个const &参数可以绑定到一个右值, T: int
|
|
}
|
|
static void DemoC()
|
|
{
|
|
functionC(78); // OK, 实参是一个 int 类型的右值,T: int
|
|
}
|
|
|
|
// 2. 引用折叠和右值引用参数
|
|
// Note:
|
|
// 不能将一个右值引用绑定到一个左值上。但是,C++语言在正常绑定规则之外定义了【两个例外规则】,允许这种绑定。
|
|
// Exception 1: 第一个例外规则影响右值引用参数的推断如何进行。
|
|
// 将一个【左值】(传递)-->
|
|
// 函数的右值引用参数【且】此右值引用指向模板类型参数,则模板类型 (推断)-->
|
|
// 实参的左值引用类型 故当调用 functionC(i)
|
|
// 时,T 为 int& 而非 int. 看起来就像是传递了: int& && 即 int&
|
|
// 的右值引用。
|
|
// 通常,我们不能(直接)定义一个引用的引用。但是,通过【类型别名】或通过【模板类型参数】间接定义是可以的。
|
|
// Exception 2: 上述中的【间接】定义的引用的引用,则形成【折叠】
|
|
// 折叠就只有两种情况:
|
|
// ①只有右值引用右值引用会折叠成一个右值引用,即T&&
|
|
//&& => T&& 其他均为 T& ②其他:T&,例如:T&&, T&&&
|
|
|
|
// Example: 将 1 的特殊推断和 2 的折叠结合起来则可以对一个左值调用
|
|
// functionC
|
|
// functionC(i) --> Line 41 --> T: int&
|
|
// 结合折叠规则 T& && ==> T& 推出结果为 int& 因此:即使 functionC
|
|
// 的函数参数形式是一个右值引用(即,T&&),此调用也会用一个左值引用类型(即,int&)实例化
|
|
// functionC
|
|
|
|
// 3.上述两个规则将导致两个重要结果:
|
|
// Result 1:
|
|
// 如果一个函数参数是一个指向模板类型参数的右值引用(如,T&&),则它【可以】被绑定到一个【左值】
|
|
// Result 2: 可以将任意类型的实参传递给T&&类型的函数参数。
|
|
|
|
// 4. 在实际中,右值引用通常用于两种情况:(模板转发其实参①)或(模板被重载②)。
|
|
|
|
// 二、std::move
|
|
|
|
// 从一个左值static_cast到一个右值引用是允许的。这是一条针对右值引用的特许规则:
|
|
// Notice:
|
|
// 不能隐式地将一个左值转换为右值引用,但我们可以用static_cast显式地将一个左值转换为一个右值引用。
|
|
|
|
// 三、转发
|
|
|
|
// 转发目的:某些函数需要将其一个或多个实参【连同类型不变地】转发给其他函数。
|
|
// (在此情况下,我们需要保持被转发实参的所有性质,包括实参类型是否是const的以及实参是左值还是右值。)
|
|
|
|
// 引出疑问:将模板参数设定为 T&& ,不就既可以接受 T& 也可以接受 T&&
|
|
// 吗,为什么还需要转发?
|
|
// 问题原因:上述规则只在模板推断时起作用,但是如果说调用一个函数,函数参数即便是
|
|
// T&& 类型,实际上也会变成左值,
|
|
// 因为:【函数参数与其他任何变量一样,都是左值表达式!】yyy
|
|
// 所以:将模板参数设定为
|
|
// T&&只能解决一半的问题,还需要别的方法就是【转发】
|
|
|
|
// 我们可以使用一个名为forward的新标准库设施来传递flip2的参数,它能保持原始实参的类型,类似move。
|
|
// 与move不同,forward必须通过显式模板实参来调用。
|
|
// forward返回该显式实参类型的右值引用。即,forward<T>的返回类型是T&&。
|
|
|
|
// 模板相关
|
|
void template_relate()
|
|
{
|
|
// 1.类模板,顾名思义,类的模板。模板类,是一个类,实例化的类。
|
|
} |