C/C++ 高频八股文面试题1000题(一)

原作者:Linux教程,原文地址:C/C++ 高频八股文面试题1000题(一)

在准备技术岗位的求职过程中,**C/C++**始终是绕不开的核心考察点。无论是互联网大厂的笔试面试,还是嵌入式、后台开发、系统编程等方向的岗位,C/C++ 都扮演着举足轻重的角色。

本系列文章将围绕 "C/C++ 笔试面试中出现频率最高的 1000 道题目" 进行深入剖析与讲解。由于篇幅限制,整个系列将分为 多 篇 陆续发布,每篇50道题,本文为 第一篇

通过系统梳理这些高频考点,帮助你在面对各大厂笔试、技术面试时真正做到 心中有数、下笔有神、对答如流

建议学习方式:

  • 结合实际项目或刷题平台反复巩固
  • 对照每道题目的解析查漏补缺
  • 动手编写示例代码加深理解

本系列将持续更新,欢迎关注+收藏,一起攻克这 1000 道 C/C++ 高频题,拿下心仪 Offer!

面试题1:变量的声明和定义有什么区别?

✅ 简要回答:

  • 定义(Definition):为变量分配存储空间和地址,是创建变量的实际过程。一个变量只能被定义一次。
  • 声明(Declaration):用于告诉编译器该变量已经存在,不会分配存储空间。变量可以多次声明。

关键区别:

|----|--------|-------|
| 类型 | 是否分配内存 | 是否可重复 |
| 定义 | 是 | 否 |
| 声明 | 否 | 是 |

复制代码
extern int a;    // 声明,不分配内存空间
int a;           // 定义,分配内存空间
int a = 10;      // 定义并初始化

面试题2:如何用if语句正确判断bool、int、float和指针变量是否为零值?

复制代码
bool型数据:  if(flag) { A; } else { B;}
int型数据:   if(0==flag) { A; } else { B; }
指针变量:     if(NULL==flag) { A; } else { B; }
float型数据: #define NORM (0.000001) 
             if((flag>=-NORM) && (flag<=NORM)) { A; } else { B; }

面试题3:sizeof 和 strlen 的主要区别是什么?

  1. sizeof 是一个操作符,而 strlen 是 C 标准库中定义的一个函数。
  2. sizeof 的操作对象既可以是数据类型,也可以是变量;而 strlen 只能接受以 \0 结尾的字符串作为输入参数。
  3. sizeof 在编译阶段就会被计算出结果,而 strlen 必须等到程序运行时才能得出结果。此外,sizeof 计算的是数据类型或变量所占内存的大小,而 strlen 测量的是字符串实际字符的数量(不包括结尾的 \0)。
  4. 当数组作为 sizeof 的参数时,它表示整个数组的大小,不会退化为指针;而当数组传递给 strlen 时,则会退化为指向数组首地址的指针。

注意: 有些操作符(如 sizeof)看起来像函数,而有些函数名又类似操作符,这类名称容易引起混淆,使用时需要特别注意区分。尤其是在处理数组名等特殊类型时,这种差异可能导致意料之外的错误。

面试题4:C 语言中的 static 和 C++ 中的 static 有什么区别?

C 语言中,static 主要用于修饰:

  • 局部静态变量:延长局部变量的生命周期,使其在程序运行期间一直存在。
  • 外部静态变量和函数:限制变量或函数的作用域为当前文件,实现封装和信息隐藏。

而在 C++ 中,除了具备 C 语言中所有的功能之外,static 还被扩展用于:

  • 定义类中的静态成员变量和静态成员函数。这些静态成员属于整个类,而不是类的某个对象,可以在多个对象之间共享。

注意: 在编程中,static 所具有的"记忆性"和"全局性"特点,使得不同调用之间可以共享数据、传递信息。在 C++ 中,类的静态成员则能够在不同的对象实例之间进行通信和数据共享。

面试题5:C 中的 malloc 和 C++ 中的 new 有什么区别?

  1. new 和 delete 是 C++ 中的操作符,支持重载,只能在 C++ 程序中使用;而 malloc 和 free 是标准库函数,可以在 C 和 C++ 中通用,并且可以被覆盖(如自定义内存管理)。
  2. new 在分配内存的同时会自动调用对象的构造函数,完成初始化;相应的,delete 在释放内存时会调用对象的析构函数。而 malloc 只负责分配内存空间,free 仅用于释放内存,它们都不会触发构造函数或析构函数的执行。
  3. new 返回的是具体类型的指针,具有类型安全性;而 malloc 返回的是 void* 类型,需要显式地进行类型转换才能使用。

注意: 使用 malloc 分配的内存必须通过 free 来释放,而使用 new 分配的内存则必须使用 delete 来释放,两者不可混用。因为它们底层实现机制不同,混用可能导致未定义行为或内存泄漏。

面试题6:写一个"标准"宏 MIN

复制代码
#define MIN(a,b) ((a)<=(b)?(a):(b))

注意:在调用时一定要注意这个宏定义的副作用。

面试题7:指针可以被声明为 volatile 吗?

可以,指针可以被声明为 volatile 类型。因为从本质上讲,指针本质上也是一种变量,它保存的是一个内存地址(即整型数值),这一点与普通变量并没有本质区别。

在某些特殊场景下,比如硬件寄存器访问、中断服务程序中共享的数据等,指针的值可能会被程序之外的因素修改,此时就需要使用 volatile 来告诉编译器不要对该指针进行优化,确保每次访问都直接从内存中读取最新值。

例如,在中断服务程序中修改一个指向缓冲区(buffer)的指针时,就必须将该指针声明为 volatile,以防止编译器因优化而忽略其变化。

说明: 虽然指针具有特殊的用途------用于访问内存地址,但从变量访问的角度来看,它和其他变量一样具备可变性,因此也可以像普通变量一样使用 volatile 进行修饰。

面试题8:a 和 &a 有什么区别?请写出以下 C 程序的运行结果

复制代码
#include <stdio.h>

int main(void)
{
    int a[5] = {1, 2, 3, 4, 5};
    int *ptr = (int *)(&a + 1);
    printf("%d, %d", *(a + 1), *(ptr - 1));
    
    return 0;
}

数组名a和&a的区别

|-----|---------------|--------------------|
| 表达式 | 类型 | 含义 |
| a | int* | 指向数组首元素 a[0] 的指针 |
| &a | int (*)[5] | 指向整个数组 a 的指针 |

虽然它们的地址值相同,但类型不同,因此在进行指针运算时步长也不同:

  • a + 1 是以 sizeof(int) 为单位移动(即跳过一个 int)。
  • &a + 1 是以 sizeof(int[5]) 为单位移动(即跳过整个数组)。

输出结果:2, 5

面试题9:简述 C/C++ 程序编译时的内存分配情况

从内存分配机制角度,C/C++ 的内存管理还可以分为以下三种方式:

|------|---------------------------------------------------------|
| 分配方式 | 特点描述 |
| 静态分配 | 在程序编译阶段完成,如全局变量、静态变量。在整个程序运行过程中有效,速度最快,不易出错。 |
| 栈上分配 | 函数调用时在栈中为局部变量分配空间,函数返回后自动释放。速度快但容量有限。 |
| 堆上分配 | 使用 malloc / new 动态申请内存,程序员负责手动释放。灵活性强但容易出错,如内存泄漏、碎片等问题。 |

面试题10:简述strcpy、sprintf与memcpy的区别。

1、操作对象不同

|---------|---------------|------------|--------------|
| 函数 | 源对象类型 | 目标对象类型 | 说明 |
| strcpy | 字符串(以 \0 结尾) | 字符串 | 用于字符串之间的拷贝 |
| sprintf | 可为任意基本数据类型 | 字符串 | 格式化输出到字符串 |
| memcpy | 任意可操作的内存地址 | 任意可操作的内存地址 | 用于任意内存块之间的拷贝 |

2、功能用途不同

  • strcpy 专门用于字符串之间的拷贝,遇到 \0 停止拷贝,因此只适用于以 \0 结尾的字符串。
  • sprintf 将各种类型的数据格式化后写入字符串中,功能强大但主要用于字符串格式化拼接,不是纯粹的拷贝函数。
  • memcpy 用于两个内存块之间的直接拷贝,不依赖 \0,也不关心数据类型,适用于任意类型的数据拷贝。

3、执行效率不同

|---------|-------------------------|
| 函数 | 效率评价 |
| memcpy | 最高。直接进行内存拷贝,没有格式转换或终止判断 |
| strcpy | 中等。需要逐字节判断是否到达 \0 |
| sprintf | 最低。涉及格式解析和字符转换,开销较大 |

4、使用注意事项

  • strcpy 不检查目标缓冲区大小,容易造成缓冲区溢出,建议使用 strncpy。
  • sprintf 同样不检查缓冲区边界,推荐使用更安全的 snprintf。
  • memcpy 虽然高效,但如果源和目标内存区域有重叠,应使用 memmove 替代。

面试题11:如何将地址为0x67a9的整型变量赋值为0xaa66?

复制代码
volatile int *ptr = (volatile int *)0x67a9;
*ptr = 0xaa66;

面试题12:面向对象编程的三大基本特征是什么?

面向对象编程(OOP)的三大核心特征是:

  • 封装性(Encapsulation)
  • 继承性(Inheritance)
  • 多态性(Polymorphism)

这三大特性是构建面向对象系统的基础,它们共同支持代码的模块化、可重用性和灵活性。

|----|-----------------|----------------|
| 特征 | 含义 | 核心作用 |
| 封装 | 隐藏实现细节,提供统一接口 | 提高安全性、简化使用 |
| 继承 | 类之间共享属性和方法 | 代码复用、建立类的层级关系 |
| 多态 | 父类引用指向子类对象,动态绑定 | 实现灵活调用、提升程序扩展性 |

面试题13:C++ 的空类默认包含哪些成员函数?

缺省构造函数:用于创建对象实例时初始化对象。

缺省拷贝构造函数:当对象通过另一个同类对象进行初始化时调用。

缺省析构函数:在对象生命周期结束时自动调用,用于清理资源。

缺省赋值运算符:将一个对象的内容赋值给另一个同类的对象。

缺省取址运算符:返回对象的内存地址。

缺省取址运算符 const:对于常量对象,返回其内存地址。

注意要点:

  • 仅在需要时生成:只有当程序中实际使用了这些函数时,编译器才会为其生成相应的实现。
  • 覆盖默认行为:如果需要自定义上述任何一种行为,可以通过显式定义相应的成员函数来覆盖默认实现。
  • 全面了解默认函数:尽管有些资料可能只提及前四个默认函数,但后两个(取址运算符及其 const 版本)同样是重要的,默认情况下它们也是存在的。

面试题14:请谈谈你对拷贝构造函数和赋值运算符的理解

拷贝构造函数与赋值运算符重载在功能上都用于对象之间的复制操作,但它们在使用场景和实现逻辑上有以下两个主要区别:

(1)拷贝构造函数用于生成一个新的类对象,而赋值运算符则用于已有对象的重新赋值。

(2)由于拷贝构造函数是用于构造新对象的过程,因此在初始化之前无需判断源对象是否与新建对象相同;而赋值运算符必须进行这种判断(即自赋值检查),以避免不必要的错误。此外,在赋值操作中,如果目标对象已经分配了内存资源,则需要先释放原有资源,再进行新的内存分配和数据复制。

注意:当类中包含指针类型的成员变量时,必须显式重写拷贝构造函数和赋值运算符,避免使用编译器默认生成的版本。否则可能会导致浅拷贝问题,引发内存泄漏或重复释放等问题。

面试题15:如何用 C++ 设计一个不能被继承的类?

复制代码
template <typename T>
class A 
{
    friend T;  // 只有 T 类可以访问 A 的私有成员
private:
    A() {}     // 私有构造函数
    ~A() {}    // 私有析构函数
};

class B : virtual public A<B>
{
public:
    B() {}     // 构造函数
    ~B() {}    // 析构函数
};

class C : virtual public B
{
public:
    C() {}
    ~C() {}
};

int main(void)
{
    B b;   // 合法:B 可以实例化
    // C c; // 编译失败:因为 C 继承自 B,而 B 是 A<B> 的子类,构造受限
    return 0;
}

面试题16:访问基类的私有虚函数。请写出以下 C++ 程序的输出结果

复制代码
#include <iostream.h>

class A 
{
public:
    virtual void g() 
    {
        cout << "A::g" << endl;
    }

private:
    virtual void f()
    {
        cout << "A::f" << endl;
    }
};

class B : public A 
{
public:
    void g()
    {
        cout << "B::g" << endl;
    }

    virtual void h()
    {
        cout << "B::h" << endl;
    }
};

typedef void (*Fun)(void);

int main(void)
{
    B b;

    Fun pFun;

    for (int i = 0; i < 3; i++)
    {
        pFun = (Fun)*((int*)*(int*)(&b) + i);
        pFun();
    }

    return 0;
}

程序输出结果:

复制代码
B::g
A::f
B::h

注意:本题主要考察了面试者对虚函数的理解程度。一个对虚函数不了解的人很难正确的做出本题。 在学习面向对象的多态性时一定要深刻理解虚函数表的工作原理。

面试题17:简述类成员函数的重写(Override)、重载(Overload)和隐藏(Hide)的区别。

(1)重写 和 重载 的主要区别如下:

  • 作用范围不同: 重写的函数位于两个不同的类中(基类和派生类),而重载的函数始终在同一个类中。
  • 参数列表不同: 重写函数与被重写函数的参数列表必须完全相同;而重载函数与被重载函数的参数列表必须不同(包括参数个数、类型或顺序)。
  • virtual 关键字要求不同: 基类中的被重写函数必须使用 virtual 关键字进行修饰,才能实现多态;而重载函数无论是否使用 virtual 都不影响其重载特性。

(2)隐藏 与 重写、重载 的区别如下:

  • 作用范围不同: 与重写类似,隐藏也发生在基类与派生类之间,而不是像重载那样在同一类中。
  • 参数列表不同: 隐藏函数与被隐藏函数的参数列表可以相同,也可以不同,但它们的函数名必须相同。当参数列表不同时,不论基类中的函数是否为虚函数,都会导致基类函数被隐藏,而不是被重写。

说明: 虽然重载和重写都是实现多态性的基础,但它们的技术实现方式完全不同,所达成的目的也有本质区别。其中,重写支持的是运行时多态(动态绑定),而重载实现的是编译时多态(静态绑定)。

面试题18:说下多态实现的原理

当编译器检测到一个类中包含虚函数时,会自动为该类生成一个虚函数表(vtable)。虚函数表中的每一项都是一个指向相应虚函数的指针。

同时,编译器会在该类的实例中隐式插入一个虚函数表指针(vptr),用于指向该类对应的虚函数表。对于大多数编译器(如 VC++),这个 vptr 通常被放置在对象内存布局的最开始位置。

在调用构造函数创建对象时,编译器会在构造过程中自动执行一段隐藏代码,将 vptr 指向当前类的 vtable,从而建立起对象与虚函数表之间的关联。

此外,在构造函数内部,this 指针此时已经指向具体的派生类对象,因此可以通过 vptr 找到正确的 vtable,进而访问到实际应调用的虚函数体。这种机制使得程序可以在运行时根据对象的实际类型来动态绑定函数调用,这就是所谓的动态联编(Dynamic Binding),也是 C++ 实现多态的核心原理。

注意:

  • 要明确区分 虚函数、纯虚函数 和 虚拟继承 的概念及其区别;
  • 理解虚函数表的工作机制是掌握面向对象多态性的关键;
  • 在 C++ 面试中,虚函数与多态是高频考点,务必熟练掌握其实现原理和应用场景。

面试题19:链表和数组有什么区别?

(1)存储形式不同: 数组是一块连续的内存空间,声明时必须指定其固定长度;而链表由一系列不连续的节点组成,每个节点包含数据部分和指向下一个节点的指针,其长度可以根据需要动态增长或缩减。

(2)数据查找效率不同: 数组支持随机访问,可以通过索引直接定位元素,查找效率高;而链表只能从头节点开始逐个遍历,查找效率较低。

(3)插入与删除操作不同: 在链表中进行插入或删除操作只需修改相邻节点的指针,效率较高;而在数组中插入或删除元素通常需要移动大量数据以保持连续性,操作效率较低。

(4)越界问题: 数组存在越界风险,使用不当可能导致程序崩溃或未定义行为;链表不存在越界问题,但需注意空指针异常。

说明:

选择数组还是链表应根据具体应用场景来决定:

  • 若应用更注重快速查询,且数据量变化不大,优先考虑使用数组;
  • 若应用频繁进行插入和删除操作,数据量不确定,建议使用链表。

数组占用空间紧凑,但长度固定;链表灵活可变,但额外占用空间用于保存指针信息。合理选择数据结构有助于提升程序的效率与稳定性。

面试题20:MySQL索引哪些情况会失效?

  • 查询条件包含or,可能导致索引失效
  • 如何字段类型是字符串,where时一定用引号括起来,否则索引失效
  • like通配符可能导致索引失效。
  • 联合索引,查询时的条件列不是联合索引中的第一个列,索引失效。
  • 在索引列上使用mysql的内置函数,索引失效。
  • 对索引列运算(如,+、-、*、/),索引失效。
  • 索引字段上使用(!= 或者 < >,not in)时,可能会导致索引失效。
  • 索引字段上使用is null, is not null,可能导致索引失效。
  • 左连接查询或者右连接查询查询关联的字段编码格式不一样,可能导致索引失效。
  • mysql估计使用全表扫描要比使用索引快,则不使用索引。

面试题21:说说队列和栈的异同。

队列(Queue)和栈(Stack)都属于基础的线性数据结构,但它们在数据的插入与删除操作方式上存在明显差异。

  • 操作特性不同: 队列遵循"先进先出"(FIFO, First In First Out)原则,即最先插入的元素最先被移除; 栈则遵循"后进先出"(LIFO, Last In First Out)原则,即最后压入栈的元素最先弹出。
  • 应用场景不同: 队列适用于需要按顺序处理数据的场景,如任务调度、消息队列等; 栈常用于函数调用、表达式求值、括号匹配等需要回溯的场景。
  • 实现方式类似: 它们都可以通过数组或链表实现,但在插入和删除的位置上有明确限制。

注意:区分"数据结构中的栈"与"程序内存中的栈区"

虽然"栈"这个词在数据结构中表示一种存储模型,但它也常用于描述程序运行时的内存区域,容易引起混淆。

  • 栈区(Stack): 是程序运行时的一个内存区域,由编译器自动分配和释放,主要用于存放函数参数、局部变量等。其访问方式类似于数据结构中的栈,具有高效快速的特点。
  • 堆区(Heap): 是用于动态内存管理的一块内存区域,通常由程序员手动申请和释放(如使用 malloc / new 和 free / delete)。如果未手动释放,程序结束后可能由操作系统回收。其分配方式类似于链表,灵活但需谨慎管理。
  • 两者是完全不同的概念: 数据结构中的"堆"和"栈"描述的是数据组织的方式,而程序内存中的"堆区"和"栈区"指的是内存的分配区域,二者本质上并无直接关联。

面试题22:索引不适合哪些场景?

  • 数据量少的不适合加索引
  • 更新比较频繁的也不适合加索引
  • 区分度低的字段不适合加索引(如性别)

面试题23:编码实现直接插入排序

直接插入排序编程实现如下:

复制代码
#include<iostream.h> 
void main( void ) 
{ 
 int ARRAY[10] = { 0, 6, 3, 2, 7, 5, 4, 9, 1, 8 }; 
 int i,j; 
 for( i = 0; i < 10; i++) 
 { 
 cout<<ARRAY[i]<<" "; 
 } 
 cout<<endl; 
 for( i = 2; i <= 10; i++ ) //将 ARRAY[2],...,ARRAY[n]依次按序插入 
 { 
 if(ARRAY[i] < ARRAY[i-1]) //如果 ARRAY[i]大于一切有序的数值, 
 //ARRAY[i]将保持原位不动 
 { 
   ARRAY[0] = ARRAY[i]; //将 ARRAY[0]看做是哨兵,是 ARRAY[i]的副本   j = i - 1; 
 do{ //从右向左在有序区 ARRAY[1..i-1]中 
//查找 ARRAY[i]的插入位置 
   ARRAY[j+1] = ARRAY[j]; //将数值大于 ARRAY[i]记录后移   j-- ; 
 }while( ARRAY[0] < ARRAY[j] );  
 ARRAY[j+1]=ARRAY[0]; //ARRAY[i]插入到正确的位置上 
 } 
 } 
 for( i = 0; i < 10; i++) 
 { 
 cout<<ARRAY[i]<<" "; 
 } 
 cout<<endl; 
} 

注意:所有为简化边界条件而引入的附加结点(元素)均可称为哨兵。引入哨兵后使得查找循环条件的 时间大约减少了一半,对于记录数较大的文件节约的时间就相当可观。类似于排序这样使用频率非常高 的算法,要尽可能地减少其运行时间。所以不能把上述算法中的哨兵视为雕虫小技。

面试题24:日常工作中你是怎么优化SQL的?

可以从这几个维度回答这个问题:

  • 加索引
  • 避免返回不必要的数据
  • 适当分批量进行
  • 优化sql结构
  • 分库分表
  • 读写分离

面试题25:编码实现冒泡排序

冒泡排序编程实现如下:

复制代码
#include <stdio.h> 
#define LEN 10 //数组长度 
 void main( void ) 
 { 
 int ARRAY[10] = { 0, 6, 3, 2, 7, 5, 4, 9, 1, 8 }; //待排序数组 
 printf( "\n" ); 
 for( int a = 0; a < LEN; a++ ) //打印数组内容 
 { 
 printf( "%d ", ARRAY[a] ); 
 } 
 int i = 0; int j = 0; 
 bool isChange; //设定交换标志 
for( i = 1; i < LEN; i++ ) 
{ //最多做 LEN-1 趟排序 
 isChange = 0; //本趟排序开始前,交换标志应为假 
 for( j = LEN-1; j >= i; j-- ) //对当前无序区 ARRAY[i..LEN]自下向上扫描 
{ 
if( ARRAY[j+1] < ARRAY[j] ) 
{ //交换记录 
 ARRAY[0] = ARRAY[j+1]; //ARRAY[0]不是哨兵,仅做暂存单元 
 ARRAY[j+1] = ARRAY[j]; 
 ARRAY[j] = ARRAY[0]; 
 isChange = 1; //发生了交换,故将交换标志置为真 
 } 
} 
printf( "\n" ); 
for( a = 0; a < LEN; a++) //打印本次排序后数组内容 
 { 
 printf( "%d ", ARRAY[a] ); 
} 
if( !isChange ) 
{ 
 break; 
} //本趟排序未发生交换,提前终止算法 
printf( "\n" ); return; 
} 

面试题26:InnoDB与MyISAM的区别

|-----------|----------------|---------------------|
| 特性 | InnoDB | MyISAM |
| 是否支持事务 | ✅ | ❌ |
| 是否支持外键 | ✅ | ❌ |
| 是否支持 MVCC | ✅ | ❌ |
| 全文索引 | ✅(MySQL 5.7+) | ✅ |
| 锁级别 | 行级锁、表级锁 | 表级锁 |
| 主键要求 | 必须有主键 | 可无主键 |
| 崩溃恢复能力 | 强,支持事务恢复 | 较弱 |
| 存储结构 | 索引组织表,共享/独立表空间 | .frm/.MYD/.MYI 文件存储 |

选择合适的存储引擎应根据具体业务需求进行权衡:

  • 如果强调事务安全、并发写入、数据一致性,推荐使用 InnoDB
  • 如果以读为主、对事务要求不高、追求轻量快速,可考虑使用 MyISAM

面试题27:编程实现堆排序

堆排序编程实现:

复制代码
void createHeep(int ARRAY[], int sPoint, int Len) //生成大根堆
{
 while ((2 * sPoint + 1) < Len)
 {
 int mPoint = 2 * sPoint + 1;
 if ((2 * sPoint + 2) < Len)
 {
 if (ARRAY[2 * sPoint + 1] < ARRAY[2 * sPoint + 2])
 {
 mPoint = 2 * sPoint + 2;
 }说明:堆排序,虽然实现复杂,但是非常的实用。另外读者可是自己设计实现小堆排序的算法。虽然和
大堆排序的实现过程相似,但是却可以加深对堆排序的记忆和理解。
28.编程实现基数排序 
 }
 if (ARRAY[sPoint] < ARRAY[mPoint]) //堆被破坏,需要重新调整
 {
 int tmpData = ARRAY[sPoint]; //交换 sPoint 与 mPoint 的数据
 ARRAY[sPoint] = ARRAY[mPoint];
 ARRAY[mPoint] = tmpData;
 sPoint = mPoint;
 }
 else
 {
 break; //堆未破坏,不再需要调整
 }
 }
 return;
}
void heepSort(int ARRAY[], int Len) //堆排序
{
 int i = 0;
 for (i = (Len / 2 - 1); i >= 0; i--) //将 Hr[0,Lenght-1]建成大根堆
 {
 createHeep(ARRAY, i, Len);
 }
 for (i = Len - 1; i > 0; i--)
 {
 int tmpData = ARRAY[0]; //与最后一个记录交换
 ARRAY[0] = ARRAY[i];
 ARRAY[i] = tmpData;
 createHeep(ARRAY, 0, i); //将 H.r[0..i]重新调整为大根堆
 }
 return;
}
int main(void)
{
 int ARRAY[] = { 5, 4, 7, 3, 9, 1, 6, 8, 2 };
 printf("Before sorted:\n"); //打印排序前数组内容
 for (int i = 0; i < 9; i++)
 {
 printf("%d ", ARRAY[i]);
 }
 printf("\n");
 heepSort(ARRAY, 9); //堆排序
 printf("After sorted:\n"); //打印排序后数组内容
 for (i = 0; i < 9; i++)
 {
 printf("%d ", ARRAY[i]);
 }
 printf("\n");
}

面试题28:数据库索引的原理及为什么选择B+树?

在探讨数据库索引时,我们首先需要理解其核心目标:提高数据检索速度。为了达到这个目的,数据库系统使用了不同的数据结构来实现索引。这里我们将讨论为何B+树成为大多数关系型数据库(如MySQL)中索引的首选结构,而不是二叉树或平衡二叉树等其他选项。

1. 查询速度与效率稳定性

  • 二叉树:理论上,二叉查找树可以在O(log n)时间内完成查找、插入和删除操作。然而,在最坏情况下(例如数据已经排序的情况下),二叉树可能会退化成链表,导致时间复杂度变为O(n),这极大地影响了查询性能。
  • 平衡二叉树(如AVL树或红黑树):这些树通过各种机制保持树的高度尽可能小,从而保证操作的时间复杂度为O(log n)。但是,它们每个节点只存储一个键值,这意味着对于大规模数据集,树的高度仍然可能较大,导致磁盘I/O次数增加。
  • B树:B树允许每个节点包含多个键值和子节点指针,这样可以减少树的高度,进而减少磁盘访问次数。不过,B树中的所有节点都存储了实际的数据记录,这限制了单个节点能容纳的键值数量,间接增加了树的高度。
  • B+树:相比B树,B+树将所有数据记录仅存放在叶子节点上,非叶子节点仅用于索引。这种方式不仅减少了内部节点的大小,允许每个节点存放更多的键值,进一步降低了树的高度,而且所有叶子节点按顺序链接在一起,支持范围查询优化。

2. 存储空间与查找磁盘次数

  • 存储空间:由于B+树的非叶子节点不存储实际数据,因此同样的存储空间内,B+树可以拥有更高的分支因子,即每个节点可以有更多的孩子节点。这意味着对于相同数量的数据,B+树通常比其他类型的树更"矮胖",从而减少了查找过程中所需的磁盘访问次数。
  • 查找磁盘次数:数据库中的索引通常是基于磁盘存储的,而磁盘I/O是相对较慢的操作。B+树的设计旨在最小化磁盘I/O,因为它通过最大化每个节点的信息量来减少从根节点到叶子节点路径上的节点数,即减少查找所需读取的磁盘块数。

面试题29:谈谈你对编程规范的理解或认识

编程规范不仅仅是代码格式的统一,更是提升代码质量、增强团队协作效率、保障项目长期维护的重要基础。我认为编程规范的核心目标可以归纳为四个方面:程序的可行性、可读性、可移植性可测试性

上述四点是编程规范的总体目标,而不是简单的条文背诵。在实际开发中,通常会结合自身编程习惯和团队要求,从以下几个方面落实编程规范:

  • 命名清晰:变量、函数、类名要有意义,避免缩写模糊;
  • 函数设计简洁:一个函数只做一件事,控制长度不超过一屏;
  • 适当注释:关键逻辑加注释,但不过度依赖注释;
  • 统一代码风格:使用 IDE 格式化配置、遵循团队编码规范;
  • 模块化设计:高内聚、低耦合,便于扩展与测试。

面试题30:简述聚集索引与非聚集索引的区别

主要区别如下:

|---------|-----------------------------------------------|------------------------------------------------|
| 区别项 | 聚集索引 | 非聚集索引 |
| 数量限制 | 一个表只能有一个聚集索引 | 一个表可以有多个非聚集索引 |
| 数据存储顺序 | 索引键值的逻辑顺序决定了数据行的物理存储顺序(即数据按索引排序存储) | 索引的逻辑顺序与数据行的物理存储顺序无关 |
| 叶节点内容 | 叶节点就是实际的数据页(即索引结构和数据融合在一起) | 叶节点不包含实际数据,仅保存索引列值和指向实际数据行的指针(如行标识 RID 或聚集索引键) |
| 查询效率 | 查询效率高,特别是范围查询(如 WHERE id BETWEEN 100 AND 200) | 查询效率相对较低,通常需要回表查找数据 |
| 插入/更新代价 | 插入或更新时可能导致数据页重新排序或分裂,性能影响较大 | 插入或更新对索引维护的开销较小 |

聚集索引决定了数据在磁盘上的物理存储顺序。因此,一旦建立了聚集索引,表中的数据就按照该索引进行组织和排序。也正因为如此,每个表只能拥有一个聚集索引。

非聚集索引是一种独立于数据存储结构的索引形式。它只包含索引字段的值和一个指向对应数据行的指针(RID 或主键),因此可以在一张表上建立多个非聚集索引以满足不同查询需求。

如果将数据库索引类比为书籍目录:

  • 聚集索引就像书本内容是按照目录顺序排版的;
  • 非聚集索引则像附录的关键词索引,只是告诉你某个词在哪一页,并不改变正文的排列顺序。

面试题31:&&和&、||和|有什么区别

(1)&和|对操作数进行求值运算,&&和||只是判断逻辑关系。

(2)&&和||在在判断左侧操作数就能 确定结果的情况下就不再对右侧操作数求值。

注意:在编程的时候有些时候将&&或||替换成&或|没有出错,但是其逻辑是错误的,可能会导致不可 预想的后果(比如当两个操作数一个是 1 另一个是 2 时。

面试题32:C++ 的引用和 C 语言的指针有什么区别?

(1)初始化要求不同:

引用在声明时必须进行初始化,并且不会单独分配存储空间,它只是某个已有变量的别名;而指针在声明时可以不初始化,在后续赋值时才会指向某个内存地址,并且会占用自身的存储空间。

(2)可变性不同:

引用一旦绑定到一个变量后,就不能再改变,始终代表该变量;而指针可以在程序运行过程中指向不同的对象,具有更高的灵活性。

(3)空值表示不同:

引用不能为"空",即不存在"空引用",它必须绑定到一个有效的对象;而指针可以为空(NULL 或 nullptr),表示不指向任何对象,这在很多场景下非常有用,比如判断是否有效等。

注意:引用作为函数参数时需谨慎使用

引用常被用作函数参数,其目的是为了实现对实参的直接修改。然而,这也带来了一个潜在的问题:调用函数时从代码表面看不出该参数是否为引用,容易让人误以为传入的是普通值,从而忽略了函数可能会修改原始变量的风险。

面试题33:请简述什么是脏读、不可重复读和幻读

在数据库并发操作中,由于多个事务交替执行,可能会导致数据一致性问题。其中,脏读、不可重复读 和 幻读 是三种常见的并发异常现象,它们分别描述了不同场景下的数据不一致问题,具体如下:

(1)脏读(Dirty Read): 当一个事务读取到了另一个事务尚未提交的数据时,就可能发生脏读。例如,事务 A 修改了一条记录但还未提交,事务 B 读取了这条修改后的数据,如果事务 A 回滚,则事务 B 读到的就是无效的"脏"数据。

示例:事务 A 更新余额为 500,事务 B 读取后显示为 500,但事务 A 最终回滚,实际余额仍为 1000。

(2)不可重复读(Non-repeatable Read): 在一个事务内多次执行相同的查询,但由于其他事务对同一条数据进行了修改并提交,导致两次查询结果不一致。也就是说,同一行数据在同一个事务中被多次读取,却返回了不同的值。

示例:事务 A 第一次查询某一行得到值为 100,之后事务 B 修改该行并提交,事务 A 再次查询该行得到值为 200。

(3)幻读(Phantom Read): 一个事务在执行范围查询时,另一个事务插入或删除了符合条件的新数据并提交,导致前一个事务再次执行相同范围查询时,结果集发生了变化(多出或少了几行),这种现象称为幻读。

示例:事务 A 查询 WHERE id < 100 得到 5 条记录,事务 B 插入一条 id = 99 的记录并提交,事务 A 再次查询发现变成了 6 条记录。

总结对比表:

|-------|---------------------|----------|----------------|
| 现象 | 描述 | 涉及操作 | 典型场景 |
| 脏读 | 读取到其他事务未提交的数据 | 读已修改数据 | 数据不一致,可能出现错误数据 |
| 不可重复读 | 同一事务内多次读取同一行,结果不同 | 读已更新数据 | 数据统计或状态判断出错 |
| 幻读 | 同一事务内多次范围查询,结果集数量变化 | 读新增/删除数据 | 分页查询、范围条件统计受影响 |

面试题34:在高并发情况下,如何做到安全的修改同一行数据?

通常采用两种主流机制:悲观锁乐观锁

1、使用悲观锁(Pessimistic Lock)

悲观锁的核心思想是:假设并发冲突经常发生,因此在访问数据时就加锁,防止其他线程修改。

实现方式:

  • 在查询数据时加上排他锁(Exclusive Lock),例如在 SQL 中使用 SELECT ... FOR UPDATE。

适用场景:

  • 写操作频繁;
  • 并发冲突概率较高;
  • 对数据一致性要求极高。

2、使用乐观锁(Optimistic Lock)

乐观锁的核心思想是:假设并发冲突较少发生,先允许线程读取并修改数据,在提交更新时检查是否有其他线程已经修改过该数据,若有则拒绝更新或重试。

实现方式:

版本号机制(Version Number)

  • 在表中增加一个 version 字段;
  • 每次更新数据时检查版本号,并将其加一;

适用场景:

  • 读多写少;
  • 并发冲突较少;
  • 希望减少锁竞争以提升性能。

面试题35:简述 typedef 和 #define 的区别

typedef 和 #define 都可以在 C/C++ 中用于定义别名或常量,但它们的本质不同,分别属于不同的处理阶段,具有不同的特性和使用方式。主要区别如下:

(1)用途不同:

  • typedef 是用于为已有数据类型创建一个新的别名(alias),主要用于增强代码的可读性和可维护性;
  • #define 是预处理指令,通常用于宏定义,包括常量定义、函数宏等,本质上是文本替换。

(2)处理阶段不同:

  • typedef 是编译阶段的一部分,它参与语法和类型检查,因此更安全;
  • #define 是预编译阶段的宏替换机制,在编译之前进行简单的字符串替换,不进行类型检查,容易引入错误。

(3)作用域控制不同:

  • typedef 受作用域限制,可以在函数内部、命名空间中定义局部别名;
  • #define 定义的宏没有作用域概念,一旦定义,从定义处开始到文件结束都有效(除非被 #undef 取消)。

(4)对指针的影响不同: 这是两者一个非常容易出错的区别点:

  • 使用 typedef 定义指针类型时,能正确地将整个标识符作为指针类型;
  • 而使用 #define 宏定义指针类型时,容易因优先级问题导致理解偏差或错误用法。

面试题36:简述关键字const的作用和意义

在 C/C++ 中,const 是一个非常重要的关键字,用于声明常量性(只读性)。它可以修饰变量、指针、函数参数以及成员函数等,表示该标识符所代表的内容在定义后不能被修改。

主要优点:便于类型检查、同宏定义一样可以方便地进行参数 的修改和调整、节省空间,避免不必要的内存分配、可为函数重载提供参考。

面试题37:简述关键字static的作用

|----------------|---------------------------|
| 使用场景 | 作用描述 |
| 全局变量前加 static | 限制变量作用域,只在本文件内可见 |
| 局部变量前加 static | 延长生命周期,保持值直到程序结束,且默认初始化为0 |
| 函数前加 static | 限制函数作用域,只在本文件内可见 |
| 类中定义 static 成员 | 所有对象共享该成员 |
| 类中定义 static 方法 | 只能访问静态成员,无 this 指针 |

面试题38:简述关键字extern的作用

在 C/C++ 中,extern 是一个存储类修饰符,主要用于声明变量或函数是在当前文件之外定义的,即其定义位于其他源文件或库中。它的核心作用是告诉编译器:"这个变量或函数已经在别处定义了,请不要报错,链接时去其他模块中查找"。

|----------|------------------|
| 特性 | 说明 |
| 关键字 | extern |
| 主要用途 | 声明变量或函数在其它模块中定义 |
| 是否分配内存 | 否,仅声明 |
| 是否允许多次出现 | 是,可在多个文件中声明 |
| 是否必须有定义 | 是,否则链接时报错 |
| 典型应用场景 | 跨文件共享全局变量、调用外部函数 |

面试题39:简述为什么流操作符<<和>>重载时返回引用

在 C++ 中,流操作符 <<(输出)和 >>(输入)经常被连续使用,例如链式调用多个输出或输入操作。为了支持这种链式调用,流操作符的重载函数通常返回一个对流对象的引用。这不仅提高了代码的可读性和简洁性,还确保了流操作的灵活性和效率。

为什么返回引用?

支持链式调用:

  • 当我们连续使用流操作符时,如 cout << "Hello, " << "World!" << endl;,每个 << 操作实际上是一个函数调用。
  • 如果返回引用,则每次调用后返回的是同一个流对象的引用,这样下一个 << 操作可以直接作用于这个流对象,从而实现链式调用。

保持流对象的状态:

  • 返回引用而不是值,意味着不会创建新的流对象副本,保证了流对象的状态一致性。
  • 这对于维护流对象内部的状态(如错误状态、缓冲区等)非常重要。

提高性能:

  • 返回引用避免了不必要的对象复制,提高了程序运行效率。

|-------------------|-------|----------|---------|
| 操作符类型 | 返回值类型 | 是否支持链式调用 | 适用场景 |
| 流操作符 << 和 >> | 引用 | 是 | 输入输出流操作 |
| 赋值操作符 = | 引用 | 是 | 对象赋值 |
| 算术操作符 +, -, *, / | 对象 | 否 | 数值运算 |

注意:

  • 在重载流操作符时,务必返回流对象的引用,否则将无法支持链式调用,导致代码可读性和功能性下降。
  • 对于其他操作符,如算术运算符,由于涉及右值和临时对象的创建,应返回新构造的对象而非引用。

面试题40:简述指针常量与常量指针的区别

指针常量 指的是一个指针本身是常量,也就是说,该指针一旦初始化指向某个地址后,就不能再改变其指向。

常量指针 则指的是一个指向常量的指针,也就是说,不能通过该指针去修改其所指向的对象的值,但指针本身的指向可以更改。

|--------|------------------------|---------------------------------|
| 特性 | 指针常量(Constant Pointer) | 常量指针(Pointer to Constant) |
| 定义形式 | T* const ptr; | const T* ptr; 或 T const* ptr; |
| 是否能改指向 | ❌ 不可更改 | ✅ 可更改 |
| 是否能改内容 | ✅ 可更改 | ❌ 不可更改 |
| 强调重点 | 指针本身不能变 | 指向的内容不能变 |
| 示例 | int* const ptr = &a; | const int* ptr = &a; |

面试题41:MySQL事务的四大特性

MySQL 中的事务(Transaction)是指作为单个逻辑工作单元执行的一系列操作,这些操作要么全部成功,要么全部失败回滚。为了保证数据的一致性和可靠性,事务必须满足四个基本特性,简称 ACID 特性,分别是:

1. 原子性(Atomicity)

  • 事务是一个不可分割的工作单位,事务中的所有操作要么全部完成,要么全部不完成;
  • 如果事务中任何一个步骤失败,整个事务都会被回滚到最初状态,就像这个事务从来没有执行过一样;
  • 实现机制:通常通过 undo log(回滚日志) 来实现。

✅ 示例:银行转账操作中,A 转账给 B,若在转账过程中出现异常(如断电),系统会自动回滚,确保 A 没扣款,B 也不会多钱。

2. 一致性(Consistency)

  • 事务的执行不能破坏数据库的完整性约束和业务逻辑规则;
  • 在事务开始之前和结束之后,数据库都处于一致状态;
  • 一致性是事务的最终目标,由其他三个特性共同保障。

✅ 示例:如果一个表规定字段 age 必须大于 0,则事务执行后不能让该字段变成负数。

3. 隔离性(Isolation)

  • 多个事务并发执行时,彼此之间应该互不干扰,每个事务都感觉不到其他事务的存在;
  • 不同的隔离级别可以控制并发访问的程度,避免脏读、不可重复读、幻读等问题;
  • 实现机制:通过 锁机制MVCC(多版本并发控制) 实现。

✅ 示例:两个用户同时修改同一行数据,数据库应确保它们的操作不会互相干扰。

4. 持久性(Durability)

  • 事务一旦提交,其对数据库的修改就是永久性的,即使系统发生故障也不会丢失;
  • 实现机制:主要依赖于 redo log(重做日志)双写缓冲区 等机制。

✅ 示例:支付完成后,即使服务器突然宕机,用户的余额也已经持久化保存。

面试题42:如何避免"野指针"的产生?

"野指针"是指指向无效内存区域的指针。它并未指向一个合法的对象或内存地址,因此对它的操作可能导致程序崩溃或不可预知的行为。为了避免"野指针"的出现,可以从以下几个方面进行预防。

(1)指针变量未初始化的问题

指针变量在声明时如果没有被显式赋值,其内容是随机的,可能指向任意内存地址,从而形成野指针。

**解决办法:**在定义指针变量时应立即进行初始化,可以将其指向一个有效的变量地址,或者直接赋值为 NULL(C 语言)或 nullptr(C++11 及以上)。

(2)释放内存后未将指针置空的问题

当使用 free()(C 语言)或 delete / delete[](C++)释放指针所指向的内存后,如果未将指针设为 NULL 或 nullptr,该指针就变成了野指针。

**解决办法:**每次释放完内存后,立即将对应的指针设置为 NULL 或 nullptr,以防止后续误用。

(3)访问超出作用域的内存问题

如果指针指向的是局部变量或临时对象,在变量超出作用域之后,其所占用的内存会被系统回收,此时指针仍然保存着原来的地址,就会变成野指针。

**解决办法:**不要返回局部变量的地址;在变量的作用域结束前及时释放相关资源,并将指针置为 NULL 或 nullptr。

面试题43:简述常引用的作用

在 C++ 中,常引用(const reference) 是指通过 const 关键字修饰的引用,表示该引用所绑定的对象不能被修改。它的主要作用是在不改变原始对象的前提下,提供对对象的安全访问。

常引用的核心目的是:

  • 防止在使用引用的过程中意外修改原始变量的值;
  • 提高程序的可读性和安全性;
  • 避免不必要的拷贝操作,提升性能。

面试题44:如果一张表有近千万条数据,导致 CRUD 操作变慢,你会如何进行优化?

|--------|-------------------|------------|
| 优化方向 | 技术手段 | 适用场景 |
| 结构性优化 | 分库分表(水平/垂直)、引入中间件 | 数据量大、并发高 |
| 查询性能优化 | 索引优化、SQL 优化、覆盖索引 | 查询缓慢、索引缺失 |
| 存储与运维 | 读写分离、定期维护、缓存 | 读多写少、数据分散 |
| 架构扩展 | 异步处理、引入搜索引擎 | 实时性不高、复杂查询 |

面对千万级数据量的表,应结合具体业务场景,采用多种优化手段协同工作,既要关注查询性能,也要兼顾系统可扩展性和维护成本。

面试题45:简述strcpy、sprintf与memcpy的区别

|-----------|-----------|------------|-------------|
| 特性 | strcpy | sprintf | memcpy |
| 操作对象 | 字符串 → 字符串 | 多种类型 → 字符串 | 内存块 → 内存块 |
| 是否支持格式哦u化 | ❌ 不支持 | ✅ 支持 | ❌ 不支持 |
| 效率 | 中等 | 较低 | 最高 |
| 安全性 | 易溢出 | 易溢出 | 高(仍需手动控制长度) |
| 典型应用场景 | 字符串复制 | 数据格式化输出 | 结构体/二进制数据复制 |

面试题46:简述数据库中使用自增主键可能带来的问题

|-----------|------|--------------|-----------------------|
| 问题类型 | 是否常见 | 原因 | 推荐解决方式 |
| 分库分表主键冲突 | ✅ 高频 | 多个分片各自维护自增序列 | 改用全局唯一主键方案 |
| 锁竞争性能瓶颈 | ✅ 中频 | 自增机制加锁保护 | 使用非自增主键或调整自增步长 |
| 主键用尽溢出 | ❌ 低频 | 数据类型限制 | 使用 BIGINT 或提前扩容 |
| 安全性/可预测问题 | ✅ 中频 | 主键递增可被猜测 | 使用 UUID 或雪花算法生成不可预测主键 |

建议:

  • 单体数据库场景下,自增主键仍是简单高效的首选;
  • 但在分布式架构、高并发写入、大数据量等复杂场景中,应优先考虑使用全局唯一主键方案,以提升系统的可扩展性和稳定性。

面试题47:MVCC熟悉吗,它的底层原理?

MVCC是一种用于数据库管理系统中提高并发性能的技术。它通过保存数据对象的多个版本来支持非锁定读取,从而减少事务间的冲突,允许更高的并发度。

MVCC 的实现依赖于以下几个核心概念和技术:

1、隐藏列:

  • 每个数据行除了用户定义的列外,还会包含一些隐藏列

2、事务ID:

  • 数据库系统为每个新事务分配一个唯一的递增事务ID(Transaction ID),用于标识事务的时间顺序。

3、快照读与当前读:

  • 快照读(Snapshot Read): 读取事务开始时的数据快照,不会看到未提交的更改或在此之后发生的更改。
  • 当前读(Current Read): 直接读取最新的数据版本,能看到所有已提交的更改。

4、垃圾回收(GC):

  • 当某些旧版本的数据不再被任何活跃事务需要时,这些过期的数据版本会被清理掉,以释放存储空间。

面试题48:简述数据库连接池的概念及其必要性

数据库连接池是一种资源管理技术,它通过预先创建并维护一定数量的数据库连接对象,并对外提供获取和释放这些连接的方法,从而减少频繁建立和断开数据库连接所带来的开销。

连接池的工作机制:

  • 在初始化时,连接池会预先创建一组数据库连接,并将它们保存在一个池子里。
  • 当应用程序需要访问数据库时,不是直接创建新的连接,而是从连接池中借用一个现有的连接。
  • 使用完毕后,连接会被归还到池中,而不是真正关闭,以便后续请求可以继续使用。

数据库连接的建立过程

  1. TCP三次握手: 应用程序通过 TCP 协议与数据库服务器建立网络连接。
  2. 身份验证: 发送用户名和密码给数据库服务器进行身份验证。
  3. 执行SQL语句: 验证成功后,应用程序可以向数据库发送 SQL 查询或命令。
  4. TCP四次挥手: 操作完成后,关闭连接,释放网络资源。

连接池的好处总结

|--------|---------------------|
| 好处 | 描述 |
| 资源重用 | 减少了频繁创建和销毁连接带来的系统开销 |
| 更快响应速度 | 快速获取连接,减少等待时间 |
| 控制并发量 | 限制最大连接数,保护数据库免受过载 |
| 统一管理 | 防止连接泄漏,简化连接管理 |

面试题49:简述构造函数是否能为虚函数?

在 C++ 中,构造函数不能是虚函数 ,而析构函数不仅可以是虚函数,而且在某些复杂类层次结构中,通常需要将析构函数声明为虚函数

构造函数为何不能为虚函数?

1、虚函数表(vtable)机制:

  • 虚函数依赖于每个对象内部维护的一个虚函数表(vtable),通过该表实现动态绑定。
  • 构造函数的作用是初始化对象的状态,但在对象完全构造之前,vtable 尚未建立,因此无法支持虚函数调用。

2、构造顺序问题:

  • 在构造过程中,基类部分先于派生类部分被构造。如果允许构造函数为虚函数,则会导致试图调用一个尚未完全构造的对象的方法,这显然是不合理的。
  • 实际上,在构造函数体内调用虚函数时,只会调用到基类中的版本,因为此时派生类部分还未构造完成。

|--------|---------|----------------------|
| 函数类型 | 是否可为虚函数 | 原因 |
| 构造函数 | ❌ 不可 | 对象未完全构造前,vtable 尚未建立 |
| 析构函数 | ✅ 可以 | 确保多态删除时,派生类部分也能正确销毁 |
| 纯虚析构函数 | ✅ 可以 | 必须有定义体,以便在派生类销毁时隐式调用 |

面试题50:谈谈你对面向对象的理解

面向对象是一种程序设计思想,也是一种系统分析与设计方法。它将现实世界中的事物抽象为程序中的"对象",通过对象之间的交互来完成系统的功能。

1、面向对象的核心思想

面向对象的核心在于从"对象"的角度出发思考问题,而不是单纯地围绕功能或流程展开设计。它强调的是:

  • 万物皆对象: 将现实世界中的实体映射为程序中的对象,每个对象都具有属性(数据)和行为(方法)。
  • 模块化设计: 将复杂问题分解为多个相对独立的对象,分别进行设计和实现,提高代码的可维护性和扩展性。
  • 高内聚、低耦合: 每个对象内部封装自己的状态和行为,对外提供清晰的接口,减少对象间的依赖关系。

2、面向对象的三大基本特性

封装(Encapsulation):

  • 将对象的属性和行为包装在一起,并控制对外暴露的程度;
  • 提供访问控制机制(如 private、protected、public),增强数据的安全性。

继承(Inheritance):

  • 子类可以继承父类的属性和方法,实现代码复用;
  • 支持类层次结构的设计,体现"is-a"关系。

多态(Polymorphism):

  • 同一个接口可以有不同的实现方式;
  • 主要通过虚函数(C++)、接口(Java)或抽象类实现,提升系统的灵活性和扩展性。

3、面向对象与传统面向过程的区别

|---------|------------------|-----------------------|
| 对比维度 | 面向过程(Procedural) | 面向对象(Object-Oriented) |
| 设计视角 | 基于功能和流程 | 基于对象和交互 |
| 数据与行为关系 | 分离 | 绑定在一起 |
| 扩展性 | 修改原有代码多,扩展困难 | 易于通过继承和多态扩展 |
| 可维护性 | 结构松散,维护成本高 | 模块清晰,易于维护 |

4、面向对象不仅仅是指编程

虽然我们最常接触的是"面向对象编程(OOP)",但完整的面向对象技术还包括:

  • 面向对象分析(OOA): 分析问题领域,识别出关键的对象及其关系。
  • 面向对象设计(OOD): 在分析基础上设计系统的结构,包括类图、交互图等。
  • 面向对象建模(OOM): 使用 UML(统一建模语言)等工具对系统进行可视化建模。

由于篇幅限制,本期C/C++高频面试题就写到这儿了

(需要这份C/C++高频面试题1000道pdf文档的同学,文章底部关注后自取)

下期会再更新50道题,先剧透部分题目:

51.const、static作用。

52.c++面向对象三大特征及对他们的理解,引出多态实现原理、动态绑定、菱形继承。

53.虚析构的必要性,引出内存泄漏,虚函数和普通成员函数的储存位置,虚函数表、虚函数表指针

54.malloc、free和new、delete区别,引出malloc申请大内存、malloc申请空间失败怎么办

55.stl熟悉吗,vector、map、list、hashMap,vector底层,map引出红黑树。优先队列用过吗,使用的场景。

无锁队列听说过吗,原理是什么(比较并交换)

56.实现擅长的排序,说出原理(快排、堆排)

57.四种cast,智能指针

58.tcp和udp区别

59.进程和线程区别 60.指针和引用作用以及区别

61.c++11用过哪些特性,auto作为返回值和模板一起怎么用,函数指针能和auto混用吗

62.boost用过哪些类,thread、asio、signal、bind、function

63.单例、工厂模式、代理、适配器、模板,使用场景

64.QT信号槽实现机制,QT内存管理,MFC消息机制

65.进程间通信。会选一个详细问

66.多线程,锁和信号量,互斥和同步

67.动态库和静态库的区别

(需要这份C/C++高频面试题1000道pdf文档的同学,文章底部关注后自取)

相关推荐
mxpan1 小时前
C++ 单例模式一种实现方式
c++·设计模式
whoarethenext2 小时前
使用 C++/OpenCV 计算图像特征并用 Faiss 进行相似细节搜索
c++·opencv·faiss
only-lucky2 小时前
C++设计模式
java·c++·设计模式
范纹杉想快点毕业2 小时前
Qt构造函数详解:布局与快捷键实战
c语言·开发语言·数据库·c++·qt·命令模式
FreeBuf_3 小时前
ComfyUI遭“Pickai“C++后门攻击,全球700余台AI图像生成服务器沦陷
服务器·c++·人工智能
Hat_man_6 小时前
Windows下memcpy_s如何在Linux下使用
linux·c++
老一岁7 小时前
c++set和pair的使用
开发语言·c++
@我漫长的孤独流浪7 小时前
数据结构----排序(3)
数据结构·c++·算法
oioihoii8 小时前
C++11 GC Interface:从入门到精通
java·jvm·c++