C++ 复习要点

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/csdn_chai/article/details/78041050

本文总结一下C++面试时常遇到的问题。C++面试中,主要涉及的考点有

  • 关键字极其用法,常考的关键字有const, sizeof, typedef, inline, static, extern, new, delete等等
  • 语法问题
  • 类型转换
  • 指针以及指针和引用的区别
  • 面向对象的相关问题,如虚函数机制等
  • 泛型编程的相关问题,如模板和函数的区别等
  • 内存管理,如字节对齐(内存对齐)、动态内存管理、内存泄漏等
  • 编译和链接
  • 实现函数和类
本文不涉及STL的内容,有关STL的内容,会另有一篇文章专门总结。

零、序章

0.1 C++与C的对比

  1. C++有三种编程方式:过程性,面向对象,泛型编程。
  2. C++函数符号由 函数名+参数类型 组成,C只有函数名。所以,C没有函数重载的概念。
  3. C++ 在 C的基础上增加了封装、继承、多态的概念
  4. C++增加了泛型编程
  5. C++增加了异常处理,C没有异常处理
  6. C++增加了bool型
  7. C++允许无名的函数形参(如果这个形参没有被用到的话)
  8. C允许main函数调用自己
  9. C++支持默认参数,C不支持
  10. C语言中,局部变量必须在函数开头定义,不允许类似for(int a = 0; ;;)这种定义方法。
  11. C++增加了引用
  12. C允许变长数组,C++不允许
  13. C中函数原型可选,C++中在调用之前必须声明函数原型
  14. C++增加了STL标准模板库来支持数据结构和算法

一、重要的关键字极其用法

1.1 const 

主要用法

C++ 的const关键字的作用有很多,几乎无处不在,面试中往往会问“说一说const有哪些用法”。下面是一些常见的const用法的总结:

 const 变量

const int a;

不能修改值,必须初始化

 const 类对象

const MyClass a;

不能修改成员变量的值,不能调用非 const 函数

 指向 const 变量的指针

const int * a;

指向内容不可变,指向可变

 const 指针

int * const a;

指向内容可变,指向不可变

 指向 const 变量的 const 指针

const int * const a;

指向内容不可变,指向也不可变

const 引用

 const 变量作为函数参数

void myfun(const int a);

函数内部不能改变此参数

指向 const 变量的指针做参数,允许上层用一般指针调用。(反之不可)

 const 返回值

const string& myfun(void);

用于返回const引用

上层不能使用返回的引用来修改对象

 const 成员变量

const int a;

static const int a;

必须在初始化列表初始化,之后不能改变

static const 成员变量需要单独定义和初始化

const 成员函数

void myfun(void) const;

this指针为指向const对象的const指针

不能修改 非mutable 的成员变量

除此以外,const的用法还有:
  • const引用可以引用右值,如const int& a = 1; 
注:
  1. const 成员方法本质上是使得this指针是指向const对象的指针,所以在const方法内,
  2. const 成员函数可以被非const和const对象调用,而const对象只能调用const 成员函数。原因得从C++底层找,C++方法调用时,会传一个隐形的this参数(本质上是对象的地址,形参名为this)进去,所有成员方法的第一个参数是this隐形指针。const成员函数的this指针是指向const对象的const指针,当非const对象调用const方法时,实参this指针的类型是非const对象的const指针,赋给const对象的const指针没有问题;但是如果const对象调用非const方法,此时实参this指针是指向const对象的const指针,无法赋给非const对象的const指针,所以无法调用。注意this实参是放在ecx寄存器中,而不是压入栈中,这是this的特殊之处。在类的非成员函数中如果要用到类的成员变量,就可以通过访问ecx寄存器来得到指向对象的this指针,然后再通过this指针加上成员变量的偏移量来找到相应的成员变量。http://blog.csdn.net/starlee/article/details/2062586/
  3. const 指针、指向const的指针和指向const的const指针,涉及到const的特性“const左效、最左右效”
  4. const 全局变量有内部链接性,即不同的文件可以定义不同的同名const全局变量,使用extern定义可以消除内部链接性,称为类似全局变量,如extern const int a = 10.另一个文件使用extern const int a; 来引用。而且编译器会在编译时,将const变量替换为它的值,类似define那样。

const 常量和define 的区别

  1. const常量有数据类型,而宏定义没有数据类型。编译器可以对前者进行类型安全检查,而对后者只进行字符替换,没有类型安全检查,并且在字符替换中可能会产生意想不到的错误(边际效应)。
  2. 有些集成化的调试工具可以对const常量进行调试,但是不能对宏定义进行调试。
  3. 在C++程序中只使用const常量而不使用宏常量,即const常量完全取代宏常量。
  4. 内存空间的分配上。define进行宏定义的时候,不会分配内存空间,编译时会在main函数里进行替换,只是单纯的替换,不会进行任何检查,比如类型,语句结构等,即宏定义常量只是纯粹的置放关系,如#define null 0;编译器在遇到null时总是用0代替null它没有数据类型.而const定义的常量具有数据类型,定义数据类型的常量便于编译器进行数据检查,使程序可能出现错误进行排查,所以const与define之间的区别在于const定义常量排除了程序之间的不安全性.
  5. const常量存在于程序的数据段,#define常量存在于程序的代码段
  6. const常量存在“常量折叠”,在编译器进行语法分析的时候,将常量表达式计算求值,并用求得的值来替换表达式,放入常量表,可以算作一种编译优化。因为编译器在优化的过程中,会把碰见的const全部以内容替换掉,类似宏。

1.2 sizeof

  1. sizeof关键字不会计算表达式的值,而只会根据类型推断大小。
  2. sizeof() 的括号可以省略, 如 sizeof a ; 
  3. 类A的大小是 所有非静态成员变量大小之和+虚函数指针大小

1.3 static 

static的用法有:
(1)声明静态全局变量,如static int a; 静态全局变量的特点:
  • 该变量在全局数据区分配内存; 
  • 未经初始化的静态全局变量会被程序自动初始化为0(自动变量的值是随机的,除非它被显式初始化); 
  • 静态全局变量在声明它的整个文件都是可见的,而在文件之外是不可见的; 
(2)声明静态局部变量,即在函数内部声明的,静态局部变量的特点:
  • 该变量在全局数据区分配内存; 
  • 静态局部变量在程序执行到该对象的声明处时被首次初始化,即以后的函数调用不再进行初始化; 
  • 静态局部变量一般在声明处初始化,如果没有显式初始化,会被程序自动初始化为0; 
  • 它始终驻留在全局数据区,直到程序运行结束。但其作用域为局部作用域,当定义它的函数或语句块结束时,其作用域随之结束;
(3)声明静态函数,限定函数的局部访问性,仅在文件内部可见
(4)类的静态数据成员,与全局变量相比,静态数据成员的好处有:
  • 静态数据成员没有进入程序的全局名字空间,因此不存在与程序中其它全局名字冲突的可能性; 
  • 可以实现信息隐藏。静态数据成员可以是private成员,而全局变量不能;
(5)类的静态方法

1.4 typedef 

typedef 用来定义新的类型,类似的还有#define 和 using (C++11) (应该尽可能用using ,比如 using AAA = int64_t; )

与宏定义的对比

  1. #define 在预处理阶段进行简单替换,不做类型检查; typedef在编译阶段处理,在作用域内给类型一个别名。
  2. typedef 是一个语句,结尾有分号;#define是一个宏指令,结尾没有分号
  3. typedef int* pInt; 和 #define pInt int* 不等价,前者定义 pInt a, b;会定义两个指针,后者是一个指针,一个int。

不能声明为inline的函数

  1. 包含了递归、循环等结构的函数一般不会被内联。
  2. 虚拟函数一般不会内联,但是如果编译器能在编译时确定具体的调用函数,那么仍然会就地展开该函数。
  3. 如果通过函数指针调用内联函数,那么该函数将不会内联而是通过call进行调用。
  4. 构造和析构函数一般会生成大量代码,因此一般也不适合内联。
  5. 如果内联函数调用了其他函数也不会被内联。

1.5 inline

inline用来向编译器请求声明为内联函数,编译器有权拒绝。

与宏函数的对比

  1. 内联函数在运行时可调试,而宏定义不可以;
  2. 编译器会对内联函数的参数类型做安全检查或自动类型转换(同普通函数),而宏定义则不会;
  3. 内联函数可以访问类的成员变量,宏定义则不能;
  4. 在类中声明同时定义的成员函数,自动转化为内联函数
  5. 宏只是预定义的函数,在编译阶段不进行类型安全性检查,在编译的时候将对应函数用宏命令替换。对程序性能无影响

1.6 static const \ const \ static 

1. static const 
static const 数据成员可以在类内初始化 也可以在类外,不能在构造函数中初始化,也不能在构造函数的初始化列表中初始化
2. static
static数据成员只能在类外,即类的实现文件中初始化,也不能在构造函数中初始化,不能在构造函数的初始化列表中初始化;
3. const
const数据成员只能在构造函数的初始化列表中初始化;

1.7 explicit 

explicit禁止了隐式转换类型,用来修饰构造函数。原则上应该在所有的构造函数前加explicit关键字,当你有心利用隐式转换的时候再去解除explicit,这样可以大大减少错误的发生。如果一个构造函数 Foo(int) ;则下面的语句是合法的:
Foo f; 
f = 12; // 发生了隐式转换,先调用Foo(int)用12构建了一个临时对象,然后调用赋值运算符复制到 f 中
如果给构造函数加了explicit,即 explicit Foo(int);就只能进行显示转换,无法进行隐式转换了:
f = 12; // 非法,隐式转换
f = Foo(12); // 合法,显示转换
f = (Foo)12;//合法,显示转换,C风格

1.8 extern 

extern可以置于变量或者函数前,以标示变量或者函数的定义在别的文件中,提示编译器遇到此变量和函数时在其他模块中寻找其定义。此外extern也可用来进行链接指定。

二、语法问题

2.1 a++ 与 ++a的区别

  1. a++ 返回加之前的值,++a返回加之后的a变量
  2. a++返回的是一个临时变量,是右值,无法赋值;++a返回的是变量a,是左值

2.2 switch语句

switch语句的表达式必须是整型int , char, short等。

2.3 函数调用过程

执行到函数调用指令时:
  1. +++++++++ 入栈 ++++++++++++
  2. 将实参从右向左压入栈
  3. 压入返回地址
  4. 压入主调函数的基地址
  5. 跳到被调用函数的地址,执行函数代码,局部变量按声明顺序依次压入栈
  6. 将返回值放入寄存器eax(累加器)中
  7. +++++++++ 出栈 ++++++++++++
  8. 局部变量全部出栈
  9. 返回地址出栈,找到原执行地址
  10. 形参出栈
  11. 赋值操作将寄存器中的返回值赋给左值(如果有的话)

2.4 左值与右值

判断左值和右值的标准是是否可以取地址。右值和左值不同,有可能存在于寄存器中,无法取地址,无法被赋值,临时变量就是右值,存放在寄存器中,被赋给左值后被释放。

2.5 C语言标识符

关键字、预定义标识符、用户标识符(不能以数字开头)
a123 // 合法
_a // 合法
_0 // 合法
0asdasd// 非法

2.6 全局变量的优缺点

优点:
(1)可以减少变量的个数
(2)减少由于实际参数和形式参数的数据传递带来的时间消耗。
缺点:
(1)全局变量保存在静态存贮区,程序开始运行时为其分配内存,程序结束释放该内存。与局部变量的动态分配、动态释放相比,生存期比较长,因此过多的全局变量会占用较多的内存单元。
(2)全局变量破坏了函数的封装性能。前面的章节曾经讲过,函数象一个黑匣子,一般是通过函数参数和返回值进行输入输出,函数内部实现相对独立。但函数中 如果使用了全局变量,那么函数体内的语句就可以绕过函数参数和返回值进行存取,这种情况破坏了函数的独立性,使函数对全局变量产生依赖。同时,也降低了该 函数的可移植性。
(3)全局变量使函数的代码可读性降低。由于多个函数都可能使用全局变量,函数执行时全局变量的值可能随时发生变化,对于程序的查错和调试都非常不利。

2.7 复合类型有哪些?

6个
class, struct, union, enum, 数组,指针

2.8 运算符优先级和结合性?

结合性有两种,一种是自左至右,另一种是自右至左,大部分运算符的结合性是自左至右,只有单目运算符、三目运算符的赋值运算符的结合性自右至左
优先级有15种。记忆方法如下:
记住一个最高的:构造类型的元素或成员以及小括号。
记住一个最低的:逗号运算符。
剩余的是一、二、三、赋值。
意思是单目、双目、三目和赋值运算符。
在诸多运算符中,又分为:
算术、关系、逻辑。
两种位操作运算符中,移位运算符在算术运算符后边,逻辑位运算符在逻辑运算符的前面。再细分如下:
算术运算符分 *,/,%高于+,-。
关系运算符中,〉,〉=,<,<=高于==,!=。
逻辑运算符中,除了逻辑求反(!)是单目外,逻辑与(&&)高于逻辑或(||)。
逻辑位运算符中,除了逻辑按位求反(~)外,按位与(&)高于按位半加(^),高于按位或(|)。
这样就将15种优先级都记住了,再将记忆方法总结如下:
去掉一个最高的,去掉一个最低的,剩下的是一、二、三、赋值。双目运算符中,顺序为 算术、移位、关系(>,<,==)、逻辑位和逻辑(&& ||)。

2.9 using 声明和using 编译指令的区别?哪个更好?

using声明是指类似using std::vector;这种的,using编译指令是指using namespace std;这种的。区别:
(1)using声明使特定的标示符可用,using编译指令使整个名称空间可用。
(2)假设名称空间和声明区域定义了相同的名称。如果试图使用using声明将名称空间的名称导入该声明区域,则这两个名称会发生冲突,从而出错。如果使用using编译指令将该名称空间的名称导入该声明区域,则局部版本将隐藏名称空间版本。
(3)一般来说,使用using声明比使用using编译指令更安全,这是由于它只导入指定的名称。如果该名称与局部名称发生冲突,编译器将付出指示。using编译指令导入所有名称,包括可能并不需要的名称。如果与局部名称发生冲突,则局部名称将覆盖名称空间版本,而编译器并不会发出警告。

2.10 for循环的效率问题

1. 最长循环放到内部可以提高I cache的效率,降低因为循环跳转造成cache的miss以及流水线flush造成的延时
2. 多次相同循环后也能提高跳转预测的成功率,提高流水线效率
3. 编译器会自动展开循环提高效率, 这个不一定是必然有效的
但不是绝对正确的,比如: 1 int x[1000][100];
2 for(i=0;i<1000;i++)
3 for(j=0;j<100;j++)
4 {
5 //access x[i][j]
6 }
7
8 int x[1000][100];
9 for(j=0;j<100;j++)
10 for(i=0;i=1000;i++)
11 {
12 //access x[i][j]
13 }
14
这时候第一个的效率就比第二个的高,原因嘛和硬件也有一些关系,CPU对于内存的访问都是通过数据缓存(cache)来进行的。

三、类型转换

3.1 四种类型强制转换

  1. dynamic_cast:该转换符用于将一个指向派生类的基类指针或引用转换为派生类的指针或引用。
  2. const_cast:最常用的用途就是删除const属性。
  3. static_cast:static_cast本质上是传统c语言强制转换的替代品,比C类型转换更严格, 该操作符用于非多态类型的转换,任何标准转换都可以使用他,即static_cast可以把int转换为double,但不能把两个不相关的类对象进行转换,比如类A不能转换为一个不相关的类B类型。static_cast在类对象和基础类型转换中,会调用类的构造函数,和类型转换运算符比如operator int(),来进行显示转换。
  4. reinterpret_cast:该操作符用于将一种类型转换为另一种不同的类型,比如可以把一个整型转换为一个指针,或把一个指针转换为一个整型,因此使用该操作符的危险性较高,一般不应使用该操作符。

四、指针

4.1 指针与引用的区别

  1. 指针是一个变量,引用只是别名
  2. 指针需要解引用才能访问对象,引用不需要
  3. 引用在定义时必须初始化,且以后不可转移引用的对象,指针可以
  4. 引用没有const,即int& const a ;没有;而指针有const指针,即int* const ptr; 
  5. 引用不可以为空;而指针可以
  6. 指针变量需要分配栈空间;而引用不需要,仅仅是个别名
  7. sizeof(引用)得到对应对象的大小;sizeof(指针)得到指针大小
  8. 指针加法和引用加法不一样
  9. 引用不需要释放内存空间,在编译时就会优化掉

4.2 指针与数组名的区别

  1. 数组名不是指针,对数组名取地址,得到整个数组的地址
  2. 数组名 + 1会跳过整个数组的大小,指针+1只会跳过一个元素的大小
  3. 数组名作为函数参数传递时,退化为指针
  4. sizeof(数组名)返回整个数组的大小,sizeof(指针)返回指针大小
  5. 数组名无法修改值,是常量
  6. int (*p)[] = &arr; 才是正确的数组指针写法

4.3 野指针、空指针的概念

  1. 野指针是指指向无效内存的指针,不能对野指针取内容,delete
  2. 空指针是指置为0\NULL\nullptr的指针,可以对空指针delete多次

五、面向对象

5.1 面向对象的三大特性

三大特性:封装,继承,多态  
  1. 封装:封装是实现面向对象程序设计的第一步,封装就是将数据或函数等集合在一个个的单元中(我们称之为类)。封装的意义在于保护或者防止代码(数据)被我们无意中破坏。
  2. 继承:继承主要实现重用代码,节省开发时间。子类可以继承父类的一些东西。
  3. 多态:同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果。分为编译时多态和运行时多态。

5.2 函数重载和运算符重载

问:函数重载的依据?

答:
  1. 参数个数
  2. 参数类型
  3. const方法与非const方法构成重载

问:运算符重载的限制?

答:
  1. 被重载的运算符,至少有一个操作数是用户自定义类型,也就是说不能重载C++语言的标准运算
  2. 重载的运算符的句法规则不可以改变,操作数、结合性和优先级无法更改。以前是几元现在就是几元;该是左结合还是左结合;优先级无法更改。
  3. 不能自定义运算符,不能创建新的运算符。
  4. 不能重载的运算符有:
    1. 成员访问运算符 . 
    2. 成员指针运算符 .* 
    3. 作用域解析运算符 ::
    4. 条件运算符 ?:
    5. sizeof 
    6. typeid
    7. 四个类型转换运算符
      1. const_cast
      2. static_cast
      3. dynamic_cast
      4. reinterpret_cast
  5. 只能通过成员函数重载,而不能通过友元重载的运算符:
    1. 赋值运算符 = 
    2. 函数调用运算符 () 
    3. 下标运算符 []
    4. 通过指针访问成员运算符 ->
  6. 只能通过友元重载,不能通过成员函数重载的情况:
    1. 双目运算符最好用友元重载,单目运算符最好用成员函数重载
    2. 若运算符所需的操作数(尤其是第一个操作数)希望有隐式类型转换,则只能选用友元函数
    3. 左操作数是不同类的对象或者内部类型,比如ostream, istream, int, float等
    4. 当需要重载运算符具有可交换性时,选择重载为友元函数
  7. 对返回类型没有限制,可以是void或者其他类型
  8. 重载一元运算符需要注意,由于一元运算符没有参数,前缀和后缀无法区分,所以需要加一个哑元(dummy),哑元永远用不上,如果有哑元,则是后缀形式,否则,就是前缀。

5.3 哪些成员无法被继承?

  1. 无法被继承的有
    1. 构造函数
    2. 析构函数
    3. 赋值运算符
    4. 友元函数
  2. 可以被继承的有
    1. 静态成员
    2. 静态方法
    3. 非静态成员
    4. 非静态方法(无论是private\public\protected,只是private的继承了也无法访问)
    5. 虚表指针

5.4 定义默认构造函数的两种方法?

  1. 给已有的构造函数中的一个的所有参数加上默认值
  2. 通过方法重载定义一个无参数构造函数
注意:
  1. 隐式调用默认构造函数不要加括号(), 会被编译器解释为函数声明。

5.5 调用非默认构造函数的三种方法?

  1. Foo f(...); // 隐式调用
  2. Foo f = Foo(...) ;// 显式调用
  3. Foo* f = new Foo(); // 显式调用 

5.6 由编译器生成的6个成员函数?

注意:对于空类,不会生成任何成员函数,只会生成一个字节的占位符。
  1. 默认构造函数
  2. 析构函数
  3. 复制构造函数
  4. 赋值运算符
  5. 取地址运算符
  6. 取地址运算符 const版本

5.7 友元的三种实现方式

  1. 友元函数
  2. 友元类
  3. 友元成员函数

5.8 为什么基类的析构函数为什么要声明为虚函数?

为了能在多态情况下准确调用派生类的析构函数。如果基类的析构函数非虚函数,则用基类指针或引用引用派生类进行析构时,只会调用基类的析构函数;如果是虚析构函数,则会依次调用派生类的析构和基类的析构。(基类的析构是一定会调用的,无论是否为虚)。

5.9 为什么构造函数不可以是虚函数?

  1. 虚函数在运行期决定函数调用,而在构造一个对象时,由于对象还未构造成功,编译器无法确定对象的实际类型,继而无法决定调用哪一个构造函数。
  2. 虚函数的执行依赖于虚函数表,而虚函数表在构造函数中进行初始化工作,即初始化 vptr,让它指向正确的虚函数表,而在构造期间,虚函数表还没有初始化,所以无法决定调用哪个构造函数。

5.10 析构函数什么时候声明为私有?什么时候不能声明为私有?

  1. 私有析构函数可以使得对象只在堆上构造。在栈上创建的对象要求构造函数和析构函数必须都是公有的,否则编译器报错“析构函数不可访问”;而堆对象由程序员创建和删除,可以把析构函数声明为私有的。由于delete会调用析构函数,而私有的析构无法被访问,编译器报错,此时通过增加一个destroy()方法,在方法内调用析构函数来释放对象:
    • void destroy() 
    • {
    • delete this; 
    • }
  2. 析构函数不能声明为私有的情况:基类的析构函数不能声明为私有,因为要在派生类的析构函数中被隐式调用。

5.11 构造函数什么时候声明为私有?什么时候不能声明为私有?

  1. 单例模式。
  2. 基类的构造函数不能声明为私有,因为要在派生类的构造函数中被隐式调用。如果在派生类的构造函数中没有显式调用基类的构造,则会调用基类的默认构造函数。

5.12 不能声明为虚函数的成员函数

构造函数:

首先明确一点,在编译期间编译器完成了虚表的创建,而虚指针在构造函数期间被初始化。

如果构造函数是虚函数,那必然需要通过虚指针来找到虚构造函数的入口地址,但是这个时候我们还没有把虚指针初始化。因此,构造函数不能是虚函数。

內联函数:

编译期內联函数在调用处被展开,而虚函数在运行时才能被确定具体调用哪个类的虚函数。內联函数体现的是编译期机制,而虚函数体现的是运行期机制。

静态成员函数:

静态成员函数和类有关,即使没有生成一个实例对象,也可以调用类的静态成员函数。而虚函数的调用和虚指针有关,虚指针存在于一个类的实例对象中,如果静态成员函数被声明成虚函数,那么调用成员静态函数时又如何访问虚指针呢。总之可以这么理解,静态成员函数与类有关,而虚函数与类的实例对象有关。

非成员函数:

虚函数的目的是为了实现多态,多态和继承有关。所以声明一个非成员函数为虚函数没有任何意义。

5.13 虚函数机制以及内存分布

http://www.cnblogs.com/freeopen/p/5482965.html 重点看多继承的内存分布。
虚函数机制涉及的指针和表有:
  • 虚函数表指针 vfptr和虚函数表 vftable
  • 虚继承下还涉及 虚基类表指针 vbptr和虚基类表 vbtable
虚函数的实现过程:
1.编译器为每个含有虚函数的类或者从此类派生的类创建一个虚函数表vftable, 保存此类所有虚函数的地址,并增加一个隐藏成员虚函数表指针vfptr放在所有数据成员之前。在创建类的对象时,在构造函数内部对虚函数表指针进行初始化,指向之前创建的虚函数表。
2. 单继承情况下,派生类会继承基类所有的数据成员和虚函数表指针,并由编译器生成虚函数表,在创建派生类实例时,将虚函数表指针指向新的,属于派生类的虚函数表。
3. 多重继承情况下,会有多个虚函数表,几重继承,就会有几个虚函数表。这些表按照派生的顺序依次排列,如果派生类改写了基类的虚函数,那么就会用派生类自己的虚函数覆盖虚函数表的相应的位置,如果派生类有新的虚函数,那么就添加到第一个虚函数表的末尾
4. 虚继承情况下,会再创建一个虚基类表和一个虚基类表指针,也就是说,编译器会增加两个指针,一个是虚基类表指针,指向虚基类表,保存了所有继承过来的虚基类在内存中的地址(偏移量);另一个是继承过来的虚函数表指针,保存了虚函数的地址。如果派生类有新的虚函数,那么就再增加一个虚函数表指针,指向一个新的虚函数表,保存了派生类新的虚函数的地址。
5. 虚基类部分会在C++继承层次中只有一份。所有由虚基类派生的类都持有一个虚基类表指针,指向一个虚基类表,表里面保存了所有它继承的虚基类部分的地址。虚基类部分有一个虚函数表指针,指向虚函数表。

5.14 class 与 struct的区别

  1. class默认的继承方式为private, struct 默认继承方式为public 
  2. class的成员访问默认为private, struct默认为public 

5.15 重载、重写(覆盖)与隐藏(重定义)的关系

重载  override 
重写(覆盖)override 
隐藏  hide 
  1. 重载。函数名相同,参数个数、类型不同,或者用const重载。是同一个类中方法之间的关系,是水平关系。
  2. 重写。派生类重新定义基类中有相同名称和参数的虚函数,要求参数列表必须相同。方法在基类和派生中的访问限制可以不同。
  3. 隐藏。派生类重新定义基类中有相同名称的函数(参数列表可以不同)会把其他基类的同名方法隐藏起来,无法被派生类调用。

5.16 哪些情况下方法可以不写定义?

  1. 纯虚方法
  2. 非虚方法
所以,非纯虚的虚方法也就是普通的虚方法必须写定义,哪怕是空的,因为要生成虚函数表,没有方法定义就没有方法地址。

5.17 派生类可以不实现虚基类的纯虚方法,派生类也成了抽象类。

5.18 三种继承方式(public, private, protected)的区别?

  1. 公有继承(public): 基类成员对其对象的可见性与一般类及其对象的可见性相同,public成员可见,protected和private成员不可见,基类成员对派生类的可见性对派生类来说,基类的public和protected成员可见:基类的public成员和protected成员作为派生类的成员时,它们都保持原有状态;基类的private成员依旧是private,派生类不可访问基类中的private成员。 基类成员对派生类对象的可见性对派生类对象来说,基类的public成员是可见的,其他成员是不可见的。 所以,在公有继承时,派生类的对象可以访问基类中的public成员,派生类的成员方法可以访问基类中的public成员和protected成员。
  2. 私有继承(private) 基类成员对其对象的可见性与一般类及其对象的可见性相同,public成员可见,其他成员不可见,基类成员对派生类的可见性对派生类来说,基类的public成员和protected成员是可见的:基类的public成员和protected成员都作为派生类的private成员,并且不能被这个派生类的子类所访问;基类的私有成员是不可见的:派生类不可访问基类中的private成员,基类成员对派生类对象的可见性对派生类对象来说,基类的所有成员都是不可见的,所以在私有继承时,基类的成员只能由直接派生类访问,无法再往下继承。
  3. 保护继承(protected) 保护继承与私有继承相似,基类成员对其对象的可见性与一般类及其对象的可见性相同,public成员可见,其他成员不可见,基类成员对派生类的可见性,对派生类来说,基类的public和protected成员是可见的:基类的public成员和protected成员都作为派生类的protected成员,并且不能被这个派生类的子类所访问;基类的private成员是不可见的:派生类不可访问基类中的private成员。基类成员对派生类对象的可见性对派生类对象来说,基类的所有成员都是不可见的。所以,在保护继承时,基类的成员也只能由直接派生类访问,而无法再向下继承。C++支持多重继承。多重继承是一个类从多个基类派生而来的能力。派生类实际上获取了所有基类的特性。当一个类 是两个或多个基类的派生类时,派生类的构造函数必须激活所有基类的构造函数,并把相应的参数传递给它们 。

5.19 如果赋值构造函数参数不是传引用而是传值会有什么问题?

如果不是传引用,会造成栈溢出。因为如果是Foo(Foo f)的形式,实参初始化形参的时候也会调用复制构造函数,造成死循环。所以,复制构造函数一定要传引用:
Foo(Foo& f); 

5.20 如何实现只能动态分配类对象,不能定义类对象?

即只能将对象创建于堆上,不能创建于栈上。需要把构造函数和析构函数设为protected,派生类可以访问,外部无法访问。同时创建create和destroy函数,在内部调用构造和析构,用于创建和删除对象。其中create设为static,使用类名访问。
 class A{
 protected:
 	A(){};
 	~A(){};
 public:
 	static A* creat(){
 		return new A();
 	}
 	void destroy(){
 		delete this;
 	}
 };
 int main()
 {
 	A* a = A::creat();

 	a->destroy();
 }

5.21 如何实现只能在栈上创建对象?不能在堆上创建对象?

在堆上创建对象的唯一方法是使用new关键字,所以,只需要禁用new关键字就可以了。将operator new 设为私有的, 外部不可访问。
 class A
 {
 private:
 	void* operator new(size_t t){}     // 注意函数的第一个参数和返回值都是固定的
 	void operator delete(void* ptr){} // 重载了new就需要重载delete
 public:
 	A(){}
 	~A(){}
 };

5.22 必须在构造函数初始化式里进行初始化的数据成员有哪些?

  1. 常量成员,因为常量只能初始化不能赋值,所以必须放在初始化列表里面
  2. 引用类型,引用必须在定义的时候初始化,并且不能重新赋值,所以也要写在初始化列表里面
  3. 没有默认构造函数的类类型,因为使用初始化列表可以不必调用默认构造函数来初始化,而是直接调用拷贝构造函数初始化

5.23 抽象类和接口的区别?

抽象类是包含纯虚函数的类 C++中的接口是指只包含纯虚函数的抽象类,不能被实例化。 一个类可以实现多个接口(多重继承)

5.24 虚基类和虚继承,虚基指针和虚基表

虚基类是使用virtual继承的公共基类。虚继承使得在内存中只有基类成员的一份拷贝。虚继承消除了歧义,如果B,C,继承于A,A中有一个公有成员 i,D继承于B,C,此时D无法访问 i,因为会有歧义,不知道是B还是C的,此时使用虚继承可以解决,让B,C以虚继承方式继承A,这样就消除了歧义。底层实现原理:底层实现原理与编译器相关,一般通过虚基类指针实现,即各对象中只保存一份父类的对象,多继承时通过虚基类指针引用该公共对象,从而避免菱形继承中的二义性问题。 

虚基类的初始化与一般多继承的初始化在语法上是一样的,但构造函数的调用次序不同。派生类构造函数的调用次序有三个原则:
(1)虚基类的构造函数在非虚基类之前调用;
(2)若同一层次中包含多个虚基类,这些虚基类的构造函数按它们说明的次序调用;
(3)若虚基类由非虚基类派生而来,则仍先调用基类构造函数,再调用派生类的构造函数。
虚继承的派生类会增加一个隐藏成员虚基指针vbPtr指向虚基表vbTable。

5.25 构造函数和析构函数中可以调用调用虚函数吗?

可以,虚函数底层实现原理(但是最好不要在构造和析构函数中调用) 可以,但是没有动态绑定的效果,父类构造函数中调用的仍然是父类版本的函数,子类中调用的仍然是子类版本的函数。 effictive c++第九条,绝不在构造和析构过程中调用virtual,因为构造函数中的base的虚函数不会下降到derived上。而是直接调用base类的虚函数。

5.26 构造函数和析构函数调用顺序?

  1. 先调用基类构造函数
  2. 在调用成员类构造函数
  3. 最后调用本身的构造函数
  4. 析构顺序相反

5.27 动态绑定如何实现?

C++ 中,通过基类的引用或指针调用虚函数时,发生动态绑定。引用(或指针)既可以指向基类对象也可以指向派生类对象,这一事实是动态绑定的关键。用引用(或指针)调用的虚函数在运行时确定,被调用的函数是引用(或指针)所指对象的实际类型所定义的。

5.28 多态性有哪些?

多态指当不同的对象收到相同的消息时,产生不同的动作
  1. 编译时多态(静态绑定),函数重载,运算符重载,模板。
  2. 运行时多态(动态绑定),虚函数机制。

5.29 构造函数可不可以抛出异常?析构函数呢?

1. 构造函数中尽量不要抛出异常,能避免的就避免,如果必须,要考虑不要内存泄露!
2. 不要在析构函数中抛出异常!
理论上都可以抛出异常。 但析构函数最好不要抛出异常,将会导致析构不完全,从而有内存泄露。
为什么不应该在析构函数中抛出异常?
1)如果析构函数抛出异常,则异常点之后的程序不会执行,如果析构函数在异常点之后执行了某些必要的动作比如释放某些资源,则这些动作不会执行,会造成诸如内存泄漏的问题。
2)通常异常发生时,c++的机制会调用已经构造对象的析构函数来释放资源,此时若析构函数本身也抛出异常,则前一个异常尚未处理,又有新的异常,会造成程序崩溃的问题。
3)当在某一个析构函数中会有一些可能(哪怕是一点点可能)发生异常时,那么就必须要把这种可能发生的异常完全封装在析构函数内部,决不能让它抛出函数之外(这招简直是绝杀!呵呵!

5.30 成员函数调用底层机制?

 例如我们要调用Point的实例 p 的 vec3 normalize() 方法,即 p.normalize();编译器会做下面的转变:
1. 改写函数的原型,增加一个额外的参数 this 指针到参数列表的最前面: 
// 如果成员函数是非const函数,则this指针是指针常量
vec3 Point :: normalize( Point* const this); 
// 如果成员函数是const函数,则this指针是指向常量的指针常量
vec3 Point :: normalize( const Point* const this); 
2. 将函数内部对“非静态成员”的访问,改写为通过this指针访问
{
return sqrt(
this->x * this->x + 
this->y * this->y +
this->z * this->z
); 
}
3. 将成员函数重写写为一个外部函数,并修改函数名,避免名称和其他函数名冲突:
extern normalize__3PointFv(register const Point* const this); 

六、泛型编程

6.1 使用模板的优点和缺点?

优点:
  1. 在一些场景可以避免重复代码
  2. 有些问题难以使用OO技巧(如继承和多态)来实现,而使用模版会很方便
  3. template classes更加的类型安全,因其参数类型在编译时都是已知的。
缺点:
  1. 一些编译器对template支持不好。
  2. 编译器给出的有些出错信息比较晦涩。
  3. 为每种类型都生成额外的代码,可能导致生成的exe膨胀。
  4. 使用templates写的代码难以调试
  5. templates在头文件中,这样一旦有所变更需要重编译所有相关工程

6.2 模板函数和函数的对比?

  1. 模板函数由函数模板实例化而来,编译器推断模板实参,然后实例化出对应的函数定义。模板函数是函数模板的实例。
  2. 普通函数需要程序员手动重载才能实现对于不同类型参数的支持。
  3. 函数模板只能用于函数的参数个数相同而类型不同的情况,如果参数个数不同,则不能使用函数模板,只能使用重载。
  4. 函数模板必须要求所有实参的类型T都相同,无法进行隐式类型转换。
  5. 进行函数调用时,编译器优先选择匹配的非模板函数,如果找不到再试着进行函数模板的实例化,如果还不行,则这个调用违法。这样做可以减少函数模板实例化次数,提高效率。

6.3 模板的全特化和偏特化?

什么是特化?
所谓特化,就是将泛型的东东搞得具体化一些,从字面上来解释,就是为已有的模板参数进行一些使其特殊化的指定,使得以前不受任何约束的模板参数,或受到特定的修饰(例如const或者摇身一变成为了指针之类的东东,甚至是经过别的模板类包装之后的模板类型)或完全被指定了下来。

模板有两种特化,全特化和偏特化(局部特化) 模板函数只能全特化,没有偏特化(以后可能有)。 模板类是可以全特化和偏特化的。 全特化,就是模板中模板参数全被指定为确定的类型。 全特化也就是定义了一个全新的类型,全特化的类中的函数可以与模板类不一样。 偏特化,就是模板中的模板参数没有被全部确定,需要编译器在编译时进行确定。 在类型上加上const、&、*( cosnt int、int&、int*、等等)并没有产生新的类型。只是类型被修饰了。模板在编译时,可以得到这些修饰信息。

模板为什么要特化,因为编译器认为,对于特定的类型,如果你能对某一功能更好的实现,那么就该听你的。

模板分为类模板与函数模板,特化分为全特化与偏特化。全特化就是限定死模板实现的具体类型,偏特化就是如果这个模板有多个类型,那么只限定其中的一部分。

先看类模板:

[cpp] view plain copy
  1. template<typename T1, typename T2>  
  2. class Test  
  3. {  
  4. public:  
  5.     Test(T1 i,T2 j):a(i),b(j){cout<<"模板类"<<endl;}  
  6. private:  
  7.     T1 a;  
  8.     T2 b;  
  9. };  
  10.   
  11. template<>  
  12. class Test<int , char>  
  13. {  
  14. public:  
  15.     Test(int i, char j):a(i),b(j){cout<<"全特化"<<endl;}  
  16. private:  
  17.     int a;  
  18.     char b;  
  19. };  
  20.   
  21. template <typename T2>  
  22. class Test<char, T2>  
  23. {  
  24. public:  
  25.     Test(char i, T2 j):a(i),b(j){cout<<"偏特化"<<endl;}  
  26. private:  
  27.     char a;  
  28.     T2 b;  
  29. };  

那么下面3句依次调用类模板、全特化与偏特化:

[cpp] view plain copy
  1. Test<double , double> t1(0.1,0.2);  
  2. Test<int , char> t2(1,'A');  
  3. Test<charbool> t3('A',true);  

而对于函数模板,却只有全特化,不能偏特化:

[cpp] view plain copy
  1. //模板函数  
  2. template<typename T1, typename T2>  
  3. void fun(T1 a , T2 b)  
  4. {  
  5.     cout<<"模板函数"<<endl;  
  6. }  
  7.   
  8. //全特化  
  9. template<>  
  10. void fun<int ,char >(int a, char b)  
  11. {  
  12.     cout<<"全特化"<<endl;  
  13. }  
  14.   
  15. //函数不存在偏特化:下面的代码是错误的  
  16. /* 
  17. template<typename T2> 
  18. void fun<char,T2>(char a, T2 b) 
  19. { 
  20.     cout<<"偏特化"<<endl; 
  21. } 
  22. */  

注意:
  • 至于为什么函数不能偏特化,似乎不是因为语言实现不了,而是因为偏特化的功能可以通过函数的重载完成。
  • 函数模版的全特化不参与函数重载, 并且优先级低于函数基础模版参与匹配,也就是说,匹配的顺序是:
1. 非模板函数
2. 某个没有进行全特化的template function
3. 如果这个没有进行全特化的template function有全特化版本,并且类型也比较匹配,则选择这个全特化版本

七、内存管理

7.1 new与malloc的区别,delet和free的区别?内部实现?

new 与 malloc的区别:
  1. new 是运算符,malloc是库函数
  2. new会调用构造函数,malloc只申请内存
  3. new返回指定类型的指针,malloc返回void指针
  4. new自动计算所需的内存大小,malloc需要手动设置空间
  5. new可以被重载
new的内部实现:

delete 和 free 的区别: 
  1. delete 是运算符,free是库函数
  2. delete会调用析构函数,free是会释放内存
  3. 使用free之前要检查指针是否为空指针,delete不需要,对空指针delete没有问题
  4. free 和 delete 不能混用,也就是说new 分配的内存空间最好不要使用使用free 来释放,malloc 分配的空间也不要使用 delete来释放
delete的内部实现:

7.2 malloc, calloc, realloc, 和 alloca 申请内存的区别?

  1. calloc 是申请N个大小为S的空间,且会初始化空间值为0;malloc不会初始化,是随机的垃圾数据(在VS Debug模式下,会是0xcccccc这种特殊值,为了调试方便)
  2. malloc 是在堆上申请大小为S的一个空间,但不会初始化
  3. realloc 是将原本分配的内存扩充到新的大小,要求新的大小必须大于原大小
  4. alloca 是在栈上申请空间,不需要(不能)使用free,运行到作用域以外的时候释放申请的空间

7.3 内存泄漏(内存溢出)有哪些因素?

  1. 在类的构造函数和析构函数中没有匹配的调用new和delete函数 两种情况下会出现这种内存泄露:一是在堆里创建了对象占用了内存,但是没有显示地释放对象占用的内 存;二是在类的构造函数中动态的分配了内存,但是在析构函数中没有释放内存或者没有正确的释放内存
  2. 没有正确地清除嵌套的对象指针
  3. 在释放对象数组时在delete中没有使用方括号
  4. 指向对象的指针数组不等同于对象数组 对象数组是指:数组中存放的是对象,只需要delete []p,即可调用对象数组中的每个对象的析构函数释放空间 指向对象的指针数组是指:数组中存放的是指向对象的指针,不仅要释放每个对象的空间,还要释放每个指针的空间,delete []p只是释放了每个指针,但是并没有释放对象的空间,正确的做法,是通过一个循环,将每个对象释放了,然后再把指针释放了
  5. 缺少拷贝构造函数
  6. 两次释放相同的内存是一种错误的做法,同时可能会造成堆的奔溃。 按值传递会调用(拷贝)构造函数,引用传递不会调用。 在C++中,如果没有定义拷贝构造函数,那么编译器就会调用默认的拷贝构造函数,会逐个成员拷贝的方式来复制数据成员,如果是以逐个成员拷贝的方式来复制指针被定义为将一个变量的地址赋给另一个变量。这种隐式的指针复制结果就是两个对象拥有指向同一个动态分配的内存空间的指针。当释放第一个对象的时候,它的析构函数就会释放与该对象有关的动态分配的内存空间。而释放第二个对象的时候,它的析构函数会释放相同的内存,这样是错误的。 所以,如果一个类里面有指针成员变量,要么必须显示的写拷贝构造函数和重载赋值运算符,要么禁用拷贝构造函数和重载赋值运算符
  7. 没有将基类的析构函数定义为虚函数
  8. 指针的值被篡改,导致丧失了对内存的访问方式,无法释放申请的内存

7.4 C++内存模型(堆、栈、静态区)

C++内存分为5个区域:
  1. 堆 heap :
    由new分配的内存块,其释放编译器不去管,由我们程序自己控制(一个new对应一个delete)。如果程序员没有释放掉,在程序结束时OS会自动回收。涉及的问题:“缓冲区溢出”、“内存泄露”

  2. 栈 stack :
    是那些编译器在需要时分配,在不需要时自动清除的存储区。存放局部变量、函数参数。存放在栈中的数据只在当前函数及下一层函数中有效,一旦函数返回了,这些数据也就自动释放了。函数栈内的变量地址总是连续的,从高地址向低地址生长。

  3. 全局/静态存储区 (.bss段和.data段) :
    全局和静态变量被分配到同一块内存中。在C语言中,未初始化的静态变量放在.bss段中,初始化的放在.data段中;在C++里则不区分了。

  4. 常量存储区 (.rodata段) :
    存放常量,不允许修改(通过非正当手段也可以修改)

  5. 代码区 (.text段) :
    存放代码(如函数),不允许修改(类似常量存储区),但可以执行(不同于常量存储区)

根据c/c++对象生命周期不同,c/c++的内存模型有三种不同的内存区域,即

  1. 自由存储区(栈区):局部非静态变量的存储区域,即平常所说的栈
  2. 动态存储区(堆区): 用operator new ,malloc分配的内存,即平常所说的堆
  3. 静态存储区:全局变量 静态变量 字符串常量存在位置
注意:
  1. 栈区变量要注意析构函数的调用次序,由于是先进后出,则先创建的对象,最后被析构。

7.5 存储说明符(存储方案)有哪些?

7个存储说明符:
  1. auto (C++11去掉),存放在栈区的自动变量
  2. register 存放在寄存器的自动变量
  3. static 存放在静态区的静态变量
  4. extern 声明在外部定义的全局变量
  5. mutable 即使对象声明为了const, mutable成员也可以被修改
  6. volatile 声明不将变量放入寄存器,而是每次访问都从内存中取值,保证每次的值都是最新的
  7. thread_local 在整个线程周期存在的静态变量

7.6 堆与栈的区别?

  1. 堆是先进先出,栈是先进后出。
  2. 栈的大小固定,受限于系统中有效的虚拟内存,可能会发生栈溢出;堆可以动态生长
  3. 栈的空间有系统释放,堆内存由程序员释放
  4. 堆容易产生碎片
  5. 申请方式上,栈是系统自动分配,堆是由程序员申请

7.7 内存对齐

为什么需要内存对齐?
1)平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据,某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常
2)硬件原因:经过内存对齐之后,CPU的内存访问速度大大提升。

图一:

我们普通程序员心中的内存印象,由一个个字节组成,但是CPU却不是这么看待的

图二:

cpu把内存当成是一块一块的,块的大小可以是2,4,8,16 个字节,因此CPU在读取内存的时候是一块一块进行读取的,块的大小称为(memory granularity)内存读取粒度。

我们再来看看为什么内存不对齐会影响读取速度?

    假设CPU要读取一个4字节大小的数据到寄存器中(假设内存读取粒度是4),分两种情况讨论:

           1.数据从0字节开始

        2.数据从1字节开始

解析:当数据从0字节开始的时候,直接将0-3四个字节完全读取到寄存器,结算完成了。

        当数据从1字节开始的时候,问题很复杂,首先先将前4个字节读到寄存器,并再次读取4-7字节的数据进寄存器,接着把0字节,4,6,7字节的数据剔除,最后合并1,2,3,4字节的数据进寄存器,对一个内存未对齐的寄存器进行了这么多额外操作,大大降低了CPU的性能。

     但是这还属于乐观情况,上文提到内存对齐的作用之一是平台的移植原因,因为只有部分CPU肯干,其他部分CPU遇到未对齐边界就直接罢工了。

内存对齐的三个原则:
  1. 对于结构的各个成员,第一个成员位于偏移为0的位置,以后的每个数据成员的偏移量必须是 这个数据成员的自身长度(或者可以自己设置)的倍数。
  2. 结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储
  3. 结构体的总大小,也就是sizeof的结果,.必须是其内部最大成员的整数倍.不足的要补齐
	typedef struct A{
		int a;//0~4
		double b;//根据规则一,偏移量应该为sizeof(double)的倍数;8~15
		char c;本来应该16~17但是根据规则三,最后补位16~23
	}A;//所以A的大小应该为24
	
	struct B{
		int id;0~4
		A a;//规矩规则二,应该为8~31;
	};
	//所以最后的大小应该为32

7.8 memcpy 和 memmove的区别

void *memcpy(void *dst, const void *src, size_t count);
void *memmove(void *dst, const void *src, size_t count);
memcpy和memmove()都是C语言中的库函数,在头文件string.h中,作用是拷贝一定长度的内存的内容。他们的作用是一样的,唯一的区别是,当内存发生局部重叠的时候,memmove保证拷贝的结果是正确的,memcpy不保证拷贝的结果的正确。在内存覆盖情况下, memcpy会报错。

7.9 动态内存管理

动态内存管理是指管理动态内存,即堆内存。动态内存管理中常见的问题有(发生段错误的可能原因):
  • 1. 野指针:一些内存单元已经释放,但之前指向它的指针还在使用。
  • 2. 重复释放:程序试图释放已经被释放过的内存单元。
  • 3. 内存泄漏:没有释放不再使用的内存单元。
  • 4. 缓冲区溢出:数组越界。
  • 5. 不配对的new[]/delete
针对1~3的问题,C++11提供了只能指针解决。此三种智能指针(unique_ptr、shared_ptr及weak_ptr)使用时,需要包含头文件:<memory>。

7.10 析构函数会在什么时候被调用?

1) 变量在离开其作用域时被销毁
2) 当一个对象被销毁时,其成员被销毁
3) 容器被销毁时,其元素被销毁
4) 对于动态分配的对象,当对指向它的指针应用delete运算符时被销毁
5) 对于临时对象,当创建它的完整表达式结束时被销毁

7.11 什么是栈溢出?

栈溢出就是缓冲区溢出的一种。栈溢出就是不顾堆栈中数据块大小,向该数据块写入了过多的数据,导致数据越界,结果覆盖了老的栈数据。栈是从高地址向低地址方向增涨,堆的方向相反。在一次函数调用中,栈中将被依次压入:形参,返回地址,EBP(调用地址)。如果函数有局部变量,接下来,就在栈中开辟相应的空间以构造变量。如果这些值的大小超过了函数栈的最大容量(默认的栈大小为1MB)就会造成栈溢出。因为栈一般默认为1-2m,一旦出现死循环或者是大量的递归调用,在不断的压栈过程中,造成栈容量超过1m而导致溢出。
由于缓冲区溢出而使得有用的存储单元被改写,往往会引发不可预料的后果。向这些单元写入任意的数据,一般只会导致程序崩溃之类的事故,对这种情况我们也至多说这个程序有bug。但如果向这些单元写入的是精心准备好的数据,就可能使得程序流程被劫持,致使不希望的代码被执行,落入攻击者的掌控之中,这就不仅仅是bug,而是漏洞(exploit)了。
解决方案?
(1)用栈将递归改写为非递归
(2)使用静态变量或者动态变量替代自动变量
(3)增大函数栈的大小
#include <stdio.h>
#include <stdlib.h>
void foo()
{
    printf("foo()\n");
    exit(0);
}
void call()
{
    int buffer[2];
    buffer[3] = (int)foo; // 缓冲区溢出

}
int main(void)
{
    call();
}

八、编译和链接?

8.1 动态链接库和静态链接库的区别?


用今天的眼光来看,动态链接库节约内存这个优点越来越不重要了,特别是在服务器上:a)、现在PC的内存都很大,指令占用那一点空间已经微不足道了;b)、由于动态链接库冲突等问题,越来越多的服务器应用更倾向于把所有用到的环境文件用docker打包,这样一来占用内存也不少。

相关问题:

8.2 链接指示 extern "C"有什么作用?

extern "C"的主要作用就是为了能够正确实现C++代码调用其他C语言代码。加上extern "C"后,会指示编译器这部分代码按C语言的进行编译,而不是C++的。由于C++支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名;而C语言并不支持函数重载,因此编译C语言代码的函数时不会带上函数的参数类型,一般之包括函数名。
这个功能十分有用处,因为在C++出现以前,很多代码都是C语言写的,而且很底层的库也是C语言写的,为了更好的支持原来的C代码和已经写好的C语言库,需要在C++中尽可能的支持C,而extern "C"就是其中的一个策略。
这个功能主要用在下面的情况:
1、C++代码调用C语言代码
2、在C++的头文件中使用
3、在多个人协同开发时,可能有的人比较擅长C语言,而有的人擅长C++,这样的情况下也会有用到

8.3 现代编译器的编译过程?

  1. 预编译,展开所有的宏定义#define, 处理所有的预编译指令如#if,递归的包含文件#include,删除所有注释,添加行号和文件标识,保留所有的编译指令#pragma。
  2. 编译
    1. 词法分析
    2. 语法分析
    3. 语义分析
    4. 优化生成汇编代码
  3. 汇编,将汇编代码转化成机器可以执行的指令,得到目标文件(.o \ .obj),
  4. 连接,链接将目标文件进行处理,得到可执行文件。

8.4 pdb文件有什么用?

Visual Studio 调试需要pdb文件。Native C++ PDB包含了如下的信息:
* public,private 和static函数地址;
* 全局变量的名字和地址;
* 参数和局部变量的名字和在堆栈的偏移量;
* class,structure 和数据的类型定义;
* Frame Pointer Omission 数据,用来在x86上的native堆栈的遍历;
* 源代码文件的名字和行数;

九、实现函数和类

9.1 char *strcpy(char *dst, const char *src);

http://blog.csdn.net/yangquanhui1991/article/details/51804600
char *strcpy(char *dst, const char *src);
返回dst的原始值使函数能够支持链式表达式:strlen(strcpy(strA,strB)); 假如考虑dst和src内存重叠的情况,strcpy该怎么实现 char s[10]="hello"; strcpy(s, s+1); //应返回ello, strcpy(s+1, s);
//应返回hhello,但实际会报错,因为dst与src重叠了,把'\0'覆盖了
//
//C语言标准库函数strcpy的一种典型的工业级的最简实现。
 
//返回值:目标串的地址。
 
//对于出现异常的情况ANSI-C99标准并未定义,故由实现者决定返回值,通常为NULL。
 
//参数:des为目标字符串,source为原字符串。
 
 
 
char* strcpy(char* des,const char* source)
 
{
 
 char* r=des;
   
  assert((des != NULL) && (source != NULL));
 
 while((*r++ = *source++)!='\0');
 
 return des;
 
}
//while((*des++=*source++));的解释:赋值表达式返回左操作数,所以在赋值'\0'后,循环停止。

9.2 string类

主要的数据成员是 char* data和 int size; 
#include <iostream>
#include <cstring>
using namespace std;

class String {
public:
    // 默认构造函数
    String(const char* str = NULL);
    // 复制构造函数
    String(const String &str);
    // 析构函数
    ~String();
    // 字符串连接
    String operator+(const String & str);
    // 字符串赋值
    String & operator=(const String &str);
    // 字符串赋值
    String & operator=(const char* str);
    // 判断是否字符串相等
    bool operator==(const String &str);
    // 获取字符串长度
    int length();
    // 求子字符串[start,start+n-1]
    String substr(int start, int n);
    // 重载输出
    friend ostream & operator<<(ostream &o, const String &str);
private:
    char* data;
    int size;
};
// 构造函数
String::String(const char *str) {
    if (str == NULL) {
        data = new char[1];
        data[0] = '\0';
        size = 0;
    }//if
    else {
        size = strlen(str);
        data = new char[size + 1];
        strcpy(data, str);
    }//else
}
// 复制构造函数
String::String(const String &str) {
    size = str.size;
    data = new char[size + 1];
    strcpy(data, str.data);
}
// 析构函数
String::~String() {
    delete[] data;
}
// 字符串连接
String String::operator+(const String &str) {
    String newStr;
    //释放原有空间
    delete[] newStr.data;
    newStr.size = size + str.size;
    newStr.data = new char[newStr.size + 1];
    strcpy(newStr.data, data);
    strcpy(newStr.data + size, str.data);
    return newStr;
}
// 字符串赋值
String & String::operator=(const String &str) {
    if (data == str.data) { // 注意要先判断是否是自己给自己赋值
        return *this;
    }//if
    delete[] data;
    size = str.size;
    data = new char[size + 1];
    strcpy(data, str.data);
    return *this;
}
// 字符串赋值
String& String::operator=(const char* str) {
    if (data == str) {
        return *this;
    }//if
    delete[] data;
    size = strlen(str);
    data = new char[size + 1];
    strcpy(data, str);
    return *this;
}
// 判断是否字符串相等
bool String::operator==(const String &str) {
    return strcmp(data, str.data) == 0;
}
// 获取字符串长度
int String::length() {
    return size;
}
// 求子字符串[start,start+n-1]
String String::substr(int start, int n) {
    String newStr;
    // 释放原有内存
    delete[] newStr.data;
    // 重新申请内存
    newStr.data = new char[n + 1];
    for (int i = 0; i < n; ++i) {
        newStr.data[i] = data[start + i];
    }//for
    newStr.data[n] = '\0';
    newStr.size = n;
    return newStr;
}
// 重载输出
ostream & operator<<(ostream &o, const String &str) {
    o << str.data;
    return o;
}

int main() {
    String str1("hello ");
    String str2 = "world";
    String str3 = str1 + str2;
    cout << "str1->" << str1 << " size->" << str1.length() << endl;
    cout << "str2->" << str2 << " size->" << str2.length() << endl;
    cout << "str3->" << str3 << " size->" << str3.length() << endl;

    String str4("helloworld");
    if (str3 == str4) {
        cout << str3 << " 和 " << str4 << " 是一样的" << endl;
    }//if
    else {
        cout << str3 << " 和 " << str4 << " 是不一样的" << endl;
    }

    cout << str3.substr(6, 5) << " size->" << str3.substr(6, 5).length() << endl;
    return 0;
}

9.3 void* memcpy(void* dst, const void* src, size_t n)

memcpy将源移动n个字节到目标,但是源和目的的内存区域不能重叠,不保证内存覆盖时移动正确
void* my_memcpy(void* dst, const void* src, size_t n)  
{  
    char *tmp = (char*)dst;  
    char *s_src = (char*)src;  
  
    while(n--) {  
        *tmp++ = *s_src++;  
    }  
    return dst;  
}  

9.4 void* memmove(void* dst, const void* src, size_t n)

memmove保证内存覆盖时移动正确
void* my_memmove(void* dst, const void* src, size_t n)  
{  
    char* s_dst;  
    char* s_src;  
    s_dst = (char*)dst;  
    s_src = (char*)src;  
    if(s_dst>s_src && (s_src+n>s_dst)) {      //-------------------------第二种内存覆盖的情形。  
        s_dst = s_dst+n-1;  
        s_src = s_src+n-1;  
        while(n--) {  
            *s_dst-- = *s_src--;  
        }  
    }else {  
        while(n--) {  
            *s_dst++ = *s_src++;  
        }  
    }  
    return dst;  
}  



展开阅读全文

没有更多推荐了,返回首页