Bjarne Stroustrup 的 C++ 风格与技术 FAQ(中文版)
1、尽可能说出static关键字的作用?
(1) static修饰函数局部变量(包括main函数里的),该变量不会随着函数作用域的退出而销毁,而是只分配一次内存,下次调用时为上次调用值。
(2) static修饰全局变量,限定了该变量只能被本文件访问,不能被其他文件访问。
(3) static修饰的函数只能被本文件访问,不能被其他文件访问。
(4) static menber属于类,所有该类的实例对象共享一份拷贝(此处是名词)
(5) static menber function不接受this指针,只能访问静态成员
由此衍生出问题:静态变量和静态成员函数的用法?
2、尽可能说出const的用法?
const不能简单的只认为是 “常数”,c语言中要使用常数,比如PI,会用define,<<effective c++>>推荐使用const,因为使用宏如果编译报错,是不会显示PI的,只提示3.1415数字。const的真正用意是为了提醒程序员,该对象是不可修改的。
(1) 用const修饰一个变量,需要初始化,忘记初始化会编译报错
(2) const与指针作用时,<<c++ primer 5th>> 分为pointer to const(指向常量的指针,指针自以为指向的是常量,不能用指针来修改对象,而不要求该对象是 const的),const pointer(指针不能指向其他对象),或者const int * const p =&a;
(3) const 修饰形参,表明不能通过形参修改实参,可以接收const对象作为实参或non-const对象
(4) const引用,不能用引用去修改所引对象,但对象本身可以是可修改的, 引用本身不能再指向其他对象
(5) const修饰成员函数,则该函数不能修改成员变量,但是如果变量是指针,用指针可以修改所指的资源(见effctive c++ item 03);const men fun是可以 重载的,如const T &operator[]() const 、T&operator[]();前者可以被const T类型的对象调用,后者不能,因为const对象会把this指针转换为const 指针
(6) const修饰函数范围类型,这样就不会有a*b=c(假设重载了*)这样诡异的合法代码。
1 const classA operator*(const classA& a1,const classA& a2); 2 operator*的返回结果必须是一个const对象。如果不是,这样的变态代码也不会编译出错: 3 classA a, b, c; 4 (a * b) = c; // 对a*b的结果赋值
(7) const 与define的优点
ps:const mem fun隐含有一个const *this参数,static mem fun隐含没有this指针,所以两者一起修饰成员函数是不能通过编译的
3、引用与指针的区别?为什么不用string *const p 代替string &p
因为加入引用是为了支持operator overloading。这里有一个假设,如果没有引用,那么,用指针来operator overloading操作。A operator +(const A *a, const A *_a); 那么使用的时候,&a + &b,这样看起来是不是很难受。使用指针可读性差,*p。
而引入引用的概念,既可以满足overload operator,也不失重载value和pointer的灵活性。而且引用还带来一个指针无法替代的特性: 引用临时对象。因为引用必须在定义的时候就赋值,以后无法更改。string &s=string();
(1) 引用必须被初始化,没有void 引用,编译时引用必须与某个对象bind,引用不是对象,只是别名,引用本身不分配内存。
(2) 不能有string & const s,可以有const string&s ,后者为常量引用。
(3) 解引用操作是对指针,获取指针所指对象,不是对引用操作
(4) sizeof(引用)得到bind对象的大小,sizeof(pointer)得到指针本身的大小
(5) 作为函数形参,如果实参是指针,用实参初始化形参,是传址(本质上也是传值)——>可能需要指针类型转换,或派生类指针传给基类指针;如果是传引用,传递的是对象本身,不需要要额外的复制 开销。如果需要修改指针,可以用二级指针
4、将“引用”作为函数参数有哪些特点?
(1) 传递引用给函数与传递指针的效果是一样的。这时,被调函数的形参就成为原来主调函数中的实参变量或对象的一个别名来使用,所以在被调函数中对形参变量的操作就是对相应的目标对象(位于主调函数中)的操作。
(2) 使用引用传递函数的参数,在内存中并没有产生实参的副本,它是直接对实参操作;而使用一般变量传递函数的参数,当发生函数调用时,需要给形参分配存储单元,形参变量是 实参变量的副本;如果传递的是对象,还将调用拷贝构造函数。因此,当参数传递的数据较大时,用引用比用一般变量传递参数的效率和所占空间都好。
(3) 使用指针作为函数的参数虽然也能达到与使用引用的效果,但是,在被调函数中同样要给形参分配存储单元,且需要重复使用"*指针变量名"的形式进行运算,这很容易产生错误且 程序的阅读性较差;另一方面,在主调函数的调用点处,必须用变量的地址作为实参。而引用更容易使用,更清晰。
5、什么是多态?多态有什么用途?
要理解多态,首先要理解静态类型与动态类型,变量或表达式的static type是编译期已知的,它是变量声明的类型或表达式生成的类型;变量或表达式表示的内存中的对象是dynamic type,直到run time才知道。
1 class Base{ 2 public: 3 virtual void foo(); 4 5 }; 6 class Derived :public Base { 7 public: 8 virtual void foo(); 9 10 }; 11 int main(){ 12 Base *b = new Derived(); 13 b->foo(); 14 Derived der; 15 Derived *d = &der; 16 d->foo(); 17 }
b的动态类型是派生类对象,基类的指针或引用可以bind到派生类对象,使用该指针或引用时并不清楚所绑对象的真实类型。总结为三句话:
(1) 基类指针(引用)可以指向子类对象;对基类指针(引用)的函数调用根据所绑定的真实对象传递参数—— void bar(const Base&) ,即一个接口,多种类型的对象;用基类指针 调用虚函数,在运行时决定调用的版本。
多态的用途就是编写一个接口,可以使用于各种类型。面向对象编程的伟大之处。
多态:是对于不同对象接收相同消息时产生不同的动作。C++的多态性具体体现在运行和编译两个方面:在程序运行时的多态性通过继承和虚函数来体现;在程序编译时多态性体现在函数和运算符的重载上;
6、虚函数?什么函数不能是虚函数?虚函数必须要实现吗?虚函数可以重载吗? 分辨三个词的区别:重载,覆盖,隐藏
virtual Base(); 构造函数不能为虚函数,而析构函数可以且常常是虚函数 ,基类虚函数可以不实现,但遇到需要基类对象实例呢?所以不实现应该用纯虚函数。
virtual static int b(); nonstatic 成员函数才能为虚函数。
(1) 基类必须将两种成员函数区分开:一种是希望派生类只继承接口,而不继承实现,实现与派生类类型有关,不是通用的,二是直接继承而不修改。前者就是虚函数,基类与之类各有实现版本,所谓override
(2) 虚析构函数的用处是用基类指针指向了子类对象,delete 该基类指针,需要运行子类的析构函数,否则将产生未定义行为。
(3) 重载(overload)发生在同一个类内,声明virtual的函数是可以重载的,而虚函数的覆盖发生在类层次关系中。
7、派生类的作用域与基类的作用域关系?
派生类构成的类作用域是嵌套在基类中的,即基类的所有成员都声明在派生类的外部,如果派生类声明了基类的同名成员函数,会隐藏基类的函数。见effective c++ item:避免遮掩继承而来的名字
基类的mf1被隐藏了。
8、虚函数的实现机制?虚函数导致的开销?
虚函数的实现是C++标准没有规定,目前各编译器主流的实现是vptr 与vtable(可以用函数指针的数组实现)。
(1) 基类的每个对象拥有一个虚指针(对象不含有vtable),指向一个虚表,表内是基类的虚函数函数指针(见effective c++item07),为了效率,vptr位于对象实例地址的最前面(虚函数表的图解)
(2) 派生类对象的虚指针指向的vtable里含有基类的虚函数和自己的虚函数,依声明顺序存放。
(3) vptr在32位系统上占用了四个字节,这导致对象体积变大,每次调用都需要多一步查找虚表的操作。 http://blog.csdn.net/hengyunabc/article/details/7461919 。无端的声明为 virtual 是错误的,只有作为基类声明为虚函数,表明派生类需要overide。
9、new 与malloc的区别?
new的过程分三步:一、调用 new文件里的函数operator new()或operator new[]() 分配内存;二、编译器调用对象的构造函数,并传入初始值,三、返回一个指针,这三步却是隐藏在一条new expression后面,即 完成了内存的分配与对象的初始化。malloc 是个库函数,分配的是未初始化的内存,不会调用对象的构造函数,下面的2和并不会调用构造函数,会产生运行时错误。用malloc分配的p,可以用定位new来创建对象:new(p) string("aa"); 使用palcement new可以控制对象的创建位置1 string *p = static_cast<string*> (malloc(sizeof(string)*2)); 2 *p = "dwah"; 3 p[0] = "dal"; 4 cout << p[0];
9.1、SGI版本的stl实现了两个空间配置器
一是符合标准的allocator,封装了new和delete,效率不佳;二是alloc,alloc::allocate调用malloc分配内存(单分配内存大于128bytes时)或者从memory pool中取小内存。C++ 标准库的allocator类由allocate、construct、destory、deallocate来完成内存分配(未初始化,不能使用)、对象构造、对象析构、内存释放的操作。(见stl源码分析)
9.2、delete容易造成的问题?
(1) 忘记写delete,内存泄漏
(2) delete同一块内存两次,未定义(重复释放或释放后仍使用)
(3) 两个指针指向同一块new的内存,delete后,另一个指针不重新赋值就为野指针,指向垃圾内存(野指针)
(4) delete语句的前面发生异常,程序终止,delete语句不执行,内存泄漏(异常不安全)
(5)delete的指针必须是指向内存首地址,如果改变了指针,不能delete
9.2.1 delete和delete []的区别
delete只会调用一次析构函数,而delete[]会调用每一个成员函数的析构函数。
在More Effective C++中有更为详细的解释:“当delete操作符用于数组时,它为每个数组元素调用析构函数,然后调用operator delete来释放内存。”delete与new配套,delete []与new []配套
9.3 c++内存布局
(1) 常量区,存字符串常量,不可修改
(2) 全局/静态存储区,存全局变量或静态变量
(3) 堆栈,存局部变量,随着函数的执行而创建变量,函数退出时变量会自动销毁
(4) 堆区或自由区,malloc或new出来的内存
C/C++内存管理详解
细说new与malloc的10点区别
堆和栈究竟有什么区别
c++内存管理学习纲要
9.4 自己重载的operator new 如何使用?
遇到new表达式,编译器开始查找operator new 函数,如果分配的是类类型,首先在类及其基类作用域中查找,此时含有operator new成员函数会被调用;否则编译器在全局作用域查找,此时如果找到了用户自定义的版本,就使用该版本,没找到就使用标准库版本。
10、share_prt引用计数什么时候+1?
(1) 假设有一个share_ptr p ,p=q,q的引用计数+1,p的引用计数-1;p值传递给函数时,p作为值返回时,用p创建新对象时
11、程序编译链接的过程和函数找不到在哪个阶段报错
(1) 预处理—>编译—>链接 ,预处理有头文件替换和宏替换、条件编译,如include<iostream>这条宏会被替换为iostream头文件的内容;cpp文件是C++的编译单元,h文件不编译,编译时各个cpp文件是互 相看不见的,A文件里调用了B文件里的函数,编译到此处,只形成一条调用指令;链接器有一个符号表,记录了符号查找的位置,A里的函数调用要到B中去寻找。
(2) 函数找不到会发生在链接阶段。
11.1 模板编译的全过程,模板编译为什么报错十分复杂,为什么模板的实现与声明只能在一个头文件里?
12、C/C++内存模型
13、动态绑定的介绍?引用是否能实现动态绑定,为什么引用可以实现
14、介绍所有的构造函数?什么情况下要给类写拷贝构造函数
(1) 如果自己不定义,编译器会为类默认生成default ctr,拷贝构造函数,copy assignment 函数,析构函数。如果自定义某个函数,就不会产生默认的;如果定义了其他函数,但是想用编译器的默认版本,可以写成 Base ()=default;如果要阻止拷贝,赋值,可以Base(const Base&) =delete; 不能delete析构函数,否则对象不可析构了。
(2) 这些默认生成的函数都是public 且inine的,直到调用时编译器才生成。自定义版本可以把拷贝构造函数设为private,这样外部就不能访问了。
(3) 会调用拷贝构造函数的情况:一、拷贝初始化,Base c=b,二,用其他实例初始化 Base d(c), 三、以值传递方式给函数传递参数,如foo(Base b),四、返回类类型的值,
(4) 不能写成Base (const Base b)形式,这样实参初始化形参,会循环调用拷贝构造函数
(5) 默认的copy ctr采用bitewise 拷贝的方式(浅拷贝),如果类含有指针,则只拷贝指针,不拷贝指针所指向的资源,这样造成了被拷贝对象与拷贝对象间共享了资源;解决办法是深拷贝。
(6) =运算符的重载形式为:Base& operator =(const Base& b),返回引用可以写出连续赋值的形式,另外要检测自我赋值:b=b,检测语句为 if(this==&b)
(7) 注意单参构造函数定义了一个隐式转换,如string s=“jdka",就是由字符指针到string的转换,要禁止这种转换,用关键字explicit
(8) 移动构造函数和赋值移运算符不会默认生成 https://www.zhihu.com/question/52138073
15、struct与class的区别
关键字struct也是声明了一种自定义类型,这与class一样,而不再只是c语言里的数据集合体;struct也可以有成员变量和成员函数,默认的访问权限为public,struct不能代替class的用处是class 作为模板参数。(见inside the c++ object model 1.2 节)。
16、继承机制中对象之间是如何转换的
A *ap = new B();
B* bp = ap; // 错误
bp = new A; // 错误
存在基类对象与派生类对象之间的转换吗?
17、须在构造函数初始化式里进行初始化的数据成员有哪些?
18、内存溢出有那些因素?
(1) 使用不安全的c语言函数,strcpy,gets
(2) 递归调用层次太深,超过了栈空间
18.1 c语言缓冲区溢出的函数?
19 、C++返回{1,2},用vector作为返回类型
看了一些C++面经,有基础问题如const的几种作用、虚函数的实现机制、熟悉shared_ptr吗、这些是C++的语言特性部分,如果要考察STL,我目前只直到vector、list,对各种算法其他容器不熟悉,更不用说容器的时间效率与空间开销;如果要考察算法,链表和二叉树、快速排序、堆排序,这又可以结合make_head() 等堆函数;如果是要考察代码实现,如memcpy、strstr、strcmp等。所以我需要牢固掌握C++的基础知识,STL,再就是刷题,掌握各种常考的算法,这些才是我能达到的优势项,linux、项目等不是我短期能擅长的地方。