[TOC] # const 关键字 用于定义常量,一旦初始化之后就无法改变。 常量必须在定义的时候进行初始化,因此用 const 修饰成员变量,必须在构造函数列表中初始化。 const 修饰指针时,分为顶层 const 和底层 const。 顶层 const 表示指针本身是个常量。 ```c++ int i = 0; int* const p = &i; ``` 底层 const 表示指针所指向的对象时一个常量。 ```c++ const int i = 0; const int* p = &i; ``` const 修饰成员函数,说明该函数不应该修改非静态成员,但是这并不是十分可靠的,指针所指的非成员对象值可能会被改变。 # static 关键字 [static详解](http://blog.csdn.net/shanghairuoxiao/article/details/72904292) # extern 关键字 [C/C++中extern关键字详解](http://www.cnblogs.com/yc_sunniwell/archive/2010/07/14/1777431.html) # volatile 关键字 关键字volatile的作用是指示编译器,即使代码不对变量做任何改动,该变量的值仍可能会被外界修改。操作系统、硬件或其他线程都有可能修改该变量。该变量的值有可能遭受意料之外的修改,因此,每一次使用时,编译器都会重新从内存中获取这个值,而不是从寄存器中获取。 volatile变量不会被优化掉,这非常有用。设想有下面这个函数: ```c++ int opt = 1; void Fn(void) {     start:         if (opt == 1) goto start;         else break; } ``` 乍一看,上面的代码好像会进入无限循环,编译器可能会将这段代码优化成: ```c++ void Fn(void) {     start:         int opt = 1;         if (true)         goto start; } ``` 这样就变成了无限循环。然后,外部操作可能会将 0 写入变量 opt 的位置,从而终止循环。 为了防止编译器执行这类优化,我们需要设法通知编译器,系统其他部分可能会修改这个变量。具体做法就是使用 volatile 关键字, # new 与 maclloc 区别 1. new 分配内存按照数据类型进行分配,malloc 分配内存按照大小分配; 2. new 不仅分配一段内存,而且会调用构造函数,但是 malloc 则不会。 3. new 返回的是指定对象的指针,而 malloc 返回的是 void\*,因此 malloc 的返回值一般都需要进行类型转化; 4. new 是一个操作符可以重载,malloc 是一个库函数; 5. new 分配的内存要用 delete 销毁,malloc 要用 free 来销毁;delete 销毁的时候会调用对象的析构函数,而 free 则不会; 6. malloc 分配的内存不够的时候,可以用 realloc 扩容。扩容的原理?new 没用这样操作; 7. new 如果分配失败了会抛出 bad_malloc 的异常,而 malloc 失败了会返回 NULL。因此对于 new,正确的姿势是采用 try...catch 语法,而 malloc 则应该判断指针的返回值。为了兼容很多 c 程序员的习惯,C++ 也可以采用 new nothrow 的方法禁止抛出异常而返回 NULL; 8. new 和 new[] 的区别,new[] 一次分配所有内存,多次调用构造函数,分别搭配使用 delete 和 delete[],同理,delete[] 多次调用析构函数,销毁数组中的每个对象。而 malloc 则只能 sizeof(int) * n; 9. 如果不够可以继续谈 new 和 malloc 的实现,空闲链表,分配方法 ( 首次适配原则,最佳适配原则,最差适配原则,快速适配原则 )。delete 和 free 的实现原理,free 为什么直到销毁多大的空间? # 四种类型转换 **const_cast** 用于将 const 变量转为非 const **static_cast** 用的最多,对于各种隐式转换,非 const 转 const,void\* 转指针等 , static_cast 能用于多态想上转化,如果向下转能成功但是不安全,结果未知; **dynamic_cast** 用于动态类型转换。只能用于含有虚函数的类,用于类层次间的向上和向下转化。只能转指针或引用。向下转化时,如果是非法的对于指针返回 NULL,对于引用抛异常。要深入了解内部转换的原理。 **reinterpret_cast** 几乎什么都可以转,比如将 int 转指针,可能会出问题,尽量少用; 为什么不使用 C 的强制转换?C 的强制转换表面上看起来功能强大什么都能转,但是转化不够明确,不能进行错误检查,容易出错。 # 指针和引用的区别 指针保存的是所指对象的地址,引用是所指对象的别名,指针需要通过解引用间接访问,而引用是直接访问; 指针可以改变地址,从而改变所指的对象,而引用必须从一而终; 引用在定义的时候必须初始化,而指针则不需要; 指针有指向常量的指针和指针常量,而引用没有常量引用; 指针更灵活,用的好威力无比,用的不好处处是坑,而引用用起来则安全多了,但是比较死板。 # 指针和数组的联系 一个一维 int 数组的数组名实际上是一个 int\* const 类型; 一个二维 int 数组的数组名实际上是一个 int (\*const p)[n]; 数组名做参数会退化为指针 # 智能指针 构造函数中计数初始化为1; 拷贝构造函数中计数值加1; 赋值运算符中,左边的对象引用计数减一,右边的对象引用计数加一; 析构函数中引用计数减一; 在赋值运算符和析构函数中,如果减一后为0,则调用delete释放对象。 share_prt与weak_ptr的区别? ```c++ //share_ptr可能出现循环引用,从而导致内存泄露 class A { public:     share_ptr p; }; class B { public:     share_ptr p; } int main() {     while(true)     {         share_prt pa(new A()); //pa的引用计数初始化为1         share_prt pb(new B()); //pb的引用计数初始化为1         pa->p = pb; //pb的引用计数变为2         pb->p = pa; //pa的引用计数变为2     }     //假设pa先离开,引用计数减一变为1,不为0因此不会调用class A的析构函数,因此其成员p也不会被析构,pb的引用计数仍然为2;     //同理pb离开的时候,引用计数也不能减到0     return 0; } /* ** weak_ptr是一种弱引用指针,其存在不会影响引用计数,从而解决循环引用的问题 ``` # 多态性 作者:oscarwin 链接:https://www.nowcoder.com/discuss/59394 来源:牛客网 C++多态性与虚函数表 C++多态的实现? 多态分为静态多态和动态多态。静态多态是通过重载和模板技术实现,在编译的时候确定。动态多态通过虚函数和继承关系来实现,执行动态绑定,在运行的时候确定。 动态多态实现有几个条件: (1) 虚函数; (2) 一个基类的指针或引用指向派生类的对象; 基类指针在调用成员函数(虚函数)时,就会去查找该对象的虚函数表。虚函数表的地址在每个对象的首地址。查找该虚函数表中该函数的指针进行调用。 每个对象中保存的只是一个虚函数表的指针,C++内部为每一个类维持一个虚函数表,该类的对象的都指向这同一个虚函数表。 虚函数表中为什么就能准确查找相应的函数指针呢?因为在类设计的时候,虚函数表直接从基类也继承过来,如果覆盖了其中的某个虚函数,那么虚函数表的指针就会被替换,因此可以根据指针准确找到该调用哪个函数。 虚函数的作用? 虚函数用于实现多态,这点大家都能答上来 但是虚函数在设计上还具有封装和抽象的作用。比如抽象工厂模式。 动态绑定是如何实现的? 第一个问题中基本回答了,主要都是结合虚函数表来答就行。 静态多态和动态多态。静态多态是指通过模板技术或者函数重载技术实现的多态,其在编译器确定行为。动态多态是指通过虚函数技术实现在运行期动态绑定的技术。 虚函数表 虚函数表是针对类的还是针对对象的?同一个类的两个对象的虚函数表是怎么维护的? 编译器为每一个类维护一个虚函数表,每个对象的首地址保存着该虚函数表的指针,同一个类的不同对象实际上指向同一张虚函数表。 纯虚函数如何定义,为什么对于存在虚函数的类中析构函数要定义成虚函数 为了实现多态进行动态绑定,将派生类对象指针绑定到基类指针上,对象销毁时,如果析构函数没有定义为析构函数,则会调用基类的析构函数,显然只能销毁部分数据。如果要调用对象的析构函数,就需要将该对象的析构函数定义为虚函数,销毁时通过虚函数表找到对应的析构函数。 1 2 //纯虚函数定义 virtual ~myClass() = 0; 析构函数能抛出异常吗 答案肯定是不能。 C++标准指明析构函数不能、也不应该抛出异常。C++异常处理模型最大的特点和优势就是对C++中的面向对象提供了最强大的无缝支持。那么如果对象在运行期间出现了异常,C++异常处理模型有责任清除那些由于出现异常所导致的已经失效了的对象(也即对象超出了它原来的作用域),并释放对象原来所分配的资源, 这就是调用这些对象的析构函数来完成释放资源的任务,所以从这个意义上说,析构函数已经变成了异常处理的一部分。 (1) 如果析构函数抛出异常,则异常点之后的程序不会执行,如果析构函数在异常点之后执行了某些必要的动作比如释放某些资源,则这些动作不会执行,会造成诸如资源泄漏的问题。 (2) 通常异常发生时,c++的机制会调用已经构造对象的析构函数来释放资源,此时若析构函数本身也抛出异常,则前一个异常尚未处理,又有新的异常,会造成程序崩溃的问题。 构造函数和析构函数中调用虚函数吗? # 内存对齐 作者:oscarwin 链接:https://www.nowcoder.com/discuss/59394 来源:牛客网 从0位置开始存储; 变量存储的起始位置是该变量大小的整数倍; 结构体总的大小是其最大元素的整数倍,不足的后面要补齐; 结构体中包含结构体,从结构体中最大元素的整数倍开始存; 如果加入pragma pack(n) ,取n和变量自身大小较小的一个。 # 内联函数 宏定义在预编译的时候就会进行宏替换; 内联函数在编译阶段,在调用内联函数的地方进行替换,减少了函数的调用过程,但是使得编译文件变大。因此,内联函数适合简单函数,对于复杂函数,即使定义了内联编译器可能也不会按照内联的方式进行编译。 内联函数相比宏定义更安全,内联函数可以检查参数,而宏定义只是简单的文本替换。因此推荐使用内联函数,而不是宏定义。 使用宏定义函数要特别注意给所有单元都加上括号,#define MUL(a, b) a b,这很危险,正确写法:#define MUL(a, b) ((a) (b)) # 内存管理 C++ 内存分为那几块?(堆区,栈区,常量区,静态和全局区) 每块存储哪些变量? 学会迁移,可以说到 malloc,从 malloc 说到操作系统的内存管理,说道内核态和用户态,然后就什么高端内存,slab 层,伙伴算法,VMA 可以巴拉巴拉了,接着可以迁移到 fork()。 # STL 的内存池实现 STL 内存分配分为一级分配器和二级分配器,一级分配器就是采用 malloc 分配内存,二级分配器采用内存池。 二级分配器设计的非常巧妙,分别给 8k,16k,..., 128k 等比较小的内存片都维持一个空闲链表,每个链表的头节点由一个数组来维护。需要分配内存时从合适大小的链表中取一块下来。假设需要分配一块 10K 的内存,那么就找到最小的大于等于 10k 的块,也就是 16K,从16K 的空闲链表里取出一个用于分配。释放该块内存时,将内存节点归还给链表。如果要分配的内存大于 128K 则直接调用一级分配器。 为了节省维持链表的开销,采用了一个 union 结构体,分配器使用 union 里的 next 指针来指向下一个节点,而用户则使用 union 的空指针来表示该节点的地址。 # STL 里的 set 和 map 实现 set 和 map 都是基于红黑树实现的。 红黑树是一种平衡二叉查找树,AVL 树是完全平衡的,红黑树基本上是平衡的。 与 AVL 相比红黑数插入和删除最多只需要 3 次旋转,而 AVL 树为了维持其完全平衡性,在坏的情况下要旋转的次数太多。 # 必须在构造函数初始化列表里进行初始化的数据成员 1. 常量成员,因为常量只能初始化不能赋值,所以必须放在初始化列表里面; 2. 引用类型,引用必须在定义的时候初始化,并且不能重新赋值,所以也要写在初始化列表里面; 3. 没有默认构造函数的类类型,因为使用初始化列表可以不必调用默认构造函数来初始化,而是直接调用拷贝构造函数初始化。 # 手写 strcpy() ```c++ char* strcpy(char* dest, const char* src) {     char *save = dest;     while (*dest++ = *src++){}     return save; } ``` ```c++ char src[] = "abc"; char dest[10]; // dest 必须大于 src strcpy(dest, src); cout << dest << endl; ``` # 手写 strcat() ``` char* strcat(char* dest, const char* src) {     char* save = dest;     while (*dest) dest++;     while(*dest++ = *src++){}     return save; } ``` # 手写 strcmp() ```c++ int strcmp(const char* s1, const char* s2) {     while(*s1 == *s2 && *s1)     {         s1++;         s2++;     }     return *s1 - *s2; } ``` # 手写 strstr() 要求不能用其它库函数。 直接使用暴力方法,每次比较 src 的一部分是否与 target 相等。 循环体只需要执行 N - M + 1 次即可,由于不能用 strlen() ,因此无法直接计算出 N 与 M,可以使用一个额外的指针,令它先从 src 头部移动 M - 1 次,这样它还可以再移动的次数就为 N - M + 1 次。 ```c++ char* strstr(const char* src, const char* target) {     if (!*target) return (char*)src;     char *p1 = (char*)src, *p2 = (char*)target;     char* p1_ahead = (char*)src;     while (*++p2)         p1_ahead++;     while (*p1_ahead++)     {         char* p1_begin = p1;         p2 = (char*)target;         while (*p1 && *p2 && *p1++ == *p2++)         {         }         if (!*p2) return p1_begin;         p1 = p1_begin + 1;     }     return nullptr; } ``` # 手写 memcpy() ```c++ void* memcpy(void* dest, const void* src, size_t len) {     char* d = (char *)dest;     const char* s = (char *)src;     while (len--)         *d++ = *s++;     return dest; } ``` # 参考资料 - [C++后台开发面试常见问题汇总](https://www.nowcoder.com/discuss/59394) - [函数strcpy、strcat和strcmp实现源码](http://blog.csdn.net/wangningyu/article/details/4662891) - [Github : gcc/libgcc/memcpy.c](https://github.com/gcc-mirror/gcc/blob/master/libgcc/memcpy.c) - [Leetcode : Implement strstr() to Find a Substring in a String](https://articles.leetcode.com/implement-strstr-to-find-substring-in/)