Basic c++ grammer & knowledge
1. Overview
Def: C++ 包含哪些东西?C++ 程序可以定义为对象的集合,这些对象通过调用彼此的方法进行交互。现在让我们简要地看一下什么是类、对象,方法、即时变量。对象和变量其实一回事,只不过从不同角度来看。
数据类型 - 其实不是类,无法继承,没有成员函数,但可以看作最基础的类
类 - 类可以定义为描述对象行为/状态的模板/蓝图。
对象/变量 - 对象当作类的实体化;变量是一样的,只不过变量强调的是内存的分配。
方法 - 从基本上说,一个方法表示一种行为。一个类可以包含多个方法。可以在方法中写入逻辑、操作数据以及执行所有的动作。
Def: C++运行顺序
预处理阶段:对源代码文件中文件包含关系(头文件)、预编译语句(宏定义)进行分析和替换,生成预编译文件。
编译阶段:将经过预处理后的预编译文件转换成特定汇编代码,生成目标汇编代码 .s
汇编阶段:将编译阶段生成的汇编文件转化成机器码,生成可重定位目标文件 .o
链接阶段:将多个目标文件及所需要的库连接成最终的可执行目标文件
Def:连接
https://www.jianshu.com/p/bda60193808d,.o文件包含很多区:.data代码和.symbol符号表等等。每个符号记录了函数、变量的定义和引用,所以连接就是要把不同文件的符号的定义和引用连起来,然后重新把正确的地址赋给引用。
Def: 动态链接vs静态连接(静态库把所有代码都融在一起,所以会重复载入内存很多代码。动态库可以把重复利用的代码段提出来,然后运行时候这段代码只载入一次,之后所有需要用到的程序都运行这一段代码)
静态库是用户在链接字节的可执行程序时已经将调用到的库函数的代码段链接到最终的可执行程序中,这样的好处是这样的可执行文件放到任何地方都能执行,坏处是库函数很占地方,使得链接得到的最终的可执行程序变得很大。
使用动态库的时候,函数本身编译链接到得到可执行程序的时候,并不会讲库函数链接到可执行程序中去,而是标记这个函数用到哪些库函数,当这个可执行程序运行时,操作系统会自动加载这些库到内存中。
Example:
通过上面的分析可以知道,静态库是将库函数链接到最终的可执行程序中,而动态库是没有将库函数链接到可执行程序中。从单个可执行程序的角度出发可执行程序在内存中运行时,系统还是会将库函数加载到内存中,这样从内存角度上讲,两个可执行程序最终在内存中运行时所占的空间还是一样的。但是从多个程序出发,就不一样了,比如a程序和b程序都用到了printf函数,使用静态库时,这两个可执行程序都包含了printf函数,所以这时候内存中就包含了两份printf函数;而使用动态库的时候,系统只会加载一份printf函数,当其他函数也要用到printf函数时,只要到加载的printf函数的地址中调用即可,不需要再次加载,所以当多个程序运行时,静态库就明显比动态库更占内存。
2. DataStructure(STL)
2.1. number
### types
Def:
2.1.1. type transform
Def: transform:首先,对于内置类型,低精度的变量给高精度变量赋值会发生隐式类型转换,其次,对于只存在单个参数的构造函数的对象构造来说,函数调用可以直接使用该参数传入,编译器会自动调用其构造函数生成临时对象。
Note: try not to see type as fixed length, because it differs from PC to PC
2.1.2. cast transform
Def: const cast: 用于将const变量转为非const, 但是通过p再写是没定义的行为。
Def: static cast: 用于各种隐式转换,比如非const转const,void*转指针等, static_cast能用于多态向上转化,如果向下转能成功但是不安全,结果未知;
Def:dynamic_cast 用于动态类型转换。只能用于含有虚函数的类,用于类层次间的向上和向下转化。只能转指针或引用。向下转化时,如果是非法的对于指针返回NULL,对于引用抛异常。要深入了解内部转换的原理。
向上转换:指的是子类向基类的转换
向下转换:指的是基类向子类的转换
它通过判断在执行到该语句的时候变量的运行时类型和要转换的类型是否相同来判断是否能够进行向下转换。
这种其实也是不被推荐使用的,更多使用static_cast,dynamic本身只能用于存在虚函数的父子关系的强制类型转换,对于指针,转换失败则返回nullptr,对于引用,转换失败会抛出异常
Def: reinterpret_cast 几乎什么都可以转,比如将int转指针,可能会出问题,尽量少用;
2.2. compound types
### reference
Def: reference is not an object !!!!
不能直接初始化一个右值(右值不是对象)
Note: 存在对指针的引用(*&)但是不存在对引用的指针(因为引用不是对象)
同理也不存在对引用的引用。
### pointer
- Def:同样的必须绑定一个左值。
Note:指针+1指的是指向下一个地址,比如int就是+4,char+1.
Note:如果返回动态内存分配的对象或者内存,必须使用指针,引用可能引起内存泄露。
Note:和数组的区别
只有在两种场合下,数组名并不用指针常量来表示--就是当数组名作为sizeof操作符或单目操作符&的操作数时,sizeof返回整个数组的长度,使用的是它的类型信息,而不是地址信息,不是指向数组的指针的长度。取一个数组名的地址所产生的是一个指向数组的指针,而不是指向某个指针常量值的指针。
int p [10] 指的是 int p 的10个数组
int (p) [10] 指的是 int [10] p也就是对int 10求指针。
2.2.1. pointer & reference
- Def: 怎么定义,从右到左。*& -> 对指针的引用。
2.2.2. smart pointer
Def: 智能指针主要用于管理在堆上分配的内存,它将普通的指针封装为一个栈对象。当栈对象的生存周期结束后,会在析构函数中释放掉申请的内存,从而防止内存泄漏。C++ 11中最常用的智能指针类型为shared_ptr,它采用引用计数的方法,记录当前内存资源被多少个智能指针引用。该引用计数的内存在堆上分配。当新增一个时引用计数加1,当过期时引用计数减一。只有引用计数为0时,智能指针才会自动释放引用的内存资源。对shared_ptr进行初始化时不能将一个普通指针直接赋值给智能指针,因为一个是指针,一个是类。可以通过make_shared函数或者通过构造函数传入普通指针。并可以通过get函数获得普通指针。
换而言之,一个share_ptr就是一个指针,并且会对每个引用过的对象有一个表格记录着引用计数,当一个指针指向另一个的时候就会把原引用计数-1, 而且当计数=0会自动销毁。
- Note: 当两个对象相互使用一个shared_ptr成员变量指向对方,会造成循环引用,使引用计数失效,从而导致内存泄漏。例如: 为了解决循环引用导致的内存泄漏,引入了weak_ptr弱指针,weak_ptr的构造函数不会修改引用计数的值,从而不会对对象的内存进行管理,其类似一个普通指针,但不指向引用计数的共享内存,但是其可以检测到所管理的对象是否已经被释放,从而避免非法访问。
2.3. constraint decoration
2.3.1. const
Def: const 必须初始化,而且初始化对象是不是const都可以。
Def: 默认文件内有效,若在别的文件用会报错。(因为一个const的使用必须有定义,也就是赋值支撑,所以在多个文件使用必须多个文件定义,会重复定义是变成单独的独立变量)所以如果想要多文件共享要加extern,一个文件定义,多个文件声明。
并且定义时也要加extern。
Def:const引用
记住const可以引用非const甚至常量,但是不能修改引用,也不能非const引用=const变量上。
const & = const/n const
n const & = n const
Def:const * 这个地方只在乎底层const,顶层const随意赋值无所谓(比如说int * = int * const是无所谓的)。
const * = const/ n const
n const * = n const
Def:指针和const的位置从右到左读:先const就是* const,意味着const的是指针;后const *意味着普通指针指向const的对象
第一个const叫底层,意思是指向的对象是const的;第二个const叫顶层,意思是指针本身是const。
Note: typedef 出现的问题
2.3.2. auto
2.3.3. decltype
2.3.4. static
Def:
stack ..https://blog.csdn.net/qian27enjoy/article/details/82624031
对于函数定义和代码块之外的变量声明,static修改标识符的链接属性,由默认的external变为internal,作用域和存储类型不改变,这些符号只能在声明它们的源文件中访问。
对于代码块内部的变量声明,static修改标识符的存储类型,由自动变量改为静态变量,作用域和链接属性不变。这种变量在程序执行之前就创建,在程序执行的整个周期都存在。
对于被static修饰的普通函数,其只能在定义它的源文件中使用,不能在其他源文件中被引用
对于被static修饰的类成员变量和成员函数,它们是属于类的,而不是某个对象,所有对象共享一个静态成员。静态成员通过<类名>::<静态成员>来使用。静态成员>类名>
\1. 全局静态变量
在全局变量前加上关键字static,全局变量就定义成一个全局静态变量。
静态存储区,在整个程序运行期间一直存在。
初始化:未经初始化的全局静态变量会被自动初始化为0(自动对象的值是任意的,除非他被显式初始化)。
作用域:全局静态变量在声明他的文件之外是不可见的,准确地说是从定义之处开始,到文件结尾。
\2. 局部静态变量
在局部变量之前加上关键字static,局部变量就成为一个局部静态变量。
内存中的位置:静态存储区。
初始化:未经初始化的全局静态变量会被自动初始化为0(自动对象的值是任意的,除非他被显式初始化)。
作用域:作用域仍为局部作用域,当定义它的函数或者语句块结束的时候,作用域结束。但是当局部静态变量离开作用域后,并没有销毁,而是仍然驻留在内存当中,只不过我们不能再对它进行访问,直到该函数再次被调用,并且值不变。
\3. 静态函数
在函数返回类型前加static,函数就定义为静态函数。函数的定义和声明在默认情况下都是extern的,但静态函数只是在声明他的文件当中可见,不能被其他文件所用。
函数的实现使用static修饰,那么这个函数只可在本cpp内使用,不会同其他cpp中的同名函数引起冲突。
warning:不要再头文件中声明static的全局函数,不要在cpp内声明非static的全局函数,如果你要在多个cpp中复用该函数,就把它的声明提到头文件里去,否则cpp内部声明需加上static修饰。
\4. 类的静态成员
在类中,静态成员可以实现多个对象之间的数据共享,并且使用静态数据成员还不会破坏隐藏的原则,即保证了安全性。因此,静态成员是类的所有对象中共享的成员,而不是某个对象的成员。对多个对象来说,静态数据成员只存储一处,供所有对象共用。
\5. 类的静态函数
静态成员函数和静态数据成员一样,它们都属于类的静态成员,它们都不是对象成员。因此,对静态成员的引用不需要用对象名。
在静态成员函数的实现中不能直接引用类中说明的非静态成员,可以引用类中说明的静态成员(这点非常重要)。如果静态成员函数中要引用非静态成员时,可通过对象来引用。从中可看出,调用静态成员函数使用如下格式:<类名>::<静态成员函数名>(<参数表>);参数表>静态成员函数名>类名>
Note: 什么时候初始化?
静态变量存储在虚拟地址空间的数据段和bss段,C语言中其在代码执行之前初始化,属于编译期初始化。而C++中由于引入对象,对象生成必须调用构造函数,因此C++规定全局或局部静态对象当且仅当对象首次用到时进行构造
2.4. struct
2.5. iterators
Def:Iterator(迭代器)模式又称Cursor(游标)模式,用于提供一种方法顺序访问一个聚合对象中各个元素, 而又不需暴露该对象的内部表示。或者这样说可能更容易理解:Iterator模式是运用于聚合对象的一种模式,通过运用该模式,使得我们可以在不知道对象内部表示的情况下,按照一定顺序(由iterator提供的方法)访问聚合对象中的各个元素。
由于Iterator模式的以上特性:与聚合对象耦合,在一定程度上限制了它的广泛运用,一般仅用于底层聚合支持类,如STL的list、vector、stack等容器类及ostream_iterator等扩展iterator。
2、迭代器和指针的区别
迭代器不是指针,是类模板,表现的像指针。他只是模拟了指针的一些功能,通过重载了指针的一些操作符,->、*、++、--等。迭代器封装了指针,是一个“可遍历STL( Standard Template Library)容器内全部或部分元素”的对象, 本质是封装了原生指针,是指针概念的一种提升(lift),提供了比指针更高级的行为,相当于一种智能指针,他可以根据不同类型的数据结构来实现不同的++,--等操作。
迭代器返回的是对象引用而不是对象的值,所以cout只能输出迭代器使用*取值后的值而不能直接输出其自身。
3、迭代器产生原因
Iterator类的访问方式就是把不同集合类的访问逻辑抽象出来,使得不用暴露集合内部的结构而达到循环遍历集合的效果。
2.5.1. impact on iterators
Def: https://www.geeksforgeeks.org/iterator-invalidation-cpp/
这个主要考察的是迭代器失效的问题。1.对于序列容器vector,deque来说,使用erase(itertor)后,后边的每个元素的迭代器都会失效,但是后边每个元素都会往前移动一个位置,但是erase会返回下一个有效的迭代器;2.对于关联容器map set来说,使用了erase(iterator)后,当前元素的迭代器失效,但是其结构是红黑树,删除当前元素的,不会影响到下一个元素的迭代器,所以在调用erase之前,记录下一个元素的迭代器即可。3.对于list来说,它使用了不连续分配的内存,并且它的erase方法也会返回下一个有效的iterator,因此上面两种正确的方法都可以使用。
重点在于什么呢?在于vector删除后,连续内存会变化(后面的元素前移,所以迭代器.next指向的内存错误),而map之类的红黑树删除后,其他的内存不会变化,变化的是其他的内存的next指针。
- Note: that push_back will invalidate all of vectors iterators on OJ but not on Clion.
2.5.2. for loop(i/auto)
Note:
- can't erase item in them
2.6. string
2.6.1. delete(erase/replace)
Note: for replace
- iterator remain valid(e.g. iter = begin+3, then after replacement it is still begin+3)
- return string
Note: for erase
- iterator behind will be invalid
2.6.2. compare
Def:
alphabet order & length order
2.7. vector
Def:
1)Vector
连续存储的容器,动态数组,在堆上分配空间
底层实现:数组
两倍容量增长:
vector 增加(插入)新元素时,如果未超过当时的容量,则还有剩余空间,那么直接添加到最后(插入指定位置),然后调整迭代器。
如果没有剩余空间了,则会重新配置原有元素个数的两倍空间,然后将原空间元素通过复制的方式初始化新空间,再向新空间增加元素,最后析构并释放原空间,之前的迭代器会失效。
性能:
访问:O(1)
插入:在最后插入(空间够):很快
在最后插入(空间不够):需要内存申请和释放,以及对之前数据进行拷贝。
在中间插入(空间够):内存拷贝
在中间插入(空间不够):需要内存申请和释放,以及对之前数据进行拷贝。
删除:在最后删除:很快
在中间删除:内存拷贝
适用场景:经常随机访问,且不经常对非尾节点进行插入删除。
2、List
动态链表,在堆上分配空间,每插入一个元数都会分配空间,每删除一个元素都会释放空间。
底层:双向链表
性能:
访问:随机访问性能很差,只能快速访问头尾节点。
插入:很快,一般是常数开销
删除:很快,一般是常数开销
适用场景:经常插入删除大量数据
2、区别:
1)vector底层实现是数组;list是双向 链表。
2)vector支持随机访问,list不支持。
3)vector是顺序内存,list不是。
4)vector在中间节点进行插入删除会导致内存拷贝,list不会。
5)vector一次性分配好内存,不够时才进行2倍扩容;list每次插入新节点都会进行内存申请。
6)vector随机访问性能好,插入删除性能差;list随机访问性能差,插入删除性能好。
3、应用
vector拥有一段连续的内存空间,因此支持随机访问,如果需要高效的随即访问,而不在乎插入和删除的效率,使用vector。
list拥有一段不连续的内存空间,如果需要高效的插入和删除,而不关心随机访问,则应使用list。
2.7.1. push_back
Def:
push_back函数引起的。这个函数会对传递进来的参数进行一次拷贝(调用拷贝构造函数),并将其添加到vector中
如果对象没有拷贝构造函数,编译器会为其生成一个,但是这个编译器生成的拷贝构造函数只是进行了一次浅拷贝, 即拷贝后的对象和原对象的str都是指向同一块内存区域
Example: push_back(int& a) is fine
2.7.2. resize vs reserve
Def:
resize():改变当前容器内含有元素的数量(size()),eg: vector
v; 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复制过来,销毁之前的内存;比size还小的话自动无视。 如果size = capacity 然后insert,就double capacity,如果reserve 大于capacity的,就分配到capacity内存。
2.7.3. compare
2.7.4. push_back vs emplace_back
2.7.5. delete(erase/ replace)
Note: for erase
- iterator behind will be invalid
2.8. map & set
Def: 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。
3. Function
3.1. function type
3.1.1. inline decoration
Def: inline decoration
replace function with codes
Qua: pros & cons
- pro: 内联函数编译器处理时相当于只调用方法体里面的逻辑,好处省时,适用于循环调用该函数时用
- con:为什么不把所有函数都写成内联函数?
1、内联编译时建议性的,由编译器决定
2、逻辑简单,调用频繁的函数建议使用内联
3、递归函数无法使用内联方式,编译器会把递归函数当成普通函数处理
3.1.2. static function
Def:this 指针,A->f() == class::f(&A),这是一个隐藏的函数,所有成员函数都是用这个隐藏函数调用的。
当我们调用成员函数时,实际上是替某个对象调用它。
成员函数通过一个名为 this 的额外隐式参数来访问调用它的那个对象,当我们调用一个成员函数时,用请求该函数的对象地址初始化 this。例如,如果调用 total.isbn()则编译器负责把 total 的地址传递给 isbn 的隐式形参 this,可以等价地认为编译器将该调用重写成了以下形式:
1
2//伪代码,用于说明调用成员函数的实际执行过程
Sales_data::isbn(&total)其中,调用 Sales_data 的 isbn 成员时传入了 total 的地址。
在成员函数内部,我们可以直接使用调用该函数的对象的成员,而无须通过成员访问运算符来做到这一点,因为 this 所指的正是这个对象。任何对类成员的直接访问都被看作是对 this 的隐式引用,也就是说,当 isbn 使用 bookNo 时,它隐式地使用 this 指向的成员,就像我们书写了 this->bookNo 一样。
对于我们来说,this 形参是隐式定义的。实际上,任何自定义名为 this 的参数或变量的行为都是非法的。我们可以在成员函数体内部使用 this,因此尽管没有必要,我们还是能把 isbn 定义成如下形式:
1
std::string isbn() const { return this->bookNo; }
因为 this 的目的总是指向“这个”对象,所以 this 是一个常量指针(参见2.4.2节,第56页),我们不允许改变 this 中保存的地址。
Def: static 变量就是类全局变量;static成员函数没有this指针的隐藏函数,所以无法通过实例调用。
3.2. declaration
Def: declaration
1
2//void fun(int i,int k = 5, int j);// 编译出错
void fun(int i, int k = 5, int j = 10);
3.3. definition
Def : definition
1
2
3void fun(int i, int k, int j) {
cout << i << k << j << endl;
}
3.4. overload(重载)
Def : overload
在相同作用域内,函数名称相同,参数个数,参数类型或者返回值不同的称之为函数重载
3.4.1. const overload
Def: const + 引用/指针可以作为不同函数(底层const),顶层const/底层常量是不行的。
3.5. override (重写)
Def :
3.6. passing parameter
- Def : how to pass (based on parameter type)
3.6.1. const
- Def: const顶层是被忽略的,就和上面说到的一样。const顶层是针对传递后的决策,所以可以被忽略,这样的一个后果就是const顶层会被重复定义(如果有一个相同参数的无顶层const)
3.6.2. reference
Def: how to choose parameter type
- Note: always use const & if possible => because const & can accept const&/&, & can only accept &.
3.6.3. array
Def: when parameter = array( actually passes pointer), we should judge validaty of the parameter
or pass end(array) to parameter
pass size
3.7. return parameter
Def: how to return (based on return type)
Note: 不要反悔局部引用/指针
3.7.1. return multiple
Def: return multiple => use reference
3.7.2. return left value
Def: only when return reference.
3.8. match
Def: 怎么match:每个匹配都大于等于其他函数的匹配且存在一个大于。
怎么算大于呢?也就是阶级越小越好。
4. Class
Def: class / struct difference
4.1. compiling sequence
Def: compiling sequence:
按代码从上到下
- 编译变量(variable)
- 类型别名(typeDef, using)
- 函数声明,返回值和形参;
最后
- 再编译成员函数的函数体。
Note: 这也就解释了为什么成员函数可以访问成员变量,即使这个变量是放在类的最末尾(后于这个成员函数定义)。原因就是编译器先编译整个类(除成员函数的函数体),然后再编译成员函数体。
这还解释了为什么在类中重命名一些类型尽量放到类的最开始处。因为类中的除成员函数体之外都是顺序编译的,如果不把类型重命名放到使用该重命名的类型之前,那么编译器就不会认识这个重命名的类型。
4.2. basic functions
4.2.1. construction function
Def : construction funciton
就是在声明对象的时候可以直接赋值给该对象私有成员,不用去调用接口赋值,方便一点,这个直接赋值呢,又分为在构造函数内部(函数实现部分,大括号内)对每个私有数据成员赋值,还可以用列表初始化的方式赋值(这点比较重要,在继承的时候还会再拓展的),书上例子都有,我在这说的意思就是你自己好好去看看书上的例题代码。
4.2.2. destruction function
Def : destruction function
在该类的对象的生命周期结束时调用,只要是用于若构造函数声明了内存空间的话,释放掉,析构函数的调用次序是对象的声明顺序的逆序,因为这些对象存入的地方是栈,栈具有先进后出的特点
析构函数(方法)作用和构造方法正好相反,是对象被销毁之前最后一个被对象自动调用的方法。是PHP5中新添加的内容作用是用于实现在销毁一个对象之前执行一些特定的操作,诸如关闭文件和释放内存等。
比如delete或者free。
Note:when to use
4.2.3. const function
Def: 根据调用者的const与否进行重载
4.2.4. static function
Def: init once
4.2.5. member function
- Def: 通过传递this指针指向实例,通过this指针访问成员函数。
4.3. inheritance
4.3.1. searching sequence & override
Def: 继承中的名字查找过程。这是因为c++从内层往外层搜索,而子类作用区域在最里面。
简单来说,就是从内往外找,然后如果是通过指针调用且找到的是虚函数就返回动态类型的版本。
Def: 重定义原则,只要是重名全覆盖,因此也是为什么虚函数重载要求一模一样了,因为如果不一样求覆盖了,注意这里的覆盖指的是找不到父类函数/成员了直接。所以尽量不要定义重名函数。
4.3.2. private/public/protected
Def: 3种继承中,子类的成员函数只有private的父类变量无法访问;而如果是实例直接访问父类变量,那么只有public继承的实例可以直接访问父类public变量。
可以这么记:private继承的实例和protected继承的实例,对所有基类变量无法访问。
public继承的实例可以访问积累public变量。
而三种继承的实例的成员函数不能访问基类protected变量。
从基类角度理解,protect变量开放给基类成员函数而private不开放,public不仅开放给成员函数还开放给public实例。(类成员函数通过this访问,和实例直接访问理论上是一样的,但是这里实际上权限是不一样的)
4.3.2.1. friend
Def: 类内生命一个friend,可以访问所有
- Note: friend不被继承。而且只对声明的两个类之间的关系有效,无论是朋友1继承还是朋友2继承,只要是朋友2 访问朋友1的元素就都是可以的。
4.3.2.2. using
- Def:可以用using改变基类的public/private属性。比如 public: using Base::xx,就把xx改为public属性。
4.3.3. virtual function
Def:虚函数:等待子类定义的函数,不能是static的。这个指的是不同子类可以自定义自己的函数。和子类重定义的区别在于,重定义使得子类实例可以调用正确的函数,而重写虚函数使得子类可以用父类指针/引用表示,然后调用父类的虚函数最终调用正确的函数(也就是多了一个动态绑定的特性)
Note:必须声明,如果没有定义的话,直接继承父类虚函数。
Note:在子类中也是虚函数,但是不能直接调用。
Note: final 用于禁止子类再覆盖该虚函数,ovreride为了保证不会出现参数错误从而没覆盖上的错误。
4.3.3.1. dynamic binding
Def: 指针的动态绑定:子类中包含父类对象,所以可以用父类指针/引用指向子类。这个指的是用父类当参数,子类可以使用自己的自定义虚函数
Note:注意动态绑定不能用子类指针指向父类,因为是动态绑定,编译器无法判断这个是否安全。
Note:动态绑定不能用于把用子类给父类赋值并期待子类会类型转换为父类,只能执行父类的构造函数然后切掉一部分。
4.3.3.2. virtual xigou
Def:虚析构函数(当删除父类的时候需要递归到子类调用)
4.3.3.3. virtual table
Def:虚函数表
这个东西的流程是:1)创建一个类的时候就把虚表指向的东西搞好,创建的实例的时候就把_vptr指向类的虚表2)当使用父类调用的时候,由于 _vptr在父类作用域所以可以直接指向该实例的真实虚表从而真实调用。所以重点在于提前把虚表搞好(编译时),然后 _vptr传递给父类该实例的真实信息
4.3.3.4. pure virtual function
Def: 当一个类包含虚函数而且我们不想把这个类实例化出来(就是一个接口作用)的时候,我们定义为纯虚函数。
Note:抽象类不能直接实例化
5. Variable
Def: left & right,左值是对象,右值是临时变量。
右值引用是C++11中引入的新特性 , 它实现了转移语义和精确传递。它的主要目的有两个方面:
\1. 消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率。
\2. 能够更简洁明确地定义泛型函数。
左值和右值的概念:
左值:能对表达式取地址、或具名对象/变量。一般指表达式结束后依然存在的持久对象。
右值:不能对表达式取地址,或匿名对象。一般指表达式结束就不再存在的临时对象。
右值引用和左值引用的区别:
\1. 左值可以寻址,而右值不可以。
\2. 左值可以被赋值,右值不可以被赋值,可以用来给左值赋值。
\3. 左值可变,右值不可变(仅对基础类型适用,用户自定义类型右值引用可以通过成员函数改变)
5.1. declaration & definition
Def:difference
- Note: extern的索取搜索同名定义是在同一个作用域内搜索。
Def: TYPE(int) + {(COMPOUND) + NAME}
1
int *p, &r, g;
Def: C++ is statically typed, which means a variable <==> object (this is different from python where variable can change)
5.1.1. header
Def:
Note: multiple include same .h in one file (重复定义)
Def: 使用规则
规则1 头文件(.h)中是对于该模块接口的声明,接口包括该模块提供给其它模块调用的外部函数及外部全局变量,对这些变量和函数都需在.h中文件中冠以extern关键字声明; 规则2 模块内的函数和全局变量需在.c文件开头冠以static关键字声明;
规则3 永远不要在.h文件中定义变量;
规则4 如果要用其它模块定义的变量和函数,直接包含其头文件即可。
也就是说所有定义在.c的最外侧完成(extern的索取搜索需要在同一个作用域),这样最大程度避免重复定义,因为所有东西只定义一次,要用全部用extern索取。假如在.h中定义,那么两个文件include.h 就血崩了。
5.2. copy
- Note:深拷贝与浅拷贝的区别就在于深拷贝会在堆内存中另外申请空间来储存数据,从而也就解决了指针悬挂的问题。简而言之,当数据成员中有指针时,必须要用深拷贝。
5.2.1. copy constructor
Def: 初始化都是拷贝构造函数
Note: 为什么必须是引用:因为假如不是的话,会循环调用拷贝函数。
Note: 什么时候会被使用,简单来说传参、返回都是拷贝函数。
Note: 当涉及指针操作的时候,必须使用copy constructor。因为指针单纯复制是不行的,xigou两次的话就血崩了。
Note:有拷贝复制的话也要拷贝构造。
5.2.2. copy operation
Def: 等号都是拷贝赋值
5.2.3. shallow copy
5.2.4. deep copy
5.3. type cast
Def: invisible
Def: cast...
5.4. affect field
Def : affect field
命名空间是ANSIC++引入的可以由用户命名的作用域,用来处理程序中 常见的同名冲突。 在 C语言中定义了3个层次的作用域,即
文件,编译单元
- 可能是单个文件编译,
- 可能是很多文件.c 一起编译,那么这个时候在main外面的作用域是通用的,不能一个.c里面定义一个c另一个还定义一个c。
命名空间
C++又引入了类作用域,类是出现在文件内的。
函数(函数内)
复合语句(语句内)
Def: include的作用,就是在.h里有声明,如果你include .h后就会把声明继承过来,然后有了声明就会从所有.c文件的外围(main外面)去找对应的东西。相当于每个·c文件都有自己定义好的资源,你需要去include声明才能使用别的.c文件的资源。
5.4.1. namespace
Def: namespace
实际上就是一个由程序设计者命名的内存区域,程序设计者可以根据需要指定一些有名字的空间域,把一些全局实分别放在各个命名空间中,从而与其他全局实体分隔开来。
Qua: usage
建立一些互相分隔的作用域,把一些全局实体分隔开来。
Example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
using namespace std;
namespace myNameSpace {
int A;
class Liu{
public:
void setA(){
A = 20;
}
};
void print(){
cout << "A :" << A << endl;
}
int getMax(int num) {
return num;
}
}
using namespace myNameSpace;
int main() {
myNameSpace::A = 10;
myNameSpace::print();
myNameSpace::Liu liu;
liu.setA();
print();
std::cout << "命名空间的使用:" << myNameSpace::getMax(9) << std::endl;
return 0;
}
5.4.2. decoration.
- Def : different namespaces
1.显式,在某某命名空间中
- 2.全局命名空间,等同于全局空间。
3.匿名命名空间 例:namespace{ class A; }
6. Memory
6.1. allocation
Def : allocation
6.2. malloc
- Def:
Malloc函数用于动态分配内存。为了减少内存碎片和系统调用的开销,malloc其采用内存池的方式,先申请大块内存作为堆区,然后将堆区分为多个内存块,以块作为内存管理的基本单位。当用户申请内存时,直接从堆区分配一块合适的空闲块。Malloc采用隐式链表结构将堆区分成连续的、大小不一的块,包含已分配块和未分配块;同时malloc采用显示链表结构来管理所有的空闲块,即使用一个双向链表将空闲块连接起来,每一个空闲块记录了一个连续的、未分配的地址。
当进行内存分配时,Malloc会通过隐式链表遍历所有的空闲块,选择满足要求的块进行分配;当进行内存合并时,malloc采用边界标记法,根据每个块的前后块是否已经分配来决定是否进行块合并。
Malloc在申请内存时,一般会通过brk或者mmap系统调用进行申请。其中当申请内存小于128K时,会使用系统函数brk在堆区中分配;而当申请内存大于128K时,会使用系统函数mmap在映射区分配。
6.3. new
Def: new & malloc
malloc开辟空间类型大小需手动计算,new是由编译器自己计算;
(2)malloc返回类型为void*,必须强制类型转换对应类型指针,new则直接返回对应类型指针;
(3)malloc开辟内存时返回内存地址要检查判空,因为若它可能开辟失败会返回NULL;new则不用判断,因为内存分配失败时,它会抛出异常bac_alloc,可以使用异常机制;
(4)无论释放几个空间大小,free只传递指针,多个对象时delete需加[](原因在第3);
Def:memory leak
内存泄漏通常是由于调用了malloc/new等内存申请的操作,但是缺少了对应的free/delete。为了判断内存是否泄露,我们一方面可以使用linux环境下的内存泄漏检查工具Valgrind,另一方面我们在写代码时可以添加内存申请和释放的统计功能,统计当前申请和释放的内存是否一致,以此来判断内存是否泄露。
\3. 没有将基类的析构函数定义为虚函数。当基类指针指向子类对象时,如果基类的析构函数不是virtual,那么子类的析构函数将不会被调用,子类的资源没有正确是释放,因此造成内存泄露。
6.4. memory storage
Def
静态区域:
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命令指定。
Def: 大端小端
这就好理解了:如果把一个数看成一个字符串,比如11223344 它的尾端很显然是44,看它是高尾端还是低尾端就看尾端放在高地址还是低地址,它在内存中的放法非常直观,如下图:大端就是正常放,小端饭过来。所有都是以字节为单位也就是两位16进制数比如说0x0000001 -> 0x01000000.
6.5. heap storage
Example: in c
1
2
3
4
5
6int main() {
/**
* C语言内存申请和释放
* void *malloc(size_t size)
* void free(void *memblock)
*/Example: in c++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23//C++中内存申请和释放使用关键字new和delete,需要配套使用
//申请初始化指针,分配一块4的内存,初始化值为10
int *p = new int(10);
if (NULL == p) {
system("pause");
return 0;
}
//释放
delete p;
p = NULL;
//如何申请和释放内存块?
int *arr = new int[1000]; 创建一个10个大小的int指针数组。
//假设内存申请失败
if (NULL == arr) {
system("pause");
return 0;
}
delete[] arr;//如果不加[],则只会释放arr[0]的内存
arr = NULL;
cout << "C++内存管理" << endl;
return 0;}Example: in c++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36using namespace std;
class Tv {
public:
int x = 10;
int y = 20;
void px() {
cout << x << endl;
}
void py() {
cout << y << endl;
}
};
int main() {
Tv tv;
cout << tv.x << endl; //在内存开辟一块栈空间,不需要主动释放
tv.px();
tv.py();
// Tv *tvNew;
Tv *tvNew = new Tv(); // 向内存申请一块堆空间
if (NULL == tvNew) {
system("pause");
return 0;
}
// 类似于C语言的写法
cout << (*tvNew).x << endl;
(*tvNew).px();
(*tvNew).py();
// 类似于NDK编程的写法
cout << tvNew->x << endl;
tvNew->px();
tvNew->py();
delete tvNew; // 在内存中申请的堆空间,需要主动释放
tvNew = NULL;
return 0;
6.6. segment fault
- Def: segment fault
- 内存分配未成功,却被使用。 对策:使用内存之前检查是否分配成功。用p!=NULL判断。
- 内存分配成功,未初始化就被使用。 内存的缺省值没有统一的标准。大部分编译器以0作为初始值,但不完全是。 对策:内存初始化时赋初值。
- 内存操作越界。 对策:只能是小心了。
- 释放了内存,仍然使用。 (1) 使用显示delete和free的野指针。 对策:释放完内存,将指针置为NULL。 (2) 使用隐式delete和free的野指针。主要是指函数返回指向栈内存的指针或引用。 对策:当然是不要返回就可以了。
- 未释放内存,导致内存泄露。 用new/malloc开辟了内存,没用delete/free释放. 对策:new和delete的个数一定相同;malloc和free的个数一定相同;new[]和[]delete一定对应。
7. Template
Def:什么是模版:
- typename T
- 某一个数据类型 N
- Def: 模版能干什么?
- 函数
- 类
- Note:static归一个类的共享。
Note: 必须显示用typename T::xx 表示类型