C++知识点汇总文档
- 持续更新
- 朝花夕拾. 不写下来, 仿佛一切都没了见证.
- 编程来源于生活, 无非是对现实的抽象.
- 整理这份文档的时候我才发现, 自己对C++的了解无非是冰山一角
- 另, C++11真的太恐怖了, 新"特性"真的是"恐怖如斯"
- 另注: 本人才疏学浅, 难免有错漏之处, 还望不吝赐教
1.基本概念
内存管理
- C++中的内存划分(内容来自博客):
- 堆
- 由用户使用new delete关键字管理的内存区域
- 栈
- 栈中用来存放临时变量, 比如函数中的局部变量, 在代码块结束时会自动清除
- 自由存储区
- 由malloc等分配的内存块, 他和堆是十分相似的, 不过它是用free来结束自己的生命的
- 全局/静态存储区
- 全局变量和静态变量被分配到同一块内存中
- 常量存储区
- 比较特殊的存储区, 他们里面存放的是常量(不太了解, 有空扫下盲)
- 堆
- new 关键字
- 用来在内存(堆)中开辟一块空间, 用户获得一个指向内存中目标位置的指针
用法参考示例
//申请一块指向单个int对象的内存, 并将值初始化为"5" int *ptr_0 = new int(5); //申请一块指向5个连续int类型对象的内存 //使用C++11统一的花括号初始化, 直观地对内存进行初始化 int * ptr_1 = new int[5]{1,2,3,4,5};
- delete 关键字
- 删除使用new申请的内存
用法参考示例
//删除之前申请的内存 delete ptr_0; //删除数组的写法, 注意 delete [] ptr_1;x
左值与右值
- 左值
- 左值一般来讲就是变量(能被赋值的), "能出现在等号(赋值号)左边的"都是左值
- 可以取地址的, 有名字的, 非临时的就是左值
- 右值
- "出现在等号右边的临时值"成为右值, 通常情况下C++会产生一个临时变量来存储右值表达式的结果
- 不能取地址的, 没有名字的, 临时的就是右值
从本质上理解, 创建和销毁由编译器幕后控制, 程序员只能确保在本行代码有效的, 就是右值(包括立即数);
而用户创建的, 通过作用域规则可知其生存期的, 就是左值(包括函数返回的局部变量的引用以及const对象).
摘自博客
左值右值参考示例
int a{}; //下式中a是左值, 5+1会产生一个临时变量6, 是右值 a = 5+1;
引用
- 左值引用
- 左值引用是对左值的引用, 类似于为变量起一个别名
- 常引用
- 在左值引用时使用const修饰则会成为一个常引用
- 常引用在引用左值时, 除不能修改引用对象的值以外, 和普通左值引用相同
- 常引用在引用右值时(如常量), 会生成一个临时变量存储目标值
左值引用参考示例
int var = 1; int &ref = var; // 定义一个左值引用变量 ref = 2; // 通过左值引用修改引用内存的值
//以下语句会报错, 10不是一个合法的左值 //int &ref0 = 10; //用const修饰, 常引用, 引用常量 const int &ref0 = 10; //等价于下面两行代码 const int temp = 10; const int &ref1 = temp;
- 右值引用
- 右值引用为C++11中引入的新概念
- 右值引用的存在是为了减少对象的构造和析构操作, 从而提升效率
- 底层汇编实现与常引用引用右值时的情况相同, 都使用一个临时变量存储目标值, 不同的是右值引用可以修改引用的值
右值引用参考示例
//右值引用 int && rref = 10;
右值引用实际应用参考
存在如下类定义
class C { public: //获得类C的一个实例 static C getObject() { return C(); } };
//这一行会调用类C的复制构造函数, 参数为getObject()函数产生的临时对象 //其后临时对象会调用析构函数销毁 C c = C::getObject(); //右值引用避免了一次中间临时变量的构造与析构过程 //引用直接指向函数返回时构造的对象 C &&rref = C::getObject();
一些技巧
- 当函数返回的内容在函数体外仍然存在的时候(通常是左值), 可以考虑返回引用以提升效率
- 而函数返回的内容在函数体外不能继续存在的时候(比如函数体中的局部变量, 或右值), 则可以在接受返回值时使用右值引用以提升效率
- 引用折叠
- 首先, C++中禁止"引用的引用", 即"reference to reference", 也即不允许引用(动词)一个引用(名词)
- 当多个引用嵌套时, 采用如下规则折叠(塌缩)引用
- 所有右值引用折叠到右值引用上仍然是一个右值引用(A&& && -> A&&)
- 所有的其他引用类型之间的折叠都将变成左值引用(A& & 变成 A&; A& && -> A&; A&& & -> A&)
- 引用嵌套中出现左值引用则塌缩为左值引用
- 引用嵌套中全为右值引用则塌缩为右值引用
lambda表达式
- lambda表达式为C++11中引进的新内容
- lambda表达式用于定义和使用匿名函数, 可以部分简化编程工作. 但主要用于提高程序可读性, 使程序更加简洁
lambda表达式的语法形式
[捕获列表] (参数列表) 修饰信息 -> 返回值类型 {函数体} [capture list] (params list) mutable exception-> return-type {function-body}
- 捕获列表
- 不可省略, 捕获列表标志着一个lambda表达式的开始
- 如果捕获字段为[], 则lambda表达式不能访问任何外部变量
- 字段内可以写入"&"或"="符号来控制访问方式
- & 表示按引用访问
- = 表示按值访问
- 参数列表
- asdf
- 修饰信息
- mutable(可选项): 指示lambda表达式是否能够修改捕获到的变量
- exception(可选项):
- 返回值类型
- 可以省略
- 当省略返回类型时, 编译器自动推导类型
- 函数体
- 匿名函数要执行的语句
- 捕获列表
面向对象
- class 关键字
- class 关键字用以声明一个类
- 访问控制
- 三种访问控制
- public
- 公有. 所有位置均可访问(包括成员函数, 派生类的成员函数, 非成员函数, 本类或派生类的友元)
- protected
- 保护. 只有成员函数, 派生类的成员函数, 派生类的友元, 本类的友元可以访问
- private
- 私有. 只有成员函数, 本类的友元可以访问
- public
- 友元
- 友元机制提供了一种方案, 它让普通的函数也能访问 类的成员函数能够访问的所有内容
- 友元的访问权限与成员函数相同. 即: 成员函数可以访问的内容, 友元也都可以访问
- 需要注意的是, 这里指的"成员函数"和"友元函数"是指同一类中的成员函数和友元函数
- 举个例子, 派生类的友元是无法访问基类的private成员的. 因为派生类的成员函数也不能访问基类的private成员
访问控制示例
class C { friend void fun0(C& c);//友元函数声明 public: int pub;//公有成员 protected: int pro;//保护成员 private: int pri;//私有成员 }; void fun0(C& c) { c.pub; c.pro; c.pri;//没问题, 可以访问 } class C_plus:public C//公有继承, 基类成员可见性不变 { friend void fun2(C_plus &c);//友元函数声明 void fun1() { this->pub; this->pro; //this->pri;//报错, 不可访问 } }; void fun2(C_plus& c) { c.pub; c.pro; //c.pri;//报错, 不可访问 }
- 三种访问控制
- 默认的成员函数(如果不手动实现, 编译器会自动实现)
- 构造函数
- 当一个新的对象产生的时候调用的函数
- 构造函数都没有返回值, 也不需要指定void
典型实现
//"C"为类名 C();//一个无参构造函数 { //do something } C(int a, int b)//一个接受两个int值的构造函数 { //do something }
- 析构函数
- 当一个对象消亡(析构)的时候调用的函数
典型实现
//"C"为类名 ~C() { //do something //通常情况下这里会有一些代码用来delete在构造函数中new出的内存指针 }
- 复制构造函数(拷贝构造函数)
- 当使用一个已有的对象创建(初始化)一个新的对象的时候调用的函数
- 复制构造函数只有一个本类对象的引用作为参数(如果不使用引用会导致无限递归)
- 复制构造函数的行为涉及到深浅复制的问题
- 浅复制: 无论成员变量类型为何, 只将成员变量的值复制
- 深复制: 如果成员变量是一个指针, 则通常不能简单地复制指针的值, 而应该申请新的内存块, 并将得到的指针赋给新的指针类型成员变量
典型实现
//"C"为类名 C(const C & another)//必须使用引用作为参数; const可加可不加, 但建议加上 { //do something }
- 转移构造函数(C++11)
- 当一个已有的对象的内容转移到一个新产生的对象中时调用的函数(一般来说原对象应该要失效)
- 转移构造函数在针对指针类型的成员变量是通常采取浅复制, 并将被转移的对象的指针成员设为nullptr
典型实现
//"C"为类名 C(C && another) { //do something }
- 赋值函数(赋值操作符重载)
- 当使用一个已有的对象对另一个已有的对象进行赋值时调用的函数
典型实现
//"C"为类名 C& operator=(const C &another) { //do something return * this; }
- 转移赋值函数(赋值操作符重载)(C++11)
- 当一个已有的对象的内容转移到另一个已有的对象中时调用的函数(一般来说原对象应该要失效)
- 取地址操作符重载
- 用于对象的取地址操作, 一般不需要自行实现
典型实现
//"C"为类名 C* operator&() { return this;//直接将地址返回 }
- const取地址操作符重载
- 用于对象的取const地址操作, 一般不需要自行实现
典型实现
//"C"为类名 const C* operator&() const { return this;//直接将地址返回 }
- 构造函数
- 类型转换构造函数
- 类型转换构造函数用于将一个其他类型的对象转换为一个类对象
- 当需要一个类对象, 却提供了一个可以转换为类对象的其他类型的对象时, 自动调用对应的类型转换构造函数进行转换
典型实现
class C { public: int data;//成员变量 //类型转换构造函数, 将int类型的值转换为类对象 /*explicit */C(const int &_data) { this->data = _data; } }
- explicit关键字
- 有时候, 存在只有一个参数的构造函数, 或存在一个虽然有不止一个参数但除了第一个参数外其余参数都有默认值的构造函数, 但不希望其成为一个类型转换构造函数, 可以使用explicit关键字进行修饰, 它指示该构造函数不是隐式的, 不会自动隐式调用.(注: 用explicit关键字修饰除上述构造函数之外的函数没有意义)
类型转换函数
C++中, 类型的名字也是一种运算符, 即强制类型转换运算符
- 主要用于隐式转换, 当需要一个目标类型对象, 却提供了一个类对象时, 自动调用对应的类型转换函数进行转换
典型实现
class C { private: int data; public: //转换函数(转换为int类型), 在需求int类型时传递C类的对象会自动调用 operator int(){ return data; } }
- 在成员函数声明末尾添加const
- 该操作为成员函数的参数列表中隐含的参数this指针添加const修饰
即: 不允许修改this指针指向的对象; 不允许调用对象的非const成员函数
//类成员函数 void fun()const { //do something } //注:并没有下面这种写法, 只是本质上等价于下面的代码 //"C"为类名 //注意这里const是修饰类型C的 //这意味着this指针指向的内容被看作常量 void fun(const C * this) { //do something }
- 继承
- 继承表示了一种"is-A"关系. 即, 派生类是一种(特殊的)基类.
- 派生类(子类)继承了基类(父类)的所有成员
- 三种继承方式
- public (可见性最高, 内外皆可见)
- 公有继承, 基类的成员在派生类中保持不变(保持在基类中声明的可见性)("成员"包括成员变量和成员函数, 又称字段和方法)
- protected
- 保护继承, 基类的成员中, 可见性高于protested的, 在派生类中变为protested
- private (可见性最低, 对外不可见, 对派生类也不可见)
- 私有继承, 基类的成员在派生类中可见性变为private
- public (可见性最高, 内外皆可见)
- 简言之, 比继承方式可见性高的成员, 在派生类中可见性被减弱为继承方式的可见性
- 成员函数的重载, 重写, 隐藏(这几点很重要, 是C++面向对象中继承的核心)
- overload(重载)
- override(重写/覆盖)
- redefining(重定义/隐藏)
- 多继承
- 多继承太麻烦, 说实话也没真正写过代码, 很难保证吃透了. 略过
多态
- 多态, 指一种事物的多种形态
- 静态多态, 编译时完成
- 函数重载
- 通过对比函数调用与不同的函数原型推断具体调用的函数版本
- 泛型
- 对实现了统一接口(这里的接口指同一套操作方案)的不同对象使用同一套模板, 提高代码重用性(鸭子类型)
- 函数重载
- 动态多态
- 面向对象中的多态
- 这一节主要是指面向对象中的多态
- 静态多态, 编译时完成
- virtual关键字
- 使用virtual关键字修饰的成员函数称为"虚函数"
泛型
- C++通过使用模板机制来支持泛型编程
- 共有两种模板类型
- 函数模板
- 类模板
- 函数模板
- 未完成待续
move语义
异常处理
2.重要关键字或操作符
decltype
- decltype是一个关键字, 用于捕获对象的类型
- 对于左值与右值有着不同的表现
- 若decltype的目标类型是(被推断为)左值, 则结果为引用类型
- 若decltype的目标类型是(被推断为)右值, 则结果不为引用类型
- 由C++11引入
typeid
- typeid是一个操作符, 用于返回一个对象的类型信息对象
- 使用typeid后将产生一个type_info类型的对象(C++中唯一创建该对象的方法)
- 当typeid操作符的操作数是不带有虚函数的类类型时, typeid操作符会指出操作数的声明类型, 而不是底层对象的类型
- 个人的理解是: 如果操作数的声明类类型中没有任何虚函数, 那么调用该操作数的所有成员函数都将使用其声明类型版本的实现, 那么返回其底层具体类型将没有任何意义
- 当没有函数需要设为虚的时, 为达到目的可以将析构函数设为虚的. 事实上, 存在派生类的基类的析构函数必须设为虚的, 否则可能因为不调用派生类的析构函数而不能完全释放资源
//示例1 int a{}; //下面这一行语句的输出是"int" cout << typeid(a).name();
存在如下类定义
class C0 { }; class C0_plus:public C0 { }; class C1 { protected: //虚函数 virtual ~C1() { } }; class C1_plus :public C1 { virtual ~C1_plus() { } };
//示例2 C0* p0 = new C0(); //下面的语句输出"class C0" cout << typeid(*p0).name() << endl; C0* p1 = new C0_plus(); //下面的语句输出"class C0" cout << typeid(*p1).name() << endl; //--------------------- C1* p3 = new C1(); //下面的语句输出"class C1" cout << typeid(*p3).name() << endl; C1* p4 = new C1_plus(); //下面的语句输出"class C1_plus" cout << typeid(*p4).name() << endl;
auto
- auto关键字早在C++98时就已经存在. 当时用来指示一个变量具有自动的生命周期, 可是一个局部变量总是默认拥有自动生命周期, 因此auto很少被使用, 并且在当时是多余的
C++11标准删除了auto关键字的传统用法. 现在, auto关键字用于自动类型推断
题外话
显而易见, auto执行的类型推断是在编译时完成的,不会对运行效率有任何影响.
同时, C++编译器在编译时本身也需要判断声明类型和实际类型是否匹配, 因此应该也不会影响编译速度 (就算影响了也不差这点, 扯远了)- C++14后, lambda表达式中的参数捕获也能使用auto
auto类型推导可以用于模板, 用以省略模板参数类型的衍生类型在模板参数列表中的声明
本例摘自博客//传统写法, 需要专门为makeObject()的返回值类型在模板的参数列表中添加一项 template <typename Product, typename Creator> void processProduct(const Creator& creator) { Product* val = creator.makeObject(); // do somthing with val } //使用auto的写法, 得到一定简化 template <typename Creator> void processProduct(const Creator& creator) { auto val = creator.makeObject(); // do somthing with val }
const
class
typename
explicit
delete
using
传统上using关键字用于导入命名空间中的全部或者部分内容
比如:using namespace std;//导入std命名空间中的所有内容 using std::cout;//只导入命名空间std中的cout
但在新的C++中, using可以用于取代typedef
虽然功能相同但(似乎)更加直观//这一条语句为std::string取了一个别名"stdstring" using stdstring = std::string;
可以用在类声明中
class C { public: using uss = std::string; typedef std::string tss; };
还有个什么简化声明父类中成员函数的功能, 不是很了解, 到时候再写
mutable
- 用于labmda表达式, 指示lambda表达式中可以修改捕获到的变量
exception
3.源码剖析
4.一些术语
- RTTI
- Run-Time Type Identification: 运行时类型检测
来源:https://www.cnblogs.com/Sirius-Z/p/12239485.html