2024年最新C++基础闯关100题,你能闯多少?【2021超硬核大厂高频面试题(1),不愧是大佬

img
img

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

3、C++里面如何声明const void f(void)函数为C程序中的库函数?

在该函数前添加extern “C”声明。由于编译后的名字不同,C++程序不能直接调用C 函数。

4、指针和引用的区别是什么?
  • 指针是一个变量,存储的是一个地址,引用跟原来的变量实质上是同一个东西,是原变量的别名
  • 指针可以有多级,引用只有一级
  • 指针可以为空,引用不能为NULL且在定义时必须初始化
  • 指针在初始化后可以改变指向,而引用在初始化之后不可再改变
  • sizeof指针得到的是本指针的大小,sizeof引用得到的是引用所指向变量的大小
  • 当把指针作为参数进行传递时,也是将实参的一个拷贝传递给形参,两者指向的地址相同,但不是同一个变量,在函数中改变这个变量的指向不影响实参,而引用却可以。
  • 引用只是别名,不占用具体存储空间,只有声明没有定义;指针是具体变量,需要占用存储空间。
  • 引用在声明时必须初始化为另一变量,一旦出现必须为typename refname &varname形式;指针声明和定义可以分开,可以先只声明指针变量而不初始化,等用到时再指向具体变量。
  • 引用一旦初始化之后就不可以再改变(变量可以被引用为多次,但引用只能作为一个变量引用);指针变量可以重新指向别的变量。
  • 不存在指向空值的引用,必须有具体实体;但是存在指向空值的指针。
void test(int \*p)
{
  int a=1;
  p=&a;
  cout<<p<<" "<<\*p<<endl;
}

int main(void)
{
    int \*p=NULL;
    test(p);
    if(p==NULL)
    cout<<"指针p为NULL"<<endl;
    return 0;
}
//运行结果为:
//0x22ff44 1
//指针p为NULL
void testPTR(int\* p) {
    int a = 12;
    p = &a;

}

void testREFF(int& p) {
    int a = 12;
    p = a;

}
void main()
{
    int a = 10;
    int\* b = &a;
    testPTR(b);//改变指针指向,但是没改变指针的所指的内容
    cout << a << endl;// 10
    cout << \*b << endl;// 10

    a = 10;
    testREFF(a);
    cout << a << endl;//12
}

5、一个C++源文件从文本到可执行文件经历的过程?
  • 预处理阶段:对源代码文件中文件包含关系(头文件)、预编译语句(宏定义)进行分析和替换,生成预编译文件
  • 编译阶段:将经过预处理后的预编译文件转换成特定汇编代码(编译原理相关,词法分析、语法分析、语义分析等),生成汇编文件
  • 汇编阶段:将编译阶段生成的汇编文件转化成机器码,生成可重定位目标文件
  • 链接阶段:将多个目标文件及所需要的库打包连接成最终的可执行目标文件(或库文件以供其他程序使用)
6、堆和栈的区别是什么?
  • 申请方式不同:栈由系统自动分配;堆是自己申请和释放的。
  • 申请大小限制不同:栈顶和栈底是之前预设好的,栈是向栈底扩展,大小固定,可以通过ulimit -a查看,由ulimit -s修改;堆向高地址扩展,是不连续的内存区域,大小可以灵活调整。
  • 申请效率不同:栈由系统分配,速度快,不会有碎片;堆由程序员分配,速度慢,且会有碎片。
7、说一说C与C++的内存分配方式?
  • 从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在,如全局变量,static变量。
  • 在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
  • 从堆上分配(动态内存分配)程序在运行的时候用malloc或new申请任意多少的内存,程序员负责在何时用free或delete释放内存。动态内存的生存期自己决定,使用非常灵活。
8、区别以下指针类型?
int \*p[10]
int (\*p)[10]
int \*p(int)
int (\*p)(int)

  • int *p[10]表示指针数组,强调数组概念,是一个数组变量,数组大小为10,数组内每个元素都是指向int类型的指针变量。
  • int (*p)[10]表示数组指针,强调是指针,只有一个变量,是指针类型,不过指向的是一个int类型的数组,这个数组大小是10。
  • int *p(int)是函数声明,函数名是p,参数是int类型的,返回值是int *类型的。
  • int (*p)(int)是函数指针,强调是指针,该指针指向的函数具有int类型参数,并且返回值是int类型的。
9、new和malloc的区别?
  • new是C++运算符,malloc()是C/C++语言标准库函数
  • new会自动计算需分配的空间,malloc不行
  • new会调用构造函数,malloc不会
  • new是类型安全的,而malloc不是
int \*p = new float[2]; //编译错误
int \*p = (int\*)malloc(2 \* sizeof(double));//编译无错误

  • malloc/free要库文件支持,new/delete则不要
  • new返回指定类型指针,malloc返回void*指针,需要强制类型转换
  • new可以被重载,malloc不能
10、new和delete是如何实现的?
  • new的实现过程是:首先调用名为operator new的标准库函数,分配足够大的原始为类型化的内存,以保存指定类型的一个对象;接下来运行该类型的一个构造函数,用指定初始化构造对象;最后返回指向新分配并构造后的的对象的指针
  • delete的实现过程:对指针指向的对象运行适当的析构函数;然后通过调用名为operator delete的标准库函数释放该对象所用内存
11、虚函数是什么以及其作用?

虚函数是允许被其子类重新定义的成员函数。

可以实现用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。

有了虚函数,基类指针指向基类对象时就使用基类的成员(包括成员函数和成员变量),指向派生类对象时就使用派生类的成员,从而实现多态。

注意:构造函数不能为虚函数,但是析构函数可以为虚函数,并且虚析构函数可以防止父类指针销毁子类对象时不正常导致的内存泄漏。

12、C++中struct和class的区别?

相同点

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

不同点

  • 两者中如果不对成员不指定公私有,struct默认是公有的,class则默认是私有的
  • class默认是private继承,而struct模式是public继承
  • class可以作为模板类型,struct不行
13、C++中const和static的作用?

const

  • 不考虑类的情况

    • const常量在定义时必须初始化,之后无法更改
    • const形参可以接收const和非const类型的实参
// i 可以是 int 型或者 const int 型
void fun(const int& i){
    //...
}

  • 考虑类的情况

    • const成员变量:不能在类定义外部初始化,只能通过构造函数初始化列表进行初始化,并且必须有构造函数;不同类对其const数据成员的值可以不同,所以不能在类中声明时初始化。
    • const成员函数:const对象不可以调用非const成员函数;非const对象都可以调用;不可以改变非mutable(用该关键字声明的变量可以在const成员函数中被修改)数据的值。

static

  • 不考虑类的情况

    • 隐藏。所有不加static的全局变量和函数具有全局可见性,可以在其他文件中使用,加了之后只能在该文件所在的编译模块中使用。
    • 默认初始化为0,包括未初始化的全局静态变量与局部静态变量,都存在全局未初始化区。
    • 静态变量在函数内定义,始终存在,且只进行一次初始化,具有记忆性,其作用范围与局部变量相同,函数退出后仍然存在,但不能使用。
  • 考虑类的情况

    • static成员变量:只与类关联,不与类的对象关联。定义时要分配空间,不能在类声明中初始化,必须在类定义体外部初始化,初始化时不需要标示为static;可以被非static成员函数任意访问。
    • static成员函数:不具有this指针,无法访问类对象的非static成员变量和非static成员函数;不能被声明为const、虚函数和volatile;可以被非static成员函数任意访问。
14、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。

const int a;
int const a;
const int \*a;
int \*const a;

  • int const a和const int a均表示定义常量类型a。
  • const int *a,其中a为指向int型变量的指针,const在 * 左侧,表示a指向不可变常量。(看成const (*a),对引用加const)
  • int *const a,依旧是指针类型,表示a为指向整型数据的常指针。(看成const(a),对指针const)
15、简单介绍内存池?

内存池是一种内存分配方式。通常我们习惯直接使用new、malloc申请内存。

这样做的缺点在于所申请内存块的大小不定,当频繁使用时会造成大量的内存碎片并进而降低性能。

内存池则是在真正使用内存之前,预先申请分配一定数量、大小相等(一般情况下)的内存块留作备用。

当有新的内存需求时,就从内存池中分出一部分内存块,若内存块不够再继续申请新的内存。这样做的一个显著优点是,使得内存分配效率得到提升。

16、宏定义和函数有何区别?
  • 宏在编译时完成替换,之后被替换的文本参与编译,相当于直接插入了代码,运行时不存在函数调用,执行起来更快;函数调用在运行时需要跳转到具体调用函数。
  • 宏定义属于在结构中插入代码,没有返回值;函数调用具有返回值。
  • 宏定义参数没有类型,不进行类型检查;函数参数具有类型,需要检查类型。
  • 宏定义不要在最后加分号。
17、变量声明和定义区别?
  • 声明仅仅是把变量的声明的位置及类型提供给编译器,并不分配内存空间;定义要在定义的地方为其分配存储空间。
  • 相同变量可以在多处声明(外部变量extern),但只能在一处定义。
18、strlen和sizeof区别?
  • sizeof是运算符,并不是函数,结果在编译时得到而非运行中获得;strlen是字符处理的库函数。
  • sizeof参数可以是任何数据的类型或者数据(sizeof参数不退化);strlen的参数只能是字符指针且结尾是’\0’的字符串。
  • 因为sizeof值在编译时确定,所以不能用来得到动态分配(运行时分配)存储空间的大小。
 int main(int argc, char const \*argv[]){   

      const char\* str = "name";
      sizeof(str); // 取的是指针str的长度,是8
      strlen(str); // 取的是这个字符串的长度,不包含结尾的 \0。大小是4
      return 0;
  }

19、简单描述内存泄漏?

内存泄漏一般是指堆内存的泄漏,也就是程序在运行过程中动态申请的内存空间不再使用后没有及时释放,导致那块内存不能被再次使用。

20、什么函数不能声明为虚函数?构造函数为什么不能为虚函数?
  • 普通函数(非成员函数)、构造函数、友元函数、静态成员函数、内联成员函数。
  • 当派生类在创建对象的时候会调用基类的构造函数,但是如果基类的构造函数是虚函数的话,派生类的构造函数又会把基类的构造函数覆盖,所以无法进一步执行而出错。
  • 虚函数通过虚函数表来实现,而指向虚函数表的指针也需要在对象实例化后创建,那么就违背了先实例化后调用的准则。
21、常量指针和指针常量区别?
  • 常量指针是一个指针,读成常量的指针,指向一个只读变量。如int const *p或const int *p。
  • 指针常量是一个不能给改变指向的指针。指针是个常亮,不能中途改变指向,如int *const p。
22、a和&a有什么区别?
假设数组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类型大小来读取。
23、虚函数与纯虚函数的区别?
  • 定义一个函数为虚函数,不代表函数为不被实现的函数。
  • 定义他为虚函数是为了允许用基类的指针来调用子类的这个函数。
  • 定义一个函数为纯虚函数,才代表函数没有被实现。
  • 定义纯虚函数是为了实现一个接口,起到一个规范的作用,规范继承这个类的程序员必须实现这个函数。
24、数组名和指针(这里为指向数组首元素的指针)区别?
  • 二者均可通过增减偏移量来访问数组中的元素。

数组名不是真正意义上的指针,可以理解为常指针,所以数组名没有自增、自减等操作。

当数组名当做形参传递给调用函数后,就失去了原有特性,退化成一般指针,多了自增、自减操作,但sizeof运算符不能再得到原数组的大小了。

25、内联函数和宏定义的区别?

宏定义在预处理的时候进行简单的字符串替换,而内联函数在编译时在每个调用内联函数的地方将函数展开,这样不用使内联函数占用栈空间,提高效率。

宏定义没有类型检查,但是内联函数还是具有函数的性质,有参数以及返回值。

26、struct与union的区别?
  • struct可以存储多个成员变量信息;而union每个成员会共用同一个存储空间。
  • 系统分配给union的内存size就是size最大的成员所需空间大小,struct在分配空间时,根据成员类型不同,会存在字节对齐情况,具体对齐标准和机器有关,可能是4字节或8字节等对齐方式。
  • 在任何同一时刻,union值存放了一个被先选中的成员,而结构体struct的所有成员都存在。
27、什么是野指针和悬空指针?

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

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

int main(void) { 
  int \* p;
  std::cout<<\*p<<std::endl;
  return 0;
}

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

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

int main(void) { 
  int \* p = nullptr;
  int\* p2 = new int; 
  p = p2;
  delete p2;
}

此时 p和p2就是悬空指针,指向的内存已经被释放。继续使用这两个指针,行为不可预料。需要设置为p=p2=nullptr。此时再使用,编译器会直接保错。

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

产生原因及解决办法

野指针:指针变量未及时初始化 => 定义指针变量及时初始化,要么置空。

悬空指针:指针free或delete之后没有及时置空 => 释放操作后立即置空。

28、final和override关键字

final

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

class Base
{
    virtual void foo();
};

class A : public Base
{
    void foo() final; // foo 被override并且是最后一个override,在其子类中不可以重写
};

class B final : A // 指明B是不可以被继承的
{
    void foo() override; // Error: 在A中已经被final了
};

class C : B // Error: B is final
{
};

override

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

class A
{
    virtual void foo();
}
class B : public A
{
    void foo(); //OK
    virtual void foo(); // OK
    void foo() override; //OK
}

如果不使用override,当你手一抖,将foo()写成了foo()会怎么样呢?

结果是编译器并不会报错,因为它并不知道你的目的是重写虚函数,而是把它当成了新的函数。

如果这个虚函数很重要的话,那就会对整个程序不利。

所以,override的作用就出来了,它指定了子类的这个虚函数是重写的父类的,如果你名字不小心打错了的话,编译器是不会编译通过的:

class A
{
    virtual void foo();
};
class B : public A
{
    virtual void f00(); //OK,这个函数是B新增的,不是继承的
    virtual void f0o() override; //Error, 加了override之后,这个函数一定是继承自A的,A找不到就报错
};

29、static的用法和作用?
  • 隐藏,static函数,static变量均可。当同时编译多个文件时,所有未加static前缀的全局变量和函数都具有全局可见性。

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

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

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

    • 函数体内static变量的作用范围为该函数体,不同于auto变量,该变量的内存只被分配一次,因此其值在下次调用时仍维持上次的值;
    • 在模块内的static全局变量可以被模块内所用函数访问,但不能被模块外其它函数访问;
    • 在模块内的static函数只可被这一模块内的其它函数调用,这个函数的使用范围被限制在声明它的模块内;
    • 在类中的static成员变量属于整个类所拥有,对类的所有对象只有一份拷贝;
    • 在类中的static成员函数属于整个类所拥有,这个函数不接收this指针,因而只能访问类的static成员变量。
    • static类对象必须要在类外进行初始化,static修饰的变量先于对象存在,所以static修饰的变量要在类外初始化;
    • 由于static修饰的类成员属于类,不属于对象,因此static类成员函数是没有this指针的,this指针是指向本对象的指针。正因为没有this指针,所以static类成员函数不能访问非static的类成员,只能访问 static修饰的类成员;
    • static成员函数不能被virtual修饰,static成员不属于任何对象或实例,所以加上virtual没有任何实际意义;静态成员函数没有this指针,虚函数的实现是为每一个对象分配一个vptr指针,而vptr是通过this指针调用的,所以不能为virtual;虚函数的调用关系,this->vptr->ctable->virtual function。
30、拷贝初始化和直接初始化?
  • 当用于类类型对象时,初始化的拷贝形式和直接形式有所不同:直接初始化直接调用与实参匹配的构造函数,拷贝初始化总是调用拷贝构造函数。拷贝初始化首先使用指定构造函数创建一个临时对象,然后用拷贝构造函数将那个临时对象拷贝到正在创建的对象。
string str1("I am a string");//语句1 直接初始化
string str2(str1);//语句2 直接初始化,str1是已经存在的对象,直接调用构造函数对str2进行初始化
string str3 = "I am a string";//语句3 拷贝初始化,先为字符串”I am a string“创建临时对象,再把临时对象作为参数,使用拷贝构造函数构造str3
string str4 = str1;//语句4 拷贝初始化,这里相当于隐式调用拷贝构造函数,而不是调用赋值运算符函数

  • 为了提高效率,允许编译器跳过创建临时对象这一步,直接调用构造函数构造要创建的对象,这样就完全等价于直接初始化了(语句1和语句3等价)。但是需要辨别两种情况。
  • 当拷贝构造函数为private时:语句3和语句4在编译时会报错。
  • 使用explicit修饰构造函数时:如果构造函数存在隐式转换,编译时会报错。
31、初始化和赋值的区别?
  • 对于简单类型来说,初始化和赋值没什么区别
  • 对于类和复杂数据类型来说,这两者的区别就大了。
class A{
public:
    int num1;
    int num2;
public:
    A(int a=0, int b=0):num1(a),num2(b){};
    A(const A& a){};
    //重载 = 号操作符函数
    A& operator=(const A& a){
        num1 = a.num1 + 1;
        num2 = a.num2 + 1;
        return \*this;
    };
};
int main(){

    A a(1,1);
    A a1 = a; //拷贝初始化操作,调用拷贝构造函数
    A b;
    b = a;//赋值操作,对象a中,num1 = 1,num2 = 1;对象b中,num1 = 2,num2 = 2
    return 0;
}

32、free()一个指针两次,会出现什么问题 ?

free一次后,原来指针所指向的堆中的内容已经被清空了,但指针本身的值并没有被置为null,还是指向原来它所指向的内存空间。

再free一次时,由于堆中的内容已经是无效的东西,所以就会出错。

不过,有的编译器在free时并没有清理堆中的内存,有时你对它free两次也不一定出错。不过这是一个很大的隐患,在实际写代码中千万要注意避开这点。

img
img

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

,num1 = 1,num2 = 1;对象b中,num1 = 2,num2 = 2
return 0;
}


##### 32、free()一个指针两次,会出现什么问题 ?


free一次后,原来指针所指向的堆中的内容已经被清空了,但指针本身的值并没有被置为null,还是指向原来它所指向的内存空间。


再free一次时,由于堆中的内容已经是无效的东西,所以就会出错。


不过,有的编译器在free时并没有清理堆中的内存,有时你对它free两次也不一定出错。不过这是一个很大的隐患,在实际写代码中千万要注意避开这点。



[外链图片转存中...(img-7KeLDOpT-1715622449741)]
[外链图片转存中...(img-GyLpZBRt-1715622449742)]

**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**

**[需要这份系统化的资料的朋友,可以添加戳这里获取](https://bbs.csdn.net/topics/618668825)**


**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值