原作者:Linux教程,原文地址:C/C++ 高频八股文面试题1000题(一)
在准备技术岗位的求职过程中,C/C++始终是绕不开的核心考察点。无论是互联网大厂的笔试面试,还是嵌入式、后台开发、系统编程等方向的岗位,C/C++ 都扮演着举足轻重的角色。
本系列文章将围绕 “C/C++ 笔试面试中出现频率最高的 1000 道题目” 进行深入剖析与讲解。由于篇幅限制,整个系列将分为 多 篇陆续发布,每篇50道题,本文为 第一篇。
通过系统梳理这些高频考点,帮助你在面对各大厂笔试、技术面试时真正做到 心中有数、下笔有神、对答如流!
本系列将持续更新,欢迎关注+收藏,一起攻克这 1000 道 C/C++ 高频题,拿下心仪 Offer!
✅ 简要回答:
关键区别:
类型 |
是否分配内存 |
是否可重复 |
定义 |
是 |
否 |
声明 |
否 |
是 |
extern int a; // 声明,不分配内存空间
int a; // 定义,分配内存空间
int a = 10; // 定义并初始化
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; }
注意: 有些操作符(如 sizeof)看起来像函数,而有些函数名又类似操作符,这类名称容易引起混淆,使用时需要特别注意区分。尤其是在处理数组名等特殊类型时,这种差异可能导致意料之外的错误。
在 C 语言中,static 主要用于修饰:
而在 C++ 中,除了具备 C 语言中所有的功能之外,static 还被扩展用于:
注意: 在编程中,static 所具有的“记忆性”和“全局性”特点,使得不同调用之间可以共享数据、传递信息。在 C++ 中,类的静态成员则能够在不同的对象实例之间进行通信和数据共享。
注意: 使用 malloc 分配的内存必须通过 free 来释放,而使用 new 分配的内存则必须使用 delete 来释放,两者不可混用。因为它们底层实现机制不同,混用可能导致未定义行为或内存泄漏。
#define MIN(a,b) ((a)<=(b)?(a):(b))
注意:在调用时一定要注意这个宏定义的副作用。
可以,指针可以被声明为 volatile 类型。因为从本质上讲,指针本质上也是一种变量,它保存的是一个内存地址(即整型数值),这一点与普通变量并没有本质区别。
在某些特殊场景下,比如硬件寄存器访问、中断服务程序中共享的数据等,指针的值可能会被程序之外的因素修改,此时就需要使用 volatile 来告诉编译器不要对该指针进行优化,确保每次访问都直接从内存中读取最新值。
例如,在中断服务程序中修改一个指向缓冲区(buffer)的指针时,就必须将该指针声明为 volatile,以防止编译器因优化而忽略其变化。
说明: 虽然指针具有特殊的用途——用于访问内存地址,但从变量访问的角度来看,它和其他变量一样具备可变性,因此也可以像普通变量一样使用 volatile 进行修饰。
#include
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 |
int* |
指向数组首元素 a[0] 的指针 |
&a |
int (*)[5] |
指向整个数组 a 的指针 |
虽然它们的地址值相同,但类型不同,因此在进行指针运算时步长也不同:
输出结果:2, 5
从内存分配机制角度,C/C++ 的内存管理还可以分为以下三种方式:
分配方式 |
特点描述 |
静态分配 |
在程序编译阶段完成,如全局变量、静态变量。在整个程序运行过程中有效,速度最快,不易出错。 |
栈上分配 |
函数调用时在栈中为局部变量分配空间,函数返回后自动释放。速度快但容量有限。 |
堆上分配 |
使用 malloc / new 动态申请内存,程序员负责手动释放。灵活性强但容易出错,如内存泄漏、碎片等问题。 |
1、操作对象不同
函数 |
源对象类型 |
目标对象类型 |
说明 |
strcpy |
字符串(以 \0 结尾) |
字符串 |
用于字符串之间的拷贝 |
sprintf |
可为任意基本数据类型 |
字符串 |
格式化输出到字符串 |
memcpy |
任意可操作的内存地址 |
任意可操作的内存地址 |
用于任意内存块之间的拷贝 |
2、功能用途不同
3、执行效率不同
函数 |
效率评价 |
memcpy |
最高。直接进行内存拷贝,没有格式转换或终止判断 |
strcpy |
中等。需要逐字节判断是否到达 \0 |
sprintf |
最低。涉及格式解析和字符转换,开销较大 |
volatile int *ptr = (volatile int *)0x67a9;
*ptr = 0xaa66;
面向对象编程(OOP)的三大核心特征是:
这三大特性是构建面向对象系统的基础,它们共同支持代码的模块化、可重用性和灵活性。
特征 |
含义 |
核心作用 |
封装 |
隐藏实现细节,提供统一接口 |
提高安全性、简化使用 |
继承 |
类之间共享属性和方法 |
代码复用、建立类的层级关系 |
多态 |
父类引用指向子类对象,动态绑定 |
实现灵活调用、提升程序扩展性 |
缺省构造函数:用于创建对象实例时初始化对象。
缺省拷贝构造函数:当对象通过另一个同类对象进行初始化时调用。
缺省析构函数:在对象生命周期结束时自动调用,用于清理资源。
缺省赋值运算符:将一个对象的内容赋值给另一个同类的对象。
缺省取址运算符:返回对象的内存地址。
缺省取址运算符 const:对于常量对象,返回其内存地址。
拷贝构造函数与赋值运算符重载在功能上都用于对象之间的复制操作,但它们在使用场景和实现逻辑上有以下两个主要区别:
(1)拷贝构造函数用于生成一个新的类对象,而赋值运算符则用于已有对象的重新赋值。
(2)由于拷贝构造函数是用于构造新对象的过程,因此在初始化之前无需判断源对象是否与新建对象相同;而赋值运算符必须进行这种判断(即自赋值检查),以避免不必要的错误。此外,在赋值操作中,如果目标对象已经分配了内存资源,则需要先释放原有资源,再进行新的内存分配和数据复制。
注意:当类中包含指针类型的成员变量时,必须显式重写拷贝构造函数和赋值运算符,避免使用编译器默认生成的版本。否则可能会导致浅拷贝问题,引发内存泄漏或重复释放等问题。
template
class A
{
friend T; // 只有 T 类可以访问 A 的私有成员
private:
A() {} // 私有构造函数
~A() {} // 私有析构函数
};
class B : virtual public A
{
public:
B() {} // 构造函数
~B() {} // 析构函数
};
class C : virtual public B
{
public:
C() {}
~C() {}
};
int main(void)
{
B b; // 合法:B 可以实例化
// C c; // 编译失败:因为 C 继承自 B,而 B 是 A 的子类,构造受限
return 0;
}
#include
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
注意:本题主要考察了面试者对虚函数的理解程度。一个对虚函数不了解的人很难正确的做出本题。 在学习面向对象的多态性时一定要深刻理解虚函数表的工作原理。
(1)重写 和 重载 的主要区别如下:
(2)隐藏 与 重写、重载 的区别如下:
说明: 虽然重载和重写都是实现多态性的基础,但它们的技术实现方式完全不同,所达成的目的也有本质区别。其中,重写支持的是运行时多态(动态绑定),而重载实现的是编译时多态(静态绑定)。
面试题18:说下多态实现的原理
当编译器检测到一个类中包含虚函数时,会自动为该类生成一个虚函数表(vtable)。虚函数表中的每一项都是一个指向相应虚函数的指针。
同时,编译器会在该类的实例中隐式插入一个虚函数表指针(vptr),用于指向该类对应的虚函数表。对于大多数编译器(如 VC++),这个 vptr 通常被放置在对象内存布局的最开始位置。
在调用构造函数创建对象时,编译器会在构造过程中自动执行一段隐藏代码,将 vptr 指向当前类的 vtable,从而建立起对象与虚函数表之间的关联。
此外,在构造函数内部,this 指针此时已经指向具体的派生类对象,因此可以通过 vptr 找到正确的 vtable,进而访问到实际应调用的虚函数体。这种机制使得程序可以在运行时根据对象的实际类型来动态绑定函数调用,这就是所谓的动态联编(Dynamic Binding),也是 C++ 实现多态的核心原理。
(1)存储形式不同: 数组是一块连续的内存空间,声明时必须指定其固定长度;而链表由一系列不连续的节点组成,每个节点包含数据部分和指向下一个节点的指针,其长度可以根据需要动态增长或缩减。
(2)数据查找效率不同: 数组支持随机访问,可以通过索引直接定位元素,查找效率高;而链表只能从头节点开始逐个遍历,查找效率较低。
(3)插入与删除操作不同: 在链表中进行插入或删除操作只需修改相邻节点的指针,效率较高;而在数组中插入或删除元素通常需要移动大量数据以保持连续性,操作效率较低。
(4)越界问题: 数组存在越界风险,使用不当可能导致程序崩溃或未定义行为;链表不存在越界问题,但需注意空指针异常。
选择数组还是链表应根据具体应用场景来决定:
数组占用空间紧凑,但长度固定;链表灵活可变,但额外占用空间用于保存指针信息。合理选择数据结构有助于提升程序的效率与稳定性。
队列(Queue)和栈(Stack)都属于基础的线性数据结构,但它们在数据的插入与删除操作方式上存在明显差异。
虽然“栈”这个词在数据结构中表示一种存储模型,但它也常用于描述程序运行时的内存区域,容易引起混淆。
直接插入排序编程实现如下:
#include
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<
注意:所有为简化边界条件而引入的附加结点(元素)均可称为哨兵。引入哨兵后使得查找循环条件的 时间大约减少了一半,对于记录数较大的文件节约的时间就相当可观。类似于排序这样使用频率非常高 的算法,要尽可能地减少其运行时间。所以不能把上述算法中的哨兵视为雕虫小技。
可以从这几个维度回答这个问题:
冒泡排序编程实现如下:
#include
#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;
}
特性 |
InnoDB |
MyISAM |
是否支持事务 |
✅ |
❌ |
是否支持外键 |
✅ |
❌ |
是否支持 MVCC |
✅ |
❌ |
全文索引 |
✅(MySQL 5.7+) |
✅ |
锁级别 |
行级锁、表级锁 |
表级锁 |
主键要求 |
必须有主键 |
可无主键 |
崩溃恢复能力 |
强,支持事务恢复 |
较弱 |
存储结构 |
索引组织表,共享/独立表空间 |
.frm/.MYD/.MYI 文件存储 |
选择合适的存储引擎应根据具体业务需求进行权衡:
堆排序编程实现:
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");
}
在探讨数据库索引时,我们首先需要理解其核心目标:提高数据检索速度。为了达到这个目的,数据库系统使用了不同的数据结构来实现索引。这里我们将讨论为何B+树成为大多数关系型数据库(如MySQL)中索引的首选结构,而不是二叉树或平衡二叉树等其他选项。
1. 查询速度与效率稳定性
2. 存储空间与查找磁盘次数
编程规范不仅仅是代码格式的统一,更是提升代码质量、增强团队协作效率、保障项目长期维护的重要基础。我认为编程规范的核心目标可以归纳为四个方面:程序的可行性、可读性、可移植性 和 可测试性。
上述四点是编程规范的总体目标,而不是简单的条文背诵。在实际开发中,通常会结合自身编程习惯和团队要求,从以下几个方面落实编程规范:
主要区别如下:
区别项 |
聚集索引 |
非聚集索引 |
数量限制 |
一个表只能有一个聚集索引 |
一个表可以有多个非聚集索引 |
数据存储顺序 |
索引键值的逻辑顺序决定了数据行的物理存储顺序(即数据按索引排序存储) |
索引的逻辑顺序与数据行的物理存储顺序无关 |
叶节点内容 |
叶节点就是实际的数据页(即索引结构和数据融合在一起) |
叶节点不包含实际数据,仅保存索引列值和指向实际数据行的指针(如行标识 RID 或聚集索引键) |
查询效率 |
查询效率高,特别是范围查询(如 WHERE id BETWEEN 100 AND 200) |
查询效率相对较低,通常需要回表查找数据 |
插入/更新代价 |
插入或更新时可能导致数据页重新排序或分裂,性能影响较大 |
插入或更新对索引维护的开销较小 |
聚集索引决定了数据在磁盘上的物理存储顺序。因此,一旦建立了聚集索引,表中的数据就按照该索引进行组织和排序。也正因为如此,每个表只能拥有一个聚集索引。
非聚集索引是一种独立于数据存储结构的索引形式。它只包含索引字段的值和一个指向对应数据行的指针(RID 或主键),因此可以在一张表上建立多个非聚集索引以满足不同查询需求。
如果将数据库索引类比为书籍目录:
(1)&和|对操作数进行求值运算,&&和||只是判断逻辑关系。
(2)&&和||在在判断左侧操作数就能 确定结果的情况下就不再对右侧操作数求值。
注意:在编程的时候有些时候将&&或||替换成&或|没有出错,但是其逻辑是错误的,可能会导致不可 预想的后果(比如当两个操作数一个是 1 另一个是 2 时。
(1)初始化要求不同:
引用在声明时必须进行初始化,并且不会单独分配存储空间,它只是某个已有变量的别名;而指针在声明时可以不初始化,在后续赋值时才会指向某个内存地址,并且会占用自身的存储空间。
(2)可变性不同:
引用一旦绑定到一个变量后,就不能再改变,始终代表该变量;而指针可以在程序运行过程中指向不同的对象,具有更高的灵活性。
(3)空值表示不同:
引用不能为“空”,即不存在“空引用”,它必须绑定到一个有效的对象;而指针可以为空(NULL 或 nullptr),表示不指向任何对象,这在很多场景下非常有用,比如判断是否有效等。
注意:引用作为函数参数时需谨慎使用
引用常被用作函数参数,其目的是为了实现对实参的直接修改。然而,这也带来了一个潜在的问题:调用函数时从代码表面看不出该参数是否为引用,容易让人误以为传入的是普通值,从而忽略了函数可能会修改原始变量的风险。
在数据库并发操作中,由于多个事务交替执行,可能会导致数据一致性问题。其中,脏读、不可重复读 和 幻读 是三种常见的并发异常现象,它们分别描述了不同场景下的数据不一致问题,具体如下:
(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 条记录。
现象 |
描述 |
涉及操作 |
典型场景 |
脏读 |
读取到其他事务未提交的数据 |
读已修改数据 |
数据不一致,可能出现错误数据 |
不可重复读 |
同一事务内多次读取同一行,结果不同 |
读已更新数据 |
数据统计或状态判断出错 |
幻读 |
同一事务内多次范围查询,结果集数量变化 |
读新增/删除数据 |
分页查询、范围条件统计受影响 |
通常采用两种主流机制:悲观锁 和 乐观锁。
1、使用悲观锁(Pessimistic Lock)
悲观锁的核心思想是:假设并发冲突经常发生,因此在访问数据时就加锁,防止其他线程修改。
实现方式:
适用场景:
2、使用乐观锁(Optimistic Lock)
乐观锁的核心思想是:假设并发冲突较少发生,先允许线程读取并修改数据,在提交更新时检查是否有其他线程已经修改过该数据,若有则拒绝更新或重试。
实现方式:
版本号机制(Version Number)
适用场景:
typedef 和 #define 都可以在 C/C++ 中用于定义别名或常量,但它们的本质不同,分别属于不同的处理阶段,具有不同的特性和使用方式。主要区别如下:
(1)用途不同:
(2)处理阶段不同:
(3)作用域控制不同:
(4)对指针的影响不同: 这是两者一个非常容易出错的区别点:
在 C/C++ 中,const 是一个非常重要的关键字,用于声明常量性(只读性)。它可以修饰变量、指针、函数参数以及成员函数等,表示该标识符所代表的内容在定义后不能被修改。
主要优点:便于类型检查、同宏定义一样可以方便地进行参数 的修改和调整、节省空间,避免不必要的内存分配、可为函数重载提供参考。
使用场景 |
作用描述 |
全局变量前加 static |
限制变量作用域,只在本文件内可见 |
局部变量前加 static |
延长生命周期,保持值直到程序结束,且默认初始化为0 |
函数前加 static |
限制函数作用域,只在本文件内可见 |
类中定义 static 成员 |
所有对象共享该成员 |
类中定义 static 方法 |
只能访问静态成员,无 this 指针 |
在 C/C++ 中,extern 是一个存储类修饰符,主要用于声明变量或函数是在当前文件之外定义的,即其定义位于其他源文件或库中。它的核心作用是告诉编译器:“这个变量或函数已经在别处定义了,请不要报错,链接时去其他模块中查找”。
特性 |
说明 |
关键字 |
extern |
主要用途 |
声明变量或函数在其它模块中定义 |
是否分配内存 |
否,仅声明 |
是否允许多次出现 |
是,可在多个文件中声明 |
是否必须有定义 |
是,否则链接时报错 |
典型应用场景 |
跨文件共享全局变量、调用外部函数 |
在 C++ 中,流操作符 <<(输出)和 >>(输入)经常被连续使用,例如链式调用多个输出或输入操作。为了支持这种链式调用,流操作符的重载函数通常返回一个对流对象的引用。这不仅提高了代码的可读性和简洁性,还确保了流操作的灵活性和效率。
为什么返回引用?
支持链式调用:
保持流对象的状态:
提高性能:
操作符类型 |
返回值类型 |
是否支持链式调用 |
适用场景 |
流操作符 << 和 >> |
引用 |
是 |
输入输出流操作 |
赋值操作符 = |
引用 |
是 |
对象赋值 |
算术操作符 +, -, *, / |
对象 |
否 |
数值运算 |
注意:
指针常量指的是一个指针本身是常量,也就是说,该指针一旦初始化指向某个地址后,就不能再改变其指向。
常量指针则指的是一个指向常量的指针,也就是说,不能通过该指针去修改其所指向的对象的值,但指针本身的指向可以更改。
特性 |
指针常量(Constant Pointer) |
常量指针(Pointer to Constant) |
定义形式 |
T* const ptr; |
const T* ptr; 或 T const* ptr; |
是否能改指向 |
❌ 不可更改 |
✅ 可更改 |
是否能改内容 |
✅ 可更改 |
❌ 不可更改 |
强调重点 |
指针本身不能变 |
指向的内容不能变 |
示例 |
int* const ptr = &a; |
const int* ptr = &a; |
MySQL 中的事务(Transaction)是指作为单个逻辑工作单元执行的一系列操作,这些操作要么全部成功,要么全部失败回滚。为了保证数据的一致性和可靠性,事务必须满足四个基本特性,简称 ACID 特性,分别是:
1. 原子性(Atomicity)
✅ 示例:银行转账操作中,A 转账给 B,若在转账过程中出现异常(如断电),系统会自动回滚,确保 A 没扣款,B 也不会多钱。
2. 一致性(Consistency)
✅ 示例:如果一个表规定字段 age 必须大于 0,则事务执行后不能让该字段变成负数。
3. 隔离性(Isolation)
✅ 示例:两个用户同时修改同一行数据,数据库应确保它们的操作不会互相干扰。
4. 持久性(Durability)
✅ 示例:支付完成后,即使服务器突然宕机,用户的余额也已经持久化保存。
“野指针”是指指向无效内存区域的指针。它并未指向一个合法的对象或内存地址,因此对它的操作可能导致程序崩溃或不可预知的行为。为了避免“野指针”的出现,可以从以下几个方面进行预防。
(1)指针变量未初始化的问题
指针变量在声明时如果没有被显式赋值,其内容是随机的,可能指向任意内存地址,从而形成野指针。
解决办法:在定义指针变量时应立即进行初始化,可以将其指向一个有效的变量地址,或者直接赋值为 NULL(C 语言)或 nullptr(C++11 及以上)。
(2)释放内存后未将指针置空的问题
当使用 free()(C 语言)或 delete / delete[](C++)释放指针所指向的内存后,如果未将指针设为 NULL 或 nullptr,该指针就变成了野指针。
解决办法:每次释放完内存后,立即将对应的指针设置为 NULL 或 nullptr,以防止后续误用。
(3)访问超出作用域的内存问题
如果指针指向的是局部变量或临时对象,在变量超出作用域之后,其所占用的内存会被系统回收,此时指针仍然保存着原来的地址,就会变成野指针。
解决办法:不要返回局部变量的地址;在变量的作用域结束前及时释放相关资源,并将指针置为 NULL 或 nullptr。
在 C++ 中,常引用(const reference) 是指通过 const 关键字修饰的引用,表示该引用所绑定的对象不能被修改。它的主要作用是在不改变原始对象的前提下,提供对对象的安全访问。
常引用的核心目的是:
优化方向 |
技术手段 |
适用场景 |
结构性优化 |
分库分表(水平/垂直)、引入中间件 |
数据量大、并发高 |
查询性能优化 |
索引优化、SQL 优化、覆盖索引 |
查询缓慢、索引缺失 |
存储与运维 |
读写分离、定期维护、缓存 |
读多写少、数据分散 |
架构扩展 |
异步处理、引入搜索引擎 |
实时性不高、复杂查询 |
面对千万级数据量的表,应结合具体业务场景,采用多种优化手段协同工作,既要关注查询性能,也要兼顾系统可扩展性和维护成本。
特性 |
strcpy |
sprintf |
memcpy |
操作对象 |
字符串 → 字符串 |
多种类型 → 字符串 |
内存块 → 内存块 |
是否支持格式哦u化 |
❌ 不支持 |
✅ 支持 |
❌ 不支持 |
效率 |
中等 |
较低 |
最高 |
安全性 |
易溢出 |
易溢出 |
高(仍需手动控制长度) |
典型应用场景 |
字符串复制 |
数据格式化输出 |
结构体/二进制数据复制 |
问题类型 |
是否常见 |
原因 |
推荐解决方式 |
分库分表主键冲突 |
✅ 高频 |
多个分片各自维护自增序列 |
改用全局唯一主键方案 |
锁竞争性能瓶颈 |
✅ 中频 |
自增机制加锁保护 |
使用非自增主键或调整自增步长 |
主键用尽溢出 |
❌ 低频 |
数据类型限制 |
使用 BIGINT 或提前扩容 |
安全性/可预测问题 |
✅ 中频 |
主键递增可被猜测 |
使用 UUID 或雪花算法生成不可预测主键 |
MVCC是一种用于数据库管理系统中提高并发性能的技术。它通过保存数据对象的多个版本来支持非锁定读取,从而减少事务间的冲突,允许更高的并发度。
MVCC 的实现依赖于以下几个核心概念和技术:
1、隐藏列:
2、事务ID:
3、快照读与当前读:
4、垃圾回收(GC):
数据库连接池是一种资源管理技术,它通过预先创建并维护一定数量的数据库连接对象,并对外提供获取和释放这些连接的方法,从而减少频繁建立和断开数据库连接所带来的开销。
连接池的工作机制:
数据库连接的建立过程
连接池的好处总结
好处 |
描述 |
资源重用 |
减少了频繁创建和销毁连接带来的系统开销 |
更快响应速度 |
快速获取连接,减少等待时间 |
控制并发量 |
限制最大连接数,保护数据库免受过载 |
统一管理 |
防止连接泄漏,简化连接管理 |
在 C++ 中,构造函数不能是虚函数,而析构函数不仅可以是虚函数,而且在某些复杂类层次结构中,通常需要将析构函数声明为虚函数。
构造函数为何不能为虚函数?
1、虚函数表(vtable)机制:
2、构造顺序问题:
函数类型 |
是否可为虚函数 |
原因 |
构造函数 |
❌ 不可 |
对象未完全构造前,vtable 尚未建立 |
析构函数 |
✅ 可以 |
确保多态删除时,派生类部分也能正确销毁 |
纯虚析构函数 |
✅ 可以 |
必须有定义体,以便在派生类销毁时隐式调用 |
面向对象是一种程序设计思想,也是一种系统分析与设计方法。它将现实世界中的事物抽象为程序中的“对象”,通过对象之间的交互来完成系统的功能。
1、面向对象的核心思想
面向对象的核心在于从“对象”的角度出发思考问题,而不是单纯地围绕功能或流程展开设计。它强调的是:
2、面向对象的三大基本特性
封装(Encapsulation):
继承(Inheritance):
多态(Polymorphism):
3、面向对象与传统面向过程的区别
对比维度 |
面向过程(Procedural) |
面向对象(Object-Oriented) |
设计视角 |
基于功能和流程 |
基于对象和交互 |
数据与行为关系 |
分离 |
绑定在一起 |
扩展性 |
修改原有代码多,扩展困难 |
易于通过继承和多态扩展 |
可维护性 |
结构松散,维护成本高 |
模块清晰,易于维护 |
4、面向对象不仅仅是指编程
虽然我们最常接触的是“面向对象编程(OOP)”,但完整的面向对象技术还包括:
由于篇幅限制,本期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文档的同学,文章底部关注后自取)