C++ 基础部分
1.迭代器类型
迭代器是一种检查容器内元素并遍历元素的数据类型。C++更趋向于使用迭代器而不是下标操作,因为标准库为每一种标准容器(如vector)定义了一种迭代器类型,而只用少数容器(如vector)支持下标操作访问容器元素。
共有五类迭代器:Input Iterator ,Output Iterator,Forwaed Iterator,Bidirectional Iterator,Random Access Iterator.
- 输入迭代器(Input Iterator):通过对输入迭代器解除引用,它将引用对象,而对象可能位于集合中。最严格的输入迭代只能以只读方式访问对象。例如:istream。
- 输出迭代器(Output Iterator):该类迭代器和Input Iterator极其相似,也只能单步向前迭代元素,不同的是该类迭代器对元素只有写的权力。例如:ostream, inserter。
以上两种基本迭代器可进一步分为三类: - 前向迭代器(Forward Iterator):该类迭代器可以在一个正确的区间中进行读写操作,它拥有Input Iterator的所有特性,和Output Iterator的部分特性,以及单步向前迭代元素的能力。
- 双向迭代器(Bidirectional Iterator):该类迭代器是在Forward Iterator的基础上提供了单步向后迭代元素的能力。例如:list, set, multiset, map, multimap。
- 随机迭代器(Random Access Iterator):该类迭代器能完成上面所有迭代器的工作,它自己独有的特性就是可以像指针那样进行算术计算,而不是仅仅只有单步向前或向后迭代。例如:vector, deque, string, array。
STL迭代器,是一个类模板,模拟了指针的功能,对很多指针常用的运算符进行了重载,封装了原生的指针,提供更高级的行为。返回的是对象的引用而不是值,需要解引用才可以输出。
2.虚函数
虚函数是实现多态的一种方式。在多态中有函数重载的方法(编译器决定、静态多态),也有函数重写(运行期决定,动态多态),实现这种动态多态,就是依靠虚函数的方式,具体的说就是借助指针或者是引用,在基类中的函数必须有virtual关键字。我们使用基类的指针去指向派生类(子类对象),可以统一的用父类指针去表示各子类对象,只有当运行期间的时候,我们才知道使用的是哪个版本的函数,这就是虚函数的作用。
虚函数的实现方式是利用虚指针以及虚函数表来实现。(具体见继承篇:单继承、多继承、菱形继承)
- 每个类生成一个虚表(virtual table,vtbl),虚表中存放一堆指针,指向该类的每一个虚函数,地址将按照声明顺序排列
虚表属于类,不属于具体的对象。一个类只有一份虚表。为了让每个包含虚表的类的对象都拥有一个虚表指针,编译器在类中添加了一个指针,
*__vptr
,用来指向虚表。这样,当类的对象在创建时便拥有了这个指针,且这个指针的值会自动被设置为指向类的虚表。 - 每一个类对象,都拥有一个虚表指针(vptr),由编译器生成。虚表指针的设定和重置,都有类的复制控制(也就是构造,析构,赋值操作符)完成。。现在许多编译器,把vptr放在一个类对象的最前端。
虚析构
父类的析构函数设置为虚函数是为了防止内存泄漏,当我们使用父类的指针指向子类的对象时,希望释放基类指针的时候,可以释放掉子类的空间。
如果我们不设置父类的析构函数设置为虚函数,当父类指针指向子类时,释放父类指针,并不会调用子类的析构函数(在子类的析构函数中几乎不可避免的涉及到子类资源的释放,比如有一个指针指向了一块内存,不析构就内存泄漏了),只会调用基类的析构函数。
而不一定每一个类都有派生类都要被继承,所以默认构造并不是虚析构。
3.指针和引用
指针有一块自己的空间,本身就是一个对象,允许对指针去赋值和拷贝,指针也可以为空。
而引用是给对象起了一个别名,写成&d符号,引用必须要去初始化,之后会绑定在一起不可以修改,所以不可以为空。
他们的本质区别在于 指针指向一块内存,指针的内容就是内存的地址,而引用是某块内存的别名,无法改变指向
一些不同点:
- 运算符的意义不一样,对于指针来说,++表现的地址的变化,而引用的++,是对对像本身的++操作。
- 函数传递传指针和引用形式:指针传递本身是一种值传递,传递一个地址值。被调用的函数把形参当做局部变量,在栈中生成副本,不会影响到实参。但是引用传递不一样,同样开辟内存空间,但是存放的是主调函数存放的实参变量地址,在函数里的操作是会影响到实参的。
4.vector、map、set
vector是动态空间,size表示实际存储的数据数量,而capacity则是当前可容纳的数量,也就是开辟的内存。当vector动态增加时,一旦旧有的空间满了,就会以原来的capacity大小俩倍申请另一块空间,把内容拷贝过去,再增加新元素,最后把原空间释放。如果频繁的进行扩展,可以在一开始的时候提前声明用来开辟大容量。
无论释放(pop_back)、删除(erase)还是清空(clear),都只会改变size,不会改变capacity,只有vector析构才会清空内存。
map是关联容器,用键值对来存储,底层实现是红黑树,插入删除等,都可以再o(log n)时间内完成。
set底层也是红黑树,会自动调整二叉树结构。
关于红黑树见别的章节。
map和unordered_map
unordered_map是哈希表实现的,是无序的,时间复杂度是常数级别,但是空间复杂度很高,如果时间上要求高效率就用unoerdered_map,空间上敏感或者要求有序就用map
5.预编译
预编译其实就是在编译过程之前,进行的一些代码文本的替换工作。
- 比如我们非常熟悉的#include,预编译做的工作其实非常简单,就是把要包含的文件,原样拷贝过来,直接粘贴到include的位置,当然,编译器会进行一些头文件保护的工作,保证不会重复包含。
#define
宏定义的替换#if
#endif
如果是0,就执行if后面的,否则就执行endif后面。
6.explicit 关键字
显式构造函数。如果构造函数声明了这个关键字,就不允许隐式自动转换。
举个例子,有一个参数的的构造函数,如果声明了explicit,就不可以用A a = 1
这种方式构造,因为这其实就是一种隐式转换,把1当做参数去进行构造。
7.智能指针
https://www.cnblogs.com/EvansPudding/p/12565968.html
8.C++ cast转换
https://www.cnblogs.com/EvansPudding/p/12566210.html
9.static关键字
- 全局静态变量,全局变量前加入static,除了声明它的文件之外,别的文件看不到。
static int s_Variable = 5; //这表示这个变量只会在这个翻译单元内链接(link)。
无论是静态变量或者是函数,都意味着,链接期不会在该翻译单元范围外查找该符号定义
举个例子,我们在名为B的cpp文件上定义了一个int s_Variable = 10
,名为A的cpp文件上定义statci int s_Variable = 5
在B文件中打印s_Variable,会显示10,不会产生编译错误。
假如我们没有给A文件中的s_Variable声明为static,产生了link错误,如果我们是希望使用的是A文件的s_Variable,那么可以在B文件中,给当前的s_Variable声明为extern int s_Varible
,那么这以为会从外部编译单元去找这个变量(要注意,如果你仍然给A的变量声明为static就会产生错误,会得到一个未解析的外部变量,因为他找不到)
-
静态函数,在函数返回类型前,加上static,这个函数就只能在这个文件中使用,不会被其他文件中的同名函数冲突
-
局部静态变量 局部变量前加入static关键字,作用域只在这个局部作用域,比如一个函数,函数结束,作用域结束,但是静态变量不会销毁,仍然在内存中,直到这个函数再次被调用,值不变。
-
类中的静态数据成员和静态函数成员,他们都属于类的静态成员,是对象共享,不属于某一个某一个对象成员。对于静态成员函数来说,引用是不需要对象名的。
static在C++中实际上有俩种含义,取决于使用的环境。
第一种是在类外,第二种是在类或者结构内
类外:static outside of class means that the linkage of that symbol that you declare to be static is going to be internal meaning,it only going to be visible to that translation unit that you defined it in。
定义在类外的static意味着被声明为static的是一个内在的意思,只会在一个翻译单元内可见