1.c++内存四区
2.const和define的区别
(1)就起作用的阶段而言:#define是在编译的预处理阶段起作用(在预处理阶段进行替换),而const是在编译运行的时候起作用(const修饰的只读变量是在编译的时候确定其值) (2)就起作用的方式而言:#define只是简单的字符串替换,没有类型检查。而const有对应的类型,是要进行判断的,可以避免一些低级的错误 (3)就存储方式而言:#define只是进行展开,有多少地方使用,就替换多少次。它定义的宏常量在内存中存若干个备份;const定义的只读变量在程序中只有一份备份 (4)从代码调试的方便程度而言:const常量可以进行调试的,define是不能进行调试的,因为在预编译阶段就已经进行替换了 (5)就内存分配而言:编译器通常不为普通的const只读变量分配存储空间,而是将它们保存在符号表中,这使得它成为一个编译期间的值,没有了存储与读内存的操作,使得它的效率也很高
#define M 3
const int N = 5;
int i = N;此时为N分配内存,以后不再分配
int l = M;
int j = N;
int J = M;
const定义的只读变量从汇编的角度来看,只是给出了对应的内存地址,而不是像#define一样给出的是立即数,所以,const定义的只读变量在程序运行过程中只有一份拷贝(因为它是全局的只读变量,存放在静态区),而#define定义的宏常量在内存中有若干个拷贝。 const的优点 (1)const常量有数据类型,而宏常量没有数据类型,编译器可以对const进行类型安全检查,而对后者只进行字符替换,没有类型安全检查,并且在字符替换可能会产生意料不到的错误 (2)有些集成化的调试工具可以对const常量进行调试,但是不能对宏常量进行调试 (3)const在内存中只存储了一份,节省了空间,避免不必要的内存分配,提高了效率
3.数组,std::array和STL的vector数组有什么区别
(1)数组 这些数的类型必须相同。 这些数在内存中必须是连续存储的。 (2)std::array array 容器是 C++ 11 标准中新增的序列容器,简单地理解,它就是在 C++ 普通数组的基础上,添加了一些成员函数和全局函数。在使用上,它比普通数组更安全(原因后续会讲),且效率并没有因此变差。 和其它容器不同,array 容器的大小是固定的,无法动态的扩展或收缩,这也就意味着,在使用该容器的过程无法借由增加或移除元素而改变其大小,它只允许访问或者替换存储的元素。 (3)STL的vector ector 容器是 STL 中最常用的容器之一,它和 array 容器非常类似,都可以看做是对 C++ 普通数组的“升级版”。不同之处在于,array 实现的是静态数组(容量固定的数组),而 vector 实现的是一个动态数组,即可以进行元素的插入和删除,在此过程中,vector 会动态调整所占用的内存空间,整个过程无需人工干预。 v ector 常被称为向量容器,因为该容器擅长在尾部插入或删除元素,在常量时间内就可以完成,时间复杂度为O(1);而对于在容器头部或者中部插入或删除元素,则花费时间要长一些(移动元素需要耗费时间),时间复杂度为线性阶O(n)。 对于一个 vector 对象来说,通过该模板类提供的 capacity() 成员函数,可以获得当前容器的容量;通过 size() 成员函数,可以获得容器当前的大小。 vector容器的底层实现机制: STL 众多容器中,vector 是最常用的容器之一,其底层所采用的数据结构非常简单,就只是一段连续的线性内存空间。 通过分析 vector 容器的源代码不难发现,它就是使用 3 个迭代器(可以理解成指针)来表示的:
template <class _Ty, class _Alloc = allocator<_Ty>>
class vector{
...
protected:
pointer _Myfirst;
pointer _Mylast;
pointer _Myend;
};
其中,_Myfirst 指向的是 vector 容器对象的起始字节位置;_Mylast 指向当前最后一个元素的末尾字节;_myend 指向整个 vector 容器所占用内存空间的末尾字节。
图 1 演示了以上这 3 个迭代器分别指向的位置。 在此基础上,将 3 个迭代器两两结合,还可以表达不同的含义,例如:
_Myfirst 和 _Mylast 可以用来表示 vector 容器中目前已被使用的内存空间; _Mylast 和 _Myend 可以用来表示 vector 容器目前空闲的内存空间; _Myfirst 和 _Myend 可以用表示 vector 容器的容量。 vector扩大容量的本质: 当 vector 的大小和容量相等(size==capacity)也就是满载时,如果再向其添加元素,那么 vector 就需要扩容。vector 容器扩容的过程需要经历以下 3 步:
完全弃用现有的内存空间,重新申请更大的内存空间; 将旧内存空间中的数据,按原有顺序移动到新的内存空间中; 最后将旧的内存空间释放。 emplace_back()和push_back()的区别 emplace_back() 和 push_back() 的区别,就在于底层实现的机制不同。push_back() 向容器尾部添加元素时,首先会创建这个元素,然后再将这个元素拷贝或者移动到容器中(如果是拷贝的话,事后会自行销毁先前创建的这个元素);而 emplace_back() 在实现时,则是直接在容器尾部创建这个元素,省去了拷贝或移动元素的过程。 相同点:
三者均可以使用下表运算符对元素进行操作,即vector和array都针对下标运算符[]进行了重载
三者在内存的方面都使用连续内存,即在vector和array的底层存储结构均使用数组
不同点:
vector属于变长容器,即可以根据数据的插入删除重新构建容器容量;但array和数组属于定长容量。
vector和array提供了更好的数据访问机制,即可以使用front和back以及at访问方式,使得访问更加安全。而数组只能通过下标访问,在程序的设计过程中,更容易引发访问 错误。
vector和array提供了更好的遍历机制,即有正向迭代器和反向迭代器两种
vector和array提供了size和判空的获取机制,而数组只能通过遍历或者通过额外的变量记录数组的size
vector和array提供了两个容器对象的内容交换,即swap的机制,而数组对于交换只能通过遍历的方式,逐个元素交换的方式使用
array提供了初始化所有成员的方法fill
vector提供了可以动态插入和删除元素的机制,而array和数组则无法做到,或者说array和数组需要完成该功能则需要自己实现完成
由于vector的动态内存变化的机制,在插入和删除时,需要考虑迭代的是否失效的问题。
总结:如果只是需要固定大小的数组,那么应该使用std::array,我们可以使用很多成员函数;
如果需要的是支持插入,删除,扩展的数组,那么建议用std::vector。 需要考虑到性能的时候,如果是进行一些数***算或者算法的时候,对性能要求比较高 。
4.数组和链表,什么时候使用数组结构?什么时候使用链表结构?
- 数组和链表的区别
数组:
数组的元素在内存中连续存储的;
它的优点:因为数据是连续存储的,所以内存地址连续,在查找数据的时候效率比较高;
它的缺点:在创建的时候,我们需要确定其大小,申请一块连续的内存空间,一经创建就无法改变。在运行的时候,空间的大小是无法随着需要增加和减少而改变的。
当数据量比较大的时候,有可能会出现越界的情况,数据比较小的时候,又有可能会浪费掉内存空间。在改变数据个数时,插入、删除数据效率比较低。
链表:
链表是动态申请内存空间,其不需要像数组需要在创建的时候就确定好内存的大小,链表的具体实现有ArrayList和LinkedList,前者默认大小为10,当存储空间不足的时候会自动扩容,而后者没有默认大小,只有你想,你可以一直扩容下去。
链表只需要在用的时候申请就可以了,根据需求来动态申请或者删除内存空间,对于数据插入和删除比数组灵活。还有就是链表中的数据可以放在内存中的任何地方。
- 链表和数组使用场景
数组应用场景:
数据比较少;经常做的运算,是按序号访问数据元素;构建的线性表较稳定。 链表应用场景:
对线性表的长度或者规模难以估计;频繁做插入删除操作;构建动态性比较强的线性表。
5.static关键字的作用
全局静态变量 在全局变量前加上关键字static,全局变量就定义成一个全局静态变量。 内存位置:静态存储区,在整个程序运行期间一直存在。 初始化:未经初始化的全局静态变量会被自动初始化为0(自动对象的值是任意的,除非他 被显式初始化),编译时初始化。 作用域:全局静态变量在声明他的文件之外是不可见的,准确地说是从定义之处开始,到 文件结尾。 局部静态变量 在局部变量之前加上关键字static,局部变量就成为一个局部静态变量。 内存中的位置:静态存储区 初始化:静态局部变量在第一次使用时被首次初始化,即以后的函数调用不再进行初始 化,未经初始化的会被程序自动初始化为0; 作用域:作用域仍为局部作用域,当定义它的函数或者语句块结束的时候,作用域结束。但是当局部静态变量离开作用域后,并没有销毁,而是仍然驻留在内存当中,只不过我们 不能再对它进行访问,直到该函数再次被调用,并且值不变; 静态函数 在函数返回类型前加static,函数就定义为静态函数。函数的定义和声明在默认情况下都是 extern的,但静态函数只是在声明他的文件当中可见,不能被其他文件所用。函数的实现 使用static修饰,那么这个函数只可在本cpp内使用,不会同其他cpp中的同名函数引起冲 突; warning:不要再头文件中声明static的全局函数,不要在cpp内声明非static的全局函数, 如果你要在多个cpp中复用该函数,就把它的声明提到头文件里去,否则cpp内部声明需加 上static修饰; 类的静态数据成员 静态数据成员可以实现多个对象之间的数据共享,它是类的所有对象的共享成员,它 在内存中只占一份空间,如果改变它的值,则各对象中这个数据成员的值都被改变。 静态数据成员是在创建类对象前被分配空间,到程序结束之后才释放,只要类中指定 了静态数据成员,即使不定义对象,也会为静态数据成员分配空间。 静态数据成员可以被初始化,但是只能在类体外进行初始化,静态成员变量使用前必 须先初始化,若未对静态数据成员赋初值,则编译器会自动为其初始化为 0。 静态数据成员既可以通过对象名引用,也可以通过类名引用。 基类定义了static静态成员,则整个继承体系里只有一个这样的成员。无论派生出多少 个子类,都有一个static成员实例。 类的静态函数 静态成员函数和静态数据成员一样,他们都属于类的静态成员,而不是对象成员。 非静态成员函数有 this 指针,而静态成员函数没有 this 指针。 静态成员函数主要用来访问静态成员而不能访问非静态成员
6.指针和引用
引用 引用就是C++对C语言的重要扩充。引用就是某一变量的一个别名,对引用的操作与对变 量直接操作完全一样。引用的声明方法:类型标识符 &引用名=目标变量名;引用是对象 的一个同义词。 指针 指针就是所指对象的地址。 引用和指针的区别 大小:指针有自己的一块空间,使用sizeof看一个指针的大小是4;而引用只是一个别 名,其大小是引用对象的大小 初始化:指针可以被初始化为nullptr,在使用中可以指向其它对象;而引用必须被初 始化且必须是一个已有对象的引用;引用只能是一个对象的引用,使用时不能被改 变; 作为参数传递时,指针需要被解引用才可以对对象进行操作,而直接对引用的修改都 会改变引用所指向的对象; 可以有const指针,但是没有const引用; 指针可以有多级指针(** p),而引用只有一级; 指针和引用使用++运算符的意义不一样; 引用的底层实现 引用在内存中也会分配空间,空间中存放的是被引用变量的地址,因此引用可以看作 一个常量指针。 对引用取地址操作,其实是对被引用变量取地址,编译器将对引用取地址解释为&(* ptr) 取地址
7.野指针与悬空指针
野指针就是没有初始化的指针。野指针就是指针指向的位置是不可知的(随机的、不正确 的、没有明确限制的)指针。 解决: release会切断unique_ptr和它原来管理的对象间的联系。release返回的指针通常被 用来初始化另一个智能指针或给另一个智能指针赋值。 unique_ptrp2(p1.release()) //将p1的所有权交给p2 reset接受一个可选的指针参数,令unique_ptr重新指向给定的指针。如果 unique_ptr不为空,它指向的对象被释放。p2.reset(p3.release()) //释放p2原来指 向的对象,并指向p3指的对象 初始化时设置为nullptr 悬空指针就是最初指向的内存已经被释放了的指针。 解决:其内存被释放后,指向nullptr
8.C++里是怎么定义常量的?常量存放在内存的哪个位置?
常量在C++里的定义就是一个const加上对象类型,常量定义必须初始化。对于局部对象, 常量存放在栈区,对于全局对象,静态变量存放在静态存储区。对于常量存放在常量存储 区。 const修饰成员函数的目的是什么? const修饰的成员函数表明函数调用不会对对象做出任何更改,事实上,如果确认不会对对 象做更改,就应该为函数加上const限定,这样无论const对象还是普通对象都可以调用该 函数。
9.new/delete与malloc/free
new/delete new运算返回所分配内存单元的起始地址,所以需要把返回值保存在一个指针变量中。若 分配不成功,返回NULL,并抛出异常。new没有为创建的对象命名,只能通过指针去访问 对象或者数组。 delete释放内存,只是销毁内存上的对象,但是指针仍然存在,仍然指向原来的内存,保 存原来空间的地址。所以我们应该在释放之后将指针置空,以避免后面不小心解引用造成 问题。 malloc/free void malloc(unsigned int size) 开辟一块长度为size的连续内存空间,返回类型为void类型的指针。在使用malloc开辟一 段空间时,void 要显示的转换为所需要的类型,如果开辟失败,则返回NULL指针。 free函数是来释放动态开辟的内存的。
10.strlen和sizeof的区别
sizeof操作符的结果类型为size_t,计算的是分配空间的实际字节数。strlen结果类型 也为size_t,但strlen是计算的字符串中字符的个数(不包括‘\0’)。 sizeof是运算符,可以以类型、函数、做参数 。strlen是函数,只能以char*(字符串) 做参数。而且,要想得到的结果正确必须包含 ‘\0’(通过strlen的实现得知)。 sizeof是在编译的时候就将结果计算出来了是类型所占空间的字节数,所以以数组名 做参数时计算的是整个数组的大小。而strlen是在运行的时候才开始计算结果,这是 计算的结果不再是类型所占内存的大小,数组名就退化为指针了。 sizeof不能计算动态分配空间的大小 结构体中不能有函数声明 可以有 不能使用C++的访问修饰符 可以使用 使用结构体定义变量必须加struct 可以不用加 单纯的用作数据的复合类型 可以当作类使用
11.const关键字
const修饰普通变量 使用const修饰普通变量,在定义该变量时,必须初始化,并且之后其值不会再改变。 const的引用 把引用绑定到const对象上,称之为对常量引用。对常量引用不能被用作修改它所绑定的对 象。
const int ci=1024;
const int &ri=ci;
ri=42;
int &r2=ci;
指针和const 和引用一样,可以使用指针指向常量,这称为指向常量的指针,此时指针指向的是常量, 因此无法通过指针改变其指向对象的值,想要存放常量对象的地址,只能使用指向常量的 指针。
const int a=1;
const int *ptr=&a;
int *p=&a;
除了指向常量的指针外,还可以使用从const修饰指针,即指针本身是常量,称为常量指 针。常量指针必须初始化,并且之后该指针的值就不会再改变。
int a=1;
int *const b=a;
可以改变的,*b也可以改变,但是b无法改变,类似于常量引用。
说到底,就是const修饰谁,谁就无法改变,不是const修饰的对象就可以改变。 函数中的const参数 const修饰函数参数,表示参数不可变,此时可以使用const引用传递。const引用传递和函 数按值传递的效果是一样的,但按值传递会先建立一个类对象的副本, 然后传递过去,而它直 接传递地址,所以这种传递比按值传递更高效。
12.函数指针和指针函数
函数指针 函数指针,其本质是一个指针变量,该指针指向这个函数。总结来说,函数指针就是指向 函数的指针。 声明格式:类型说明符 (* 函数名) (参数) 指针函数 指针函数,简单的来说,就是一个返回指针的函数,其本质是一个函数,而该函数的返回 值是一个指针。 声明格式为:类型标识符* 函数名(参数表)
13.C++中struct和class的区别
在C++中,可以用struct和class定义类,都可以继承。 struct的默认继承权限和默认访问权限是public,而class的默认继承权限和默认访问权限是private。 class可以使用模板,struct不能使用模板。
14.C++ explicit
C++中的explicit关键字只能用于修饰只有一个参数的类构造函数, 它的作用是表明该构造 函数是显示的, 而非隐式的, 跟它相对应的另一个关键字是implicit, 意思是隐藏的,类构造函 数默认情况下即声明为implicit(隐式).
15.inline关键字
在c/c++中,为了解决一些频繁调用的小函数大量消耗栈空间(栈内存)的问题,特别的引入了inline修饰符,表示为内联函数。栈空间就是指放置程序的局部数据(也就是函数内数据)的内存空间。 增加了 inline 关键字的函数称为“内联函数”。内联函数和普通函数的区别在于:当编译器处理调用内联函数的语句时,不会将该语句编译成函数调用的指令,而是直接将整个函数体的代码插人调用语句处,就像整个函数体在调用处被重写了一遍一样。有了内联函数,就能像调用一个函数那样方便地重复使用一段代码,而不需要付出执行函数调用的额外开销。很显然,使用内联函数会使最终可执行程序的体积增加。以空间换取时间,或增加空间消耗来节省时间,这是计算机学科中常用的方法。 inline 和宏定义的区别 内联函数在编译时展开,宏在预编译时展开; 内联函数直接嵌入到目标代码中,宏是简单的做文本替换; 内联函数可以完成诸如类型检测,语句是否正确等编译功能,宏就不具有这样的功 能。 宏不是函数,inline函数是函数 宏在定义时要小心处理宏参数,一般用括号括起来,否则容易出现二义性。而内联函 数不会出现二义性。 使用限制 inline的使用是有所限制的,inline只适合涵数体内代码简单的涵数使用,不能包含复杂的 结构控制语句例如while、switch,并且不能内联函数本身不能是直接递归函数(即,自己 内部还调用自己的函数)。 16.struct和union struct和union都是由多个不同的数据类型成员组成, 但在任何同一时刻, union中只存 放了一个被选中的成员, 而struct的所有成员都存在。 在struct中,各成员都占有自己的内存空间,它们是同时存在的。一个struct变量的总 长度等于所有成员长度之和。 在union中,所有成员不能同时占用它的内存空间,它们不能同时存在。Union变量的 长度等于最长的成员的长度。对于union的不同成员赋值, 将会对其它成员重写, 原来 成员的值就不存在了, 而对于struct的不同成员赋值是互不影响的。
16.为什么析构函数必须是虚函数
对于一个基类和派生类来说,在调用构造函数时先调用基类的构造函数,再调用派生类构 造函数;而当调用析构函数时,则要先调用派生类的析构函数再调用基类的析构函数。如 果定义了一个指向派生类对象的基类指针,当析构函数为普通函数时,释放该基类指针 时,只会调用基类的析构函数,而不会调用派生类的析构函数,会导致内存泄漏。当我们把基类析构函数定义为虚函数时,在调用析构函数时,会在程序运行期间根据指向的对象 类型到它的虚函数表中找到对应的虚函数(动态绑定),此时找到的是派生类的析构函 数,因此调用该析构函数;而调用派生类析构函数之后会再调用基类的析构函数,因此不 会导致内存泄漏。
17.为什么C++默认的析构函数不是虚函数
C++默认的析构函数不是虚函数是因为虚函数需要额外的虚函数表和虚表指针,占用额外 的内存。而对于不会被继承的类来说,其析构函数如果是虚函数,就会浪费内存。因此 C++默认的析构函数不是虚函数,而是只有当需要当作父类时,设置为虚函数。
18.为什么构造函数不能是虚函数
如果构造函数是虚函数,那么一定有一个已经存在的类对象obj,obj中的虚指针来指向虚 表的构造函数地址(通过obj的虚指针来调用); 可是构造函数又是用来创建并初始化对 象的,虚指针也是存储在对象的内存空间的; 所以构造函数是虚函数的前提是它要有类的 对象存在,但在这之前又没有其他可以创建并初始化类对象的函数,所以矛盾。总的来说 就是调用虚函数需要有类的对象,但是构造函数就是用来生成对象的,所以矛盾。
19.C++中析构函数的作用
析构函数和构造函数对应,当对象结束其生命周期,如对象所在的函数已调用完毕时,系 统会自动执行析构函数。析构函数释放对象使用的资源,并销毁对象的非static数据成员。 析构函数是一个类的成员函数,名字由波浪号接类名构成。它没有返回值,也不接受参 数。由于析构函数不接受参数,因此它不能被重载。对于一个给定类,只会有唯一一个析 构函数。 当一个类未定义自己的析构函数时,编译器会为它定义一个合成析构函数(即使自定义了 析构函数,编译器也总是会为我们合成一个析构函数,并且如果自定义了析构函数,编译 器在执行时会先调用自定义的析构函数再调用合成的析构函数)。对于某些类,合成析构 函数被用来阻止该类型的对象被销毁。否则,合成析构函数的函数体就为空。因此,许多 简单的类中没有用显式的析构函数。合成析构函数无法自动释放动态内存。 如果一个类中有指针,且在使用的过程中动态的申请了内存,那么最好显示构造析构函数 在销毁类之前,释放掉申请的内存空间,避免内存泄漏。 类析构顺序:1)派生类本身的析构函数;2)对象成员析构函数;3)基类析构函数。
20.静态函数和虚函数的区别
静态函数在编译的时候就已经确定运行时机,虚函数在运行的时候动态绑定。虚函数因为 用了虚函数表机制,调用的时候会增加一次内存开销。
21.重载、覆盖、隐藏
重载 两个函数名相同,但是参数列表不同(个数,类型,顺序),返回值类型没有要求,在同 一作用域中,常用来处理实现功能类似但数据类型不同的问题。单纯的返回值不同不是重 载。 覆盖(重写) 子类继承了父类,父类中的函数是虚函数,在子类中重新定义了这个虚函数,这种情况是 覆盖。其中函数名,参数列表,返回值,调用约定等均相同。对比覆盖和隐藏,不难发现 函数覆盖其实是函数隐藏的特例。如果派生类中定义了一个与基类虚函数同名但是参数列 表不同的非virtual函数,则此函数是一个普通成员函数,并形成对基类中同名虚函数的隐 藏,而非虚函数覆盖。 隐藏 指不同作用域中定义的同名函数构成隐藏(不要求函数返回值和函数参数类型相同)。 比如派生类成员函数隐藏与其同名的基类成员函数、类成员函数隐藏全局外部函数。隐藏 的实质是;在函数查找时,名字查找先于类型检查。如果派生类中成员和基类中的成员同 名,就隐藏掉。编译器首先在相应作用域中查找函数,如果找到名字一样的则停止查找。
22.多态
我们把具有继承关系的多个类型称为多态类型。当使用基类的引用或指针调用基类中定义 的函数时,它会有多种执行状态,即多态。多态实际上是一种思想,虚函数是该思想的实 现基础。 静态多态主要是重载,在编译的时候就已经确定;动态多态是用虚函数机制实现的,在运 行期间动态绑定。举个例子:一个父类类型的指针指向一个子类对象时候,使用父类的指 针去调用子类中重写了的父类中的虚函数的时候,会调用子类重写过后的函数,在父类中 声明为加了virtual关键字的函数,在子类中重写时候不需要加virtual也是虚函数。 多态的好处:提高了代码的维护性(继承保证),提高了代码的扩展性(多态保证)
23.虚函数
概念 基类希望其派生类进行覆盖的函数被称为虚函数。当使用指针或引用调用虚函数时,该调 用将被动态绑定。根据引用或指针绑定的对象类型不同,该调用可能执行基类的版本,也 可能执行派生类的版本。因此虚函数的解析过程发生在运行时。 实现 虚函数是多态实现的基石,而该虚函数的动态绑定主要是依据虚函数表(虚表)。 类的虚表:每个包含了虚函数的类都包含一个虚表。如果一个基类包含了虚函数,那 么其继承类也可调用这些虚函数,换句话说,一个类继承了包含虚函数的基类,那么 这个类也拥有自己的虚表。虚表是一个指针数组,其元素是虚函数的指针,每个元素 对应一个虚函数的函数指针。需要指出的是,普通的函数即非虚函数,其调用并不需 要经过虚表,所以虚表的元素并不包括普通函数的函数指针。虚表内的条目,即虚函 数指针的赋值发生在编译器的编译阶段,也就是说在代码的编译阶段,虚表就可以构 造出来了。虚表是属于类的,而不是属于某个具体的对象,一个类只需要一个虚表即 可。同一个类的所有对象都使用同一个虚表。 虚函数表位于只读数据段(.rodata),也就是C++内存模型中的常量区;而虚函数 则位于代码段(.text),也就是C++内存模型中的代码区。 虚表指针:指向虚表的指针。类的每个对象都有一个虚表指针,指向该类的虚表。这 样,当类的对象在创建时便拥有了这个指针,且这个指针的值会自动被设置为指向类 的虚表。虚表指针位于对象内存的最前面,后面才是成员变量。 动态绑定:检测到有虚函数的重写时,编译器会用子类重写的虚函数地址覆盖掉之前 父类的虚函数地址,当调用虚函数时,检测到函数是虚函数就会从虚表中找对应的位 置调用,若子类没有重写,虚表中的虚函数地址就还是父类的,若子类中有重写,虚 表记录的就是子类重写的虚函数地址,即实现了父类的指针调用子类的函数。使用了 虚函数,会增加访问内存开销,降低效率.
24.纯虚函数
定义 纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自 己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加 =0: virtual void funtion1()=0 引入原因 为了方便使用多态特性,我们常常需要在基类中定义虚函数。 在某些情况下,基类本身是不能生成对象的。例如,动物作为一个基类可以派生出老 虎、孔雀等子类,但动物本身生成对象明显不合常理。 将函数定义为纯虚函数,则编译器要求在派生类中必须予以重写以实现多态性,而在抽象 类中只有声明没有定义。声明了纯虚函数的类是一个抽象类,它不能生成对象,只能创建 它的派生类的实例。 纯虚函数的意义,让所有的类对象(主要是派生类对象)都可以执行纯虚函数的动作,但 类无法为纯虚函数提供一个合理的默认实现。所以类纯虚函数的声明就是在告诉子类的设 计者,“你必须提供一个纯虚函数的实现,但我不知道你会怎样实现它”。
25.抽象类
抽象类是一种特殊的类,它是为了抽象和设计的目的建立的,它处于继承层次结构的较上 层。带有纯虚函数的类称为抽象类。 作用 抽象类的主要作用是将有关的操作作为结果接口组织在一个继承层次结构中,并为派生类 提供基类。抽象类是用来捕捉子类的通用特性的,它是被用来创建继承层级里子类的模 板。 注意 抽象类只能作为基类来使用,其纯虚函数的实现由派生类给出。如果派生类中没有重新定 义纯虚函数,而只是继承基类的纯虚函数,则这个派生类仍然还是一个抽象类。如果派生 类中给出了基类纯虚函数的实现,则该派生类就不再是抽象类了,它是一个可以建立对象 的具体的类。在有动态分配堆上内存的时候,析构函数必须是虚函数,但没有必要是纯虚 函数。抽象类无法被实例化,无法定义对象
26.拷贝构造函数取引用的原因
拷贝构造函数的发生情况: 为了方便使用多态特性,我们常常需要在基类中定义虚函数。 在某些情况下,基类本身是不能生成对象的。例如,动物作为一个基类可以派生出老 虎、孔雀等子类,但动物本身生成对象明显不合常理。 将一个对象作为实参传递给一个非引用类型的形参 从一个返回类型为非引用类型的函数返回一个对象 用花括号列表初始化一个数组中的元素或一个聚合类中的成员(A={}) 某些类型还会对它们所分配的对象使用拷贝初始化,如insert或push函数,容器会使 用拷贝初始化,但是使用emplace时进行直接初始化。 原因: 如果拷贝构造函数的形参不是引用类型,那么我们调用拷贝构造函数时就会发生第一种情 况,即拷贝它的实参,但拷贝它的实参,又会需要调用拷贝构造函数,这样就会无限循 环。
27.C++中类成员的访问权限
类的访问权限 C++通过 public、protected、private 三个关键字来控制成员变量和成员函数的访问权 限,它们分别表示公有的、受保护的、私有的,被称为成员访问限定符。在类的内部(定 义类的代码内部),无论成员被声明为 public、protected 还是 private,都是可以互相 访问的,没有访问权限的限制。在类的外部(定义类的代码之外),只能通过对象访问成 员,并且通过对象只能访问 public 属性的成员,不能访问 private、protected 属性的成 员。 public: 可以被该类中的函数、子类的函数、友元函数访问,也可以在类的外部使用类的对 象访问; protect:类的内部可以访问,但是类的外部不能访问。受保护的成员对于派生类的成员和 友元是可以访问的。对于派生类来说,派生类的成员或友元只能通过派生类对象访问基类 的受保护成员。派生类不能访问基类对象中的受保护成员。 private:可以被该类中的函数、友元函数访问,但不可以由子类的函数、该类的对象、访 问。(只能在该类的内部进行访问)
28.深拷贝和浅拷贝
浅拷贝会把指针变量的地址复制; 深拷贝会重新开辟内存空间。 在类中默认拷贝构造函数可以完成对象的数据成员简单的复制,这也称为浅拷贝。对象的数 据资源是由指针指向的堆时,默认的拷贝构造函数只是将指针复制,而不会开辟新的内存空 间,这样会有两个指针指向同一块内存,当释放指针的时候,会导致该内存释放两次,会 报错。因此当类中存在指针指向的数据时,需要自己写拷贝构造函数。
29.final关键字
1.修饰类,防止该类被继承 2.修饰方法,防止该方法被重写
30.友元函数
类的友元函数是定义在类外部,但有权访问类的所有私有(private)成员和保护 (protected)成员。尽管友元函数的原型有在类的定义中出现过,但是友元函数并不是成 员函数。友元可以是一个函数,该函数被称为友元函数;友元也可以是一个类,该类被称 为友元类,在这种情况下,整个类及其所有成员都是友元。
|