导图社区 c plus plus思维导图
关于C 的思维导图整理,包含关于C 的知识梳理,可供学习、复习、面试等使用,一起来看吧!
编辑于2023-03-05 23:23:05 江苏省C++(cpp)
22/3/1考研复试或者工作面试使用
《Effective C++》
让自己习惯C++
视C++为语言联邦
C 风格
面向对象OOP
泛型编程
STL 标准库
尽量以 const, enum, inline 替换 #define
以编译器代替预处理器
数值常量
```#define Pi 3.1415const double Pi = 3.1415; // const表示```
字符常量
利用 string 类代替 char*```const char* const authorName = 'Tianwell'const std::string authorNmae = 'Tianwell' ```
表达式
经典的宏替换例子:```#define GET_MAX(a,b) f((a) > (b) ? (a) : (b))int a = 5, b = 0;GET_MAX(++a,b); // a 被累加两次GET_MAX(++a,b+10); // a被累加一次```例2: 求 i,j 的结果值为 ? (括号区别)```#define MA(x, y) (x*y)#define MB(x, y) ((x)*(y))int i = 5,j = 5;i = MA(i,i+1)-7;j = MB(i,i+1)-7;```计算过程```i*i+1-7=19j*(j+1)-7 = 23;```**通过template函数模板解决该问题**```templateinline void MA(const T&a, const T&b){f(a > b ? a : b);}```
尽可能用到 const
const 语法
记忆:```const 在 * 左,表示被指物为常量,const 在 * 右,则是指针自身是常量;特殊情况:两种表达方式效果一样void f1(const double* p);void f2(double const* p);```
const 成员函数
在 const 和 non-const 成员函数中避免重复
确定对象被使用前已先被初始化
内置类型的初始化是由编译器决定的,因此最好手动增加进行初始化赋值。而类对象的初始化则是由其构造函数决定,而其中const或 reference 对象都只能通过初始化而非copy, 因此只能够使用 列表初始化这种方式。class 成员变量总是以其声明的次序别初始化,虽然赋值时以不同顺序也合法,但是从书写习惯上来看坚决不要这样。
构造/析构/赋值运算
资源管理
设计与声明
实现
继承与面向对象
模板与泛型编程
面试问题
虚函数
虚函数的实现是由两个部分组成的,**虚指针**与**虚函数表**。1. 有虚函数的类才有虚函数表, 虚函数表存的是每个虚函数的地址(因为可能有多个虚函数)2. 这个类的每一个对象都会包含一个**虚指针**(虚指针存在于对象实例地址的最前面,保证虚函数表有最高的性能),这个虚指针指向虚函数表**注:对象不包含虚函数表,只有虚指针,类才包含虚函数表,派生类会生成一个兼容基类的虚函数表。**- 单继承时的虚函数派生类继承基类,并且重写定义了虚函数,则会产生一个兼容基类的虚函数表- 单继承时的虚函数(重写)如果重写,则会将 base::x() 指向自己的虚函数- 多继承虚函数多个继承就会有多个虚函数表
虚函数实现原理
虚函数的实现是由两个部分组成的,**虚指针**与**虚函数表**有关虚函数的几点知识-**虚函数表是针对类的,一个类的所有对象的虚函数表都一样,类的所有对象共享这个类的虚函数表**-每个对象内部都保存一个指向该类虚函数表的指针vptr,每个对象的vptr的存放地址都不一样,但是都指向同一虚函数表。``` 在有虚函数的类中,类的最开始部分是一个虚函数表的指针,这个指针指向一个虚函数表,表中放了虚函数的地址,实际的虚函数在**代码段(.text)**中。 当子类继承了父类的时候也会继承其虚函数表,当子类重写父类中虚函数时候,会将其继承到的虚函数表中的地址**替换**为重新写的函数地址。使用了虚函数,会增加访问内存开销,降低效率。```
虚函数表
**每个类的实例化对象都会拥有虚函数指针并且都排列在对象的地址首部**。而它们也都是按照一定的顺序组织起来的,从而构成了一种表状结构,称为**虚函数表(virtual table)** 。[见博客](https://blog.csdn.net/weixin_43329614/article/details/89103574)
虚指针
从本质上来说就只是一个指向函数的指针,与普通指针并无区别。它指向用户所定义的虚函数,具体是在子类里的实现。当子类调用虚函数的时候,实际上是通过调用该虚函数指针从而找到接口。虚函数指针是确实存在的数据类型,**在一个被实例化的对象中,它总是被存放在该对象的地址首位,**这种做到的目的是为了保证运行的快速性。与对象的成员不同,虚函数指针**对外部是完全不可见的**,除非通过直接访问地址的做法或者 DEBUG 模式中,否则它使不可见的也不能被外界调用。**只有拥有虚函数的类才会拥有虚指针**,每一个虚函数也都会对应一个虚函数指针。所以拥有虚函数的类的所有对象都会因为虚函数产生额外的开销,并且也会在一定程度上降低程序速度。与java不同,C++将是否使用虚函数这一权利交给了开发者。
虚函数表存储
在有虚函数的类中,类的最开始部分是一个虚函数表的指针,这个指针指向一个虚函数表,表中放了虚函数的地址,实际的虚函数在**代码段(.text)**中。当子类继承了父类的时候也会继承其虚函数表,当子类重写父类中虚函数时候,会将其继承到的虚函数表中的地址**替换**为重新写的函数地址。使用了虚函数,会增加访问内存开销,降低效率。
默认的析构函数不是虚函数原因
C++默认的析构函数不是虚函数是因为**虚函数需要额外的虚函数表和虚表指针,占用额外的内存**。而对于不会被继承的类来说,其析构函数如果是虚函数,就会浪费内存。因此C++默认的析构函数不是虚函数,**而是只有当需要当作父类时,设置为虚函数**。
基类析构函数必须是虚函数原因
**为什么要将基类的析构函数定义为虚函数**将可能会被继承的父类的析构函数设置为虚函数,可以保证当我们new一个子类,**然后使用基类指针指向该子类对象,释放基类指针时可以释放掉子类的空间,防止内存泄漏**。不论基类的析构函数是否为virtual的,派生类的对象在过期时都是**先调用自己析构函数,然后再调用基类的析构函数**。- virtual的作用是“**让基类能够正确调用派生类的函数**”析构函数使用virtual的作用是“**当使用基类指针指向派生类的时候,delete该指针可以正确调用派生类的析构函数**- 继承虚构的调用是通过递归进行调用
回避虚函数机制
在某些情况下,我们希望对虚函数不要进行动态绑定,而是强迫其执行虚函数的某个特定版本,使用**作用域运算符**可以实现这一目的。```Base baseP;double undiscounted = baseP->Quote::net_price(42); // 不管baseP的动态类型是什么```通常情况下,只有成员函数(或友元)中的代码才需要使用作用域运算符来回避虚函数的机制。如果一个派生类虚函数需要调用它的基类版本,但是没有使用作用域运算符,则在运行时该调用将被解析为对派生类版本自身的调用,从而导致无限递归。友元关系不能传递,也不能继承。每个类负责控制各自成员的访问权限。
虚函数能否被overload
能够,但必须写明函数体{]```class A{public:virtual void foo(){};virtual int foo(){};};```
纯虚函数
虚函数存放在虚函数表中,但是纯虚函数在虚函数表中对应的位置上是一个 **pure\_virtual\_called()函数实例**,他扮演该位置的空间保卫者的角色,如果被调用就会触发**执行期异常处理**,除非该纯虚函数被后代实现,该位置才会被覆盖为普通的虚函数**纯虚函数只能被静态地调用,不能经由虚拟机制调用**在父类中纯虚函数是否实现取决于设计者,但**纯虚析构函数一定要定义实现!**因为每一个继承的子类的析构函数会被扩张,**将以静态调用的方式调用每一个上层的基类的析构函数。因此只要缺乏任何一个基类的析构函数,都会导致链接失败**即就比如,如果要将一个基类定义为抽象类,但是没有合适的纯虚函数时,就可以将析构函数定义为纯虚函数。但是一定要有实现,因为当基类指针指向派生类的对象时,如果对象释放掉,依次调用派生类的析构函数,基类的析构函数,如果基类没有析构函数,那编译器应该会出问题。
运行时多态是怎么实现的
所谓**多态性**,顾名思义就是“多个性态”。更具体一点的就是,**用一个名字定义多个函数**,这些函数执行不同但相似的工作。最简单的多态性的实现方式就是**函数重载**和**模板**,这两种属于**静态多态性**。还有一种是**动态多态性**,就是**虚函数**。静态多态是在编译时就已经确定的,静态多态主要就是**重载**,在编译的时候就已经确定了;动态多态是用**虚函数机制**实现的,在运行期间**动态绑定**。调用对象时才表现出来的,也就是虚表指针指向的虚表中的虚函数不同(子类对象重写父类虚函数),表现出不同的状态。如果不使用 virtual 则是静态绑定,在允许过程中不会根据具体指向的类类型而调用其函数.
静态成员函数为什么不能声明为虚函数
static成员不属于任何类对象或类实例,所以即使给此函数加上virutal也是没有任何意义的。静态与⾮静态成员函数之间有⼀个主要的区别。那就是静态成员函数没有this指针。ps:,静态成员函数也不能被声明为const和volatile. ⽽static成员函数没有this指针,所以使⽤const来修饰static成员函数没有任何意义。volatile的道理也是如此。
构造/析构函数
C++中析构函数的作用
进行类的清理工作,具体来说就是释放构造函数开辟的内存空间和资源。析构函数与构造函数对应,当对象结束其生命周期,如对象所在的函数已调用完毕时,系统会自动执行析构函数。析构函数名也应与类名相同,只是在函数名前面加一个位取反符,例如stud( ),以区别于构造函数。它不能带任何参数,也没有返回值(包括void类型)。**只能有一个析构函数,不能重载。**如果用户没有编写析构函数,编译系统会自动生成一个缺省的析构函数(即使自定义了析构函数,编译器也总是会为我们合成一个析构函数,并且如果自定义了析构函数,**编译器在执行时会先调用自定义的析构函数再调用合成的析构函数**),它也不进行任何操作。所以许多简单的类中没有用显式的析构函数。如果一个类中有指针,且在使用的过程中动态的申请了内存,那么最好显示构造析构函数在销毁类之前,释放掉申请的内存空间,避免内存泄漏。当基类的析构函数为非虚函数时,删除一个基类指针指向的派生类实例时,**只清理了派生类从基类继承过来的资源,而派生类自己独有的资源却没有被清理**,因此这才需要将析构函数设置为虚函数。**将可能会被继承的父类的析构函数设置为虚函数,可以保证当我们new一个子类,然后使用基类指针指向该子类对象,释放基类指针时可以释放掉子类的空间,防止内存泄漏。**
类析构顺序
其实就是构造的逆过程1)派生类本身的析构函数;2)对象成员析构函数;3)基类析构函数
编译器自动生成默认构造函数的情况
在四种情况下1,2编译器目的都是调用对应的构造函数,3,4都是由于虚指针和虚函数表的影响1.**内含一个成员变量,而这个成员变量所属的类中含有默认构造函数**, 则此时需要为此类生成一个implicit default constructor(**隐式的默认构造函数**),这个implicit default constructor是nontrivial的,**因为内含的成员变量需要进行默认构造操作**。2.**继承自一个类,且其父类中有默认构造函数**。派生类中没有定义默认构造函数,则编译器会为派生类提供一个上一层基类的默认构造函数。3.**class中声明了一个虚函数。** 声明了虚函数代表该class中将出现vptr,并需要为虚函数构造一张虚函数表vtbl,这个操作是必须的,因此使该class的默认构造函数成为nontrivial的,**因此需要为了这个vptr和vtbl构建默认构造函数,进行初始化操作**。4.**带有虚基类的class。** 因为虚基类的引入,必须要有一个指针或者类似索引的东西来指向虚基类的区域,**以使虚基类的派生类们能找到共享的虚基类的存储区域。**
默认析构函数不是虚函数原因
C++默认的析构函数不是虚函数是因为**虚函数需要额外的虚函数表和虚表指针,占用额外的内存**。而对于不会被继承的类来说,其析构函数如果是虚函数,就会浪费内存。因此C++默认的析构函数不是虚函数,**而是只有当需要当作父类时,设置为虚函数**。
构造函数不能为虚函数
【构造函数不能为虚函数】虚函数指针vptr指针指向虚函数表,执行虚函数的时候,会调用vptr指针指向的虚函数的地址。当定义一个对象的时候,首先会分配对象内存空间,然后调用构造函数来初始化对象。vptr变量是在构造函数中进行初始化的。又因为执行虚函数需要通过vptr指针来调用。如果可以定义构造函数为虚函数,那么就会陷入先有鸡还是先有蛋的循环讨论中。
基类析构函数必须为虚函数
---当基类的析构函数被定义成虚函数时,我们再来删除这个指针时,先调用派生类的析构函数,再调用基类的析构函数,很明显这才是我们想要的结果。因为指针指向的是一个派生类实例,我们销毁这个实例时,肯定是希望即清理派生类自己的资源,同时又清理从基类继承过来的资源。---而当基类的析构函数为非虚函数时,删除一个基类指针指向的派生类实例时,只清理了派生类从基类继承过来的资源,而派生类自己独有的资源却没有被清理,这显然不是我们希望的。---所以说,如果一个类会被其他类继承,那么我们有必要将被继承的类(基类)的析构函数定义成虚函数。这样,释放基类指针指向的派生类实例时,清理工作才能全面进行,才不会发生内存泄漏```#include using namespace std;class Father {public: ~Father() { cout
成员初始化列表中给成员赋值和在构造函数体内给成员赋值有什么区别
一、若类的数据成员是静态的(const)和引用类型,必需用初始化列表静态(const)的数据成员只能初始化而不能赋值,同样引用类型也是只可以被初始化,那么只有用初始化列表。二、当用第二种方法初始化数据成员时会两次对string的成员函数的调用:一次是缺省构造函数,另一次是赋值。而用第一种方法(初始化列表)只是一次调用缺省的构造函数,并不会调用赋值函数。会减少不必要的开支,当类相当复杂时,就会看出使用初始化列表的好处【即初始化和赋值之间的关系:初始化≠赋值】
拷贝构造函数为什么要⽤引用
不然在调⽤拷贝构造函数时,创建形参临时变量⼜会调⽤拷贝构造函数,会引起⽆限递归调⽤。
指针和引用
数组名与指针的区别
**含义、访问数据、用于、分配内存、本质、sizeof**| 指针 | 数组名 || ------------------------------------------------------------ | ------------------------------------ || 指针存储的地址是自身地址 | 数组名存储的地址是数组首元素地址 || 间接访问数据,首先获得指针的内容,然后将其作为地址,从该地址中提取数据 | 直接访问数据 || 通常用于动态的数据结构 | 通常用于固定数目且数据类型相同的元素 || 通过Malloc分配内存,free释放内存 | 隐式的分配和删除 || 指针就是个变量 | 数组名应当是常量指针 || sizeof(指针)就是sizeof(指针) | sizeof(数组名)是整个数组的大小 |PS:数组名作为函数形参时,在函数体内,其失去了本身的内涵,仅仅只是一个指针;在失去其内涵的同时,它还失去了其常量特性,可以作自增、自减等操作,**可以被修改。**
指针和引用的区别
【**引用的定义**】 引用就是C++对C语言的重要扩充。**引用就是某一变量的一个别名**,对引用的操作与对变量直接操作完全一样。 引用的声明方法:`类型标识符 &引用名=目标变量名;` 引用引入了对象的一个同义词。定义引用的表示方法与定义指针相似,只是用&代替了\*。【**引用的好处**】1.减少复制消耗的时间2.相比同样目的比指针操作方便3.修改多个值方便,解决return多个参数问题**指针和引用的区别:****含义、大小、初始化、做参数、const、改变、多级、++、返回动态**1.指针有自己的一块空间,而引用只是一个别名;2.使用sizeof看一个指针的大小在固定机器固定编译器下是固定的,而引用则是被引用对象的大小;3.指针可以被初始化为NULL,而引用必须被初始化且必须是一个已有对象的引用;4.作为参数传递时,指针需要被解引用才可以对对象进行操作,而直接对引用的修改都会改变引用所指向的对象;5.**可以有const指针,但是没有const引用**```int &const c = a; //错误,这个是不能编译的const int & c = a; //正确,'对const的引用'简称'常量引用'``` 因为符号表中要存放变量名,变量地址 如果是指针,变量名就是他自己的,然后表中地址就是指针本身的地址,本身地址可以到一个地方; 引用,名字是他自己的,但是地址放的是被引用的对象,因为引用相当于是对象的别名,其指向的地址是不能改变的,因为引用只能在初始化的时候赋值,那就没有所谓的const6.指针在使用中可以指向其它对象,但是引用只能是一个对象的引用,不能被改变;7.指针可以有多级指针(\*\*p),而引用只有一级;8.指针和引用使用++运算符的意义不一样;9.如果返回动态内存分配的对象或者内存,必须使用指针,引用可能引起内存泄露
函数指针
- 什么是函数指针 如果在程序中定义了一个函数,那么再编译时系统就会为这个函数代码分配一段存储空间,**这段存储空间的首地址称为这个函数的地址,而且函数名表示的就是这个地址**。 定义一个指针变量来存放这个地址,这个指针变量就叫做**函数指针变量**,简称**函数指针**- 用途 调用函数和做函数的参数,比如回调函数。- 定义 ```c++ 函数返回值类型 (* 指针变量名) (函数参数列表); int (*p)(int a, int b); // 此函数指针 p 的类型即为 int(*)(int, int); ``` 但是这里需要注意的是:“(*指针变量名)”两端的括号不能省略,括号改变了运算符的优先级。如果省略了括号,就不是定义函数指针而是一个函数声明了,即声明了一个**返回值类型为指针型的函数**。 注意,指向函数的指针变量没有 ++ 和 -- 运算。 - 怎么判断一个指针变量是**指向变量的指针变量**还是**指向函数的指针变量**呢? 首先看变量名前面有没有“\*”,如果有“\*”说明是指针变量;其次看变量名的后面有没有带有形参类型的圆括号,如果有就是指向函数的指针变量,即函数指针,如果没有就是指向变量的指针变量。
智能指针
[什么是智能指针?为什么要用智能指针?](https://blog.csdn.net/weixin_34112030/article/details/91383878)- 什么是智能指针 智能指针是一个 **类**,它封装了一个原始的C++指针,**主要用于管理在堆上分配的内存。**没有单一的智能指针类型,但所有这些都尝试以实用的方式抽象原始指针。智能指针应优于原始指针,因为其可以缓解原始指针的许多问题,主要是忘记删除对象和泄露内存。 **简单地说,它是一种可以像指针一样使用的值,但提供了自动内存管理的附加功能:当指针不再使用时,它指向的内存被释放,从而防止内存泄漏。** 当类中有指针成员时,一般有两种方式来**管理指针成员**,一是**采用值型的方式管理**,每个类对象都保留一份指针指向的对象的拷贝,另一种更优雅的方式是**使用智能指针,从而实现指针指向的对象的共享**。- 为什么要使用智能指针 智能指针的作用是管理一个指针,因为存在以下这种情况: 申请的空间在函数结束时忘记释放,造成内存泄漏。使用智能指针可以很大程度上的避免这个问题,因为智能指针就是一个类,当超出了类的作用域是,类会自动调用析构函数,析构函数会自动释放资源。 所以智能指针的作用原理就是在函数结束时自动释放内存空间,不需要手动释放内存空间。- 何时应该使用智能指针 在代码中设计跟踪一块内存的所有权,分配或者取消分配; 使用智能指针通常可以省掉这些操作。- 应该如何使用常规指针 主要在忽略内存所有权的代码中,这通常是在从其他地方获取了指针,并且不进行分配,解除分配或存储器执行更长的指针的副本的函数中。
继承
继承关系之间的转换规则
1. 从派生类向基类的类型转换只对指针或引用类型有效2. 基类向派生类不存在隐式类型转换3. 派生类向基类的类型转换也可能由于访问受限而变得不可行
防止继承的发生
有时我们会定义这样一种类,我们不希望其他类继承它,或者不想考虑它是否适合作为一个基类,为了实现这以目的,在类名后跟一个关键字final。```c++class A final{};// error: cannot derive from 'final' base 'A' in derived type 'B'class B : public A{}; ```
菱形继承
通过虚继承来防止菱形继承的函数重复问题```class A{};class B:virtual public A{};class C:virtual public A{};class D:public B,pulic C{};```
基本概念
const
什么是多态性
所谓**多态性**,顾名思义就是“多个性态”。更具体一点的就是,**用一个名字定义多个函数**,这些函数执行不同但相似的工作。最简单的多态性的实现方式就是**函数重载**和**模板**,这两种属于**静态多态性**。还有一种是**动态多态性**,就是**虚函数**。```例如 bark();狗叫和鸟叫之间是有区别的.```静态多态是在编译时就已经确定的,静态多态主要就是**重载**,在编译的时候就已经确定了;动态多态是用**虚函数机制**实现的,在运行期间**动态绑定**。调用对象时才表现出来的,也就是虚表指针指向的虚表中的虚函数不同(子类对象重写父类虚函数),表现出不同的状态。
静态多态和动态多态区别
覆盖、重载、隐藏
初始值与赋值
#include
【include头文件的顺序】对于include的头文件来说,如果在文件a.h中声明一个在文件b.h中定义的变量,而不引用b.h。那么要在a.c文件中引用b.h文件,并且要先引用b.h,后引用a.h,否则汇报变量类型未声明错误。【include双引号””和尖括号的区别】**编译器预处理阶段查找头文件的路径不一样。**-对于使用**双引号**包含的头文件,查找头文件路径的顺序为: 当前头文件目录 编译器设置的头文件路径(编译器可使用-I显式指定搜索路径)``` 系统变量CPLUS_INCLUDE_PATH/C_INCLUDE_PATH指定的头文件路径```-对于使用**尖括号**包含的头文件,查找头文件的路径顺序为: 编译器设置的头文件路径(编译器可使用-I显式指定搜索路径)``` 系统变量CPLUS_INCLUDE_PATH/C_INCLUDE_PATH指定的头文件路径```
Java 和 C++ 之间比较
Java:JVM 虚拟机,跨平台处理/自带内存管理反射机制解释性语言C++:编译型语言指针/静态变量声明/多继承说白了 编译机制,内存安全性,效率问题
C与C++区别
- 设计思想上: C++是面向对象的语言,具有封装、继承和多态三种特性(类)【降低耦合性】 C是面向过程(函数)的结构化编程语言- 泛型编程:Cpp支持,比如模板类、函数模板、类模板- 引用:C没有引用;Cpp有引用。- 动态管理内存:C中使用malloc/free;Cpp使用new/delete。- 函数重载:C中没有函数重载;Cpp支持函数重载(依靠name mangling手法)。 **key:Cpp的名字修饰与C不同**- 函数参数列表 没有指定参数列表:C可以接收任意多个参数;Cpp会进行严格的参数类型检测,不接受任何参数。 缺省参数:C不支持缺省参数;Cpp支持指定默认值。
内存管理
### new/delete与malloc/free的区别**在导入new和delete运算符之前,承担class内存管理的唯一方法就是在constructor中指定this指针**- new/delete是C++的关键字 new不止是分配内存空间,还会调用构造函数,不用指定内存大小,返回的指针不用强转;delete同理不止释放空间,还会调用析构函数- malloc/free是C语言的库函数 需要给定申请内存的大小,返回的指针需要强转,不会调用构造函数和析构函数。其次类中,new其实不止是分配内存空间,还自动调用构造函数,### new与malloc的区别**类型、操作、成功返回、失败返回、重载、调用、内存位置、空间不足、数组操作**1. 类型 1. malloc/free是C标准库函数 2. new/delete是Cpp的操作运算符/关键字,实际上是调用了运算符重载函数`::operator new()`和`::operator delete()` *可以在全局或者类的作用域中提供自定义的new和delete运算符的重载函数,以改变默认的malloc和free内存开辟释放行为,比如说实现内存池。*2. 操作 1. malloc/free 单纯分配内存空间并释放。 2. new/delete 基于前者实现,在开辟/释放空间后,会调用对象的构造函数/析构函数进行初始化/清理。3. 成功返回 1. malloc 成功后会返回 void* ,如需使用必须强制类型转换。 2. new 成功后会直接返回对应类型的指针。4. **失败返回** 1. malloc 开辟内存失败之后会返回nullptr指针,需要检查判空(也只能这样)。 2. new 开辟内存失败之后会抛出bad_alloc类型的 **异常**,可以捕获异常。用户可以指定处理函数或重新制定分配器(new_handle)5. 能否重载:只有new/delete能被重载。6. 调用 1. malloc对开辟的空间大小需要严格指定。 2. new只需要内置类型/对象名。7. 内存位置 *PS:堆是操作系统维护的一块内存,而自由存储区是C++中通过new与delete动态分配和释放对象的抽象概念。堆与自由存储区并不等价。* 1. malloc在堆(heap)上分配内存。 2. new申请内存位置有一个抽象概念——自由存储区(free store),它可以在堆上,**也可以在静态存储区上分配**,这主要取决于operator new实现细节,取决与它在哪里为对象分配空间。8. **空间不足的弥补** 1. 使用malloc:malloc本身不会进行尝试,可以由开发者再使用realloc进行扩充或缩小。 2. 使用new:不能被如前者一样直观地改变。当空间不足时**会触发new_handler机制**,此机制留有一个set_new_handler句柄,看看用户是否设置了这个句柄,如果设置了就去执行,句柄的目的是看看能不能尝试着从操作系统释放点内存,找点内存,**如果空间确实不足就抛出bad_alloc异常;**9. 数组操作 new[] 对应 delete[],malloc分配的一块内存可以直接用free释放
设计思想
引用
运行机制
C语言是怎么进行函数调用
1、参数入栈2、返回地址入栈3、函数栈帧开辟4、函数栈帧回退- 函数调用操作所使用的栈部分叫做**栈帧结构**,每个函数调用都有属于自己的栈帧结构,栈帧结构由**两个指针**指定,**帧指针**(指向起始),**栈指针**(指向栈顶),函数对大多数**数据的访问**都是基于**帧指针**。 - esp寄存器:栈指针 - ebp寄存器:帧指针每一个函数调用都会分配函数栈,在栈内进行函数执行过程。调用前,先把返回地址压栈,然后把当前函数的esp指针压栈。
C语言参数压栈顺序
从右到左
C表达式调用顺序
问是先进入 (x-1) 还是进入 (x-2)```int fibbo(int x){if(x
C++如何处理返回值
生成一个临时变量,把它的引用作为函数参数传入函数内。
C++中拷贝赋值函数的形参
C++函数栈空间的最大值
默认是1M,不过可以调整可以根据编译器的属性设置更改
常量存放在内存的位置
对于局部常量,存放在栈区对于全局常量,常量存放在全局/静态存储区字面值常量,常量存放在常量存储区,比如字符串,放在常量区。
隐式类型转换
对于内置类型**,低精度的变量给高精度变量赋值会发生隐式类型转换,**对于只存在单个参数的构造函数的对象构造来说**,函数调用可以直接使用该参数传入,编译器会自动调用其构造函数生成**临时对象**。用explicit可以防止类的隐式类型转换[C++ explicit关键字详解](https://blog.csdn.net/guoyunfei123/article/details/89003369)
unsigned int x = 0; x-- 是多少
4294967295 = 2^32-1
常见关键字
静态变量static
- **自动变量**,指**auto**。所有变量如果不带说明,默认都是auto,即自动变量(**动态变量**)。 特征:每次进入该函数运行时,值都是不确定的,需要初始化。退出该函数时,变量所占的内存被回收了。 - **静态变量**,指**static**, 特征:每次进入该函数运行时,值是上次运行时的值(如是第一次,则需要初始化)。退出该函数时,变量不会被回收。- 分配内存位置 全局静态变量和局部静态变量在内存中的位置都为:静态存储区。 static定义的静态局部变量分配在数据段上,普通的局部变量分配在栈上,会因为函数栈帧的释放而被释放掉。- 类静态成员的使用: - 初始化在类体外进行,而前面不加static,(这点需要注意)以免与一般静态变量或对象相混淆。 - 初始化时不加该成员的访问权限控制符private,public等。 - 初始化时使用作用域运算符来标明它所属类,因此,静态数据成员是类的成员,而不是对象的成员。 - 静态数据成员是静态存储的,它是静态生存期,**必须对它进行初始化。** - 类的静态函数仅可以调用静态成员,注意这里,**可以是外部的静态成员**(函数或者变量都可以)。也就是说只有类中的静态变量是一定要初始化的(还有用auto作为变量类型的时候,即动态变量),且初始化要在类声明之外。其他的即使没有初始化也会被初始化为0。
static 关键字的作用
作用、内存分配、初始化、生命周期、作用域1. 全局静态变量 在全局变量前加上关键字static,全局变量就定义成一个全局静态变量. 静态存储区,在整个程序运行期间一直存在。 初始化:未经初始化的全局静态变量会被自动初始化为0(自动对象的值是任意的,除非他被显式初始化); **作用域:全局静态变量在声明他的文件之外是不可见的,准确地说是从定义之处开始,到文件结尾。**2. 局部静态变量 在局部变量之前加上关键字static,局部变量就成为一个局部静态变量。 内存中的位置:静态存储区 初始化:未经初始化的全局静态变量会被自动初始化为0(自动对象的值是任意的,除非他被显式初始化); 作用域:作用域仍为局部作用域,当定义它的函数或者语句块结束的时候,作用域结束。但是当局部静态变量离开作用域后,并没有销毁,而是仍然驻留在内存当中,只不过我们不能再对它进行访问,直到该函数再次被调用,并且值不变;3. 静态函数 在函数返回类型前加static,函数就定义为静态函数。函数的定义和声明在默认情况下都是extern的,但**静态函数只是在声明他的文件当中可见,不能被其他文件所用。** 函数的实现使用static修饰,那么这个函数只可在本cpp内使用,不会同其他cpp中的同名函数引起冲突; warning:不要再头文件中声明static的全局函数,不要在cpp内声明非static的全局函数,如果你要在多个cpp中复用该函数,就把它的声明提到头文件里去,否则cpp内部声明需加上static修饰;**类的静态成员有两种:静态成员变量和静态成员函数 没有this指针,直接通过class访问** 4. 静态成员变量(类的) 在类中,静态成员可以实现多个对象之间的数据共享,并且使用静态数据成员还不会破坏隐藏的原则,即保证了安全性。因此,静态成员是类的所有对象中共享的成员,而不是某个对象的成员。对多个对象来说,静态数据成员只存储一处,供所有对象共用5. 静态成员函数(类的) 静态成员函数和静态数据成员一样,它们都属于类的静态成员,它们都不是对象成员。因此,对静态成员的引用不需要用对象名。 在静态成员函数的实现中不能直接引用类中说明的非静态成员,可以引用类中说明的静态成员(这点非常重要)。如果静态成员函数中要引用非静态成员时,可通过对象来引用。从中可看出,调用静态成员函数使用如下格式:::();
局部静态变量
保持了什么样的语义?1. 局部变量的构造函数只能施行一次2. 局部变量的析构函数只能施行一次旧策略:无条件地在程序起始时构造出对象,即使他们所在的函数从来未被调用。而现在的C++ standard已经强制要求:只有函数被调用的时候才构造出对应的对象。解决方案:**在全局处产生一个保护对象,作为戒护之用,并处以零初始化**当第一次进入函数时,评估临时对象为false,于是施行构造函数,**并将构造成功后的对象地址取出,由保护对象掌控**后续进入函数,如果保护对象不为空,说明是后续进入,什么都不做。析构函数也处以类似的手法。
const 限定符
const只对它左边的东西起作用,唯一的例外就是const本身就是最左边的修饰符,那么它才会对右边的东西起作用。### 为什么使用const采用符号常量写出的代码更容易维护;指针常常是边读边移动,而不是边写边移动;许多函数参数是只读不写的。### const的分类注:在**常变量、常引用、常对象、常数组**中,**const** 与 “类型说明符” 或 ”类名(类名是一种自定义的类型说明符)“的位置可以互换,如:const int a = 5; 与 int const a = 5; 同- 常变量 ```c++ const 类型说明符 p; ```- 常引用 ```c++ const 类型说明符 &p; ```- 常对象 ```c++ 类名 const 对象名; ```- 常数组 ```c++ 类型说明符 const a[siz]; ```- 常成员函数 ```c++ 类名::fun(形参) const ``` 当存在同名同参数和返回值的常量函数和非常量函数时,具体调用哪个函数是根据调用对象是常量对像还是非常量对象来决定的。常量对象调用常量成员;非常量对象调用非常量的成员。- 常指针 ```c++ const 类型说明符* ptr; 类型说明符* const ptr; ```- 用法1:常量 - 取代了C中的宏定义,声明时必须进行初始化(C++类中则不然)。 const限制了常量的使用方式,并没有描述常量应该如何分配。如果编译器知道了某const的所有使用,它甚至可以不为该const分配空间。 最简单的常见情况就是常量的值在编译时已知,而且不需要分配存储 - 用const声明的变量虽然增加了分配空间,但是可以保证类型安全 - C标准中,const定义的常量是全局的,C++中视声明位置而定。- 用法2:指针和常量 - 使用指针时涉及到两个对象:该指针本身和被它所指的对象。 出现在 \* 之前的 const 是作为基础类型的一部分 ```c++ char *const cp; // 到 char 的 const 指针 char const *pc1; // 到 const char 的指针 const char *pc2; // 到 const char 的指针(与上一个等同) ``` ```c++ // 从右向左读的记忆方式: cp is a const pointer to char. // 故 cp 不能指向别的字符串,但可以修改其指向的字符串的内容 pc is a pointer to const char. // 故 不能通过*pc修改其所指向的内容,但是可以通过变量自己或者其他方式来修改,且pc可以指向别的字符串 ``` **注意:允许把非const对象的地址赋给const对象的指针,不允许把一个const对象的地址赋给一个普通的、非const对象的指针。**- 用法3:const 修饰函数传入参数 - 将函数传入参数声明为const,以指明使用这种参数仅仅是为了效率的原因,而不是想让调用函数能够修改对象的值。同理,将指针参数声明为const,**函数将不修改由这个参数所指的对象**,具体不能改变的东西根据用法2中的介绍。 - 常用修饰指针参数和引用参数 ```c++ void fun(const A *in); // 修饰指针型传入参数 void fun(const A &in); // 修饰引用型传入参数 ```- 用法4:修饰函数返回值 可以组织用户修改返回值,返回值也要相应的付给一个常量或常指针。- **用法5:const修饰成员函数(即类中的函数)(c++特性)** **const 对象只能访问 const 成员函数**,而非 const 对象可以访问任意的成员函数,包括const成员函数。 - const修饰成员函数的目的 const修饰的成员函数表明函数调用不会对对象做出任何更改,事实上,如果确认不会对对象做更改,就应该为函数加上const限定,这样无论const对象还是普通对象都可以调用该函数。 - const的定义声明 - const数据成员只在某个对象生存期内是常量,而**对于整个类而言却是可变的**。因为类可以创建多个对象,**不同的对象其const数据成员的值可以不同**。所以**不能在类声明中初始化const数据成员**,因为类的对象未被创建时,编译器不知道const 数据成员的值是什么。 - 要想建立在整个类中都恒定的常量,应该用类中的枚举常量来实现 enum {size=100;}; 枚举常量不会占用对象的存储空间,他们在编译时被全部求值。但是枚举常量的隐含数据类型是整数,其最大值有限,且不能表示浮点数。- 其他 对于**非内部数据类型的输入参数**,因该将“值传递”的方式改为**“const引用传递”**,目的是为了提高效率。例如,将void Func(A a)改为void Func(const A &a) 对于**内部数据类型的输入参数**,**不要将“值传递”的方式改为“const引用传递”**。否则既达不到提高效率的目的,又降低了函数的可理解性。例如void Func(int x)不应该改
const在c和c++中的区别
- C++中的const正常情况下是看成编译期的常量,编译器并不为const分配空间,只是在编译的时候将期值保存在名字表中,并在适当的时候折合在代码中- 在C语言中: const int size; 这个语句是正确的,因为它被C编译器看作一个声明,指明在别的地方分配存储空间.但在C++中这样写是不正确的.C++中const默认是内部连接,如果想在C++中达到以上的效果,必须要用extern关键字.即**C++中,const默认使用内部连接,而C中使用外部连接.** **(1) 内连接**:编译器只对正被编译的文件创建存储空间,别的文件可以使用相同的表示符或全局变量.C/C++中内连接使用static关键字指定. **(2) 外连接**:所有被编译过的文件创建一片单独存储空间.一旦空间被创建,连接器必须解决对这片存储空间的引用.全局变量和函数使用外部连接.通过extern关键字声明,可以从其他文件访问相应的变量和函数.- **C++中,是否为const分配空间要看具体情况**.如果加上关键字extern或者取const变量地址,则编译器就要为const分配存储空间.- C++中定义常量的时候不再采用define,因为define只做简单的宏替换,并不提供类型检查.
global 全局变量
在C中,global被视为一个“临时性的定义”。一个“临时性的定义”可以在程序中发生多次,**而多个实例最终会被链接器折叠起来,只留下一个单独实例**,被放在 data segment 的 .Bss 段(Block started by symbol)注意,C的global不会设置初值**C++没有,也不支持所谓的“临时变量”**(因为class存在饮食构造行为)。因此,global在C++中被视为完全定义(他会阻碍第二个或更多定义)。C和C++的一个差异就在于,.Bss段在C++中相对地不重要**C++的所有全局对象都被以“初始化过的数据”来对待**,所有未初始化的都会被处以0初始化,但要到程序启动时才会实施
inline 内联
inline 定义与优缺点
【inline定义】inline是C++关键字,在函数声明或定义中,函数返回类型前加上关键字inline,即可以把函数指定为内联函数。这样可以解决一些频繁调用的函数大量消耗栈空间(栈内存)的问题。**引入它的主要原因是用它替代C中表达式形式的宏定义**1.inline定义的类的内联函数,函数的代码被放入符号表中,**在使用时直接进行替换(像宏一样展开),没有了调用的开销,效率也很高**。2.很明显,类的内联函数也是一个真正的函数,编译器在调用一个内联函数时,会首先检查它的参数的类型,保证调用正确。然后进行一系列的相关检查,就像对待任何一个真正的函数一样。这样就消除了它的隐患和局限性。3.inline可以作为某个类的成员函数,当然就可以在其中使用所在类的保护成员及私有成员。【inline的优缺点】- 优点 - 对于内联函数,C++有可能直接用函数体代码来替代对函数的调用,这一过程称为函数体的内联展开。 - 对于只有几条语句的小函数来说,与函数的调用、返回有关的准备和收尾工作的代码往往比函数体本身的代码要大得多。因此,对于这类简单的、使用频繁的小函数,将之说明为内联函数可提高运行效率。 - 缺点 - 内联是以代码膨胀复制为代价,仅仅省去了函数调用的开销,从而提高函数的执行效率。如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收获会很少。 另一方面,每一处内联函数的调用都要复制代码, **将使程序的总代码量增大,消耗更多的内存空间**。 以下情况不宜使用内联: 如果函数体内的代码比较长,使用内联将导致内存消耗代价较高。 如果函数体内出现循环,那么执行函数体内代码的时间要比函数调用的开销大。
内联函数与一般函数区别
1. 内联含函数比一般函数在前面多一个inline修饰符。2. 内联函数是**直接复制“镶嵌”到主函数中**去的,就是将内联函数的代码直接放在内联函数的位置上,这与一般函数不同,主函数在调用一般函数的时候,是指令跳转到被调用函数的入口地址,执行完被调用函数后,指令再跳转回主函数上继续执行后面的代码; 而由于内联函数是将函数的代码直接放在了函数的位置上,所以没有指令跳转,指令按顺序执行。3. 一般函数的代码段只有一份,放在内存中的某个位置上,当程序调用它是,指令就跳转过来;当下一次程序调用它是,指令又跳转过来; **而内联函数是程序中调用几次内联函数,内联函数的代码就会复制几份放在对应的位置上**4. 内联函数一般在头文件中定义,而一般函数在头文件中声明,在cpp中定义。
处理inline的两个阶段
- 处理一个inline的两个阶段 1. 分析函数定义,以决定函数的**“intrinslc inline ability”**(本质的 ilnine 能力) 如果函数因其复杂度,或者其他原因被判断不可成为 inline,可能会被转为一个 static 函数,并在“被编译模块”内产生对应的函数定义。 在一个支持模块个别编译的环境中,**连接器会将被产生出来的重复的东西清理掉,Unix系统的strip命令可以达到此目的** 2. 真正的inline函数扩展操作是在被调用的那一点上 **这会带来参数的求值操作以及临时性对象的管理**- **形参管理** ```c++ inline int min(int i, int j){ return i
typename和class
`template`与`template`一般情况下这两个通用,但有一个特例,就是当 T 是一个类,而这个类又有子类(假设名为 innerClass) 时,应该用 `template``typename T::innerClass myInnerObject;`这里的 `typename` 告诉编译器,`T::innerClass` 是一个类,程序要声明一个 `T::innerClass` 类的对象,而不是声明 T 的静态成员,而 typename 如果换成 class 则语法错误。
extern 声明
- **基本解释:声明外部变量** extern可以置于变量或者函数前,以标示变量或者函数的定义在别的文件中,提示编译器遇到此变量或函数时在其他模块中寻找其定义。 此外extern也可用来进行链接指定。 默认的应该就是extern, 定义函数时如果没有指定是static, 那么编译器会自动给函数添加extern关键字。 - 还有一个作用是:`extern 'C' double sqrt(double);` 作用是告诉c++的编译器,对于接下来的函数fun()要采用C的编译器的方式来处理它。因为c++处于优化的考虑会把代码里面的函数会重新命名,这样的话就可能导致外部对该程序功能的调用失败,即找不到原来的函数,而c的编译器不会改变函数名,所以这样声明。- 全局变量是在外部使用声明时,extern关键字是必须的,如果变量没有extern修饰且没有显示的初始化,同样成为变量的定义,因此此时必须加extern,而编译器再次标记存储空间在执行时加载内存并初始化为0,而**局部变量的声明不能有extern的修饰**,且局部变量在运行时才在堆栈部分分配内存。- 全局变量或函数本质上讲没有区别,函数名是指向函数二进制块开头处的指针,而全局变量是在函数外部声明的变量。函数名也在函数外,因此函数也是全局的。- **声明可以多次,定义只能一次** - ```c extern int i; // 声明,不是定义 int i; // 声明,也是定义 ```- **extern与static** - extern表明该变量在别的地方已经定义过了,在这里要使用那个变量 - static表示静态的变量,分配内存的时候存储在静态区,不存储在栈上。 - static作用范围是内部连接的关系,和extern有点相反,它和对象本身是分开存储的,extern也是分开存储的,但是extern可以被其他的对象用extern引用,而static不可以,只允许对象本身用它。 - 具体差别: - 首先,static与extern是一对”水火不容“的家伙,也就是说extern和static**不能同时修饰一个变量**; - 其次,**static修饰的全局变量声明和定义同时进行**,也就是说当你在头文件中使用static声明了全局变量后,它也同时被定义了。 - 最后,static修饰全局变量的作用域只能是本身的编译单元,也就是说他的“全局”只对本编译单元有效,其他编译单元则看不到他。### 说一说extern“C”上面有提到C++调用C函数需要extern C,因为C语言没有函数重载。作用是告诉c++的编译器,对于接下来的函数fun()要采用C的编译器的方式来处理它。因为c++处于优化的考虑会把代码里面的函数会重新命名,这样的话就可能导致外部对该程序功能的调用失败,即找不到原来的函数,而c的编译器不会改变函数名,所以这样声明。
STL容器
iterator
STL中迭代器的作用
Iterator(迭代器)模式又称**Cursor(游标)模式**,用于提供一种方法顺序访问一个聚合对象中各个元素, 而又不需暴露该对象的内部表示。或者这样说可能更容易理解:Iterator模式是运用于聚合对象的一种模式,通过运用该模式,使得我们可以**在不知道对象内部表示的情况下,按照一定顺序(由iterator提供的方法)访问聚合对象中的各个元素**。由于Iterator模式的以上特性:与聚合对象耦合,在一定程度上限制了它的广泛运用,**一般仅用于底层聚合支持类**,如STL的list、vector、stack等容器类及ostream_iterator等扩展iterator。- 迭代器产生的原因,为何有指针还需要迭代器 Iterator类的访问方式就是把不同集合类的访问逻辑抽象出来,使得**不用暴露集合内部的结构而达到循环遍历集合的效果**。
迭代器和指针的区别
迭代器不是指针,**是类模板,表现的像指针**。他只是模拟了指针的一些功能,通过重载了指针的一些操作符,->、*、++、--等。迭代器封装了指针,是一个**“可遍历STL( Standard Template Library)容器内全部或部分元素”的对象**, 本质是封装了原生指针,是指针概念的一种提升(lift),提供了比指针更高级的行为,**相当于一种智能指针**,他可以根据不同类型的数据结构来实现不同的++,--等操作。**迭代器返回的是对象引用而不是对象的值**,所以cout只能输出迭代器使用*取值后的值而不能直接输出其自身
STL迭代器是怎么删除元素
这个主要考察的是**迭代器失效**的问题。1. 对于序列容器vector,deque来说,使用erase(itertor)后,后边的每个元素的迭代器都会失效,但是后边每个元素都会往前移动一个位置,但是**erase会返回下一个有效的迭代器**;2. 对于关联容器map、set来说,使用了erase(iterator)后,当前元素的迭代器失效,但是其结构是红黑树,删除当前元素的,不会影响到下一个元素的迭代器,所以在调用erase之前,记录下一个元素的迭代器即可。3. 对于list来说,它使用了不连续分配的内存,并且它的erase方法也会返回下一个有效的iterator,因此上面两种正确的方法都可以使用。
resize和reserve的区别
- resize(): **改变当前容器内含有元素的数量(size())**,``` eg: `vectorv`; `v.resize(len);` $v$ 的 $size$ 变为 $len$,如果原来 $v$ 的 $size$ 小于 $len$,那么容器新增$(len-size)$ 个元素,元素的值为默认为 $0$。当 $v.push_back(3);$ 之后,则是 $3$ 是放在了 $v$ 的末尾,即下标为 $len$ ,此时容器是 $size$ 为 $len+1$;```- reserve():``` **改变当前容器的最大容量(capacity),它不会生成元素,只是确定这个容器允许放入多少对象** 如果 $reserve(len)$ 的值大于当前的 $capacity()$,那么会重新分配一块能存 $len$ 个对象的空间,然后把之前 $v.size()$ 个对象通过 copy construtor 复制过来,**销毁之前的内存**;```
map/multimap
1. Map映射,map 的所有元素都是 pair,同时拥有实值(value)和键值(key)。pair 的第一元素被视为键值,第二元素被视为实值。所有元素都会根据元素的键值自动被排序。不允许键值重复。 底层实现:红黑树 适用场景:有序键值对不重复映射2. Multimap 多重映射。...同上,但允许键值重复。 底层实现:红黑树 适用场景:有序键值对可重复映射
map与set
map和set都是C++的关联容器,其底层实现都是红黑树(RB-Tree)。由于 map 和set所开放的各种操作接口,RB-tree 也都提供了,所以几乎所有的 map 和set的操作行为,都只是转调 RB-tree 的操作行为。map和set区别在于:1. map中的元素是key-value(关键字—值)对:关键字起到索引的作用,值则表示与索引相关联的数据;Set与之相对就是关键字的简单集合,set中每个元素只包含一个关键字。2. set的迭代器是const的,不允许修改元素的值;map允许修改value,但不允许修改key。其原因是因为map和set是根据关键字排序来保证其有序性的,如果允许修改key的话,那么首先需要删除该键,然后调节平衡,再插入修改后的键值,调节平衡,如此一来,严重破坏了map和set的结构,导致iterator失效,不知道应该指向改变前的位置,还是指向改变后的位置。所以STL中将set的迭代器设置成const,不允许修改迭代器的值;而map的迭代器则不允许修改key值,允许修改value值。3. map支持下标操作,set不支持下标操作。map可以用key做下标,map的下标运算符[ ]将关键码作为下标去执行查找,如果关键码不存在,则插入一个具有该关键码和mapped_type类型默认值的元素至map中,因此下标运算符[ ]在map应用中需要慎用,const_map不能用,只希望确定某一个关键值是否存在而不希望插入元素时也不应该使用,mapped_type类型没有默认值也不应该使用。如果find能解决需要,尽可能用find。
unordered_map原理
unordered_map 原理hashtable + bucket: unordered_map 内部采用 hashtable 的数据结构存储,每个特定的 key 会通过特定的哈希运算映射到一个特定的位置。一般来说,hashtable 是可能存在冲突的,即不同的key值经过哈希运算之后得到相同的结果。解决方法是:在每个位置放一个桶,用于存放映射到此位置的元素, 当桶内数据量在8以内使用链表来实现桶,当数据量大于8 则自动转换为红黑树结构 也就是有序map的实现结构
STL容器是否是线程安全的
STL容器不是线程安全的。对于vector,即使写方(生产者)是单线程写入,但是并发读的时候,由于潜在的内存重新申请和对象复制问题,会导致读方(消费者)的迭代器失效【解法一】```加锁是一种解决方案,但是加std::mutex互斥锁确实性能较差。对于多读少写的场景可以用读写锁(也叫共享独占锁),来缓解。C++17引入了std::shared_mutex```
erase 与 remove
像 vector
Class 类
C++的struct和class的区别
在C++中,可以用struct和class定义类,都可以继承。区别在于:struct的**默认继承权限**和**默认访问权限**是public,而class的默认继承权限和默认访问权限是private。
类实例的创建/拷贝赋值方式
异常
段错误
段错误通常发生在**访问非法内存地址的时候**,具体来说分为以下几种情况:1. 访问非法内存地址(数组访问越界等)2. 访问只读的内存地址(字符串常量等)3. 栈溢出。
栈溢出
- **栈溢出概念** 栈溢出指的是程序向栈中某个变量中写入的字节数超过了这个变量本身所申请的字节数,因而导致栈中与其相邻的变量的值被改变- **溢出原因** - **局部数组过大**。当函数内部的数组过大时,有可能导致堆栈溢出。**局部变量是存储在栈中的**,因此这个很好理解。 解决这类问题的办法有两个,一是增大栈空间,二是改用动态分配,使用堆(heap)而不是栈(stack)。 - **递归调用层次太多**。递归函数在运行时会执行压栈操作,当压栈次数太多时,也会导致堆栈溢出。 - **指针或数组越界**。这种情况最常见,例如进行字符串拷贝,或处理用户输入等等。
传参方式
按值传参
按值传参的概念非常好理解,就是函数接收到了传递过来的参数后,将其拷贝一份,其函数内部执行的代码操作的都是传递参数的拷贝。也就是说,按值传参最大的特点就是不会影响到传递过来的参数的值,但因为拷贝了一份副本,会更浪费资源一些```double average( double a, double b);double z = average( x, y );```
按(左值)引用传参
按值传参不会影响到原来的参数的值,那么问题来了,如果我们就是想要对参数进行一些修改,怎么办呢?这时候按(左值)引用传参就应运而生了```void swap ( double & a, double & b);swap( x, y);```
按常量引用传参
我们之前说了,如果想要对传入参数进行一些修改,我们要使用按(左值)引用传参,那么我们不想要对传入参数进行修改,是不是就只能使用按值传参了呢?我们能不能既拥有引用的节省拷贝开支的优点,又拥有按值传参的不影响原值的优点呢?于是乎,按常量引用传参也就应运而生了。如果我们只是想要看一看参数的值,而并不需要去修改它,我们就可以使用按常量引用传值,从而减少拷贝方面的开销。```string randomItem( const vector & arr );vector v { 'hello', 'world' };cout
右值传递
对于右值来说,其存储的是临时的将要被摧毁的资源,移动一个对象的状态总会比赋值这个对象的状态要来的简单(开销小)```string randomItem( vector && arr );vector v { 'hello', 'world' };cout
算法试题
**书写注意事项**```1.要加const的一定加const2.类不要忘记写出构造和析构3.所有new的对象结束后,都必须增加delete部分```
给定一个三角形,判断某点是否在该三角形内.
1.如果给定已知点为顺时针方向则通过向量叉乘结果的正负性判断向量AP在其他向量的左右侧.【向量叉乘方向】PA x PB : 从 PA 转向 PB,大拇指即为指向方向[右手定则]
设计日志存储系统
无序数组找中位数
反转链表
模拟vector
《C++ Primer》
22/1/21记录《C++ Pimer》主要内容22/3/26对这本书的使用意见是:先大致浏览,然后再在实际操作中当作字典使用.22/6/16对Class部分进行调序归类,原书里的内容比较分散.
C++基础
C++11新特性
类型相关
类型操作
decltype
explicit
用explicit可以防止类的隐式类型转换
类型
constexpr
nullptr
reference
rvalue
lvalue
smart pointer
shared_ptr
unique_ptr
weak_ptr
template
class 类
成员函数
= default
= delete
类类型
final
iterator 迭代器
auto
begin
表达式
lambda 表达式
range for 范围for
regular 正则化表达式
内存管理
move
new / delete
异常
noexcept
变量与基本类型
内置类型
基本类型
算数类型
算数逻辑类型
无符号
unsigned xx
有符号
整型
int
long long
浮点型
float
double
字符类型
char
string(cpp)
void 空类型
数组类型
数组指针
**数组指针和指针数组**```int *p1[5];// 指针数组int (*p2)[5]; // 数组指针int (*p3)[5]();// 返回数组的函数指针```【函数中的数组参数】```void foo(int a[]){}```
数组引用
以下是函数数组参数的三种传递方式数组默认传入首地址指针, 所以 Arr 和 Arr1 是等价的.而数组的引用则是对引用大小进行了限制说明.下面的三种输出方式也等价:*(arry + x) , a[x]```void Arr(int *a){ a[0] = 1;}void Arr1(int a[]){a[0] = 2;}// void Arr2(int &a[]){} //error: 被认为引用的数组报错 void Arr2(int(&a) [3]){a[0] = 3;}int main(){ int a[3] = {0,1,2}; Arr(a); cout
复合类型
指针
定义```int *a, *b;```获取对象地址```int i = 0;int *p = i; // p 存放 i 的地址, p指向变量 i 的指针double j;*p = j; // 错误,指针类型必须严格相对应```
指针操作
访问对象
```int val = 42;int *p = &val; // & 取出 val的地址,然后赋值给 *pcout
复合类型声明
```// int 前者数据类型, * / & 类型修饰符int i = 1024, *p = &i, &r = i;int* p; //合法但容易产生误导```
指针的指针
```int i = 1;int *p = &i;int **p = &p;//在理解对象类型时,时采用从右往左读的形式```
指针类型
空指针
```int *p1 = nullptr; // 等价于 int &*p = 0;int *p2 = 0;int *p3 = NULL; // 在 中被定义int x = 0;int *p4= x;// 错误,此 0 非彼 0,一个表示空指针一个表示数值,而数值和空指针的数据内部表示方式是不一样的[计组]```nullptr 为 C++11 新标准,NULL为预处理变量,即在编译之前的预处理阶段进行宏替换。而一般要少用 #define 宏替换类型,因为会导致调试报错问题找不到对应位置其次不使用 p = 0 是因为,在编程过程中,不知道 p 到底表示的是数值还是数值指针。所以用 nullptr 就解决上述的两个问题
void*
一种特殊的指针类型,用于存放任意对象的地址
野指针
【野指针】访问一个**已销毁**或者**访问受限的内存区域**的指针,野指针不能判断是否为NULL来避免
垂悬指针
【垂悬指针】指针正常初始化,曾指向一个对象,该对象被销毁了,但是指针未制空,那么就成了悬空指针
函数指针
【**什么是函数指针?**】 如果在程序中定义了一个函数,那么再编译时系统就会为这个函数代码分配一段存储空间,**这段存储空间的首地址称为这个函数的地址,而且函数名表示的就是这个地址**。 定义一个指针变量来存放这个地址,这个指针变量就叫做**函数指针变量**,简称**函数指针**【用途】 调用函数和做函数的参数,比如回调函数【定义】``` 函数返回值类型 (* 指针变量名) (函数参数列表); int (*p)(int a, int b); // 此函数指针 p 的类型即为 int(*)(int, int);``` 但是这里需要注意的是:“(*指针变量名)”两端的括号不能省略,括号改变了运算符的优先级。如果省略了括号,就不是定义函数指针而是一个函数声明了,即声明了一个**返回值类型为指针型的函数**。 注意,指向函数的指针变量没有 ++ 和 -- 运算。【怎么判断一个指针变量是**指向变量的指针变量**还是**指向函数的指针变量**呢?】主要通过看括号 (变量类型) (*p) (); 函数指针, 变量类型 *p,变量指针.
数组指针
**数组指针和指针数组**```int *p1[5];// 指针数组int (*p2)[5]; // 数组指针int (*p3)[5]();// 返回数组的函数指针```
函数指针数组
形如```char* (*fun[3])(char* p);```即该函数指针是一个 指针数组,能够包含三个函数指针.
智能指针
【什么是智能指针】 智能指针是一个**类**,它封装了一个原始的C++指针,**主要用于管理在堆上分配的内存。**没有单一的智能指针类型,但所有这些都尝试以实用的方式抽象原始指针。智能指针应优于原始指针,因为其可以缓解原始指针的许多问题,主要是忘记删除对象和泄露内存。 **简单地说,它是一种可以像指针一样使用的值,但提供了自动内存管理的附加功能:当指针不再使用时,它指向的内存被释放,从而防止内存泄漏。** 当类中有指针成员时,一般有两种方式来**管理指针成员**,一是**采用值型的方式管理**,每个类对象都保留一份指针指向的对象的拷贝,另一种更优雅的方式是**使用智能指针,从而实现指针指向的对象的共享**。【为什么要使用智能指针】 智能指针的作用是管理一个指针,因为存在以下这种情况: 申请的空间在函数结束时忘记释放,造成**内存泄漏**。使用智能指针可以很大程度上的避免这个问题,因为智能指针就是一个类,当超出了类的作用域是,类会自动调用析构函数,析构函数会自动释放资源。 所以智能指针的作用原理就是在函数结束时自动释放内存空间,不需要手动释放内存空间。【何时应该使用智能指针】 在代码中设计跟踪一块内存的所有权,分配或者取消分配; 使用智能指针通常可以省掉这些操作。【应该如何使用常规指针】 主要在忽略内存所有权的代码中,这通常是在从其他地方获取了指针,并且不进行分配,解除分配或存储器执行更长的指针的副本的函数中。
auto_ptr
auto_ptr(c++98的方案,cpp11已经抛弃)采用所有权模式```c++auto_ptr p1 (new string ('I reigned.”));auto_ptr p2;p2 = p1; //auto_ptr不会报错.```此时不会报错,p2剥夺了p1的所有权,但是当程序运行时访问p1将会报错。所以**auto_ptr的缺点是:存在潜在的内存崩溃问题**
unique_ptr
【unique\_ptr(替换auto_ptr)】 unique_ptr**实现独占式拥有或严格拥有概念,保证同一时间内只有一个智能指针可以指向该对象**。它对于避免资源泄露(例如“以new创建对象后因为发生异常而忘记调用delete”)特别有用。 不能拷贝构造,但是可以**移动构造和移动赋值**。 采用所有权模式``` unique_ptr p3 (new string ('auto')); unique_ptr p4; p4 = p3;//此时会报错!!``` 编译器认为p4=p3非法,避免了p3不再指向有效数据的问题。因此,unique\_ptr比auto\_ptr更安全。 当程序试图将一个 unique\_ptr 赋值给另一个时,如果源 unique\_ptr 是个临时右值,编译器允许这么做;如果源 unique\_ptr 将存在一段时间,编译器将禁止这么做 unique\_ptr不能直接复制,必须使用`std::move()`转移其管理的指针,转移后原 unique\_ptr 为空``` unique_ptr ps1(new int(1)), ps2, ps3; ps3 = ps1; // 不可以 ps2 = move(ps1); // 可以 if(ps1 == nullptr) cout p1(new string('hi')); unique_ptr p2(p1.release()); // 将p1置为空,返回指针,p2当前是hi cout p3(new string('hello,world')); p2.reset(p3.release()); // reset释放了p2原来指向的内存 // 然后令p2指向p3所指向的对象,然后release()将p3置为空 if(p1 != nullptr) // p1已经被释放了,没有了 cout
shared_ptr
【shared_ptr】c++11最常用的智能指针类型为shared\_ptr,它采用**引用计数**。shared_ptr**实现共享式拥有概念**。对shared_ptr进行初始化时不能将一个普通指针直接赋值给智能指针,因为一个是指针,一个是类。可以通过make_shared函数或者通过构造函数传入普通指针。并可以通过get函数获得普通指针。多个智能指针可以指向相同对象,该对象和其相关资源会在**“最后一个引用被销毁”时候释放**。从名字share就可以看出了资源可以被多个指针共享,它使用**计数机制**来表明资源被几个指针共享。当计数等于0时,资源会被释放。除了可以通过new来构造,还可以通过传入auto\_ptr, unique\_ptr,weak\_ptr来构造。当我们调用release()时,当前指针会释放资源所有权,计数减一。shared\_ptr 是为了解决 auto\_ptr 在对象所有权上的局限性(auto\_ptr 是独占的), 在使用引用计数的机制上提供了可以共享所有权的智能指针。- **成员函数**- use_count 返回引用计数的个数,资源的所有者个数 - unique 返回是否是独占所有权( use_count 为 1) - swap 交换两个 shared_ptr 对象(即交换所拥有的对象) - reset 放弃内部对象的所有权或拥有对象的变更, 会引起原有对象的引用计数的减少 - get 返回内部对象(指针), 由于已经重载了()方法, 因此和直接使用对象是一样的.如 `shared_ptr sp(new int(1));` `sp` 与 `sp.get()`是等价的
weak_ptr
- **只引用,不计数**,但是有没有,要检查`expired()`应运而生。 weak\_ptr **是一种不控制对象生命周期的智能指针**, 它指向一个 shared\_ptr 管理的对象..进行该对象的内存管理的是那个强引用的 shared\_ptr. weak\_ptr只是提供了对管理对象的一个访问手段。 weak\_ptr 设计的目的是为配合 shared\_ptr 而引入的一种智能指针来协助 shared\_ptr 工作, 它只可以从一个 shared\_ptr 或另一个 weak\_ptr 对象构造, **它的构造和析构不会引起引用记数的增加或减少**。 **weak\_ptr是用来解决shared_ptr相互引用时的死锁问题**。如果说两个shared\_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。它是对对象的一种弱引用,**不会增加对象的引用计数**,和shared\_ptr之间可以相互转化,shared\_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared\_ptr。 - weak\_ptr调用lock函数转换成shared_ptr lock() 会检查`weak\_ptr`所指向的对象是否存在,如果存在就返回一个共享对象shared\_ptr。 ```c++ shared_ptr pa(new A()); shared_ptr p = pa->pb_.lock(); // pb_ 是 weak_ptr ``` 资源无法释放,use_count都为2 解决办法只需要A,B其中一个的 pb\_,或者pa\_类型为 weak\_ptr weak\_ptr 的构造和析构不会引起引用记数的增加或减少 ```c++ struct Node{ shared_ptr pPre; shared_ptr pNext; int val; }; void func(){ shared_ptr p1(new Node()); shared_ptr p2(new Node()); cout pNext = p2; p2->pPre = p1; cout
this 指针
指针与地址
指针与地址的联系主要通过取址符来建立.```int a[2] ;int *p = &a; //将 a 的地址传给 pint &p1[2] = a; // error:会认为引用的数组int (&p1)[2] = a; // 这里是传引用``` 指针的增加默认按照 类型大小进行地址移动
引用
引用的好处在于,同样效果,减少拷贝的处理,提高运行速度(也就是常用的浅拷贝方法)
右值引用
C++对于左值和右值没有标准定义,但是有一个被广泛认同的说法: 可以取地址的,有名字的,非临时的就是左值;不能取地址的,没有名字的,临时的就是右值;可见立即数,函数返回的值等都是右值;而非匿名对象(包括变量),函数返回的引用,const对象等都是左值。从本质上理解,创建和销毁由编译器幕后控制,程序员只能确保在本行代码有效的,就是右值(包括立即数);而用户创建的,通过作用域规则可知其生存期的,就是左值(包括函数返回的局部变量的引用以及const对象)。
左值引用
左值引用要求右边的值必须能够取地址,如果无法取地址,可以用常引用;但使用常引用后,我们只能通过引用来读取数据,无法去修改数据,因为其被const修饰成常量引用了```int i = 1024;int &ref = i; // ref 指向 i (i 的另一个别名)// int &ref; // 报错,引用必须被初始化double pi = 3.14;int &ref = pi; //报错,引用类型和绑定类型不一致// int &var = 10; // 报错,无法对立即数取地址const int &var = 10;/* 等同于 创建了临时常量,再进行引用const int temp = 10;const int &var = temp;*/```
左值与右值
当一个对象被用作右值时,用的是对象的值(内容);当对象被用作左值的时候,用的是对象的身份(内存中的位置)```//左值运算1.赋值运算(非常量赋值)2.取地址符(指针是右值)3.内置解引用、下标,的求职结果是佐治4.内置类型和迭代器的递增底建运算符```
枚举类型 enum
字面值常量
转义符
“\n”换行符,光标到下行行首,即在下一行输出;“\r”为回车符,光标到本行行首,即把字符显示到本行开始的位置,即“\r”后面的字符会替换覆盖本行“\r”前面的字符;“\t”为制表符(跳到下一个Tab位置),每个数据之间默认是8个字符。例如分别显示在列的第0、8、16、24位等;“\f”是分页符;“\b”是退格符,“\b”后面的字符会向“\b”前退一个字符,即“\b”后面的字符会替代“\b”前面的一个字符;“\”代表一个“\”;“’”代表一个单引号“’”;“'”代表一个双引号“'”;“\0”是字符串的结束标志,字符串到“\0”处结束,“\0”后面的字符不再执行;
类型
前缀
Unicode 16 / u
Unicode 32 / U
后缀
unsigned / u
long / l
long long / ll
long double / L
整型和浮点型
```整型20 /* dec */024 /* oct */0x24 /* hex */浮点型3.1415E0 //[E 后面为幂]3.1415e0```
字符/字符串
```'a' //字符'Hello world' // 字符串```
变量操作
初始化
初始值
变量初始化虽然有 =, 但并非一种特殊赋值。在其他语言中可能一致,但 C++ 中两者不能混为一谈```double price = 109;```赋值是**值擦除**然后用新值代替,如果初始化等于赋值,说明该变量有默认初始化值。
列表初始化
```int u = 0;int u = {0};int u{0};//利用花括号进行初始化操作```
默认初始化
与编译器有关,但是最好不要依赖该操作
声明与定义
C++ 支持分离式编译机制,声明便是表示该变量在别处被定义,用于多文件共用变量时有好处```extern int i; // 声明且非定义int j; // 声明并定义extern int i = 1;// 声明且定义```从编译原理上来说,**声明**是仅仅告诉编译器,有个某类型的变量会被使用,但是编译器并**不会**为它**分配任何内存**,**而定义就是分配了内存**。```//函数体内部声明初始化不合法void foo(){extern int j = 1;}```声明与定义的具体区别``` 一种是需要建立存储空间的。例如:int a 在声明的时候就已经建立了存储空间。 另一种是不需要建立存储空间的。 例如:extern int a 其中变量a是在别的文件中定义的。 声明是向编译器介绍名字--标识符。它告诉编译器“这个函数或变量在某处可找到,它的模样象什么”。 而定义是说:“在这里建立变量”或“在这里建立函数”。它为名字分配存储空间。无论定义的是函数还是变量,编译器都要为它们在定义点分配存储空间。 对于变量,编译器确定变量的大小,然后在内存中开辟空间来保存其数据,对于函数,编译器会生成代码,这些代码最终也要占用一定的内存。 总之就是:把建立空间的声明成为“定义”,把不需要建立存储空间的成为“声明”。 基本类型变量的声明和定义(初始化)是同时产生的;而对于对象来说,声明和定义是分开的。```
命名标识符
标识符规则``` 标识符由字母、数字、下划线“_”组成。 不能把C++关键字作为标识符。 标识符长度限制32字符。 标识符对大小写敏感。 首字符只能是字母或下划线,不能是数字。```常见书写格式```- 变量名小写字母- 类名大写开头 Sales_items- 多单词类型,单词间有明显区分 stu_a , stuA```
作用域
块作用域
块是用一对花括号{}括起来的代码区域,例如,整个函数体是一个块,而一个复合语句也是一个块。块作用域的可见范围是从**定义处**到包含该**定义的块的末尾**```vectoru,v;void func(){ for(auto x : u){ int i = 0; for(auto y : v){ cout
全局作用域
文件作用域中的变量,从定义处到该定义所在文件的末尾均可见,文件作用域变量也被称为全局变量.全局作用域主要指 文件内函数以外```int i = 0; // 全局变量int main(){cout
作用域嵌套
```int re = 1;//全局变量int main(){int u = 0;//输出全局变量std::cout
存储期
一般变量的存储是自动的,对于函数来说,如果函数执行完毕,如果没有特殊情况,在离开变量作用域时,作用域中的变量内存就会被回收,也就是它们的地址不再有效。不过所谓的**静态变量**在存储上有所不同,静态的意思是该变量在内存中原地不动,静态变量又可分为**块作用域**和**外部链接**两种
块作用域静态变量
我们使用static关键字指出一个变量是静态的,静态变量在该函数首次调用时被初始化且只初始化一次,当 static_add() 函数执行完毕后,它的 cnt 变量仍会保留.```void add(){ int cnt = 0; cnt += 1; cout
外部链接静态变量
外部链接的变量多个文件不能有相同的类型定义.```#include 'value.h' // 含有 s 的头文件extern const char* s; // 使用外部文件的变量声明,等同于全局变量void foo(){ cout
类型处理
typedef 关键字
**关键字 typedef**```typedef double wages; // dou 与 double 同义typedef wages base, *p; // base 与 double同义, p 为 double * 同义词```别名声明```using SI = Sales_item;wages hourly, weekly;SI item;typedef char *pstring;const pstring cstr = 0; // cstr 是 char 的常量指针const pstring *ps; // ps是一个指针,指向char的常量指针const char *cstr ≠ const pstring cstr;指向常量的指针 - 常量指针```
auto 类型
auto 是 C++编译器自动分析表达式所属的类型让编译器通过初始值来推算变量的类型; 因此 auto 定义的变量必须具有初始值
decltype 函数
decltype 作用是选择并返回操作数的数据类型```decltype(f()) sum = x; // sum的类型即函数 f()的返回类型int i = 1;decltype((i)) d; // d 为 int&; 双层括号表示引用declttpe(i) e; // e 为 int```
size_t 类型
size\_t 类型定义在cstddef头文件中,该文件是C标准库的头文件stddef.h的C++版。它是一个与机器相关的unsigned类型,其大小足以保证存储内存中对象的大小。```例如:bitset的size操作返回bitset对象中二进制位中1的个数,返回值类型是size_t。例如:在用下标访问元素时,vector使用vector::size_type作为下标类型,而数组下标的正确类型则是size_t。vector使用的下标实际也是size_t,源码是typedef size_t size_type```
const 限定符
```const int MAX = 1e3;MAX = 1;// 错误:const 对象不能赋值// const 常量必须初始化,初始化 ≠ 赋值const int i = get_size();const int j = 42;const int k; // 未初始化//const 只在文件内部有效,要在多个文件生效就需要 extern 声明方式使用变量//多文件中唯一定义,其余声明和使用file_1.ccextern const int a = fuc();file_2.hextern const int a;```const 的目的实际上就是为了区分对象的读写权限。比如有一个文件,我能读写,但是只允许其他人读而不能写,我就可以分配一个 const 文件引用,或者 const 文件指针 给他们。引用的好处就是少一次解引用操作,更加直观的使用。
const 常量
const 引用
又被称为 '常量引用',由于引用是别名,因此该const指顶层const.【注意理解 一种引用和两种指针的区别】引用类型一般需要域其所引用对象的类型一致,但是有两个例外。```int i = 1;const int &r = i; // 常量引用对象/变量const int &r2 = 1; // 常量引用, 常量引用常量const int &r3 = r1 * 2; // 例外1:表达式作为常量引用初始值int &r4 = r1 * 2; // 错误, 普通非常量引用没有这种功能double Pi = 3.14;const int &pi = Pi; // 不同类型的常量引用,编译器自动补足语句// const int temp = Pi; const int &pi = temp; temp 为临时对象,属于编译器未命名对象```
const 指针
指向常量的指针[但指针本事不一定是常量]常量只能用 const 指针 指向而非常量只能用 非const指针 指向```const double pi = 3.14; //常量,值不能改变double *ptr = π // 错误,ptr 是一个普通指针,普通指针有修改所指对象值的权力,与常量的定义之间有矛盾冲突,所以不可以const double *cptr = π //正确*cptr = 42; //常量指针不能赋值或者修改其所指对象的数值cptr = nullptr; //正确,指向常量的指针式能够被修改的```常量指针```int err = 0;int *const curErr = &err; //const int * pi 区别? curErr 将一直指向 err 且不能被修改const double pi = 3.14;const double *const pip = π// 指向常量对象的常量指针```理解的核心方法就是从右往左阅读int *const curErr; 表示 const curErr 常量对象,int * 表示 int 类型的指针,所以连起来为 指向常量对象的 非常量 int 指针;而对于 const double *const pip 即为,常量对象 pip 的 double 类型常量指针。
顶层/底层 const
对于指针分为两类讨论,实际上源于:指针本事是一个对象,它又可以指向另外一个对象。因此被区分为顶层const和底层const两个问题顶层const:表示指针本身是常量底层const:表示指针所指对象是常量```int i = 0;int *const p1 = &i; // 不能改变 p1 值, 顶层 constconst int ci = 1;// 不能改变 ci值,顶层 constconst int *p2 = & ci;//允许改变 p2,底层 constconst int *const p3 = p2;//左底层,右顶层const int &r = ci; // 均为底层 const```
常量表达式
常量表达式```const int max = 20; //常量表达式const int min = max - 1;//常量表达式int size = 20;const int sz = get_size(); // 常量表达式```constexpr 变量```constexpr int max = 20; //常量表达式constexpr int min = max - 1;//常量表达式constexpr int sz = get_size(); // 常量表达式【注意 const 和 constexpr 区别】const int *p ≠ constexpr int *p```字面值类型
const / constexptr
常量表达式是指值不会改变并且在编译过程中就能得到计算结果的表达式。编译过程中得到计算结果。**字面值属于常量表达式**,用常量表达式初始化的const对象也是常量表达式。一个对象(或表达式)是不是常量表达式由它的数据类型和初始值共同决定。```(注意!!!)对于这条语句:const int sz = get_size();,sz本身是常量,但它的具体值直到运行时才能获得,不是常量表达式。```const 与 constexptr常量与常量表达式```const 有两层,顶层和底层constconst int a = get(); /* 表示 a 是一个常量,但是get()返回结果是运行过程中得到的,在不同允许条件下 a 的常量值结果不同,所以仅为常量而常量表达式的限制更高,其中右侧赋值表达式本身不会受允许条件而发生改变。因此字面值也是常量表达式的一种*/```
static 关键字
(1)在修饰变量的时候,static 修饰的静态局部变量只执行初始化一次,而且延长了局部变量的生命周期,直到程序运行结束以后才释放。(2)static 修饰全局变量的时候,这个全局变量只能在本文件中访问,不能在其它文件中访问,即便是 extern 外部声明也不可以。(3)static 修饰一个函数,则这个函数的只能在本文件中调用,不能被其他文件调用。static 修饰的变量存放在全局数据区的静态变量区,包括全局静态变量和局部静态变量,都在全局数据区分配内存。初始化的时候自动初始化为 0。(4)不想被释放的时候,可以使用static修饰。比如修饰函数中存放在栈空间的数组。如果不想让这个数组在函数调用结束释放可以使用 static 修饰。(5)考虑到数据安全性(当程序想要使用全局变量的时候应该先考虑使用 static)
类型转换
隐式转换
由编译器自动转换运算对象的类型```1.非bool -> bool2.初始化,直接转化为对于变量类型3.函数调用时发生
算术转换
1.整型提升:小转大```3.14L + 'a'// 'a' 从 char 转为 int,值为对饮字符码的数值。即 int(97)+long double(3.14)=100.14L```
其他转换
数组转换为指针```int ia[10];int* ip = ia; // ia被转化为数组首元素指针```指针转换```0 -> nullptr -> void* ```转化为bool```while(cin>>s){}// IO库定义了从istream向布尔值转换的规则。如果最后一次读入成功,则转换得到的布尔值是true;相反则是 false```转化为常量类类型转换
显示转换
强制类型转换 cast
```转换形式例如 double slope = i/j;```命名强制类型转换```cast-name(expression);cast-name:static_cast \ dynamic_cast \ const_cast \ reniterpret_cast```reniterpret_cast:提供位模式的重新解释
static_cast
static_cast: 具有明确意义的类型转换。且不包含底层const```void* p = &d; // 任何非常量对象的地址能够存入void*中double *dp = static_cast(p);```静态转换(1)主要用于内置数据类型之间的相互转换;(2)用于自定义类时,静态转换会判断转换类型之间的关系,如果转换类型之间没有任何关系,则编译器会报错,不可转换;(3)把void类型指针转为目标类型指针(不安全)。```继承类能够相互转换A *pA = new A;B *pB = static_cast(pA); // 编译不会报错, B类继承于A类```
dynamic_cast
```1)dynamic_cast是运行时处理的,运行时要进行类型检查,而其他三种都是编译时完成的;2)不能用于内置基本数据类型间的强制转换;3)使用dynamic_cast进行转换时,基类中一定要有虚函数,否则编译不通过;4)dynamic_cast转换若成功,返回的是指向类的指针或引用;若失败则会返回NULL;5)在类的转换时,在类层次间进行上行转换时,dynamic_cast和static_cast的效果是一样的。在进行下行转换时,dynamic_cast具有类型检查的功能,比static_cast更安全。向下转换的成败取决于将要转换的类型,即要强制转换的指针所指向的对象实际类型与将要转换后的类型一定要相同,否则转换失败```
const_cast
const_cast:只能改变对象的底层const有三个作用:1.常量指针 被强转为 非常量指针,且仍然指向原来的对象;2.常量引用 被强转为 非常量引用,且仍然指向原来的对象;3.常量对象 被强转为 非常量对象```const char *pc;char *p = const_cast(pc);//去除底层const性质```
reniterpret_cast
```int *ip;char *pc = reinterpret_cast(ip);//编译不报错,但是运行会出现异常,最好不要碰这个玩意```**建议:避免强制类型转换**
旧式强制类型转换
```type (expr); //函数形式强制类型转换(type) expr; //C语言风格的强制类型转换char *pc = (char*) ip; // ip是指向整数的指针,效果和reinterpret_cast一样//与命名强制类型转换相比,旧式转换表现形式不那么明显,出现问题追踪时更加困难```
类型转换
类型转换涉及到编译器的处理,因此应该减少与移植性相关的编写
bool -> 非 bool
初始值 0 -> false初始值非 0 -> truefalse -> 0true -> 1
浮点 -> 整数
保留整数部分,高位取余有精度损失
无符号 与 有符号
内部编码的方式
隐式类型转换
对于内置类型,低精度的变量给高精度变量赋值会发生隐式类型转换,**对于只存在单个参数的构造函数的对象构造来说**,函数调用可以直接使用该参数传入,编译器会自动调用其构造函数生成**临时对象**。用explicit可以防止类的隐式类型转换
自定义类型
class 类
template 模板
struct 结构体
语法结构```struct Sales_data{std::string bookNo;unsigned units_sold = 0;double revenue = 0.0;};Sales_data accum, trans, *salesptr;```
C与C++的struct的区别
C++中的struct是对C中的struct进行了扩充,所以增加了很多功能1. 声明时的区别 | | C | C++ | | -------- | ---------------------- | ------------------ | | 成员函数 | 不能 | 可以 | | 静态成员 | 不能 | 可以 | | 防控属性 | 默认public,不能修改 | 有三种,默认public | | 继承关系 | 不可以继承 | 可从其他结构体继承 | | 初始化 | 不能直接初始化数据成员 | 可以 |2. 使用过程中的区别 **在C中使用结构体时需要加上struct,或者对结构体使用typedef取别名,而C++可直接使用**,例如: ```c++ 结构体声明,C和C++使用同一个 struct Student{ int iAgeNum; string strName; } typedef struct Student Student2;//C中取别名 struct Student stu1; //C中正常使用 Student2 stu2; //C中通过取别名的使用 Student stu3; //C++使用 ```3. 模板中的使用 class这个关键字还可用于定义模板参数,就像typename。但是strcut不用与定义模板参数,例如: ```c++ template //可以把typename 换成 class int Func( const T& t, const Y& y ){ //TODO } ```
结构体⾥⾯有没有构造函数和析构函数
可以有,和CALSS区别如下.class这个关键字还⽤于定义模板参 数,就像“typename”。但关键字“struct”不⽤于定义模板参数。class中默认继承⽅式是private,⽽struct的默认继承⽅式是publicclass中默认成员访问权限是private,⽽struct的默认访问权限是public
常见运算符
基本概念
运算符分类
一元运算符
```*p [解引用], &p[取地址]```
二元运算符
```a * b // 乘以a && b // 逻辑运算 - 与```
三元运算符
```条件运算符a ? b : c可嵌套finalgrade = (grade > 90) ? 'high pass' : (grade
运算符重载
优先级与结合律
复合表达式、表达式转换【波兰/逆波兰式】```// 括号最高级优先级int last = *(ia + 4);last = *ia + 4;```求值顺序```int i = f1() * f2(); // f1(),f2()哪一个优先调用?cout
图表
运算符
溢出与运算异常
//除零异常 与 溢出```在不同系统中,溢出计算带来的结果可能不同甚至崩溃涮熟运算的核心还是其在OS内部的操作码表示方法```
关系运算符
```if(i
赋值运算符
```// 初始化int i = 0, j = 0, k = 0;const int ci = i;// 非法赋值1024 = k;i + j = k;ci = k;// C++11 花括号初始化k = {3.14}vectorvi = {0,1,2,...,9};```赋值运算满足右结合律```int i, j;i = j = 0;// 从右往左运算```赋值运算的优先级较低```while(i != 42){ i = get_value(); }// 更好的改写方式while((i = get_value()) != 42){}```赋值与相等运算符```= , == // 新手毛病```
递增与递减运算符
前置 / 后置```i = ++j;i = j++;优先使用前置递增1.必满不必要的编程判断逻辑2.后置需要把原始值存储下来便于返回未修改内容,如果不需要修改,后置操作就是一种浪费。```后置递增 > 解引用运算符```*pbeg++ 等于 *(pbeg++)// 即先增加再输出for(auto it = s.begin(); it != s.end() && !isspace(*it); ++it) *it = toupper(*it);再用 while 采取看似等价的方法while(beg != s.end() && !isspace(*beg))*beg = toupper(*beg++);//赋值语句未定义```
成员访问符
```箭头运算符ptr->mem 等价于 (*ptr).mem点运算符ptr.mem```
位运算符
```> 右移& 与,| 或,异或 ^```IO 运算符```满足左结合律```((cout
sizeof运算符
```sizeof() 返回 size_t 类型的常量表达式利用sizeof获得数组元素个数constexpr size_t sz = sizeof(ia)/sizeof(*ia);```
表达式与语句
简单语句
空语句
复合语句
条件语句
if else
```注意花括号:悬垂 else 语法else 与作用域中最近的一个 if 相匹配if ( a > 0) if( a == 0)else sum ++;//这里的else 匹配 if(a == 0)```
switch
case 标签```case 标签: 整型常量表达式case 3.14: //err:不是一个整数case ival: //err:不是一个常量case 'a': case true:```case 与对应的 break 语句```case 'x': break;case 'a': case 'e': break; //写在同一行也是合法方式```defult 语句```除条件判断case外,均会执行defult语句,因此只有一个,且必须具有defult结束.defult: ; //没有内容则增加空语句
循环语句
while
do while
for(init ; con ; exp)
范围 for(auto: )
跳转语句
goto
break
continue
异常处理
try / catch
语法```try{//程序段内容}catch(异常声明){//处理语句}catch(异常声明){处理语句}```异常处理顺序先将异常压栈处理,然后处理顺序从栈底回退到栈顶**编写关于异常的安全代码非常困难**异常处理后会简单的终止程序,但是如果还需要程序继续的进行下去,则需要对内存资源管理具有相当的知识才能处理。
throw
对于出现异常的部分不通过语句输出的方式得出结果,而是通过扔出一条异常来得到;异常的抛出会使得该部分程序停止,而语句输出则会继续执行```throw runtime_error('message')```
语句作用域
块
语句
函数操作
概念
返回类型
形参 / 实参
```void f2(void){} // 显式定义int f3(int a, b){} // 必须把所有类型都写出来```
函数体
调用运算符
局部对象
**对象生命周期**```局部变量会隐藏外层作用域的其他所有同名声明```自动对象```函数开始时,为形参申请存储空间。如果不含有初始值,则将进行默认初始化。```局部静态对象``````
声明 / 定义
```void sum(int a,int b);//这是函数的声明void sum(int a,int b){} // 函数定义```**声明在定义前****定义有函数体{},且没有分号结尾****声明不会分配空间,定义时才会分配空间**
参数传递
引用传递 / 值传递
引用的好处```1.使用引用避免拷贝2.通过引用返回额外信息```
指针形参
指针的形参是拷贝指针所指内容;在函数中修改形参时,外实参不会改变
管理
使用标记指定数组长度```// 对于C风格字符串,末尾会携带'\0' 空字符void print(const char *cp){ if(cp)while(*cp) // 空字符判结尾cout void print(const int ia[], size_t size){for (size_t i = 0; i != size; i++){}}// 函数调用print(j, end(j)-begin(j));```
const 形参与实参
注意const 顶层与底层不同的限制```const int a
数组形参
1.数组传参不会拷贝,会自动转化为首元素指针传递```void print(const int*);void print(const int[]);void print(const int[10]);// 虽然标注大小,但是实际上没有限定上面三种表达式的实际运行逻辑等价而大小限制则是通过引用的方式标注```2.多维数组传递```int (*arr)[10] // 指向含有 10 个整数的数组指针,等价于 int arr[][10]int *arr[10] // 10 个指针构成的数组//理解上,都是从右往左阅读```
数组引用
```void print(int (&arr)[10]){for ( auto elem : arr)cout
main参数
```1.空形参列表int main(){}2.可传入int main(int argc,char **argv){}argc:数组字符串数量argv:数组```
可变形参函数
C++11 提供 initializer_list```initializer_listlst;initializer_listlst{a,b,..};lst2(lst) // 拷贝挥着赋值lst2 = lst // 和原始列表副本共享元素lst.size()lst.begin()lst.end()```
返回 / return
基础表达式```return;return exp;```不要返回局部对象的**引用或指针**只能传值,要么不返回```const string &f(){string ret = 'hello';return ret; // 错误:返回局部对象引用return 'hello'; //错误:返回局部临时量}//上述return语句均返回未定义值```
无返回值函数
隐式地执行return```void f(){// 编译器隐藏 return void;}```
有返回值函数
在编写了 return 后,编译器就不会再默认结尾处添加 return 函数```bool str(){if()return ; // 返回值类型不匹配报错else// 省略返回值 会报错}```
返回左值
```char &get_val(string &str, string::size_type ix){return str[ix];}int main(){string s('value');get_val(s,0) = 'A'; // 将s[0]的值改为A// 实际上应该等价于 s[0] = 'A'; get_val的用途就是返回引用地址}
列表初始化返回值
```vector p(){return {'Hello','World'};}```
主函数 main 返回值
```#include int main(){if(t)return EXIT_FAILUE; // 预处理变量不能在其前面添加 std::, 和 using 声明elsereturn EXIT_SUCCESS;```
返回数组指针
```typedef int arr[10]; // arr 是类型别名,表示类型含有10个整数的数组using arr = int[10]; // 和上述声明等价arr *func(int i);// 返回指向含有10个整数的数组指针/*必须牢记*/int arr[10];int *pl[10]; //数组指针int (*p2)[10] = &arr; // 指针,指向有10个整数的数组```对于不确定元素类型使用 decltype```decltype(odd) *arrPtr(int i){}```
尾置返回类型
C++11新标准```// 接受一个int类型的实参,返回一个指针,该指针指向含有10个整数的数组auto func(int i) -> int(*)[10];```
默认实参
在没有输入参数时默认初始化值【实例】```typedef string::size_type sz;string screen(sz ht = 24, sz wid = 90);```
语言特性
内联函数
以空间换时间;由于函数在运行中需要调用资源,会额外耗时,而内敛函数则是将函数视为一个表达式处理。因此能够节省作为函数调用的时间,缺点是每一次使用该函数,就需要一次内容复写,最好避免递归调用。
constexpr函数
constexpr 函数,返回的类型和所有形参都是字面值类型,且函数体中有且仅有一条return 语句
const 函数
```void function() const{}1.const的成员 不能访问非const的函数2.const函数不能修改其数据成员```
函数指针
函数指针就指向函数```bool (*pf)(const string &,const string &); // 未初始化,引用必须初始化,而指针未初始化会有安全漏洞bool *pf(const string &,const string &); // 函数返回 bool*```函数指针使用```bool foo(const string &,const string &){};pf = foo;pf = &fpp;// 等价赋值语句,取址符可以选择bool x1 = pf('a','b'); // 调用foo函数bool x2 = (*pf)('a','b');bool x3 = foo('a','b');//三句都等价```
函数重载
同名函数但形参列表不同称为,重载函数```main 函数不能重载重载的区别在于 数量和类型【与名称、返回类型、顶层const 无关】```const_cast ```string &short(string &s1,string &s2){auto &r = short(const_cast(s1),const_cast(s2));return const_cast(r);}```重载与作用域在C++中,名字查早发生在类型检查之前```void print(double);void foo(){bool read = false;string s = read();void print(int); // 新作用域:对之前的print进行隐藏print(3.14); // 调用 print(int)而非print(double)}```
调试
assert 预处理宏
NDEBUG 预处理变量
了解STL基础
命名空间 using
using 声明```using namespace::name;```头文件不应包含using声明:头文件中使用using声明,由于头文件被包含于其他文件中,则using声明会被多次使用,导致命名冲突。
string 对象
定义和初始化// 拷贝初始化 和 直接初始化```string s1; //初始化为空字符串string s2 = s1; // s2 是 s1的副本string s2(s1);string s3 = 'value'; // s3 为字符串字面值副本string s4(n,'c'); //s4的内容是 n个字符c组成的串```string 对象上的操作```cin>>s;cout ,>= //字典序比较```string 输入相关的细节内容```cin>>string; //读入时忽略前缀空白符和制表符等进行存储getline(cin,s);//读入直到换行符为止结束,包括空白;但string存储则会自动舍弃空白前缀```string::size_type 类型string对象的处理
C 风格字符串
C风格字符串,容易引发程序漏洞,是诸多安全问题的根本原因```#include strlen(p); //长度,检查到 '\0'停止strcmp(p1,p2); // p1 ? p2strcat(p1,p2); // 将p2连接到p1后strcpy(p1,p2); //将p2拷贝给p1,返回p1strlen会引发安全漏洞char ca[] = {'C','+','+'};cout
vector 容器
初始化```// 数组赋值int int_arr[] = {0,1,2,3,4,5};vector ivec(begin(int_arr), end(int_arr)); //利用首尾指针进行赋值vector v1;vector v2(v1);vector v2 = v1; //复制拷贝vector v3(n,val);vector v4(n);vector v5{a,b,c}vector v5={a,b,c};```其他基本操作```v.empty();v.size();v.push_back();v[n];v1 = v2;v1 = {a,b,c}v1 == v2v1 != v2
iterator 迭代器
容器迭代器的运算符```*iter //返回元素引用iter->mem //获取成员元素++iter //令iter指向容器中的下一元素--iter //指针指向iter1 == iter2iter1 != iter2```begin和end运算符```v.end(); //尾后迭代器v.begin(); //首迭代器v.cend(); //返回常量类型v.cend();```结合解引用和成员访问操作```(*it).empty()```迭代器运算```iter + niter - n
数组对象
```const char a4[6] = “Daniel”; // 错误:没有空间存放 常量字符串末尾的空字符 '\0'int a[] = {0,1,2};int a2[] = a; // 数组不能初始化数组a2 =a; //数组不能直接赋值//某些编译器支持数组的赋值,就是所谓的编译器扩展。一般来说最好避免使用非标准特性和vector一样int *ptrs[10];// ptrs是含有10个整型指针的数组int &refs[10]; // 错误:不存在引用的数组int (*Parray)[10] = &arr; // 指向一个含有10个整数的数组int (&arrRef)[10] = arr; // 引用一个含有10个整数的数组```数组与指针```string *p2 = nums; // 编译器将数组自动替换为其首元素的指针string *p = &num[0]; // 0位置元素的地址int ia[] = {0,1,2,...}; // ia 是一个含有10个整数的数组auto ia2(ia); //ia2 是一个整型指针,指向ia的第一个元素,auto推断ia2 = 42; //错误:ia2是一个指针,不能用int值给指针赋值```标准函数库 begin 和 end```int ia[] = {0,1,..,9};int *beg = begin(ia);int *last = end(ia);```指针运算```constexpr size_t sz = 5;int arr[sz] = {1,2,3,4,5};int *ip = arr; // 等价于 int *ip = &arr[0]int *ip2 = ip+4; // ip2指向arr的尾元素int *ip3 = ip+10; // 注意:指针越域auto n = end(arr)-begin(arr); // n 为5, 类型是ptrdiff_t的标准库类型,是一个带符号类型```多维数组初始化```int ia[3][4] = {0};int ia[3][4] = {{0},{4},{8}};```
指针与多维数组
多维数组理解为数组的数组```int ia[3][4]; // 大小为3,每个元素含有4个整数int (*p)[4] = ia;// 指针,指向的数组类型有4个整数int *ip[4]; // 整型指针的数组p = &ia[2]; // p 指向 ia 的(一维)尾元素```利用auto和begin/end函数,减少编程时的指针判断```for(auto p = begin(ia); p != end(ia); ++p){for(auto q = begin(*p); q != end(*p); ++q) cout
for 简化
```//利用C++11 auto 减化循环for(auto x : t){}//除最内层循环外,都需要使用引用进行循环```
C++标准库
IO库
IO类
``` 读写流 文件读写 字符串读写```IO类型之间的关系```标准库使得读入忽略不同类型流之间的差异,通过继承机制实现。通过模板 template ,可以使用继承关系类。```IO对象无拷贝或赋值```ofstream out1, out2;out1 = out2; // 错误```
条件状态
```while(cin >> val); // cin >> val 会返回流的状态,返回 true / false;```
输出缓冲
输出流管理一个缓冲区,用来保存程序读写数据。刷新输出缓冲区```cout
文件输入输出
暂时跳过该内容
string 流
```string str;getline(cin , str); istringstream record(line); // 记录绑定到刚读入的行```
顺序容器
容器:一些特定类型对象的集合常见类型```vector : [可变大小数组] / 随机访问 / 尾部之外位置插入或删除慢deque : [双端队列] / 随机访问 / 头尾操作快list : [双向链表] / 任何位置操作快forward_list : [单向链表] / 只支持单项顺序访问 array : [固定大小数组] / 不能删添string : 与 vector 类型,专用于保存字符```选择理由```1.一般默认 vector2.随机访问: vector / deque3.添删: list / forward_list4.两端添删:deque```
容器操作
类型别名:```iterator 迭代器const_iterator 可读,不可改迭代器size_type 无符号整型,容器大小[sizeof()返回值]difference_type 带符号,两迭代器之间距离value_type 元素类型reference 左值类型const_reference 元素const左值```构造函数```C c; // 默认构造C c1(c2); // 构造拷贝C c(b,e); // 范围拷贝:b,e为迭代器C c{a,b,c} // 列表初始化```赋值与swap```c1 = c2;c1 = {a,b,c};a.swap(b);swap(a,b);```大小```c.size();c.max_size();c.empty();```添加删除```c.insert(); // 拷贝进cc.emplace(); // 使用inits构造c.erase(); // 删除制定c.clear(); // 删除所有c.push_back();```迭代器```c.begin(),c.end()c.cbegin(),c.cend() // const_iterator反向容器c.rbegin(),c.end();c.crbegin(),c.crend();```
迭代器
默认元素范围 [begin,end)五种迭代器:输入~:只读不写,单遍扫描只能递增输出~:只写不读,单遍扫描只能递增前向~:可读写,多变扫描,只能递增双向~:可读写,多变扫描,可递增递减随机访问~:可读写,多变扫描,支持全部迭代器运算
前向迭代器
正向迭代器```*end() // 一般迭代器*cend() //常量迭代器*cbegin() // ```反向迭代器】从尾元素向首元素反向移动的迭代器```*crend()//逆向,有关键字 r++it; 反向迭代器向前移```
输出迭代器
swap函数
swap 不对任何元素进行拷贝、删除或者插入,swap只交换了两个容器的内部数据结构```因此swap会导致迭代器、引用、指针失效```
泛型算法
标准库提供的算法适用于各种类型的容器,因此称为泛型算法。泛型算法的核心是利用迭代器[统一接口]操作容器,```#include ```
基本算法
```1.求和int sum = accumulate(vec.cbegin(),vec.cend(),0);和初始值为0,求vec.begin -> vec.end 的元素和2.判等equal()3.填充fill()4.拷贝copy()```lambda表达式
排序
```排序sort(*begin,*end,cmp);去重unique(*begin,*end)```
lambda表达式
for_each算法
关联容器
两个主要的关联[映射]容器: map 和 set```头文件#include#include```关键字类型的比较函数```// 常量引用比较bool cmp(const A &a, const A &b){return a.x
pair类型
pair:两个类型构成的一个向量头文件```#include```操作```pair p;pair p{v1,v2};make_pair(v1,v2)p.first()p.second()```
关联容器操作
迭代器
```1.set迭代器是const类型,而map是非const类型2.遍历for(auto &tmp: set){}```
添加删除
```插入c.insert()替代c.emplace()```
无序容器
利用hash技术进行存储,即不按照关键字有序进行存储```unordered_map 和 unordered_set```无须容器允许重复关键字的版本而存储重复关键字的容器则是【桶】每个桶保存0或多个元素。在查找重复hash值的结果时,进入同种进程查找,如果一个桶中保存了很多元素,则查找单个元素需要大量比较操作```桶接口c.bucket_count()c.max_bucket_count()c.bucket()```hash重载```size_t hasher(const A &a){return hash() (a.func());}bool eql(const A &a,const A &b){return a.func() == b.func();}```
动态内存
编写程序中所使用的对象都有着严格定义的生存期```【全局对象】在程序启动时分配,在程序结束时销毁。【局部自动对象】,当我们进入其定义所在的程序块被创建时,离开块时销毁。而【局部static对象】则在第一次使用前分配,程序结束时销毁。C++的【动态分配对象】生存期与哪里创建无关,只有显式被释放才会销毁。```自由空间```程序中有内存池,该部分内存被称为自由空间(free store)或堆(heap)。程序堆来存储动态分配```动态内存与智能指针```new 分配空间,并返回对象指针delete 销毁动态对象指针```使用动态内存原因```1.程序不知道自己需要多少对象2.程序不知道所需对象的准确类型3.程序需要在多个对象间共享数据```
直接管理内存
使用 new 动态分配和初始化对象```int *pi = new int; // pi 指向一个动态分配的、未初始化的无名对象int *pi2 = new int(1024); // pi 指向对象值为 1024int *p3 = new int(); // 默认初始化为 0auto p1 = new auto(obj); // 自动初始化```内存耗尽```int *p2 = new (nothrow) int; // 如果分配失败, new 返回一个空指针int *p1 = new int; // 如果分配失败, new 抛出 std::bad_alloc;```delete 删除:对于 释放一块非new分配的内存,或者将相同的指针值释放多次,其行为是未定义的```delete 后重置指针值:指针被删除后被称为空悬指针。在指针被删除后,将nullptr赋值指针,就可以清除避免指针指向问题。但有时候,由于指针赋值,导致删除后,必须找到所有指向指针赋值。但是此操作非常困难。```
智能指针
【什么是智能指针】 智能指针是一个**类**,它封装了一个原始的C++指针,**主要用于管理在堆上分配的内存。**没有单一的智能指针类型,但所有这些都尝试以实用的方式抽象原始指针。智能指针应优于原始指针,因为其可以缓解原始指针的许多问题,主要是忘记删除对象和泄露内存。 **简单地说,它是一种可以像指针一样使用的值,但提供了自动内存管理的附加功能:当指针不再使用时,它指向的内存被释放,从而防止内存泄漏。** 当类中有指针成员时,一般有两种方式来**管理指针成员**,一是**采用值型的方式管理**,每个类对象都保留一份指针指向的对象的拷贝,另一种更优雅的方式是**使用智能指针,从而实现指针指向的对象的共享**。【为什么要使用智能指针】 智能指针的作用是管理一个指针,因为存在以下这种情况: 申请的空间在函数结束时忘记释放,造成**内存泄漏**。使用智能指针可以很大程度上的避免这个问题,因为智能指针就是一个类,当超出了类的作用域是,类会自动调用析构函数,析构函数会自动释放资源。 所以智能指针的作用原理就是在函数结束时自动释放内存空间,不需要手动释放内存空间。【何时应该使用智能指针】 在代码中设计跟踪一块内存的所有权,分配或者取消分配; 使用智能指针通常可以省掉这些操作。【应该如何使用常规指针】 主要在忽略内存所有权的代码中,这通常是在从其他地方获取了指针,并且不进行分配,解除分配或存储器执行更长的指针的副本的函数中。
auto_ptr
auto_ptr(c++98的方案,cpp11已经抛弃)采用所有权模式```c++auto_ptr p1 (new string ('I reigned.”));auto_ptr p2;p2 = p1; //auto_ptr不会报错.```此时不会报错,p2剥夺了p1的所有权,但是当程序运行时访问p1将会报错。所以**auto_ptr的缺点是:存在潜在的内存崩溃问题**
unique_ptr
【unique\_ptr(替换auto_ptr)】 unique_ptr**实现独占式拥有或严格拥有概念,保证同一时间内只有一个智能指针可以指向该对象**。它对于避免资源泄露(例如“以new创建对象后因为发生异常而忘记调用delete”)特别有用。 不能拷贝构造,但是可以**移动构造和移动赋值**。 采用所有权模式``` unique_ptr p3 (new string ('auto')); unique_ptr p4; p4 = p3;//此时会报错!!``` 编译器认为p4=p3非法,避免了p3不再指向有效数据的问题。因此,unique\_ptr比auto\_ptr更安全。 当程序试图将一个 unique\_ptr 赋值给另一个时,如果源 unique\_ptr 是个临时右值,编译器允许这么做;如果源 unique\_ptr 将存在一段时间,编译器将禁止这么做 unique\_ptr不能直接复制,必须使用`std::move()`转移其管理的指针,转移后原 unique\_ptr 为空``` unique_ptr ps1(new int(1)), ps2, ps3; ps3 = ps1; // 不可以 ps2 = move(ps1); // 可以 if(ps1 == nullptr) cout p1(new string('hi')); unique_ptr p2(p1.release()); // 将p1置为空,返回指针,p2当前是hi cout p3(new string('hello,world')); p2.reset(p3.release()); // reset释放了p2原来指向的内存 // 然后令p2指向p3所指向的对象,然后release()将p3置为空 if(p1 != nullptr) // p1已经被释放了,没有了 cout
shared_ptr
【shared_ptr】c++11最常用的智能指针类型为shared\_ptr,它采用**引用计数**。shared_ptr**实现共享式拥有概念**。对shared_ptr进行初始化时不能将一个普通指针直接赋值给智能指针,因为一个是指针,一个是类。可以通过make_shared函数或者通过构造函数传入普通指针。并可以通过get函数获得普通指针。多个智能指针可以指向相同对象,该对象和其相关资源会在**“最后一个引用被销毁”时候释放**。从名字share就可以看出了资源可以被多个指针共享,它使用**计数机制**来表明资源被几个指针共享。当计数等于0时,资源会被释放。除了可以通过new来构造,还可以通过传入auto\_ptr, unique\_ptr,weak\_ptr来构造。当我们调用release()时,当前指针会释放资源所有权,计数减一。shared\_ptr 是为了解决 auto\_ptr 在对象所有权上的局限性(auto\_ptr 是独占的), 在使用引用计数的机制上提供了可以共享所有权的智能指针。- **成员函数**- use_count 返回引用计数的个数,资源的所有者个数 - unique 返回是否是独占所有权( use_count 为 1) - swap 交换两个 shared_ptr 对象(即交换所拥有的对象) - reset 放弃内部对象的所有权或拥有对象的变更, 会引起原有对象的引用计数的减少 - get 返回内部对象(指针), 由于已经重载了()方法, 因此和直接使用对象是一样的.如 `shared_ptr sp(new int(1));` `sp` 与 `sp.get()`是等价的
语法
头文件```#include```shared_ptr:智能指针也是模板,因此创建模板必须提供额外信息:类型。```shared_ptr p1; // shared_ptr, 可以指向 string常见操作:p.get(); 返回 p 中返回对象。如果智能指针释放了其对象,返回的指针所指向的对象也就消失了```make_shared 函数:利用make_shared来分配,更加安全。```shared_ptr p3 = make_shared(42);// 指向 42[常量] 的 int 的 shared_ptr```销毁管理对象```利用模板自身的成员函数 - 析构函数```
自定义实例
目标:定义一个管理 string 的类, 并实现动态共享思路:```1.集合类型[利用标准库容器] -> 采用 vector```Code```class StrBlob{public: typedef std::vector::size_type size_type; // 别名 sz特指vectorStrBlob();// 构造函数声明StrBlob(std::initializer_listsize();} // size [const]函数bool empty() const{return data->empty();}//添加和删除元素void push_back(const std::string &t) {data->push_back(t);}void pop_back();//元素访问std::string& front();std::string& back();private:std::shared_ptr> data; // 共享指针类成员 datavoid check(size_type i, const std::string &msg) const;};```StrBlob 构造函数```StrBlob::StrBlob(): data(make_shared>()){}//默认初始化指向一个空的vector指针StrBlob::StrBlob(initializer_list li):data(make_share> (li)){}//拷贝构造```元素访问成员函数```// 边界范围检查函数void StrBlob::check(size_type i, const string &,sg) const{if(i >= data->size())throw out_of_range(msg);}```
与 new 结合使用
智能指针用 new 初始化```shared_ptr p1;shared_ptr p2(new int(42));shared_ptr p1 = new int(1024); // 错误,必须使用直接初始化形式[不能进行内置指针与智能指针之间的转换]```不要混合使用普通指针和智能指针不要使用get初始化另一个智能指针或为智能指针赋值
异常
在程序出现异常时,资源需要被正确释放,即最简单的方法就是使用智能指针。```例如使用网络库时,连接后如果没有调用关闭函数,则会出现无法关闭的情况。但如果使用智能指针,有析构函数,结束时自动回收,就不会出现这种问题了```定义自己的释放操作```shared_ptr p(&c, end_connection);```
weak_ptr
- **只引用,不计数**,但是有没有,要检查`expired()`应运而生。 weak\_ptr **是一种不控制对象生命周期的智能指针**, 它指向一个 shared\_ptr 管理的对象..进行该对象的内存管理的是那个强引用的 shared\_ptr. weak\_ptr只是提供了对管理对象的一个访问手段。 weak\_ptr 设计的目的是为配合 shared\_ptr 而引入的一种智能指针来协助 shared\_ptr 工作, 它只可以从一个 shared\_ptr 或另一个 weak\_ptr 对象构造, **它的构造和析构不会引起引用记数的增加或减少**。 **weak\_ptr是用来解决shared_ptr相互引用时的死锁问题**。如果说两个shared\_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。它是对对象的一种弱引用,**不会增加对象的引用计数**,和shared\_ptr之间可以相互转化,shared\_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared\_ptr。 - weak\_ptr调用lock函数转换成shared_ptr lock() 会检查`weak\_ptr`所指向的对象是否存在,如果存在就返回一个共享对象shared\_ptr。 ```c++ shared_ptr pa(new A()); shared_ptr p = pa->pb_.lock(); // pb_ 是 weak_ptr ``` 资源无法释放,use_count都为2 解决办法只需要A,B其中一个的 pb\_,或者pa\_类型为 weak\_ptr weak\_ptr 的构造和析构不会引起引用记数的增加或减少 ```c++ struct Node{ shared_ptr pPre; shared_ptr pNext; int val; }; void func(){ shared_ptr p1(new Node()); shared_ptr p2(new Node()); cout pNext = p2; p2->pPre = p1; cout
类设计者工具
OOP概述
核心是数据抽象、继承和动态绑定。
A : B 继承
Keyword: 基类、派生类、虚函数【**虚函数**】```基类希望其派生类自定义自身版本函数,次数就将这些函数声明为虚函数[java接口,必须被覆写]```【派生类列表】C++11新标准允许派生类显式地注明它将使用哪个成员函数改写基类的虚函数,即在形参列表后增加override关键字```class Bulk : public Quote{public:double price(std::size_t) const override;```
动态绑定
运行时绑定: 在运行过程中选择函数对应版本```double print_total(ostream &os,const Quote &item, size_t n); // 可以允许基类和其派生类print_total(cout,basic,20); // 基类print_total(cout,bulk,20); // 派生类```而对于非虚函数,则是在编译时进行绑定,因此运行时绑定对象不会改变
三大特性
封装
访问控制
访问说明符```public : 成员在整个程序内可以访问private : 成员可以被类成员函数访问```class 和 struct 关键字```struct 全为 public, 所以在需要 private 成员时,就使用class所以,其唯一区别就是在默认的访问权限```
名字查找
名字查找```1.首先块内以及使用语句前位置[定义在前,而不是存粹的记录顺序]2.再查找外层作用域3.未找到,报错```通过显式标识来使用同名成员```void Screen::fcn(pos height){cur = width * this->height;// cur = width * Screen::height;}```
类类型
对于类成员完全相同,但是类名不同,都算作不同类型。
类的声明
前向声明:```class Screen; //Screen 类的声明```**不完全类型:**声明之后,定义之前,不清楚它到底包含哪些成员
友元 friend
私有成员只能在类的成员函数内部访问,如果想在别处访问对象的私有成员,只能通过类提供的接口(成员函数)间接地进行。这固然能够带来数据隐藏的好处,利于将来程序的扩充,但也会增加程序书写的麻烦.C++ 是从结构化的C语言发展而来的,需要照顾结构化设计程序员的习惯,所以在对私有成员可访问范围的问题上不可限制太死。C++ 设计者认为, 如果有的程序员真的非常怕麻烦,就是想在类的成员函数外部直接访问对象的私有成员,那还是做一点妥协以满足他们的愿望为好,这也算是眼前利益和长远利益的折中。因此,C++ 就有了友元(friend)的概念。打个比方,这相当于是说:朋友是值得信任的,所以可以对他们公开一些自己的隐私友元分为两种:友元函数和友元类。**但是,不能把其他类的私有成员函数声明为友元****不能把成员成名为友元**
友元函数
在定义一个类的时候,可以把一些函数(包括全局函数和其他类的成员函数)声明为“友元”,这样那些函数就成为该类的友元函数,在友元函数内部就可以访问该类对象的私有成员了。将全局函数声明为友元的写法如下:```friend 返回值类型 函数名(参数表);将其他类的成员函数声明为友元的写法如下:friend 返回值类型 其他类的类名::成员函数名(参数表);```但是,不能把其他类的私有成员函数声明为友元。```class Sales_data{friend Sales_data add(const Sales_data&, const Sales_data&);// 一般来说,最好在类定义的开始或者结束前集中友元声明```private 封装的益处```1.确保用户代码不会无意破坏封装对象状态2.被封装的类具体实现细节可以随时改变,而无需调整用户级别的代码```
友元类
一个类 A 可以将另一个类 B 声明为自己的友元,类 B 的所有成员函数就都可以访问类 A 对象的私有成员。在类定义中声明友元类的写法如下:````friend class 类名;```【实例】```class Screen{friend class Window_mgr; // 类友元friend void Window_mgr::clear(); // 友元函数页可以定义在函数内部};class Window_mgr{public:using ScreenIndex = std::vector::size_type;}```
类类型
聚合类
即用户可以直接访问其成员,并且具有特殊的初始化语法形式```1.所有成员为public2.没有定义构造函数3.没有类初始值4.没有基类,virtual函数```很明显,struct 就符合这个条件初始化```Data vall = { 0, 'Anna' };//初始值顺序必须和声明顺序一致[空间存放问题]```
字面值常量类
定义:数据成员都是字面值类型的聚合类constexpr 构造函数```一半类的构造函数不能是 const[因为需要初始化], 但是字面值常量类的构造函数可以为 constexpr.class Stu{public:constexpr Std(int num = 0): id(num), age(num){}};```
基类和派生类
定义基类
虚析构函数:一般在继承关系中的根节点定义一个虚析构函数【实例】```class Quote{public:Quote() = default; // 默认构造Quote(const std::string &book, double sales_price): bookNo(book), price(sales_price){}std::string isbn() const{return bookNo;}// 虚函数指定派生类覆写计算折扣公式virtual double net_price(std::size_t n) const {return n*price; }virtual ~Quote() = default; private:std::string bookNo;protected:double price = 0.0;```
成员函数与继承
1.使用指针或引用调用虚函数时,该调用将被动态绑定
访问控制与继承
派生类继承基类时同样准售 private 等声明的访问控制权.其中 protected 访问运算符则表明,仅其派生类有权访问该成员,同时禁止其他用户访问.
抽象基类
含有纯虚函数的类是抽象基类
定义派生类
【类派生列表】:指明从哪个基类继承而来.```class A : public B{}```
派生类中的虚函数
如果没有覆盖虚函数,则该虚函数类似其他普通成员为直接继承基类中的版本.1.覆盖时函数前使用 virtual 关键字2.声明后添加 override
派生类型转换
1.继承基类的部分和派生类自定义部分不一定时连续存储的【**派生类到基类**类型转换】由于派生类中有基类的组成部分,所以能把派生类的对象当作基类对象来使用.
静态成员与继承
访问权限仍遵顼通用的访问控制规则静态成员整个继承体系中只存在该成员的唯一定义
类型转换与继承
基类不能向派生类转换
不存在从基类向派生类的转换```class A{};class B:A{};A a;B* b = a; // 不能向派生类转换```
实例对象之间不存在类型转换
对象之间只会进行成员赋值,而不会发展类型的转换```A a; // A 为 B 的父类B b(a); // 调用 B::B(const B&) 构造函数b = a; // 调用 B::operator = (const B&) 赋值符号```
类成员
可变数据成员
```mutable data1.即使在 const 成员函数内部都可以进行改变```
类静态成员
静态成员:由于类的某些成员与类直接相关,而不是与各个对象保持关联,所以,通过改该类的成员值,直接修改所有类对象的值。由此需要,则使用静态成员声明:成员名之前加上关键字 static```class Account{public:static double rate(){}```调用```//使用作用域运算符访问静态成员double r;r = Account::rate();//Account ac1;Account *ac2 = &ac2;//调用静态成员函数r = ac1.rate(); r = ac2->rate();```静态成员可以使不完全类型【即】
类成员再探
即类类型在类中的别名```class Screen{public:typedef std::string::size_type pos;// 等价于 using pos = std::string::size_type;private:pos cur = 0;pos height = 0, width = 0;std::string cont;};```
类函数
成员函数
```std::string isbn() const { return bookNo; // 等价于 this->bookNo; }total.isbn(); // 隐式将total的地址对象传入 isbn 中```由于 this 默认为指向类类型的非常量版指针,但是它按照逻辑本身不能进行修改。```std::string Sales_data::isbn(const Sales_data *const this){ return this->isbn; }```成员函数的类作用域```double Sales_data::avg_price() const{if(units)return revenue;elsereturn 0;}// 在作用域 Sales_data 中,隐式使用了units 和 revenue 两个成员对象```返回this对象的函数```Sales_data& Sales_data::combine(const Sales_data &rhs){units += rhs.units;rev += rhs.rev;return *this;}// 返回调用 this 的引用```
返回 *this 成员函数
this 左值返回```inline Screen &Screen::move(pos r,pos c){pos row = r * width;cur = row + c;return *this; // 左值返回,返回引用,内部成员发送改变 [也就是说,函数内部的 this 是一个临时变量]}myScreen.move(4,0).set('#');//如果不返回引用,只能进行拷贝赋值```
成员内联函数
重载成员函数
调用基类成员函数
通过静态编译,指定对应作用域```Base::foo();```
类外定义成员函数
一般来说函数体是写在类里面的```class A{public:void foo();};// 可以在类外定义函数体void A::foo(){ xxx};```注意 【override 和 virtual 关键字】一开始在class里定义了override方法,在class外部实现的时候还写了override,多此一举。只要在class里声明override即可,实现并不需要加关键字。同理的还有virtual关键字。```class A{virtual void foo();void goo() override;};// virtual void A::foo(){} 错误void A::foo(){ }// void A::goo() override {} 错误void A::goo(){ }
纯虚函数
纯虚函数(pure virtual) **纯虚函数就是没有函数体,同时在定义的时候,其函数名后面要加上“= 0”**。**对于一个含有纯虚函数的类(抽象类)来说,其无法进行实例化**。【与Java中类比的便是抽象类】```写在类内部 格式: 函数声明 = 0;class Disc_quote : public Quote{public:double net_price(std::size_t) const = 0; // ```虚函数存放在虚函数表中,但是纯虚函数在虚函数表中对应的位置上是一个 **pure\_virtual\_called()函数实例**,他扮演该位置的空间保卫者的角色,如果被调用就会触发**执行期异常处理**,除非该纯虚函数被后代实现,该位置才会被覆盖为普通的虚函数**纯虚函数只能被静态地调用,不能经由虚拟机制调用**在父类中纯虚函数是否实现取决于设计者,但**纯虚析构函数一定要定义实现!**因为每一个继承的子类的析构函数会被扩张,**将以静态调用的方式调用每一个上层的基类的析构函数。因此只要缺乏任何一个基类的析构函数,都会导致链接失败**即就比如,如果要将一个基类定义为抽象类,但是没有合适的纯虚函数时,就可以将析构函数定义为纯虚函数。但是一定要有实现,因为当基类指针指向派生类的对象时,如果对象释放掉,依次调用派生类的析构函数,如果基类没有析构函数,那编译器应该会出问题。
构造函数
初始化类对象数据成员```特点:1.没有返回值2.类可以包含多个构造函数,和函数重载原理类似3.构造函数不能声明为 const4.没有声明,则执行默认构造函数;有些类不能依赖于合成的默认构造函数5.= default; 指定使用默认构造函数```构造函数初始值表```Sales_data(const std::string &s):bookNo(s){}Sales_data(const std::string &s, unsigned n, double p):bookNo(s), units_sold(n), revenue(p*n){}```类外部定义构造函数```Sales_data::Sales_data(std::istream &is){read(is, *this);}```拷贝/赋值/移动```A (A& a); //拷贝构造函数A (const A& a); //拷贝构造函数A& operator= (const A& a); //赋值构造函数```
默认构造函数
```class Box{ Box() = default; // 格式}```
委托构造函数
C++11 新标准避免你有多个参数表不同但是逻辑相近(或者有公共部分)的构造函数的时候,一个逻辑写好几遍造成代码重复.即A构造函数调用自己其他的构造函数实现逻辑```class Stu{public:// 非委托构造Stu(std::string s, unsigned cnt, double price): bookNo(s) , units_sold(cnt), revenue(cnt*price) {}// 委托构造Stu(): Stu('',0,0){}Stu(std::string s):Stu(s,0,0){}Stu(std::istream &is):Stu(){ read(is, *this); }};```
继承初始化
```// Disc_quote 继承 Quoteclass Bulk_quote : public Disc_quote{public:Bulk_quote() = default;Bult_quote(const std::string& book, double price, std::size_t qty, double disc): Disc_quote(book, price, qty,disc){} // 直接基类继承初始化[重构]double net_price(std::size_t) const override;};```
转换构造函数
在使用赋值符号时,会发生隐式转换调用其构造函数.可以通过 explicit 声明从而阻止这种隐式转换.```// 列表初始化构造函数Sales_data::Sales_data(const std::string &s): bookNo(s);string null_book = 'null';Sales_data sd = null_book; /*发生隐式转换Sales_data sd(null_book);调用其构造函数*/```
拷贝构造函数
**复制构造函数**是构造函数的一种,也称拷贝构造函数,它只有一个参数,参数类型是本类的引用.默认构造函数可能不存在,但是赋值构造函数一定存在.```#includeusing namespace std;class Complex{public: double real, imag; Complex(double r, double i) { real= r; imag = i; }//拷贝构造函数 Complex(const Complex& c){ coutreal = c.real; this->imag = c.imag; }};int main(){ Complex cl(1, 2); Complex c2 (cl); //用复制构造函数初始化c2 cout
成员列表初始化
语法结构```class Book{ Book(double n,double p,std::string &ISN) : number(n), price(p*m) , isn(ISN){};}// 格式为 冒号 成员(值), ... ;```初始化顺序问题```class X{int i,j;public:X(int val): j(val), i(j){}};//未定义式初始化,会出现警告//最好按照定义的顺序进行赋值```**初始化函数列表和构造函数内赋值的区别**当用第二种方法初始化数据成员时会两次对string的成员函数的调用:一次是缺省构造函数,另一次是赋值。而用第一种方法(初始化列表)只是一次调用缺省的构造函数,并不会调用赋值函数。会减少不必要的开支,当类相当复杂时,就会看出使用初始化列表的好处
explicit 构造声明
【抑制构造函数定义的隐式转换】explicit是个C++关键字,其作用是指定仅有一个参数或除第一个参数外其它参数均有默认值的类构造函数不能作为类型转化操作符被隐含的使用,防止该类的对象直接被对应内置类型隐式转化后赋值,从而规定这个构造函数必须被明确调用。1. 是个构造函数;2 .是个默认且隐含的类型转换操作符。【举例】```#include using namespace std;class A{public: A(int i = 5) { m_a = i; }private: int m_a;}; int main(){ A s; //我们会发现,我们没有重载'='运算符,但是却可以把内置的int类型赋值给了对象A. s = 10; //实际上,10被隐式转换成了下面的形式,所以才能这样. //s = A temp(10); system('pause'); return 0;}```
移动构造函数
移动构造的核心就是利用 右值引用.右值是一个临时变量,在创建并拷贝给左值后会被销毁。但是拷贝过程就会浪费时间,因此为了更加节约时间,于是将该临时变量拷贝时间减去,从而实现右值的引用。因此 移动构造即基于右值引用的方式,来对类进行初始化.```//参数为左值引用的深拷贝构造函数,转移堆内存资源所有权,改变 source.ptr_ 的值Integer(Integer& source) : ptr_(source.ptr_) { source.ptr_ = nullptr; cout
析构函数
【格式】析构函数不接受参数,因此其不能被重载。对于一个给定类,只会有唯一一个析构函数```class Foo{public:~Foo(); //析构函数};```【析构函数完成的工作】1.内置类型没有析构函数,因此销毁时什么都不需要做。其中智能指针和指针区别在于一个是类,一个是内置类型。所以智能指针有析构函数。【什么时候调用析构函数】1.离开作用域2.对象销毁时,成员销毁3.容器销毁,元素销毁4.delete动态分配对象5.临时对象,创建完成后
拷贝函数
拷贝 / 赋值
拷贝构造函数
拷贝构造函数在几种情况下都会被隐式地使用。因此,拷贝构造函数通常不应该是explicit 声明的【格式】```class Foo{public:Foo(); // 默认构造函数Foo(const Foo&); // 拷贝构造函数};```**不调用构造函数就可以构造出新的实例**```class A{private: static int object_count;public: A() { ++object_count; }; void print() { std::cout
拷贝初始化
1.直接初始化和拷贝初始化之间的区别2.拷贝初始化发生场景```花括号列表初始化一个数组中的元素或一个聚合类中的成员```【拷贝初始化限制】```vector v1(10); // 直接初始化vector v2 = 10; // 错误,构造函数时 explicit 的```
赋值运算符
仅执行浅拷贝,将对象指针指向对应的内存
重载赋值运算符
【实例】```class Foo{public:Foo& operator=(const Foo&); // 赋值运算符重载};```注:赋值运算符通常应该返回一个指向其左侧的引用
析构函数
【格式】析构函数不接受参数,因此其不能被重载。对于一个给定类,只会有唯一一个析构函数```class Foo{public:~Foo(); //析构函数};```【析构函数完成的工作】1.内置类型没有析构函数,因此销毁时什么都不需要做。其中智能指针和指针区别在于一个是类,一个是内置类型。所以智能指针有析构函数。【什么时候调用析构函数】1.离开作用域2.对象销毁时,成员销毁3.容器销毁,元素销毁4.delete动态分配对象5.临时对象,创建完成后
=default
显式要求编译器生成合成函数版本;(即显式要求默认构造)
阻止拷贝
定义类时需要采取某种机制阻止拷贝或赋值.
=delete
【定义删除的函数】被删除函数: 声明后,不能以任何方式使用它们;```struct NoCopy{NoCopy() = default;NoCopy(const NoCopy&) = delete; //阻止拷贝NoCopy &operator=(const NoCopy&) = delete; // 阻止赋值}```
private 拷贝控制
```class PrivateCopy{PrivateCopy(cosnt PrivateCopy&);PrivateCopy &operator=(const PrivateCopy&);public:};```
对象移动
由于在拷贝构造过程中会创建临时变量,赋值之后再销毁。那么就会引起一些问题:1.拷贝时间久2.包含不共享资源是,不能够进行拷贝[例如指针或者IO缓冲]
移动构造函数
移动赋值运算符
右值引用
【右值引用】即必须绑定再右值的引用,**且只能绑定到一个将要销毁的对象**```void fun(int &x) {}int main() { fun(10); // 传入右值错误 return 0;}```【左值引用】即常规引用,我们不能将其绑定在要求转换的表达式、字面常量或者返回右值的表达式中【左右值的区别】右值定义为除左值以外的情况:**左值指的是可以取地址的变量**,记住,左值与右值的根本区别在于能否获取内存地址,而能否赋值不是区分的依据。通常临时量均为右值。1.左值具有持久状态,而右值要么字面常量,要么表达式求值过程中的临时变量.```1. 左值引用,使用 `T&`,只能绑定左值2. 右值引用,使用 `T&&`,只能绑定右值3. 常量左值,使用 `const T&`,既可以绑定左值,又可以绑定右值4. **已命名的右值引用,编译器会认为是个左值**```2.变量是左值3.从 '=' 左右侧来看,等号左边为左值,等号右侧为右值【标准库move函数】**move函数**获取绑定到左值上的右值引用```#include int &&rr3 = std::move(rr1);```
std::move()
【定义】函数模板```template typename remove_reference::type&& move(T&& t){return static_cast::type&&>(t);}```【工作原理】```string s1('hello'),s2;s2 = std::move(s1);```1.类型推断2.对象实例化3.返回右值成员类型
深拷贝/浅拷贝
1、浅拷贝只复制一层对象的属性、值引用 (场景:对于只有一层结构的Array和Object想要拷贝一个副本时使用)2、深拷贝则递归复制了所有层级、地址引用 (场景:复制深层次的object的数据结构)
继承
多重继承
【定义】```class Bear : public ZooAnimal{}class Panda : public Bear, public Endanered{};```
构造函数
【初始化】```//显式初始化所有基类Panda::Panda(std::string name,bool onExhibit) : Bear(name,onExhibit,'Panda'), Endangered(Endangered::critical){}//隐式使用Bear默认构造函数初始化Bear子对象Panda::Panda() : Endangered(Endangered ::critical){}```【初始化顺序/析构顺序】从祖先开始依次继承;从自己开始析构【多个相同构造函数继承】相同构造函数(形参列表完全相同)此时必须自定义相同形参表的构造函数
作用域
【过程】多重继承下,查找名字在所有直接基类中同时进行。如果名字在多个基类中被找到,则改名字的使用将具有二义性.对于二义性的函数或者成员,如果不加前缀或者不重载则会引发错误.
虚继承 / 菱形继承
- **概念:** 虚继承用于解决多继承问题。 在多继承情况下,不同途径继承来的同一基类,会在子类存在多个拷贝(比如菱形继承问题)。这样会产生两个问题:**浪费空间,存在二义性**。 C++提供虚基类,使得继承间接共同使用同一基类时,**只保留一份成员**。 子类继承可以继承多个虚基类表指针。- **原理:** 每个虚继承的子类都有一个虚基类表指针,它指向一个虚基类表。虚基类表中记录了虚基类和本类的偏移地址,通过偏移地址就找到了虚基类的成员。 虚基类依旧占据继承类的存储空间。```//间接基类Aclass A{protected: int m_a;};//直接基类Bclass B: virtual public A{ //虚继承protected: int m_b;};//直接基类Cclass C: virtual public A{ //虚继承protected: int m_c;};//派生类Dclass D: public B, public C{public: void seta(int a){ m_a = a; } //正确 void setb(int b){ m_b = b; } //正确 void setc(int c){ m_c = c; } //正确 void setd(int d){ m_d = d; } //正确private: int m_d;};int main(){ D d; return 0;}```
访问权限限制
保护继承
使用protected继承时,派生类内部可以访问基类中public和protected成员,类外不能通过派生类的对象访问基类的成员(可以在派生类中添加公有成员函数接口间接访问基类中的public和protected成员)。 (1)基类的public成员在派生类中变为protected成员。 (2)基类的protected成员在派生类中依然是protected成员。 (3)基类中的private成员在派生类中不可访问
私有继承
class 继承默认为 私有继承(private)使用private继承时,派生类内部可以访问基类中public和protected成员,并且类外也不能通过派生类的对象访问基类的成员(可以在派生类中添加公有成员函数接口间接访问基类中的public和protected成员)。 (1)基类的public成员在派生类中变成private成员。 (2)基类的protected成员在派生类中变成private成员。 (3)基类的private成员在派生类中不可访问
公有继承
struct 结构体默认为 公有继承(public)使用public继承时,派生类内部可以访问基类中public和protected成员,但是类外只能通过派生类的对象访问基类的public成员。 (1)基类的public成员在派生类中依然是public的。 (2)基类中的protected成员在派生类中依然是protected的。 (3)基类中的private成员在派生类中不可访问。
final 防止继承
对于某个类,当我们不希望其他类继承它时,C++提供一种防止继承的方法,在类名后添加关键字 final:```class Student final{};```只有虚函数才有final, 非虚函数没有final```virtual void foo() final; // A::foo is final void bar() final; // Error: non-virtual function cannot be final ```
防止类继承
```class XiaoMi {public: XiaoMi(){}};class XiaoMi2 final : public XiaoMi { XiaoMi2(){}};class XiaoMi3 : public XiaoMi2 { //不能把XiaoMi2作为基类};```
防止虚函数重写
```class XiaoMi {public: virtual void func() final;};void XiaoMi::func() { //不需要再写final cout
多态
重载运算
基本概念
【定义】一种特殊的函数,也包含返回类型、参数和函数体【不能重载函数】1.不能重载内置类型运算符2.不能重载未出现的运算符【不能被重载运算符】```:: 作用域.* . 成员? : 三目```
输入和输出
【输出运算符重载】```ostream &operator
类型转换
虚函数
默认实参
默认实参值由调用的静态类型决定.可以理解为基类和派生类调用时默认参数一致.
回避虚函数机制
希望在执行时不进行动态绑定,而是强迫执行虚函数的特定版本.调用实在编译时进行解析,属于**静态绑定**,运行时不涉及到虚函数表查找。```Base baseP;double undiscounted = baseP->Quote::net_price(42); // 不管baseP的动态类型是什么```一般情况下,只有成员函数(或友元)中的代码才需要使用作用域运算符来回避虚函数机制.
= 0 纯虚函数
语法```class A{public:virtual A() = 0;}```
实现原理
虚函数的实现是由两个部分组成的,**虚指针**与**虚函数表**有关虚函数的几点知识-**虚函数表是针对类的,一个类的所有对象的虚函数表都一样,类的所有对象共享这个类的虚函数表**-每个对象内部都保存一个指向该类虚函数表的指针vptr,每个对象的vptr的存放地址都不一样,但是都指向同一虚函数表。``` 在有虚函数的类中,类的最开始部分是一个虚函数表的指针,这个指针指向一个虚函数表,表中放了虚函数的地址,实际的虚函数在**代码段(.text)**中。 当子类继承了父类的时候也会继承其虚函数表,当子类重写父类中虚函数时候,会将其继承到的虚函数表中的地址**替换**为重新写的函数地址。使用了虚函数,会增加访问内存开销,降低效率。```
模板与泛型编程
模板定义
函数模板
【函数模板】模板定义以关键字 template 快开始,后跟一个模板参数列表```template int compare(const T &v1,const T &v2){if (v1 // 错误,类型参数必须使用关键字 class 或 typename;```使用模板时,制定模板实参,将其绑定到模板参数上.【非类型模板参数】```template ```【实例化函数模板】编译器根据模板参数来创建实例对象【inline和constexpr函数模板】```template inline T min(const T&, const T&); \\ inline说明符在模板参数列表后```【编写原则】1.模板中的函数参数是const的引用2.函数体中断条件判断仅使用
类模板
【类模板】与函数模板不同的是编译器不能为类模板推断模板参数类型.
模板实参推断
std::move()
【定义】函数模板```template typename remove_reference::type&& move(T&& t){return static_cast::type&&>(t);}```【工作原理】```string s1('hello'),s2;s2 = std::move(s1);```1.类型推断2.对象实例化3.返回右值成员类型
高级主题
异常处理
【抛出异常】控制权转移:1.沿调用链的函数可能会提前退出2.一旦程序开始处理异常,则沿调用链对象将被销毁【栈展开】在某 try 语句嵌套在其他 try 块中,逐渐从类向外检查匹配的 catch 语句.在找不到匹配的 catch 时,程序将调用标准库 terminate.
命名空间
【作用域】命名空间中的每个名字必须表示唯一实体.命名空间的成员能够被内嵌作用与的单位访问.【内联命名空间】```inline namespace Find_name{}```【using声明】using 声明仍遵守作用域规则
内存分配
内存泄漏
**内存泄漏(memory leak)是指由于疏忽或错误造成了程序未能释放掉不再使用的内存的情况。**内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,失去了对该段内存的控制,因而造成了内存的浪费。【内存泄漏的分类】1.**堆内存泄漏 (Heap leak)**。对内存指的是程序运行中根据需要分配通过malloc,realloc new等从堆中分配的一块内存,再是完成后必须通过调用对应的 free或者delete 删掉。如果程序的设计的错误导致这部分内存没有被释放,那么此后这块内存将不会被使用,就会产生Heap Leak.2.**系统资源泄露(Resource Leak)**。主要指程序使用系统分配的资源比如 Bitmap,handle ,SOCKET等没有使用相应的函数释放掉,导致系统资源的浪费,严重可导致系统效能降低,系统运行不稳定。3.**没有将基类的析构函数定义为虚函数。**当基类指针指向子类对象时,如果基类的析构函数不是virtual,那么子类的析构函数将不会被调用,子类的资源没有正确是释放,因此造成内存泄露
内存泄漏分类
**内存泄漏(memory leak)是指由于疏忽或错误造成了程序未能释放掉不再使用的内存的情况。**内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,失去了对该段内存的控制,因而造成了内存的浪费。【内存泄漏的分类】1. **堆内存泄漏 (Heap leak)**。对内存指的是程序运行中根据需要分配通过malloc,realloc new等从堆中分配的一块内存,再是完成后必须通过调用对应的 free或者delete 删掉。如果程序的设计的错误导致这部分内存没有被释放,那么此后这块内存将不会被使用,就会产生Heap Leak.```例如:char *s = new char[100];delete s; // 仅删除首地址元素内容s = nullptr;//注意指针制空```2. **系统资源泄露(Resource Leak)**。主要指程序使用系统分配的资源比如 Bitmap,handle ,SOCKET等没有使用相应的函数释放掉,导致系统资源的浪费,严重可导致系统效能降低,系统运行不稳定。3. **没有将基类的析构函数定义为虚函数。**当基类指针指向子类对象时,如果基类的析构函数不是virtual,那么子类的析构函数将不会被调用,子类的资源没有正确是释放,因此造成内存泄露
如何判断是否内存泄漏
**内存泄漏通常是由于调用了malloc/new等内存申请的操作,但是缺少了对应的free/delete。**为了判断内存是否泄露,我们一方面可以使用linux环境下的内存泄漏检查工具Valgrind,另一方面我们在写代码时可以添加内存申请和释放的统计功能,统计当前申请和释放的内存是否一致,以此来判断内存是否泄露。
内存泄漏的几种情况
【总共8种】new动态创建的对象必须要用delete来撤销该对象,不然内存会泄露1. **没有匹配地调用new/delete操作** 一是在heap中创建了对象占用了内存,但是没有显式地释放所占用的内存。 二是在类构造的时候对于可变大小的member动态分配了内存,但是在析构函数中没有对应地释放内存。2. **没有正确地清除嵌套的对象指针**3. **delete释放对象数组时没有使用[ ]** 在定义了析构函数,对象数组中的元素确实需要进行有效析构(内置类型就不需要,不写[ ]并不会出问题),且析构函数功能完备的情况下。 如果没有使用[ ]来delete一个对象数组,那么只会析构,释放数组的第一个对象。 其他元素会依然存在,但是!相关内存已经要求归还了。 而对于[ ]中是否需要指定元素个数,在Jonathan的原始版本中会优先采用程序员指定的size。 但是几乎新进所有的C++编译器都不考虑程序员的显式指定。4. **指向对象的指针数组** 在定义了析构函数,对象数组中的元素确实需要进行有效析构(内置类型就不需要,不写[ ]并不会出问题),且析构函数功能完备的情况下。 即使delete [ ] ptr_arr,但是释放的只是指针所拥有的空间,指针指向的对象所拥有的空间仍然被占用,而且我们会失去对它们的控制。 解决方法就是迭代遍历整个数组手动调用析构函数5. **缺少拷贝构造函数 $or$ =号运算符重载** 假设某个class内含一些指向heap空间的指针。 而使用者没有提供一个显式的拷贝构造函数,此时如果此class的结构让编译器不能触发bitwise语义,那么在需要拷贝构造的场景下(比如以值传递实参转入形参),编译器就会触发memberwise语义,构造出一个逐member复制的拷贝构造函数。 很明显,如果在memberwise copy的情况下,**会造成两个不同指针指向同一块heap空间的情况**。 而如果此时的使用者编写了完备的析构函数,**那么就会造成两次对同一heap空间内存的释放,这会导致heap空间的崩溃。** heap空间崩溃,内存自然就泄漏了。6. **返回不可控栈上空间** 主要点在于栈上空间由操作系统管理,而不是编译器/程序员管理,所以对于一些栈空间我们是不可控的(比如函数内部临时开的栈)。 我们可以得到一个栈空间内的地址并进行访存,但是如果这块栈的“生命”结束了,我们继续使用这个地址进行访存,就可能破坏某些操作系统功能,于是程序崩溃,内存泄漏。7. **free( )之后没有将指针滞空** free( )这个C library函数只是告诉编译器/操作系统:我放弃了这块内存的控制权,你可以使用了。 但是事实上是否放弃这块内存还是取决于程序员的行为,也就是说你可以free之后继续使用这块内存,但是此时操作系统也有管理权,于是我们就可能使用到“垃圾内存”。 这与上一点本质上是一样的,都是访问到了受限/不合法的内存,有可能造成程序的崩溃。8. **继承链中的base class没有将析构函数设置为virtual**
常量存放位置
对于局部常量,存放在栈区对于全局常量,常量存放在全局/静态存储区字面值常量,常量存放在常量存储区,比如字符串,放在常量区。
C++/C的内存分配
-**静态区域:** **text segment(代码段):**包括 **只读存储区** 和 **文本区** -其中只读存储区存储字符串常量 -文本区存储程序的机器代码 **data segment(数据段):**存储程序中**已初始化**的**全局变量**和**静态变量** **bss segment:**存储未初始化的全局变量和静态变量(局部+全局),以及所有被初始化为0的全局变量和静态变量,对于未初始化的全局变量和静态变量,程序运行main之前时会统一清零。**即未初始化的全局变量编译器会初始化为0**-**动态区域:** **heap(堆):** 当进程未调用malloc时是没有堆段的,**只有调用malloc时采用分配一个堆**,并且在程序运行过程中可以动态增加堆大小(移动break指针),**从低地址向高地址增长**。**分配小内存时使用该区域。** 堆的起始地址由mm_struct 结构体中的start_brk标识,结束地址由brk标识。 **memory mapping segment(映射区):**存储动态链接库等文件映射、申请大内存(malloc时调用mmap函数),位于堆和栈之间 **stack(栈):**使用栈空间存储函数的返回地址、参数、局部变量、返回值,**从高地址向低地址增长**。**在创建进程时会有一个最大栈大小,**Linux可以通过ulimit命令指定。补充:[C++成员函数和成员变量存储说明](https://blog.csdn.net/tsh123321/article/details/88966421)所有类成员函数和非成员函数代码存放在代码区,而类的静态成员变量在类定义时就已经在全局数据区分配了内存。因此成员函数的代码段都不占用对象的存储空间。为运行函数而分配的局部变量、函数参数、返回数据、返回地址等存放在栈区;
自由存储区与堆是否等价
-自由存储是C++中通过new与delete动态分配和释放对象的**抽象概念**,而堆(heap)是C语言和操作系统的术语,是操作系统维护的一块动态分配内存。-new所申请的内存区域在C++中称为自由存储区。藉由堆实现的自由存储,可以说new所申请的内存区域在堆上。但是new分配的内存空间是可以指定在别的地方比如栈上,因为可以重载operator new-堆与自由存储区还是有区别的,**它们并非等价**
不同平台下int和指针的大小
**错误:**int型数据的大小,也就是sizeof(int)的大小完全跟随硬件平台的位数。**正确:**int型数据的大小和硬件平台位数无关,它是**由C语言标准和编译器共同决定的**。int类型的大小是由limits.h文件中INT_MIN和INT_MAX两个宏定义来决定的,而limits.h文件在编译器库文件中可以找到。1. 16位系统中,int型为16位大小,两字节2. 32位系统中,int型为32位大小,四字节3. 64位系统中,int型为32位大小,四字节long 类型也类似1. 16位系统中,long型为32位大小,4字节2. 32位系统中,long型为32位大小,4字节3. 64位系统中,long型为64位大小,8字节**指针****错误:**在32位系统下指针类型为32位,在64位系统下指针类型为64位,以此类推。指针本质上是变量,它的值是内存中的地址,既然需要通过指针能够访问当内存当中所有的数据,那么这个指针类型的宽度至少要**大于等于地址总线的宽度**。打个比方一个芯片的**地址总线**是32位,那么内存地址的范围就是0~4G,那么这个指针类型的宽度至少需要32位,才能保证访问到内存中每个字节。**实际情况:**芯片的位数由芯片一次能处理的数据宽度决定,可看成是数据总线的宽度,但是地址总线和数据总线的宽度有时候并不一致。**正确:**指针的大小完全由实际使用的**地址总线的宽度(+数据对齐)**来决定,而并非由芯片位数来决定。
C++中四种cast类型转换
C++中四种类型转换是:**static_cast, dynamic_cast, const_cast, reinterpret_cast**1. **const_cast** **用于将const变量转为非const** 对于未定义const版本的成员函数,我们通常需要使用const_cast来去除const引用对象的const,完成函数调用。 另外一种使用方式,结合static_cast,可以在非const版本的成员函数内添加const,调用完const版本的成员函数后,再使用const_cast去除const限定。2. **static_cast** 用于各种隐式转换,完成基础数据类型;同一个继承体系中类型的转换;任意类型与空指针类型void* 之间的转换。比如非const转const,void\*转指针等, static_cast\* **能用于多态向上转化**,如果向下转能成功但是不安全,结果未知;3. **dynamic_cast** 用于**动态类型转换**。**只能用于含有虚函数的类**,用于类层次间的向上和向下转化。只能转指针或引用。向下转化时,如果是非法的对于指针返回NULL,对于引用抛异常。要深入了解内部转换的原理。 向上转换:指的是子类向基类的转换 向下转换:指的是基类向子类的转换 它通过判断在执行到该语句的时候变量的运行时类型和要转换的类型是否相同来判断是否能够进行向下转换。4. **reinterpret_cast** 可以用于任意类型的指针之间的转换,对转换的结果不做任何保证 最常用用途是转换“函数指针”类型 用来处理无关类型的指针,**但是需要与原值有相同比特位数** **神奇用途:临时隐藏该指针类型,让他成为“只携带地址”的指针** PS:该转换不具有移植性5. 为什么不使用C的强制转换? C的强制转换表面上看起来功能强大什么都能转,但是转化不够明确,不能进行错误检查,**容易出错**。
new / malloc 相关
new / delete 步骤
new 执行步骤```new关键字调用operator newoperator new调用malloc强制转换调用构造函数(这也是为什么malloc不能申请包含指针的类内存的原因)PS:new关键字是表达式,固定形式;operator new才可以重载```delete 执行步骤```delete关键字调用析构函数delete关键字调用operator deleteoperator delete调用free```
malloc原理
【基于 linux 环境下的 malloc 实现】malloc函数用于动态分配内存,为了减少内存碎片和系统调用的开销,malloc采用**内存池**的方式,先申请大块内存作为堆区,然后将堆区分为多个内存块,**以块作为内存管理的基本单位**。当用户申请内存时,直接从堆区分配一块合适的空闲块。Malloc采用隐式链表结构将堆区分成连续的、大小不一的块,包含已分配块和未分配块;同时malloc采用显示链表结构来管理所有的空闲块,即**使用一个双向链表将空闲块连接起来,每一个空闲块记录了一个连续的、未分配的地址**。当进行内存分配时,Malloc会通过隐式链表遍历所有的空闲块,选择满足要求的块进行分配;当进行内存合并时,malloc采用边界标记法,根据每个块的前后块是否已经分配来决定是否进行块合并。Malloc在申请内存时,一般会通过brk或者mmap系统调用进行申请。其中当申请内存小于128K时,会使用系统函数brk在堆区中分配;而当申请内存大于128K时,会使用系统函数mmap在映射区分配。-在 malloc 和 free 的面前没有对象没有数组,只有“**内存块**”。**一次 malloc 分配的东西,一次 free 一定能回收**。 而如果使用 new[] 一次分配所有内存,多次调用构造函数,要搭配使用 delete[],多次调用析构函数,摧毁数组的每个对象。-结论 -当开辟的空间小于128K时,调用 **brk() 函数**,malloc的底层实现是系统调用函数 brk() ,其主要移动指针 \_enddata (此时的\_enddata指的是**Linux地址空间中堆栈的末尾地址?**,不是数据段的末尾地址) - 当开辟的空间大于128K时,**mmap()** 系统调用函数来在虚拟地址空间中(堆和栈中间,称为“**文件映射区域**”的地方)找一块空间来开辟-具体内容 当一个进程发生缺页中断的时候,进程会陷入核心态,执行以下操作: -检查要访问的虚拟地址是否合法 -查找/分配一个物理页 -填充物理页内容(读取磁盘,或者直接置0,或者什么都不做) -建立映射关系(虚拟地址到物理地址的映射关系) -重复执行发生缺页中断的那条指令-内存分配原理 从操作系统角度看,进程分配内存有两种方式,分别由两个系统调用完成:brk 和 mmap (不考虑共享内存) -**brk** 是将数据段(.data)的最高地址指针 \_edata 往高地址推 -**mmap** 是在进程的虚拟地址空间中(堆和栈中间,称为“文件映射区域”的地方)找一块空闲的虚拟内存 **这两种方式分配的都是虚拟内存,没有分配物理内存**。在第一次访问已分配的虚拟地址空间的时候,**发生缺页中断,操作系统负责分配物理内存**,然后建立虚拟内存和物理内存之间的映射关系。 也就是说如果malloc分配了一块内存给一个进程A后,从不访问它,则A对应的物理页也不会被分配 由于 \_edata 指针只有一个,因此 **brk 分配的内存需要等到高地址内存释放以后才能释放,而 mmap 分配的内存可以单独释放** 默认情况下: 当最高地址空间的空闲内存超过128K时,执行内存紧缩操作(trim)
new/delete与malloc/free的区别
**在导入new和delete运算符之前,承担class内存管理的唯一方法就是在constructor中指定this指针**-new/delete是C++的关键字 new不止是分配内存空间,还会调用构造函数,不用指定内存大小,返回的指针不用强转;delete同理不止释放空间,还会调用析构函数-malloc/free是C语言的库函数 需要给定申请内存的大小,返回的指针需要强转,不会调用构造函数和析构函数其次类中,new其实不止是分配内存空间,还自动调用构造函数,```**类型、操作、成功返回、失败返回、重载、调用、内存位置、空间不足、数组操作**1. 类型 1. malloc/free是C标准库函数 2. new/delete是Cpp的操作运算符/关键字,实际上是调用了运算符重载函数`::operator new()`和`::operator delete()` *可以在全局或者类的作用域中提供自定义的new和delete运算符的重载函数,以改变默认的malloc和free内存开辟释放行为,比如说实现内存池。*2. 操作 1. malloc/free 单纯分配内存空间并释放。 2. new/delete 基于前者实现,在开辟/释放空间后,会调用对象的构造函数/析构函数进行初始化/清理。3. 成功返回 1. malloc 成功后会返回 void* ,如需使用必须强制类型转换。 2. new 成功后会直接返回对应类型的指针。4. **失败返回** 1. malloc 开辟内存失败之后会返回nullptr指针,需要检查判空(也只能这样)。 2. new 开辟内存失败之后会抛出bad_alloc类型的 **异常**,可以捕获异常。用户可以指定处理函数或重新制定分配器(new_handle)5. 能否重载:只有new/delete能被重载。6. 调用 1. malloc对开辟的空间大小需要严格指定。 2. new只需要内置类型/对象名。7. 内存位置 *PS:堆是操作系统维护的一块内存,而自由存储区是C++中通过new与delete动态分配和释放对象的抽象概念。堆与自由存储区并不等价。* 1. malloc在堆(heap)上分配内存。 2. new申请内存位置有一个抽象概念——自由存储区(free store),它可以在堆上,**也可以在静态存储区上分配**,这主要取决于operator new实现细节,取决与它在哪里为对象分配空间。8. **空间不足的弥补** 1. 使用malloc:malloc本身不会进行尝试,可以由开发者再使用realloc进行扩充或缩小。 2. 使用new:不能被如前者一样直观地改变。当空间不足时**会触发new_handler机制**,此机制留有一个set_new_handler句柄,看看用户是否设置了这个句柄,如果设置了就去执行,句柄的目的是看看能不能尝试着从操作系统释放点内存,找点内存,**如果空间确实不足就抛出bad_alloc异常;**9. 数组操作 new[] 对应 delete[],malloc分配的一块内存可以直接用free释放```
多次delete引发的问题
```int* p = new int;delete p;delete p;```很明显会引起程序崩溃,这是我本地执行的错误信息,错误提示也给出了double free的字样,告诉我们这可能是两次释放导致的问题这种情况有没有简单的规避方式吗?我看到好多人这么写```int* p = new int;delete p;p = nullptr; //或 p = NULL;delete p;```
new 创建和不用new创建类对象区别
new创建的对象返回指针,且只能用delete进行回收,否则在局部不会自动回收.而直接实例化,离开局部作用域后则会自动调用析构函数进行回收.
delete 与 delete[] 区别
delete和delete []的区别delete只会调用一次析构函数,而delete[]会调用每一个成员函数的析构函数。在More Effective C++中有更为详细的解释:“当delete操作符用于数组时,它为每个数组元素调用析构函数,然后调用operator delete来释放内存。”delete与new配套,delete []与new []配套作者:程序员柠檬链接:https://zhuanlan.zhihu.com/p/472726416来源:知乎著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
编译原理
编译过程
对于C++源文件,从文本到可执行文件一般需要四个过程:- **预处理阶段** 对源代码文件中文件包含关系(头文件)、预编译语句(宏定义)进行分析和替换,**生成预编译文件**。 主要处理源代码文件中的以“#”开头的预编译指令。 对于.h文件会当做头文件编译- **编译阶段** 将经过预处理后的预编译生成的.i文件,进行一系列词法分析、语法分析、语义分析及优化后,生成相应的汇编代码文件- **汇编阶段** 将编译阶段生成的汇编文件转化成**机器码,生成可重定位目标文件**- **链接阶段** 把不同源文件产生的目标文件进行链接,最终成为一个**可执行目标文件**。链接分为静态链接和动态链接 1. 静态链接,在使用静态库的时候,**链接器 从库中复制这些函数和数据**,并把他们和应用程序的其他模块组合起来 缺点:空间浪费,副本需要额外的运行时存储空间,更新困难 优点:运行速度快 2. 动态链接,只有在程序运行的时候才把模块链接成一个程序,共享库文件不需要副本 缺点:性能损耗 ```c++ g++ a.o -o a //动态链接 g++ a.o -static -o a //静态链接 ```
编译机制
预处理器
预处理器;```#include // 利用制定头文件内容替代 #include```头文件保护符```#define //已定义预处理变量#ifdef //表示变量定义时为真#ifndef //表示变量未定义时为真#endif //操作结果直到 #endif#ifndef SALES_DATA_H#define SALES_DATA_H#include struct Sales_data {std::string bookNo;unsigned units_sold = 0;double rev = 0.0;};#endif```
头文件
库文件
库文件是计算机上的一类文件,提供给使用者一些开箱即用的变量、函数或类。**库文件分为静态库和动态库**,静态库和动态库的区别体现在程序的链接阶段:静态库在程序的链接阶段被复制到了程序中;动态库在链接阶段没有被复制到程序中,而是程序在运行时由系统动态加载到内存中供程序调用。**使用动态库系统只需载入一次**,不同的程序可以得到内存中相同的动态库的副本,因此节省了很多内存,而且使用动态库也便于模块化更新程序
静态链接库
静态库在链接阶段,会将汇编生成的目标文件.o与引用到的库一起链接打包到可执行文件中。因此对应的链接方式称为静态链接。静态库与汇编生成的目标文件一起链接为可执行文件,那么静态库必定跟.o文件格式相似。其实一个静态库可以简单看成是一组目标文件(.o/.obj文件)的集合,即很多目标文件经过压缩打包后形成的一个文件。静态库特点总结```1:静态库对函数库的链接是放在编译时期完成的。2:程序在运行时与函数库再无瓜葛,移植方便。3:浪费空间和资源,因为所有相关的目标文件与牵涉到的函数库被链接合成一个可执行文件。```其中 windows 静态链接库后缀为 **.lib**linux 静态链接库后缀为 **.a**
动态链接库
通过上面的介绍发现静态库,容易使用和理解,也达到了代码复用的目的,那为什么还需要动态库呢?```静态库的特点导致。空间浪费是静态库的一个问题。另一个问题是静态库对程序的更新、部署和发布页会带来麻烦。如果静态库liba.lib更新了,所以使用它的应用程序都需要重新编译、发布给用户(对于玩家来说,可能是一个很小的改动,却导致整个程序重新下载,全量更新)。动态库在程序编译时并不会被连接到目标代码中,而是在程序运行是才被载入。不同的应用程序如果调用相同的库,那么在内存里只需要有一份该共享库的实例,规避了空间浪费问题。动态库在程序运行是才被载入,也解决了静态库对程序的更新、部署和发布页会带来麻烦。用户只需要更新动态库即可,增量更新。```windows下动态库文件后缀为 **.dll**linux 下动态库文件为 **.so**
项目
头文件重复引用
环境配置
vscode
win
linux
编译器
gcc / g++ / gdb 环境```sudo apt-get install g++```
插件
```C/C+//必需Code Runner//必需C/C++ Snippets // 建议,提供一些常用的C/C++片段EPITECH C/C++ Headers // 建议,为C/C++文件添加头部(包括作者、创建和修改日期等),并为.h头文件添加防重复的宏File Templates // 建议,文件模板,可以自己添加文件模板GBKtoUTF8 // 建议,GBK编码文件转换为UTF-8Include Autocomplete // 建议,头文件自动补全One Dark Pro // 建议,可以打造好看的VS Code主题```