C/C++常见面试题总结

目录

  • Reference
  • 知识点总结
    • 【题目1】map与unordered_map区别
    • 【题目2】智能指针是如何实现的,计数器存放在哪里
    • 【题目3】C/C++程序编译过程
    • 【题目4】结构体和类的区别
    • 【题目5】C++多态
    • 【题目6】一个.c文件如何访问另外一个.c文件中的static类型变量
    • 【题目7】什么是野指针和内存泄漏?如何避免野指针?
    • 【题目8】#include <>与#include ””的区别
    • 【题目9】C 库函数 atoi函数实现
    • 【题目10】什么运算符重载只能用友元函数定义
    • 【题目11】面向对象与面向过程的区别
    • 【题目12】C语言变长数组 & struct内存对齐
    • 【题目13】处理哈希冲突的常见方法
    • 【题目14】动态链接库& 静态链接库
    • 【题目15】重载、重写(覆盖)和隐藏
    • 【题目16】常量引用

Reference

  • C++经典面试题(最全,面中率最高)
  • C/C++引用指针对比及部分关键字(面试复习整理)
  • C/C++ 经典面试题(一)之常考概念【持续更新】
  • C++面试题集锦
  • C++面试题目,整理自牛客网
  • c++面试汇总(牛客)
  • C/C++ 最常见50道面试题

知识点总结

【题目1】map与unordered_map区别

*C++ set与map、unordered_map、unordered_set与哈希表

(1)排序:map在缺省下,map按照递增的顺序进行排序;unordered_map不排序

(2)内部原理:map内部采用了自平衡的二叉搜索树,实现了数据排序;unordered_map内部采用了哈希表

(3)搜索操作时间:map的搜索时间复杂度为O(log(n));unordered_map平均搜索时间O(1),最坏情况为O(n)

(4)插入操作时间:map复杂度为log(n)+再平衡时间;unordered_map平均插入时间O(1),最坏情况为O(n)

(5)删除操作时间:与插入操作时间复杂度一样

【题目2】智能指针是如何实现的,计数器存放在哪里

智能指针是一个类,用来存储指向动态分配对象的指针,负责自动释放动态分配的对象,防止堆内存泄漏。动态分配的资源,交给一个类对象去管理,当类对象声明周期结束时,自动调用析构函数释放资源。

Reference

  • 智能指针的原理及实现
  • C++ STL 四种智能指针
  • C++的四种智能指针

【题目3】C/C++程序编译过程

1、预处理: 展开头文件/宏替换/去掉注释/条件编译 ,生成.i文件。

2、编译: 检查语法,将预处理后的文件转换成汇编语言,生成.s文件

3、汇编:汇编变为目标代码(机器代码)生成.o的文件。

4、链接:连接目标代码,生成可执行程序。 链接过程是将单个编译后的文件链接成一个可执行程序。前面的预编译、汇编、编译都是正对单个文件,以一个文件为一个编译单元,而链接则是将所有关联到的编译后单元文件和应用的到库文件,进行一次链接处理,之前编译过的文件 如果有用到其他文件里面定义到的函数,全局变量,在这个过程中都会进行解析。

  • C++ —— C++程序编译的四个过程
  • C/C++编译过程
  • c语言编译过程详解,预处理,编译,汇编,链接(干货满满)

【题目4】结构体和类的区别

C的结构体和C++结构体的区别

  • C的结构体内不允许有函数存在,C++允许有内部成员函数,且允许该函数是虚函数。所以C的结构体是没有构造函数、析构函数、和this指针的。

  • C的结构体对内部成员变量的访问权限只能是public,而C++允许public,protected,private三种。

  • C语言的结构体是不可以继承的,C++的结构体是可以从其他的结构体或者类继承过来的。

以上都是表面的区别,实际区别就是面向过程和面向对象编程思路的区别:
C的结构体只是把数据变量给包裹起来了,并不涉及算法。
而C++是把数据变量及对这些数据变量的相关算法给封装起来,并且给对这些数据和类不同的访问权限。

C语言中是没有类的概念的,但是C语言可以通过结构体内创建函数指针实现面向对象思想。

二、C++的结构体和C++类的区别
2.1 C++结构体内部成员变量及成员函数默认的访问级别是public,而c++类的内部成员变量及成员函数的默认访问级别是private。
2.2 C++结构体的继承默认是public,而c++类的继承默认是private。

【题目5】C++多态

C/C++常见面试题总结_第1张图片
C++多态性总结

【题目6】一个.c文件如何访问另外一个.c文件中的static类型变量

直接用变量bai名访问是不行的,要通过函数调用来读写。例du如zhi:假设1.c文件里有静态变量static int a; 如果2.c文件想要读写a,就dao必须在1.c文件里添加读写a的外部函数。1.c写法示例:
static int a;
extern int get_a() { return a; }
extern void set_a(int v) { a=v; }
这样,在2.c里就可以调用get_a()和set_a(int v)函数来读写变量a了。

【题目7】什么是野指针和内存泄漏?如何避免野指针?

内存泄漏

①访问已经释放的内存
②访问没有权限的内存

野指针

指向内存被释放的内存或者没有访问权限的内存的指针。

https://blog.csdn.net/qq_35212671/article/details/51920851

【题目8】#include <>与#include ””的区别

  • 一种是在包含指令#include后面”<>”将头文件名括起来。这种方式用于标准或系统提供的头文件,到保存系统标准头文件的位置查找头文件。
  • 另一种是在包含指令#include后用双引号””将头文件包括起来。这种方式常用与程序员自己的头文件。用这种格式时,C编译器先查找当前目录是否有指定名称的头文件,然后在从标准头文件目录中查找。

【题目9】C 库函数 atoi函数实现

atoi用法
atoi()函数的功能:将字符串转换成整型数;atoi()会扫描参数nptr字符串,跳过前面的空格字符,直到遇上数字或正负号才开始做转换,而再遇到非数字或字符串时(’\0’)才结束转化,并将结果返回(返回转换后的整型数)。

#include 
int my_atoi(char *input)
{
    int flag = 1, result = 0;
    if (input == NULL)
        return 0;
    while (*input == ' ' || *input == '\t')
        ++input;
    if (*input == '- ')
        flag = -1;
    if (*input == '+ ' || *input == '- ' )
        ++input;
    while (*input != '\0') {
        if (*input >= 0 && *input <= '9')
            result = result*10 + (*input - '0');
        else
            break;
        ++input;
    }
    return flag * result;
}

int main(void)
{
    int output;
    char input[32];
    scanf("%s",input);
    output = my_atoi(input);
    printf("%d\n",output);
    return 0;
}
#include 

int my_atoi(char* pstr)
{
    int Ret_Integer = 0;
    int Integer_sign = 1;

    /*
    * 判断指针是否为空
    */
    if(pstr == NULL){
        printf("Pointer is NULL\n");
        return 0;
    }

    /*
    * 跳过前面的空格字符
    */
    while(*pstr == '\0') {
        pstr++;
    }

    /*
    * 判断正负号
    * 如果是正号,指针指向下一个字符
    * 如果是负号,把负号标记Integer_sign置-1,然后再把指针指向下一个字符
    */
    if(*pstr == '-') {
        Integer_sign = -1;
    }
    if(*pstr == '-' || *pstr == '+') {
        pstr++;
    }

    /*
    * 把数字字符串逐个转换成整数,并把最后转换好的整数赋给Ret_Integer
    */
    while(*pstr >= '0' && *pstr <= '9') {
        Ret_Integer = Ret_Integer * 10 + *pstr - '0';
        pstr++;
    }
    Ret_Integer = Integer_sign * Ret_Integer;

    return Ret_Integer;
}

int main()
{
    int output;
    char input[32];
    scanf("%s",input);
    output = my_atoi(input);
    printf("%d\n",output);
    return 0;
}

【题目10】什么运算符重载只能用友元函数定义

有的操作符重载函数只能是成员函数
(1) 不能重载的运算符只有5个:

.           ( 成员访问运算符 )
 
.*          ( 成员指针访问运算符 )
 
::          ( 域运算符 )
 
sizeof      ( 长度运算符 )
 
?:          ( 条件运算符 )
 

(2) 运算符必须是成员函数

=          ( 赋值 )
 
[]         ( 下标 )
 
()         ( 调用) 
 
->         ( 成员访问箭头 )

(3) 只能为友元

>>    ( 输入流操作 )
><<  ( 输出流操作 )

【题目11】面向对象与面向过程的区别

面向对象与面向过程的区别

  • 对象就是类,过程就是顺序,选择,循环,数组,函数

  • 面向对象是以数据为中心,而面向过程以功能为中心

面向过程就是分析出解决问题所需要的步骤,然后用函数把这些问题一步一步的实现,然后再使用的时候依次调用就可以了。

面向对象就是把构成问题的事物分解成各个对象,构建对象的目的不是来完成一个步骤的,而是为了描述某个事物在解决整个问题的步骤中的行为

面向过程: 当需要实现一个功能的时候,每一个步骤我们都需要自己去做,处理实现功能的每一个细节。
面向对象
当需要实现一个功能的时候,我们不需要自己去做,可以直接找一个已经具有该功能的东西,来帮我解决问题。
区别
面向过程强调的是步骤。
面向对象强调的是对象,这里的对象就是饭店。
特点
面向对象是一种更符合我们思考习惯的一种思想,他可以将问题简单化,并将我们从执行者变成了指挥者。在面向对象的语言中,有三大基本特性:
1.封装性
方法就是一种封装
private关键字也是一种封装
2.继承性
3.多态性

【题目12】C语言变长数组 & struct内存对齐

  • C语言变长数组讲解
  • 结构体内存对齐
  • 结构体内存对齐
  • 结构体内存对齐原则

【题目13】处理哈希冲突的常见方法

哈希冲突

哈希函数有五种实现方式:

A. 直接定址法:取关键字的线性函数值作为哈希地址。
B. 数字分析法:取关键字的中的若干位作为哈希地址。
C. 平方取中法:取关键字平方后的中间几位作为哈希地址。
D. 折叠法:将关键字分割成位数相同的几部分(最后一部分可以不同),然后取这几部分的叠加和作为哈希地址。
E. 除留余数法:H(key) = key MOD p ,p<=m ,m为不大于哈希表的数。
F. 随机函数法
上述五中实现方式中最常用的是除留余数法,而通过哈希函数寻址的过程可能出现“冲突”------即若干个不同的key却对应相同的哈希地址。解决哈希冲突有如下的方法:

作者:晚歌y
链接:https://www.jianshu.com/p/b9e6015703c2
来源:简书

哈希冲突的定义:某个哈希函数H对于不同的关键字K1,K2得到相同的哈希地址,即H(K1) = H(K2),这种现象称为~

Hash冲突发生的场景:当关键字值域远大于哈希表的长度,而且事先并不知道关键字的具体取值时。hash冲突就会发生。

Hash溢出发生的场景:当关键字的实际取值大于哈希表的长度时,而且表中已装满了记录,如果插入一个新记录,不仅发生冲突,而且还会发生溢出。

解决哈希冲突的四种方法

  • 1. 开放地址方法

(1)线性探测
   按顺序决定值时,如果某数据的值已经存在,则在原来值的基础上往后加一个单位,直至不发生哈希冲突。

(2)二次平方探测
   按顺序决定值时,如果某数据的值已经存在,则在原来值的基础上先加1的平方个单位,若仍然存在则减1的平方个单位。随之是2的平方,3的平方等等。直至不发生哈希冲突。

(3)伪随机探测
   按顺序决定值时,如果某数据已经存在,通过随机函数随机生成一个数,在原来值的基础上加上随机数,直至不发生哈希冲突。

2.链地址法(HashMap的哈希冲突解决方法)

对于相同的值,使用链表进行连接。使用数组存储每一个链表。

优点:

(1)拉链法处理冲突简单,且无堆积现象,即非同义词决不会发生冲突,因此平均查找长度较短;

(2)由于拉链法中各链表上的结点空间是动态申请的,故它更适合于造表前无法确定表长的情况;

(3)开放定址法为减少冲突,要求装填因子α较小,故当结点规模较大时会浪费很多空间。而拉链法中可取α≥1,且结点较大时,拉链法中增加的指针域可忽略不计,因此节省空间;

(4)在用拉链法构造的散列表中,删除结点的操作易于实现。只要简单地删去链表上相应的结点即可。
  
  缺点:指针占用较大空间时,会造成空间浪费,若空间用于增大散列表规模进而提高开放地址法的效率。

3.建立公共溢出区
  哈希表分为基本表和溢出表。凡是和基本表发生冲突的元素,一律填入溢出表。

4.再哈希法
  当发生冲突时,使用第二个、第三个、哈希函数计算地址,直到无冲突。缺点:计算时间增加。

【题目14】动态链接库& 静态链接库

C++知识点14:静态链接库和动态链接库区别

:函数的集合
作用:共享代码

静态、动态指链接
程序编译过程中,在链接阶段,程序生成的汇编文件和库进行链接,生成可执行文件。
C/C++常见面试题总结_第2张图片

Linux下得库有动态与静态两种,动态通常用.so为后缀,静态用.a为后缀。面对比一下两者:

静态链接库:当要使用时,连接器会找出程序所需的函数,然后将它们拷贝到执行文件,由于这种拷贝是完整的,所以一旦连接成功,静态程序库也就不再需要了。

动态库而言:某个程序在运行中要调用某个动态链接库函数的时候,操作系统首先会查看所有正在运行的程序,看在内存里是否已有此库函数的拷贝了。如果有,则让其共享那一个拷贝;只有没有才链接载入。在程序运行的时候,被调用的动态链接库函数被安置在内存的某个地方,所有调用它的程序将指向这个代码段。因此,这些代码必须使用相对地址,而不是绝对地址。在编译的时候,我们需要告诉编译器,这些对象文件是用来做动态链接库的,所以要用地址无关代码(Position Independent Code (PIC))。

动态链接库的加载方式有两种:隐式加载和显示加载。

注意:linux下进行连接的缺省操作是首先连接动态库,也就是说,如果同时存在静态和动态库,不特别指定的话,将与动态库相连接(见本文第四部分)。

【题目15】重载、重写(覆盖)和隐藏

  • C++中重载、重写(覆盖)和隐藏的区别

【题目16】常量引用

“常量引用”其实是“对 const 的引用”的简称。
顾名思义,它把它所指向的对象看作是常量(不一定是常量),因此不可以通过该引用来修改它所指向的对象的值。

严格来说,并不存在常量引用,因为引用不是一个对象,所以我们没法让引用本身恒定不变。事实上,由于 C++ 语言并不允许随意改变引用所绑定的对象,所以从这层意思上理解所有的引用又都算是常量。
与普通引用不同的是,“常量引用”(即对 const 的引用)不能被用作修改它所绑定的对象。

(1)指向常量对象时,一定要使用“常量引用”,而不能是一般的引用。

因为不允许直接为常量赋值,当然也就不能通过引用去改变常量。因此直接规定当引用一个常量时,必须使用“常量引用”。

const int ci = 1024;
const int &r1 = ci;         // 正确:引用及其对应的对象都是常量
r1 = 42;                    // 错误:r1是对常量的引用,不能被用作修改它所绑定的对象
int &r2 = ci;               // 错误:试图让一个非常量引用指向一个常量对象

(2)“常量引用”可以指向一个非常量对象,但不允许用该引用修改非常量对象的值。

必须认识到,“常量引用”仅对引用可参与的操作做出了限定,对于引用的对象本身是不是一个常量未作限定。因为对象也可能是个非常量,所以允许通过其他途径改变它的值:

int i = 42;
int &r1 = i;                // 普通引用指向非常量对象 i
const int &r2 = i;          // 常量引用也绑定非常量对象 i
r1 = 0;                     // 正确,r1并非常量引用
r2 = 0;                     // 错误:r2是一个常量引用

r2 绑定非常量整数 i 是合法的行为。然而不允许通过 r2 修改 i 的值。尽管如此,i 的值仍然允许通过其他途径修改,既可以直接给 i 赋值,也可以通过像 r1 一样绑定到 i 的其他引用来修改。

(3)引用的类型必须和所引用的类型严格匹配,且不能与字面值或者某个表达式的计算结果绑定在一起,但是 “常量引用” 是例外(只要被引用的类型能够转换为常量引用的类型)。

尤其,允许为一个常量引用绑定非常量的对象、字面值,甚至是一个一般表达式:

int i = 42;
const int &r1 = i;          // 正确:指向非常量对象
const int &r2 = 42;         // 正确:r2 是一个常量引用
const int &r3 = r1 * 2;     // 正确:r3 是一个常量引用
int &r4 = r1 * 2;           // 错误:r4 是一个普通的非常量引用

下面的操作也是允许的:

double dval = 3.14;
const int &r1 = dval;

此处 const int &r1 = dval 编译器实际上相当于执行了下列语句:引用和原 dval 已经不是同一个地址了:

const int temp = dval;      // 生成一个临时的整型常量
const int &r1 = temp;       // 让 r1 绑定这个临时量

在这些情况下,“常量引用”实际上是绑定了一个临时量(temporary)对象。也就是说,允许“常量引用”指向一个临时量对象。
当 r1 不是“常量引用”时,如果执行了类似于上面的初始化操作会带来什么样的后果?
如果 r1 不是常量,就允许对 r1 赋值,这样就会改变 r1 所引用的对象的值。注意,此时绑定的对象是一个临时量而非 dval。程序员既然想让 r1 引用 dval,就肯定想通过 r1 改变 dval 的值,否则干什么要给 r1 赋值呢?如此看来,既然大家基本上不会想着把引用绑定到临时量上,C++ 语言也就把这种行为归为非法。

也就是说,不允许一个普通引用与字面值或者某个表达式的计算结果,或类型不匹配的对象绑定在一起,其实就是不允许一个普通引用指向一个临时变量,只允许将“常量引用”指向临时对象。

(4)在函数参数中,使用常量引用非常重要。因为函数有可能接受临时对象,而且同时需要禁止对所引用对象的一切修改。

下面程序执行发生错误,因为不可以将一个字面值常量赋值给普通引用;函数的返回值如果是非引用类型时,实际上是作为一个临时变量返回的,经过上面的讨论,不允许一个普通引用指向临时对象。

int test() {
	return 1;
}

void fun(int &x) {
    cout << x << endl;
}
 
int main()
{
	int m = 1;
	fun(m);         // ok
	fun(1);         // error
    fun(test());    // error
 
	return 0;
}

按下面修改后,fun()函数无论是接受字面值常量作为参数,还是将函数的返回值作为参数均可:

int test() {
	return 1;
}
 
void fun(const int &x) {
    cout << x << endl;
}
 
int main()
{
    fun(1);         // ok
	fun(test());    // ok
 
	return 1;
}

复制构造函数也是构造函数,但它只有一个参数,这个参数是本类 的对象(不能是其他类的对象),而且采用对象的引用的形式(一般约定加const声明,使参数值不能改变,以免在调用此函数时因不慎而使对 象值被修改)。如果用户自己未定义复制构造函数,则编译系统会自动提供一个默认的 复制构造函数,其作用只是简单地复制类中每个数据成员。

拷贝(复制)构造函数定义及3种调用情况举例

你可能感兴趣的:(笔试面经,C++面试题)