C++ 复习要点
来源:互联网 发布:美国 博士 知乎 编辑:程序博客网 时间:2024/06/05 18:54
本文总结一下C++面试时常遇到的问题。C++面试中,主要涉及的考点有
- 关键字极其用法,常考的关键字有const, sizeof, typedef, inline, static, extern, new, delete等等
- 语法问题
- 类型转换
- 指针以及指针和引用的区别
- 面向对象的相关问题,如虚函数机制等
- 泛型编程的相关问题,如模板和函数的区别等
- 内存管理,如字节对齐(内存对齐)、动态内存管理、内存泄漏等
- 编译和链接
- 实现函数和类
零、序章
0.1 C++与C的对比
- C++有三种编程方式:过程性,面向对象,泛型编程。
- C++函数符号由 函数名+参数类型 组成,C只有函数名。所以,C没有函数重载的概念。
- C++ 在 C的基础上增加了封装、继承、多态的概念
- C++增加了泛型编程
- C++增加了异常处理,C没有异常处理
- C++增加了bool型
- C++允许无名的函数形参(如果这个形参没有被用到的话)
- C允许main函数调用自己
- C++支持默认参数,C不支持
- C语言中,局部变量必须在函数开头定义,不允许类似for(int a = 0; ;;)这种定义方法。
- C++增加了引用
- C允许变长数组,C++不允许
- C中函数原型可选,C++中在调用之前必须声明函数原型
- C++增加了STL标准模板库来支持数据结构和算法
一、重要的关键字极其用法
1.1 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;
- const 成员方法本质上是使得this指针是指向const对象的指针,所以在const方法内,
- 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/
- const 指针、指向const的指针和指向const的const指针,涉及到const的特性“const左效、最左右效”
- const 全局变量有内部链接性,即不同的文件可以定义不同的同名const全局变量,使用extern定义可以消除内部链接性,称为类似全局变量,如extern const int a = 10.另一个文件使用extern const int a; 来引用。而且编译器会在编译时,将const变量替换为它的值,类似define那样。
const 常量和define 的区别
- const常量有数据类型,而宏定义没有数据类型。编译器可以对前者进行类型安全检查,而对后者只进行字符替换,没有类型安全检查,并且在字符替换中可能会产生意想不到的错误(边际效应)。
- 有些集成化的调试工具可以对const常量进行调试,但是不能对宏定义进行调试。
- 在C++程序中只使用const常量而不使用宏常量,即const常量完全取代宏常量。
- 内存空间的分配上。define进行宏定义的时候,不会分配内存空间,编译时会在main函数里进行替换,只是单纯的替换,不会进行任何检查,比如类型,语句结构等,即宏定义常量只是纯粹的置放关系,如#define null 0;编译器在遇到null时总是用0代替null它没有数据类型.而const定义的常量具有数据类型,定义数据类型的常量便于编译器进行数据检查,使程序可能出现错误进行排查,所以const与define之间的区别在于const定义常量排除了程序之间的不安全性.
- const常量存在于程序的数据段,#define常量存在于程序的代码段
- const常量存在“常量折叠”,在编译器进行语法分析的时候,将常量表达式计算求值,并用求得的值来替换表达式,放入常量表,可以算作一种编译优化。因为编译器在优化的过程中,会把碰见的const全部以内容替换掉,类似宏。
1.2 sizeof
- sizeof关键字不会计算表达式的值,而只会根据类型推断大小。
- sizeof() 的括号可以省略, 如 sizeof a ;
- 类A的大小是 所有非静态成员变量大小之和+虚函数指针大小
1.3 static
- 该变量在全局数据区分配内存;
- 未经初始化的静态全局变量会被程序自动初始化为0(自动变量的值是随机的,除非它被显式初始化);
- 静态全局变量在声明它的整个文件都是可见的,而在文件之外是不可见的;
- 该变量在全局数据区分配内存;
- 静态局部变量在程序执行到该对象的声明处时被首次初始化,即以后的函数调用不再进行初始化;
- 静态局部变量一般在声明处初始化,如果没有显式初始化,会被程序自动初始化为0;
- 它始终驻留在全局数据区,直到程序运行结束。但其作用域为局部作用域,当定义它的函数或语句块结束时,其作用域随之结束;
- 静态数据成员没有进入程序的全局名字空间,因此不存在与程序中其它全局名字冲突的可能性;
- 可以实现信息隐藏。静态数据成员可以是private成员,而全局变量不能;
1.4 typedef
与宏定义的对比
- #define 在预处理阶段进行简单替换,不做类型检查; typedef在编译阶段处理,在作用域内给类型一个别名。
- typedef 是一个语句,结尾有分号;#define是一个宏指令,结尾没有分号
- typedef int* pInt; 和 #define pInt int* 不等价,前者定义 pInt a, b;会定义两个指针,后者是一个指针,一个int。
不能声明为inline的函数
- 包含了递归、循环等结构的函数一般不会被内联。
- 虚拟函数一般不会内联,但是如果编译器能在编译时确定具体的调用函数,那么仍然会就地展开该函数。
- 如果通过函数指针调用内联函数,那么该函数将不会内联而是通过call进行调用。
- 构造和析构函数一般会生成大量代码,因此一般也不适合内联。
- 如果内联函数调用了其他函数也不会被内联。
1.5 inline
与宏函数的对比
- 内联函数在运行时可调试,而宏定义不可以;
- 编译器会对内联函数的参数类型做安全检查或自动类型转换(同普通函数),而宏定义则不会;
- 内联函数可以访问类的成员变量,宏定义则不能;
- 在类中声明同时定义的成员函数,自动转化为内联函数
- 宏只是预定义的函数,在编译阶段不进行类型安全性检查,在编译的时候将对应函数用宏命令替换。对程序性能无影响
1.6 static const \ const \ static
static const 数据成员可以在类内初始化 也可以在类外,不能在构造函数中初始化,也不能在构造函数的初始化列表中初始化
2. static
static数据成员只能在类外,即类的实现文件中初始化,也不能在构造函数中初始化,不能在构造函数的初始化列表中初始化;
3. const
const数据成员只能在构造函数的初始化列表中初始化;
1.7 explicit
1.8 extern
二、语法问题
2.1 a++ 与 ++a的区别
- a++ 返回加之前的值,++a返回加之后的a变量
- a++返回的是一个临时变量,是右值,无法赋值;++a返回的是变量a,是左值
2.2 switch语句
2.3 函数调用过程
- +++++++++ 入栈 ++++++++++++
- 将实参从右向左压入栈
- 压入返回地址
- 压入主调函数的基地址
- 跳到被调用函数的地址,执行函数代码,局部变量按声明顺序依次压入栈
- 将返回值放入寄存器eax(累加器)中
- +++++++++ 出栈 ++++++++++++
- 局部变量全部出栈
- 返回地址出栈,找到原执行地址
- 形参出栈
- 赋值操作将寄存器中的返回值赋给左值(如果有的话)
2.4 左值与右值
2.5 C语言标识符
2.6 全局变量的优缺点
(1)可以减少变量的个数
(2)全局变量破坏了函数的封装性能。前面的章节曾经讲过,函数象一个黑匣子,一般是通过函数参数和返回值进行输入输出,函数内部实现相对独立。但函数中 如果使用了全局变量,那么函数体内的语句就可以绕过函数参数和返回值进行存取,这种情况破坏了函数的独立性,使函数对全局变量产生依赖。同时,也降低了该 函数的可移植性。
(3)全局变量使函数的代码可读性降低。由于多个函数都可能使用全局变量,函数执行时全局变量的值可能随时发生变化,对于程序的查错和调试都非常不利。
2.7 复合类型有哪些?
2.8 运算符优先级和结合性?
优先级有15种。记忆方法如下:
记住一个最高的:构造类型的元素或成员以及小括号。
记住一个最低的:逗号运算符。
剩余的是一、二、三、赋值。
意思是单目、双目、三目和赋值运算符。
在诸多运算符中,又分为:
算术、关系、逻辑。
两种位操作运算符中,移位运算符在算术运算符后边,逻辑位运算符在逻辑运算符的前面。再细分如下:
算术运算符分 *,/,%高于+,-。
关系运算符中,〉,〉=,<,<=高于==,!=。
逻辑运算符中,除了逻辑求反(!)是单目外,逻辑与(&&)高于逻辑或(||)。
逻辑位运算符中,除了逻辑按位求反(~)外,按位与(&)高于按位半加(^),高于按位或(|)。
这样就将15种优先级都记住了,再将记忆方法总结如下:
去掉一个最高的,去掉一个最低的,剩下的是一、二、三、赋值。双目运算符中,顺序为 算术、移位、关系(>,<,==)、逻辑位和逻辑(&& ||)。
2.9 using 声明和using 编译指令的区别?哪个更好?
2.10 for循环的效率问题
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 四种类型强制转换
- dynamic_cast:该转换符用于将一个指向派生类的基类指针或引用转换为派生类的指针或引用。
- const_cast:最常用的用途就是删除const属性。
- static_cast:static_cast本质上是传统c语言强制转换的替代品,比C类型转换更严格, 该操作符用于非多态类型的转换,任何标准转换都可以使用他,即static_cast可以把int转换为double,但不能把两个不相关的类对象进行转换,比如类A不能转换为一个不相关的类B类型。static_cast在类对象和基础类型转换中,会调用类的构造函数,和类型转换运算符比如operator int(),来进行显示转换。
- reinterpret_cast:该操作符用于将一种类型转换为另一种不同的类型,比如可以把一个整型转换为一个指针,或把一个指针转换为一个整型,因此使用该操作符的危险性较高,一般不应使用该操作符。
四、指针
4.1 指针与引用的区别
- 指针是一个变量,引用只是别名
- 指针需要解引用才能访问对象,引用不需要
- 引用在定义时必须初始化,且以后不可转移引用的对象,指针可以
- 引用没有const,即int& const a ;没有;而指针有const指针,即int* const ptr;
- 引用不可以为空;而指针可以
- 指针变量需要分配栈空间;而引用不需要,仅仅是个别名
- sizeof(引用)得到对应对象的大小;sizeof(指针)得到指针大小
- 指针加法和引用加法不一样
- 引用不需要释放内存空间,在编译时就会优化掉
4.2 指针与数组名的区别
- 数组名不是指针,对数组名取地址,得到整个数组的地址
- 数组名 + 1会跳过整个数组的大小,指针+1只会跳过一个元素的大小
- 数组名作为函数参数传递时,退化为指针
- sizeof(数组名)返回整个数组的大小,sizeof(指针)返回指针大小
- 数组名无法修改值,是常量
- int (*p)[] = &arr; 才是正确的数组指针写法
4.3 野指针、空指针的概念
- 野指针是指指向无效内存的指针,不能对野指针取内容,delete
- 空指针是指置为0\NULL\nullptr的指针,可以对空指针delete多次
五、面向对象
5.1 面向对象的三大特性
- 封装:封装是实现面向对象程序设计的第一步,封装就是将数据或函数等集合在一个个的单元中(我们称之为类)。封装的意义在于保护或者防止代码(数据)被我们无意中破坏。
- 继承:继承主要实现重用代码,节省开发时间。子类可以继承父类的一些东西。
- 多态:同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果。分为编译时多态和运行时多态。
5.2 函数重载和运算符重载
问:函数重载的依据?
- 参数个数
- 参数类型
- const方法与非const方法构成重载
问:运算符重载的限制?
- 被重载的运算符,至少有一个操作数是用户自定义类型,也就是说不能重载C++语言的标准运算
- 重载的运算符的句法规则不可以改变,操作数、结合性和优先级无法更改。以前是几元现在就是几元;该是左结合还是左结合;优先级无法更改。
- 不能自定义运算符,不能创建新的运算符。
- 不能重载的运算符有:
- 成员访问运算符 .
- 成员指针运算符 .*
- 作用域解析运算符 ::
- 条件运算符 ?:
- sizeof
- typeid
- 四个类型转换运算符
- const_cast
- static_cast
- dynamic_cast
- reinterpret_cast
- 只能通过成员函数重载,而不能通过友元重载的运算符:
- 赋值运算符 =
- 函数调用运算符 ()
- 下标运算符 []
- 通过指针访问成员运算符 ->
- 只能通过友元重载,不能通过成员函数重载的情况:
- 双目运算符最好用友元重载,单目运算符最好用成员函数重载
- 若运算符所需的操作数(尤其是第一个操作数)希望有隐式类型转换,则只能选用友元函数
- 左操作数是不同类的对象或者内部类型,比如ostream, istream, int, float等
- 当需要重载运算符具有可交换性时,选择重载为友元函数
- 对返回类型没有限制,可以是void或者其他类型
- 重载一元运算符需要注意,由于一元运算符没有参数,前缀和后缀无法区分,所以需要加一个哑元(dummy),哑元永远用不上,如果有哑元,则是后缀形式,否则,就是前缀。
5.3 哪些成员无法被继承?
- 无法被继承的有
- 构造函数
- 析构函数
- 赋值运算符
- 友元函数
- 可以被继承的有
- 静态成员
- 静态方法
- 非静态成员
- 非静态方法(无论是private\public\protected,只是private的继承了也无法访问)
- 虚表指针
5.4 定义默认构造函数的两种方法?
- 给已有的构造函数中的一个的所有参数加上默认值
- 通过方法重载定义一个无参数构造函数
- 隐式调用默认构造函数不要加括号(), 会被编译器解释为函数声明。
5.5 调用非默认构造函数的三种方法?
- Foo f(...); // 隐式调用
- Foo f = Foo(...) ;// 显式调用
- Foo* f = new Foo(); // 显式调用
5.6 由编译器生成的6个成员函数?
- 默认构造函数
- 析构函数
- 复制构造函数
- 赋值运算符
- 取地址运算符
- 取地址运算符 const版本
5.7 友元的三种实现方式
- 友元函数
- 友元类
- 友元成员函数
5.8 为什么基类的析构函数为什么要声明为虚函数?
5.9 为什么构造函数不可以是虚函数?
- 虚函数在运行期决定函数调用,而在构造一个对象时,由于对象还未构造成功,编译器无法确定对象的实际类型,继而无法决定调用哪一个构造函数。
- 虚函数的执行依赖于虚函数表,而虚函数表在构造函数中进行初始化工作,即初始化 vptr,让它指向正确的虚函数表,而在构造期间,虚函数表还没有初始化,所以无法决定调用哪个构造函数。
5.10 析构函数什么时候声明为私有?什么时候不能声明为私有?
- 私有析构函数可以使得对象只在堆上构造。在栈上创建的对象要求构造函数和析构函数必须都是公有的,否则编译器报错“析构函数不可访问”;而堆对象由程序员创建和删除,可以把析构函数声明为私有的。由于delete会调用析构函数,而私有的析构无法被访问,编译器报错,此时通过增加一个destroy()方法,在方法内调用析构函数来释放对象:
- void destroy()
- {
- delete this;
- }
- 析构函数不能声明为私有的情况:基类的析构函数不能声明为私有,因为要在派生类的析构函数中被隐式调用。
5.11 构造函数什么时候声明为私有?什么时候不能声明为私有?
- 单例模式。
- 基类的构造函数不能声明为私有,因为要在派生类的构造函数中被隐式调用。如果在派生类的构造函数中没有显式调用基类的构造,则会调用基类的默认构造函数。
5.12 不能声明为虚函数的成员函数
构造函数:
首先明确一点,在编译期间编译器完成了虚表的创建,而虚指针在构造函数期间被初始化。
如果构造函数是虚函数,那必然需要通过虚指针来找到虚构造函数的入口地址,但是这个时候我们还没有把虚指针初始化。因此,构造函数不能是虚函数。
內联函数:
编译期內联函数在调用处被展开,而虚函数在运行时才能被确定具体调用哪个类的虚函数。內联函数体现的是编译期机制,而虚函数体现的是运行期机制。
静态成员函数:
静态成员函数和类有关,即使没有生成一个实例对象,也可以调用类的静态成员函数。而虚函数的调用和虚指针有关,虚指针存在于一个类的实例对象中,如果静态成员函数被声明成虚函数,那么调用成员静态函数时又如何访问虚指针呢。总之可以这么理解,静态成员函数与类有关,而虚函数与类的实例对象有关。
非成员函数:
虚函数的目的是为了实现多态,多态和继承有关。所以声明一个非成员函数为虚函数没有任何意义。
5.13 虚函数机制以及内存分布
- 虚函数表指针 vfptr和虚函数表 vftable
- 虚继承下还涉及 虚基类表指针 vbptr和虚基类表 vbtable
5.14 class 与 struct的区别
- class默认的继承方式为private, struct 默认继承方式为public
- class的成员访问默认为private, struct默认为public
5.15 重载、重写(覆盖)与隐藏(重定义)的关系
- 重载。函数名相同,参数个数、类型不同,或者用const重载。是同一个类中方法之间的关系,是水平关系。
- 重写。派生类重新定义基类中有相同名称和参数的虚函数,要求参数列表必须相同。方法在基类和派生中的访问限制可以不同。
- 隐藏。派生类重新定义基类中有相同名称的函数(参数列表可以不同)会把其他基类的同名方法隐藏起来,无法被派生类调用。
5.16 哪些情况下方法可以不写定义?
- 纯虚方法
- 非虚方法
5.17 派生类可以不实现虚基类的纯虚方法,派生类也成了抽象类。
5.18 三种继承方式(public, private, protected)的区别?
- 公有继承(public): 基类成员对其对象的可见性与一般类及其对象的可见性相同,public成员可见,protected和private成员不可见,基类成员对派生类的可见性对派生类来说,基类的public和protected成员可见:基类的public成员和protected成员作为派生类的成员时,它们都保持原有状态;基类的private成员依旧是private,派生类不可访问基类中的private成员。 基类成员对派生类对象的可见性对派生类对象来说,基类的public成员是可见的,其他成员是不可见的。 所以,在公有继承时,派生类的对象可以访问基类中的public成员,派生类的成员方法可以访问基类中的public成员和protected成员。
- 私有继承(private) 基类成员对其对象的可见性与一般类及其对象的可见性相同,public成员可见,其他成员不可见,基类成员对派生类的可见性对派生类来说,基类的public成员和protected成员是可见的:基类的public成员和protected成员都作为派生类的private成员,并且不能被这个派生类的子类所访问;基类的私有成员是不可见的:派生类不可访问基类中的private成员,基类成员对派生类对象的可见性对派生类对象来说,基类的所有成员都是不可见的,所以在私有继承时,基类的成员只能由直接派生类访问,无法再往下继承。
- 保护继承(protected) 保护继承与私有继承相似,基类成员对其对象的可见性与一般类及其对象的可见性相同,public成员可见,其他成员不可见,基类成员对派生类的可见性,对派生类来说,基类的public和protected成员是可见的:基类的public成员和protected成员都作为派生类的protected成员,并且不能被这个派生类的子类所访问;基类的private成员是不可见的:派生类不可访问基类中的private成员。基类成员对派生类对象的可见性对派生类对象来说,基类的所有成员都是不可见的。所以,在保护继承时,基类的成员也只能由直接派生类访问,而无法再向下继承。C++支持多重继承。多重继承是一个类从多个基类派生而来的能力。派生类实际上获取了所有基类的特性。当一个类 是两个或多个基类的派生类时,派生类的构造函数必须激活所有基类的构造函数,并把相应的参数传递给它们 。
5.19 如果赋值构造函数参数不是传引用而是传值会有什么问题?
5.20 如何实现只能动态分配类对象,不能定义类对象?
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 如何实现只能在栈上创建对象?不能在堆上创建对象?
class A { private: void* operator new(size_t t){} // 注意函数的第一个参数和返回值都是固定的 void operator delete(void* ptr){} // 重载了new就需要重载delete public: A(){} ~A(){} };
5.22 必须在构造函数初始化式里进行初始化的数据成员有哪些?
- 常量成员,因为常量只能初始化不能赋值,所以必须放在初始化列表里面
- 引用类型,引用必须在定义的时候初始化,并且不能重新赋值,所以也要写在初始化列表里面
- 没有默认构造函数的类类型,因为使用初始化列表可以不必调用默认构造函数来初始化,而是直接调用拷贝构造函数初始化
5.23 抽象类和接口的区别?
5.24 虚基类和虚继承,虚基指针和虚基表
(2)若同一层次中包含多个虚基类,这些虚基类的构造函数按它们说明的次序调用;
(3)若虚基类由非虚基类派生而来,则仍先调用基类构造函数,再调用派生类的构造函数。
5.25 构造函数和析构函数中可以调用调用虚函数吗?
5.26 构造函数和析构函数调用顺序?
- 先调用基类构造函数
- 在调用成员类构造函数
- 最后调用本身的构造函数
- 析构顺序相反
5.27 动态绑定如何实现?
5.28 多态性有哪些?
- 编译时多态(静态绑定),函数重载,运算符重载,模板。
- 运行时多态(动态绑定),虚函数机制。
5.29 构造函数可不可以抛出异常?析构函数呢?
2. 不要在析构函数中抛出异常!
2)通常异常发生时,c++的机制会调用已经构造对象的析构函数来释放资源,此时若析构函数本身也抛出异常,则前一个异常尚未处理,又有新的异常,会造成程序崩溃的问题。
5.30 成员函数调用底层机制?
六、泛型编程
6.1 使用模板的优点和缺点?
- 在一些场景可以避免重复代码
- 有些问题难以使用OO技巧(如继承和多态)来实现,而使用模版会很方便
- template classes更加的类型安全,因其参数类型在编译时都是已知的。
- 一些编译器对template支持不好。
- 编译器给出的有些出错信息比较晦涩。
- 为每种类型都生成额外的代码,可能导致生成的exe膨胀。
- 使用templates写的代码难以调试
- templates在头文件中,这样一旦有所变更需要重编译所有相关工程
6.2 模板函数和函数的对比?
- 模板函数由函数模板实例化而来,编译器推断模板实参,然后实例化出对应的函数定义。模板函数是函数模板的实例。
- 普通函数需要程序员手动重载才能实现对于不同类型参数的支持。
- 函数模板只能用于函数的参数个数相同而类型不同的情况,如果参数个数不同,则不能使用函数模板,只能使用重载。
- 函数模板必须要求所有实参的类型T都相同,无法进行隐式类型转换。
- 进行函数调用时,编译器优先选择匹配的非模板函数,如果找不到再试着进行函数模板的实例化,如果还不行,则这个调用违法。这样做可以减少函数模板实例化次数,提高效率。
6.3 模板的全特化和偏特化?
模板为什么要特化,因为编译器认为,对于特定的类型,如果你能对某一功能更好的实现,那么就该听你的。
模板分为类模板与函数模板,特化分为全特化与偏特化。全特化就是限定死模板实现的具体类型,偏特化就是如果这个模板有多个类型,那么只限定其中的一部分。
先看类模板:
那么下面3句依次调用类模板、全特化与偏特化:
而对于函数模板,却只有全特化,不能偏特化:
注意:
- 至于为什么函数不能偏特化,似乎不是因为语言实现不了,而是因为偏特化的功能可以通过函数的重载完成。
- 函数模版的全特化不参与函数重载, 并且优先级低于函数基础模版参与匹配,也就是说,匹配的顺序是:
七、内存管理
7.1 new与malloc的区别,delet和free的区别?内部实现?
- new 是运算符,malloc是库函数
- new会调用构造函数,malloc只申请内存
- new返回指定类型的指针,malloc返回void指针
- new自动计算所需的内存大小,malloc需要手动设置空间
- new可以被重载
- delete 是运算符,free是库函数
- delete会调用析构函数,free是会释放内存
- 使用free之前要检查指针是否为空指针,delete不需要,对空指针delete没有问题
- free 和 delete 不能混用,也就是说new 分配的内存空间最好不要使用使用free 来释放,malloc 分配的空间也不要使用 delete来释放
7.2 malloc, calloc, realloc, 和 alloca 申请内存的区别?
- calloc 是申请N个大小为S的空间,且会初始化空间值为0;malloc不会初始化,是随机的垃圾数据(在VS Debug模式下,会是0xcccccc这种特殊值,为了调试方便)
- malloc 是在堆上申请大小为S的一个空间,但不会初始化
- realloc 是将原本分配的内存扩充到新的大小,要求新的大小必须大于原大小
- alloca 是在栈上申请空间,不需要(不能)使用free,运行到作用域以外的时候释放申请的空间
7.3 内存泄漏(内存溢出)有哪些因素?
- 在类的构造函数和析构函数中没有匹配的调用new和delete函数 两种情况下会出现这种内存泄露:一是在堆里创建了对象占用了内存,但是没有显示地释放对象占用的内 存;二是在类的构造函数中动态的分配了内存,但是在析构函数中没有释放内存或者没有正确的释放内存
- 没有正确地清除嵌套的对象指针
- 在释放对象数组时在delete中没有使用方括号
- 指向对象的指针数组不等同于对象数组 对象数组是指:数组中存放的是对象,只需要delete []p,即可调用对象数组中的每个对象的析构函数释放空间 指向对象的指针数组是指:数组中存放的是指向对象的指针,不仅要释放每个对象的空间,还要释放每个指针的空间,delete []p只是释放了每个指针,但是并没有释放对象的空间,正确的做法,是通过一个循环,将每个对象释放了,然后再把指针释放了
- 缺少拷贝构造函数
- 两次释放相同的内存是一种错误的做法,同时可能会造成堆的奔溃。 按值传递会调用(拷贝)构造函数,引用传递不会调用。 在C++中,如果没有定义拷贝构造函数,那么编译器就会调用默认的拷贝构造函数,会逐个成员拷贝的方式来复制数据成员,如果是以逐个成员拷贝的方式来复制指针被定义为将一个变量的地址赋给另一个变量。这种隐式的指针复制结果就是两个对象拥有指向同一个动态分配的内存空间的指针。当释放第一个对象的时候,它的析构函数就会释放与该对象有关的动态分配的内存空间。而释放第二个对象的时候,它的析构函数会释放相同的内存,这样是错误的。 所以,如果一个类里面有指针成员变量,要么必须显示的写拷贝构造函数和重载赋值运算符,要么禁用拷贝构造函数和重载赋值运算符
- 没有将基类的析构函数定义为虚函数
- 指针的值被篡改,导致丧失了对内存的访问方式,无法释放申请的内存
7.4 C++内存模型(堆、栈、静态区)
堆 heap :
由new分配的内存块,其释放编译器不去管,由我们程序自己控制(一个new对应一个delete)。如果程序员没有释放掉,在程序结束时OS会自动回收。涉及的问题:“缓冲区溢出”、“内存泄露”栈 stack :
是那些编译器在需要时分配,在不需要时自动清除的存储区。存放局部变量、函数参数。存放在栈中的数据只在当前函数及下一层函数中有效,一旦函数返回了,这些数据也就自动释放了。函数栈内的变量地址总是连续的,从高地址向低地址生长。全局/静态存储区 (.bss段和.data段) :
全局和静态变量被分配到同一块内存中。在C语言中,未初始化的静态变量放在.bss段中,初始化的放在.data段中;在C++里则不区分了。常量存储区 (.rodata段) :
存放常量,不允许修改(通过非正当手段也可以修改)代码区 (.text段) :
存放代码(如函数),不允许修改(类似常量存储区),但可以执行(不同于常量存储区)
根据c/c++对象生命周期不同,c/c++的内存模型有三种不同的内存区域,即
- 自由存储区(栈区):局部非静态变量的存储区域,即平常所说的栈
- 动态存储区(堆区): 用operator new ,malloc分配的内存,即平常所说的堆
- 静态存储区:全局变量 静态变量 字符串常量存在位置
- 栈区变量要注意析构函数的调用次序,由于是先进后出,则先创建的对象,最后被析构。
7.5 存储说明符(存储方案)有哪些?
- auto (C++11去掉),存放在栈区的自动变量
- register 存放在寄存器的自动变量
- static 存放在静态区的静态变量
- extern 声明在外部定义的全局变量
- mutable 即使对象声明为了const, mutable成员也可以被修改
- volatile 声明不将变量放入寄存器,而是每次访问都从内存中取值,保证每次的值都是最新的
- thread_local 在整个线程周期存在的静态变量
7.6 堆与栈的区别?
- 堆是先进先出,栈是先进后出。
- 栈的大小固定,受限于系统中有效的虚拟内存,可能会发生栈溢出;堆可以动态生长
- 栈的空间有系统释放,堆内存由程序员释放
- 堆容易产生碎片
- 申请方式上,栈是系统自动分配,堆是由程序员申请
7.7 内存对齐
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遇到未对齐边界就直接罢工了。
- 对于结构的各个成员,第一个成员位于偏移为0的位置,以后的每个数据成员的偏移量必须是 这个数据成员的自身长度(或者可以自己设置)的倍数。
- 结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储
- 结构体的总大小,也就是sizeof的结果,.必须是其内部最大成员的整数倍.不足的要补齐
typedef struct A{int a;//0~4double b;//根据规则一,偏移量应该为sizeof(double)的倍数;8~15char c;本来应该16~17但是根据规则三,最后补位16~23}A;//所以A的大小应该为24struct B{int id;0~4A a;//规矩规则二,应该为8~31;};//所以最后的大小应该为32
7.8 memcpy 和 memmove的区别
void *memmove(void *dst, const void *src, size_t count);
7.9 动态内存管理
- 1. 野指针:一些内存单元已经释放,但之前指向它的指针还在使用。
- 2. 重复释放:程序试图释放已经被释放过的内存单元。
- 3. 内存泄漏:没有释放不再使用的内存单元。
- 4. 缓冲区溢出:数组越界。
- 5. 不配对的new[]/delete
7.10 析构函数会在什么时候被调用?
2) 当一个对象被销毁时,其成员被销毁
3) 容器被销毁时,其元素被销毁
4) 对于动态分配的对象,当对指向它的指针应用delete运算符时被销毁
5) 对于临时对象,当创建它的完整表达式结束时被销毁
7.11 什么是栈溢出?
#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 动态链接库和静态链接库的区别?
- DLL hell http://blog.csdn.net/qwertyupoiuytr/article/details/53999586
8.2 链接指示 extern "C"有什么作用?
这个功能十分有用处,因为在C++出现以前,很多代码都是C语言写的,而且很底层的库也是C语言写的,为了更好的支持原来的C代码和已经写好的C语言库,需要在C++中尽可能的支持C,而extern "C"就是其中的一个策略。
这个功能主要用在下面的情况:
1、C++代码调用C语言代码
2、在C++的头文件中使用
3、在多个人协同开发时,可能有的人比较擅长C语言,而有的人擅长C++,这样的情况下也会有用到
8.3 现代编译器的编译过程?
- 预编译,展开所有的宏定义#define, 处理所有的预编译指令如#if,递归的包含文件#include,删除所有注释,添加行号和文件标识,保留所有的编译指令#pragma。
- 编译
- 词法分析
- 语法分析
- 语义分析
- 优化生成汇编代码
- 汇编,将汇编代码转化成机器可以执行的指令,得到目标文件(.o \ .obj),
- 连接,链接将目标文件进行处理,得到可执行文件。
8.4 pdb文件有什么用?
* 全局变量的名字和地址;
* 参数和局部变量的名字和在堆栈的偏移量;
* class,structure 和数据的类型定义;
* Frame Pointer Omission 数据,用来在x86上的native堆栈的遍历;
* 源代码文件的名字和行数;
九、实现函数和类
9.1 char *strcpy(char *dst, const char *src);
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类
#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)
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; }
- 复习C要点
- C语言复习要点
- c语言复习要点摘要
- c\c++ 复习基础要点06---联合体
- 复习要点
- c\c++复习基础要点08--c++单例模式
- c\c++复习基础要点10---智能指针
- c\c++复习基础要点12---容器vector
- c\c++复习基础要点13---容器deque
- c\c++复习基础要点14----容器list
- c\c++复习基础要点16----枚举类型
- 考研英国文学复习要点
- 《电子商务概论》复习要点
- 密码学基础复习要点
- 防病毒复习要点
- c++复习注意要点
- 数据结构复习要点
- C++ 考试复习要点
- compile spark2.0.0 with maven
- 多线程之futureTask(future,callable)实例,jdbc数据多线程查询
- Your account already has a valid iOS Distribution certificate!问题解决
- elk5.6.0 centos7 及问题
- C++ Primer Plus 编程练习 第三章
- C++ 复习要点
- javascript第六章2
- "ZUMA"(COCI#2009-2010#contest 5)
- link标签的用法及link属性大全
- Python 中 __all__ 的作用
- 安装Webtatic yum源
- 抽奖
- java.net.ProtocolException: Server redirected too many times (20)
- Linux笔记(1):VMware虚拟机 Ubuntu 16.04 安装 VMware Tools