C/C++高频题
题目和答案均来自网络,本人只整理和修改了部分内容,如有侵权告知后必删。答案不一定准确,仅供参考。
目录
[TOC]
C/C++基础
引用和指针的区别?
- 引用底层是通过指针实现的。
- 初始化不同:
- 引用在定义的时候必须进行初始化,并且不能够改变。
- 指针在定义的时候不一定要初始化,并且指向的空间可变。(注:引用总是表达代表一个对象,因此不能为NULL,而指针可以为空)。因此使用指针前最好检查,防止野指针。
- 访问逻辑不同:
- 引用访问一个变量是直接访问
- 指针访问一个变量是间接访问
- 运算结果不同:
- 自增运算结果不同(指针是指向下一个空间,引用时引用的变量值加1)
- sizeof结果不同(指针是一个实体,需要分配内存空间。引用只是变量的别名,不需要分配内存空间。sizeof 引用得到的是所指向的变量(对象)的大小,而sizeof 指针得到的是指针本身的大小)
- 当函数参数时不同(传指针的实质是传值,传递的值是指针的地址;传引用的实质是传地址,传递的是变量的地址)
- 多级:有多级指针,但是没有多级引用。
从汇编层去解释一下引用
1 | 1. 9: int a = 1; |
- a的地址为ebp-4,b的地址为ebp-8,栈地址由高到底分配
- 第4行将a的地址放入eax寄存器,第5行将eax的值放入b的地址中
- 上面两条汇编的作用即:将a的地址存入变量b中。因此引用是通过指针来实现的。
你什么情况用指针当参数,什么时候用引用,为什么?
首先,二者大多数情况下可以替换
一些不同的情况:
以指针传递使用前需要检查指针是否为空,涉及指针的操作有时容易出错;以引用传递节省了为空检查,更方便bug更少。
同时,指针可以为空,可以用来表示一些参数具有可选性质。
一种编程风格:使用const引用作为输入参数,使用指针作为需要读写的参数。
const引用可以传递临时对象,因为const引用可以绑定右值。而指针不能对临时对象取地址:
1
2void f(const T& t);
f(T(a, b, c));
C++中的指针参数传递和引用参数传递
- 指针参数传递的本质是值传递, 传递的值是对象的地址, 在调用时形参会在函数栈中开辟空间用于存放传递过来的对象的地址,此时形参相当于是实参的副本, 对形参的任何操作都不会反映到实参上, 但是通过形参间接访问对象的修改是会反应到函数之外的.
- 引用参数传递的本质是传地址, 传递的是实参变量的地址, 首先形参会在函数栈中开辟空间用来存放实参变量的地址, 然后对该形参的任何操作都会被处理未间接寻址,即通过形参中的地址访问主调函数中的实参变量, 因为通过形参的任何操作都将被应用于主调函数中.
- 从逻辑上引用相当于对变量起了一个别名, 通过该别名可以对变量进行直接访问, 由编译器负责将直接访问转换为间接访问; 而指针访问变量都是间接访问.
形参与实参的区别?
- 形参变量只有在被调用时才分配内存单元,在调用结束时, 即刻释放所分配的内存单元。因此,形参只有在函数内部有效
- 实参可以是常量、变量、表达式、函数等, 无论实参是何种类型的量,在进行函数调用时,它们都必须具有确定的值, 以便把这些值传送给形参
- 实参和形参在数量上,类型上,顺序上应一致, 否则会发生“类型不匹配”的错误。
- 当进行值传递时,二者互不影响;当进行引用传递时,形参值发生改变会影响实参的值。
static?
static在不同上下文下意思不同:
- static使全局标识符(变量和函数)具有内部连接性(internal linkage),即只在当前文件可见。(全局变量和函数默认具有外部链接性)
- static修饰局部变量(local static variable):作用是使变量从自动生命周期(auto duration)变为静态生命周期(static duration)。第一次经过对象定义语句时初始化(唯一的一次初始化),直到程序结束销毁。
- static修饰类内变量:在类中的static成员变量属于整个类所拥有,对类的所有对象只有一份拷贝。必须要在类外进行初始化,static修饰的变量先于对象存在(即使对象不存在,static变量也已初始化)
- static修饰类成员函数:由于static修饰的类成员属于类,不属于对象,因此static类成员函数是没有this指针,this指针是指向本对象的指针。正因为没有this指针,所以static类成员函数不能访问非static的类成员,只能访问 static修饰的类成员;static成员函数不能被virtual修饰,虚函数的实现是为每一个对象分配一个vptr指针,而vptr是通过this指针调用的,所以不能为virtual;虚函数的调用关系,this->vptr->vtable->virtual function。const成员函数也不能是static,同理。
- 其他:static变量默认初始化为0其实全局变量也具备这一属性,因为全局变量也存储在静态数据区。在静态数据区,内存中所有的字节默认值都是0x00,某些时候这一特点可以减少程序员的工作量。
- 参考:https://www.learncpp.com/cpp-tutorial/static-local-variables/
全局变量和static变量的区别?
- 从储存形式看:都储存于静态数据区(变量生命周期)
- 从链接性看:全局变量具有外部链接性,全局静态变量内部链接性,局部静态变量无链接性
静态局部变量什么时候初始化
- 静态局部变量和全局变量一样,数据都存放在全局区域,所以在主程序之前,编译器已经为其分配好了内存。
- 但在C和C++中静态局部变量的初始化节点又有点不太一样:
- 在C中,初始化发生在代码执行之前,编译阶段分配好内存之后,就会进行初始化。所以我们看到在C语言中无法使用变量对静态局部变量进行初始化,在程序运行结束,变量所处的全局内存会被全部回收。
- 在C++中,初始化时在执行相关代码时才会进行初始化。主要是由于C++引入对象后,要进行初始化必须执行相应构造函数和析构函数,在构造函数或析构函数中经常会需要进行某些程序中需要进行的特定操作,并非简单地分配内存。所以在C++中是可以使用变量对静态局部变量进行初始化的。
const
变量:阻止一个变量被改变,在定义该const变量时,通常需要对它进行初始化,因为以后就没有机会再去改变它了。特例:指针。对指针来说,可以指定指针本身为const,也可以指定指针所指的数据为const,或二者同时指定为const。
可以通过类型转换符const_cast将const类型转换为非const类型。
函数形参:当进行值传递时,加不加const都不会对实参产生什么影响;当进行引用或指针传递时,加const可以保护原实参不被改变。
类内变量:同一般变量
类内函数:表明其是一个常函数,不能修改类的成员变量,类的常对象只能访问类的常成员函数,即const对象只能调用const成员函数。(因为不能把const的this指针赋值给非const的this指针)
const成员函数的理解和应用?
1 | const Stock & Stock::topval (const Stock & s)const |
第一个const:确保返回的Stock对象在以后的使用中不能被修改
第二个const:确保此方法不修改传递的参数 S
第三个const:保证此方法不修改调用它的对象,const对象只能调用const成员函数,不能调用非const函数
指针和const的用法
从右往左读即可。
指向const的指针。const int p(推荐)或int const p。(读作:指针指向const int)
const指针。int *const p。(读作:const指针指向int)
两者结合。const int const p或int const const p。
mutable
- 如果需要在const成员方法中修改一个成员变量,那么需要将这个成员变量修饰为mutable。即用mutable修饰的成员变量不受const成员方法的限制;
- 可以认为mutable的变量是类的辅助状态,但是只是起到类的一些方面表述的功能,修改他的内容我们可以认为对象的状态本身并没有改变的。
- 实际上由于const_cast的存在,这个概念很多时候用处不是很到了。比如使用A* pA=const_cast<A*>(this),然后通过pA去修改成员变量值即可。区别:mutable一开始就使该变量非const,而const_cast允许你去修改const变量,比较危险。
extern
extern在不同上下文下意思不同
extern修饰变量的声明
如果文件a.c需要引用b.c中变量int v(变量v必须有外部链接性,比如是一个全局变量或使用extern修饰的const/constexpr变量等),就可以在a.c中声明extern int v,然后就可以引用变量v。
extern修饰函数的声明
- extern修饰函数声明时是多余的,可以省略。唯一的用处是增加了一点可读性。
- extern修饰符可用于指示C或者C++函数的调用规范。比如在C++中调用C库函数,就需要在C++程序中用
extern "C" int plain_c_func(int param);
声明要引用的函数。这是给链接器用的,告诉链接器在链接的时候用C函数规范来链接。主要原因是C++和C程序编译完成后在目标代码中命名规则不同。
strcpy,strncpy,strcat,strcmp,memset,memcpy,memmove的实现
1 | /* |
简述strcpy、sprintf 与memcpy 的区别
- 复制操作:
strcpy
,memcpy
- 复制内容不一样:
strcpy
是用于复制字符串的, 不能用去其他类型, 而memcpy
是用于复制任意类型的数据类型 - 复制方式不一样:
strcpy
是通过检测字符中的\0
判断结束的, 存在溢出风险,(strncpy)
更加安全; 而memcpy
是需要指定复制的字节数的.
- 复制内容不一样:
- 字符串格式化:
sprintf
- 将格式化的数据写入字符串中
- 注意
sprintf
对写入字符串没有限制大小, 也就存在溢出风险, 建议采用snprintf
int转字符串,字符串转int?
c++11标准增加了全局函数std::to_string
可以使用std::stoi/stol/stoll等等函数
深拷贝与浅拷贝?
- 浅复制:浅复制仅仅是指向被复制的内存地址,如果原地址中对象被改变了,那么浅复制出来的对象也会相应改变。
- 浅拷贝问题:比如类包含指针类型,浅拷贝将只复制指针类型,在析构时,同一个指针可能会释放两次,从而出现运行错误。
- 深复制:在计算机中开辟了一块新的内存地址用于存放复制的对象。
struct?
- C中struct:是用户自定义数据类型,只能是一些变量的集合体,不能包含函数
- C++中struct:是抽象数据类型。与类基本没什么区别,支持成员函数定义,能继承,能多态。默认访问控制符为public(为了与C兼容)。
union?
union
是一种数据格式,能够存储不同的数据类型,但只能同时存储其中的一种类型。union
的数据成员是共享内存的, 以成员最大的做为结构体的大小- 每个数据成员在内存中的起始地址是相同的
- 与struct类似但不同:
- 包含访问权限(默认public)、成员变量、成员函数(可以包含构造函数和析构函数)。
- 不能包含虚函数和静态数据变量。也不能被用作其他类的基类,它本身也不能从某个基类派生而来。
- 几种用途:
- 类似variant(c++17),即用一个数据类型表示多种类型。某些情况下(这些类型数据不会同时用到),节省了空间。
- 类似类型双关(type punning),即用多种类型去解析内存中的一个数据。union比直接type punning可读性更好。但这种做法不是很类型安全的,一般不建议使用。
函数指针?
- 函数指针是指向函数的指针,它指向的是函数的入口地址,其类型由该函数的返回值和参数列表共同确定,与函数名无关。作用:
- 可以通过函数指针进行函数调用
- 作为函数参数进行传递
- 提供抽象:调用者不需要知道具体是哪个函数被调用,只用知道满足特定参数列表和特定返回值
说说你对c和c++的看法,c和c++的区别?
- 面向过程 / 面向对象
- struct
- C中: struct是自定义数据类型; 是变量的集合, 不能添加拥有成员函数; 没有访问权限控制的概念; 结构体名称不能作为参数类型使用, 必须在其前加上struct才能作为参数类型
- C++中: struct是抽象数据类型, 是一个特殊的类, 可以有成员函数, 默认访问权限和继承权限都是public, 结构体名可以作为参数类型使用
- 动态管理内存:
malloc/free
和new/delete
- 重载:C++支持函数重载,而C不支持函数重载。而C++支持重载的依仗就在于C++的名字修饰与C不同,例如在C++中函数int fun(int ,int)经过名字修饰之后变为 _fun_int_int ,而C是 _fun,一般是这样的,所以C++才会支持不同的参数调用不同的函数
- 引用:C语言没有引用的概念, 更没有左值引用, 右值引用
c++的内存分配,详细说一下栈、堆、静态存储区?
- 栈区(stack):存放自动变量。由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
- 堆区(heap): 存放动态变量。一般由程序员分配释放,若程序员不释放,程序结束时可能由OS(操作系统)回收。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。
- 静态存储区(static):存放静态变量。全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后由系统释放。
- 文字常量区:全局常量、函数地址就是放在这里的。程序结束后由系统释放。
- 程序代码区:存放函数体的二进制代码。
堆与栈的区别?
- 管理方式:
栈由编译器自动管理,无需我们手工控制;堆需要手动释放不再使用的堆空间
memory leak
。 - 空间大小:
32
位系统下, 堆内存可以达到4G
(3G
用户空间,1G
内核空间).- 栈空间是受限的, 默认大小为
1M
- 碎片问题:
- 对于堆来说,频繁的
new/delete
势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。 - 对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列,永远都不可能有一个内存块从栈中间弹出
- 栈内存连续,对于CPU缓存也是友好的,效率较高。注意,栈比堆分配效率高的最主要原因是在于分配方式,CPU缓存会有部分影响。
- 对于堆来说,频繁的
- 生长方向:
- 对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向;
- 对于栈来讲,它的生长方向是向下的,是向着内存地址减小的方向增长。
- 分配方式:
- 堆都是动态分配的,没有静态分配的堆。
- 栈有2种分配方式:静态分配和动态分配。
- 静态分配是编译器完成的,比如局部变量的分配。
- 动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的,它的动态分配是由编译器进行释放,无需我们手工实现。
- 分配效率:
- 栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。
- 堆则是
C/C++
函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/
操作系统)在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。
- 应尽量避免堆内存的分配,比如对字符串的各种操作,否则会影响程序性能。
悬空指针和野指针?
- 野指针(wild
pointer)就是没有被初始化(甚至不是NULL)过的指针,因此可能指向随机的地址。
- 避免:使用指针前必须完成初始化(最好一开始就初始化了)
- 悬空指针(dangling
pointer)是指针最初指向的内存已经被释放了的一种指针。
- 原因:
- 原先指向的内存被主动释放(free/delete)
- 指向的变量离开作用域,自动被析构。
- 避免:使用智能指针
- 原因:
内存泄漏?
内存泄漏
内存泄漏指应用程序分配某段内存后,由于设计错误,失去了对该段内存的控制(无法释放掉这段内存)
后果
只发生一次小的内存泄漏可能不被注意,但泄漏大量内存的程序将会性能下降到内存逐渐用完,导致另一个程序失败;
如何排除
- 人工进行代码审查
- 写一个工具,监控内存分配和释放情况(通过重载operator new)。参考:Track memory allocation in C++
- 使用编译器或第三方工具来检查。如现代编译器gcc,clang都带有一些Sanitizer(memorySanitizer, leadSanitizer等),编译时必须手动加上一些flag。或者使用第三方动态分析工具(程序运行时检查代码)如valgrind。
解决方法:智能指针。
new和malloc的区别?
- new/delete是C++关键字,需要编译器支持。malloc/free是库函数,需要头文件cstdlib支持;
- 使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算。而malloc则需要显式地指出所需内存的尺寸。
- new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符。而malloc内存分配成功则是返回void * ,需要通过强制类型转换将void*指针转换成我们需要的类型。
- new内存分配失败时,会抛出bac_alloc异常。malloc分配内存失败时返回NULL。
- new operator会先调用operator new函数,申请足够的内存(通常底层使用malloc实现)。然后调用类型的构造函数,初始化成员变量,最后返回自定义类型指针。delete operator先调用析构函数,然后调用operator delete函数释放内存(通常底层使用free实现)。malloc/free是库函数,只能动态的申请和释放内存,无法强制要求其做自定义类型对象构造和析构工作。
- 因此new是类型安全的,更推荐使用。而且new可以被类重载。
delete p;与delete[]p,allocator?
delete和new配套使用,delete[]和new[]配套使用。delete[]时,数组中的元素按逆序的顺序进行销毁;
new在内存分配上面有一些局限性,new的机制是将内存分配和对象构造组合在一起,同样的,delete也是将对象析构和内存释放组合在一起的。allocator将这两部分分开进行,allocator申请一部分内存,不进行初始化对象,只有当需要的时候才进行初始化操作。
new和delete的实现原理, delete如何知道释放内存的大小的?
new:
new operator表达式调用一个名为operator new(operator new[])函数,分配一块足够大的、原始的、未命名的内存空间
编译器运行相应的构造函数以构造这些对象,并为其传入初始值
对象被分配了空间并构造完成,返回一个指向该对象的指针
如果使用数据形式,会在p的前四个字节写入数组大小n信息
delete:
- delete先进行对象析构
- delete operator再调用operator delete(operator delete[])函数
假设指针p指向new[]分配的内存。因为要4字节存储数组大小,实际分配的内存地址为[p-4],系统记录的也是这个地址。delete[]实际释放的就是p-4指向的内存。而delete会直接释放p指向的内存(并且至多会调用一次析构函数),这个内存根本没有被系统记录,所以会产生未定义行为。
- 参考:《ECPP》item16
malloc申请的存储空间能用delete释放吗?
不能。
delete除了释放内存,还会进行对象析构操作。也许对于一些基本类型来说,比如char*,使用delete释放内存不会报错。但是实际操作中,这样的行为是未定义的,不能确保对于所有类型,对于各类编译器,各类平台上都能成功。所以不能这么使用。
malloc与free的实现原理?
- malloc:操作系统中有一个记录空闲内存地址的链表。当操作系统收到程序的申请时,就会遍历该链表。
- 如果找到第一个空间大于所申请空间的堆结点,然后就将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。
- 如果找不到足够大小的堆结点,则使用brk()系统调用使系统扩展堆大小。
- free:把该结点重新加到空闲链表结点中。
malloc、realloc、calloc的区别?
1 |
|
智能指针,RAII
RAII全称是“Resource Acquisition is Initialization”,直译过来是“资源获取即初始化”,也就是说在构造函数中申请分配资源,在析构函数中释放资源。因为C++的语言机制保证了,当一个对象创建的时候,自动调用构造函数,当对象超出作用域的时候会自动调用析构函数。所以,在RAII的指导下,我们应该使用类来管理资源,将资源和对象的生命周期绑定。
智能指针即RAII最具代表的实现,使用智能指针,可以实现自动的内存管理,故不需要担心内存泄漏。常用的智能指针:
auto_ptr(c++98被引入,c++17被弃用)语义和unique_ptr一样,但是被弃用,有如下几个缺点:
- 在拷贝构造函数和赋值运算符中使用了移动语义,这样的接口设计不合理
- 不支持数组
- 与标准类库的容器和算法等不够兼容,容易发生不经意的移动
unique_ptr
- 语义为唯一拥有所指向对象(替代了auto_ptr)
- 其只支持移动语义, 不允许常规拷贝、赋值
- 当unique_ptr指针生命周期结束, 则会将所指向对象释放掉.
- 出于异常安全考虑,初始化时优先使用make_unique,而不是new裸指针。
shared_ptr
- 语义为共享拥有多指向的对象, 其支持拷贝、移动、赋值语义
shared_ptr
内部存在两个指针:一个是数据,一个是计数器。每减少一个shared_ptr
则计数器减一, 每多一个则计数器加一- 当计数器为零时则释放所指向的对象.
- 初始化时优先使用make_shared,而不是new裸指针。异常安全且效率更高(同时分配内存给两个指针)
weak_ptr:
- 和shared_ptr配套使用。不会修改shared_ptr的引用计数,不享有资源的所有权,只是用来访问指针内容。解决了循环引用问题。
- 循环引用:两个或多个对象互相使用一个
shared_ptr
成员变量指向对方,形成环状。(一个对象也可以自己指向自己,造成循环引用)。循环引用会造成计数额外增加,导致最后计数器不能归零,从而内存泄漏。
- 参考:Circular dependency issues with std::shared_ptr, and std::weak_ptr,《ECPP》item13,item17
手写实现智能指针类?
- shared_ptr:
1 | // 智能指针,基于引用计数,暂时不支持动态数组 |
- unique_ptr:
1 | // 智能指针,基于所有权,暂时不支持动态数组 |
内存对齐?位域?
内存对齐目的:
- 平台原因(移植原因):
- 不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常
- 性能原因:
- 数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。使得从内存中读取时更高效,但缺点是牺牲了空间。可以通过pack强制设置补齐的大小,当设置为1时,表示不使用补齐。但这样读取的速度会慢一点。
内存对齐规则:
分配内存的顺序是按照声明的顺序。
每个变量相对于起始位置的偏移量必须是该变量类型大小的整数倍,不是整数倍空出内存,直到偏移量是整数倍为止。
最后整个结构体的大小必须是里面变量类型最大值的整数倍。
**添加了#pragma pack(n)后规则:*
偏移量要是n和当前变量大小中较小值的整数倍
整体大小要是n和最大变量大小中较小值的整数倍
n值必须为1,2,4,8…,为其他值时就按照默认的分配规则
位域
- 位域为一种数据结构,可以把数据以位的形式紧凑的储存,并允许程序员对此结构的位进行操作。无名位域可以用来padding。
- 优点:节省了内存
- 缺点:其内存分配与内存对齐的实现方式依赖于具体的机器和系统,在不同的平台可能有不同的结果,这导致了位段在本质上是不可移植的。
结构体变量比较是否相等?
C语言中struct对比是element-wise的对比,有时会有问题(比如对比成员中的两个指针)
C++语言中扩展了struct的特性,其可以重载operator==函数,因此可以进行深对比(对比两个成员指针具体指向的值)
函数调用过程栈的变化?返回值和参数变量哪个先入栈?
- 每个运行中的函数都对应于一个栈帧
- 帧栈可以认为是程序栈的一段
- 它有两个端点
- 一个标识起始地址, 开始地址指针ebp;
- 一个标识着结束地址,结束地址指针esp;
- 函数调用使用的参数, 返回地址等都是通过栈来传递的.
- 函数调用过程:
- 参数逆序入栈
- 主调函数返回地址入栈
- 主调函数栈顶入栈
- 设置当前栈顶为被调函数栈底(被调用函数开始执行)
- 被调函数局部变量顺序入栈
- 出栈顺序与入栈顺序相反。
- 返回值可以先于参数入栈,但实际也不一定。返回后,控制权返回主调函数,由主调函数负责被调函数栈剩下的清理工作,如参数变量。
递归的原理是啥?递归中遇到栈溢出怎么解决?
- 递归的本质是函数调用,函数调用是通过栈实现的。因此,每一级的递归函数都有自己的堆栈(但是函数代码不会复制)。
- 递归函数中,递归调用前的语句的执行顺序和调用顺序相同,递归调用后的语句的执行顺序和调用顺序相反。
- 当调用函数遇到递归边界时,再依次返回出栈。由于栈资源有限,如果递归深度超过一定程度后,就会出现栈溢出。
- 因此递归应尽量避免过深(可以设置一个最大深度),也可使用迭代等方案替代。
怎样判断两个浮点数是否相等?
- 设置一个可以接受的精度阈值T。
- 浮点数与0的比较也应该注意这个问题,与浮点数的表示方式有关。
分别写出BOOL
,int
,float
,
指针类型的变量a
与“零”的比较语句
1 |
|
宏定义一个取两个数中较大值的功能?
1 |
define与const、typedef、inline使用方法?
define与const区别:
- 作用阶段: const在编译阶段其作用,有类型检查,define在预处理阶段起作用
- 功能: define可以配合条件预编译指令, 完成特殊的逻辑, 例如防止重复引入头文件
- 作用域: define没有作用域限制,而const定义的变量通常有作用域的限制
- 存储位置: const定义的是变量, 会储存在数据段空间, define是宏替换, 其值会储存在代码段
- const不能重定义,而define可以通过#undef取消某个符号的定义,进行重定义
define 和typedef的区别:
- 作用阶段:typedef在编译阶段有效,有类型检查,define在预处理阶段起作用
- 功能:typedef用来定义类型的别名,define不只是可以为类型取别名,还可以定义常量、变量、编译开关等
- 作用域:define没有作用域限制,typedef有
define 与inline的区别:
- 作用阶段:inline在编译阶段有效,有类型检查,define在预处理阶段起作用
- define可读性、类型安全、可预料行为上都不如inline
应该尽可能使用const,inline,enum,而不是define。预处理阶段只是简单的字符替换,虽然功能很多且适用性很广,但灵活性不如编译阶段起作用的几个关键字。
- 参考:《ECPP》item2
include的顺序以及尖括号和双引号的区别?
尖括号:编译器在存储标准头文件的主机系统查找
双引号:首先在当前目录查找,找不到再去标准位置查找
lambda函数?
- 利用lambda表达式可以编写内嵌的匿名函数,用以替换独立函数或者函数对象,一些情况下,可以简化函数的使用;
- 每当你定义一个lambda表达式后,编译器会自动生成一个匿名类(这个类当然重载了()运算符),我们称为闭包类型(closure type)。那么在运行时,这个lambda表达式就会返回一个匿名的闭包实例,其实一个右值。所以,我们上面的lambda表达式的结果就是一个个闭包。闭包的一个强大之处是其可以通过传值或者引用的方式捕捉其封装作用域内的变量,前面的方括号就是用来定义捕捉模式以及变量,我们又将其称为lambda捕捉块。参考代码如下:
1 |
|
lambda表达式的语法定义如下:
1
[capture](params)mutable->return-type{statement}
lambda必须使用尾置返回来指定返回类型,可以忽略参数列表和返回值,但必须永远包含捕获列表和函数体
printf实现原理?
- 函数的调用过程: 参数逆序入栈, 返回地址入栈, 调用函数栈顶入栈, 设置被调函数栈底, 然后是被调函数的局部变量
- 在调用printf时,由于参数逆序入栈,通过堆栈指针可以首先获取到第一个形参,也就是字符指针。然后解析所指向的字符串,得到后续参数的个数和数据类型
- 然后就可以计算出各参数的偏移量,通过偏移得到
- printf("%d,%d",a,b)输出对应的字符串
hello world 程序开始到打印到屏幕上的全过程?
- 应用程序
- 应用程序载入内存变成进程
- 进程获取系统的标准输出接口
- 系统为进程分配CPU
- 触发缺页中断
- 通过puts系统调用, 往标准输出接口上写字符串
- 操作系统将字符串发送到显示器驱动上
- 驱动判断该操作的合法性, 然后将该操作变成像素, 写入到显示器的储存映射区
- 硬件将该像素值改变转变成控制信号控制显示器显示
- 用户告诉操作系统执行
HelloWorld
程序(通过键盘输入等) - 操作系统:找到
helloworld
程序的相关信息,检查其类型是否是可执行文件;并通过程序首部信息,确定代码和数据在可执行文件中的位置并计算出对应的磁盘块地址。 - 操作系统:创建一个新进程,将
HelloWorld
可执行文件映射到该进程结构,表示由该进程执行helloworld
程序。 - 操作系统:为
helloworld
程序设置cpu
上下文环境,并跳到程序开始处。 - 执行
helloworld
程序的第一条指令,发生缺页异常 - 操作系统:分配一页物理内存,并将代码从磁盘读入内存,然后继续执行
helloworld
程序 helloword
程序执行puts
函数(系统调用),在显示器上写一字符串- 操作系统:找到要将字符串送往的显示设备,通常设备是由一个进程控制的,所以,操作系统将要写的字符串送给该进程
- 操作系统:控制设备的进程告诉设备的窗口系统,它要显示该字符串,窗口系统确定这是一个合法的操作,然后将字符串转换成像素,将像素写入设备的存储映像区
- 视频硬件将像素转换成显示器可接收和一组控制数据信号
- 显示器解释信号,激发液晶屏
OK
,我们在屏幕上看到了HelloWorld
左值右值?
- 在C++11中所有的值必属于左值、右值两者之一,右值又可以细分为纯右值、将亡值。在C++11中可以取地址的、有名字的就是左值,反之,不能取地址的、没有名字的就是右值(将亡值或纯右值)。例子略。
- C++11对C++98中的右值进行了扩充。在C++11中右值又分为纯右值(prvalue,Pure Rvalue)和将亡值(xvalue,eXpiring Value)。其中纯右值的概念等同于我们在C++98标准中右值的概念,指的是临时变量和不跟对象关联的字面量值;将亡值则是C++11新增的跟右值引用相关的表达式,这样表达式通常是将要被移动的对象(移为他用),比如返回右值引用T&&的函数返回值、std::move的返回值,或者转换为T&&的类型转换函数的返回值。将亡值可以理解为通过“盗取”其他变量内存空间的方式获取到的值。在确保其他变量不再被使用、或即将被销毁时,通过“盗取”的方式可以避免内存空间的释放和分配,能够延长变量值的生命期。
- 左值引用就是对一个左值进行引用的类型。右值引用就是对一个右值进行引用的类型,事实上,由于右值通常不具有名字,我们也只能通过引用的方式找到它的存在。右值引用和左值引用都是属于引用类型。无论是声明一个左值引用还是右值引用,都必须立即进行初始化。而其原因可以理解为是引用类型本身自己并不拥有所绑定对象的内存,只是该对象的一个别名。左值引用是具名变量值的别名,而右值引用则是不具名(匿名)变量的别名。左值引用通常也不能绑定到右值,但常量左值引用是个“万能”的引用类型。它可以接受非常量左值、常量左值、右值对其进行初始化。不过常量左值所引用的右值在它的“余生”中只能是只读的。相对地,非常量左值只能接受非常量左值对其进行初始化。
- 右值值引用通常不能绑定到任何的左值。绑定一个左值到右值引用,通常需要std::move()将左值强制转换为右值。
C语言的编译链接过程?
预处理 Preprocessing
读取c源程序,对其中的伪指令(以#开头的指令)和特殊符号进行处理。包括宏定义替换、条件编译指令、头文件包含指令、特殊符号。 预编译程序所完成的基本上是对源程序的“替代”工作。经过此种替代,生成一个没有宏定义、没有条件编译指令、没有特殊符号的输出文件。.i预处理后的c文件,.ii预处理后的C++文件。
编译阶段 Compilation
编译程序所要作得工作就是通过词法分析和语法分析,在确认所有的指令都符合语法规则之后,将其翻译成等价的中间代码表示或汇编代码。.s文件
汇编过程 Assembly
汇编过程实际上指把汇编语言代码翻译成目标机器指令的过程。对于被翻译系统处理的每一个C语言源程序,都将最终经过这一处理而得到相应的目标文件。目标文件中所存放的也就是与源程序等效的目标的机器语言代码。.o目标文件
链接阶段 Linking
链接程序的主要工作就是将有关的目标文件彼此相连接,也即将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得所有的这些目标文件成为一个能够诶操作系统装入执行的统一整体。
cout和printf有什么区别?
- cout有缓冲区,printf无缓冲区。有缓冲区意味着
- 操作系统可以待用户刷新缓冲区时输出, 或则缓冲区存满的时候输出
- 如果操作系统空闲的话也会检查缓冲区是否有值, 如果有的话立即输出
endl
相当于输出回车后,再强迫缓冲输出。flush
立即强迫缓冲输出。
- cout是一个全局对象, operator<<对常见数据类型进行了重载, 所以能自动识别数据的类型并进行输出;printf是一个函数,可以更好的进行格式化输出。
重载运算符?
只能重载已有的运算符;对于一个重载的运算符,其
优先级
和结合律
与内置类型一致才可以;不能改变运算符操作数个数;不能重载的5个
- . (成员访问运算符)
- .* (成员指针访问运算符)
- :: (域运算符)
- sizeof (长度运算符)
- ?: (条件运算符)
两种重载方式:成员运算符和非成员运算符。成员运算符比非成员运算符少一个参数;下标运算符、箭头运算符(重载的箭头运算符必须返回类的指针)、解引用运算符必须是成员运算符;
当重载的运算符是成员函数时,
this
绑定到左侧运算符对象。成员运算符函数的参数数量比运算符对象的数量少一个;下标运算符operator[]必须是成员函数,下标运算符通常以所访问元素的引用作为返回值,同时最好定义下标运算符的常量版本和非常量版本;
前置递增运算符和后置递增运算符不同:
1
2
3
4
5
6
7
8
9
10
11
12//重载前置++运算符
MyInteger &operator++() {
m_Number++;
return *this;
}
//重载后置++运算符
MyInteger operator++(int) {
MyInteger temp = *this;
m_Number++;
return temp;
}
函数重载函数匹配原则?
- 首先进行名字查找, 确定候选函数
- 然后按照以下顺序进行匹配:
- 精确匹配:参数匹配而不做转换,或者只是做微不足道的转换,如数组名到指针、函数名到指向函数的指针、
T
到const T
; - 提升匹配:即整数提升(如
bool
到int
、char
到int
、short
到int
、float
到double
),; - 使用标准转换匹配:如
int
到double
、double
到int
、double
到long double
、Derived*
到Base*
、T*
到void*
、int
到unsigned int
; - 使用用户自定义匹配;
- 使用省略号匹配:类似于
printf
中省略号参数。
- 精确匹配:参数匹配而不做转换,或者只是做微不足道的转换,如数组名到指针、函数名到指向函数的指针、
迭代器++it,it++哪个好,为什么?
推荐用前置:
- 前置返回引用,后置返回对象
- 前置不会产生临时对象,后置必须产生临时对象,临时对象会导致效率降低
i++,++i是否是原子操作?
不是。
i++在汇编中分为三步:
- 内存中取出i到寄存器
- 寄存器i加1
- 再从寄存器写回内存
因此,如果两个线程同时读了第一步,最终结果只会加1次
++i类似,还是分为多条汇编语句进行的。
定义和声明的区别?
- 如果是指变量的声明和定义,从编译原理上来说
- 变量声明是仅仅告诉编译器,有个某类型的变量会被使用,使这个名字被其他程序可见,但是编译器并不会为它分配任何内存。
- 变量定义就是分配了内存。
- 如果是指函数的声明和定义
- 函数声明:一般在头文件里,对编译器说:这里我有一个函数叫
function()
让编译器知道这个函数的存在。 - 函数定义:一般在源文件里,具体就是函数的实现过程写明函数体。
- 函数声明:一般在头文件里,对编译器说:这里我有一个函数叫
- 一般声明可以多个,定义只能有一个。
C++显式类型转换?
显式类型转换更加安全,可读性更好,容易找出程序中的错误:
- static_cast:
静态类型转换(不能移除const/volatile属性)是最常看到的类型转换, 几个功能.
- 内置类型之间的转换, 精度耗损需要有程序员把握
- 继承体系中的上下行转换(上行:子类转父类,安全转换; 下行:父类转子类, 不安全转换,一般只有当有先验信息时,比如确切知道基类指针指向的子类类型时,才能这样使用)
- 指针类型转换:
空指针转换成目标类型的空指针
,把任何类型转换成void 类型
。
- dynamic_cast: 主要用在继承体系中的安全向下转型
- 它能安全地将指向基类的
指针/引用
转型为指向子类的指针/引用
, 转型失败会返回null(转型对象为指针时)或抛出异常bad_cast(转型对象为引用时)。 dynamic_cast
会利用运行时的信息(RTTI)
来进行动态类型检查,因此dynamic_cast 存在一定的效率损失。- 而且
dynamic_cast
进行动态类型检查时, 利用了虚表中的信息, 所以只能用于有虚函数的类对象中.
- 它能安全地将指向基类的
- const_cast:
- 用来移除
const
或volatile
属性。但需要特别注意的是const_cast
不是用于去除变量的常量性,而是去除指向常数对象的指针或引用的常量性,其去除常量性的对象必须为指针或引用。 - 如果对一个指向常量的指针,通过
const_cast
移除const
属性, 然后进行修改, 编译通过,但是运行时会报段错误
- 用来移除
- reinterpret_cast:强制类型转换, 不安全
- 它可以把一个指针转换成一个整数,也可以把一个整数转换成一个指针(先把一个指针转换成一个整数,在把该整数转换成原类型的指针,还可以得到原先的指针值)。
- 用于type punning等情形
说一下理解 ifdef endif?
- 从源文件到可执行程序的过程, 通常要经历:
预编译
,编译
,汇编
,链接
等过程 ifdef
,endif
为条件预编译指令, 生效于预编译阶段, 根据条件可以完成一些特殊的逻辑, 例如防止文件重复引用#ifdef
,#else
,#endif
为完整的逻辑, 分别表示, 如果用#define
定义了某个标识符, 则编译后续程序段, 否则编译另外一个程序段(条件编译)- 因为预编译阶段处于编译链的第一阶段, 它可以直接影响应用程序的大小.
隐式转换,如何消除隐式转换?
- 隐式转换,是指不需要用户干预,编译器私下进行的类型转换行为。很多时候用户可能都不知道进行了哪些转换,例如:
- 类型提升:
(bool, int)
;(short, int)
;(float, double)
- 类型转换:
(int, float)
;(int, double)
,(Derived*, Base*)
- 类型提升:
- 基本数据类型的转换, 通常发生于从小到大的变换, 以保证精度不丢失
- 对于用户自定义类型, 如果存在单参数构造函数,
或者除一个参数外其他参数都有默认参数的,
此时编译器可能完成由此参数类型到自定义类型的隐式变换,
消除方式为使用关键字
explicit
禁止隐式转换.
C++如何处理多个异常的?
C++异常处理机制:
异常处理基本思想:执行一个函数的过程中发现异常,可以不用在本函数内立即进行处理, 而是抛出该异常,让函数的调用者直接或间接处理这个问题。
C++
异常处理机制由3 个模块组成:try(检查)
、throw(抛出)
、catch(捕获)
首先是: 抛出异常的语句格式为:
throw 表达式
;如果try块中程序段发现了异常则抛出异常, 则依次尝试通过catch进行捕获, 如果捕获成功则调用相应的函数处理段, 如果捕获失败, 则终止程序.
1
2
3
4
5
6
7
8try{
// 可能抛出异常的语句;(检查)
} catch(类型名[形参名]){ //捕获特定类型的异常
//处理1;
} catch(类型名[形参名]){//捕获特定类型的异常
//处理2;
} catch (…){ //捕获所有类型的异常
}
C++标准的异常
std::exception
: 所有标准C++
异常的父类。std::bad_alloc
: 该异常可以通过new抛出std::bad_cast
: 该异常可以通过dynamic_cast抛出std::logic_error
: 逻辑错误(无效的参数, 太长的std::string
, 数组越界)
我们可以通过这些类派生出自己的错误类型
动态链接与静态链接?
- 静态链接:
- 函数和数据被编译进一个二进制文件。在使用静态库的情况下,在编译链接可执行文件时,链接器从库中复制这些函数和数据并把它们和应用程序的其它模块组合起来创建最终的可执行文件。
- 空间浪费:因为每个可执行程序中对所有需要的目标文件都要有一份副本,所以如果多个程序对同一个目标文件都有依赖,会出现同一个目标文件在多个程序内都存在一个副本;
- 更新困难:每当库函数的代码修改了,这个时候就需要重新进行编译链接形成可执行程序。
- 运行速度快:但是静态链接的优点就是,在可执行程序中已经具备了所有执行程序所需要的任何东西,在执行的时候运行速度快。
- 函数和数据被编译进一个二进制文件。在使用静态库的情况下,在编译链接可执行文件时,链接器从库中复制这些函数和数据并把它们和应用程序的其它模块组合起来创建最终的可执行文件。
- 动态链接:
- 动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件。
- 共享库:就是即使需要每个程序都依赖同一个库,但是该库不会像静态链接那样在内存中存在多个副本,而是这多个程序在执行时共享同一份副本;
- 更新方便:更新时只需要替换原来的目标文件,而无需将所有的程序再重新链接一遍。当程序下一次运行时,新版本的目标文件会被自动加载到内存并且链接起来,程序就完成了升级的目标。
- 性能损耗:因为把链接推迟到了程序运行时,所以每次执行程序都需要进行链接,所以性能会有一定损失。
- 节省空间
- 更新方便:更新时只需要替换原来的目标文件,而无需将所有的程序再重新链接一遍。当程序下一次运行时,新版本的目标文件会被自动加载到内存并且链接起来,程序就完成了升级的目标。
在不使用额外空间的情况下,交换两个数?
算术
1
2
3x = x + y;
y = x - y;
x = x - y;异或
1
2
3x = x^y;
y = x^y;
x = x^y;
执行int main(int argc, char *argv[])时的内存结构?
- main函数是用户代码的入口函数, 其调用过程依旧是函数调用过程,
区别在于main函数的参数有固定的规范
- main函数参数规范如下:
- 第一个参数为:
int
型, 表示参数的个数 - 第二个参数为:
char* 数组
, 每一个char*
元素指向一个以字符串形式储存在内存中的参数的首地址, 其中第一个参数为程序的名字
- 第一个参数为:
- main函数参数规范如下:
- 函数调用过程如下:
- 首先将参数以字符串的形式保存在内存中, 然后利用字符串起始字符指针组成char* 数组, 并计算参数的个数.
- 然后将进行函数调用(略)
volatile关键字的作用?
- 主要用途:多任务环境下各任务间共享的标志应该加
volatile
,防止被编译器优化掉 - volatile关键字是一种类型修饰符,被它修饰的变量拥有三大特性:
易变性, 不可优化性, 顺序性
- 易变性:
编译器对
valatile
的访问总是从内存中读取数据, 即使它前面的指令刚刚从该处读取过数据。而且读取的数据立刻被保存。 - 不可优化性:
volatile
告诉编译器,不要对我这个变量进行各种激进的优化,甚至将变量直接消除,保证程序员写在代码中的指令,一定会被执行。 - 顺序性:
保证
Volatile
变量间的顺序性,编译器不会进行乱序优化。但是可能会被CPU优化
- 易变性:
编译器对
讲讲大端小端,如何检测?
大端big endian:内存中低地址保存数据的高位字节,即高位数据先被保存。
小端little endian:内存中低地址保存数据的低位字节,即低位数据先被保存。
1 | int num = 1; |
- 其他方法:union法,htol法等
为什么会有大端小端,htol 这一类函数的作用?
- 计算机以字节为基本单位进行管理,
每个地址单元都对应着一个字节,一个字节为
8bit
。但是我们常用到大于一个字节的数据类型, 例如short
,int
,float
等, 此时就会存在字节如何放置的问题, 从而出现了大端模式和小端模式. - 大端: 高字节放于低地址处(网络字节序为大端)
- 小端: 低字节放于低地址处(通常主机字节序为小端)
标准库是什么?
C++ 标准库可以分为两部分: 标准函数库:
这个库是由通用的、独立的、不属于任何类的函数组成的。函数库继承自C语言
。
面向对象类库: 这个库是类及其相关函数的集合。
- 标准函数库:
输入
/
输出I/O
、字符串和字符处理、数学、时间、日期和本地化、动态分配、其他、宽字符函数 - 面向对象类库: 标准的
C++
I/O
类、String
类、数值类、STL
容器类、STL
算法、STL
函数对象、STL
迭代器、STL
分配器、本地化库、异常处理类、杂项支持库
const char* 与string 之间的关系,传递参数问题?
string
是c++
标准库里面其中一个,封装了对字符串的操作,实际操作过程我们可以用const char*
给string
类初始化三者的转化关系如下所示:
string转const char*
1
2string s = “abc”;
const char* c_s = s.c_str();const char*转string,直接赋值即可
1
2const char* c_s = “abc”;
string s(c_s);string转char*
1
2
3
4
5string s = “abc”;
char* c;
const int len = s.length();
c = new char[len+1];
strcpy(c,s.c_str());char*转string
1
2char* c = “abc”;
string s(c);const char转char
1
2
3const char* cpc = “abc”;
char* pc = new char[strlen(cpc)+1];
strcpy(pc,cpc);char转const char,直接赋值即可
1
2char* pc = “abc”;
const char* cpc = pc;
new、delete、operator new、operator delete、placement new、placement delete?
new operator
new operator
完成了两件事情:用于申请内存和初始化对象。- 例如:
string* ps = new string("abc")
;
operator new
operator new
类似于C
语言中的malloc
,只是负责申请内存。- 例如:
1
void* buffer = operator new(sizeof(string)); // 注意这里new 前要有个operator。
placement new
- 用于在给定的内存中初始化对象。
- 例如:
1
2void* buffer = operator new(sizeof(string));
buffer = new(buffer) string("abc"); - 调用了
placement new
,在buffer
所指向的内存中创建了一个string
类型的对象并且初始值为“abc
”。
- 因此可以看出:
new operator
可以分解operator new
和placement new
两个动作,是operator new
和placement new
的结合。
- 与
new
对应的delete
没有placement delete
语法- 它只有两种,分别是
delete operator
和operator delete
。 delete operator
和new operator
对应,完成析构对象和释放内存的操作。- 而
operator delete
只是用于内存的释放,与C语言
中的free
相似。
- 它只有两种,分别是
怎么快速定位错误出现的地方?
- 重复测试,使得错误具有一定的可重复性
- 分析错误出现在哪个功能模块
- 如果使用了版本控制工具,可以使用git diff找出改动代码的地方,进一步缩小错误到具体的文件
- gdb单步调试,定位错误到具体函数,具体行等。
- 解决错误。
sizeof 和strlen 的区别
sizeof
是一个取字节运算符,计算变量所占的内存数(字节大小), 可以用于任意类型strlen
是个函数, 计算字符串的具体长度(只能是字符串),不包括字符串结束符(\0
)。strlen
是个不安全的函数, 如果没有\0
将会发生段错误。sizeof
和strlen
对同一个字符串求值, 结果差一.- 数组做
sizeof
的参数不退化,传递给strlen
就退化为指针
如何实现某一变量某位清0 或置1?
1 |
|
数组和指针的区别?
sizeof操作结果不同:
- 数组所占存储空间:
sizeof(
数组名);数组大小:sizeof(
数组名)/sizeof(
数组元素数据类型); - 对指针使用
sizeof
操作符得到的是一个指针变量的字节数,而不是p
所指的内存容量。即发生退化。
- 数组所占存储空间:
&取地址后结果不同:
- 数组:会返回一个指针,指向该数组第一个元素,比如int(*)[5]
- 指针:会返回该指针的地址,比如int**
用一个地址赋值时:数组会编译错误;指针会正常赋值
字符串初始化时不同:
1
2char array[] = "abc"; // 数组中有四个char,可读写
char* pointer="abc"; // 不可通过该地址修改对指针算术操作是有效的:
1
2pointer++; /*Legal*/
array++; /*illegal*/
assert 与NDEBUG?
assert宏的原型定义在
<assert.h>
中,其作用是如果它的条件返回错误,则终止程序执行,原型定义:1
- assert的作用是计算表达式
expression
,如果其值为假(即为0),那么它先向stderr
打印一条出错信息,然后通过调用abort
来终止程序运行。如果表达式为真,assert
什么也不做。
- assert的作用是计算表达式
NDEBUG宏是
Standard C
中定义的宏,专门用来控制assert()
的行为,二者配合使用。- 如果定义了这个宏,则
assert
不会起作用。 - 定义
NDEBUG
能避免检查各种条件所需的运行时开销,当然此时根本就不会执行运行时检查。
- 如果定义了这个宏,则
Debug 和release的区别?
- 调试版本,包含调试信息。
体积
比Release
大很多,并且不进行任何优化
(优化会使调试复杂化,因为源代码和生成的指令间关系会更复杂),便于程序员调试。 会产生调试文件记录代码中断点等调试信息; - 发布版本,不对源代码进行调试,编译时对应用程序的速度进行优化,使得程序在代码大小和运行速度上都是最优的。
- 实际上,
Debug
和Release
并没有本质的界限,他们只是一组编译选项的集合,编译器只是按照预定的选项行动。事实上,我们甚至可以修改这些选项,从而得到优化过的调试版本或是带跟踪语句的发布版本。
c++怎么实现一个函数先于main 函数运行?
- gcc编译器在main前后注册函数:
1 | // 在main之前 |
- 全局对象/全局静态变量的生存期和作用域都高于
mian
函数, 在main
函数之前初始化
1 | class Test { |
回调函数的作用?
当发生某种事件时,系统或其他函数将会自动调用你定义的一段函数;回调函数就相当于一个中断处理函数(或叫事件函数),由系统在符合你设定的条件时自动调用。回调函数是一个广泛的概念,具体实现不一定一样。
为此,你需要做三件事:1,声明;2,定义;3,设置触发条件(或叫注册),就是在你的函数中把你的回调函数名称转化为地址作为一个参数,以便于系统调用;
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用为调用它所指向的函数时,我们就说这是回调函数;
因为可以把调用者与被调用者分开。调用者不关心谁是被调用者,所有它需知道的,只是存在一个具有某种特定原型、某些限制条件(如返回值为int)的被调用函数。
更好的做法是定义一个接口类,这样回调函数也可以是普通的成员函数。在GUI编程中也经常会用到回调机制。
随机数的生成?
1 |
|
- 由于
rand()
的内部实现是用线性同余法做的,所以生成的并不是真正的随机数,而是在一定范围内可看为随机的伪随机数。 - 种子写为
srand(time(0))
代表着获取系统时间,每一秒系统时间的改变,数字序列的改变得到的数字
OOP
虚函数可以声明为inline吗?
- 虚函数用于实现运行时的多态,或者称为晚绑定或动态绑定。而内联函数用于提高效率。内联函数的原理是,在编译期间,对调用内联函数的地方的代码替换成函数代码。内联函数对于程序中需要频繁使用和调用的小函数非常有用。
- 虚函数要求在运行时进行类型确定,而内联函数要求在编译期完成相关的函数替换;因此不能。
- 参考:《ECPP》item30
类成员初始化方式?为什么用成员初始化列表会快一些?
赋值初始化,通过在函数体内进行赋值初始化;
相当于先初始化一次,然后进行赋值。
列表初始化,在冒号后使用初始化列表进行初始化。
分配内存空间时就进行初始化。因此可以避免临时对象的产生,故更快。
构造函数的执行顺序 ?析构函数执行顺序?
- 虚基类的构造函数(多个虚基类则按照继承的顺序执行构造函数)。
- 基类的构造函数(多个普通基类也按照继承的顺序执行构造函数)。
- 类类型的成员对象的构造函数(按照初始化顺序)
- 派生类自己的构造函数。
- 析构函数顺序相反
成员列表初始化?
- 必须使用列表初始化的四种情况
当初始化一个引用成员时;
当初始化一个常量成员时;
当调用一个基类的构造函数,而它拥有一组参数时;
当调用一个成员类的构造函数,而它拥有一组参数时;
- 成员初始化列表做了什么
- 编译器会以适当的顺序在构造函数之前进行初始化操作。
- 初始化顺序由类中的成员声明顺序决定的,不是由初始化列表的顺序决定的。
构造函数为什么不能为虚函数?
- 在对象中插入一个指向虚函数表的指针是由构造函数完成的, 也就是说在调用构造函数时并没有指向虚函数表的指针, 也就不能完成虚函数的调用
- 语义上讲,构造函数声明为虚函数也没有任何意义:构造函数只执行一次,虚函数的本身目的在于支持对象的动态行为,因此没有必要
析构函数为什么要是虚函数?
- 在使用多态语义时,确保正确的析构顺序
- 参考:《ECPP》item7
析构函数的作用?
- 析构函数与构造函数同名,但该函数前面加~。 析构函数没有参数,也没有返回值,而且不能重载,在一个类中只能有一个析构函数。
- 用于释放对象分配的内存空间。在对象生命周期结束时,编译器会自动调用析构函数,按一定顺序析构释放资源。
类什么时候会析构?
- 对象生命周期结束,被销毁时自动调用。
- 如果该对象是自动变量:程序出了作用域后自动调用
- 如果该对象是静态变量:程序退出时调用
- 如果该对象是动态变量:delete时自动调用(特殊情况是虚析构函数)
- 此外,当对象析构时,会调用基类和成员变量的析构函数。
构造函数和析构函数可以调用虚函数吗,为什么?
- 不可以。ctor和dtor中不允许使用virtual函数,其内的函数也不许调用virtual函数。
- 在派生对象的基类部分构造期间,对象的类型是基类而不是派生类,此时virtual函数失效。
- 析构期间同理。
- 参考:《ECPP》item9
构造函数析构函数可否抛出异常?
- C++只会析构已经完成的对象,对象只有在其构造函数执行完毕才算是完全构造妥当。在构造函数中发生异常,控制权转出构造函数之外(类似goto破坏了程序结构)。因此,在对象b的构造函数中发生异常,对象b的析构函数不会被调用。因此会造成内存泄漏。
- 解决:
- 使用智能指针管理堆内存,免除了抛出异常时发生资源泄漏的危机,不再需要在析构函数中手动释放资源
- 捕捉异常,并正确处理资源释放等工作,不要抛出异常
- 参考:《ECPP》item8
类如何实现只能静态分配和只能动态分配?
- 只能静态分配(禁止new):new operator总会调用operator new,后者可以自己声明为private即可(或=delete)。同理可以禁止operator delete以及它们的数组形式。
- 只能动态分配(只能new):构造函数设置为public,析构函数设置为private,并设置伪析构函数。
1 | // 只能动态分配的类 |
- 参考:《More ECPP》item27
如果想将某个类用作基类,为什么该类必须定义而非声明?
派生类中包含并且可以使用它从基类继承而来的成员,为了使用这些成员,派生类必须知道他们是什么。
类的声明又叫前向声明,其只是向程序引入这个类的名字,告诉程序这个类类型。在声明之后定义之前是一个不完全类型(incomplete type),无法知道其有哪些成员,因此无法用作父类。
什么情况会自动生成默认构造函数?
如果一个类没有构造函数,一共四种情况会合成构造函数:
- 存在虚函数的情况
- 因为虚表指针vptr是在构造函数中初始化的,所以没有构造函数的话该指针无法被初始化
- 存在虚基类的情况
- 基类成员存在默认构造函数的情况
- 因为只有这样基类的构造函数才能被调用
- 对象成员对象存在默认构造函数的情况
- 因为不合成一个默认构造函数那么该成员对象的构造函数不能调用
- 合成默认构造函数总是不会初始化类的内置类型及复合类型的数据成员。
- 分清楚默认构造函数被程序需要与被编译器需要,只有被编译器需要的默认构造函数,编译器才会合成它。
- 参考:《深度探索C++对象模型》第二章2.1
何时需要合成复制构造函数?
- 有三种情况会以一个对象的内容作为另一个对象的初值: 1)
对一个对象做显示的初始化操作,
X xx = x;
2) 当对象被当做参数交给某个函数时; 3) 当函数传回一个类对象时; - 如果一个类没有复制构造函数,合成复制构造函数的四种情况:
- 存在虚函数
- 存在虚基类
- 基类具有拷贝构造函数(明确声明或编译器合成的)
- 成员对象有拷贝构造函数(明确声明或编译器合成的)
- 参考:《深度探索C++对象模型》第二章2.2
什么是类的继承?
- 所谓的继承就是一个类继承了另一个类的属性和方法,这个新的类包含了上一个类的所有属性和方法,被称为子类或者派生类,被继承的类称为父类或者基类;
- 子类拥有父类的所有属性和方法,子类可以拥有父类没有的属性和方法,子类对象可以当做父类对象使用(Is-a关系)
什么是组合?
- 一个类里面的数据成员是另一个类的对象,即内嵌其他类的对象作为自己的成员(Has-a关系)
- 构造函数的执行顺序:先调用内嵌对象的构造函数,按照内嵌对象成员在组合类中的定义顺序。然后执行构造函数的函数体。析构函数调用顺序相反。
抽象基类?
- 定义:包含纯虚函数的类称为抽象类,抽象类不能创建对象。纯虚函数必须在子类中实现,如果派生类中没有重新定义纯虚函数,而只是继承,则这个派生类仍然还是一个抽象类。
- 语义上讲,抽象基类本身生成对象是不合情理的。例如,动物作为一个抽象基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。
为什么友元函数必须在类内部声明?
因为编译器需要知道哪些函数可以访问该类的私有部分,因此必须在该类内部声明才行。
但友元函数的定义可以在内部和外部。
介绍一下C++里面的多态?
- 静态多态(重载, 模板): 是在编译的时候,就确定调用函数的类型。
- 动态多态(覆盖, 虚函数实现): 在运行时,才确定调用的是哪个函数,动态绑定。运行基类指针指向派生类的对象,并调用派生类的函数。
用C语言实现C++的继承
类成员函数通过函数指针实现
类继承通过将父类作为成员变量实现(has-a)
- 参考:https://www.pvv.ntnu.no/~hakonhal/main.cgi/c/classes/
继承机制中对象之间如何转换?指针和引用之间如何转换?
- 父类指针(引用)->子类指针(引用):dynamic_cast或强制类型转换。如果转换失败,返回空指针(指针)或抛出异常(引用)
- 如果知道父类指针真实类型,用static_cast也可以(拥有先验信息)。但一般只能运行时通过对象的动态信息获取其真实类型。
- 子类指针(引用)->父类指针(引用):static_cast或隐式类型装换
- 子类对象->父类对象:static_cast或隐式类型转换。(出现截断)
- 父类对象->子类对象:无法进行!
指针->引用:对指针解引用*
引用->指针:对引用取地址&
组合与继承优缺点?
- 继承is-a关系
- 优点:子类可以重写父类的方法来方便地实现对父类的扩展,减少了重复代码
- 缺点:
- 如果对父类的方法做了修改的话(比如增加了一个参数),则子类的方法必须做出相应的修改。所以说子类与父类是一种高耦合
- 继承层次不能太多
- is-a的关系必须仔细确定
- 组合has-a关系
- 优点:低耦合。
- 缺点:易产生过多对象
C++中类成员的访问权限和继承权限问题?
- 三种访问限定符号
public
:可以被任意实体访问protected
:只允许子类及本类的成员函数还有友元函数访问private
:只允许本类的成员函数以及友元函数访问
- 三种继承方式:
public 继承
,protect 继承
,private 继承
- 组合结果
基类中 | 继承方式 | 子类中 |
---|---|---|
public | public继承 | public |
public | protected继承 | protected |
public | private继承 | private |
protected | public继承 | protected |
protected | protected继承 | protected |
protected | private继承 | private |
private | public继承 | 子类无权访问 |
private | protected继承 | 子类无权访问 |
private | private继承 | 子类无权访问 |
移动构造函数?
当我们用右值初始化一个左值时, 通常是使用复制构造函数构造左值,然后对右值调用析构函数, 此时存在大量的浪费.
移动构造函数的参数为右值引用, 它的作用就是将此右值的内容转移到左值内, 从而避免右值调用构造函数。也避免了左值分配内存进行构造. 特别的,拷贝构造函数中,对于指针,我们一定要采用深层复制,而移动构造函数中,对于指针可采用浅层复制:
1
2
3
4
5
6
7
8
9
10
11// move copy ctor
Example6 (Example6&& x):ptr(x.ptr){
x.ptr = nullptr;
}
// move assignment
Example6& operator= (Example6&& x){
delete ptr;
ptr = x.ptr;
x.ptr=nullptr;
return *this;
}
静态成员与普通成员的区别?
- 生命周期
- 静态成员变量从类被加载开始,一直存在直到程序结束;
- 普通成员变量只有在类创建对象后才开始存在,对象结束,它的生命期结束;
- 普通成员变量存储在栈或堆中,而静态成员变量存储在静态全局区;
- 共享方式
- 静态成员变量是全类共享(通过类名访问);普通成员变量是每个对象单独享用的(通过对象访问);
- 初始化位置
- 普通成员变量在类中初始化;静态成员变量在类外初始化;
- 参数
- 静态函数只可以使用静态成员变量,普通成员函数只可使用普通成员变量。
虚函数的内存结构,那菱形继承的虚函数内存结构呢?
如果派生类的基类存在虚函数,则
- 编译器会复制基类的虚表形成一个副本, 然后给该派生类对象插入一个指向该虚表副本的指针
- 如果该派生类对基类的虚函数进行了重定义, 则会替换虚表副本中的对应函数入口地址
- 如果该派生类新增了虚函数, 则对该虚表副本增加对应的项
菱形结构通常使用虚拟多重继承的方式, 防止同一类中存在基类的多个副本
- 如果类
B
虚拟继承自类A
, 则类B
中存在一个虚基类表指针,指向一个虚基类表(存在静态区, 不占用对象内存), 此虚基类表中存储中虚基类相对于当前类对象的偏移量. - 不同的编译器对虚基类表指针的处理方式不同,
例如
VS
编译器将虚基类表指针插入到对象中(会占用对象内存), 而SUN/GCC
公式的编译器则是插入到虚函数表中(不占用对象内存)
- 参考:《深度探索C++对象模型》第三章第四章
多继承的优缺点,作为一个开发者怎么看待多继承?
C++
允许为一个派生类指定多个基类,这样的继承结构被称做多重继承。- 优点: 对象可以调用多个基类中的接口,有时会比较方便;
- 缺点:
- 如果基类重存在多个相同的基类或则方法, 则会出现二义性(解决方案是调用时加上全局限定符)
- 菱形继承
- 增加了代码复杂度,一些情形下容易出现问题
- 个人觉得应尽可能避免使用多重继承,在使用接口类时,可以使用。
在成员函数中调用delete this会出现什么问题?对象还可以使用吗?如果在类的析构函数中调用delete this,会发生什么?
当调用delete this时,析构函数被调用,类对象的内存空间被释放。
- 当对象是局部变量(会自动调用析构):当变量出了作用域后自身调用析构时,会出现内存二次释放的问题,程序异常退出。
- 当对象是动态变量(不会自动调用析构):在delete this之后进行的其他任何函数调用,只要不涉及到this指针的内容,都能够正常运行。一旦涉及到this指针,如操作数据成员,调用虚函数等,就会未定义的行为。
- delete this释放了类对象的内存空间,但是内存空间却并不是马上被回收到系统中,可能是缓冲或者其他什么原因,导致这段内存空间暂时并没有被系统收回。此时这段内存是可以访问的,但是其中的值却是不确定的。
- 会导致堆栈溢出:delete this会去调用本对象的析构函数,而析构函数中又调用delete this,形成无限递归,造成堆栈溢出,程序崩溃。
动态联编与静态联编?
- 联编binding是指C++标识符(变量或函数)被转换为具体地址的过程。根据阶段的不同,分为静态联编early binding(static binding)和动态联编late binding(dynamic binding)。下面主要关注函数的联编。
- 静态联编。发生在编译期,编译器可以直接将标识符和机器地址进行关联。普通函数调用时,编译器直接可以找到调用函数的地址。当运行到该函数时,直接可以跳转到该函数的位置。
- 动态联编。发生在运行期,直到运行时,才能与具体地址关联。比如通过函数指针调用函数,虚函数机制等。
- 对比。静态联编速度快,效率高;动态联编需要运行时先找到要跳转的地址再跳转,因此效率低些,但更加灵活(运行期再决定调用哪个函数)。
为什么拷贝构造函数必须传引用不能传值?
- 如果以值传递的形式传递参数,则参数复制时会调用拷贝构造函数,因此产生无限递归调用,内存溢出。
空类的大小是多少?为什么?
C++
空类的大小不为0
,不同编译器设置不一样,一般为1
;C++
标准指出,不允许一个对象(当然包括类对象)的大小为0
,因为不同的对象不能具有相同的地址;
- 参考:《深度探索C++对象模型》第三章
类对象的大小?
- 对象本身非静态成员变量:包括继承的和自身的
- 语言本身额外负担:虚表指针,虚基类指针
- 编译器特殊情况优化处理:某些编译器会对emty virtual base class特殊处理
- 内存对齐
- 参考:《深度探索C++对象模型》第三章3.1节
静态函数能定义为虚函数吗?常函数?
不能 !
static
成员不属于任何类对象或类实例,没有this指针(静态与非静态成员函数的一个主要区别)。- 虚函数调用链为:
vptr
->vtable
->virtual function
- 但是访问
vptr
需要使用this
指针但是static
成员函数没有this
指针, 从而无法实现虚函数的调用
同理
- 常函数const修饰的是成员函数,可以想象成修饰const *this。静态函数没有this指针,故没法是常函数。
this指针调用成员变量时,堆栈会发生什么变化?
- 当我们在类中定义非静态成员函数时, 编译器会为此成员函数添加一个参数(最后一个形参), 类型为当前类型的指针
- 当我们进行通过对象或对象指针调用此成员函数时,
编译器会自动将对象的地址传给作为隐含参数传递给函数,这个隐含参数就是
this
指针。即使你并没有写this
指针,编译器在链接时也会加上this
的,对各成员的访问都是通过this
的。 - 函数调用时,
this
指针首先入栈,然后成员函数的参数从右向左进行入栈,最后函数返回地址入栈。
虚函数的代价?
- 空间上:每个类多了一个虚函数表,每个对象多了一个虚表指针
- 时间上:动态绑定,虚函数调用效率低于普通函数调用。而且不能再是inline函数,因为inline函数在编译阶段进行替代。
哪些函数不能是虚函数?
- 构造函数: 首先是没必要使用虚函数, 其次不能使用虚函数
- 内联函数: 表示在编译阶段进行函数体的替换操作,而虚函数意味着在运行期间进行类型确定,所以内联函数不能是虚函数;
- 静态函数: 静态函数不属于对象属于类,静态成员函数没有this 指针,因此静态函数设置为虚函数没有任何意义。
- 友元函数: 友元函数不属于类的成员函数,不能被继承,没this指针。对于没有继承特性的函数没有虚函数的说法。
- 普通函数: 普通函数不属于类的成员函数,不具有继承特性,没this指针,因此普通函数没有虚函数。
虚函数与纯虚函数的区别?
- 虚函数目的是为了给子类提供缺省实现,子类也可以重写。纯虚函数目的是为了给子类提供接口,要求子类强制实现(否则不能产生对象)。
成员函数里memset(this,0,sizeof(*this))
会发生什么?
- 如果类中的所有成员都是内置的数据类型的,则不会存在问题 ,这句话会将成员都初始化为0。
- 如果有以下情况之一会出现问题:
- 存在对象成员。破坏了对象内存。
- 存在虚函数/虚基类。虚表指针/虚基类指针将失效,相应功能也会异常。
- 如果在构造函数中分配了堆内存, 而此操作可能会产生内存泄漏。
STL
vector与list的区别与应用?怎么找某vector或者list的倒数第二个元素
从增删查改的角度说明效率。略
vector的实现?
size()函数返回的是已用空间大小,capacity()返回的是总空间大小,capacity()-size()则是剩余的可用空间大小。当size()和capacity()相等,说明vector目前的空间已被用完,如果再添加新元素,则会引起vector空间的动态增长。
由于动态增长会引起重新分配内存空间、拷贝原空间、释放原空间,这些过程会降低程序效率。因此,可以使用reserve(n)预先分配一块较大的指定大小的内存空间,这样当指定大小的内存空间未使用完时,是不会重新分配内存空间的,这样便提升了效率。只有当n>capacity()时,调用reserve(n)才会改变vector容量。
vector迭代器失效?
- 在
capacity
内insert
和erase
都会导致在后续元素发生移动, 进而该位置及该位置之后的元素迭代器失效或则改变 - 如果
insert
或则push_back
导致空间不足, 则会发生整体的移动操作, 所有迭代器都将失效.
vector扩容为什么是1.5倍而不是2倍?
使用 k=2增长因子的缺点在于,每次扩展的新尺寸必然刚好大于之前分配的总和。也就是说,之前分配的内存空间不可能被重复使用。这样对于缓存并不友好。最好设置为1-2之间的数值(不包括1和2)。
增长因子太大太小都不好。
- 参考:https://www.zhihu.com/question/36538542
vector释放空间?(两种方法)
和一个空的vector进行swap
clear(); shrink_to_fit();
注意:clear和resize都只是改变具体值和size,空间和capacity都不改变。
容器内部删除一个元素?
- 顺序容器
erase 迭代器
不仅使所指向被删除的迭代器失效,而且使被删元素之后的所有迭代器失效(list除外
),所以不能使用erase(it++)
的方式,但是erase
的返回值是下一个有效迭代器;it = c.erase(it);
- 关联容器
erase 迭代器
只使被删除元素的迭代器失效, 其他迭代器不失效,但是返回值是void
,所以要采用erase(it++)
的方式删除迭代器;c.erase(it++)
STL迭代器如何实现?
- 迭代器的作用就是,屏蔽了底层实现细节,提供一个遍历容器内部所有元素的接口。迭代器模式。
- 迭代器不是指针,是类模板,表现的像指针。他只是模拟了指针的一些功能,重载了指针的一些操作符来支持遍历,
->
、*
、++
、--
等。
set与hash_set(unordered_set)的区别? map与hashmap(unordered_map)的区别?
- set:
- RB tree
- 自动排序,适合范围查找。访问复杂度O(lg n),较慢
- 任何类型都可以,只要提供比较函数operator<
- unordered_set:
- Hash-table
- 无排序。访问复杂度O(1),更快
- 提供hash_value()函数。大多数基本类型可以,但一些类型仍无法处理。
- map与hashmap类似。
map、set是怎么实现的,红黑树是怎么能够同时实现这两种容器? 为什么使用红黑树?
他们的底层都是以红黑树的结构实现,因此插入删除等操作都在O(logn)时间内完成,因此可以完成高效的插入删除;
在这里我们定义了一个模版参数,如果它是key那么它就是set,如果它是map,那么它就是map;底层是红黑树,实现map的红黑树的节点数据类型是key+value,而实现set的节点数据类型是key。
因为map和set要求是自动排序的,红黑树能够实现这一功能,而且时间复杂度比较低。
如何在共享内存上使用stl标准库?
- 无法直接放在共享内存中。因此,可把STL容器内容逐个复制到共享内存中,使用时再依次取出。
- 最好的办法是使用boost自带的库。
- 参考:Boost.Interprocess (使用自带的stl类型,因此使用时需要转换),IPC 共享内存
map插入方式有几种?
5种
1 | // single element |
hash_map如何解决冲突以及扩容?
解决冲突
- 开放定址法:一旦发生冲突,加上偏移量后,再寻找下一个地址(线性探测、二次探测、随机探测)
- 再散列函数法:一旦冲突,换一个散列函数算
- 链地址法:chaining,直接在冲突后面加一个链表
- 公共溢出区法:将发生冲突的移到一个单独的表,查找时顺序查找。
扩容:
什么时候扩容: 哈希表键值发生碰撞的概率, 随着负载因子(负载/容量)的增加而增加, 所以当负载因子大于阈值(0.75)的时候就需要扩容了.
- 怎么扩容
(resize)
: 通过增加桶的数量(两倍扩张)以达到扩容的目的, 然后将原来的所有键值rehash
到新的哈希表中, 增大哈希表并不会影响哈希表的插入删除时间, 那是rehash
需要的时间复杂度为n
, 所以对实时性非常严格的情况下不要使用
- 怎么扩容
- 参考:《STL源码剖析》第五章
vector越界访问下标,map越界访问下标?vector删除元素时会不会释放空间?
- vector:
- operator[]不检查下标
- at()检查下标,如果越界,抛出out_of_range异常
- map:
- operator[]不检查下标,如果key不存在,会自动加进去
- at()检查下标,如果越界,抛出out_of_range异常
- erase、resize、clear只删除元素,不会释放空间。
map[]与find的区别?
- 如果存在该key:
- map返回key对应的值
- find返回对应的迭代器
- 如果不存在key:
- map加入该键值对
- find返回end()尾迭代器
STL中list与deque之间的区别?
- list:
- 双向链表实现。
- 任何位置插入删除很快,但查找复杂度O(n)
- 需要存储额外的链接信息(指针等),开销增大。
- deque:
- 动态数组实现。动态地以分段连续空间组合而成,并不保证连续。
- 开头结尾插入删除很快,查找复杂度O(1)
STL中的allocator,deallocator
- 参考:《STL源码剖析》第二章,参考链接
模板
C++模板是什么,底层怎么实现的?
编译器并不是把函数模板处理成能够处理任意类的函数;编译器从函数模板通过具体类型产生不同的函数;编译器会对函数模板进行两次编译:在声明的地方对模板代码本身进行编译,在调用的地方对参数替换后的代码进行编译。
这是因为函数模板要被实例化后才能成为真正的函数,在使用函数模板的源文件中包含函数模板的头文件,如果该头文件中只有声明,没有定义,那编译器无法实例化该模板,最终导致链接错误
模板类和模板函数的区别是什么?
- 函数模板的实例化是由编译程序在处理函数调用时自动完成的
- 类模板的实例化必须由程序员在程序中显式地指定。即函数模板允许隐式调用和显式调用而类模板只能显示调用。在使用时类模板必须加
,而函数模板不必
为什么模板类一般都是放在一个h文件中?
- 编译器并不是把函数模板处理成能够处理任意类型的函数;编译器从函数模板通过具体类型==产生==不同的函数;
- 编译器会对函数模板进行两次编译:
- 在声明的地方对模板代码本身进行编译,
- 在调用的地方对参数替换后的代码进行编译。
- 如果模板函数不是定义在.h文件中
- 编译器编译.h文件时并不知道另一个.cpp文件的存在,
也不会去查找(查找通常是链接阶段的事)
- 在定义模板函数的.cpp文件中, 编译器对函数模板进行了第一次编译, 但是它并没有发现任何调用, 故而没有生产任何的函数实例
- 在调用了模板函数的.cpp文件中, 编译器发现调用其他函数, 但是在此.cpp文件中并没有定义, 所以将此次调用处理为外部连接符号, 期望链接阶段由连接器给出被调函数的函数地址.
- 在链接阶段, 连接器找不到被调函数故而报不能识别的外部链接错误.
- 编译器编译.h文件时并不知道另一个.cpp文件的存在,
也不会去查找(查找通常是链接阶段的事)
写一个比较大小的模板函数
1 | template<typename T> |