【图解八股-C/C++】

作者简介和专栏内容见专栏介绍:https://www.nowcoder.com/creation/manager/columnDetail/0eL5bM

麻烦看到贴子的伙伴点点赞大家点赞订阅支持下,提前祝各位offer多多,有问题评论区见~~

图解版

文字版

C语言

与C++区别上

预处理&关键字

写一个max宏

一个简单的max宏可能定义为:

#define MAX (x,y) ((x) > (y) ? (x) : (y))

这个宏看起来很简单,但是如果我们用它来比较两个表达式,比如:

int a = 1; int b = 2; int c = MAX (a++, b++);

我们期望得到的结果是c = 2,但实际上得到的结果是c = 3。这是因为宏会对参数进行文本替换,导致a++和b++被计算了两次。展开后的代码如下:

int c = ((a++) > (b++) ? (a++) : (b++));

为了避免这种问题,我们可以使用一些技巧来改进宏的定义,比如:

  • 使用括号来避免运算符优先级的问题。
  • 使用typeof或decltype来避免类型不匹配的问题。
  • 使用复合语句或内联函数来避免重复计算的问题。

例如,一个改进后的max宏可能定义为:

#define MAX (x,y) ({typeof (x) _x = (x);typeof (y) _y = (y);_x > _y ? _x : _y;})

这个宏可以避免上述问题,但是仍然有一些局限性,比如:

  • 这个宏依赖于GCC扩展的语法,不是标准C语言的一部分。
  • 这个宏不能处理浮点数或其他特殊类型的比较。
  • 这个宏不能作为函数参数或返回值使用。

因此,一个更好的选择是使用函数而不是宏来实现max操作。函数可以提供类型安全、作用域控制、调试支持等优点。而且如果函数足够简单,编译器可以自动将其内联化,从而消除函数调用的开销。例如,一个简单的max函数可能定义为:

inline int max (int x, int y) { return x > y ? x : y; }

举例一个使用二级指针的例子

// 使用二级指针作为函数参数,交换两个一级指针所指向的数据 
void swap(int **x, int **y) { int *temp = *x; *x = *y; *y = temp; }

// 使用二级指针动态分配一个二维数组 
int **create_array(int rows, int cols) { 
    int *arr = malloc(rows * sizeof(int)); // 分配行数个一级指针 
    for (int i = 0; i < rows; i++) { 
        arr[i] = malloc(cols * sizeof(int)); 
    // 分配每行对应的列数个整型数据 
    } 
    return arr; 
}

int(*p)(void(*)(void*),int**)表示什么

  • p是一个指针
  • p指向一个函数
  • 这个函数有两个参数
  • 这个函数的返回类型是int

如何在C语言实现接口

如何用C语言实现读写寄存器变量?

答案:

#define rBANKCON0 (*(volatile unsigned long *)0x48000004) rBANKCON0 = 0x12;

解读:

(1)由于是寄存器地址,所以需要先将其强制类型转换为 ”volatile unsigned long *”。

(2)由于后续需要对寄存器直接赋值,所以需要解引用。

用预处理指令#define声明一个常数,用以表明1年中有多少秒(忽略闰年问题)。

答案:

#define SECONDS_PER_YEAR (60 * 60 * 24 * 365)UL

解读:(1)注意预处理器将为你计算常数表达式的值,并且整个宏体要用括号括起来。

(2)注意这个表达式将使一个16位机的整型数溢出,因此要用到无符号长整型符号UL,告诉编译器这个常数是的无符号长整型数。

一个参数既可以是const还可以是volatile吗?一个指针可以是volatile吗?下面的函数有什么问题?

int square(volatile int *ptr)   
{   
    return *ptr * *ptr;   
}

(1)是的。一个例子是只读的状态寄存器,它是volatile因为它可能被意想不到地改变,它是const因为程序不应该试图去修改它。

(2)是的。一个例子是当一个中断服务子程序修改一个指向一个缓冲区的指针时。

(3)这个函数的目的是用来返回指针*ptr指向值的平方,但是,由于*ptr指向一个volatile型参数,编译器将产生类似下面的代码:

int square(volatile int *ptr)   
{   
    int a, b;   
    a = *ptr;   
    b = *ptr;   
    return a * b;   
}

由于*ptr的值可能被意想不到地改变,因此a和b可能是不同的。结果,这段代码可能返不是你所期望的平方值!正确的代码如下:

long square(volatile int *ptr)   
{   
    int a;   
    a = *ptr;   
    return a * a;   
}  

关键字typedef 在C语言中频繁用以声明一个已经存在的数据类型的同义字。也可以用预处理器做类似的事。例如,思考一下下面的例子:

#define dPS struct s *

typedef struct s * tPS; //(顺序、分号、#号)

以上两种情况的意图都是要定义dPS 和 tPS 作为一个指向结构体s的指针。哪种方法更好呢?为什么?

(1)typedef更好。

(2)举个例子:

dPS p1, p2;

tPS p3, p4;

第一行代码扩展为 struct s * p1, p2; 即定义p1为一个指向结构体的指针,p2为一个实际的结构体,这也许不是你想要的。第二行代码正确地定义了p3 和p4 两个指针。

下面代码能不能编译通过?

#define c 3 c++;

答案:不能。

解读:自增运算符++用于变量,3是常量

关键字extern的作用是什么?

volatile在编译阶段,extern在链接阶段。

答案:用于跨文件引用全局变量,即在本文件中引用一个已经在其他文件中定义的全局变量。

解读:

(1)注意引用时不能初始化,如extern var,而不能是extern var = 0。

(2)另外,函数默认是extern类型的,表明是整个程序(工程)可见的,加不加都一样。

extern”C”的作用?

答案:

(1)在C++代码中调用C函数,用法:extern “C”{C函数库头文件/函数声明}。

(2)在C代码中调用C++函数,用法:在C++的头文件中加extern“C”{头文件/函数声明}。

注意:extern”C”只能用于C++文件中。

关键字register的作用是什么?使用时需要注意什么?

(1)作用:编译器会将register修饰的变量尽可能地放在CPU的寄存器中,以加快其存取速度,一般用于频繁使用的变量。

(2)注意:register变量可能不存放在内存中,所以不能用&来获取该变量的地址;只有局部变量和形参可以作为register变量;寄存器数量有限,不能定义过多register变量。

数据类型

用变量a给出下面的定义

(5)一个有10个指针的数组,这10个指针是指向整型数的(指针数组): int *a[10]。(6)一个指向有10个整型数数组的指针(数组指针):int (*a)[10]。(7)一个指向函数的指针,该函数有一个整型参数并返回一个整型数(函数指针):int (*a)(int)。(8)一个有10个指针的数组,这10个指针均指向函数,该函数有一个整型参数并返回一个整型数(函数指针数组): int (*a[10])(int)

下面代码有什么错误?

#include<stdio.h>  
void main()  
{  
    char *s = "AAA";  
    s[0] = 'B';  
    printf("%s", s);  
}

(1)"AAA"是字符串常量,s是指针,指向这个字符串常量,所以声明s的时候就有问题,应该是const char* s="AAA"。

(2)然后又因为是常量,所以对是s[0]的赋值操作是不合法的。

int a[10][10], &a + 1 和 a + 1是否相同

结构体内存对齐原则?

答案:

(1)第一个成员的首地址(地址偏移量)为0。

(2)成员对齐:以4字节对齐为例,如果自身类型小于4字节,则该成员的首地址是自身类型大小的整数倍;如果自身类型大于等于4字节,则该成员的首地址是4的整数倍。若内嵌结构体,则内嵌结构体的首地址也要对齐,只不过自身类型大小用内嵌结构体的最大成员类型大小来表示。数组可以拆开看做n个数组元素,不用整体看作一个类型。

(3)最后结构体总体补齐:以4字节对齐为例,如果结构体中最大成员类型小于4字节,则大小补齐为结构体中最大成员类型大小的整数倍;如果大于等于4字节,则大小补齐为4的整数倍。内嵌结构体也要补齐。

注意:32位编译器,一般默认对齐方式是4字节。

结构体内存对齐的原因?

(1)平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据。

(2)性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐,因为访问未对齐的内存,处理器需要做两次内存访问,而访问对齐的内存仅需要一次。如下图所示,访问对齐的short变量只需要一次,而访问未对齐的int变量则需要访问两次。

数组首元素地址和数组地址的异同?

(1)异:数组首元素地址和数组地址是两个不同的概念。例如int a[10],a的值是数组首元素地址,所以a+1就是第二个元素的地址,int类型占用4个字节,所以两者相差4。而&a是数组地址,所以&a+1就是向后移动(10*4)个单位,所以两者相差40。

(2)同:数组首元素地址和数组地址的值是相等的。

struct和union区别

structunion都是结构体类型,用于组织和存储数据。它们的区别在于:

  1. 内存分配方式:struct分配的内存空间是根据成员变量的类型和数量来计算的,每个成员变量都有自己的内存空间;而union分配的内存空间是所有成员变量共享的,它们占用的是同一块内存空间。
  2. 成员变量访问方式:struct的成员变量可以同时被访问,每个成员变量都有自己的地址;而union的成员变量只能单独访问,不能同时访问,因为它们共享同一块内存空间。
  3. 内存使用方式:struct用于存储不同类型的数据,每个成员变量都有自己的内存空间,可以同时存储多个值;而union用于存储同一类型的数据,所有成员变量共享同一块内存空间,只能存储一个值。

综上所述,structunion的主要区别在于内存分配方式、成员变量访问方式和内存使用方式。在实际应用中,需要根据具体的情况选择合适的类型,以便最大限度地发挥它们的优势。

下面代码输出是什么?

#include<stdio.h>  
void main()  
{  
    int a[5] = {1, 2, 3, 4, 5};  
    int *ptr = (int *)(&a + 1);  
    printf("%d, %d", *(a + 1), *(ptr - 1));  
}  

答案:输出为2, 5。

解读: a是数组首元素地址,所以*(a + 1)就是第二个元素a[1]。&a是数组地址,所以&a + 1是整个数组结尾的下一个地址,*(ptr - 1)就是a[4]。

下面代码的输出结果是什么?

#include<stdio.h>  
void main()   
{   
     char *str[] = {"ab", "cd", "ef", "gh", "ij", "kl"};  //指针数组  
     char *t;   
     t = (str + 4)[-1];    
     printf("%s", t);    
}  

答案:输出"gh"。

解读:str表示数组首元素地址,str + 4表示数组第五个元素地址,(str + 4)[-1]表示在第五个元素地址的基础上往前移一个元素并解引用,因此输出是第四个元素。

给了一个地址a,分别强转类型为:int变量、int指针、数组指针、指针数组、函数指针。

int变量

(int) a;

int指针

(int *)a;

数组指针

(int (*)[])a;

指针数组

(int *[])a;

函数指针

(int (*)(int))a;

C语言中不同数据类型之间的赋值规则?

答案:

(1)整数与整数之间(char, short, int, long):

①长度相等:内存中的数据不变,只是按不同的编码格式来解析。

②长赋值给短:截取低位,然后按短整数的数据类型解析。

③短赋值给长:如果都是无符号数,短整数高位补0;如果都是有符号数,短整数高位补符号数;如果一个有符号一个无符号,那么先将短整数进行位数扩展,过程中保持数据不变,然后按照长整数的数据类型解析数据。

(2)整数与浮点数之间

①浮点数转整数:截取整数部分。

②整数转浮点数:小数部分为0,整数部分与整数相等。

(3)float与double之间

①double转float会丢失精度。

②float转double不会丢失精度。

注意:整数在内存中都是以补码的形式存储的

内存管理

大小端存储

小端:一个数据的低位字节数据存储在低地址

大端:一个数据的高位字节数据存储在低地址

例如:int a=0x12345678; //a首地址为0x200,大端存储格式如下:

大小端转化:对一个输入的整型数进行大小端存储模式转化

思路:大小端转化就是将一个整型数的低字节放到高字节,高字节放到低字节,跟前面的位翻转类似,只不过这里的单位是字节,因此需要将位翻转中的&0x01改为&0xFF,<< bit改为size * 8,>>= 1改为 >> 8。

int endian_convert(int input)  
{  
    int result = 0;  
    int size = sizeof(input);  
    while(size--)  
    {  
        result |= ((input & 0xFF) << (size * 8));  
        input >>= 8;  
    }  
    return result;  
}

请问运行下面的Test()函数会有什么样的后果?

void Test(void)  
{  
    char *str = (char *) malloc(100);  
    strcpy(str,"hello");  
    free(str);       
    if(str != NULL)  
    {  
        strcpy(str, "world");   
        printf("%s\n", str);  
    }  
}  

答案:篡改堆区野指针指向的内容,后果难以预料,非常危险。

解读:

(1)free(str);之后,str成为野指针,没有置为NULL,if(str != NULL)语句不能阻止篡改操作。

(2)野指针不是NULL指针,是指向被释放的或者访问受限的内存的指针。

(3)造成野指针原因:①指针变量没有被初始化,任何刚创建的指针不会自动成为NULL;②指针被free或delete之后,没有置NULL;③指针操作超越了变量的作用范围,比如要返回指向栈内存的指针或引用,因为栈内存在函数结束时会被释放。

堆栈溢出一般是由什么原因导致的?

(1)堆栈溢出一般包括堆内存溢出和栈内存溢出,两者都属于缓冲区溢出。

(2)堆内存溢出可能是堆堆的尺寸设置得过小/动态申请的内存没有释放。

(3)栈内存溢出可能是栈的尺寸设置得过小/递归层次太深/函数调用层次过深/分配了过大的局部变量。

内存溢出和内存越界的区别?

(1)内存溢出:要求分配的内存超出了系统所能给予的,于是产生溢出。

(2)内存越界:向系统申请了一块内存,而在使用时超出了申请的范围,常见的是数组访问越界。

综合

编译和链接有什么不同?(如对外部符号的处理)

(1)编译(+汇编)生成的是目标文件(*.o)。编译过程中对于外部符号(如用extern跨文件引用的全局变量)不做任何解释和处理,外部符号对应的就是“符号”。

(2)链接生成的是可执行程序。链接将会解释和处理外部符号,外部符号对应的是地址。

gcc优化代码执行速度的编译选项是?

-O0/没有-O

不进行任何优化,用尽可能直接的方法来编译源代码,每行代码被直接转换为可执行文件中对应的指令。当调试一个程序时,这是最佳选项。

-O1/-O

使用能减少目标文件大小、执行时间,同时又不会使编译时间明显增加的代码,在编译大型程序的时候会显著增加编译时内存的使用。

-O2

使目标文件执行速度变快,但会增加编译时间。包含-O1的优化,同时执行了不会使目标文件变大、执行速度变慢的优化,不执行循环展开以及函数内联。

-O3

执行所有-O2优化选项,同时打开更深度的优化来提升执行文件的速度,比如函数内嵌,但也可能增加其大小。

-Os

专门优化目标文件的大小,执行所有不增加目标文件大小的-O2优化选项,并执行专门优化目标文件大小的选项。

a = b * 2; a = b / 4; a = b % 8; a = b / 8 * 8 + b % 4 ; a = b * 15;效率最高的算法?

答案:

a = b * 2

a = b << 1;

a = b / 4

a = b >> 2;

a = b % 8

a = b & 7; // 7 = (0b111)

a = b / 8 * 8 + b % 4

a = ((b >> 3) << 3) + (b & 3); // 3 = 0b11

a = b * 15

a = (b << 4) - b

解读:*、/、%分别可以用<<、>>、&来实现,效率更高。

时间换空间、空间换时间的例子?

(1)时间换空间:冒泡排序。

(2)空间换时间:快速排序。

为什么C++可以重载C不可以

C++可以函数重载,C不行的原因是两种语言在编译和链接时对函数名的处理方式不同。C语言只用函数名来区分函数,而C++会根据函数名、参数数量和类型来生成一个独一无二的名字,这样就可以支持同名但不同参数的函数。

例如,如果有两个函数:

int fun(int a, int b);
int fun(int a, char b);

在C语言中,编译器会认为这是重复定义,因为它们都叫 fun ,而在C++中,编译器会生成类似 fun_int_int 和 _fun_int_char 这样的名字,就可以区分它们了。

如果要在C++中调用C语言的函数,或者反过来,在C语言中调用C++的函数,就需要用 extern "C" 来告诉编译器按照C语言的方式处理函数名,否则会出现链接错误²。

例如,如果有一个C语言的头文件 cExample.h ,在C++中引用它时,需要这样写:

extern "C" {
  #include "cExample.h"
}

这样就可以避免因为函数名不匹配而导致的问题了。

C++基础

指针和引用的区别

先讲为什么,再讲底层区别,讲具体应用场景(指针传递,引用传递)

为什么要有指针和引用?

指针和引用的存在是为了方便程序员对内存的操作。指针可以用来访问数组、结构体等复杂数据类型,引用可以用来作为函数参数,避免了复制大量的数据。

底层区别

引用传递不需要创建临时变量,开销要更小

表象区别

  • 从定义上看:指针是个变量,存储指向的内存地址;引用相当于原变量的别名。也因此,指针是可以有多级的,也可以为空,初始化后也可以改变指向;而引用只能有一级,在定义的时候必须初始化,初始化后就不能变了。sizeof指针得到的是本指针的大小,sizeof引用得到的是引用所指向变量的大小

应用区别

从使用上来讲:

引用

  • 一般在传递函数参数的时候我们首先选择引用传递。引用传递不需要创建临时变量,开销小。
  • 对栈空间大小比较敏感(比如递归)的时候使用引用。
  • 类对象作为参数传递的时候使用引用,这是C++类对象传递的标准方式

指针

  • 如果需要返回函数内局部变量的内存,需要用指针。
  • 如果数据是内置数据类型,则使用指针
  • 如果数据对象是数组,则只能使用指针

值传递、指针传递、引用传递的区别和效率

1) 值传递:值传递过程中,被调函数的形式参数作为被调函数的局部变量处理,会在栈中开辟内存空间以存放由主调函数传递进来的实参值,从而形成了实参的一个副本(替身)。

值传递的特点是,被调函数对形式参数的任何操作都是作为局部变量进行的,不会影响主调函数的实参变量的值(形参指针变了,实参指针不会变)。如果值传递的对象是类对象或是大的结构体对象,将耗费一定的时间和空间。(传值)

2) 指针传递:指针参数传递本质上是值传递,它所传递的是一个地址值。同样有一个形参向函数所属的栈拷贝数据的过程,但拷贝的数据是一个固定为4字节的地址。

3) 引用传递:引用参数传递过程中,被调函数的形式参数也作为局部变量在栈中开辟了内存空间,但是这时存放的是由主调函数放进来的实参变量的地址。

被调函数对形参(本体)的任何操作都被处理成间接寻址,即通过栈中存放的地址访问主调函数中的实参变量(根据别名找到主调函数中的本体)。

因此,被调函数对形参的任何操作都会影响主调函数中的实参变量。

4) 引用传递和指针传递是不同的,虽然他们都是在被调函数栈空间上的一个局部变量,但是任何对于引用参数的处理都会通过一个间接寻址的方式操作到主调函数中的相关变量。

而对于指针传递的参数,如果改变被调函数中的指针地址,它将应用不到主调函数的相关变量。如果想通过指针参数传递来改变主调函数中的相关变量(地址),那就得使用指向指针的指针或者指针引用。

5) 从编译的角度来讲,程序在编译时分别将指针和引用添加到符号表上,符号表中记录的是变量名及变量所对应地址。

指针变量在符号表上对应的地址值为指针变量的地址值,而引用在符号表上对应的地址值为引用对象的地址值(与实参名字不同,地址相同)。符号表生成之后就不会再改,因此指针可以改变其指向的对象(指针变量中的值可以改),而引用对象则不能修改。

6) 效率上讲,指针传递和引用传递比值传递效率高。使用引用传递函数的参数,在内存中并没有产生实参的副本,它是直接对实参操作,开销小,一般主张使用引用传递,代码逻辑上更加紧凑、清晰。

程序必须从main函数开始吗?

在C++语言中,程序并不一定要从main函数开始执行。实际上,在程序运行期间,操作系统会首先加载程序的代码和数据到内存中,并从程序的入口点开始执行。在C++语言中,程序的入口点是main函数。

然而,C++标准并没有规定程序必须从main函数开始。一些特殊的应用场景,例如裸机编程、嵌入式开发等,可能需要在程序启动时执行一些特殊的初始化工作,这时可以使用一些特殊的入口点。例如,在一些嵌入式系统中,程序可以从Reset向量处开始执行,执行一些硬件初始化工作,之后跳转到main函数开始执行应用程序代码。

在使用非标准入口点时,需要注意操作系统的限制和安全性问题。例如,一些操作系统可能会禁止程序从非main函数入口点开始执行,或者会对程序的入口点进行安全检查,以防止恶意程序的执行。

new / delete 与 malloc / free

  • 总体上来讲,前者是在C的基础上对后者的封装,封装之后作为C++运算符,申请释放内存更加安全便捷。安全就比如malloc大小与类型不对应时编译不会报错,而new是类型安全的;new也可以自己计算要分配的空间大小。
  • 底层实现
  • 另外,new 和 delete在C++中是运算符,那两个是标准库函数。malloc返回void*,需要手动计算大小

delete[]如何知道调用多少次

需要在 new [] 一个对象数组时,需要保存数组的维度,C++ 的做法是在分配数组空间时多分配了 4 个节的大小,专门保存数组的大小,在 delete [] 时就可以取出这个保存的数,就知道了需要调用析构函数多少次了。

malloc如何分配内存(实现原理)

从操作系统层面上看,malloc是通过两个系统调用来实现的: brk和mmap

brk是将进程数据段(.data)的最高地址指针向高处移动,这一步可以扩大进程在运行时的堆大小

mmap是在进程的虚拟地址空间中寻找一块空闲的虚拟内存,这一步可以获得一块可以操作的堆内存。

通常,分配的内存小于128k时,使用brk调用来获得虚拟内存,大于128k时就使用mmap来获得虚拟内存。

进程先通过这两个系统调用获取或者扩大进程的虚拟内存,获得相应的虚拟地址,在访问这些虚拟地址的时候,通过缺页中断,让内核分配相应的物理内存,这样内存分配才算完成。

malloc申请的存储空间能用delete释放吗?

不能,malloc /free主要为了兼容C,new和delete 完全可以取代malloc /free的。

malloc /free的操作对象都是必须明确大小的,而且不能用在动态类上。

new 和delete会自动进行类型检查和大小,malloc/free不能执行构造函数与析构函数,所以动态对象它是不行的。

当然从理论上说使用malloc申请的内存是可以通过delete释放的。不过一般不这样写的。而且也不能保证每个C++的运行时都能正常。

C++中有几种类型的new

在C++中,new有三种典型的使用方法:plain new,nothrow new和placement new

(1)plain new

言下之意就是普通的new,就是我们常用的new。plain new在空间分配失败的情况下,抛出异常std::bad_alloc而不是返回NULL

(2)nothrow new

nothrow new在空间分配失败的情况下是不抛出异常,而是返回NULL

(3)placement new

这种new允许在一块已经分配成功的内存上重新构造对象或对象数组。placement new不用担心内存分配失败,因为它根本不分配内存,它做的唯一一件事情就是调用对象的构造函数。

使用placement new需要注意两点:

palcement new的主要用途就是反复使用一块较大的动态分配的内存来构造不同类型的对象或者他们的

数组placement new构造起来的对象数组,要显式的调用他们的析构函数来销毁(析构函数并不释放对象的内存),千万不要使用delete,这是因为placement new构造起来的对象或数组大小并不一定等于原来分配的内存大小,使用delete会造成内存泄漏或者之后释放内存时出现运行时错误

类如何实现只能静态分配和只能动态分配

1) 前者是把new、delete运算符重载为private属性。后者是把构造、析构函数设为protected属性,再用子类来动态创建

2) 建立类的对象有两种方式:

①静态建立,静态建立一个类对象,就是由编译器为对象在栈空间中分配内存;

②动态建立,A *p = new A();动态建立一个类对象,就是使用new运算符为对象在堆空间中分配内存。这个过程分为两步,第一步执行operator new()函数,在堆中搜索一块内存并进行分配;第二步调用类构造函数构造对象;

3) 只有使用new运算符,对象才会被建立在堆上,因此只要限制new运算符就可以实现类对象只能建立在栈上,可以将new运算符设为私有

如何阻止一个类被实例化?有哪些方法?

1) 将类定义为抽象基类或者将构造函数声明为private;

2) 不允许类外部创建类对象,只能在类内部创建对象

宏定义与typedef const inline

  • 宏替换发生在预编译阶段(typedef是编译的一部分,而const在编译),之后被替换的文本参与编译,相当于直接插入了代码,运行时不存在函数调用。(函数)
  • 宏定义属于在结构中插入代码,没有返回值。(函数)
  • 宏定义参数没有类型,不进行类型检查。(函数,typedef,const)
  • 宏定义不是语句,不在最后加分号。(函数,typedef)
  • 宏主要用于定义常量及书写复杂的内容;typedef主要用于定义类型别名。
  • 宏定义的数据没有分配内存空间,只是插入替换掉(const)
  • 内联函数在编译时直接将函数代码嵌入到目标代码中,省去函数调用的开销来提高执行效率,并且进行参数类型检查,具有返回值,可以实现重载。

为什么不能把所有的函数写成内联函数?

内联函数以代码复杂为代价,它以省去函数调用的开销来提高执行效率。所以一方面如果内联函数体内代码执行时间相比函数调用开销较大,则没有太大的意义;另一方面每一处内联函数的调用都要复制代码,消耗更多的内存空间,因此以下情况不宜使用内联函数:

函数体内的代码比较长,将导致内存消耗代价函数体内有循环,函数执行时间要比函数调用开销大

const和static

static

1.先来介绍它的第一条也是最重要的一条:隐藏。(static函数,static变量均可)

当同时编译多个文件时,所有未加static前缀的全局变量和函数都具有全局可见性。

2.static的第二个作用是保持变量内容的持久

(static变量中的记忆功能和全局生存期)存储在静态数据区的变量会在程序刚开始运行时就完成初始化,也是唯一的一次初始化。共有两种变量存储在静态存储区:全局变量和static变量,只不过和全局变量比起来,static可以控制变量的可见范围,说到底static还是用来隐藏的。

3.static的第三个作用是默认初始化为0(static变量)

其实全局变量也具备这一属性,因为全局变量也存储在静态数据区。在静态数据区,内存中所有的字节默认值都是0x00,某些时候这一特点可以减少程序员的工作量。

4.static的第四个作用:C++中的类成员声明static

1) 函数体内static变量的作用范围为该函数体,不同于auto变量,该变量的内存只被分配一次,因此其值在下次调用时仍维持上次的值;

2) 在模块内的static全局变量可以被模块内所用函数访问,但不能被模块外其它函数访问;

3) 在模块内的static函数只可被这一模块内的其它函数调用,这个函数的使用范围被限制在声明它的模块内;

4) 在类中的static成员变量属于整个类所拥有,对类的所有对象只有一份拷贝;

5) 在类中的static成员函数属于整个类所拥有,这个函数不接收this指针,因而只能访问类的static成员变量。

类内:

6) static类对象必须要在类外进行初始化,static修饰的变量先于对象存在,所以static修饰的变量要在类外初始化;

7) 由于static修饰的类成员属于类,不属于对象,因此static类成员函数是没有this指针的,this指针是指向本对象的指针。正因为没有this指针,所以static类成员函数不能访问非static的类成员,只能访问 static修饰的类成员;

8) static成员函数不能被virtual修饰,static成员不属于任何对象或实例,所以加上virtual没有任何实际意义;静态成员函数没有this指针,虚函数的实现是为每一个对象分配一个vptr指针,而vptr是通过this指针调用的,所以不能为virtual;虚函数的调用关系,this->vptr->ctable->virtual function

const

1) 阻止一个变量被改变,可以使用const关键字。在定义该const变量时,通常需要对它进行初始化,因为以后就没有机会再去改变它了;

2) 对指针来说,可以指定指针本身为const,也可以指定指针所指的数据为const,或二者同时指定为const;

3) 在一个函数声明中,const可以修饰形参,表明它是一个输入参数,在函数内部不能改变其值;

4) 对于类的成员函数,若指定其为const类型,则表明其是一个常函数,不能修改类的成员变量,类的常对象只能访问类的常成员函数;

5) 对于类的成员函数,有时候必须指定其返回值为const类型,以使得其返回值不为“左值”。

6) const成员函数可以访问非const对象的非const数据成员、const数据成员,也可以访问const对象内的所有数据成员;

7) 非const成员函数可以访问非const对象的非const数据成员、const数据成员,但不可以访问const对象的任意数据成员;

8) 一个没有明确声明为const的成员函数被看作是将要修改对象中数据成员的函数,而且编译器不允许它为一个const对象所调用。因此const对象只能调用const成员函数。

9) const类型变量可以通过类型转换符const_cast将const类型转换为非const类型;

10) const类型变量必须定义的时候进行初始化,因此也导致如果类的成员变量有const类型的变量,那么该变量必须在类的初始化列表中进行初始化;

全局变量和static变量的区别

这两者在存储方式上并无不同。这两者的区别在于非静态全局变量的作用域是整个源程序,当一个源程序由多个原文件组成时,非静态的全局变量在各个源文件中都是有效的。 而静态全局变量则限制了其作用域,即只在定义该变量的源文件内有效,在同一源程序的其它源文件中不能使用它。由于静态全局变量的作用域限于一个源文件内,只能为该源文件内的函数公用,因此可以避免在其他源文件中引起错误。

static全局变量与普通的全局变量的区别是static全局变量只初始化一次,防止在其他文件单元被引用。

static函数与普通函数有什么区别?

static函数与普通的函数作用域不同,仅在本文件中。只在当前源文件中使用的函数应该说明为内部函数(static),内部函数应该在当前源文件中说明和定义。

对于可在当前源文件以外使用的函数应该在一个头文件中说明,要使用这些函数的源文件要包含这个头文件。

static函数与普通函数最主要区别是static函数在内存中只有一份,普通静态函数在每个被调用中维持一份拷贝程序的局部变量存在于(堆栈)中,全局变量存在于(静态区)中,动态申请数据存在于(堆)

初始化时机

全局变量和静态变量在程序运行前就已经被初始化了,具体时间取决于变量定义的位置和类型。

全局变量和静态变量的定义位置分为两种情况:

  1. 定义在函数外部:此时全局变量和静态变量会在程序加载时初始化。如果没有指定初始值,则全局变量和静态变量会被自动初始化为0或NULL。
  2. 定义在函数内部:此时静态变量会在程序运行时第一次进入该函数时初始化,而全局变量在函数内部定义不合法。如果没有指定初始值,则静态变量会被自动初始化为0或NULL。

需要注意的是,全局变量和静态变量的初始化顺序是按照它们在程序中出现的顺序进行的。如果两个变量相互依赖,那么它们的初始化顺序就很重要,否则会出现未定义的行为。

全局变量和局部变量有什么区别?

生命周期不同:全局变量随主程序创建和创建,随主程序销毁而销毁;局部变量在局部函数内部,甚至局部循环体等内部存在,退出就不存在;

使用方式不同:通过声明后全局变量在程序的各个部分都可以用到;局部变量分配在堆栈区,只能在局部使用。

操作系统和编译器通过内存分配的位置可以区分两者,全局变量分配在全局数据段并且在程序开始运行的时候被加载。局部变量则分配在堆栈里面 。

静态成员与普通成员的区别是什么?

1) 生命周期

静态成员变量从类被加载开始到类被卸载,一直存在;

普通成员变量只有在类创建对象后才开始存在,对象结束,它的生命期结束;

2) 共享方式

静态成员变量是全类共享;普通成员变量是每个对象单独享用的;

3) 定义位置

普通成员变量存储在栈或堆中,而静态成员变量存储在静态全局区;

4) 初始化位置

普通成员变量在类中初始化;静态成员变量在类外初始化;

5) 默认实参

可以使用静态成员变量作为默认实参,

静态变量什么时候初始化

1) 初始化只有一次,但是可以多次赋值,在主程序之前,编译器已经为其分配好了内存。

2) 静态局部变量和全局变量一样,数据都存放在全局区域,所以在主程序之前,编译器已经为其分配好了内存,但在C和C++中静态局部变量的初始化节点又有点不太一样。在C中,初始化发生在代码执行之前,编译阶段分配好内存之后,就会进行初始化,所以我们看到在C语言中无法使用变量对静态局部变量进行初始化,在程序运行结束,变量所处的全局内存会被全部回收。

3) 而在C++中,初始化时在执行相关代码时才会进行初始化,主要是由于C++引入对象后,要进行初始化必须执行相应构造函数和析构函数,在构造函数或析构函数中经常会需要进行某些程序中需要进行的特定操作,并非简单地分配内存。所以C++标准定为全局或静态对象是有首次用到时才会进行构造,并通过atexit()来管理。在程序结束,按照构造顺序反方向进行逐个析构。所以在C++中是可以使用变量对静态局部变量进行初始化的

如何设计一个类计算子类的个数?

1、为类设计一个static静态变量count作为计数器;

2、类定义结束后初始化count;

3、在构造函数中对count进行+1;

4、 设计拷贝构造函数,在进行拷贝构造函数中进行count +1,操作;

5、设计复制构造函数,在进行复制函数中对count+1操作;

6、在析构函数中对count进行-1;

C++的顶层const和底层const

概念区分

顶层const:指的是const修饰的变量本身是一个常量,无法修改,指的是指针,就是 * 号的右边

底层const:指的是const修饰的变量所指向的对象是一个常量,指的是所指变量,就是 * 号的左边

int a = 10;int* const b1 = &a; //顶层const,b1本身是一个常量
const int* b2 = &a; //底层const,b2本身可变,所指的对象是常量
const int b3 = 20; //顶层const,b3是常量不可变
const int* const b4 = &a; //前一个const为底层,后一个为顶层,b4不可变
const int& b5 = a; //用于声明引用变量,都是底层const

区分作用

执行对象拷贝时有限制,常量的底层const不能赋值给非常量的底层const

使用命名的强制类型转换函数const_cast时,只能改变运算对象的底层const

final和override关键字

override

当在父类中使用了虚函数时候,你可能需要在某个子类中对这个虚函数进行重写

如果不使用override,当你手一抖,将foo()写成了f00()会怎么样呢?结果是编译器并不会报错,因为它并不知道你的目的是重写虚函数,而是把它当成了新的函数。如果这个虚函数很重要的话,那就会对整个程序不利。所以,override的作用就出来了,它指定了子类的这个虚函数是重写的父类的,如果你名字不小心打错了的话,编译器是不会编译通过的。

final

当不希望某个类被继承,或不希望某个虚函数被重写,可以在类名和虚函数后添加final关键字,添加 final关键字后被继承或重写,编译器会报错。

volatile、mutable和explicit关键字的用法

(1)volatile

volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改,比如:操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。 告诉编译器不要去假设(优化)这个变量的值,因为这个变量可能会被意想不到地改变。精确地说就是,优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。

当要求使用 volatile 声明的变量的值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。

volatile定义变量的值是易变的,每次用到这个变量的值的时候都要去重新读取这个变量的值,而不是读寄存器内的备份。多线程中被几个任务共享的变量需要定义为volatile类型。

volatile 指针

volatile 指针和 const 修饰词类似,const 有常量指针和指针常量的说法,volatile 也有相应的概念

注意:

  • 可以把一个非volatile int赋给volatile int,但是不能把非volatile对象赋给一个volatile对象。
  • 除了基本类型外,对用户定义类型也可以用volatile类型进行修饰。
  • C++中一个有volatile标识符的类只能访问它接口的子集,一个由类的实现者控制的子集。用户只能用const_cast来获得对类型接口的完全访问。此外,volatile向const一样会从类传递到它的成员。

volatile用在如下的几个地方:

1) 中断服务程序中修改的供其它程序检测的变量需要加volatile;

2) 多任务环境下各任务间共享的标志应该加volatile;

3) 存储器映射的硬件寄存器通常也要加volatile说明,因为每次对它的读写都可能由不同意义;

多线程下的volatile

有些变量是用volatile关键字声明的。当两个线程都要用到某一个变量且该变量的值会被改变时,应该用volatile声明,该关键字的作用是防止优化编译器把变量从内存装入CPU寄存器中。如果变量被装入寄存器,那么两个线程有可能一个使用内存中的变量,一个使用寄存器中的变量,这会造成程序的错误执行。volatile的意思是让编译器每次操作该变量时一定要从内存中真正取出,而不是使用已经存在寄存器中的值。

(2)mutable

mutable的中文意思是“可变的,易变的”,跟constant(既C++中的const)是反义词。在C++中,mutable也是为了突破const的限制而设置的。被mutable修饰的变量,将永远处于可变的状态,即使在一个const函数中。我们知道,如果类的成员函数不会改变对象的状态,那么这个成员函数一般会声明成const的。但是,有些时候,我们需要在const函数里面修改一些跟类状态无关的数据成员,那么这个函数就应该被mutable来修饰,并且放在函数后后面关键字位置

(3)explicit (下一个问题)

explicit关键字用来修饰类的构造函数,被修饰的构造函数的类,不能发生相应的隐式类型转换,只能以显示的方式进行类型转换,注意以下几点:

  • explicit 关键字只能用于类内部的构造函数声明上
  • explicit 关键字作用于单个参数的构造函数
  • 被explicit修饰的构造函数的类,不能发生相应的隐式类型转换

隐式转换,如何消除隐式转换?

1、C++的基本类型中并非完全的对立,部分数据类型之间是可以进行隐式转换的。所谓隐式转换,是指不需要用户干预,编译器私下进行的类型转换行为。很多时候用户可能都不知道进行了哪些转换

2、C++面向对象的多态特性,就是通过父类的类型实现对子类的封装。通过隐式转换,你可以直接将一个子类的对象使用父类的类型进行返回。在比如,数值和布尔类型的转换,整数和浮点数的转换等。某些方面来说,隐式转换给C++程序开发者带来了不小的便捷。C++是一门强类型语言,类型的检查是非常严格的。

3、 基本数据类型 基本数据类型的转换以取值范围的作为转换基础(保证精度不丢失)。隐式转换发生在从小->大的转换中。比如从char转换为int。从int->long。自定义对象子类对象可以隐式的转换为父类对象。

4、 C++中提供了explicit关键字,在构造函数声明的时候加上explicit关键字,能够禁止隐式转换。

5、如果构造函数只接受一个参数,则它实际上定义了转换为此类类型的隐式转换机制。可以通过将构造函数声明为explicit加以制止隐式类型转换,关键字explicit只对一个实参的构造函数有效,需要多个实参的构造函数不能用于执行隐式转换,所以无需将这些构造函数指定为explicit。

C++ 的 四 种 强 制 转 换 reinterpret_cast/const_cast/static_cast/dynamic_cast

reinterpret_cast

reinterpret_cast<type-id> (expression)

type-id 必须是一个指针、引用、算术类型、函数指针或者成员指针。它可以用于类型之间进行强制转换。

const_cast

const_cast<type_id> (expression)

该运算符用来修改类型的const或volatile属性。除了const 或volatile修饰之外, type_id和expression的类型是一样的。用法如下:

  • 常量指针被转化成非常量的指针,并且仍然指向原来的对象
  • 常量引用被转换成非常量的引用,并且仍然指向原来的对象
  • const_cast一般用于修改底指针。如const char *p形式

static_cast

static_cast < type-id > (expression)

该运算符把expression转换为type-id类型,但没有运行时类型检查来保证转换的安全性。它主要有如下几种用法:

  • 用于类层次结构中基类(父类)和派生类(子类)之间指针或引用引用的转换
  • 用于基本数据类型之间的转换,如把int转换成char,把int转换成enum。这种转换的安全性也要开发人员来保证。
  • 把空指针转换成目标类型的空指针
  • 把任何类型的表达式转换成void类型

注意:static_cast不能转换掉expression的const、volatile、或者__unaligned属性。

dynamic_cast

有类型检查,基类向派生类转换比较安全,但是派生类向基类转换则不太安全

dynamic_cast (expression)

该运算符把expression转换成type-id类型的对象。type-id 必须是类的指针、类的引用或者void*如果 type-id 是类指针类型,那么expression也必须是一个指针,如果 type-id 是一个引用,那么expression 也必须是一个引用

dynamic_cast运算符可以在执行期决定真正的类型,也就是说expression必须是多态类型。如果下行转换是安全的(也就说,如果基类指针或者引用确实指向一个派生类对象)这个运算符会传回适当转型过的指针。如果 如果下行转换不安全,这个运算符会传回空指针(也就是说,基类指针或者引用没有指向一个派生类对象)

dynamic_cast主要用于类层次间的上行转换和下行转换,还可以用于类之间的交叉转换

在类层次间进行上行转换时,dynamic_cast和static_cast的效果是一样的

在进行下行转换时,dynamic_cast具有类型检查的功能,比static_cast更安全

static_cast比C语言中的转换强在哪里?

1) 更加安全;

2) 更直接明显,能够一眼看出是什么类型转换为什么类型,容易找出程序中的错误;可清楚地辨别代码中每个显式的强制转;可读性更好,能体现程序员的意图

使用dynamic_cast进行指针类型转换的类中需定义什么?

虚函数,必须是多态

形参与实参的区别?

1) 形参变量只有在被调用时才分配内存单元,在调用结束时,即刻释放所分配的内存单元。因此,形参只有在函数内部有效。 函数调用结束返回主调函数后则不能再使用该形参变量。

2) 实参可以是常量、变量、表达式、函数等,无论实参是何种类型的量,在进行函数调用时,它们都必须具有确定的值,以便把这些值传送给形参。 因此应预先用赋值,输入等办法使实参获得确定值,会产生一个临时变量。

3) 实参和形参在数量上,类型上,顺序上应严格一致,否则会发生“类型不匹配”的错误。

4) 函数调用中发生的数据传送是单向的。 即只能把实参的值传送给形参,而不能把形参的值反向地传送给实参。 因此在函数调用过程中,形参的值发生改变,而实参中的值不会变化。

5) 当形参和实参不是指针类型时,在该函数运行时,形参和实参是不同的变量,他们在内存中位于不同的位置,形参将实参的内容复制一份,在该函数运行结束的时候形参被释放,而实参内容不会改变。

常量指针和指针常量区别?

指针常量是一个指针,读成常量的指针,指向一个只读变量,也就是后面所指明的int const 和 const int,都是一个常量,可以写作int const *p或const int *p。

常量指针是一个不能给改变指向的指针。指针是个常量,必须初始化,一旦初始化完成,它的值(也就是存放在指针中的地址)就不能在改变了,即不能中途改变指向,如int *const p。

数组名和指针区别?

二者均可通过增减偏移量来访问数组中的元素。

数组名不是真正意义上的指针,可以理解为常指针,所以数组名没有自增、自减等操作。当数组名当做形参传递给调用函数后,就失去了原有特性,退化成一般指针,多了自增、自减操作,但sizeof运算符不能再得到原数组的大小了。

假设数组int a[10]; int (*p)[10] = &a;其中:

  • a是数组名,是数组首元素地址,+1表示地址值加上一个int类型的大小,如果a的值是0x00000001,加1操作后变为0x00000005。*(a + 1) = a[1]。
  • &a是数组的指针,其类型为int (*)[10](就是前面提到的数组指针),其加1时,系统会认为是数组首地址加上整个数组的偏移(10个int型变量),值为数组a尾元素后一个元素的地址。
  • 若(int *)p ,此时输出 *p时,其值为a[0]的值,因为被转为int *类型,解引用时按照int类型大小来读取。

数组和指针的区别

1) 数组在内存中是连续存放的,开辟一块连续的内存空间;数组所占存储空间:sizeof(数组名);数组大小:sizeof(数组名)/sizeof(数组元素数据类型);

2) 用运算符sizeof 可以计算出数组的容量(字节数)。sizeof(p),p 为指针得到的是一个指针变量的字节数,而不是p 所指的内存容量。

3) 编译器为了简化对数组的支持,实际上是利用指针实现了对数组的支持。具体来说,就是将表达式中的数组元素引用转换为指针加偏移量的引用。

4) 在向函数传递参数的时候,如果实参是一个数组,那用于接受的形参为对应的指针。也就是传递过去是数组的首地址而不是整个数组,能够提高效率;

5) 在使用下标的时候,两者的用法相同,都是原地址加上下标值,不过数组的原地址就是数组首元素的地址是固定的,指针的原地址就不是固定的

野指针和悬空指针

讲智能指针可以引申到这里,,也可以从这里引申到智能指针的思想(类,)

都是是指向无效内存区域(这里的无效指的是"不安全不可控")的指针,访问行为将会导致未定义行为。

野指针

野指针,指的是没有被初始化过的指针

为了防止出错,对于指针初始化时都是赋值为 nullptr,这样在使用时编译器就会直接报错,产生非法内存访问。

悬空指针

悬空指针,指针最初指向的内存已经被释放了的一种指针。

避免野指针比较简单,但悬空指针比较麻烦。c++引入了智能指针,C++智能指针的本质就是避免悬空指针的产生。

函数指针?

1) 什么是函数指针?

函数指针指向的是特殊的数据类型,函数的类型是由其返回的数据类型和其参数列表共同决定的,而函数的名称则不是其类型的一部分。

一个具体函数的名字,如果后面不跟调用符号(即括号),则该名字就是该函数的指针(注意:大部分情况下,可以这么认为,但这种说法并不很严格)。

2) 函数指针的声明方法

int (*pf)(const int&, const int&); (1)

上面的pf就是一个函数指针,指向所有返回类型为int,并带有两个const int&参数的函数。注意*pf两边的括号是必须的,否则上面的定义就变成一个函数声明pf,其返回类型为int *,带有两个const int&参数。

3) 为什么有函数指针

函数与数据项相似,函数也有地址。我们希望在同一个函数中通过使用相同的形参在不同的时间使用产生不同的效果。

#include <stdio.h>

int add(int a, int b) {
    return a + b;
}

int subtract(int a, int b) {
    return a - b;
}

int calculate(int (*operation)(int, int), int a, int b) {
    return operation(a, b);
}

int main() {
    int a = 10, b = 5;
    int result;

    result = calculate(add, a, b);
    printf("Addition result: %d\n", result);

    result = calculate(subtract, a, b);
    printf("Subtraction result: %d\n", result);

    return 0;
}

两种方法赋值:

指针名 = 函数名;指针名 = &函数名

函数指针可以用于实现回调函数。例如,我们可以定义一个函数指针类型,然后将回调函数作为该类型的变量传递给其他函数。当事件发生时,其他函数将调用该函数指针变量,从而调用回调函数。

回调函数

1) 当发生某种事件时,系统或其他函数将会自动调用你定义的一段函数;

2) 回调函数就相当于一个中断处理函数,由系统在符合你设定的条件时自动调用。为此,你需要做三件事:1,声明;2,定义;3,设置触发条件,就是在你的函数中把你的回调函数名称转化为地址作为一个参数,以便于系统调用;

3) 回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用为调用它所指向的函数时,我们就说这是回调函数;

4) 因为可以把调用者与被调用者分开。调用者不关心谁是被调用者,所有它需知道的,只是存在一个具有某种特定原型、某些限制条件(如返回值为int)的被调用函数。

浅拷贝和深拷贝的区别

浅拷贝

浅拷贝只是拷贝一个指针,并没有新开辟一个地址,拷贝的指针和原来的指针指向同一块地址,如果原来的指针所指向的资源释放了,那么再释放浅拷贝的指针的资源就会出现错误。

深拷贝

深拷贝不仅拷贝值,还开辟出一块新的空间用来存放新的值,即使原先的对象被析构掉,释放内存了也不会影响到深拷贝得到的值。在自己实现拷贝赋值的时候,如果有指针变量的话是需要自己实现深拷贝的。

怎样判断两个浮点数是否相等?

对两个浮点数判断大小和是否相等不能直接用==来判断,会出错!明明相等的两个数比较反而是不相等!对于两个浮点数比较只能通过相减并与预先设定的精度比较,记得要取绝对值!浮点数与0的比较也应该注意。与浮点数的表示方式有关。

C++的异常处理的方法

1) C++中的异常情况:

语法错误(编译错误):比如变量未定义、括号不匹配、关键字拼写错误等等编译器在编译时能发现的错误,这类错误可以及时被编译器发现,而且可以及时知道出错的位置及原因,方便改正。

运行时错误:比如数组下标越界、系统内存不足等等。这类错误不易被程序员发现,它能通过编译且能进入运行,但运行时会出错,导致程序崩溃。为了有效处理程序运行时错误,C++中引入异常处理机制来解决此问题

(1)try、throw和catch关键字

C++中的异常处理机制主要使用trythrowcatch三个关键字

(2)函数的异常声明列表

有时候,程序员在定义函数的时候知道函数可能发生的异常,可以在函数声明和定义时,指出所能抛出异常的列表

(3)C++标准异常类 exception

C++ 标准库中有一些类代表异常,这些类都是从 exception 类派生而来的

  • bad_typeid:使用typeid运算符,如果其操作数是一个多态类的指针,而该指针的值为 NULL,则会拋出此异常
  • bad_cast:在用 dynamic_cast 进行从多态基类对象(或引用)到派生类的引用的强制类型转换时,如果转换是不安全的,则会拋出此异常
  • bad_alloc:在用 new 运算符进行动态内存分配时,如果没有足够的内存,则会引发此异常
  • out_of_range:用 vector 或 string的at 成员函数根据下标访问元素时,如果下标越界,则会拋出此异常

类成员初始化方式?构造函数的执行顺序 ?为什么用成员初始化列表会快一些?

1) 赋值初始化,通过在函数体内进行赋值初始化;列表初始化,在冒号后使用初始化列表进行初始化。

这两种方式的主要区别在于:

  • 对于在函数体中初始化,是在所有的数据成员被分配内存空间后才进行的。
  • 列表初始化是给数据成员分配内存空间时就进行初始化,就是说分配一个数据成员只要冒号后有此数据成员的赋值表达式(此表达式必须是括号赋值表达式),那么分配了内存空间后在进入函数体之前给数据成员赋值,就是说初始化这个数据成员此时函数体还未执行。

用初始化列表会快一些的原因是,对于类型,它少了一次调用构造函数的过程,而在函数体中赋值则会多一次调用。而对于内置数据类型则没有差别。 由于对象成员变量的初始化动作发生在进入构造函数之前,对于内置类型没什么影响,但如果有些成员是类,那么在进入构造函数之前,会先调用一次默认构造函数,进入构造函数后所做的事其实是一次赋值操作(对象已存在),所以如果是在构造函数体内进行赋值的话,等于是一次默认构造加一次赋值,而初始化列表只做一次赋值操作。

2) 一个派生类构造函数的执行顺序如下:

①虚拟基类的构造函数(多个虚拟基类则按照继承的顺序执行构造函数)。

②基类的构造函数(多个普通基类也按照继承的顺序执行构造函数)。

③类类型的成员对象的构造函数(按照初始化顺序)

④派生类自己的构造函数。

3) 方法一是在构造函数当中做赋值的操作,而方法二是做纯粹的初始化操作。我们都知道,C++的赋值

操作是会产生临时对象的。临时对象的出现会降低程序的效率。

有哪些情况必须用到成员列表初始化?作用是什么?

1) 必须使用成员初始化的四种情况

①当初始化一个引用成员时;

②当初始化一个常量成员时;

③当调用一个基类的构造函数,而它拥有一组参数时;

④当调用一个成员类的构造函数,而它拥有一组参数时;

2) 成员初始化列表做了什么

①编译器会一一操作初始化列表,以适当的顺序在构造函数之内安插初始化操作,并且在任何显示用户代码之前;

② list中的项目顺序是由类中的成员声明顺序决定的,不是由初始化列表的顺序决定的;

成员初始化顺序与类内声明顺序有关还是和构造函数初始化列表中的初始化顺序有关

对于一个类中的变量,初始化的顺序并不是按照初始化成员列表的顺序进行初始化,而是根据类中变量定义的顺序来初始化的

为什么友元函数必须在类内部声明?

因为编译器必须能够读取这个结构的声明以理解这个数据类型的大小、行为等方面的所有规则。

有一条规则在任何关系中都很重要,那就是谁可以访问我的私有部分。

友元函数和友元类

友元提供了不同类的成员函数之间、类的成员函数和一般函数之间进行数据共享的机制。通过友元,一个不同函数或者另一个类中的成员函数可以访问类中的私有成员和保护成员。友元的正确使用能提高程序的运行效率,但同时也破坏了类的封装性和数据的隐藏性,导致程序可维护性变差。

1)友元函数

有元函数是定义在类外的普通函数,不属于任何类,可以访问其他类的私有成员。但是需要在类的定义中声明所有可以访问它的友元函数。一个函数可以是多个类的友元函数,但是每个类中都要声明这个函数。

2)友元类

友元类的所有成员函数都是另一个类的友元函数,都可以访问另一个类中的隐藏信息(包括私有成员和保护成员)。但是另一个类里面也要相应的进行声明

使用友元类时注意:

(1) 友元关系不能被继承

(2) 友元关系是单向的,不具有交换性。若类B是类A的友元,类A不一定是类B的友元,要看在类中是否有相应的声明。

(3) 友元关系不具有传递性。若类B是类A的友元,类C是B的友元,类C不一定是类A的友元,同样要看类中是否有相应的申明

讲讲程序加载运行的全过程(代码到可执行程序)

(1)预编译

主要处理源代码文件中的以“#”开头的预编译指令。处理规则见下:

1. 删除所有的#define,展开所有的宏定义。

2. 处理所有的条件预编译指令,如“#if”、“#endif”、“#ifdef”、“#elif”和“#else”。

3. 处理“#include”预编译指令,将文件内容替换到它的位置,这个过程是递归进行的,文件中包含其他文件。

4. 删除所有的注释,“//”和“/**/”。

-5. 保留所有的#pragma 编译器指令,编译器需要用到他们,如:#pragma once 是为了防止有文件被重复引用。

6. 添加行号和文件标识,便于编译时编译器产生调试用的行号信息,和编译时产生编译错误或警告是能够显示行号。

(2)编译

把预编译之后生成的xxx.i或xxx.ii文件,进行一系列词法分析、语法分析、语义分析及优化后,生成相应的汇编代码文件。

1. 词法分析:利用类似于“有限状态机”的算法,将源代码程序输入到扫描机中,将其中的字符序列分割成一系列的记号。

2. 语法分析:语法分析器对由扫描器产生的记号,进行语法分析,产生语法树。由语法分析器输出的语法树是一种以表达式为节点的树。

3. 语义分析:语法分析器只是完成了对表达式语法层面的分析,语义分析器则对表达式是否有意义进行判断,其分析的语义是静态语义——在编译期能分期的语义,相对应的动态语义是在运行期才能确定的语义。

4. 优化:源代码级别的一个优化过程。

5. 目标代码生成:由代码生成器将中间代码转换成目标机器代码,生成一系列的代码序列——汇编语言表示。

6. 目标代码优化:目标代码优化器对上述的目标机器代码进行优化:寻找合适的寻址方式、使用位移来替代乘法运算、删除多余的指令等。

(3)汇编

将汇编代码转变成机器可以执行的指令(机器码文件)。 汇编器的汇编过程相对于编译器来说更简单,没有复杂的语法,也没有语义,更不需要做指令优化,只是根据汇编指令和机器指令的对照表一一翻译过来,汇编过程有汇编器as完成。经汇编之后,产生目标文件(与可执行文件格式几乎一样)xxx.o(Windows下)、xxx.obj(Linux下)。

(4)链接

将不同的源文件产生的目标文件进行链接,从而形成一个可以执行的程序。链接分为静态链接和动态链接:

静态链接

静态编译,编译器在编译可执行文件时,把需要用到的对应动态链接库中的部分提取出来,连接到可执行文件中去,使可执行文件在运行时不需要依赖于动态链接库;

空间浪费:因为每个可执行程序中对所有需要的目标文件都要有一份副本,所以如果多个程序对同一个目标文件都有依赖,会出现同一个目标文件都在内存存在多个副本;

更新困难:每当库函数的代码修改了,这个时候就需要重新进行编译链接形成可执行程序。

运行速度快:但是静态链接的优点就是,在可执行程序中已经具备了所有执行程序所需要的任何东西,在执行的时候运行速度快。

动态链接

动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件。

共享库:就是即使需要每个程序都依赖同一个库,但是该库不会像静态链接那样在内存中存在多分,副本,而是这多个程序在执行时共享同一份副本;

更新方便:更新时只需要替换原来的目标文件,而无需将所有的程序再重新链接一遍。当程序下一次运行时,新版本的目标文件会被自动加载到内存并且链接起来,程序就完成了升级的目标。

性能损耗:因为把链接推迟到了程序运行时,所以每次执行程序都需要进行链接,所以性能会有一定损失。

C++函数调用的压栈过程

当函数从入口函数main函数开始执行时,编译器会将我们操作系统的运行状态,main函数的返回地址、main的参数、mian函数中的变量、进行依次压栈;

当main函数开始调用func()函数时,编译器此时会将main函数的运行状态进行压栈,再将func()函数的返回地址、func()函数的参数从右到左、func()定义变量依次压栈;

当func()调用f()的时候,编译器此时会将func()函数的运行状态进行压栈,再将的返回地址、f()函数的参数从右到左、f()定义变量依次压栈

文字化表述

函数的调用过程:

1)从栈空间分配存储空间

2)从实参的存储空间复制值到形参栈空间

3)进行运算

形参在函数未调用之前都是没有分配存储空间的,在函数调用结束之后,形参弹出栈空间,清除形参空间。

数组作为参数的函数调用方式是地址传递,形参和实参都指向相同的内存空间,调用完成后,形参指针被销毁,但是所指向的内存空间依然存在,不能也不会被销毁。

当函数有多个返回值的时候,不能用普通的 return 的方式实现,需要通过传回地址的形式进行,即地址/指针传递。

方法调用的原理(栈,汇编,说上边的吧)

1) 机器用栈来传递过程参数、存储返回信息、保存寄存器用于以后恢复,以及本地存储。而为单个过程分配的那部分栈称为帧栈;帧栈可以认为是程序栈的一段,它有两个端点,一个标识起始地址,一个标识着结束地址,两个指针结束地址指针esp,开始地址指针ebp;

2) 由一系列栈帧构成,这些栈帧对应一个过程,而且每一个栈指针+4的位置存储函数返回地址;每一个栈帧都建立在调用者的下方,当被调用者执行完毕时,这一段栈帧会被释放。由于栈帧是向地址递减的方向延伸,因此如果我们将栈指针减去一定的值,就相当于给栈帧分配了一定空间的内存。如果将栈指针加上一定的值,也就是向上移动,那么就相当于压缩了栈帧的长度,也就是说内存被释放了。

3) 过程实现

①备份原来的帧指针,调整当前的栈帧指针到栈指针位置;

②建立起来的栈帧就是为被调用者准备的,当被调用者使用栈帧时,需要给临时变量分配预留内存;

③使用建立好的栈帧,比如读取和写入,一般使用mov,push以及pop指令等等。

④恢复被调用者寄存器当中的值,这一过程其实是从栈帧中将备份的值再恢复到寄存器,不过此时这些值可能已经不在栈顶了

⑤恢复被调用者寄存器当中的值,这一过程其实是从栈帧中将备份的值再恢复到寄存器,不过此时这些值可能已经不在栈顶了。

⑥释放被调用者的栈帧,释放就意味着将栈指针加大,而具体的做法一般是直接将栈指针指向帧指针,因此会采用类似下面的汇编代码处理。

⑦恢复调用者的栈帧,恢复其实就是调整栈帧两端,使得当前栈帧的区域又回到了原始的位置。

⑧弹出返回地址,跳出当前过程,继续执行调用者的代码。

4) 过程调用和返回指令

① call指令

② leave指令

③ ret指令

结构体内存对齐问题?

  • 结构体内成员按照声明顺序存储,第一个成员地址和整个结构体地址相同。
  • 未特殊说明时,按结构体中size最大的成员对齐(若有double成员,按8字节对齐。)

c++11以后引入两个关键字 alignasalignof。其中alignof可以计算出类型的对齐方式,alignas可以指

定结构体的对齐方式。

被free回收的内存是立即返还给操作系统吗?

不是的,被free回收的内存会首先被ptmalloc使用双链表保存起来,当用户下一次申请内存的时候,会尝

试从这些内存中寻找合适的返回。这样就避免了频繁的系统调用,占用过多的系统资源。同时ptmalloc

也会尝试对小块内存进行合并,避免过多的内存碎片。

声明和定义区别?

如果是指变量的声明和定义:

从编译原理上来说,声明是仅仅告诉编译器,有个某类型的变量会被使用,但是编译器并不会为它分配任何内存。而定义就是分配了内存。

如果是指函数的声明和定义:

声明:一般在头文件里,对编译器说:这里我有一个函数叫function() 让编译器知道这个函数的存在。

定义:一般在源文件里,具体就是函数的实现过程 写明函数体。

strcpy,printf和memcpy的区别是什么?

1、复制的内容不同。

  • strcpy的两个操作对象均为字符串
  • sprintf的操作源对象可以是多种数据类型,目的操作对象是字符串
  • memcpy可以复制任意内容,例如字符数组、整型、结构体、类等

2、复制的方法不同。strcpy不需要指定长度,它遇到被复制字符的串结束符"\0"才结束,所以容易溢出。memcpy则是根据其第3个参数决定复制的长度。

3、 实现功能不同

  • strcpy主要实现字符串变量间的拷贝
  • sprintf主要实现其他数据类型格式到字符串的转化
  • memcpy主要是内存块间的拷贝。

strcpy函数和strncpy函数的区别?哪个函数更安全?

char* strcpy(char* strDest, const char* strSrc)
char* strncpy(char* strDest, const char* strSrc, int pos)

strcpy函数: 如果参数 dest 所指的内存空间不够大,可能会造成缓冲溢出(buffer Overflow)的错误情况,在编写程序时请特别留意,或者用strncpy()来取代。

strncpy函数:用来复制源字符串的前n个字符,src 和 dest 所指的内存区域不能重叠,且 dest 必须有足够的空间放置n个字符。

3) 如果目标长>指定长>源长,则将源长全部拷贝到目标长,自动加上’\0’

如果指定长<源长,则将源长中按指定长度拷贝到目标字符串,不包括’\0’

如果指定长>目标长,运行时错误 ;

成员函数里memset(this,0,sizeof(*this))会发生什么

1) 有时候类里面定义了很多int,char,struct等c语言里的那些类型的变量,我习惯在构造函数中将它们初始化为0,但是一句句的写太麻烦,所以直接就memset(this, 0, sizeof *this);将整个对象的内存全部置为0。对于这种情形可以很好的工作,但是下面几种情形是不可以这么使用的;

2) 类含有虚函数表:这么做会破坏虚函数表,后续对虚函数的调用都将出现异常;

3) 类中含有C++类型的对象:例如,类中定义了一个list的对象,由于在构造函数体的代码执行之前就对list对象完成了初始化,假设list在它的构造函数里分配了内存,那么我们这么一做就破坏了list对象的内存。

strlen和sizeof区别?

sizeof是运算符,并不是函数,结果在编译时得到而非运行中获得;strlen是字符处理的库函数。

sizeof参数可以是任何数据的类型或者数据(sizeof参数不退化);strlen的参数只能是字符指针且结尾是'\0'的字符串。

因为sizeof值在编译时确定,所以不能用来得到动态分配(运行时分配)存储空间的大小。

C++中新增了string,它与C语言中的 char *有什么区别吗?它是如何实现的?

string继承自basic_string,其实是对char*进行了封装,封装的string包含了char*数组,容量,长度等等属性。

string可以进行动态扩展,在每次扩展的时候另外申请一块原空间大小两倍的空间(2^n),然后将原字符串拷贝过去,并加上新增的内容。

string 是c++标准库里面其中一个,封装了对字符串的操作,实际操作过程我们可以用const char*给string类初始化

对象复用的了解,零拷贝的了解

对象复用

对象复用其本质是一种设计模式:Flyweight享元模式。

通过将对象存储到“对象池”中实现对象的重复利用,这样可以避免多次创建重复对象的开销,节约系统资源。

零拷贝

零拷贝就是一种避免 CPU 将数据从一块存储拷贝到另外一块存储的技术。

零拷贝技术可以减少数据拷贝和共享总线操作的次数。

在C++中,vector的一个成员函数emplace_back()很好地体现了零拷贝技术,它跟push_back()函数一样可以将一个元素插入容器尾部,区别在于:使用push_back()函数需要调用拷贝构造函数和转移构造函数,而使用emplace_back()插入的元素原地构造,不需要触发拷贝构造和转移构造,效率更高。

在main执行之前和之后执行的代码可能是什么?

main函数执行之前,主要就是初始化系统相关资源:

  • 设置栈指针
  • 初始化静态static变量和global全局变量,即.data段的内容
  • 将未初始化部分的全局变量赋初值:数值型short,int,long等为0,bool为FALSE,指针为NULL等等,即.bss段的内容
  • 全局对象初始化,在main之前调用构造函数,这是可能会执行前的一些代码
  • 将main函数的参数argc,argv等传递给main函数,然后才真正运行main函数

main函数执行之后

  • 全局对象的析构函数会在main函数之后执行;
  • 可以用 atexit 注册一个函数,它会在main 之后执行;
  • __attribute__((destructor))

程序在执行int main(int argc, char *argv[])时的内存结构,你了解吗?

参数的含义是程序在命令行下运行的时候,需要输入argc 个参数,每个参数是以char 类型输入的,依次存在数组里面,数组是 argv[],所有的参数在指针char * 指向的内存中,数组的中元素的个数为 argc 个,第一个参数为程序的名称。

main函数的返回值有什么值得考究之处吗?

函数返回值类型必须是int,这样返回值才能传递给程序激活者(如操作系统)表示程序正常退出。

ifdef endif代表着什么?

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

(1)重载(overload)

重载是指在同一范围定义中的同名成员函数才存在重载关系。主要特点是函数名相同,参数类型和数目有所不同,不能出现参数个数和类型均相同,仅仅依靠返回值不同来区分的函数。重载和函数成员是否是虚函数无关。

(2)重写(覆盖)(override)

重写指的是在派生类中覆盖基类中的同名函数,重写就是重写函数体要求基类函数必须是虚函数且:与基类的虚函数有相同的参数个数与基类的虚函数有相同的参数类型与基类的虚函数有相同的返回值类型。

重载与重写的区别:

  • 重写是父类和子类之间的垂直关系,重载是不同函数之间的水平关系
  • 重写要求参数列表相同,重载则要求参数列表不同,返回值不要求
  • 重写关系中,调用方法根据对象类型决定,重载根据调用时实参表与形参表的对应关系来选择函数体

(3)隐藏(hide)

隐藏指的是某些情况下,派生类中的函数屏蔽了基类中的同名函数,包括以下情况:

  • 两个函数参数相同,但是基类函数不是虚函数。和重写的区别在于基类函数是否是虚函数。调用子类方法。(下一个问题)
  • 两个函数参数不同,无论基类函数是不是虚函数,都会被隐藏。和重载的区别在于两个函数不在同一个类中。

子类对父类的非虚函数重写,通过基类指针调用的函数版本是哪版?为什么?

父类方法

这是因为,非虚函数的调用是在编译时确定的,而不是在运行时确定的。因此,当通过基类指针调用非虚函数时,编译器只会考虑基类的版本,而不会考虑子类的版本。如果需要调用子类的版本,可以将函数声明为虚函数。

C++中struct和class的区别

两者界限很模糊,class能做的struct也能,在一些细节上有所不同

相同点

  • 两者都拥有成员函数、公有和私有部分
  • 任何可以使用class完成的工作,同样可以使用struct完成

不同点

  • 两者中如果不对成员指定公私有,struct默认是公有的,class则默认是私有的
  • struct模式是public继承,class默认是private继承

C++和C的struct区别

  • C语言中:struct是用户自定义数据类型(UDT);C++中struct是抽象数据类型(ADT),支持成员函数的定义,(C++中的struct能继承,能实现多态)
  • C中struct是没有权限的设置的,且struct中只能是一些变量的集合体,可以封装数据却不可以隐藏数据,而且成员不可以是函数
  • C++中,struct增加了访问权限,且可以和类一样有成员函数,成员默认访问说明符为public(为了与C兼容)
  • (可不讲)struct作为类的一种特例是用来自定义数据结构的。一个结构标记声明后,在C中必须在结构标记前加上struct,才能做结构类型名(除:typedef struct class{};);C++中结构体标记(结构体名)可以直接作为结构体类型名使用,此外结构体struct在C++中被当作类的一种特例

C和C++的类型安全

什么是类型安全?

类型安全很大程度上可以等价于内存安全,类型安全的代码不会试图访问自己没被授权的内存区域。“类型安全”常被用来形容编程语言,其根据在于该门编程语言是否提供保障类型安全的机制;有的时候也用“类型安全”形容某个程序,判别的标准在于该程序是否隐含类型错误。

类型安全的编程语言与类型安全的程序之间,没有必然联系。好的程序员可以使用类型不那么安全的语言写出类型相当安全的程序,相反的,差一点儿的程序员可能使用类型相当安全的语言写出类型不太安全的程序。绝对类型安全的编程语言暂时还没有。

转到malloc和new 作为举例

C++的类型安全

  • 操作符new返回的指针类型严格与对象匹配,而不是void*
  • C中很多以void*为参数的函数可以改写为C++模板函数,而模板是支持类型检查的;
  • 引入const关键字代替#define constants,它是有类型、有作用域的,而#define constants只是简单的文本替换
  • 一些#define宏可被改写为inline函数,结合函数的重载,可在类型安全的前提下支持多种类型,当然改写为模板也能保证类型安全
  • C++提供了dynamic_cast关键字,使得转换过程更加安全,因为dynamic_cast比static_cast涉及更多具体的类型检查。

C++和C语言的区别

  • C++中new和delete是对内存分配的运算符,取代了C中的malloc和free。
  • 标准C++中的字符串类取代了标准C函数库头文件中的字符数组处理函数(C中没有字符串类型)。
  • C++中用来做控制态输入输出的iostream类库替代了标准C中的stdio函数库。
  • C++中的try/catch/throw异常处理机制取代了标准C中的setjmp()和longjmp()函数。
  • 在C++中,允许有相同的函数名,不过它们的参数类型不能完全相同,这样这些函数就可以相互区别开来。而这在C语言中是不允许的。也就是C++可以重载,C语言不允许。
  • C++语言中,允许变量定义语句在程序中的任何地方,只要在是使用它之前就可以;而C语言中,必须要在函数开头部分。而且C++允许重复定义变量,C语言也是做不到这一点的
  • 在C++中,除了值和指针之外,新增了引用。引用型变量是其他变量的一个别名,我们可以认为他们只是名字不相同,其他都是相同的。
  • C++相对与C增加了一些关键字,如:bool、using、dynamic_cast、namespace等等

C++与Java的区别

语言特性

  • Java语言给开发人员提供了更为简洁的语法;完全面向对象,由于JVM可以安装到任何的操作系统上,所以说它的可移植性强 。C++也可以在其他系统运行,但是需要不同的编码(这一点不如Java,只编写一次代码,到处运行),例如对一个数字,在windows下是大端存储,在unix中则为小端存储。Java程序一般都是生成字节码,在JVM里面运行得到结果 。
  • Java语言中没有指针的概念,引入了真正的数组。不同于C++中利用指针实现的“伪数组”,Java引入了真正的数组,同时将容易造成麻烦的指针从语言中去掉,这将有利于防止在C++程序中常见的因为数组操作越界等指针操作而对系统数据进行非法读写带来的不安全问题
  • Java用接口(Interface)技术取代C++程序中的抽象类。接口与抽象类有同样的功能,但是省却了在实现和维护上的复杂性

垃圾回收

  • C++用析构函数回收垃圾,写C和C++程序时一定要注意内存的申请和释放
  • Java语言不使用指针,内存的分配和回收都是自动进行的,程序员无须考虑内存碎片的问题

应用场景

  • Java在桌面程序上不如C++实用,C++可以直接编译成exe文件,指针是c++的优势,可以直接对内存的操作,但同时具有危险性 。(操作内存的确是一项非常危险的事情,一旦指针指向的位置发生错误,或者误删除了内存中某个地址单元存放的重要数据,后果是可想而知的)
  • Java在Web 应用上具有C++ 无可比拟的优势,具有丰富多样的框架
  • 对于底层程序的编程以及控制方面的编程,C++很灵活,因为有句柄的存在

为什么C++没有垃圾回收机制?这点跟Java不太一样。

首先,实现一个垃圾回收器会带来额外的空间和时间开销。你需要开辟一定的空间保存指针的引用计数和对他们进行标记mark。然后需要单独开辟一个线程在空闲的时候进行free操作。

垃圾回收会使得C++不适合进行很多底层的操作。

C++和Python的区别

  • Python是一种脚本语言,是解释执行的,而C++是编译语言,是需要编译后在特定

剩余60%内容,订阅专栏后可继续查看/也可单篇购买

计算机实习秋招全阶段指南 文章被收录于专栏

作者简介:2个月时间逆袭嵌入式开发,拿下理想汽车-ssp、小米汽车-sp、oppo-sp、迈瑞医疗、三星电子等八家制造业大厂offer~ 专栏内容:涵盖算法、八股、项目、简历等前期准备的详细笔记和模板、面试前中后的各种注意事项以及后期谈薪、选offer等技巧。保姆级全阶段教程帮你获得信息差,早日收到理想offer~

全部评论

相关推荐

7 51 评论
分享
牛客网
牛客企业服务