C++面试基础知识

1 C++基础面试知识

1.1 什么是函数指针?

  1. 函数指针就是指向函数的指针变量。每一个函数都有一个入口地址,该入口地址就是函数指针所指向的地址。
    定义形式:
int func(int a);  
int (*f)(int a);  
f = &func;
  1. 典型应用:callback。我们调用别人提供的 API函数(Application Programming Interface,应用程序编程接口),称为Call;如果别人的库里面调用我们的函数,就叫Callback。

1.2 静态变量

  1. 静态变量什么时候初始化?
    对于C语言的全局和静态变量,初始化发生在任何代码执行之前,属于编译期初始化。
    而C++标准规定:全局或静态对象当且仅当对象首次用到时才进行构造。
  2. 作用域:C++存在的作用域有6种;全局,局部,类,语句,命名空间和文件作用域。
    静态全局变量 :全局作用域+文件作用域,所以无法在其他文件中使用。
    静态局部变量 :局部作用域,只被初始化一次,直到程序结束。
    类静态成员变量:类作用域。
  3. 所在空间:都在静态存储区。因为静态变量都在静态存储区,所以下次调用函数的时候还是能取到原来的值。
  4. 生命周期:静态全局变量、静态局部变量都在静态存储区,直到程序结束才会回收内存。类静态成员变量在静态存储区,当超出类作用域时回收内存。

1.3 什么是野指针,怎么产生的,如何避免?

  1. 概念:野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)。
  2. 产生原因:释放内存后指针不及时置空(野指针),依然指向了该内存,那么可能出现非法访问的错误。这些我们都要注意避免。
  3. 如何避免野指针的方法:
    • 定义即初始化NULL
    • 申请内存后判空;
    • 指针释放后置NULL
    • 使用智能指针
  4. 如何产生的野指针及危害
char *p = (char *)malloc(sizeof(char)*100);  
strcpy(p, "Douya");  
free(p);		//p所指向的内存被释放,但是p所指的地址仍然不变  
...  
if (p != NULL){//没有起到防错作用  
    strcpy(p, "hello, Douya!");//出错  
}  
  1. 写代码正确形式
    (1)初始化置NULL
    (2)申请内存后判空
    (3)指针释放后置NULL
// 形式1
int *p = NULL; //初始化置NULL
p = (int *)malloc(sizeof(int)*n); //申请n个int内存空间  
assert(p != NULL); //判空,防错设计
p = (int *) realloc(p, 25);//重新分配内存, p 所指向的内存块会被释放并分配一个新的内存地址
free(p);  
p = NULL; //释放后置空

// 形式2
int *p1 = NULL; //初始化置NULL
p1 = (int *)calloc(n, sizeof(int)); //申请n个int内存空间同时初始化为0 
assert(p1 != NULL); //判空,防错设计
free(p1);  
p1 = NULL; //释放后置空

// 形式3
int *p2 = NULL; //初始化置NULL
p2 = new int[n]; //申请n个int内存空间  
assert(p2 != NULL); //判空,防错设计
delete []p2;  
p2 = nullptr; //释放后置空  

1.4 说说静态变量 全局变量 局部变量的特点以及使用场景

  1. 首先从作用域考虑:C++里作用域可分为6种:全局 局部 类 语句 命名空间和文件作用域。
  • 全局变量:全局作用域,可以通过extern作用于其他非定义的源文件。
  • 静态全局变量 :全局作用域+文件作用域,所以无法在其他文件中使用。
  • 局部变量:局部作用域,比如函数的参数,函数内的局部变量等等。
  • 静态局部变量 :局部作用域,只被初始化一次,直到程序结束。
  1. 从所在空间考虑:除了局部变量在栈上外,其他都在静态存储区。因为静态变量都在静态存储区,所以下次调用函数的时候还是能取到原来的值。
  2. 生命周期: 局部变量在栈上,出了作用域就回收内存;而全局变量、静态全局变量、静态局部变量都在静态存储区,直到程序结束才会回收内存。

1.5 说说内联函数和宏函数的区别

  1. 宏定义不是函数,但是使用起来像函数。预处理器用复制宏代码的方式代替函数的调用,省去了函数压栈退栈过程,提高了效率;而内联函数本质上是一个函数,内联函数一般用于函数体的代码比较简单的函数,不能包含复杂的控制语句,while、switch,并且内联函数本身不能直接调用自身。
  2. 宏函数是在预编译的时候把所有的宏名用宏体来替换,简单的说就是字符串替换 ;而内联函数则是在编译的时候进行代码插入,编译器会在每处调用内联函数的地方直接把内联函数的内容展开,这样可以省去函数的调用的开销,提高效率。
  3. 宏定义是没有类型检查的,无论对还是错都是直接替换;而内联函数在编译的时候会进行类型的检查,内联函数满足函数的性质,比如有返回值、参数列表等

解释2:
先说宏和函数的区别:

  1. 宏做的是简单的字符串替换(注意是字符串的替换,不是其他类型参数的替换),而函数的参数的传递,参数是有数据类型的,可以是各种各样的类型.
  2. 宏的参数替换是不经计算而直接处理的,而函数调用是将实参的值传递给形参,既然说是值,自然是计算得来的.
  3. 宏在编译之前进行,即先用宏体替换宏名,然后再编译的,而函数显然是编译之后,在执行时,才调用的.因此,宏占用的是编译的时间,而函数占用的是执行时的时间.
  4. 宏的参数是不占内存空间的,因为只是做字符串的替换,而函数调用时的参数传递则是具体变量之间的信息传递,形参作为函数的局部变量,显然是占用内存的.
  5. 函数的调用是需要付出一定的时空开销的,因为系统在调用函数时,要保留现场,然后转入被调用函数去执行,调用完,再返回主调函数,此时再恢复现场,这些操作,显然在宏中是没有的.

同其它函数不同的是,最好将inline函数定义在头文件,而不仅仅是声明,因为编译器在处理inline函数时,需要在调用点内联展开该函数,所以仅需要函数声明是不够的。
示例:

/宏定义示例
#define MAX(a, b) ((a)>(b)?(a):(b))
MAX(a,"Hello"); //错误地比较int和字符串,没有参数类型检查

//内联函数示例
#include <stdio.h>
inline int add(int a, int b){
    return (a + b);
}
int main(void){
    int a;
    a = add(1, 2);   //会进行类型检查
    printf("a+b=%d\n", a);
    return 0;
}
//以上a = add(1, 2);处在编译时将被展开为:a = (a + b);

1.6 内涵函数和宏函数之间的区别

  • 内联函数和普通函数的参数传递机制相同,但是编译器会在每处调用内联函数的地方将内联函数内容展开,这样既避免了函数调用的开销又没有宏机制的缺陷
  • 普通函数在被调用的时候,系统首先要到函数的入口地址去执行函数体,执行完成之后再回到函数调用的地方继续执行,函数始终只有一个复制。
  • 内联函数不需要寻址,当执行到内联函数的时候,将此函数展开,如果程序中有N次调用了内联函数则会有N次展开函数代码
  • 内联函数有一定的限制,内联函数体要求代码简单,不能包含复杂的结构控制语句。如果内联函数函数体过于复杂,编译器将自动把内联函数当成普通函数来执行

1.7 new和malloc的区别,各自底层实现原理

  • new是操作符,而malloc是函数。
  • new在调用的时候先分配内存,在调用构造函数,释放的时候调用析构函数;而malloc没有构造函数和析构函数。
  • malloc需要给定申请内存的大小,返回的指针需要强转;new会调用构造函数,不用指定内存的大小,返回指针不用强转。
  • new可以被重载;malloc不行
  • new分配内存更直接和安全。
  • new发生错误抛出异常,malloc返回null

1.7.1 底层如何实现:

  1. malloc底层实现:当开辟的空间小于 128K 时,调用 brk()函数;当开辟的空间大于 128K 时,调用mmap()。malloc采用的是内存池的管理方式,以减少内存碎片。先申请大块内存作为堆区,然后将堆区分为多个内存块。当用户申请内存时,直接从堆区分配一块合适的空闲快。采用隐式链表将所有空闲块,每一个空闲块记录了一个未分配的、连续的内存地址。

  2. new底层实现:关键字new在调用构造函数的时候实际上进行了如下的几个步骤:

  • 创建一个新的对象
  • 将构造函数的作用域赋值给这个新的对象(因此this指向了这个新的对象)
  • 执行构造函数中的代码(为这个新对象添加属性)
    8 返回新对象

1.8 const和define的区别

const用于定义常量;而define用于定义宏,而宏也可以用于定义常量。都用于常量定义时,它们的区别有:

  • const生效于编译的阶段;define生效于预处理阶段。
  • const定义的常量,在C语言中是存储在内存中、需要额外的内存空间的;define定义的常量,运行时是直接的操作数,并不会存放在内存中。
  • const定义的常量是带类型的;define定义的常量不带类型。因此define定义的常量不利于类型检查。

1.9 C++中函数指针和指针函数的区别

  1. 定义不同(看中心词)
    指针函数本质是一个函数,其返回值为指针。
    函数指针本质是一个指针,其指向一个函数。
  2. 定义形式
指针函数:int *fun(int x,int y);
函数指针:int (*fun)(int x,int y);   // fun是一个指向函数的指针
  1. 用法不同
//指针函数示例
typedef struct  _Data{
    int a;
    int b;
}Data;
//指针函数
Data* f(int a,int b){
    Data * data = new Data;
    //...
    return data;
}
int main(){
    //调用指针函数
    Data * myData = f(4,5);
    //Data * myData = static_cast<Data*>(f(4,5));
   //...
}


//函数指针示例
int add(int x,int y){
    return x+y;
}
//函数指针
int (*fun)(int x,int y);
//赋值
fun = add;
//调用
cout << "(*fun)(1,2) = " << (*fun)(1,2) ;
//输出结果
//(*fun)(1,2) =  3

1.20 const int *a, int const *a, const int a, int *const a, const int *const a分别是什么,有什么特点。

直接看定义:

1. const int a;     //指的是a是一个常量,不允许修改。
2. const int *a;    //a指针所指向的内存里的值不变,即(*a)不变
3. int const *a;    //同const int *a;
4. int *const a;    //a指针所指向的内存地址不变,即a不变
5. const int *const a;   //都不变,即(*a)不变,a也不变

1.21 说说指针需要注意什么

  1. 定义指针时,先初始化为NULL。
  2. 用malloc或new申请内存之后,应该立即检查指针值是否为NULL。防止使用指针值为NULL的内存。
  3. 不要忘记为数组和动态内存赋初值。防止将未被初始化的内存作为右值使用。
  4. 避免数字或指针的下标越界,特别要当心发生“多1”或者“少1”操作
  5. 动态内存的申请与释放必须配对,防止内存泄漏
  6. 用free或delete释放了内存之后,立即将指针设置为NULL,防止“野指针”

具体做法为:
(1)初始化置NULL
(2)申请内存后判空
(3)指针释放后置NULL

int *p = NULL; //初始化置NULL
p = (int *)malloc(sizeof(int)*n); //申请n个int内存空间  
assert(p != NULL); //判空,防错设计
p = (int *) realloc(p, 25);//重新分配内存, p 所指向的内存块会被释放并分配一个新的内存地址
free(p);  
p = NULL; //释放后置空

int *p1 = NULL; //初始化置NULL
p1 = (int *)calloc(n, sizeof(int)); //申请n个int内存空间同时初始化为0 
assert(p1 != NULL); //判空,防错设计
free(p1);  
p1 = NULL; //释放后置空

int *p2 = NULL; //初始化置NULL
p2 = new int[n]; //申请n个int内存空间  
assert(p2 != NULL); //判空,防错设计
delete []p2;  
p2 = nullptr; //释放后置空  

1.22 内联函数与普通函数的区别

  1. 内联函数比普通函数多了关键字inline
  2. 内联函数避免了函数调用的开销;普通函数有调用的开销
  3. 普通函数在被调用的时候,需要寻址(函数入口地址);内联函数不需要寻址。
  4. 内联函数有一定的限制,内联函数体要求代码简单,不能包含复杂的结构控制语句;普通函数没有这个要求。
  5. 内联函数的作用:内联函数在调用时,是将调用表达式用内联函数体来替换。避免函数调用的开销。

答案解析
在使用内联函数时,应注意如下几点:

  • 在内联函数内不允许用循环语句和开关语句。 
    如果内联函数有这些语句,则编译将该函数视同普通函数那样产生函数调用代码,递归函数是不能被用来做内联函数的。内联函数只适合于只有1~5行的小函数。对一个含有许多语句的大函数,函数调用和返回的开销相对来说8 微不足道,所以也没有必要用内联函数实现。
  • 内联函数的定义必须出现在内联函数第一次被调用之前。即不仅仅是只有申明;

1.23 C++几种传值方式,之间的区别是什么?

传参方式有:值传递、引用传递、指针传递。

  1. 值传递:形参即使在函数体内值发生变化,也不会影响实参的值;
  2. 引用传递:形参在函数体内值发生变化,会影响实参的值;
  3. 指针传递:在指针指向没有发生改变的前提下,形参在函数体内值发生变化,会影响实参的值;
    值传递用于对象时,整个对象会拷贝一个副本,这样效率低;而引用传递用于对象时,不发生拷贝行为,只是绑定对象,更高效;指针传递同理,但不如引用传递安全。

【代码示例】

//代码示例
#include <iostream>
using namespace std;

void testfunc(int a, int *b, int &c){//形参a值发生了改变,但是没有影响实参i的值;但形参*b、c的值发生了改变,影响到了实参*j、k的值
    a += 1;
    (*b) += 1;
    c += 1;
    printf("a= %d, b= %d, c= %d\n",a,*b,c);//a= 2, b= 2, c= 2
}
int main(){
       int i = 1;
    int a = 1;
    int *j = &a;
    int k = 1;
    testfunc(i, j, k);
    printf("i= %d, j= %d, k= %d\n",i,*j,k);//i= 1, j= 2, k= 2
    return 0;
}

1.23 const* 和 *const的区别

【代码示例】

//const* 是常量指针,*const 是指针常量

int const *a;    //a指针所指向的内存里的值不变,即(*a)不变
int *const a;    //a指针所指向的内存地址不变,即a不变

2 C++内存

2.1 简述一下堆和栈的区别

  1. 堆栈空间分配不同。栈由操作系统自动分配释放 ,存放函数的参数值,局部变量的值等;堆一般由程序员分配释放。
  2. 堆栈缓存方式不同。栈使用的是一级缓存, 它们通常都是被调用时处于存储空间中,调用完毕立即释放;堆则是存放在二级缓存中,速度要慢些。
  3. 堆栈数据结构不同。堆类似数组结构;栈类似栈结构,先进后出。

2.2 简述C++内存管理

  1. 内存分配方式
    在C++中,内存分成5个区,他们分别是堆、栈、自由存储区、全局/静态存储区和常量存储区。
    栈:在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。
    堆:就是那些由new分配的内存块,一般一个new就要对应一个delete。
    自由存储区:就是那些由malloc等分配的内存块,和堆是十分相似的,不过是用free来结束自己的生命。
    全局/静态存储区:全局变量和静态变量被分配到同一块内存中
    常量存储:这是一块比较特殊的存储区,里面存放的是常量,不允许修改。

  2. 常见的内存错误及其对策
    (1)内存分配未成功,却使用了它。
    (2)内存分配虽然成功,但是尚未初始化就引用它。
    (3)内存分配成功并且已经初始化,但操作越过了内存的边界。
    (4)忘记了释放内存,造成内存泄露。
    (5)释放了内存却继续使用它。

对策:

  1. 定义指针时,先初始化为NULL。
  2. 用malloc或new申请内存之后,应该立即检查指针值是否为NULL。防止使用指针值为NULL的内存。
  3. 不要忘记为数组和动态内存赋初值。防止将未被初始化的内存作为右值使用。
  4. 避免数字或指针的下标越界,特别要当心发生“多1”或者“少1”操作
  5. 动态内存的申请与释放必须配对,防止内存泄漏
  6. 用free或delete释放了内存之后,立即将指针设置为NULL,防止“野指针”
  7. 使用智能指针。

2.3 内存泄漏及解决办法

  1. 什么是内存泄漏及解决办法
    简单地说就是申请了一块内存空间,使用完毕后没有释放掉。(1)new和malloc申请资源使用后,没有用delete和free释放;(2)子类继承父类时,父类析构函数不是虚函数。(3)Windows句柄资源使用后没有释放
  2. 怎么检测?
    怎么检测?
    第一:良好的编码习惯,使用了内存分配的函数,一旦使用完毕,要记得使用其相应的函数释放掉。
    第二:将分配的内存的指针以链表的形式自行管理,使用完毕之后从链表中删除,程序结束时可检查改链表。
    第三:使用智能指针。
    第四:一些常见的工具插件,如ccmalloc、Dmalloc、Leaky、Valgrind等等。

2.3.1 堆和栈的区别

区别:

  1. 堆栈空间分配不同。栈由操作系统自动分配释放 ,存放函数的参数值,局部变量的值等;堆一般由程序员分配释放。
  2. 堆栈 缓存方式不同。栈使用的是一级缓存, 它们通常都是被调用时处于存储空间中,调用完毕立即释放;堆则是存放在二级缓存中,速度要慢些。
  3. 堆栈数据结构不同。堆类似数组结构;栈类似栈结构,先进后出。

2.4 malloc和局部变量分配在堆还是栈?

malloc是在堆上分配内存,需要程序员自己回收内存;局部变量是在栈中分配内存,超过作用域就自动回收。

2.5 程序有哪些section,分别的作用?怎么判断数据分配在栈上还是堆上?

从低地址到高地址,一个程序由代码段、数据段、 BSS 段组成。

  1. 数据段:存放程序中已初始化的全局变量和静态变量的一块内存区域。
  2. 代码段:存放程序执行代码的一块内存区域。只读,代码段的头部还会包含一些只读的常数变量。
  3. BSS 段:存放程序中未初始化的全局变量和静态变量的一块内存区域。
  4. 可执行程序在运行时又会多出两个区域:堆区和栈区。
    堆区:动态申请内存用。堆从低地址向高地址增长。
    栈区:存储局部变量、函数参数值。栈从高地址向低地址增长。是一块连续的空间。
  5. 最后还有一个文件映射区,位于堆和栈之间。

2.6 什么是内存对齐,问什么需要内存对齐?

首先:内存对齐应用于三种数据类型:struct、class、union。
在union中,所有的共用体成员共用一个空间,并且同一时间只能储存其中一个成员变量的值。
主要就是看struct结构体的对齐,数据大小
需要字节对齐的根本原因在于CPU访问数据的效率问题;
内存对齐原则有四个

  1. 数据成员对齐规则:结构(struct)或联合(union)的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小的整数倍开始。
  2. 结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部"最宽基本类型成员"的整数倍地址开始存储。(struct a里存有struct b,b里有char,int ,double等元素,那b应该从8的整数倍开始存储)。
    3, 收尾工作:结构体的总大小,也就是sizeof的结果,必须是其内部最大成员的"最宽基本类型成员"的整数倍。不足的要补齐。(基本类型不包括struct/class/uinon)。
  3. sizeof(union),以结构里面size最大元素为union的size,因为在某一时刻,union只有一个成员真正存储于该地址。

2.6.1 内存对齐作用

  1. 平台移植原因:不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
  2. 性能原因:经过内存对齐后,CPU的内存访问速度大大提升,高速缓存。为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。

什么是对齐?即所谓的“对齐”,比如4字节的int型,其起始地址应该位于4字节的边界上,即起始地址能够被4

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值